llm-cli-gateway 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +7 -5
  3. package/dist/acp/event-normalizer.d.ts +42 -0
  4. package/dist/acp/event-normalizer.js +71 -0
  5. package/dist/acp/flight-redaction.d.ts +25 -0
  6. package/dist/acp/flight-redaction.js +40 -0
  7. package/dist/acp/host-services.d.ts +16 -0
  8. package/dist/acp/host-services.js +29 -0
  9. package/dist/acp/permission-bridge.d.ts +15 -0
  10. package/dist/acp/permission-bridge.js +90 -0
  11. package/dist/acp/process-manager.js +7 -1
  12. package/dist/acp/provider-registry.d.ts +1 -1
  13. package/dist/acp/provider-registry.js +13 -0
  14. package/dist/acp/runtime.d.ts +35 -0
  15. package/dist/acp/runtime.js +125 -0
  16. package/dist/acp/session-map.d.ts +42 -0
  17. package/dist/acp/session-map.js +67 -0
  18. package/dist/acp/smoke-harness.d.ts +28 -0
  19. package/dist/acp/smoke-harness.js +90 -0
  20. package/dist/api-http.d.ts +18 -0
  21. package/dist/api-http.js +122 -0
  22. package/dist/api-provider.d.ts +83 -0
  23. package/dist/api-provider.js +258 -0
  24. package/dist/api-request.d.ts +30 -0
  25. package/dist/api-request.js +51 -0
  26. package/dist/approval-manager.d.ts +1 -1
  27. package/dist/approval-manager.js +6 -7
  28. package/dist/async-job-manager.d.ts +19 -4
  29. package/dist/async-job-manager.js +211 -35
  30. package/dist/claude-mcp-config.d.ts +2 -2
  31. package/dist/claude-mcp-config.js +42 -52
  32. package/dist/cli-updater.js +16 -1
  33. package/dist/config.d.ts +20 -0
  34. package/dist/config.js +93 -35
  35. package/dist/doctor.d.ts +1 -1
  36. package/dist/flight-recorder.d.ts +1 -0
  37. package/dist/flight-recorder.js +11 -0
  38. package/dist/index.d.ts +56 -5
  39. package/dist/index.js +639 -38
  40. package/dist/job-store.d.ts +15 -0
  41. package/dist/job-store.js +39 -5
  42. package/dist/mcp-registry.d.ts +17 -0
  43. package/dist/mcp-registry.js +5 -0
  44. package/dist/metrics.js +7 -2
  45. package/dist/model-registry.js +11 -0
  46. package/dist/prompt-parts.d.ts +6 -6
  47. package/dist/provider-login-guidance.js +21 -0
  48. package/dist/provider-status.js +4 -1
  49. package/dist/provider-tool-capabilities.d.ts +4 -3
  50. package/dist/provider-tool-capabilities.js +93 -6
  51. package/dist/request-helpers.d.ts +6 -6
  52. package/dist/request-helpers.js +1 -4
  53. package/dist/session-manager-pg.js +2 -9
  54. package/dist/session-manager.d.ts +9 -4
  55. package/dist/session-manager.js +13 -4
  56. package/dist/upstream-contracts.js +112 -2
  57. package/dist/validation-normalizer.d.ts +2 -2
  58. package/dist/validation-orchestrator.d.ts +2 -0
  59. package/dist/validation-orchestrator.js +28 -7
  60. package/dist/validation-tools.d.ts +61 -0
  61. package/dist/validation-tools.js +36 -21
  62. package/migrations/005_provider_type_open_api_names.sql +28 -0
  63. package/npm-shrinkwrap.json +4 -3
  64. package/package.json +12 -9
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [2.11.0] - 2026-06-18: API providers, Devin, ACP runtime, and a strip-at-publish internal-MCP boundary
8
+
9
+ The accumulated work since 2.10.0: a generic HTTP API-provider surface, a sixth
10
+ CLI provider (Devin), the Phase B ACP runtime (gated, dormant by default), and a
11
+ release-time strip that keeps gateway-internal MCP server names out of the
12
+ published npm artifact. Default local-stdio behaviour is unchanged; the new
13
+ API/ACP surfaces are opt-in. Every change in this release was landed via PR with
14
+ green CI and an adversarial multi-LLM review.
15
+
16
+ ### Added
17
+
18
+ - **API providers (Slices 0–5).** An `ApiProvider` interface with OpenAI /
19
+ Anthropic / xAI adapters, a generic `api_<name>_request[_async]` tool surface,
20
+ an `HttpJobRunner` that treats HTTP calls as first-class async jobs, API-provider
21
+ discovery/catalog, and the ability to use API providers as validation
22
+ reviewers/judges. Open-string provider-identity widening (`kind:"api"`).
23
+ - **Devin (Slices D0–D1).** Devin as a sixth gateway provider —
24
+ `devin_request[_async]` — with permission-mode aliases corrected against the
25
+ real CLI and a passing native-ACP smoke (third ACP runtime pilot).
26
+ - **ACP runtime, Phase B (Slices B1–B7).** Read-only smoke harness,
27
+ deny-by-default HostServices boundary, permission bridge to ApprovalManager,
28
+ session map, session/update event normalizer, flight-recorder redaction, and
29
+ gated runtime pilots (Mistral + Grok + Devin) for first live ACP prompt routing.
30
+ Dormant by default.
31
+
32
+ ### Changed
33
+
34
+ - **Strip internal MCP names from the published artifact.** Gateway-internal MCP
35
+ server names and host commands are centralised in `src/mcp-registry.ts` and
36
+ stripped at release time, so the published tarball contains zero internal names.
37
+ Enforced by an explicit, non-bypassable tarball token-guard
38
+ (`scripts/verify-no-internal-mcp.mjs`). `ClaudeMcpServerName` widens to `string`
39
+ and the request `mcpServers` schemas accept arbitrary names; no implicit default
40
+ server is injected.
41
+ - **Vibe `permissionMode` accepts any `--agent` name.** Replaces the closed
42
+ agent-mode enum (which had stale `chat`/`explore`/`lean` values) with an open
43
+ string — Vibe owns its agent registry (builtins plus install-gated and custom
44
+ agents), so the gateway passes the name through and lets Vibe validate it.
45
+ - **Gemini `mcpServers` accepted for approval tracking.** `gemini_request` no
46
+ longer rejects non-empty `mcpServers`; the names feed the approval policy but are
47
+ never passed to the Antigravity CLI (which manages its own MCP configuration).
48
+
49
+ ### Security / supply chain
50
+
51
+ - `hono` pinned to `^4.12.25` via `overrides` (clears the advisories on 4.12.22),
52
+ with a hono-floor tripwire in the release security audit.
53
+
54
+ ### Deps
55
+
56
+ - Dev-dependency bumps (keeps `type-is@2.1.0` out via the `body-parser` floor) and
57
+ a `github/codeql-action` group bump.
58
+
7
59
  ## [2.10.0] - 2026-06-15: Per-principal isolation on the request handlers
