Add launcher shell harness for membership and governance flows

This commit is contained in:
Joshua 2026-02-17 20:48:19 -08:00
parent b716b52f3f
commit d0fcc2c119
4 changed files with 749 additions and 0 deletions

View File

@ -18,3 +18,22 @@ Free control-plane application for EDUT onboarding and entitlement-aware install
## Boundary
Launcher never contains private kernel internals. It verifies and installs signed paid runtimes only after entitlement checks pass.
## Local Harness (Current)
`app/index.html` is a local launcher shell harness for end-to-end API validation:
1. Wallet intent + verify
2. Membership quote + confirm
3. Member channel register/poll/ack/support
4. Governance install token/confirm/status
5. Lease heartbeat + offline renew
Run locally:
```bash
cd /Users/vsg/Documents/VSG\ Codex/launcher/app
python3 -m http.server 4310
```
Then open `http://127.0.0.1:4310` and point API base URL at running `secretapi`.

346
app/app.js Normal file
View File

@ -0,0 +1,346 @@
/* global fetch */
const $ = (id) => document.getElementById(id);
const state = {
eventMap: new Map(),
};
function nowISO() {
return new Date().toISOString();
}
function baseURL() {
return $("apiBase").value.trim().replace(/\/+$/, "");
}
function wallet() {
return $("walletAddress").value.trim();
}
function chainID() {
return Number($("chainId").value || 0);
}
function logLine(label, payload) {
const log = $("log");
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
log.textContent = line + log.textContent;
}
async function request(method, path, body) {
const opts = {
method,
headers: { "Content-Type": "application/json" },
};
if (body !== undefined) {
opts.body = JSON.stringify(body);
}
const res = await fetch(`${baseURL()}${path}`, opts);
const text = await res.text();
let json = {};
if (text.trim() !== "") {
try {
json = JSON.parse(text);
} catch {
json = { raw: text };
}
}
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
}
return json;
}
function requireWallet() {
const value = wallet();
if (!value) {
throw new Error("wallet is required");
}
return value;
}
function deviceID() {
return $("deviceId").value.trim();
}
function orgRootID() {
return $("orgRootId").value.trim();
}
function principalID() {
return $("principalId").value.trim();
}
function principalRole() {
return $("principalRole").value.trim();
}
function renderEvents(events) {
const list = $("eventList");
list.innerHTML = "";
for (const evt of events) {
state.eventMap.set(evt.event_id, evt);
const card = document.createElement("article");
card.className = "event";
const title = document.createElement("h3");
title.textContent = `${evt.class} · ${evt.event_id}`;
card.appendChild(title);
const body = document.createElement("p");
body.textContent = evt.body || evt.title || "";
card.appendChild(body);
const meta = document.createElement("div");
meta.className = "meta";
meta.textContent = `${evt.created_at} · scope=${evt.visibility_scope} · ack=${evt.requires_ack}`;
card.appendChild(meta);
if (evt.requires_ack) {
const actions = document.createElement("div");
actions.className = "actions";
const button = document.createElement("button");
button.textContent = "Ack";
button.addEventListener("click", async () => {
try {
const out = await request("POST", `/member/channel/events/${evt.event_id}/ack`, {
wallet: requireWallet(),
device_id: deviceID(),
acknowledged_at: nowISO(),
});
logLine(`event ack ${evt.event_id}`, out);
} catch (err) {
logLine(`event ack ${evt.event_id} error`, { error: String(err) });
}
});
actions.appendChild(button);
card.appendChild(actions);
}
list.appendChild(card);
}
}
async function onIntent() {
const payload = {
address: requireWallet(),
origin: $("walletOrigin").value.trim() || "https://edut.ai",
locale: $("walletLocale").value.trim() || "en",
chain_id: chainID(),
};
const out = await request("POST", "/secret/wallet/intent", payload);
$("intentId").value = out.intent_id || "";
$("designationCode").value = out.designation_code || "";
$("displayToken").value = out.display_token || "";
logLine("wallet intent", out);
}
async function onVerify() {
const out = await request("POST", "/secret/wallet/verify", {
intent_id: $("intentId").value.trim(),
address: requireWallet(),
chain_id: chainID(),
signature: $("walletSignature").value.trim(),
});
if (out.designation_code) {
$("designationCode").value = out.designation_code;
}
logLine("wallet verify", out);
}
async function onStatus() {
const out = await request(
"GET",
`/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`,
);
if (out.designation_code) {
$("designationCode").value = out.designation_code;
}
logLine("membership status", out);
}
async function onQuote() {
const payload = {
designation_code: $("designationCode").value.trim(),
address: requireWallet(),
chain_id: chainID(),
};
const payerWallet = $("payerWallet").value.trim();
const payerProof = $("payerProof").value.trim();
const sponsorOrgRoot = $("sponsorOrgRoot").value.trim();
if (payerWallet) {
payload.payer_wallet = payerWallet;
}
if (payerProof) {
payload.payer_proof = payerProof;
}
if (sponsorOrgRoot) {
payload.sponsor_org_root_id = sponsorOrgRoot;
}
const out = await request("POST", "/secret/membership/quote", payload);
$("quoteId").value = out.quote_id || "";
$("quoteValue").value = out.value || "";
$("quotePayer").value = out.payer_wallet || "";
logLine("membership quote", out);
}
async function onConfirmMembership() {
const out = await request("POST", "/secret/membership/confirm", {
designation_code: $("designationCode").value.trim(),
quote_id: $("quoteId").value.trim(),
tx_hash: $("confirmTxHash").value.trim(),
address: requireWallet(),
chain_id: chainID(),
});
logLine("membership confirm", out);
}
async function onRegisterChannel() {
const out = await request("POST", "/member/channel/device/register", {
wallet: requireWallet(),
chain_id: chainID(),
device_id: deviceID(),
platform: $("platform").value.trim(),
org_root_id: orgRootID(),
principal_id: principalID(),
principal_role: principalRole(),
app_version: $("appVersion").value.trim(),
push_provider: "none",
});
logLine("channel register", out);
}
async function onUnregisterChannel() {
const out = await request("POST", "/member/channel/device/unregister", {
wallet: requireWallet(),
device_id: deviceID(),
});
logLine("channel unregister", out);
}
async function onPollEvents() {
const cursor = $("eventCursor").value.trim();
const limit = $("eventLimit").value.trim() || "25";
const query = new URLSearchParams({
wallet: requireWallet(),
device_id: deviceID(),
limit,
});
if (cursor) {
query.set("cursor", cursor);
}
const out = await request("GET", `/member/channel/events?${query.toString()}`);
if (out.next_cursor) {
$("eventCursor").value = out.next_cursor;
}
renderEvents(out.events || []);
logLine("channel poll", out);
}
async function onSupportTicket() {
const out = await request("POST", "/member/channel/support/ticket", {
wallet: requireWallet(),
org_root_id: orgRootID(),
principal_id: principalID(),
category: "admin_support",
summary: $("supportSummary").value.trim(),
context: {
source: "launcher-shell",
requested_at: nowISO(),
},
});
logLine("support ticket", out);
}
async function onInstallToken() {
const out = await request("POST", "/governance/install/token", {
wallet: requireWallet(),
org_root_id: orgRootID(),
principal_id: principalID(),
principal_role: principalRole(),
device_id: deviceID(),
launcher_version: $("appVersion").value.trim(),
platform: $("platform").value.trim(),
});
$("installToken").value = out.install_token || "";
$("entitlementId").value = out.entitlement_id || "";
$("runtimeVersion").value = out.package?.runtime_version || "";
$("packageHash").value = out.package?.package_hash || "";
logLine("install token", out);
}
async function onInstallConfirm() {
const out = await request("POST", "/governance/install/confirm", {
install_token: $("installToken").value.trim(),
wallet: requireWallet(),
device_id: deviceID(),
entitlement_id: $("entitlementId").value.trim(),
package_hash: $("packageHash").value.trim(),
runtime_version: $("runtimeVersion").value.trim(),
installed_at: nowISO(),
launcher_receipt_hash: `receipt-${Date.now()}`,
});
logLine("install confirm", out);
}
async function onInstallStatus() {
const query = new URLSearchParams({
wallet: requireWallet(),
device_id: deviceID(),
});
const out = await request("GET", `/governance/install/status?${query.toString()}`);
logLine("install status", out);
}
async function onLeaseHeartbeat() {
const out = await request("POST", "/governance/lease/heartbeat", {
wallet: requireWallet(),
org_root_id: orgRootID(),
principal_id: principalID(),
device_id: deviceID(),
});
logLine("lease heartbeat", out);
}
async function onOfflineRenew() {
const out = await request("POST", "/governance/lease/offline-renew", {
wallet: requireWallet(),
org_root_id: orgRootID(),
principal_id: principalID(),
renewal_bundle: {
bundle_id: `renew-${Date.now()}`,
source: "launcher-shell",
},
});
logLine("offline renew", out);
}
function bind(id, handler) {
$(id).addEventListener("click", async () => {
try {
await handler();
} catch (err) {
logLine(`${id} error`, { error: String(err) });
}
});
}
bind("btnIntent", onIntent);
bind("btnVerify", onVerify);
bind("btnStatus", onStatus);
bind("btnQuote", onQuote);
bind("btnConfirmMembership", onConfirmMembership);
bind("btnRegisterChannel", onRegisterChannel);
bind("btnUnregisterChannel", onUnregisterChannel);
bind("btnPollEvents", onPollEvents);
bind("btnSupportTicket", onSupportTicket);
bind("btnInstallToken", onInstallToken);
bind("btnInstallConfirm", onInstallConfirm);
bind("btnInstallStatus", onInstallStatus);
bind("btnLeaseHeartbeat", onLeaseHeartbeat);
bind("btnOfflineRenew", onOfflineRenew);
logLine("launcher shell ready", {
api_base: baseURL(),
chain_id: chainID(),
});

