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 ## Boundary
Launcher never contains private kernel internals. It verifies and installs signed paid runtimes only after entitlement checks pass. 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;
}
}