llm-cli-gateway 2.9.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 (66) hide show
  1. package/CHANGELOG.md +92 -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 +670 -48
  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/resources.d.ts +2 -0
  54. package/dist/resources.js +24 -15
  55. package/dist/session-manager-pg.js +2 -9
  56. package/dist/session-manager.d.ts +9 -4
  57. package/dist/session-manager.js +13 -4
  58. package/dist/upstream-contracts.js +112 -2
  59. package/dist/validation-normalizer.d.ts +2 -2
  60. package/dist/validation-orchestrator.d.ts +2 -0
  61. package/dist/validation-orchestrator.js +28 -7
  62. package/dist/validation-tools.d.ts +61 -0
  63. package/dist/validation-tools.js +36 -21
  64. package/migrations/005_provider_type_open_api_names.sql +28 -0
  65. package/npm-shrinkwrap.json +6 -5
  66. package/package.json +12 -9
package/CHANGELOG.md CHANGED
@@ -4,6 +4,98 @@ 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
+
59
+ ## [2.10.0] - 2026-06-15: Per-principal isolation on the request handlers
60
+
61
+ A follow-up to the 2.9.0 per-principal isolation (F3): the ownership model was
62
+ enforced on the `session_*` / `llm_*` bookkeeping tools but **not** on the
63
+ `*_request` execution handlers, the workspace/worktree resolvers, or the
64
+ `sessions://*` resources. An adversarial multi-LLM review of the 2.9.0 surface
65
+ found two HIGH cross-principal bugs reachable in the opt-in remote/OAuth
66
+ multi-tenant modes; this release closes them. The default local-stdio and shared
67
+ static-bearer paths collapse to a single principal and are unaffected (no
68
+ behaviour change). Provider CLI release targets are unchanged from 2.8.0/2.9.0
69
+ (see `docs/upstream/release-targets.md`).
70
+
71
+ ### Security
72
+
73
+ - Cross-principal session takeover (F3b, request handlers):
74
+ `getExistingSessionForProvider` now rejects a caller-supplied session id owned
75
+ by a different principal — the ownership check is ordered before the
76
+ provider-type comparison, so a foreign session never leaks its provider. This
77
+ is the choke point for every `*_request` / `*_request_async` handler (claude,
78
+ codex, gemini, grok, mistral, grok-api) and `codex_fork_session`. Previously a
79
+ remote principal could resume or read another principal's conversation by
80
+ supplying its session id.
81
+ - Global active-session pointer is now owner-filtered at every request-handler
82
+ adoption site (and in the codex async path, which bypassed the choke point),
83
+ so a no-`sessionId` request can no longer adopt and resume another principal's
84
+ active session.
85
+ - Workspace-isolation bypass (F3b, resolvers): `resolveWorktreeForRequest` and
86
+ `resolveWorkspaceAndWorktreeForRequest` ignore a referenced session the caller
87
+ does not own, so a foreign session's metadata can no longer select the
88
+ workspace/worktree working directory or satisfy the remote "registered
89
+ workspace" gate.
90
+ - `sessions://*` resources are now owner-filtered: `sessions://all` and the five
91
+ per-provider session resources return only the caller's own rows and active
92
+ pointer, closing the session-id/metadata enumeration vector.
93
+
94
+ ### Tests
95
+
96
+ - Adds `src/__tests__/f3b-request-handler-isolation.test.ts` (cross-principal
97
+ deny-path coverage for the request handlers and the `sessions://*` resources).
98
+
7
99
  ## [2.9.0] - 2026-06-14: MCP-surface red-team remediation
8
100
 
9
101
  This release remediates all 17 findings of a multi-LLM red-team of the gateway's
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>;