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
27 changes: 26 additions & 1 deletion docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ ocx start --port 10100 --socks5
ocx start --socks5-off
```

### `ocx stop`
### `ocx stop [--json]`

Stop the running proxy (by PID), remove the PID file, and restore native Codex. If a managed
background service is installed, `ocx stop` also stops it first so it cannot respawn the proxy.
Expand All @@ -65,6 +65,14 @@ It does not enter the forced-stop fallback for a process already observed to hav
receipt-backed deferral still leaves final restoration and receipt cleanup with the parent;
failure to restore shared client configuration keeps the stop failed and its receipt outstanding.

`ocx stop --json` runs exactly the same stop path and prints one versioned summary document
(`schema: "ocx-stop/1"`) on stdout, while the human progress lines move to stderr. The summary
carries the outcome class (`stopped`, `not-running`, `history-incomplete`, `history-deferred`,
or `failed`), the service and proxy classifications, whether the runtime is down, and a stable
one-line message. Exit codes are identical with and without `--json`: 0 on success, 1 on failure,
79 when only Codex history cleanup did not complete, and 80 when the shared teardown was deferred
and is still owed.

### `ocx restart`

When a proxy is running, ask that exact attested PID and port to restart in place, wait for its
Expand Down Expand Up @@ -239,6 +247,23 @@ The CLI's own `--json` output is deliberately narrower than the HTTP body: it em
`unreachable`. Exit codes are 0 for ready; 1 for not-ready, pending, failed, timeout, or
unreachable; and 64 for invalid arguments.

### `ocx resolve [--json]`

Resolve the runtime facts a shell needs without re-implementing them: the config home, the
effective port, and the identity-checked liveness verdict. `--json` emits one versioned
document (`schema: "ocx-resolve/1"`) with `cliVersion`, `configHome`, `port`
(`effective`, `configured`, and `source`), and `liveness` (`status`, `pid`, `port`,
`source`, plus `version`, `role`, and `hostname` when the live proxy reports them).
Liveness has three answers: `live`, `absent-proven` (every recorded and configured endpoint
definitively refused or answered non-opencodex), and unknown — a timed-out probe or a listener
that withholds `/healthz` exits 1 rather than reading as absent, so only `absent-proven` may
authorise starting a new runtime. The port is the live listener's port when a proxy answers,
otherwise the configured port (default 10100). Exit 0 carries a trustworthy verdict; exit 1 means
the CLI could not resolve — including an invalid `config.json`, which is never repaired to
defaults here — and the caller must refuse to guess; any unknown argument exits 64. Discovery uses
the same ownership-safe probe budget as `ocx start`, because a false "nothing listening" answer
is how duplicate proxies happen. The verb is read-only and skips the shim auto-restore preflight.

### `ocx doctor`

The default report includes the native-write coordinator state and exact path using immutable
Expand Down
3 changes: 3 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,13 +423,16 @@
"cli-ready-subprocess.test.ts": "cli",
"cli-ready.test.ts": "cli",
"cli-registry.test.ts": "cli",
"cli-resolve-subprocess.test.ts": "cli",
"cli-resolve.test.ts": "cli",
"cli-restart-health.test.ts": "cli",
"cli-restore-back.test.ts": "cli",
"cli-start-auxiliary-bind.test.ts": "cli",
"cli-start-journal-order.test.ts": "cli",
"cli-status-hub-state.test.ts": "cli",
"cli-status-json.test.ts": "cli",
"cli-status-oauth-health.test.ts": "cli",
"cli-stop-json.test.ts": "cli",
"cli-storage-inspect.test.ts": "cli",
"cli-transport-honesty.test.ts": "cli",
"cli-usage-report.test.ts": "cli",
Expand Down
17 changes: 16 additions & 1 deletion skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ JSON mode: `envelope`.

- Reads /healthz plus local config; drives no management API route.

### `ocx resolve`

One JSON document naming the config home, the effective port, and the identity-checked proxy liveness verdict.

Drives no management route.

| Flag | Value | Meaning |
|---|---|---|
| `--json` | boolean | Emit the resolve document as JSON (the shell contract). |

JSON mode: `envelope`.

- Exit 0 carries a trustworthy verdict (live or proven absent); exit 1 means the CLI could not resolve and a caller must refuse to guess — unknown liveness never reads as absent.
- Built for embedding shells (desktop app): the liveness budgets stay owned by src/server/proxy-liveness.ts.

### `ocx capabilities`

List the declared CLI capabilities and the management routes they drive.
Expand Down Expand Up @@ -915,6 +930,6 @@ JSON mode: `payload`.

## Counts

- declared capabilities: 49
- declared capabilities: 50
- of those, state-changing: 25
- head-resolved invocations: 2
15 changes: 15 additions & 0 deletions src/cli/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ export const CAPABILITIES: readonly Capability[] = [
json: "envelope",
details: ["Reads /healthz plus local config; drives no management API route."],
},
{
command: ["resolve"],
summary: "One JSON document naming the config home, the effective port, and the identity-checked proxy liveness verdict.",
// No management route, same split as status: discovery is the identity-checked
// /healthz probe inside findLiveProxy plus local config and the home from
// src/config/paths.ts.
routes: [],
flags: [{ name: "--json", value: "boolean", summary: "Emit the resolve document as JSON (the shell contract)." }],
mutates: false,
json: "envelope",
details: [
"Exit 0 carries a trustworthy verdict (live or proven absent); exit 1 means the CLI could not resolve and a caller must refuse to guess — unknown liveness never reads as absent.",
"Built for embedding shells (desktop app): the liveness budgets stay owned by src/server/proxy-liveness.ts.",
],
},
{
command: ["hub", "invite"],
summary: "Mint a single-use pairing code on a hub and print the exact `ocx connect` line for one more machine.",
Expand Down
3 changes: 3 additions & 0 deletions src/cli/codex-shim-autorestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
if (command === "uninstall" || command === "remove") return true;
// `lab` is read-only inspection; it must not trigger shim side effects.
if (command === "lab") return true;
// `resolve` is read-only inspection for embedding shells: a lookup made to populate
// a consent surface must not trigger a shim repair side effect first.
if (command === "resolve") return true;
// The entire updater-inspection namespace is zero-effect, including malformed
// or future actions. A later `apply` implementation must own its preflight.
if (command === "system" && args[1] === "codex-cli-update") return true;
Expand Down
37 changes: 34 additions & 3 deletions src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { stripGrokConfig } from "../grok/inject";
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
import { isJsonOption, takeFlag, terminalSafeError } from "./runtime-api";
import { printStopSummary, type StopOutcome } from "./stop-report";
import type { ResolveArgs } from "./resolve";
import type { ClientConnectionState } from "../client/state";
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";

Expand All @@ -44,8 +46,9 @@ export interface CliDispatchDeps {
/** Spawn a detached proxy child (stdio ignore, unref'd, provenance env). */
spawnDetached: (argv: readonly string[]) => void;
handleStart: () => Promise<void>;
handleStop: () => Promise<boolean>;
handleStop: () => Promise<StopOutcome>;
handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
handleResolve: (args: ResolveArgs) => Promise<number>;
handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
handleTrayProxyRestart: () => Promise<void>;
handleRestartStartWhenStopped: () => Promise<boolean | "skipped">;
Expand Down Expand Up @@ -98,11 +101,39 @@ const commandRunners: Record<string, CommandRunner> = {
stop: async deps => {
// Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
// re-start the proxy immediately, so warning there would contradict the next line.
if (await deps.handleStop()) {
console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').");
const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
if (!takeFlag(deps.args.slice(1), "--json")) {
// handleStop returns the structured outcome now; an object is always truthy, so
// the warning must key on .ok — otherwise a failed stop would still claim downtime.
if ((await deps.handleStop()).ok) console.log(warning);
return Number(process.exitCode ?? 0);
Comment on lines +105 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Test the stop outcome boolean in human mode

After handleStop was changed from returning a boolean to returning StopOutcome, the awaited value here is always a truthy object. Thus a failed stop—including ownership refusal, an unresolvable PID, or a respawned proxy—still prints that requests will fail until the proxy is restarted, even though the proxy may remain live. Check (await deps.handleStop()).ok, as the JSON branch already does.

Useful? React with 👍 / 👎.

}
// --json is a reporting layer over the SAME stop path: the receipt, the drain, the
// respawn verification and the client-config restore run unchanged. Human output
// still prints, but on stderr, so stdout carries exactly one JSON summary document.
// The exit code (0/1/79/80) crosses the process boundary untouched — the shell reads
// it from the child, and the stop-contract codes must survive the JSON mode.
const humanLog = console.log;
console.log = console.error;
let outcome: StopOutcome | undefined;
try {
outcome = await deps.handleStop();
if (outcome.ok) console.log(warning);
} finally {
console.log = humanLog;
}
// A throw above propagates after the finally restores the console, so reaching here
// with an undefined outcome cannot happen; the guard keeps the assignment provable.
if (outcome) printStopSummary(outcome.summary);
return Number(process.exitCode ?? 0);
},
resolve: async deps => {
// Same fail-closed shape as `ready`: parseCliHead pre-parsed the verb before any
// preflight side effect, so a missing resolveArgs means dispatch diverged. Refuse
// with code 64 and perform NO I/O.
if (!deps.head.resolveArgs) return 64;
return await deps.handleResolve(deps.head.resolveArgs);
},
restore: async deps => {
const restoreArgs = deps.args.slice(1);
const restoreJson = takeFlag(restoreArgs, "--json");
Expand Down
3 changes: 2 additions & 1 deletion src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Usage:
ocx setup Interactive setup (alias: init)
ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]
Start the proxy; SOCKS5 defaults to 127.0.0.1:10808
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
ocx stop [--json] Stop the proxy AND restore native Codex (plain codex works again)
ocx restore Restore native Codex without stopping (alias: eject)
ocx restore back Re-point codex at the running proxy (undo restore)
ocx restore --remove-codex-provider-table
Expand Down Expand Up @@ -63,6 +63,7 @@ Usage:
ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
ocx capabilities [--json] List declared capabilities and the API routes they drive
ocx ready [--json] [--wait [--timeout <s>]] Check post-sync readiness (exit 0 only when ready)
ocx resolve [--json] Config home, effective port, and liveness (JSON for shells)
ocx provider <sub> Providers, connectivity, quota, and selected models
ocx account <sub> Accounts, login/reauth, key pools, and quota controls
ocx models <sub> Live/custom models, visibility, context, and shadow calls
Expand Down
Loading
Loading