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.
- package/CHANGELOG.md +92 -0
- package/README.md +7 -5
- package/dist/acp/event-normalizer.d.ts +42 -0
- package/dist/acp/event-normalizer.js +71 -0
- package/dist/acp/flight-redaction.d.ts +25 -0
- package/dist/acp/flight-redaction.js +40 -0
- package/dist/acp/host-services.d.ts +16 -0
- package/dist/acp/host-services.js +29 -0
- package/dist/acp/permission-bridge.d.ts +15 -0
- package/dist/acp/permission-bridge.js +90 -0
- package/dist/acp/process-manager.js +7 -1
- package/dist/acp/provider-registry.d.ts +1 -1
- package/dist/acp/provider-registry.js +13 -0
- package/dist/acp/runtime.d.ts +35 -0
- package/dist/acp/runtime.js +125 -0
- package/dist/acp/session-map.d.ts +42 -0
- package/dist/acp/session-map.js +67 -0
- package/dist/acp/smoke-harness.d.ts +28 -0
- package/dist/acp/smoke-harness.js +90 -0
- package/dist/api-http.d.ts +18 -0
- package/dist/api-http.js +122 -0
- package/dist/api-provider.d.ts +83 -0
- package/dist/api-provider.js +258 -0
- package/dist/api-request.d.ts +30 -0
- package/dist/api-request.js +51 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +6 -7
- package/dist/async-job-manager.d.ts +19 -4
- package/dist/async-job-manager.js +211 -35
- package/dist/claude-mcp-config.d.ts +2 -2
- package/dist/claude-mcp-config.js +42 -52
- package/dist/cli-updater.js +16 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +93 -35
- package/dist/doctor.d.ts +1 -1
- package/dist/flight-recorder.d.ts +1 -0
- package/dist/flight-recorder.js +11 -0
- package/dist/index.d.ts +56 -5
- package/dist/index.js +670 -48
- package/dist/job-store.d.ts +15 -0
- package/dist/job-store.js +39 -5
- package/dist/mcp-registry.d.ts +17 -0
- package/dist/mcp-registry.js +5 -0
- package/dist/metrics.js +7 -2
- package/dist/model-registry.js +11 -0
- package/dist/prompt-parts.d.ts +6 -6
- package/dist/provider-login-guidance.js +21 -0
- package/dist/provider-status.js +4 -1
- package/dist/provider-tool-capabilities.d.ts +4 -3
- package/dist/provider-tool-capabilities.js +93 -6
- package/dist/request-helpers.d.ts +6 -6
- package/dist/request-helpers.js +1 -4
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +24 -15
- package/dist/session-manager-pg.js +2 -9
- package/dist/session-manager.d.ts +9 -4
- package/dist/session-manager.js +13 -4
- package/dist/upstream-contracts.js +112 -2
- package/dist/validation-normalizer.d.ts +2 -2
- package/dist/validation-orchestrator.d.ts +2 -0
- package/dist/validation-orchestrator.js +28 -7
- package/dist/validation-tools.d.ts +61 -0
- package/dist/validation-tools.js +36 -21
- package/migrations/005_provider_type_open_api_names.sql +28 -0
- package/npm-shrinkwrap.json +6 -5
- 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`
|
|
282
|
-
|
|
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):
|
|
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
|
|
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`:
|
|
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: {
|
|
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>;
|