llm-cli-gateway 2.10.0 → 2.11.1

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 +75 -1
  2. package/README.md +46 -14
  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 +18 -5
  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 +8 -3
  50. package/dist/provider-tool-capabilities.js +107 -17
  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 +184 -24
  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 +6 -5
  64. package/package.json +12 -9
package/CHANGELOG.md CHANGED
@@ -2,7 +2,81 @@
2
2
 
3
3
  All notable changes to the llm-cli-gateway project.
4
4
 
5
- ## Unreleased
5
+ ## [2.11.1] - 2026-06-22: Provider contract refresh and stable upstream scans
6
+
7
+ ### Changed
8
+
9
+ - **Upstream contracts refreshed for all providers.** Live `--probe-installed` probes were run across the installed provider CLIs. Synced contract surfaces and bumped ACP `targetVersion` pins:
10
+ - claude → 2.1.185 (added `--ax-screen-reader`, `--safe-mode` to acknowledged flags; `agents --all` to subcommand contract)
11
+ - codex → 0.141.0 (aligned `exec resume` / `exec review` / top-level `review` flag lists with current advertised surfaces)
12
+ - gemini (agy) → 1.0.10
13
+ - grok → 0.2.60 (474c2bbfc)
14
+ - mistral (vibe) → 2.17.1
15
+ - devin → 2026.7.23 (3bd47f77) (added new flags to `acknowledgedUpstreamFlags`)
16
+ - **Mistral upstream scan source stabilized.** The scanner now tracks GitHub's latest-release API for Mistral Vibe instead of the volatile HTML releases page, so `--live --fail-on-critical` no longer trips on release-page chrome/hash churn.
17
+ - **Provider skill docs updated.** All five `.agents/skills/provider-*` files bumped to metadata version 1.2. Descriptions now call out `/subcommand` behaviour changes; added current-version "Tested against" notes and usage examples for the cache levers (Grok compaction, Claude `cacheControl`).
18
+ - **Cache usage documentation expanded with concrete examples.** Added exact parameters, emitted CLI flags, and stream-json payload shapes for Grok `compactionMode`/`compactionDetail` and Claude `promptParts.cacheControl` (including the `assembleClaudeCacheBlocks` NDJSON structure with `cache_control: {type:"ephemeral", ttl:"1h"}`) to:
19
+ - `docs/personal-mcp/PROVIDER_CACHE_SURFACES.md`
20
+ - `README.md` (updated Cache-aware operation section and matrix)
21
+ - `.agents/skills/session-workflow/SKILL.md`
22
+ - Cross-LLM reviews (Claude, Codex, Mistral) on the implementation plan were incorporated: kept provider-specific levers (no new unifying `CacheDirective` abstraction); focused on documentation and discoverability.
23
+ - **Internal caching hygiene.** Promoted the ad-hoc capability cache in `provider-tool-capabilities.ts` to a documented reusable TTL structure (`CAPABILITY_CACHE`, helpers, test accessors) for easier future reuse.
24
+
25
+ ### Tests / CI
26
+
27
+ - `npm run upstream:contracts`, relevant cache/session/provider-tool tests, and build all green.
28
+
29
+ ## [2.11.0] - 2026-06-18: API providers, Devin, ACP runtime, and a strip-at-publish internal-MCP boundary
30
+
31
+ The accumulated work since 2.10.0: a generic HTTP API-provider surface, a sixth
32
+ CLI provider (Devin), the Phase B ACP runtime (gated, dormant by default), and a
33
+ release-time strip that keeps gateway-internal MCP server names out of the
34
+ published npm artifact. Default local-stdio behaviour is unchanged; the new
35
+ API/ACP surfaces are opt-in. Every change in this release was landed via PR with
36
+ green CI and an adversarial multi-LLM review.
37
+
38
+ ### Added
39
+
40
+ - **API providers (Slices 0–5).** An `ApiProvider` interface with OpenAI /
41
+ Anthropic / xAI adapters, a generic `api_<name>_request[_async]` tool surface,
42
+ an `HttpJobRunner` that treats HTTP calls as first-class async jobs, API-provider
43
+ discovery/catalog, and the ability to use API providers as validation
44
+ reviewers/judges. Open-string provider-identity widening (`kind:"api"`).
45
+ - **Devin (Slices D0–D1).** Devin as a sixth gateway provider —
46
+ `devin_request[_async]` — with permission-mode aliases corrected against the
47
+ real CLI and a passing native-ACP smoke (third ACP runtime pilot).
48
+ - **ACP runtime, Phase B (Slices B1–B7).** Read-only smoke harness,
49
+ deny-by-default HostServices boundary, permission bridge to ApprovalManager,
50
+ session map, session/update event normalizer, flight-recorder redaction, and
51
+ gated runtime pilots (Mistral + Grok + Devin) for first live ACP prompt routing.
52
+ Dormant by default.
53
+
54
+ ### Changed
55
+
56
+ - **Strip internal MCP names from the published artifact.** Gateway-internal MCP
57
+ server names and host commands are centralised in `src/mcp-registry.ts` and
58
+ stripped at release time, so the published tarball contains zero internal names.
59
+ Enforced by an explicit, non-bypassable tarball token-guard
60
+ (`scripts/verify-no-internal-mcp.mjs`). `ClaudeMcpServerName` widens to `string`
61
+ and the request `mcpServers` schemas accept arbitrary names; no implicit default
62
+ server is injected.
63
+ - **Vibe `permissionMode` accepts any `--agent` name.** Replaces the closed
64
+ agent-mode enum (which had stale `chat`/`explore`/`lean` values) with an open
65
+ string — Vibe owns its agent registry (builtins plus install-gated and custom
66
+ agents), so the gateway passes the name through and lets Vibe validate it.
67
+ - **Gemini `mcpServers` accepted for approval tracking.** `gemini_request` no
68
+ longer rejects non-empty `mcpServers`; the names feed the approval policy but are
69
+ never passed to the Antigravity CLI (which manages its own MCP configuration).
70
+
71
+ ### Security / supply chain
72
+
73
+ - `hono` pinned to `^4.12.25` via `overrides` (clears the advisories on 4.12.22),
74
+ with a hono-floor tripwire in the release security audit.
75
+
76
+ ### Deps
77
+
78
+ - Dev-dependency bumps (keeps `type-is@2.1.0` out via the `body-parser` floor) and
79
+ a `github/codeql-action` group bump.
6
80
 