8
60
 
9
61
  A follow-up to the 2.9.0 per-principal isolation (F3): the ownership model was
package/README.md CHANGED
@@ -278,8 +278,10 @@ Vibe-specific notes:
278
278
  `VIBE_MODELS`, injects `VIBE_ACTIVE_MODEL` only when a model is explicitly
279
279
  requested or Vibe config needs recovery, and retries once after a
280
280
  model-not-found failure with refreshed discovery.
281
- - **`permissionMode` accepts** `default | plan | accept-edits | auto-approve |
282
- chat | explore | lean` and emits `--agent <mode>`. The gateway's
281
+ - **`permissionMode` is the Vibe `--agent` name.** Builtins are
282
+ `default | plan | accept-edits | auto-approve`; Vibe also accepts install-gated
283
+ builtins (e.g. `lean`) and custom agents from `~/.vibe/agents`, so any name is
284
+ passed through and Vibe validates availability. The gateway's
283
285
  programmatic-mode default is `auto-approve`; pick a stricter mode
284
286
  explicitly if you need approval gates.
285
287
  - **`allowedTools` is allow-list only** — the gateway emits one
@@ -391,7 +393,7 @@ Execute a Claude Code request with optional session management.
391
393
  - `excludeDynamicSystemPromptSections` (boolean, optional): Trim dynamic system prompt sections
392
394
  - `approvalStrategy` (string, optional): `"legacy"` (default) or `"mcp_managed"`
393
395
  - `approvalPolicy` (string, optional): `"strict"`, `"balanced"`, or `"permissive"`
394
- - `mcpServers` (string[], optional): Claude MCP servers to expose (default: `["sqry","exa","ref_tools"]`; `"trstr"` available as opt-in)
396
+ - `mcpServers` (string[], optional): Names of MCP servers to expose to Claude (default: none). The gateway resolves each name to a launch command from its local registry / Codex MCP config; unknown names are reported as unavailable. Configure the servers your deployment uses in the gateway environment.
395
397
  - `strictMcpConfig` (boolean, optional): Require Claude to use only supplied MCP config, default: true (request fails if any requested server is unavailable)
396
398
  - `optimizePrompt` (boolean, optional): Optimize prompt for token efficiency (44% reduction), default: false
