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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ Cloud viewer journey is still unproven, and the old draft
evidence for current `main`. The source-candidate contract is:

- Starting a session prints a link and a QR code.
- `aether --json rc start|link|status` gives local integrations one structured
session/device identity. Only `start` and `link` return the short-lived
observer link; `status` never replays it. Integrations must not log that link.
- Your phone or browser **watches** the run. It never gets tool authority.
- One command shows what is exposed; another revokes it.
- Outbound TLS only — no inbound listener on your machine.
Expand Down
58 changes: 44 additions & 14 deletions src/commands/rc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { detectBrowserRuntime } from "../core/browser_runtime.js";
import { McpClient } from "../core/mcp.js";
import { loadEnrollmentMetadata } from "../core/device_runtime/identity.js";
import {
RC_HOST_SCHEMA,
RcError,
attachHost,
flushOutbox,
Expand Down Expand Up @@ -252,25 +253,49 @@ export interface RcCommandDeps {
err: (text: string) => void;
isTTY: boolean;
columns: number | undefined;
json: boolean;
}

interface RcObserverInvitation {
url: string;
expires_at: string;
}

/** Stable machine handoff for a caller that must bind its own browser session. */
export function renderStatusJson(view: RcStatusView, invitation: RcObserverInvitation | null = null): string {
return JSON.stringify({
schema: RC_HOST_SCHEMA,
host_state: view.running ? view.state : "off",
session_id: view.session_id,
device_id: view.device_id,
project_ref: view.project_ref,
revoke_pending: view.revoke_pending,
outbox_pending: view.pending,
acked_seq: view.acked,
viewer_capabilities: VIEWER_CAPABILITIES,
observer: invitation,
}) + "\n";
}

async function printObserverLink(
deps: RcCommandDeps,
hostDeps: RcHostDeps,
sessionId: string,
): Promise<boolean> {
): Promise<RcObserverInvitation | null> {
try {
const grant = await mintObserverGrant(hostDeps, sessionId, newObserverId());
const link = observerLink(grant);
deps.out(`\nObserver link (expires ${grant.expires_at}):\n${link}\n`);
const qr = deps.isTTY ? observerQr(link, deps.columns) : null;
if (qr) deps.out(`${qr}\n`);
else deps.out("Open the link directly; this terminal cannot fit a scannable QR.\n");
return true;
if (!deps.json) {
deps.out(`\nObserver link (expires ${grant.expires_at}):\n${link}\n`);
const qr = deps.isTTY ? observerQr(link, deps.columns) : null;
if (qr) deps.out(`${qr}\n`);
else deps.out("Open the link directly; this terminal cannot fit a scannable QR.\n");
}
return { url: link, expires_at: grant.expires_at };
} catch (error) {
const code = error instanceof RcError ? error.code : "RC_BROKER_UNREACHABLE";
deps.err(`${code}: RC is running, but an observer link could not be minted. Retry with \`aether rc link\`.\n`);
return false;
return null;
}
}

Expand All @@ -282,7 +307,7 @@ function viewOf(record: OutboxRecord, deps: RcCommandDeps, observers: number | n
browser: browser?.code ?? null,
connector: deps.connector(),
last_receipt: null,
device_id: enrolled?.device_id ?? null,
device_id: record.session_id ? record.device_id : enrolled?.device_id ?? null,
device_name: enrolled?.display_name ?? null,
session_id: record.session_id || null,
project_ref: record.project_ref || null,
Expand Down Expand Up @@ -358,9 +383,10 @@ async function start(
saveOutbox(hostDeps.outboxPath, record);
const flushed = await flushOutbox(hostDeps, record);

deps.out(renderStatus(viewOf(record, deps, null)));
if (flushed.ok) await printObserverLink(deps, hostDeps, session.session_id);
else deps.err(`${flushed.code}: RC is running, but its opening events are pending. Retry with \`aether rc link\`.\n`);
if (!deps.json) deps.out(renderStatus(viewOf(record, deps, null)));
const invitation = flushed.ok ? await printObserverLink(deps, hostDeps, session.session_id) : null;
if (!flushed.ok) deps.err(`${flushed.code}: RC is running, but its opening events are pending. Retry with \`aether rc link\`.\n`);
if (deps.json) deps.out(renderStatusJson(viewOf(record, deps, null), invitation));
return EXIT_OK;
} catch (error) {
if (error instanceof RcError) {
Expand Down Expand Up @@ -396,6 +422,7 @@ export async function cmdRc(
err: overrides.err ?? ((text): void => void process.stderr.write(text)),
isTTY: overrides.isTTY ?? Boolean(process.stdout.isTTY),
columns: overrides.columns ?? process.stdout.columns,
json: overrides.json ?? ctx.flags.json,
};

// Connector state is read once, best-effort, before anything renders. A
Expand All @@ -421,7 +448,7 @@ export async function cmdRc(
return start(deps, hostDeps, record, flags.str("name"), projectRef);

case "status":
deps.out(renderStatus(viewOf(record, deps, null)));
deps.out(deps.json ? renderStatusJson(viewOf(record, deps, null)) : renderStatus(viewOf(record, deps, null)));
return EXIT_OK;

case "link":
Expand All @@ -435,7 +462,9 @@ export async function cmdRc(
deps.err(`${flushed.code}: RC opening events are still pending. Retry \`aether rc link\` after reconnecting.\n`);
return EXIT_OPERATIONAL;
}
return await printObserverLink(deps, hostDeps, record.session_id) ? EXIT_OK : EXIT_OPERATIONAL;
const invitation = await printObserverLink(deps, hostDeps, record.session_id);
if (deps.json && invitation) deps.out(renderStatusJson(viewOf(record, deps, null), invitation));
return invitation ? EXIT_OK : EXIT_OPERATIONAL;
}

case "exposure":
Expand All @@ -460,7 +489,8 @@ export async function cmdRc(
deps.err(`${outcome.code}: ${outcome.detail}\n`);
return EXIT_OPERATIONAL;
}
deps.out("RC is off. The session, its grants and its streams are revoked.\n");
deps.out(deps.json ? renderStatusJson(viewOf(record, deps, null)) :
"RC is off. The session, its grants and its streams are revoked.\n");
return EXIT_OK;
}

Expand Down
102 changes: 102 additions & 0 deletions test/rc_command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
RC_NO_CONTROL_LINE,
cmdRc,
renderExposure,
renderStatus,
renderStatusJson,
type RcStatusView,
} from "../src/commands/rc.js";
import { COMMAND_MANIFEST_SOURCE } from "../src/commands/command_manifest_data.js";
import { assertViewerManifest, tokenize } from "../src/core/rc/viewer_profile.js";
import { producerCoverage } from "../src/core/rc/producers.js";
import { payloadDigest } from "../src/core/rc/receipts.js";
import type { AppContext } from "../src/core/context.js";
import type { CommandFlags } from "../src/core/command_dispatch.js";

const TOKEN_SHAPED = "aek_" + "Z".repeat(32);

Expand Down Expand Up @@ -215,6 +223,100 @@ test("status renders with nothing running and invents no session", () => {
assert.match(text, /session\s+—/);
});

test("machine status binds session and device without replaying an invitation", () => {
const data = JSON.parse(renderStatusJson(view())) as Record<string, unknown>;
assert.equal(data["schema"], "aether.cli.rc/1");
assert.equal(data["session_id"], "rs_" + "e".repeat(32));
assert.equal(data["device_id"], "dev-1");
assert.equal(data["host_state"], "active");
assert.deepEqual(data["viewer_capabilities"], ["observe"]);
assert.equal(data["observer"], null);
assert.ok(!renderStatusJson(view()).includes("rsgt_"));
});

test("machine handoff carries a one-time link only in the requested start or link result", () => {
const url = "https://app.aethersystems.net/rc#grant=rsgt_canary";
const data = JSON.parse(renderStatusJson(view(), {
url,
expires_at: "2026-09-07T00:05:00.000Z",
})) as { observer: { url: string; expires_at: string } };
assert.equal(data.observer.url, url);
assert.equal(data.observer.expires_at, "2026-09-07T00:05:00.000Z");
});

test("json start and status bind one RC host without replaying the one-time link", async () => {
const directory = mkdtempSync(join(tmpdir(), "aether-rc-json-"));
const priorConfig = process.env["AETHER_CONFIG_DIR"];
process.env["AETHER_CONFIG_DIR"] = directory;
const output: string[] = [];
const sessionId = "rs_" + "1".repeat(32);
const grantToken = "rsgt_" + "a".repeat(48);
const api = {
async postJson(path: string, body: unknown): Promise<unknown> {
if (path === "/remote/sessions") return { session_id: sessionId, state: "pending_host" };
if (path.endsWith("/host/attach")) return { session_id: sessionId, state: "live" };
if (path.endsWith("/host/events")) {
const events = (body as { events: Array<{ host_event_id: string; payload: Record<string, unknown> }> }).events;
return { session_id: sessionId, receipts: events.map((event, index) => ({
host_event_id: event.host_event_id, seq: index + 1, payload_digest: payloadDigest(event.payload),
})) };
}
if (path.endsWith("/grants")) return {
session_id: sessionId, purpose: "observe", device_id: (body as { device_id: string }).device_id,
token: grantToken, expires_at: new Date(Date.now() + 300_000).toISOString(),
};
if (path.endsWith("/revoke")) return {};
throw new Error(`unexpected route ${path}`);
},
};
const ctx = { api, flags: { cwd: directory, json: true } } as unknown as AppContext;
const flags = { str: () => undefined } as unknown as CommandFlags;
const overrides = {
cwd: directory,
enrollment: () => ({ device_id: "dev-1", display_name: "test" }),
repo: () => ({ repo: "fixture", branch: "main", base_commit: "0".repeat(40), dirty_file_count: 0 }),
connector: () => null,
browser: () => null,
out: (value: string) => output.push(value),
err: (value: string) => { throw new Error(value); },
isTTY: false,
columns: undefined,
};
try {
assert.equal(await cmdRc(ctx, ["start"], flags, overrides), 0);
assert.equal(output.length, 1);
const started = JSON.parse(output.pop()!) as { session_id: string; device_id: string; observer: { url: string } };
assert.equal(started.session_id, sessionId);
assert.equal(started.device_id, "dev-1");
assert.equal(new URL(started.observer.url).search, "");
assert.ok(started.observer.url.includes(`#grant=${grantToken}`));

assert.equal(await cmdRc(ctx, ["status"], flags, overrides), 0);
assert.equal(output.length, 1);
const status = JSON.parse(output[0]!) as { session_id: string; observer: unknown };
assert.equal(status.session_id, sessionId);
assert.equal(status.observer, null);
assert.ok(!output[0]!.includes(grantToken));

output.length = 0;
assert.equal(await cmdRc(ctx, ["link"], flags, overrides), 0);
assert.equal(output.length, 1);
const linked = JSON.parse(output.pop()!) as { session_id: string; observer: { url: string } };
assert.equal(linked.session_id, sessionId);
assert.ok(linked.observer.url.includes(grantToken));

assert.equal(await cmdRc(ctx, ["off"], flags, overrides), 0);
assert.equal(output.length, 1);
const closed = JSON.parse(output[0]!) as { session_id: string | null; host_state: string };
assert.equal(closed.session_id, null);
assert.equal(closed.host_state, "off");
} finally {
if (priorConfig === undefined) delete process.env["AETHER_CONFIG_DIR"];
else process.env["AETHER_CONFIG_DIR"] = priorConfig;
rmSync(directory, { recursive: true, force: true });
}
});

// ── 3. Nothing rendered can carry a credential ──────────────────────────────

test("no rendered surface exposes a secret-bearing field", () => {
Expand Down
Loading