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
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/guides/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,13 @@ ocx integration client enable --client commandcode
ocx export --client commandcode --out ./providers.json
```

`ocx commandcode restore --op <opId>` accepts only Command Code operations, including
`--preview` and `--plan-fingerprint` requests. It rejects client/profile overrides.
The running proxy must support client-scoped restore; older proxies return an error
without falling back to the generic restore endpoint.
Use `ocx integration client restore` for the generic journal and the explicit Aside
profile commands above for Aside operations.

## Factory Droid

Run Droid once to create `~/.factory`, then explicitly enable this integration with
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ko/guides/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ ocx integration client history --client hermes
ocx integration client restore --op <opId> [--confirm-drift]
```

`ocx commandcode restore --op <opId>`는 Command Code 작업만 복원합니다.
`--preview`와 `--plan-fingerprint`에도 같은 조건이 적용되며, 클라이언트나
프로필을 바꾸는 옵션은 받지 않습니다. 실행 중인 프록시가 이 전용 복원 경로를
지원하지 않으면 일반 복원으로 재시도하지 않고 오류를 반환합니다. 일반 작업 기록은 위의
`ocx integration client restore`를, Aside 작업은 아래의 프로필 전용 명령을 사용하세요.

`--overwrite-conflict`는 **Replace**의 터미널 명령입니다.