217
app/index.html Normal file
View File

@ -0,0 +1,217 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EDUT Launcher Shell</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main class="shell">
<header class="hero">
<h1>EDUT Launcher</h1>
<p>Wallet-first onboarding shell (local integration harness)</p>
</header>
<section class="panel">
<h2>Connection</h2>
<div class="grid two">
<label>
API base URL
<input id="apiBase" value="http://127.0.0.1:8080" />
</label>
<label>
Chain ID
<input id="chainId" type="number" value="84532" />
</label>
</div>
</section>
<section class="panel">
<h2>Wallet Intent</h2>
<div class="grid three">
<label>
Wallet
<input id="walletAddress" placeholder="0x..." />
</label>
<label>
Origin
<input id="walletOrigin" value="https://edut.ai" />
</label>
<label>
Locale
<input id="walletLocale" value="en" />
</label>
</div>
<div class="actions">
<button id="btnIntent">Create intent</button>
<button id="btnStatus">Membership status</button>
</div>
<div class="grid three">
<label>
Intent ID
<input id="intentId" />
</label>
<label>
Designation code
<input id="designationCode" />
</label>
<label>
Display token
<input id="displayToken" />
</label>
</div>
<label>
Signature (EIP-712)
<textarea id="walletSignature" rows="2" placeholder="0x..."></textarea>
</label>
<div class="actions">
<button id="btnVerify">Verify signature</button>
</div>
</section>
<section class="panel">
<h2>Membership Quote + Confirm</h2>
<div class="grid three">
<label>
Payer wallet (optional)
<input id="payerWallet" placeholder="0x..." />
</label>
<label>
Payer proof (optional)
<input id="payerProof" placeholder="0x..." />
</label>
<label>
Sponsor org root (optional)
<input id="sponsorOrgRoot" placeholder="org_root_id" />
</label>
</div>
<div class="actions">
<button id="btnQuote">Get quote</button>
</div>
<div class="grid three">
<label>
Quote ID
<input id="quoteId" />
</label>
<label>
Tx value
<input id="quoteValue" />
</label>
<label>
Payer used
<input id="quotePayer" />
</label>
</div>
<label>
Confirm tx hash
<input id="confirmTxHash" placeholder="0x..." />
</label>
<div class="actions">
<button id="btnConfirmMembership">Confirm membership tx</button>
</div>
</section>
<section class="panel">
<h2>Member Channel</h2>
<div class="grid three">
<label>
Device ID
<input id="deviceId" value="desktop-local-01" />
</label>
<label>
Platform
<select id="platform">
<option>desktop</option>
<option>ios</option>
<option>android</option>
</select>
</label>
<label>
App version
<input id="appVersion" value="0.1.0" />
</label>
</div>
<div class="grid three">
<label>
Org root ID
<input id="orgRootId" value="org.local.root" />
</label>
<label>
Principal ID
<input id="principalId" value="human.local" />
</label>
<label>
Principal role
<select id="principalRole">
<option>org_root_owner</option>
<option>workspace_member</option>
</select>
</label>
</div>
<div class="actions">
<button id="btnRegisterChannel">Register channel</button>
<button id="btnUnregisterChannel">Unregister channel</button>
</div>
<div class="grid two">
<label>
Cursor
<input id="eventCursor" />
</label>
<label>
Limit
<input id="eventLimit" type="number" value="25" />
</label>
</div>
<div class="actions">
<button id="btnPollEvents">Poll events</button>
</div>
<div id="eventList" class="event-list"></div>
<label>
Support summary (owner only)
<input id="supportSummary" value="Need diagnostics snapshot." />
</label>
<div class="actions">
<button id="btnSupportTicket">Open support ticket</button>
</div>
</section>
<section class="panel">
<h2>Governance Install + Lease</h2>
<div class="actions">
<button id="btnInstallToken">Issue install token</button>
<button id="btnInstallStatus">Install status</button>
</div>
<div class="grid three">
<label>
Install token
<input id="installToken" />
</label>
<label>
Entitlement ID
<input id="entitlementId" />
</label>
<label>
Runtime version
<input id="runtimeVersion" />
</label>
</div>
<label>
Package hash
<input id="packageHash" />
</label>
<div class="actions">
<button id="btnInstallConfirm">Confirm install</button>
<button id="btnLeaseHeartbeat">Lease heartbeat</button>
<button id="btnOfflineRenew">Offline renew</button>
</div>
</section>
<section class="panel">
<h2>Response Log</h2>
<pre id="log"></pre>
</section>
</main>
<script src="./app.js"></script>
</body>
</html>

