Skip to content
Draft
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
22 changes: 2 additions & 20 deletions skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,24 +472,6 @@ JSON mode: `payload`.
- `policy set` never enables implicitly: omitting `--enabled` keeps the stored value.
- `policy run` forces a run regardless of schedule, so it needs `--yes`.

### `ocx system codex-restart`

Restart the Codex app-server.

| Method | Route |
|---|---|
| POST | `/api/system/codex-restart` |

| Flag | Value | Meaning |
|---|---|---|
| `--yes` | boolean | Required: restarts the operator's running Codex app-server. |
| `--json` | boolean | Emit the restart result as JSON. |

JSON mode: `payload`.

- `sync --restart-codex` is not a substitute: it restarts only as a side effect after a catalog or cache write, so it cannot restart a healthy install on request.
- --yes is mandatory because this interrupts a running editor session, which must never happen because an agent guessed a subcommand.

### `ocx integration native`

Show or toggle the native Claude, Claude Desktop, Codex, and Grok integrations.
Expand Down Expand Up @@ -530,6 +512,6 @@ JSON mode: `payload`.

## Counts

- declared capabilities: 29
- of those, state-changing: 11
- declared capabilities: 28
- of those, state-changing: 10
- head-resolved invocations: 2
15 changes: 0 additions & 15 deletions src/cli/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,21 +418,6 @@ export const CAPABILITIES: readonly Capability[] = [
"The GUI reads this state directly; without a verb an agent could not tell whether the Codex app-server was reachable at all.",
],
},
{
command: ["system", "codex-restart"],
summary: "Restart the Codex app-server.",
routes: [{ method: "POST", path: "/api/system/codex-restart" }],
flags: [
{ name: "--yes", value: "boolean", summary: "Required: restarts the operator's running Codex app-server." },
{ name: "--json", value: "boolean", summary: "Emit the restart result as JSON." },
],
mutates: true,
json: "payload",
details: [
"`sync --restart-codex` is not a substitute: it restarts only as a side effect after a catalog or cache write, so it cannot restart a healthy install on request.",
"--yes is mandatory because this interrupts a running editor session, which must never happen because an agent guessed a subcommand.",
],
},
{
command: ["claude", "desktop", "status"],
summary: "Applied-vs-desired Claude Desktop state, including staleness, drift, and health.",
Expand Down
10 changes: 0 additions & 10 deletions src/cli/system-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const USAGE = `Usage:
ocx system diagnostics [--json]
ocx system sync [--json]
ocx system codex-app-server [--json]
ocx system codex-restart --yes [--json]
ocx system update check [--channel <latest|preview>] [--json]
ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
ocx system update status <job-id> [--json]`;
Expand Down Expand Up @@ -111,15 +110,6 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
// an agent could not see whether the Codex app-server was reachable at all.
const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
} else if (sub === "codex-restart") {
// --yes required: this restarts the user's running Codex app-server, so it is exactly the
// class of action that must not happen because an agent guessed a subcommand.
const args = [...rest];
const wantsJson = takeFlag(args, "--json");
const yes = takeFlag(args, "--yes");
if (!yes) throw new CliUsageError("system codex-restart requires --yes", USAGE);
rejectArgs(args, USAGE);
printData(await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps), wantsJson, ["Codex app-server restart requested."]);
} else if (sub === "update") await update(rest, deps);
else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
});
Expand Down
2 changes: 1 addition & 1 deletion src/server/management/route-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
{ method: "GET", path: "/api/storage", module: "server/management/storage-log-guard-routes", mutates: false, mechanism: "negated-guard" },
{ method: "GET", path: "/api/routing-analytics", module: "server/management/routing-analytics-routes", mutates: false, mechanism: "negated-guard" },
{ method: "GET", path: "/api/system/codex-app-server", module: "server/management/system-routes", mutates: false, mechanism: "path-constant" },
{ method: "POST", path: "/api/system/codex-restart", module: "server/management/system-routes", mutates: true, mechanism: "path-constant" },
{ method: "POST", path: "/api/system/codex-restart", module: "server/management/system-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Interrupts active editor turns and therefore requires a gui-session principal; it must never gain an agent CLI capability." } },
{ method: "POST", path: "/api/providers/reload", module: "server/management/provider-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "capability-principal", why: "Gated on the local-provider-reload-capability principal (provider-routes.ts:467), not an operator action." } },
{ method: "GET", path: "/api/client-integrations/{clientId}", module: "server/management/integration-routes", mutates: false, mechanism: "prefix-decode" },
{ method: "PUT", path: "/api/client-integrations/{clientId}", module: "server/management/integration-routes", mutates: true, mechanism: "prefix-decode" },
Expand Down
9 changes: 9 additions & 0 deletions src/server/management/system-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
(url.pathname === CODEX_APP_SERVER_STATE_PATH && req.method === "GET")
|| (url.pathname === CODEX_RESTART_PATH && req.method === "POST")
) {
if (
url.pathname === CODEX_RESTART_PATH
&& ctx.principal !== "gui-session"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Support Codex restart from remote dashboards

When the proxy uses a non-loopback bind, issueGuiSession returns null (src/server/management-auth.ts:250), so the supported remote dashboard signs in with the raw admin token and managementPrincipal resolves it as admin-token (src/server/management-auth.ts:436-442). This check therefore returns 403 for every restart confirmed in a remote dashboard, permanently breaking its restart control. Provide a consent flow that can grant authenticated remote browsers an appropriate session principal, or explicitly disable the control and document the restriction; the current Web Dashboard and Management API docs still describe remote-token authentication and this endpoint without that limitation.

AGENTS.md reference: AGENTS.md:L300-L301

Useful? React with 👍 / 👎.

) {
return jsonResponse({
success: false,
error: "Restart requires confirmation from the dashboard.",
}, 403, req, config);
}
// Resolved inside the path check, not at the top of this function: every
// /api/system/* request runs through here, and an unconditional import would
// pull the platform process-enumeration helpers into requests that never
Expand Down
28 changes: 27 additions & 1 deletion tests/codex-restart-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ function contextFor(
path: string,
method: string,
service: NonNullable<ManagementContext["deps"]["codexRestartService"]>,
principal: ManagementContext["principal"] | null = "gui-session",
): ManagementContext {
const url = new URL(`http://127.0.0.1:10100${path}`);
return {
req: new Request(url, { method }),
url,
config: loadConfig(),
deps: { codexRestartService: service },
...(principal !== null ? { principal } : {}),
} as ManagementContext;
}

Expand Down Expand Up @@ -93,6 +95,31 @@ describe("GET /api/system/codex-app-server", () => {
});

describe("POST /api/system/codex-restart", () => {
test.each([
["a raw admin token", "admin-token" as const],
["direct dispatch without a principal", null],
])("rejects %s before performing the restart", async (_label, principal) => {
let restarted = false;
const response = await handleSystemRoutes(contextFor(
CODEX_RESTART_PATH,
"POST",
stubService({
performRestart: async () => {
restarted = true;
return STOPPED;
},
}),
principal,
));

expect(response?.status).toBe(403);
expect(await response?.json()).toEqual({
success: false,
error: "Restart requires confirmation from the dashboard.",
});
expect(restarted).toBe(false);
});

test("returns the restart result in the contract shape", async () => {
const response = await handleSystemRoutes(
contextFor(CODEX_RESTART_PATH, "POST", stubService()),
Expand Down Expand Up @@ -161,4 +188,3 @@ describe("POST /api/system/codex-restart", () => {
expect(response).toBeNull();
});
});

Loading