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
4 changes: 2 additions & 2 deletions docs/generated/commands.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- GENERATED FILE: run `npm run docs:generate`; do not edit by hand. -->
<!-- manifest-digest: sha256:245165ce8a61a9c39f71a60be3347cca54e3785feec2d261b9d81779aca571b9 -->
<!-- manifest-digest: sha256:a8c24ac055d15ec28da0252b99970330b8e6d8d1c2994e8b54f4fd126680e11f -->
# Generated command reference

This reference is generated from the validated, versioned command manifest. Availability is evaluated at runtime; a listed command may still require authentication, a hosted capability, or local tooling.
Expand Down Expand Up @@ -233,7 +233,7 @@ Permission: `local-write` · Availability: `runtime-dependent` · Telemetry: `sh

### System

#### `aether pc [map|doctor|verify-browser|open|inspect-browser] [target]`
#### `aether pc [map|doctor|verify-browser|open|inspect-browser|draft-browser] [target]`

inspect PC capabilities and diagnose app performance with scoped actions

Expand Down
8 changes: 7 additions & 1 deletion docs/pc-capability-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ or hosted-service entitlement claim.
| `aether pc verify-browser` | Opens a loopback readiness page | Interactive approval and a one-use callback prove that a browser rendered the page. The listener closes after the result. |
| `aether pc open [aether-cloud\|claude\|chatgpt]` | Opens one fixed site | One-use interactive approval bound to target and detected browser state. `--yes` and headless sessions cannot approve. Launcher start is reported as dispatch, not as verified page rendering. |
| `aether pc inspect-browser [aether-cloud\|claude\|chatgpt] [--json]` | Opens a fixed HTTPS site in a disposable Edge profile | After fresh interactive approval, observes a real top-level document, checks its origin, loader identity, and bounded main-document HTTP status class, then reports only fixed structural booleans for a successful response. HTTP 4xx/5xx return `http-error` without inspecting structure. `rendered` does not mean authenticated; `login-required` means a password field or login route was observed. The profile is closed and removed afterward. |
| `aether pc draft-browser [aether-cloud\|claude\|chatgpt] [--json]` | Inserts one locally typed line into one observed empty composer in a disposable Edge profile | Requires approval to open the page and a second approval bound to its document and element identity before insertion. It rechecks origin, tab count, document loader, element identity, positive layout box, and empty state before focus and again before insertion; then checks that the DOM contains the entered text without returning it. The site may save or send data in response to typing or focus. It never clicks a send button. Missing or changed composers refuse text insertion. |

`pc doctor` reports recommendations from observed resource pressure. CPU and
memory now use three timed samples and show their range. The optional fixed
Expand Down Expand Up @@ -68,7 +69,12 @@ inspection. It watches for extra page targets during navigation and fails proof
if one appears, even if that page closes before the final count. Its
`browser.inspect` map entry remains **unverified** merely from
driver presence; the receipt from a particular run carries that run's proof.
Browser clicking, typing, and authenticated-session claims remain unavailable.
Generic browser clicking, sending, and authenticated-session claims remain unavailable.
The v2 map lists `browser.draft` separately from generic `browser.act` and does
not infer runtime, installed, or hosted qualification from source presence.
Draft text is entered at the terminal, is not accepted in command arguments,
and is omitted from receipts and the redacted audit journal. A controlled
page can react to typing, so the second approval explicitly covers that risk.
The temporary DevTools endpoint is local to this user's session, not an OS
isolation boundary against another same-user process. A failed profile cleanup
turns inspection into failure and is reported instead of hidden.
Expand Down
8 changes: 4 additions & 4 deletions src/commands/command_manifest_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1644,9 +1644,9 @@ export const COMMAND_MANIFEST_SOURCE: readonly CommandManifestEntry[] = [
"aliases": [],
"compatibilityAliases": [],
"deprecatedAliases": [],
"args": "[map|doctor|verify-browser|open|inspect-browser] [target]",
"args": "[map|doctor|verify-browser|open|inspect-browser|draft-browser] [target]",
"summary": "inspect PC capabilities and diagnose app performance with scoped actions",
"detailedHelp": "aether pc map [v1|v2] | doctor [aether-cloud|claude|chatgpt|ollama] [--probe-network] | verify-browser | open [aether-cloud|claude|chatgpt] | inspect-browser [aether-cloud|claude|chatgpt]\nMap defaults to the v2 axes; map v1 preserves the legacy JSON view for one transition release. Network probes require --probe-network. Browser verification opens a loopback page. Browser inspection opens a disposable Edge profile and reports only origin, document readiness and structural presence. Browser actions require fresh interactive approval; --yes cannot approve them.",
"detailedHelp": "aether pc map [v1|v2] | doctor [aether-cloud|claude|chatgpt|ollama] [--probe-network] | verify-browser | open [aether-cloud|claude|chatgpt] | inspect-browser [aether-cloud|claude|chatgpt] | draft-browser [aether-cloud|claude|chatgpt]\nMap defaults to the v2 axes; map v1 preserves the legacy JSON view for one transition release. Network probes require --probe-network. Browser verification opens a loopback page. Browser inspection opens a disposable Edge profile and reports only origin, document readiness and structural presence. Browser draft opens that profile, finds one empty composer, and requires a second approval before inserting one line of text. The site may save or send data when focused or typed into. Browser actions require fresh interactive approval; --yes cannot approve them.",
"section": "System",
"hidden": false,
"permissionClass": "local-write",
Expand Down Expand Up @@ -1705,13 +1705,13 @@ export const COMMAND_MANIFEST_SOURCE: readonly CommandManifestEntry[] = [
"module": "src/commands/command_manifest_data.ts",
"symbol": "COMMAND_MANIFEST_SOURCE",
"target": "pc",
"usage": "aether pc [map|doctor|verify-browser|open|inspect-browser] [target]",
"usage": "aether pc [map|doctor|verify-browser|open|inspect-browser|draft-browser] [target]",
"visible": true,
"disposition": "generated"
},
"release": {
"disposition": "new",
"note": "Read-only PC map and doctor, plus approved browser readiness and disposable Edge structural inspection. Desktop and unsandboxed command actions remain unavailable."
"note": "Read-only PC map and doctor, approved browser readiness and inspection, and separately approved insertion into one observed empty composer in disposable Edge. Desktop and unsandboxed command actions remain unavailable."
}
},
{
Expand Down
84 changes: 80 additions & 4 deletions src/commands/pc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { AppContext } from "../core/context.js";
import type { CommandFlags } from "../core/command_dispatch.js";
import { detectBrowserRuntime, verifyBrowserLaunch, type VerifyResult } from "../core/browser_runtime.js";
import { openTargetChecked } from "../core/opener.js";
import { PcActionBroker } from "../core/pc/broker.js";
import { PcActionBroker, type PcActionReceipt } from "../core/pc/broker.js";
import { controlledEdgeExecutable, inspectControlledPage, type BrowserInspection } from "../core/pc/browser_inspect.js";
import { PcFileAudit, PcHostGateway } from "../core/pc/gateway.js";
import { PC_TARGETS, isPcTarget, pcDoctor, pcMap, pcMapV2, pcTargetUrl, type PcDoctorReport } from "../core/pc/doctor.js";
Expand All @@ -28,17 +28,34 @@ function renderDoctor(report: PcDoctorReport): string {
return lines.join("\n") + "\n";
}

async function explicitApproval(message: string): Promise<boolean> {
async function explicitApproval(message: string, signal?: AbortSignal): Promise<boolean> {
if (!process.stdin.isTTY) return false;
const rl = createInterface({ input: process.stdin, output: process.stderr });
try {
const answer = await new Promise<string>((resolve) => rl.question(`${message}\nApprove this one action? [y/N] `, resolve));
const answer = await new Promise<string>((resolve) => {
const done = (value: string) => { signal?.removeEventListener("abort", abort); resolve(value); };
const abort = () => { rl.close(); done(""); };
if (signal?.aborted) { done(""); return; }
signal?.addEventListener("abort", abort, { once: true });
rl.question(`${message}\nApprove this one action? [y/N] `, done);
});
return /^y(es)?$/i.test(answer.trim());
} finally {
rl.close();
}
}

async function draftLine(): Promise<string | null> {
if (!process.stdin.isTTY) return null;
const rl = createInterface({ input: process.stdin, output: process.stderr });
try {
const value = await new Promise<string>((resolve) => rl.question("Draft text (one line, at most 2000 characters): ", resolve));
return value.length > 0 && value.length <= 2000 && !/[\r\n\0]/.test(value) ? value : null;
} finally {
rl.close();
}
}

export async function cmdPc(ctx: AppContext, argv: string[], flags: CommandFlags): Promise<number> {
const sub = argv[0] ?? "map";
if (sub === "map" && (argv.length === 1 || (argv.length === 2 && (argv[1] === "v1" || argv[1] === "v2")))) {
Expand Down Expand Up @@ -168,6 +185,65 @@ export async function cmdPc(ctx: AppContext, argv: string[], flags: CommandFlags
`Browser inspection: ${proof?.state ?? receipt.status}\n${proof?.reason ?? receipt.reason}\n`);
return receipt.status === "succeeded" ? 0 : 3;
}
process.stderr.write("usage: aether pc map [v1|v2] | doctor [aether-cloud|claude|chatgpt|ollama] [--probe-network] | verify-browser | open [aether-cloud|claude|chatgpt] | inspect-browser [aether-cloud|claude|chatgpt]\n");
if (sub === "draft-browser" && argv.length === 2) {
const target = argv[1]!;
if (!isPcTarget(target) || target === "ollama") {
process.stderr.write("PC browser draft supports aether-cloud, claude and chatgpt.\n");
return 2;
}
if (ctx.flags.yes || !process.stdin.isTTY) {
process.stderr.write("PC browser draft requires fresh interactive approval; --yes and headless execution do not grant it.\n");
return 3;
}
const executable = controlledEdgeExecutable();
if (!executable) {
process.stderr.write("Controlled Edge browser unavailable on this Windows installation.\n");
return 3;
}
const text = await draftLine();
if (text === null) {
process.stderr.write("Draft text must be one nonempty line of at most 2000 characters.\n");
return 2;
}
const url = pcTargetUrl(target);
let draftAbort: AbortController | null = null;
const broker = new PcActionBroker(randomUUID(), userInfo().username, {
interactive: true,
approve: (plan) => plan.adapter === "browser.inspect"
? explicitApproval(`Open and inspect ${new URL(url).origin} in a disposable Edge profile?`)
: explicitApproval(`Insert ${text.length} characters into the observed empty ${plan.operation} at ${new URL(url).origin}? Element ${plan.target.split(":").at(-1)}. The site may save or send data when focused or typed into.`, draftAbort?.signal),
});
const gateway = new PcHostGateway(broker, new PcFileAudit());
const plan = broker.plan({ adapter: "browser.inspect", operation: "inspect", target, expectedState: executable });
const observation: { inspection?: BrowserInspection; draftReceipt?: PcActionReceipt } = {};
const openReceipt = await gateway.execute(plan, () => controlledEdgeExecutable() ?? "unavailable", async () => {
draftAbort = new AbortController();
const onInterrupt = () => draftAbort?.abort();
process.once("SIGINT", onInterrupt);
try {
observation.inspection = await inspectControlledPage(url, { signal: draftAbort.signal }, async (composer) => {
const action = broker.plan({
adapter: "browser.draft", operation: composer.kind,
target: `${target}:${composer.identity}`, expectedState: composer.identity,
}, 60_000);
observation.draftReceipt = await gateway.execute(action, () => composer.observe(), () => composer.insert(text));
});
} finally {
process.removeListener("SIGINT", onInterrupt);
}
return {
dispatched: observation.inspection.browserLaunched,
verified: observation.inspection.profileCleaned && (observation.inspection.state === "rendered" || observation.inspection.state === "login-required"),
};
});
const finalReceipt = observation.draftReceipt?.status === "succeeded" && openReceipt.status !== "succeeded"
? { ...observation.draftReceipt, status: "unknown" as const, reason: "draft insertion succeeded but browser inspection or cleanup failed; verify before retry" }
: observation.draftReceipt;
process.stdout.write(ctx.flags.json ? JSON.stringify({ openReceipt, draftReceipt: finalReceipt ?? null, proof: observation.inspection ?? null }) + "\n"
: `Browser draft: ${finalReceipt?.status ?? "unavailable"}\n${finalReceipt?.reason ??
(observation.inspection?.state === "rendered" ? "No single empty editable composer was observed." : observation.inspection?.reason) ?? openReceipt.reason}\n`);
return openReceipt.status === "succeeded" && finalReceipt?.status === "succeeded" ? 0 : 3;
}
process.stderr.write("usage: aether pc map [v1|v2] | doctor [aether-cloud|claude|chatgpt|ollama] [--probe-network] | verify-browser | open [aether-cloud|claude|chatgpt] | inspect-browser [aether-cloud|claude|chatgpt] | draft-browser [aether-cloud|claude|chatgpt]\n");
return 2;
}
Loading
Loading