Skip to content
Closed
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
15 changes: 14 additions & 1 deletion skills/ocx/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: ocx
description: Drive a running opencodex (`ocx`) proxy from the CLI — account pools, provider routing, model catalog, usage and cost attribution, request logs, access keys, storage cleanup, and the management API. Use when a task involves controlling or inspecting an opencodex proxy rather than editing the opencodex codebase. Triggers: ocx, opencodex, proxy control, account pool, pause account, pool strategy, provider routing, usage report, cost attribution, access key, request log, conversation trace, storage cleanup, management API.
description: "Drive a running opencodex (`ocx`) proxy from the CLI — account pools, provider routing, model catalog, usage and cost attribution, request logs, access keys, storage cleanup, and the management API. Use when a task involves controlling or inspecting an opencodex proxy rather than editing the opencodex codebase. Triggers: ocx, opencodex, proxy control, account pool, pause account, pool strategy, provider routing, usage report, cost attribution, access key, request log, conversation trace, storage cleanup, management API."
---

# Operating `ocx`
Expand Down Expand Up @@ -90,6 +90,19 @@ starring would be useful, say so and let the user decide.
The same boundary covers the session-gated `/api/codex-prompt` writes: read them with
`ocx inspect codex-prompt`, and leave the writes to the dashboard.

## Secret-bearing commands

**Do not run a command that creates an access key or starts a rotation.** This includes
`ocx access key create`, its `access keys` and `api-key` aliases, `ocx access key rotate <id>`,
the equivalent `opencodex` forms, and direct `POST /api/keys` or `POST /api/keys/rotate` calls.
Their one-time responses contain a new plaintext data-plane credential, and command output can
enter the agent transcript. Ask the user to perform that step in a terminal outside the agent
session, configure the replacement, and confirm only that it is configured plus any non-secret key
or rotation id needed for follow-up. Never ask the user to paste the key into chat. Configuration
confirmation is not approval to revoke the existing credential. Before committing an in-place
rotation or removing an old key after a separately created replacement, identify the existing key
id that will be revoked and obtain separate explicit user approval immediately before that command.

## Destructive verbs

`storage trash restore` and `storage policy run` refuse without `--yes` (exit 2, nothing sent).
Expand Down
39 changes: 33 additions & 6 deletions skills/ocx/references/03_recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,47 @@ Read `accounts[]`. Two things to respect:
`providers[]` and `models[]` carry `estimatedCostUsd`. Costs are estimates; `estimateReasons` in the
log rows tells you why (for example `usage_estimated`, `expected_price_overlay`).

## 5. Rotate an access key and confirm it went quiet
## 5. Prepare an access-key rotation without exposing the new key

```bash
ocx access key list --json
ocx access key create rotated --json # the plaintext key is in THIS response only
```

Creating a key or starting an in-place rotation returns a one-time plaintext credential. **Do not
run either secret-returning command from an agent session, including through the `access keys` or
`api-key` aliases, the `opencodex` executable, or direct management-API POSTs to `/api/keys` and
`/api/keys/rotate`:** terminal output can enter the model transcript. Ask the user to perform that
step in a separate terminal, configure the replacement, and report only that it is configured plus
any non-secret key or rotation id needed for follow-up. Never ask for the key itself.

Configuration confirmation is not approval to revoke the existing credential. After confirmation,
identify the existing key id that will be revoked and obtain separate explicit user approval
immediately before one of the following paths.

For an in-place rotation, commit the pending replacement on the same id:

```bash
ocx access key rotate commit <id> <rotation-id> --json
```

For a separately created replacement key, remove the old id:

```bash
ocx access key remove <old-id> --yes --json
ocx access key list --json # the old id is gone; check usage on the rest
```

Note the argument style: `create <name>` and `remove <id>` are **positionals**, not `--label` and
`--id`. `remove` also refuses without `--yes`.
Then verify the matching result:

```bash
ocx access key list --json
```