```bash
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-ocx-skill-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const DOMAINS = [
{ name: "providers-models", roots: ["provider", "models", "alias"] },
{ name: "accounts", roots: ["account", "auth", "login", "logout"] },
{ name: "agents-routing", roots: ["agent", "combo", "route", "v2", "effort", "memory", "message"] },
{ name: "integrations", roots: ["claude", "integration", "grok", "codex-shim"] },
{ name: "integrations", roots: ["claude", "integration", "commandcode", "grok", "codex-shim"] },
{ name: "observe-system", roots: ["companion", "usage", "logs", "storage", "inspect", "system", "observe", "debug", "export", "import", "cost", "update", "config", "tray"] },
{ name: "access-remote", roots: ["link", "remote-workspace", "hub", "connect", "api", "access"] },
{ name: "lab", roots: ["lab"] },
Expand Down
10 changes: 7 additions & 3 deletions skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ These answer in the CLI head and never reach the proxy, so they work with nothin
| [providers-models](01_surface_providers-models.md) | 47 |
| [accounts](01_surface_accounts.md) | 40 |
| [agents-routing](01_surface_agents-routing.md) | 50 |
| [integrations](01_surface_integrations.md) | 40 |
| [integrations](01_surface_integrations.md) | 41 |
| [observe-system](01_surface_observe-system.md) | 92 |
| [access-remote](01_surface_access-remote.md) | 28 |
| [lab](01_surface_lab.md) | 21 |
Expand Down Expand Up @@ -827,6 +827,10 @@ Original invocation order. These headings preserve links to the previous single-

[State-changing task](01_surface_integrations.md#ocx-integration-client-restore)

### `ocx commandcode restore`

[State-changing task](01_surface_integrations.md#ocx-commandcode-restore)

### `ocx claude config status`

[Read-oriented task](01_surface_integrations.md#ocx-claude-config-status)
Expand Down Expand Up @@ -1369,6 +1373,6 @@ Original invocation order. These headings preserve links to the previous single-

## Counts

- declared capabilities: 330
- of those, state-changing: 199
- declared capabilities: 331
- of those, state-changing: 200
- head-resolved invocations: 2
29 changes: 28 additions & 1 deletion skills/ocx/references/01_surface_integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Use these declarations to choose a task, then check its flags and authority before execution.
Non-mutating probes may still contact providers, consume quota or refresh caches.

Declared capabilities: 40.
Declared capabilities: 41.

### `ocx claude config`

Expand Down Expand Up @@ -517,6 +517,33 @@ JSON mode: `payload`.
- Preview preserves exact opId/profile/confirm-drift intent. Generic restore derives the client on the server; the returned plan does not embed opId or complete command input.
- A refused or no-op preview is completed inspection, not applied work. A stale bound mutation exits5 with a fixed re-preview hint; no replacement token is automatically adopted.

### `ocx commandcode restore`

Usage: `ocx commandcode restore --op <opId> [--confirm-drift] [--preview | --plan-fingerprint <token>] [--json]`

Restore a Command Code rollback operation, retaining client ownership and drift checks.

State-changing: yes.

| Method | Route |
|---|---|
| POST | `/api/client-integrations/commandcode/restore` |
| POST | `/api/client-integrations/commandcode/restore/preview` |

| Flag | Value | Meaning |
|---|---|---|
| `--op` | string | Command Code operation ID; --op-id is an alias. |
| `--confirm-drift` | boolean | Explicitly allow replacing edits made after the snapshot. |
| `--preview` | boolean | Inspect restoration without mutating; exclusive with a commit fingerprint. |
| `--plan-fingerprint` | string | Optional versioned token from a matching preview; stale intent is refused without retry. |
| `--json` | boolean | Emit the result as JSON. |

JSON mode: `payload`.

- cmd restore is an alias. --client and --profile are rejected; the dedicated route binds the operation to Command Code.
- Older proxies without the dedicated route fail without a generic restore fallback. Redirects are refused.
- Preview preserves the operation and drift intent. A stale bound mutation requires a new explicit preview.

### `ocx claude config status`

Usage: `ocx claude config status [--json]`
Expand Down
16 changes: 16 additions & 0 deletions src/cli/capabilities-integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ export const INTEGRATION_CAPABILITIES: readonly Capability[] = [
json: "payload",
details: ["With --client, restore requires --profile; only Aside profiles accept that selector. No automatic drift confirmation or retry.","Preview preserves exact opId/profile/confirm-drift intent. Generic restore derives the client on the server; the returned plan does not embed opId or complete command input.","A refused or no-op preview is completed inspection, not applied work. A stale bound mutation exits5 with a fixed re-preview hint; no replacement token is automatically adopted."],
},
{
command: ["commandcode", "restore"],
usage: "ocx commandcode restore --op <opId> [--confirm-drift] [--preview | --plan-fingerprint <token>] [--json]",
summary: "Restore a Command Code rollback operation, retaining client ownership and drift checks.",
routes: [{ method: "POST", path: "/api/client-integrations/commandcode/restore" }, { method: "POST", path: "/api/client-integrations/commandcode/restore/preview" }],
flags: [
{ name: "--op", value: "string", summary: "Command Code operation ID; --op-id is an alias.", required: true },
{ name: "--confirm-drift", value: "boolean", summary: "Explicitly allow replacing edits made after the snapshot." },
{ name: "--preview", value: "boolean", summary: "Inspect restoration without mutating; exclusive with a commit fingerprint." },
{ name: "--plan-fingerprint", value: "string", summary: "Optional versioned token from a matching preview; stale intent is refused without retry." },
{ name: "--json", value: "boolean", summary: "Emit the result as JSON." },
],
mutates: true,
json: "payload",
details: ["cmd restore is an alias. --client and --profile are rejected; the dedicated route binds the operation to Command Code.", "Older proxies without the dedicated route fail without a generic restore fallback. Redirects are refused.", "Preview preserves the operation and drift intent. A stale bound mutation requires a new explicit preview."],
},
{
command: ["claude", "config", "status"],
usage: "ocx claude config status [--json]",
Expand Down
9 changes: 8 additions & 1 deletion src/cli/integration-preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** Preview and explicitly bound mutations use the existing management authority. */
import type { IntegrationMutationPlan, IntegrationPlanOperation } from "../integrations/mutation-plan";
import type { IntegrationClientId } from "../integrations/registry";
import { redactSecretString, redactUserPath } from "../lib/redact";
import {
CliUsageError, RuntimeApiError, printData, runtimeRequest, terminalSafeText,
Expand Down Expand Up @@ -186,9 +187,15 @@ function reportError(error: unknown): number {
return 1;
}

export async function handleIntegrationPreviewCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
export async function handleIntegrationPreviewCommand(argv: string[], deps: RuntimeApiDeps = {}, expectedClientId?: IntegrationClientId): Promise<number> {
try {
const intent = parseIntent(argv);
if (expectedClientId !== undefined && intent.operation === "restore") {
if (intent.client !== undefined || intent.profile !== undefined) throw new CliUsageError("client-specific restore does not accept --client or --profile", USAGE);
intent.client = expectedClientId;
intent.path = `${clientIntegrationPath(expectedClientId)}/restore${intent.preview ? "/preview" : ""}`;
intent.body.expectedClientId = expectedClientId;
}
const result = await runtimeRequest(intent.path, {
method: intent.preview || intent.operation === "restore" ? "POST" : "PUT",
redirect: "error", body: JSON.stringify(intent.body),
Expand Down
16 changes: 12 additions & 4 deletions src/cli/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type RuntimeApiDeps,
} from "./runtime-api";
import { clientIntegrationPath, validateAsideProfile } from "./integration-input";
import type { IntegrationClientId } from "../integrations/registry";

const CLAUDE_USAGE = `Usage:
ocx claude config [status] [--json]
Expand Down Expand Up @@ -215,7 +216,11 @@ function singleClientStatusLines(result: unknown): string[] {
export async function handleClientIntegrationCommand(
argv: string[],
deps: RuntimeApiDeps = {},
expectedClientId?: IntegrationClientId,
): Promise<number> {
if (expectedClientId !== undefined && argv.some(arg => ["--client", "--profile"].some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
return runCliAction(async () => { throw new CliUsageError("client-specific restore does not accept --client or --profile", CLIENT_USAGE); });
}
if ((argv[0] === "history" || argv[0] === "journal") && argv[1] === "remove") {
const { handleIntegrationJournalRemove } = await import("./integration-journal");
return handleIntegrationJournalRemove(argv.slice(2), deps);
Expand All @@ -227,7 +232,7 @@ export async function handleClientIntegrationCommand(
if (argv[0] === "preview" || argv.some(arg => ["--preview", "--plan-fingerprint", "--reasoning-default", "--clear-reasoning-defaults"]
.some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
const { handleIntegrationPreviewCommand } = await import("./integration-preview");
return handleIntegrationPreviewCommand(argv, deps);
return handleIntegrationPreviewCommand(argv, deps, expectedClientId);
}
return runCliAction(async () => {
const args = [...argv];
Expand Down Expand Up @@ -289,9 +294,12 @@ export async function handleClientIntegrationCommand(
if (client !== undefined && profile === undefined) throw new CliUsageError("restore --client requires --profile", CLIENT_USAGE);
rejectArgs(args, CLIENT_USAGE);
if (!opId) throw new CliUsageError("--op <opId> is required", CLIENT_USAGE);
const result = await runtimeRequest(profile === undefined ? "/api/client-integrations/restore" : `${clientIntegrationPath("aside", profile)}/restore`, {
const restorePath = expectedClientId !== undefined ? `${clientIntegrationPath(expectedClientId)}/restore`
: profile === undefined ? "/api/client-integrations/restore" : `${clientIntegrationPath("aside", profile)}/restore`;
const result = await runtimeRequest(restorePath, {
method: "POST",
body: JSON.stringify({ opId, confirmDrift }),
...(expectedClientId === undefined ? {} : { redirect: "error" as const }),
body: JSON.stringify({ opId, confirmDrift, ...(expectedClientId === undefined ? {} : { expectedClientId }) }),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}, deps);
printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? "Restored.")]);
return;
Expand Down Expand Up @@ -402,7 +410,7 @@ export async function handleCommandcodeCommand(argv: string[], deps: RuntimeApiD
}
const rest = verbIndex === -1 ? args : [...args.slice(0, verbIndex), ...args.slice(verbIndex + 1)];
const forwarded = action === "restore" ? [action, ...rest] : [action, ...rest, "--client", "commandcode"];
const code = await handleClientIntegrationCommand(forwarded, deps);
const code = await handleClientIntegrationCommand(forwarded, deps, action === "restore" ? "commandcode" : undefined);
if (code === 0 && (action === "enable" || action === "disable")) {
console.error("Command Code reads providers on startup.");
}
Expand Down
27 changes: 25 additions & 2 deletions src/server/management/integration-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,25 @@ function invalidClientResponse(ctx: ManagementContext): Response {
}, 400, ctx.req, ctx.config);
}

/** Bind an explicitly scoped restore before profile delegation or mutation. */
function restoreClientScopeResponse(ctx: ManagementContext, opId: string, expectedClientId: unknown, pathClientId?: IntegrationClientId): Response | null {
if (pathClientId !== undefined) {
if (expectedClientId !== undefined && (typeof expectedClientId !== "string" || !isIntegrationClientId(expectedClientId))) return invalidClientResponse(ctx);
if (expectedClientId !== undefined && expectedClientId !== pathClientId) return jsonResponse({ error: "restore identity does not match the client path", code: "integration_client_mismatch" }, 409, ctx.req, ctx.config);
expectedClientId = pathClientId;
}
if (expectedClientId === undefined) return null;
if (typeof expectedClientId !== "string" || !isIntegrationClientId(expectedClientId)) return invalidClientResponse(ctx);
try {
const operation = integrationStore().findOperation(opId);
if (!operation) return jsonResponse({ error: "integration operation not found", code: "integration_operation_not_found", opId }, 404, ctx.req, ctx.config);
if (operation.clientId !== expectedClientId) return jsonResponse({ error: "integration operation belongs to a different client", code: "integration_client_mismatch" }, 409, ctx.req, ctx.config);
return null;
} catch (error) {
return internalErrorResponse(error, ctx);
}
}

function isStringRecord(value: unknown): value is Record<string, string> {
return value !== null && typeof value === "object" && !Array.isArray(value)
&& Object.values(value).every(item => typeof item === "string");
Expand Down Expand Up @@ -775,7 +794,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
}
}

if (url.pathname === "/api/client-integrations/restore/preview") {
if (url.pathname === "/api/client-integrations/restore/preview" || url.pathname === "/api/client-integrations/commandcode/restore/preview") {
if (req.method !== "POST") return null;
const parsed = await readJsonBody(ctx);
if (parsed instanceof Response) return parsed;
Expand All @@ -786,6 +805,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
return jsonResponse({ error: "confirmDrift must be a boolean", code: "invalid_confirm_drift" }, 400, req, ctx.config);
}
const opId = parsed.opId.trim();
const scopeResponse = restoreClientScopeResponse(ctx, opId, parsed.expectedClientId, url.pathname === "/api/client-integrations/commandcode/restore/preview" ? "commandcode" : undefined);
if (scopeResponse) return scopeResponse;
try {
const store = integrationStore();
const operation = store.findOperation(opId);
Expand Down Expand Up @@ -819,7 +840,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
}
}

if (url.pathname === "/api/client-integrations/restore") {
if (url.pathname === "/api/client-integrations/restore" || url.pathname === "/api/client-integrations/commandcode/restore") {
if (req.method !== "POST") return null;
const parsed = await readJsonBody(ctx);
if (parsed instanceof Response) return parsed;
Expand All @@ -837,6 +858,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
}

const opId = parsed.opId.trim();
const scopeResponse = restoreClientScopeResponse(ctx, opId, parsed.expectedClientId, url.pathname === "/api/client-integrations/commandcode/restore" ? "commandcode" : undefined);
if (scopeResponse) return scopeResponse;
const confirmDrift = parsed.confirmDrift ?? false;
const restoreBinding = planBindingOf(parsed);
if (restoreBinding === "half") return halfBoundResponse(ctx);
Expand Down
2 changes: 2 additions & 0 deletions src/server/management/route-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
{ method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
{ method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false },
{ method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false },
{ method: "POST", path: "/api/client-integrations/commandcode/restore", module: "server/management/integration-routes", mutates: true },
{ method: "POST", path: "/api/client-integrations/commandcode/restore/preview", module: "server/management/integration-routes", mutates: false },
// server/management/lab-automation-routes
{ method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
{ method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
Expand Down
2 changes: 2 additions & 0 deletions structure/cli-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Management route declarations describe actual HTTP calls only; local Lab automat

## Integration inspection, recovery and maintenance

`src/cli/capabilities-integrations.ts` declares `commandcode restore` and its dedicated restore/preview routes separately from generic integration restore. Its generated operating reference preserves the client-bound command, supported flags, and refusal on older proxies without those routes.

`src/cli/integration-input.ts` shares pure profile paths/validation and owns exact optional Droid-map/fingerprint grammar. `src/cli/integration-preview.ts` handles explicit preview and new-option writes; `src/cli/integration-plan-dto.ts` validates value-free plans without runtime imports from GUI/planner/writer code. The original direct mutation bodies remain when new options are absent. Refused and no-op previews are completed observations; stale commits return a re-preview instruction without adopting a replacement token. The server owns coordinated binding and writes.

`src/cli/integration-journal.ts` addresses exact global/Aside/profile history with an explicit confirmation. It reports retired records separately from incomplete snapshot cleanup. `src/cli/integration-aside-sync.ts` retains the existing attested helper and its original dependency object rather than selecting a different transport through synthesized baseUrl. Empty, malformed and partial outcomes remain distinct. New fixed runtime requests reject redirects; this is not a global legacy transport rewrite.
Expand Down
Loading
Loading