167
app/style.css Normal file
View File

@ -0,0 +1,167 @@
:root {
color-scheme: dark;
--bg: #0b1220;
--bg-soft: #121b2e;
--line: #24344f;
--text: #d8e2f2;
--muted: #8fa2c2;
--accent: #39c36b;
--warn: #ffcd57;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "IBM Plex Mono", "SF Mono", "Menlo", monospace;
background: radial-gradient(circle at top, #182642 0%, var(--bg) 55%);
color: var(--text);
}
.shell {
width: min(1180px, 94vw);
margin: 20px auto 42px;
display: grid;
gap: 14px;
}
.hero,
.panel {
background: color-mix(in oklab, var(--bg-soft) 88%, black);
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px;
}
.hero h1 {
margin: 0 0 6px;
letter-spacing: 0.04em;
}
.hero p {
margin: 0;
color: var(--muted);
}
.panel h2 {
margin: 0 0 10px;
font-size: 14px;
letter-spacing: 0.05em;
color: var(--accent);
}
.grid {
display: grid;
gap: 10px;
}
.grid.two {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.grid.three {
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
label {
display: grid;
gap: 5px;
font-size: 12px;
color: var(--muted);
}
input,
select,
textarea,
button {
font: inherit;
}
input,
select,
textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 8px;
background: #0f1828;
color: var(--text);
padding: 8px 10px;
}
textarea {
resize: vertical;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 10px 0;
}
button {
border: 1px solid #2d7f4a;
background: #173325;
color: #b8efcf;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
}
button:hover {
filter: brightness(1.08);
}
button:active {
transform: translateY(1px);
}
.event-list {
display: grid;
gap: 8px;
margin: 10px 0 6px;
}
.event {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
background: #0d1625;
}
.event h3 {
margin: 0 0 4px;
font-size: 12px;
color: var(--warn);
}
.event p {
margin: 0;
font-size: 12px;
line-height: 1.45;
}
.event .meta {
margin-top: 7px;
font-size: 11px;
color: var(--muted);
}
#log {
margin: 0;
max-height: 300px;
overflow: auto;
padding: 12px;
border-radius: 8px;
background: #08111d;
border: 1px solid var(--line);
font-size: 11px;
}
@media (max-width: 720px) {
.shell {
margin-top: 12px;
}
}