For an in-place rotation, the same id remains and `pendingRotation` disappears. For a separately
created replacement, the old id disappears; check usage on the remaining keys. Note the argument
style: `remove <id>` is positional, not `--id`, and refuses without `--yes`.

The list carries per-key usage, so a key whose count stops advancing is genuinely unused. The
plaintext key appears once, in the `create` response, and is never retrievable again.
replacement key's plaintext is never retrievable after creation.

An `ambiguous` footer on the list means two configured keys share an id, so per-key totals do not
exist for them — do not attribute usage to either.
Expand Down
53 changes: 53 additions & 0 deletions tests/skill-ocx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,57 @@ describe("the consent boundary is stated, not implied", () => {
expect(recipes).toContain("PREVIEW");
expect(recipes).toContain("get approval");
});

const secretBearingAccessKeyCommand =
/\b(?:ocx|opencodex)(?:\.(?:exe|mjs))?["']?\s+(?:access\s+keys?|api-key)\s+(?:create\b|rotate\b(?!\s+(?:--json\s+)?(?:commit|abort)\b))/gim;
const secretBearingManagementRequest =
/(?:(?:\bPOST\b|(?:--request|-X|-Method)\s+["']?POST["']?|method\s*:\s*["']POST["'])[^\n]{0,240}\/api\/keys(?:\/rotate)?(?=$|[\s"'?#])|\/api\/keys(?:\/rotate)?(?=$|[\s"'?#])[^\n]{0,240}(?:\bPOST\b|(?:--request|-X|-Method)\s+["']?POST["']?|method\s*:\s*["']POST["']))/gim;

function secretBearingCommandsInFences(text: string): string[] {
const matches: string[] = [];
for (const block of text.matchAll(/```[^\n]*\n([\s\S]*?)```/g)) {
// Fold common shell continuations so a wrapped command cannot evade the check.
const executableText = block[1]!.replace(/(?:\\|`|\^)\r?\n\s*/g, " ");
matches.push(...executableText.matchAll(secretBearingAccessKeyCommand).map(match => match[0]));
matches.push(...executableText.matchAll(secretBearingManagementRequest).map(match => match[0]));
}
return matches;
}

test("the secret-bearing command detector covers aliases and shell wrappers", () => {
for (const command of [
"ocx access key create rotated --json",
"& ocx access keys create rotated --json",
"opencodex api-key rotate old-id --json",
"ocx access key rotate --json old-id",
"ocx access key \\\n create rotated --json",
"curl -X POST http://127.0.0.1:3000/api/keys",
"Invoke-RestMethod http://127.0.0.1:3000/api/keys/rotate -Method Post",
]) {
expect(secretBearingCommandsInFences("```bash\n" + command + "\n```"), command).toHaveLength(1);
}
for (const command of [
"ocx access key rotate commit old-id rotation-id",
"ocx access key rotate --json commit old-id rotation-id",
"ocx access key rotate --json abort old-id rotation-id",
]) {
expect(secretBearingCommandsInFences("```bash\n" + command + "\n```"), command).toEqual([]);
}
expect(secretBearingCommandsInFences("```bash\ncurl -X POST http://127.0.0.1:3000/api/keys/rotate/commit\n```")).toEqual([]);
});

test("agent-facing guidance never exposes secret-bearing access-key commands", () => {
for (const file of ["SKILL.md", ...REFERENCES.map(ref => join("references", ref))]) {
expect(secretBearingCommandsInFences(read(file)), file).toEqual([]);
}
const skill = readFileSync(SKILL, "utf8");
const recipes = read("references/03_recipes.md");
expect(skill).toMatch(/outside the agent\s+session/);
expect(skill).toMatch(/configuration\s+confirmation is not approval/i);
expect(skill).toMatch(/separate explicit user approval/i);
const approvalAt = recipes.indexOf("obtain separate explicit user approval");
expect(approvalAt).toBeGreaterThanOrEqual(0);
expect(approvalAt).toBeLessThan(recipes.indexOf("ocx access key rotate commit"));
expect(approvalAt).toBeLessThan(recipes.indexOf("ocx access key remove"));
});
});
Loading