397
399
  - `optimizeResponse` (boolean, optional): Optimize response for token efficiency (37% reduction), default: false
@@ -825,7 +827,7 @@ consumes = ["OUT:mcp-reconnected"]
825
827
  Run a Mistral Vibe agentic coding request. Like `grok_request` in shape, but with Vibe's specific surface:
826
828
 
827
829
  - `model` (string, optional): Vibe model alias (for example `mistral-medium-3.5` or `latest`). The resolved value is injected via the `VIBE_ACTIVE_MODEL` environment variable; omit it to let the gateway discover Vibe config and avoid stale hardcoded defaults.
828
- - `permissionMode`: `default | plan | accept-edits | auto-approve | chat | explore | lean` — emitted as `--agent <mode>`. Defaults to `auto-approve` in programmatic mode.
830
+ - `permissionMode`: the Vibe `--agent` name — builtins `default | plan | accept-edits | auto-approve`, or any install-gated/custom agent. Emitted as `--agent <name>`. Defaults to `auto-approve` in programmatic mode.
829
831
  - `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry (allow-list only).
830
832
  - `disallowedTools` (string[], optional): Accepted for parity with the other providers; ignored at the CLI boundary with a logged warning.
831
833
  - `outputFormat` (string, optional): Vibe 2.x values are `"text"`, `"json"`, or `"streaming"`; legacy aliases `"plain"` and `"stream-json"` are accepted and normalized before spawn.
@@ -861,7 +863,7 @@ Async request tools accept the same approval strategy fields as their sync varia
861
863
 
862
864
  - `approvalStrategy`: `"legacy"` (default) or `"mcp_managed"`
863
865
  - `approvalPolicy`: `"strict"|"balanced"|"permissive"` override
864
- - `mcpServers`: Requested MCP servers (`sqry`, `exa`, `ref_tools`, `trstr`)
866
+ - `mcpServers`: Names of requested MCP servers, resolved against the gateway's local registry / Codex MCP config
865
867
  - `claude_request_async` also supports `strictMcpConfig` and fails fast when requested servers are unavailable
866
868
 
867
869
  ##### `llm_job_status`
@@ -0,0 +1,42 @@
1
+ import type { ContentBlock, SessionUpdateNotification } from "./types.js";
2
+ export type AcpProgressEvent = {
3
+ readonly kind: "agent_message";
4
+ readonly text: string;
5
+ } | {
6
+ readonly kind: "agent_thought";
7
+ readonly text: string;
8
+ } | {
9
+ readonly kind: "user_message";
10
+ readonly text: string;
11
+ } | {
12
+ readonly kind: "tool_call";
13
+ readonly toolCallId: string;
14
+ readonly title: string;
15
+ readonly status?: string;
16
+ readonly toolKind?: string;
17
+ } | {
18
+ readonly kind: "tool_update";
19
+ readonly toolCallId: string;
20
+ readonly status?: string;
21
+ readonly title?: string;
22
+ } | {
23
+ readonly kind: "plan";
24
+ readonly entryCount: number;
25
+ } | {
26
+ readonly kind: "mode";
27
+ readonly currentModeId: string;
28
+ } | {
29
+ readonly kind: "usage";
30
+ readonly size: number;
31
+ readonly used: number;
32
+ } | {
33
+ readonly kind: "other";
34
+ readonly sessionUpdate: string;
35
+ };
36
+ export declare function summarizeContentBlock(block: ContentBlock): string;
37
+ export declare function normalizeSessionUpdate(notification: SessionUpdateNotification): AcpProgressEvent;
38
+ export declare class AcpEventNormalizer {
39
+ private text;
40
+ handle(notification: SessionUpdateNotification): AcpProgressEvent;
41
+ get finalText(): string;
42
+ }
@@ -0,0 +1,71 @@
1
+ function str(value) {
2
+ return typeof value === "string" ? value : undefined;
3
+ }
4
+ export function summarizeContentBlock(block) {
5
+ if (block.type === "text") {
6
+ return str(block.text) ?? "";
7
+ }
8
+ return `[${block.type}]`;
9
+ }
10
+ function chunkText(update) {
11
+ const content = update.content;
12
+ if (content && typeof content === "object") {
13
+ return summarizeContentBlock(content);
14
+ }
15
+ return "";
16
+ }
17
+ export function normalizeSessionUpdate(notification) {
18
+ const update = notification.update;
19
+ const variant = str(update.sessionUpdate) ?? "";
20
+ switch (variant) {
21
+ case "agent_message_chunk":
22
+ return { kind: "agent_message", text: chunkText(update) };
23
+ case "agent_thought_chunk":
24
+ return { kind: "agent_thought", text: chunkText(update) };
25
+ case "user_message_chunk":
26
+ return { kind: "user_message", text: chunkText(update) };
27
+ case "tool_call":
28
+ return {
29
+ kind: "tool_call",
30
+ toolCallId: str(update.toolCallId) ?? "",
31
+ title: str(update.title) ?? "",
32
+ status: str(update.status),
33
+ toolKind: str(update.kind),
34
+ };
35
+ case "tool_call_update":
36
+ return {
37
+ kind: "tool_update",
38
+ toolCallId: str(update.toolCallId) ?? "",
39
+ status: str(update.status),
40
+ title: str(update.title),
41
+ };
42
+ case "plan":
43
+ return {
44
+ kind: "plan",
45
+ entryCount: Array.isArray(update.entries) ? update.entries.length : 0,
46
+ };
47
+ case "current_mode_update":
48
+ return { kind: "mode", currentModeId: str(update.currentModeId) ?? "" };
49
+ case "usage_update":
50
+ return {
51
+ kind: "usage",
52
+ size: typeof update.size === "number" ? update.size : 0,
53
+ used: typeof update.used === "number" ? update.used : 0,
54
+ };
55
+ default:
56
+ return { kind: "other", sessionUpdate: variant };
57
+ }
58
+ }
59
+ export class AcpEventNormalizer {
60
+ text = "";
61
+ handle(notification) {
62
+ const event = normalizeSessionUpdate(notification);
63
+ if (event.kind === "agent_message") {
64
+ this.text += event.text;
65
+ }
66
+ return event;
67
+ }
68
+ get finalText() {
69
+ return this.text;
70
+ }
71
+ }
@@ -0,0 +1,25 @@
1
+ import type { ContentBlock } from "./types.js";
2
+ import type { FlightLogResult, FlightLogStart } from "../flight-recorder.js";
3
+ import type { CliType } from "../session-manager.js";
4
+ export declare function summarizeAcpPromptForFlight(prompt: ReadonlyArray<ContentBlock>): string;
5
+ export declare function summarizeAcpResponseForFlight(responseText: string): string;
6
+ export declare function redactAcpTextForFlight(text: string): string;
7
+ export interface AcpFlightStartParams {
8
+ readonly correlationId: string;
9
+ readonly provider: CliType;
10
+ readonly model: string;
11
+ readonly prompt: ReadonlyArray<ContentBlock>;
12
+ readonly gatewaySessionId?: string;
13
+ readonly asyncJobId?: string;
14
+ }
15
+ export declare function buildAcpFlightStart(params: AcpFlightStartParams): FlightLogStart;
16
+ export interface AcpFlightResultParams {
17
+ readonly responseText: string;
18
+ readonly durationMs: number;
19
+ readonly status: "completed" | "failed";
20
+ readonly exitCode: number;
21
+ readonly inputTokens?: number;
22
+ readonly outputTokens?: number;
23
+ readonly errorMessage?: string;
24
+ }
25
+ export declare function buildAcpFlightResult(params: AcpFlightResultParams): FlightLogResult;
@@ -0,0 +1,40 @@
1
+ import { redactAcpMessage } from "./errors.js";
2
+ import { isGatewaySessionId } from "./session-map.js";
3
+ import { redactSecrets } from "../secret-redaction.js";
4
+ export function summarizeAcpPromptForFlight(prompt) {
5
+ const n = prompt.length;
6
+ return `[acp prompt: ${n} content block${n === 1 ? "" : "s"}]`;
7
+ }
8
+ export function summarizeAcpResponseForFlight(responseText) {
9
+ return `[acp response: ${responseText.length} char${responseText.length === 1 ? "" : "s"}]`;
10
+ }
11
+ export function redactAcpTextForFlight(text) {
12
+ return redactSecrets(redactAcpMessage(text));
13
+ }
14
+ export function buildAcpFlightStart(params) {
15
+ const sessionId = params.gatewaySessionId !== undefined && isGatewaySessionId(params.gatewaySessionId)
16
+ ? params.gatewaySessionId
17
+ : undefined;
18
+ return {
19
+ correlationId: params.correlationId,
20
+ cli: params.provider,
21
+ model: params.model,
22
+ prompt: summarizeAcpPromptForFlight(params.prompt),
23
+ sessionId,
24
+ asyncJobId: params.asyncJobId,
25
+ };
26
+ }
27
+ export function buildAcpFlightResult(params) {
28
+ return {
29
+ response: summarizeAcpResponseForFlight(params.responseText),
30
+ durationMs: params.durationMs,
31
+ status: params.status,
32
+ exitCode: params.exitCode,
33
+ retryCount: 0,
34
+ circuitBreakerState: "closed",
35
+ optimizationApplied: false,
36
+ inputTokens: params.inputTokens,
37
+ outputTokens: params.outputTokens,
38
+ errorMessage: params.errorMessage !== undefined ? redactAcpTextForFlight(params.errorMessage) : undefined,
39
+ };
40
+ }
@@ -0,0 +1,16 @@
1
+ import type { HostCallbackContext, HostServices } from "./client.js";
2
+ import type { ReadTextFileRequest, ReadTextFileResponse, RequestPermissionRequest, RequestPermissionResponse, WriteTextFileRequest, WriteTextFileResponse } from "./types.js";
3
+ import type { Logger } from "../logger.js";
4
+ export type AcpPermissionDecider = (request: RequestPermissionRequest, context: HostCallbackContext) => Promise<RequestPermissionResponse>;
5
+ export interface GatewayHostServicesOptions {
6
+ readonly logger?: Logger;
7
+ readonly permissionDecider?: AcpPermissionDecider;
8
+ }
9
+ export declare class GatewayHostServices implements HostServices {
10
+ private readonly logger;
11
+ private readonly permissionDecider?;
12
+ constructor(options?: GatewayHostServicesOptions);
13
+ readTextFile(_request: ReadTextFileRequest, context: HostCallbackContext): Promise<ReadTextFileResponse>;
14
+ writeTextFile(_request: WriteTextFileRequest, context: HostCallbackContext): Promise<WriteTextFileResponse>;
15
+ requestPermission(request: RequestPermissionRequest, context: HostCallbackContext): Promise<RequestPermissionResponse>;
16
+ }
@@ -0,0 +1,29 @@
1
+ import { AcpPermissionDeniedError } from "./errors.js";
2
+ import { noopLogger } from "../logger.js";
3
+ export class GatewayHostServices {
4
+ logger;
5
+ permissionDecider;
6
+ constructor(options = {}) {
7
+ this.logger = options.logger ?? noopLogger;
8
+ this.permissionDecider = options.permissionDecider;
9
+ }
10
+ async readTextFile(_request, context) {
11
+ this.logger.info("acp.host.read_denied", { provider: context.provider });
12
+ throw new AcpPermissionDeniedError(context.provider, "filesystem read host service is disabled", { provider: context.provider, debug: { method: context.method } });
13
+ }
14
+ async writeTextFile(_request, context) {
15
+ this.logger.info("acp.host.write_denied", { provider: context.provider });
16
+ throw new AcpPermissionDeniedError(context.provider, "filesystem write host service is disabled", { provider: context.provider, debug: { method: context.method } });
17
+ }
18
+ async requestPermission(request, context) {
19
+ if (this.permissionDecider) {
20
+ return this.permissionDecider(request, context);
21
+ }
22
+ this.logger.info("acp.permission.denied", {
23
+ provider: context.provider,
24
+ reason: "no_approval_bridge",
25
+ optionCount: request.options.length,
26
+ });
27
+ return { outcome: { outcome: "cancelled" } };
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ import type { HostCallbackContext } from "./client.js";
2
+ import type { RequestPermissionRequest, RequestPermissionResponse } from "./types.js";
3
+ import { ApprovalManager, type ApprovalCli, type ApprovalPolicy } from "../approval-manager.js";
4
+ import type { Logger } from "../logger.js";
5
+ export type AcpPermissionCategory = "read" | "write" | "execute" | "other";
6
+ export interface AcpPermissionBridgeDeps {
7
+ readonly approvalManager: ApprovalManager;
8
+ readonly provider: ApprovalCli;
9
+ readonly allowWrite?: boolean;
10
+ readonly allowTerminal?: boolean;
11
+ readonly policy?: ApprovalPolicy;
12
+ readonly logger?: Logger;
13
+ }
14
+ export declare function categorizeToolCall(toolCall: Record<string, unknown>): AcpPermissionCategory;
15
+ export declare function createAcpPermissionDecider(deps: AcpPermissionBridgeDeps): (request: RequestPermissionRequest, context: HostCallbackContext) => Promise<RequestPermissionResponse>;
@@ -0,0 +1,90 @@
1
+ import { noopLogger } from "../logger.js";
2
+ const WRITE_KINDS = new Set(["edit", "delete", "move"]);
3
+ const EXECUTE_KINDS = new Set(["execute"]);
4
+ const READ_KINDS = new Set(["read", "search", "think"]);
5
+ export function categorizeToolCall(toolCall) {
6
+ const kind = typeof toolCall.kind === "string" ? toolCall.kind.toLowerCase() : "";
7
+ if (WRITE_KINDS.has(kind))
8
+ return "write";
9
+ if (EXECUTE_KINDS.has(kind))
10
+ return "execute";
11
+ if (READ_KINDS.has(kind))
12
+ return "read";
13
+ return "other";
14
+ }
15
+ function isAllowOption(option) {
16
+ const kind = typeof option.kind === "string" ? option.kind.toLowerCase() : "";
17
+ return kind.startsWith("allow");
18
+ }
19
+ const CANCELLED = { outcome: { outcome: "cancelled" } };
20
+ export function createAcpPermissionDecider(deps) {
21
+ const logger = deps.logger ?? noopLogger;
22
+ return async (request, _context) => {
23
+ const category = categorizeToolCall(request.toolCall);
24
+ if (category === "write" && deps.allowWrite !== true) {
25
+ logger.info("acp.permission.denied", {
26
+ provider: deps.provider,
27
+ category,
28
+ reason: "write_disabled",
29
+ });
30
+ return CANCELLED;
31
+ }
32
+ if (category === "execute" && deps.allowTerminal !== true) {
33
+ logger.info("acp.permission.denied", {
34
+ provider: deps.provider,
35
+ category,
36
+ reason: "terminal_disabled",
37
+ });
38
+ return CANCELLED;
39
+ }
40
+ if (category === "other") {
41
+ logger.info("acp.permission.denied", {
42
+ provider: deps.provider,
43
+ category,
44
+ reason: "unknown_kind_denied",
45
+ });
46
+ return CANCELLED;
47
+ }
48
+ let approved;
49
+ try {
50
+ const record = deps.approvalManager.decide({
51
+ cli: deps.provider,
52
+ operation: `acp_permission:${category}`,
53
+ prompt: `ACP permission request from ${deps.provider}`,
54
+ bypassRequested: false,
55
+ fullAuto: false,
56
+ requestedMcpServers: [],
57
+ policy: deps.policy,
58
+ metadata: { acp: true, category, optionCount: request.options.length },
59
+ });
60
+ approved = record.status === "approved";
61
+ }
62
+ catch (err) {
63
+ logger.error("acp.permission.decide_error", {
64
+ provider: deps.provider,
65
+ category,
66
+ errorClass: err instanceof Error ? err.name : "unknown",
67
+ });
68
+ return CANCELLED;
69
+ }
70
+ if (!approved) {
71
+ logger.info("acp.permission.denied", {
72
+ provider: deps.provider,
73
+ category,
74
+ reason: "approval_denied",
75
+ });
76
+ return CANCELLED;
77
+ }
78
+ const allow = request.options.find(isAllowOption);
79
+ if (!allow) {
80
+ logger.info("acp.permission.denied", {
81
+ provider: deps.provider,
82
+ category,
83
+ reason: "no_allow_option",
84
+ });
85
+ return CANCELLED;
86
+ }
87
+ logger.info("acp.permission.approved", { provider: deps.provider, category });
88
+ return { outcome: { outcome: "selected", optionId: allow.optionId } };
89
+ };
90
+ }
@@ -117,6 +117,8 @@ export class AcpProcessManager {
117
117
  callbacks: options.callbacks,
118
118
  idleTimeoutMs: options.idleTimeoutMs ?? this.config.processIdleTimeoutMs,
119
119
  initializeTimeoutMs: this.config.initializeTimeoutMs,
120
+ sessionNewTimeoutMs: this.config.sessionNewTimeoutMs,
121
+ promptTimeoutMs: this.config.promptTimeoutMs,
120
122
  onTerminal: m => this.live.delete(m),
121
123
  });
122
124
  this.live.add(managed);
@@ -192,7 +194,11 @@ class ManagedProcessImpl {
192
194
  hostServices: options.hostServices,
193
195
  callbacks: options.callbacks,
194
196
  logger: this.logger,
195
- timeouts: { initializeMs: options.initializeTimeoutMs },
197
+ timeouts: {
198
+ initializeMs: options.initializeTimeoutMs,
199
+ sessionNewMs: options.sessionNewTimeoutMs,
200
+ promptMs: options.promptTimeoutMs,
201
+ },
196
202
  });
197
203
  options.child.on("exit", (code, signal) => this.handleExit(code, signal));
198
204
  options.child.on("error", err => this.handleSpawnError(err));
@@ -1,5 +1,5 @@
1
1
  import type { CliType } from "../session-manager.js";
2
- export type AcpProviderStatus = "native_smoke_passed" | "adapter_mediated_deferred" | "absent_watchlist";
2
+ export type AcpProviderStatus = "native_smoke_passed" | "native_candidate" | "adapter_mediated_deferred" | "absent_watchlist";
3
3
  export type AcpSupportKind = "native" | "adapter_mediated" | "none";
4
4
  export interface AcpEntrypoint {
5
5
  readonly command: string;
@@ -64,6 +64,19 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
64
64
  adapterCandidates: Object.freeze([]),
65
65
  caveat: "No ACP flag or subcommand at the target version. Legacy Gemini CLI ACP evidence does not transfer to Antigravity agy. Watchlist only.",
66
66
  }),
67
+ devin: Object.freeze({
68
+ provider: "devin",
69
+ displayName: "Cognition Devin CLI",
70
+ status: "native_smoke_passed",
71
+ supportKind: "native",
72
+ targetVersion: "devin 2026.5.26-8 (1a388fa9)",
73
+ entrypoint: Object.freeze({ command: "devin", args: Object.freeze(["acp"]) }),
74
+ runtimeEnabledDefault: false,
75
+ shipRuntimePilot: true,
76
+ runtimePriority: 3,
77
+ adapterCandidates: Object.freeze([]),
78
+ caveat: "Native ACP entrypoint `devin acp` (stdio JSON-RPC). Manual initialize + session/new smoke passed with the installed CLI managing credentials (`devin auth login`; WINDSURF_API_KEY for empty-env); empty-env smoke is expected to fail. Third native runtime pilot; runtime routing stays config-gated.",
79
+ }),
67
80
  });
68
81
  export function getAcpProviderRegistry() {
69
82
  return ACP_PROVIDER_REGISTRY;
@@ -0,0 +1,35 @@
1
+ import { type AcpSpawnFn, type ProcessEnv } from "./process-manager.js";
2
+ import type { ApprovalManager } from "../approval-manager.js";
3
+ import type { AcpConfig } from "../config.js";
4
+ import type { FlightLogResult, FlightLogStart } from "../flight-recorder.js";
5
+ import type { Logger } from "../logger.js";
6
+ import type { CliType, ISessionManager } from "../session-manager.js";
7
+ export interface AcpFlightSink {
8
+ logStart(entry: FlightLogStart): void;
9
+ logComplete(correlationId: string, result: FlightLogResult): void;
10
+ }
11
+ export interface AcpRuntimeDeps {
12
+ readonly config: AcpConfig;
13
+ readonly sessionManager: ISessionManager;
14
+ readonly approvalManager: ApprovalManager;
15
+ readonly flightRecorder?: AcpFlightSink;
16
+ readonly logger?: Logger;
17
+ readonly spawn?: AcpSpawnFn;
18
+ readonly baseEnv?: ProcessEnv;
19
+ readonly now?: () => number;
20
+ }
21
+ export interface AcpRunRequest {
22
+ readonly provider: CliType;
23
+ readonly prompt: string;
24
+ readonly model?: string;
25
+ readonly sessionId?: string;
26
+ readonly cwd?: string;
27
+ readonly correlationId: string;
28
+ }
29
+ export interface AcpRunResult {
30
+ readonly text: string;
31
+ readonly gatewaySessionId: string;
32
+ readonly protocolVersion: number | null;
33
+ readonly durationMs: number;
34
+ }
35
+ export declare function runAcpRequest(deps: AcpRuntimeDeps, req: AcpRunRequest): Promise<AcpRunResult>;
@@ -0,0 +1,125 @@
1
+ import { AcpEventNormalizer } from "./event-normalizer.js";
2
+ import { AcpDisabledError, AcpError, AcpProtocolError, ProviderRuntimeDisabledError, isAcpError, } from "./errors.js";
3
+ import { buildAcpFlightResult, buildAcpFlightStart } from "./flight-redaction.js";
4
+ import { GatewayHostServices } from "./host-services.js";
5
+ import { createAcpPermissionDecider } from "./permission-bridge.js";
6
+ import { AcpProcessManager } from "./process-manager.js";
7
+ import { createAcpSession, recordAcpSessionInfo, resolveAcpResume } from "./session-map.js";
8
+ import { noopLogger } from "../logger.js";
9
+ export async function runAcpRequest(deps, req) {
10
+ const logger = deps.logger ?? noopLogger;
11
+ const now = deps.now ?? Date.now;
12
+ const startedAt = now();
13
+ const elapsed = () => Math.max(0, now() - startedAt);
14
+ if (!deps.config.enabled) {
15
+ throw new AcpDisabledError({ provider: req.provider });
16
+ }
17
+ const providerConfig = deps.config.providers[req.provider];
18
+ if (!providerConfig?.runtimeEnabled) {
19
+ throw new ProviderRuntimeDisabledError(req.provider, { provider: req.provider });
20
+ }
21
+ const permissionDecider = createAcpPermissionDecider({
22
+ approvalManager: deps.approvalManager,
23
+ provider: req.provider,
24
+ allowWrite: deps.config.allowWriteHostServices,
25
+ allowTerminal: deps.config.allowTerminalHostServices,
26
+ logger,
27
+ });
28
+ const hostServices = new GatewayHostServices({ logger, permissionDecider });
29
+ const manager = new AcpProcessManager({
30
+ config: deps.config,
31
+ logger,
32
+ spawn: deps.spawn,
33
+ baseEnv: deps.baseEnv,
34
+ });
35
+ const normalizer = new AcpEventNormalizer();
36
+ let gatewaySessionId;
37
+ let resumeProviderSessionId = null;
38
+ if (req.sessionId) {
39
+ const resume = await resolveAcpResume(deps.sessionManager, req.sessionId, req.provider);
40
+ if (!resume.ok) {
41
+ throw new AcpProtocolError(`ACP session resume failed (${resume.reason}).`, {
42
+ provider: req.provider,
43
+ debug: { reason: resume.reason },
44
+ });
45
+ }
46
+ gatewaySessionId = req.sessionId;
47
+ resumeProviderSessionId = resume.providerSessionId;
48
+ }
49
+ else {
50
+ gatewaySessionId = await createAcpSession(deps.sessionManager, {
51
+ provider: req.provider,
52
+ cwd: req.cwd,
53
+ });
54
+ }
55
+ const promptBlocks = [{ type: "text", text: req.prompt }];
56
+ deps.flightRecorder?.logStart(buildAcpFlightStart({
57
+ correlationId: req.correlationId,
58
+ provider: req.provider,
59
+ model: req.model ?? "default",
60
+ prompt: promptBlocks,
61
+ gatewaySessionId,
62
+ }));
63
+ let proc = null;
64
+ try {
65
+ proc = await manager.start({
66
+ provider: req.provider,
67
+ cwd: req.cwd,
68
+ hostServices,
69
+ callbacks: { onSessionUpdate: update => normalizer.handle(update) },
70
+ });
71
+ const cwd = proc.resolved.cwd;
72
+ const init = proc.client.agentInfo;
73
+ let providerSessionId;
74
+ if (resumeProviderSessionId) {
75
+ await proc.client.loadSession({ sessionId: resumeProviderSessionId, cwd, mcpServers: [] });
76
+ providerSessionId = resumeProviderSessionId;
77
+ }
78
+ else {
79
+ const session = await proc.client.newSession({ cwd, mcpServers: [] });
80
+ providerSessionId = session.sessionId;
81
+ await recordAcpSessionInfo(deps.sessionManager, gatewaySessionId, {
82
+ providerSessionId,
83
+ protocolVersion: init?.protocolVersion,
84
+ agentName: init?.agentInfo?.name,
85
+ agentVersion: init?.agentInfo?.version,
86
+ });
87
+ }
88
+ await proc.client.prompt({ sessionId: providerSessionId, prompt: promptBlocks });
89
+ const text = normalizer.finalText;
90
+ const durationMs = elapsed();
91
+ deps.flightRecorder?.logComplete(req.correlationId, buildAcpFlightResult({ responseText: text, durationMs, status: "completed", exitCode: 0 }));
92
+ logger.info("acp.request.success", { provider: req.provider, durationMs });
93
+ return {
94
+ text,
95
+ gatewaySessionId,
96
+ protocolVersion: init?.protocolVersion ?? null,
97
+ durationMs,
98
+ };
99
+ }
100
+ catch (err) {
101
+ const durationMs = elapsed();
102
+ deps.flightRecorder?.logComplete(req.correlationId, buildAcpFlightResult({
103
+ responseText: "",
104
+ durationMs,
105
+ status: "failed",
106
+ exitCode: 1,
107
+ errorMessage: isAcpError(err) ? err.userMessage : "ACP request failed.",
108
+ }));
109
+ logger.error("acp.request.failure", {
110
+ provider: req.provider,
111
+ durationMs,
112
+ errorClass: err instanceof Error ? err.name : "unknown",
113
+ });
114
+ throw err instanceof AcpError
115
+ ? err
116
+ : new AcpProtocolError("ACP request failed unexpectedly.", {
117
+ provider: req.provider,
118
+ debug: { errorClass: err instanceof Error ? err.name : "unknown" },
119
+ });
120
+ }
121
+ finally {
122
+ proc?.shutdown("SIGTERM");
123
+ manager.shutdownAll("SIGKILL");
124
+ }
125
+ }