Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
409 changes: 409 additions & 0 deletions devlog/_plan/260911_hub_single_port/040_hub_token_ux.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@
"history-ocx-compaction-recovery.test.ts": "codex-integration",
"hyperbolic-provider.test.ts": "providers",
"hub-gated-local-clients.test.ts": "cli",
"hub-invite.test.ts": "cli",
"identity-neutralize.test.ts": "adapters",
"init-backup-cleanup.test.ts": "service",
"init-eof.test.ts": "service",
Expand Down
26 changes: 24 additions & 2 deletions skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,28 @@ JSON mode: `payload`.

- Uses the exact upstream model ID after the first slash. Omitted cache rates default to zero; sibling model prices are preserved.

### `ocx hub invite`

Mint a single-use pairing code on a hub and print the exact `ocx connect` line for one more machine.

Drives no management route.

| Flag | Value | Meaning |
|---|---|---|
| `--json` | boolean | Emit code, expiresAt, dataUrl, managementUrl, and command. |
| `--data-url` | string | Advertise this data origin instead of hub.dataPublicOrigin or the bind address. |
| `--management-url` | string | Confirm the management origin; it must equal hub.managementPublicOrigin. |
| `--clients` | string | Pre-select codex and/or claude in the printed connect command. |

JSON mode: `envelope`.

- Hub only: refuses when runtimeRole is not hub, and requires a running attested proxy.
- The code is secret, single-use and short-lived; it is bound to hub.managementPublicOrigin and to the connecting machine's loopback browser origin.
- The bound browser origin is always printed; when it is not http://localhost:10100 the warning names the port the connecting machine must use.
- Refuses when the advertised data origin would be loopback (a loopback or wildcard bind with no hub.dataPublicOrigin and no --data-url) rather than printing a line that dials the other machine itself.
- Prints no data-plane token. Remote machines receive their own revocable per-client key from the exchange.
- Mints through the attested local pairing-grant route, the same one ocx gui pair uses; no admin token is read.

### `ocx connect rotate`

Rotate the connected client's data key against the hub, with commit and abort.
Expand Down Expand Up @@ -706,6 +728,6 @@ JSON mode: `payload`.

## Counts

- declared capabilities: 38
- of those, state-changing: 17
- declared capabilities: 39
- of those, state-changing: 18
- head-resolved invocations: 2
28 changes: 28 additions & 0 deletions src/cli/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ export const CAPABILITIES: readonly Capability[] = [
json: "envelope",
details: ["Reads /healthz plus local config; drives no management API route."],
},
{
command: ["hub", "invite"],
summary: "Mint a single-use pairing code on a hub and print the exact `ocx connect` line for one more machine.",
// Deliberately empty. The command DOES drive `POST /api/gui/pairing-grants` -- the attested
// local mint route `ocx gui pair` uses, authorized by a capability HMAC'd with the running
// proxy's own attestation secret rather than by the admin token, which is why it needs
// nothing exported in the shell. That route is answered in the composition root, ahead of
// `handleManagementAPI`, so it is not in MANAGEMENT_ROUTES; declaring it here would fail the
// capability/registry reconciliation rather than inform anyone. Widening the registry's scope
// to `src/server/index.ts` is its own change.
routes: [],
flags: [
{ name: "--json", value: "boolean", summary: "Emit code, expiresAt, dataUrl, managementUrl, and command." },
{ name: "--data-url", value: "string", summary: "Advertise this data origin instead of hub.dataPublicOrigin or the bind address." },
{ name: "--management-url", value: "string", summary: "Confirm the management origin; it must equal hub.managementPublicOrigin." },
{ name: "--clients", value: "string", summary: "Pre-select codex and/or claude in the printed connect command." },
],
mutates: true,
json: "envelope",
details: [
"Hub only: refuses when runtimeRole is not hub, and requires a running attested proxy.",
"The code is secret, single-use and short-lived; it is bound to hub.managementPublicOrigin and to the connecting machine's loopback browser origin.",
"The bound browser origin is always printed; when it is not http://localhost:10100 the warning names the port the connecting machine must use.",
"Refuses when the advertised data origin would be loopback (a loopback or wildcard bind with no hub.dataPublicOrigin and no --data-url) rather than printing a line that dials the other machine itself.",
"Prints no data-plane token. Remote machines receive their own revocable per-client key from the exchange.",
"Mints through the attested local pairing-grant route, the same one ocx gui pair uses; no admin token is read.",
],
},
{
command: ["connect", "rotate"],
summary: "Rotate the connected client's data key against the hub, with commit and abort.",
Expand Down
7 changes: 7 additions & 0 deletions src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,13 @@ const commandRunners: Record<string, CommandRunner> = {
},
});
},
hub: async deps => {
const { runHubCommand } = await import("./hub");
return runHubCommand(deps.args.slice(1), {
loadConfig: deps.loadConfig,
findLiveProxy: deps.findLiveProxy,
});
},
service: async deps => {
process.exitCode = 0;
await deps.serviceCommand(...deps.args.slice(1));
Expand Down
13 changes: 1 addition & 12 deletions src/cli/gui-pair-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GUI_PAIR_NONCE_HEADER,
GUI_PAIR_PATH,
canonicalGuiBrowserOrigin,
canonicalHttpOrigin,
createGuiPairCapability,
} from "../lib/gui-pair-capability";
import { directLocalHttpFetch } from "../server/direct-local-http";
Expand Down Expand Up @@ -52,18 +53,6 @@ function sameRuntime(left: RuntimePortState, right: RuntimePortState | null): bo
&& timingSafeEqual(leftSecret, rightSecret);
}

function canonicalHttpOrigin(value: unknown): string | null {
if (typeof value !== "string") return null;
try {
const parsed = new URL(value);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) return null;
return parsed.origin;
} catch {
return null;
}
}

function parseCreatedResult(value: unknown, browserOrigin: string): GuiPairRequestResult | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const record = value as Record<string, unknown>;
Expand Down
3 changes: 3 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Usage:
ocx logout <provider> Remove a stored OAuth login
ocx gui [pair --origin <browser-origin> [--json]]
Open the dashboard or create a single-use remote pairing grant
ocx hub invite [--json] Print a ready-to-run \`ocx connect\` line for one more machine
(hub only; see \`ocx help hub\` for the one-port topology)
ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
ocx restart Stop and restart the proxy
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)
Expand Down Expand Up @@ -99,6 +101,7 @@ Examples:
ocx start Start on default port (10100)
ocx start --port 8080 Start on custom port
ocx help service Show service command help
ocx help hub Explain the hub topology, token file, and invites
ocx sync Sync available models to Codex`);
}

Expand Down
Loading
Loading