mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
fix(cli): align generated consumers with private preview contract
This commit is contained in:
@@ -8796,6 +8796,11 @@ paths:
|
||||
tags:
|
||||
- Cli tools
|
||||
summary: "POST cli tools › apply"
|
||||
description: >-
|
||||
Submit the original toolId, apiKey, optional baseUrl/model and optional
|
||||
dryRun in the JSON body. Returned content is a redacted, non-cacheable
|
||||
preview, not an importable configuration. A non-dry-run request writes
|
||||
the original generated configuration; the container write guard remains active.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
@@ -8804,6 +8809,25 @@ paths:
|
||||
tags:
|
||||
- Cli tools
|
||||
summary: "GET cli tools › config"
|
||||
description: >-
|
||||
Returns redacted, non-cacheable previews. Send the configuration API key
|
||||
in x-omniroute-config-api-key, separate from management authentication.
|
||||
API keys in query strings are rejected. Preview content must not be
|
||||
copied into a credential-bearing configuration or submitted as an apply payload.
|
||||
parameters:
|
||||
- in: header
|
||||
name: x-omniroute-config-api-key
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
- in: query
|
||||
name: baseUrl
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: uri
|
||||
description: Absolute HTTP(S) URL without embedded username/password credentials.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
@@ -8811,6 +8835,11 @@ paths:
|
||||
tags:
|
||||
- Cli tools
|
||||
summary: "POST cli tools › config"
|
||||
description: >-
|
||||
Submit toolId, apiKey and optional baseUrl/model as JSON. Unknown fields
|
||||
are rejected. Returned content is a redacted, non-cacheable preview,
|
||||
not an importable configuration. To apply, submit the original inputs
|
||||
to POST /api/cli-tools/apply instead of replaying the preview content.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
|
||||
@@ -97,8 +97,11 @@ host answers **`422`** with `containerEphemeralTarget: true`, the safe error
|
||||
text and — for the tools with a host recipe (claude, codex, opencode, cline,
|
||||
kilo, continue) — a `hostSetupCommand` (e.g. `omniroute setup-opencode`) to run
|
||||
on the host instead; nothing is written. `dryRun: true` keeps working in container
|
||||
mode and returns the generated content + target path without touching disk, so
|
||||
you can preview from the dashboard and apply on the host. This behavior is
|
||||
mode and returns a redacted preview + target path without touching disk. Preview
|
||||
content is not a credential-bearing configuration to copy or import. Apply with
|
||||
the original tool/base URL/API key/model inputs on the host, or use the indicated
|
||||
host-side setup command. See [CLI configuration security](../security/CLI-CONFIGURATION.md)
|
||||
for the preview header and request contract. This behavior is
|
||||
intentional and regression-guarded by
|
||||
`tests/unit/api/cli-tools/apply-container-guard.test.ts` — never "fix" a 422
|
||||
by removing the guard.
|
||||
|
||||
@@ -398,31 +398,38 @@ curl https://localhost:20128/api/cli-tools/all-statuses \
|
||||
|
||||
POST cli tools › apply
|
||||
|
||||
Submit the original toolId, apiKey, optional baseUrl/model and optional dryRun in the JSON body. Returned content is a redacted, non-cacheable preview, not an importable configuration. A non-dry-run request writes the original generated configuration; the container write guard remains active.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/apply \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
-d '{"toolId":"claude","apiKey":"<configuration-api-key>","dryRun":true}'
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/config
|
||||
|
||||
GET cli tools › config
|
||||
|
||||
Returns redacted, non-cacheable previews. Send the configuration API key in x-omniroute-config-api-key, separate from management authentication. API keys in query strings are rejected. Preview content must not be copied into a credential-bearing configuration or submitted as an apply payload.
|
||||
|
||||
```bash
|
||||
curl https://localhost:20128/api/cli-tools/config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "x-omniroute-config-api-key: <configuration-api-key>"
|
||||
```
|
||||
|
||||
### POST /api/cli-tools/config
|
||||
|
||||
POST cli tools › config
|
||||
|
||||
Submit toolId, apiKey and optional baseUrl/model as JSON. Unknown fields are rejected. Returned content is a redacted, non-cacheable preview, not an importable configuration. To apply, submit the original inputs to POST /api/cli-tools/apply instead of replaying the preview content.
|
||||
|
||||
```bash
|
||||
curl -X POST https://localhost:20128/api/cli-tools/config \
|
||||
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
-d '{"toolId":"claude","apiKey":"<configuration-api-key>"}'
|
||||
```
|
||||
|
||||
### GET /api/cli-tools/deepseek-tui-settings
|
||||
|
||||
49
src/lib/agentSkills/apiOperationExample.ts
Normal file
49
src/lib/agentSkills/apiOperationExample.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { buildCliConfigurationExample } from "./cliConfigurationExample";
|
||||
|
||||
interface Operation {
|
||||
path: string;
|
||||
method: string;
|
||||
}
|
||||
|
||||
function dashboardExample({ path, method }: Operation): string[] {
|
||||
if (path === "/api/auth/login" && method === "POST") {
|
||||
return [
|
||||
`curl -X POST https://localhost:20128${path} \\`,
|
||||
' -H "Content-Type: application/json" \\',
|
||||
" -c cookie.jar \\",
|
||||
' -d \'{"password":"<management-password>"}\'',
|
||||
];
|
||||
}
|
||||
if (method === "GET") {
|
||||
return [`curl https://localhost:20128${path} \\`, " -b cookie.jar"];
|
||||
}
|
||||
const hasJsonBody = ["POST", "PUT", "PATCH"].includes(method);
|
||||
return [
|
||||
"CSRF_TOKEN=$(curl -s https://localhost:20128/api/auth/csrf -b cookie.jar | jq -r .token)",
|
||||
`curl -X ${method} https://localhost:20128${path} \\`,
|
||||
" -b cookie.jar \\",
|
||||
` -H "x-omniroute-csrf: $CSRF_TOKEN"${hasJsonBody ? " \\" : ""}`,
|
||||
...(hasJsonBody ? [' -H "Content-Type: application/json" \\', " -d '{}'"] : []),
|
||||
];
|
||||
}
|
||||
|
||||
function bearerExample({ path, method }: Operation): string[] {
|
||||
const curlMethod = method === "GET" ? "" : `-X ${method} `;
|
||||
const hasJsonBody = ["POST", "PUT", "PATCH"].includes(method);
|
||||
return [
|
||||
`curl ${curlMethod}https://localhost:20128${path} \\`,
|
||||
` -H "Authorization: Bearer $OMNIROUTE_TOKEN"${hasJsonBody ? " \\" : ""}`,
|
||||
...(hasJsonBody ? [' -H "Content-Type: application/json" \\', " -d '{}'"] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/** Preserve the authentication model while supplying a valid configuration example. */
|
||||
export function buildApiOperationExample(
|
||||
operation: Operation,
|
||||
dashboardSession: boolean
|
||||
): string[] {
|
||||
return (
|
||||
buildCliConfigurationExample(operation) ??
|
||||
(dashboardSession ? dashboardExample(operation) : bearerExample(operation))
|
||||
);
|
||||
}
|
||||
27
src/lib/agentSkills/cliConfigurationExample.ts
Normal file
27
src/lib/agentSkills/cliConfigurationExample.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/** Examples for the configuration API keep credentials out of URLs and apply dry-run-only. */
|
||||
export function buildCliConfigurationExample(operation: {
|
||||
path: string;
|
||||
method: string;
|
||||
}): string[] | undefined {
|
||||
const { path, method } = operation;
|
||||
if (path !== "/api/cli-tools/config" && path !== "/api/cli-tools/apply") return;
|
||||
if (method === "GET" && path === "/api/cli-tools/config") {
|
||||
return [
|
||||
`curl https://localhost:20128${path} \\`,
|
||||
' -H "Authorization: Bearer $OMNIROUTE_TOKEN" \\',
|
||||
' -H "x-omniroute-config-api-key: <configuration-api-key>"',
|
||||
];
|
||||
}
|
||||
if (method !== "POST") return;
|
||||
const body = {
|
||||
toolId: "claude",
|
||||
apiKey: "<configuration-api-key>",
|
||||
...(path.endsWith("/apply") ? { dryRun: true } : {}),
|
||||
};
|
||||
return [
|
||||
`curl -X POST https://localhost:20128${path} \\`,
|
||||
' -H "Authorization: Bearer $OMNIROUTE_TOKEN" \\',
|
||||
' -H "Content-Type: application/json" \\',
|
||||
` -d '${JSON.stringify(body)}'`,
|
||||
];
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import path from "node:path";
|
||||
import { getCatalog, refreshCatalog } from "./catalog";
|
||||
import { parseOpenapi } from "./openapiParser";
|
||||
import { parseCliRegistry } from "./cliRegistryParser";
|
||||
import { buildApiOperationExample } from "./apiOperationExample";
|
||||
import type { AgentSkill, GeneratorOptions, GeneratorReport } from "./types";
|
||||
import type { ParsedOpenapi } from "./openapiParser";
|
||||
import type { ParsedCliRegistry } from "./cliRegistryParser";
|
||||
@@ -116,39 +117,7 @@ function buildApiBody(skill: AgentSkill, sources: BuildSources): string {
|
||||
// Minimal curl example. Only omni-auth establishes and consumes a dashboard
|
||||
// session; generic API skills use independently usable Bearer examples.
|
||||
lines.push("```bash");
|
||||
if (usesDashboardSession && op.path === "/api/auth/login" && op.method === "POST") {
|
||||
lines.push(`curl -X POST https://localhost:20128${op.path} \\`);
|
||||
lines.push(' -H "Content-Type: application/json" \\');
|
||||
lines.push(" -c cookie.jar \\");
|
||||
lines.push(' -d \'{"password":"<management-password>"}\'');
|
||||
} else if (usesDashboardSession) {
|
||||
const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `;
|
||||
if (op.method === "GET") {
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(" -b cookie.jar");
|
||||
} else {
|
||||
lines.push(
|
||||
"CSRF_TOKEN=$(curl -s https://localhost:20128/api/auth/csrf -b cookie.jar | jq -r .token)"
|
||||
);
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(" -b cookie.jar \\");
|
||||
const hasJsonBody = ["POST", "PUT", "PATCH"].includes(op.method);
|
||||
lines.push(` -H "x-omniroute-csrf: $CSRF_TOKEN"${hasJsonBody ? " \\" : ""}`);
|
||||
if (hasJsonBody) {
|
||||
lines.push(' -H "Content-Type: application/json" \\');
|
||||
lines.push(" -d '{}'");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `;
|
||||
const hasJsonBody = ["POST", "PUT", "PATCH"].includes(op.method);
|
||||
lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`);
|
||||
lines.push(` -H "Authorization: Bearer $OMNIROUTE_TOKEN"${hasJsonBody ? " \\" : ""}`);
|
||||
if (hasJsonBody) {
|
||||
lines.push(' -H "Content-Type: application/json" \\');
|
||||
lines.push(" -d '{}'");
|
||||
}
|
||||
}
|
||||
lines.push(...buildApiOperationExample(op, usesDashboardSession));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
49
tests/unit/cli-configuration-consumer-contract.test.ts
Normal file
49
tests/unit/cli-configuration-consumer-contract.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { buildSkillMarkdown } from "../../src/lib/agentSkills/generator.ts";
|
||||
import { parseOpenapi } from "../../src/lib/agentSkills/openapiParser.ts";
|
||||
import { buildApiOperationExample } from "../../src/lib/agentSkills/apiOperationExample.ts";
|
||||
|
||||
test("generated CLI skill uses header previews and original-input dry-run application", () => {
|
||||
const { body } = buildSkillMarkdown("omni-cli-tools", {
|
||||
openapi: parseOpenapi(),
|
||||
cliRegistry: { commands: new Map(), families: new Map() },
|
||||
});
|
||||
const section = (method: string, path: string) =>
|
||||
body.split(`### ${method} ${path}\n`)[1]?.split("\n### ")[0] || "";
|
||||
const preview = section("GET", "/api/cli-tools/config");
|
||||
assert.ok(preview.includes("x-omniroute-config-api-key: <configuration-api-key>"));
|
||||
assert.ok(!preview.includes("?apiKey="));
|
||||
assert.ok(preview.includes("must not be"));
|
||||
for (const endpoint of ["config", "apply"]) {
|
||||
const example = section("POST", `/api/cli-tools/${endpoint}`);
|
||||
const payload = JSON.parse(example.match(/-d '(.*)'/)?.[1] || "{}");
|
||||
assert.equal(payload.toolId, "claude");
|
||||
assert.equal(payload.apiKey, "<configuration-api-key>");
|
||||
assert.equal(payload.content, undefined);
|
||||
assert.equal(payload.dryRun, endpoint === "apply" ? true : undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("configuration examples preserve unrelated bearer and dashboard-session authentication", () => {
|
||||
assert.deepEqual(buildApiOperationExample({ path: "/api/models", method: "GET" }, false), [
|
||||
"curl https://localhost:20128/api/models \\",
|
||||
' -H "Authorization: Bearer $OMNIROUTE_TOKEN"',
|
||||
]);
|
||||
const login = buildApiOperationExample({ path: "/api/auth/login", method: "POST" }, true).join(
|
||||
"\n"
|
||||
);
|
||||
assert.ok(login.includes("-c cookie.jar"));
|
||||
assert.ok(!login.includes("OMNIROUTE_TOKEN"));
|
||||
const mutation = buildApiOperationExample(
|
||||
{ path: "/api/auth/logout", method: "POST" },
|
||||
true
|
||||
).join("\n");
|
||||
assert.ok(mutation.includes("-b cookie.jar"));
|
||||
assert.ok(mutation.includes("x-omniroute-csrf: $CSRF_TOKEN"));
|
||||
const read = buildApiOperationExample({ path: "/api/auth/status", method: "GET" }, true).join(
|
||||
"\n"
|
||||
);
|
||||
assert.ok(read.includes("-b cookie.jar"));
|
||||
assert.ok(!read.includes("CSRF_TOKEN"));
|
||||
});
|
||||
@@ -61,20 +61,27 @@ vi.mock("@/shared/components", async () => {
|
||||
const React = await import("react");
|
||||
return {
|
||||
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
ModelSelectModal: () => null,
|
||||
ManualConfigModal: ({ isOpen, title }: { isOpen: boolean; title?: string }) =>
|
||||
isOpen ? <div data-testid="manual-config-modal">{title}</div> : null,
|
||||
ManualConfigModal: ({
|
||||
isOpen,
|
||||
title,
|
||||
configs,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
configs?: Array<{ filename: string; content: string }>;
|
||||
}) =>
|
||||
isOpen ? (
|
||||
<div data-testid="manual-config-modal">
|
||||
{title}
|
||||
<pre data-testid="manual-config-content">{JSON.stringify(configs)}</pre>
|
||||
</div>
|
||||
) : null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -112,9 +119,8 @@ afterEach(() => {
|
||||
|
||||
// ── Import under test (after mocks) ───────────────────────────────────────────
|
||||
|
||||
const { default: ClaudeToolCard } = await import(
|
||||
"@/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard"
|
||||
);
|
||||
const { default: ClaudeToolCard } =
|
||||
await import("@/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard");
|
||||
|
||||
async function renderExpanded() {
|
||||
const container = document.createElement("div");
|
||||
@@ -172,5 +178,12 @@ describe("ClaudeToolCard — manual-config CTA when CLI is not detected", () =>
|
||||
});
|
||||
|
||||
expect(container.querySelector("[data-testid='manual-config-modal']")).not.toBeNull();
|
||||
const payload = container.querySelector("[data-testid='manual-config-content']")?.textContent;
|
||||
const configs = JSON.parse(payload || "[]");
|
||||
expect(JSON.parse(configs[0].content).env.ANTHROPIC_BASE_URL).toBe("http://localhost:20128");
|
||||
expect(payload).not.toContain("[redacted]");
|
||||
// The manual-copy consumer builds its own config; it never imports an API preview.
|
||||
const calls = vi.mocked(globalThis.fetch).mock.calls.map(([url]) => String(url));
|
||||
expect(calls.some((url) => /\/api\/cli-tools\/(?:config|apply)(?:\?|$)/.test(url))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user