7
81
  ## [2.10.0] - 2026-06-15: Per-principal isolation on the request handlers
8
82
 
package/README.md CHANGED
@@ -188,17 +188,47 @@ Every `*_request` and `*_request_async` tool accepts an optional `promptParts` f
188
188
 
189
189
  `prompt` and `promptParts` are mutually exclusive — pass exactly one.
190
190
 
191
- Per-CLI capability matrix:
191
+ Per-CLI capability matrix (prefix discipline is automatic via `promptParts` for all; explicit levers are provider-specific):
192
+
193
+ | CLI | Prefix discipline | Explicit lever(s) |
194
+ | ------- | ----------------- | ----------------- |
195
+ | claude | yes | `promptParts.cacheControl` + `outputFormat: "stream-json"` (Anthropic `cache_control` breakpoints on stable blocks; `ttl="1h"` forced) |
196
+ | codex | yes | none (OpenAI implicit) |
197
+ | gemini | yes | none (implicit server-side) |
198
+ | grok | yes | `compactionMode` / `compactionDetail` (context compaction: `summary|transcript|segments`; `segments` writes per-segment markdown) |
199
+ | mistral | yes | none (implicit) |
200
+
201
+ **Claude example (explicit cacheControl)**
202
+
203
+ ```ts
204
+ claude_request({
205
+ promptParts: {
206
+ system: "You are a helpful code reviewer.",
207
+ context: "<long stable file dump>",
208
+ task: "Review the diff.",
209
+ cacheControl: { system: true, context: true } // task is never marked
210
+ },
211
+ outputFormat: "stream-json"
212
+ })
213
+ ```
214
+
215
+ Gateway emits the `stream-json` stdin path with `cache_control: {type:"ephemeral", ttl:"1h"}` on marked blocks only.
216
+
217
+ **Grok example (compaction)**
218
+
219
+ ```ts
220
+ grok_request({
221
+ promptParts: { system: "...", context: "...", task: "..." },
222
+ compactionMode: "segments",
223
+ compactionDetail: "balanced"
224
+ })
225
+ ```
226
+
227
+ Emits `--compaction-mode segments --compaction-detail balanced`.
192
228
 
193
- | CLI | Prefix discipline (auto via `promptParts`) | Explicit `cache_control` emission |
194
- | ------- | ------------------------------------------ | ---------------------------------------------------------------------------- |
195
- | claude | yes | yes, opt-in via `promptParts.cacheControl` and `outputFormat: "stream-json"` |
196
- | codex | yes | n/a (OpenAI implicit cache, no CLI lever) |
197
- | gemini | yes | n/a (implicit prefix cache server-side) |
198
- | grok | yes | n/a (no surfaced cache lever) |
199
- | mistral | yes | n/a (no surfaced cache lever) |
229
+ See `docs/personal-mcp/PROVIDER_CACHE_SURFACES.md` for full surfaces, telemetry differences (e.g. Grok `-p` vs ACP), exact stream-json payload shapes, and cross-LLM review notes.
200
230
 
201
- Opt-in flags (all default off) live under `[cache_awareness]` in `~/.llm-cli-gateway/config.toml`. See `docs/personal-mcp/PROVIDER_CACHE_SURFACES.md` for the per-model minimum cacheable token thresholds and field-name divergences.
231
+ Opt-in flags (all default off) live under `[cache_awareness]` in `~/.llm-cli-gateway/config.toml`.
202
232
 
203
233
  ### Reliability & Performance
204
234
 
@@ -278,8 +308,10 @@ Vibe-specific notes:
278
308
  `VIBE_MODELS`, injects `VIBE_ACTIVE_MODEL` only when a model is explicitly
279
309
  requested or Vibe config needs recovery, and retries once after a
280
310
  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
311
+ - **`permissionMode` is the Vibe `--agent` name.** Builtins are
312
+ `default | plan | accept-edits | auto-approve`; Vibe also accepts install-gated
313
+ builtins (e.g. `lean`) and custom agents from `~/.vibe/agents`, so any name is
314
+ passed through and Vibe validates availability. The gateway's
283
315
  programmatic-mode default is `auto-approve`; pick a stricter mode
284
316
  explicitly if you need approval gates.
285
317
  - **`allowedTools` is allow-list only** — the gateway emits one
@@ -391,7 +423,7 @@ Execute a Claude Code request with optional session management.
391
423
  - `excludeDynamicSystemPromptSections` (boolean, optional): Trim dynamic system prompt sections
392
424
  - `approvalStrategy` (string, optional): `"legacy"` (default) or `"mcp_managed"`
393
425
  - `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)
426
+ - `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
427
  - `strictMcpConfig` (boolean, optional): Require Claude to use only supplied MCP config, default: true (request fails if any requested server is unavailable)
396
428
  - `optimizePrompt` (boolean, optional): Optimize prompt for token efficiency (44% reduction), default: false
397
429
  - `optimizeResponse` (boolean, optional): Optimize response for token efficiency (37% reduction), default: false
@@ -825,7 +857,7 @@ consumes = ["OUT:mcp-reconnected"]
825
857
  Run a Mistral Vibe agentic coding request. Like `grok_request` in shape, but with Vibe's specific surface:
826
858
 
827
859
  - `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.
860
+ - `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
861
  - `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry (allow-list only).
830
862
  - `disallowedTools` (string[], optional): Accepted for parity with the other providers; ignored at the CLI boundary with a logged warning.
831
863
  - `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 +893,7 @@ Async request tools accept the same approval strategy fields as their sync varia
861
893
 
862
894
  - `approvalStrategy`: `"legacy"` (default) or `"mcp_managed"`
863
895
  - `approvalPolicy`: `"strict"|"balanced"|"permissive"` override
864
- - `mcpServers`: Requested MCP servers (`sqry`, `exa`, `ref_tools`, `trstr`)
896
+ - `mcpServers`: Names of requested MCP servers, resolved against the gateway's local registry / Codex MCP config
865
897
  - `claude_request_async` also supports `strictMcpConfig` and fails fast when requested servers are unavailable
866
898
 
867
899
  ##### `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;
@@ -4,7 +4,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
4
4
  displayName: "Mistral Vibe",
5
5
  status: "native_smoke_passed",
6
6
  supportKind: "native",
7
- targetVersion: "vibe 2.14.1",
7
+ targetVersion: "vibe 2.17.1",
8
8
  entrypoint: Object.freeze({ command: "vibe-acp", args: Object.freeze([]) }),
9
9
  runtimeEnabledDefault: false,
10
10
  shipRuntimePilot: true,
@@ -17,7 +17,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
17
17
  displayName: "xAI Grok CLI",
18
18
  status: "native_smoke_passed",
19
19
  supportKind: "native",
20
- targetVersion: "grok 0.2.50 (cadf94855)",
20
+ targetVersion: "grok 0.2.60 (474c2bbfc)",
21
21
  entrypoint: Object.freeze({ command: "grok", args: Object.freeze(["agent", "stdio"]) }),
22
22
  runtimeEnabledDefault: false,
23
23
  shipRuntimePilot: true,
@@ -30,7 +30,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
30
30
  displayName: "OpenAI Codex CLI",
31
31
  status: "adapter_mediated_deferred",
32
32
  supportKind: "adapter_mediated",
33
- targetVersion: "codex-cli 0.139.0",
33
+ targetVersion: "codex-cli 0.141.0",
34
34
  entrypoint: null,
35
35
  runtimeEnabledDefault: false,
36
36
  shipRuntimePilot: false,
@@ -43,7 +43,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
43
43
  displayName: "Anthropic Claude Code",
44
44
  status: "adapter_mediated_deferred",
45
45
  supportKind: "adapter_mediated",
46
- targetVersion: "claude 2.1.175",
46
+ targetVersion: "claude 2.1.185",
47
47
  entrypoint: null,
48
48
  runtimeEnabledDefault: false,
49
49
  shipRuntimePilot: false,
@@ -56,7 +56,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
56
56
  displayName: "Google Antigravity",
57
57
  status: "absent_watchlist",
58
58
  supportKind: "none",
59
- targetVersion: "agy 1.0.7",
59
+ targetVersion: "agy 1.0.10",
60
60
  entrypoint: null,
61
61
  runtimeEnabledDefault: false,
62
62
  shipRuntimePilot: false,
@@ -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.7.23 (3bd47f77)",
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>;