llm-cli-gateway 2.13.2 → 2.14.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 (79) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +68 -29
  3. package/dist/acp/client.d.ts +29 -1
  4. package/dist/acp/client.js +78 -4
  5. package/dist/acp/errors.d.ts +9 -1
  6. package/dist/acp/errors.js +19 -0
  7. package/dist/acp/event-normalizer.d.ts +12 -0
  8. package/dist/acp/event-normalizer.js +16 -0
  9. package/dist/acp/flight-redaction.d.ts +3 -0
  10. package/dist/acp/flight-redaction.js +3 -0
  11. package/dist/acp/permission-bridge.js +11 -5
  12. package/dist/acp/process-manager.d.ts +2 -1
  13. package/dist/acp/process-manager.js +43 -4
  14. package/dist/acp/provider-registry.js +19 -11
  15. package/dist/acp/runtime.d.ts +8 -0
  16. package/dist/acp/runtime.js +47 -4
  17. package/dist/acp/types.d.ts +3083 -55
  18. package/dist/acp/types.js +242 -5
  19. package/dist/async-job-manager.d.ts +38 -1
  20. package/dist/async-job-manager.js +287 -20
  21. package/dist/codex-json-parser.d.ts +1 -0
  22. package/dist/codex-json-parser.js +6 -0
  23. package/dist/config.d.ts +19 -0
  24. package/dist/config.js +84 -1
  25. package/dist/flight-recorder.d.ts +2 -0
  26. package/dist/flight-recorder.js +20 -0
  27. package/dist/gemini-json-parser.d.ts +2 -0
  28. package/dist/gemini-json-parser.js +45 -8
  29. package/dist/grok-json-parser.d.ts +14 -0
  30. package/dist/grok-json-parser.js +156 -0
  31. package/dist/index.d.ts +47 -2
  32. package/dist/index.js +504 -122
  33. package/dist/job-store.d.ts +119 -11
  34. package/dist/job-store.js +372 -42
  35. package/dist/model-registry.d.ts +1 -0
  36. package/dist/model-registry.js +46 -0
  37. package/dist/oauth.js +2 -2
  38. package/dist/postgres-job-store-worker.d.ts +1 -0
  39. package/dist/postgres-job-store-worker.js +444 -0
  40. package/dist/pricing.d.ts +3 -2
  41. package/dist/provider-acp-capabilities.d.ts +52 -0
  42. package/dist/provider-acp-capabilities.js +101 -0
  43. package/dist/provider-admin-tools.d.ts +100 -0
  44. package/dist/provider-admin-tools.js +572 -0
  45. package/dist/provider-capability-cache.d.ts +46 -0
  46. package/dist/provider-capability-cache.js +248 -0
  47. package/dist/provider-capability-discovery.d.ts +85 -0
  48. package/dist/provider-capability-discovery.js +461 -0
  49. package/dist/provider-capability-resolver.d.ts +29 -0
  50. package/dist/provider-capability-resolver.js +92 -0
  51. package/dist/provider-definition-assertions.d.ts +9 -0
  52. package/dist/provider-definition-assertions.js +147 -0
  53. package/dist/provider-definitions.d.ts +127 -0
  54. package/dist/provider-definitions.js +758 -0
  55. package/dist/provider-help-parser.d.ts +34 -0
  56. package/dist/provider-help-parser.js +203 -0
  57. package/dist/provider-model-discovery.d.ts +30 -0
  58. package/dist/provider-model-discovery.js +229 -0
  59. package/dist/provider-output-metadata.d.ts +7 -0
  60. package/dist/provider-output-metadata.js +68 -0
  61. package/dist/provider-schema-builder.d.ts +22 -0
  62. package/dist/provider-schema-builder.js +55 -0
  63. package/dist/provider-surface-generator.d.ts +98 -0
  64. package/dist/provider-surface-generator.js +140 -0
  65. package/dist/provider-tool-capabilities.d.ts +3 -2
  66. package/dist/provider-tool-capabilities.js +77 -62
  67. package/dist/request-helpers.d.ts +37 -4
  68. package/dist/request-helpers.js +134 -0
  69. package/dist/resources.d.ts +6 -1
  70. package/dist/resources.js +64 -192
  71. package/dist/session-manager.js +18 -11
  72. package/dist/sqlite-driver.d.ts +1 -1
  73. package/dist/sqlite-driver.js +2 -1
  74. package/dist/stream-json-parser.d.ts +1 -0
  75. package/dist/stream-json-parser.js +3 -0
  76. package/dist/upstream-contracts.d.ts +17 -1
  77. package/dist/upstream-contracts.js +209 -36
  78. package/npm-shrinkwrap.json +2 -2
  79. package/package.json +8 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,145 @@
2
2
 
3
3
  All notable changes to the llm-cli-gateway project.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [2.14.0] - 2026-07-03
8
+
9
+ ### Fixed
10
+
11
+ - **Durable instance-lease orphan recovery (#139).** The blanket
12
+ `markOrphanedOnStartup` sweep in the `AsyncJobManager` constructor rewrote
13
+ every `running` row to `orphaned`, so on a SHARED store (`backend =
14
+ "postgres"`) a fresh instance (especially an ephemeral stdio spawn)
15
+ transiently orphaned other live instances' in-flight jobs (a `running` ->
16
+ `orphaned` -> `completed` flap a poller can trip on). The sweep is now a
17
+ per-job fencing lease: each instance registers a lease and advances a
18
+ `jobs.lease_deadline` on every heartbeat, and the sweep orphans a
19
+ `queued`/`running` job only when its own `lease_deadline` has expired, never
20
+ because a different live instance started. Because heartbeat and sweep are
21
+ both `UPDATE`s on the same `jobs` rows, they serialize on the row lock
22
+ (Postgres READ COMMITTED) and are trivially serial under single-writer sqlite,
23
+ so no job whose owner heartbeated within the lease TTL is ever swept. A
24
+ genuinely stale-then-reviving owner self-heals to the correct terminal state
25
+ via the guarded `recordComplete` and a flight-recorder reconcile. The fix is
26
+ transport-aware (an extra `httpJobGrace` for no-pid http jobs, an advisory
27
+ never-vetoing `kill(pid,0)` for same-host process jobs), durable-admission is
28
+ fail-closed (a failed `recordStart`/`registerInstance` fails the request and
29
+ releases the limiter permit), and graceful shutdown drains in-flight terminal
30
+ writes before deregistering the lease. Additive schema (`jobs.owner_instance`,
31
+ `jobs.lease_deadline`, a `gateway_instances` table), auto-created on both
32
+ sqlite and postgres, no data migration.
33
+
34
+ ### Changed
35
+
36
+ - **`[persistence].ownsOrphanRecovery` is deprecated (#139).** The interim gate
37
+ (PR #140) that let one designated `postgres` instance own the blanket startup
38
+ sweep is superseded by the durable lease above, which is safe to run from
39
+ every instance. The flag is still parsed (so existing configs do not error)
40
+ and now emits a one-time deprecation warning; it no longer changes behaviour
41
+ and will be removed in a later release. New `[persistence]` knobs tune the
42
+ lease: `instanceHeartbeatMs` (15000), `instanceLeaseTtlMs` (90000),
43
+ `httpJobGraceMs` (300000), `orphanSweepIntervalMs` (30000), `instanceGcMs`
44
+ (3600000), validated so `instanceLeaseTtlMs >= 2 * instanceHeartbeatMs` and
45
+ `httpJobGraceMs >= instanceLeaseTtlMs`.
46
+
47
+ ## [2.14.0-rc.1] - 2026-07-03: full-featured provider integration + security-review hardening
48
+
49
+ Release candidate. Every non-Cursor provider (Claude, Codex, Gemini, Grok,
50
+ Mistral, Devin) becomes a first-class, provider-definition-driven integration,
51
+ and a security review of the change set is folded in. Cursor stays working via
52
+ the shared generation and is maintenance-only.
53
+
54
+ ### Added
55
+
56
+ - **Provider definitions as the single source of truth** (`src/provider-definitions.ts`):
57
+ one `ProviderDefinition` per CLI type drives request schemas, resources,
58
+ discovery, and capabilities, with compile-time exhaustiveness assertions and a
59
+ `scripts/provider-surfaces-check.mjs` ratchet wired into `npm run check`.
60
+ - **Runtime provider capability discovery with an on-disk cache**: an injectable
61
+ probe runner (no shell interpolation of caller input), a cache keyed on
62
+ discovery checksums with a shape-driven secret scrubber and Zod-validated
63
+ reads, and a TTL that bounds staleness so an upgraded or moved provider CLI is
64
+ re-discovered instead of served stale.
65
+ - **Registry-generated `models://` and `sessions://` resources** for every
66
+ provider (Devin and Cursor now first-class), plus **live model discovery**
67
+ wired into `models://<provider>` and `list_models` via a memoized resolver
68
+ with a fire-and-forget startup warm (reads never block on a spawn).
69
+ - **Complete CLI request-field coverage** across providers, guarded by
70
+ upstream-contract and coverage-closure tests; interactive/admin-only flags are
71
+ represented as typed capability facts rather than silently dropped.
72
+ - **Discovery-driven native ACP surface**: Grok, Mistral, and Devin route
73
+ natively over ACP when enabled (no masquerade), with capability-gated session
74
+ methods and state-mutating session ops gated behind
75
+ `acp.allow_mutating_session_ops`.
76
+ - **Provider admin tools**: `provider_admin_list` / `provider_admin_run`
77
+ (read-only) and `provider_admin_mutate` (gated). Availability is
78
+ discovery-driven; mutating ops require `[admin] allow_mutating_cli_admin_ops`
79
+ and, for remote callers, `LLM_GATEWAY_CLI_ADMIN=1` plus the `cli:admin` OAuth
80
+ scope, routed through the approval manager with a fail-closed pre-spawn audit.
81
+ - **Postgres job store** (`PostgresJobStore`): a durable async-job and
82
+ validation-run/receipt backend over the `pg` pool, driven through a dedicated
83
+ worker thread (`postgres-job-store-worker.ts`). Selectable via
84
+ `persistence.backend = "postgres"`; the ephemeral memory backend deliberately
85
+ does not implement the validation-run surface.
86
+ - **Search-engine discoverability for the docs site**: Google Search Console
87
+ and Bing IndexNow submission (`npm run search:*`), plus `sitemap.xml`,
88
+ `robots.txt`, and `llms.txt`, alongside a marketing-site refresh.
89
+
90
+ ### Changed
91
+
92
+ - **Unified provider output/event normalization**: a real Grok JSON parser, and
93
+ `stopReason`/error signals threaded across the CLI parsers and the ACP event
94
+ normalizer. The provider-minted session id and terminal stop reason are
95
+ preserved through the async job and flight recorder so a deferred job resumes
96
+ with the real provider session id.
97
+ - **`provider_tool_capabilities`** now reports ACP `runtimeEnabled` from the
98
+ resolved config (previously hardcoded), with entrypoint/version/mediation
99
+ derived from the registry.
100
+ - Docs: README provider-capability table, per-provider skills refreshed to the
101
+ installed CLI versions, ACP documented as sync-only for now, and the
102
+ `provider-acp://` resource registered on the MCP server.
103
+
104
+ ### Fixed
105
+
106
+ - Codex and Gemini clean exits that actually carried a failed terminal event
107
+ now surface a warning instead of being reported as a silent empty success;
108
+ the ACP transport likewise flags a refusal/cancelled/truncated turn.
109
+ - Codex surfaces the real `turn.failed` reason on a non-zero exit instead of a
110
+ partial agent message.
111
+ - json-mode output parsers tolerate a stray banner line around the JSON object
112
+ (telemetry is no longer all-or-nothing), and the Gemini stream replaces rather
113
+ than doubles a consolidated non-delta final message.
114
+ - Implausible `agy models` label lines are filtered instead of surfacing as
115
+ bogus models in `models://gemini`.
116
+
117
+ ### Security
118
+
119
+ - **Remote host-path confinement.** Remote HTTP/OAuth callers can no longer hand
120
+ a provider CLI an arbitrary host path to read, write, or load code from. The
121
+ advanced path/plugin fields are rejected for remote callers (local stdio is
122
+ unaffected): `systemPromptFile`/`appendSystemPromptFile`/`settings`/
123
+ `pluginDir`/`pluginUrl`/`debugFile` (Claude), string `outputSchema`/`images`/
124
+ `outputLastMessage` (Codex), `promptFile`/`config`/`agentConfig`/string
125
+ `exportSession` (Devin), and `promptFile`/`leaderSocket`/`rules`/`agent`
126
+ (Grok). `workingDir`/`addDir`/`includeDirs` remain workspace-confined.
127
+ - **Read-only provider-admin ops** now require the same remote `cli:admin` gate
128
+ as the mutating path, so a remote caller cannot enumerate or trigger
129
+ host-global provider-CLI spawns unprivileged.
130
+ - **ACP permission bridge** never turns a single approval into a standing
131
+ `allow_always` grant (it selects a one-time allow, else denies).
132
+ - **ACP subprocess reaping**: a graceful termination signal escalates to
133
+ SIGKILL after a grace window so a provider CLI that ignores SIGTERM is not
134
+ leaked as a zombie.
135
+ - Job results scrub the provider session id from returned streams for remote
136
+ callers; ACP error redaction covers Google/GitHub/Slack token shapes;
137
+ `session_info_update` is handled as a forward-compatible unknown ACP variant.
138
+ - CI/release: the Cloudflare Pages token is fetched from Azure Key Vault via
139
+ GitHub OIDC federation (no stored secret); the release tooling supports
140
+ prerelease cuts (a prerelease publishes under a side npm dist-tag, never
141
+ `latest`); typos and gitleaks allowlists cover a deliberate identifier and
142
+ secret-fixture test files.
143
+
5
144
  ## [2.13.2] - 2026-07-01: remote HTTP + OAuth connector UX and hardening
6
145
 
7
146
  ### Added
package/README.md CHANGED
@@ -9,9 +9,11 @@
9
9
  > _"Without consultation, plans are frustrated, but with many counselors they succeed."_
10
10
  > — Proverbs 15:22 (LSB)
11
11
 
12
- A Model Context Protocol (MCP) gateway for running Claude Code, Codex, Gemini, Grok, Mistral (Vibe), Devin, and Cursor Agent CLIs from one MCP endpoint, with durable async jobs, session continuity, cache-aware prompting, observability, and personal-appliance setup tooling.
12
+ **Secure local control plane for AI coding agents.**
13
13
 
14
- **Why developers try it:** one local MCP endpoint for cross-LLM validation, multi-agent coding workflows, and repeatable assistant-led setup across registered provider CLIs.
14
+ `llm-cli-gateway` lets supported MCP clients operate Claude Code, Codex, Gemini/Antigravity, Grok Build, Mistral Vibe, Cognition Devin, Cursor Agent, and configured HTTP API providers through one user-owned gateway while preserving native CLI sessions, local credentials, durable async jobs, validation receipts, and review workflows.
15
+
16
+ **Why developers try it:** use the client you are already in to delegate work to local coding agents, scope remote execution to registered workspaces, gate risky actions, survive disconnects, and collect auditable review evidence without turning those agents into a generic chat proxy.
15
17
 
16
18
  **Current signals:** CI and security workflows pass on `main`, OpenSSF Scorecard is published, OpenSSF Best Practices is passing, releases use Sigstore signing, and the package is MIT licensed.
17
19
 
@@ -36,9 +38,9 @@ Or use directly with `npx` from an MCP client:
36
38
 
37
39
  ## What It Provides Today
38
40
 
39
- `llm-cli-gateway` is a single-user MCP gateway for cross-LLM validation and multi-agent coding workflows. It is more than a thin CLI wrapper:
41
+ `llm-cli-gateway` is a single-user MCP control plane for operating AI coding agents from supported local or remote clients. It is more than a thin CLI wrapper:
40
42
 
41
- - Runs registered provider CLIs through consistent sync and async MCP tools.
43
+ - Runs registered provider CLIs and configured HTTP API providers through consistent sync and async MCP tools.
42
44
  - Persists long-running jobs, supports restart-safe result collection, deduplication, cancellation, and sync-to-async deferral.
43
45
  - Tracks sessions, real CLI resume paths, structured response metadata, and cache telemetry.
44
46
  - Supports cache-aware `promptParts`, including explicit Claude `cache_control` when opted in.
@@ -51,7 +53,7 @@ Or use directly with `npx` from an MCP client:
51
53
 
52
54
  The repo ships agent-ready workflow skills under [`.agents/skills`](.agents/skills) for async orchestration, session continuity, multi-LLM review, implement-review-fix loops, and secure approval-gated dispatch. Six caller-facing skills are bundled in the published npm package: `async-job-orchestration`, `multi-llm-review`, `session-workflow`, `secure-orchestration`, `implement-review-fix`, and `public-demo-session`. Machine-readable DAG-TOML plans live under [`docs/plans`](docs/plans) and [`setup/install-plan.dag.toml`](setup/install-plan.dag.toml) for workflows that need deterministic sequencing and verification gates.
53
55
 
54
- The next documentation focus is provider-specific skill and DAG-TOML pairs for each outbound CLI: Claude, Codex, Gemini, Grok, and Mistral Vibe. The implementation plan is tracked in [`docs/plans/provider-workflow-assets.dag.toml`](docs/plans/provider-workflow-assets.dag.toml), with each provider asset expected to cover install/login checks, session behavior, approval modes, cache/telemetry surfaces, failure modes, and a smoke-test gate.
56
+ The next documentation focus is provider-specific skill and DAG-TOML pairs for each outbound CLI and API-provider family: Claude, Codex, Gemini/Antigravity, Grok, Mistral Vibe, Devin, Cursor Agent, OpenAI-compatible endpoints, Anthropic Messages, and xAI Responses. The implementation plan is tracked in [`docs/plans/provider-workflow-assets.dag.toml`](docs/plans/provider-workflow-assets.dag.toml), with each provider asset expected to cover install/login checks or token-env checks, session behavior, approval modes, cache/telemetry surfaces, failure modes, and a smoke-test gate.
55
57
 
56
58
  ## Trust & Supply Chain
57
59
 
@@ -61,13 +63,13 @@ The next documentation focus is provider-specific skill and DAG-TOML pairs for e
61
63
  - CI runs build, lint, format, tests, package checks, and npm audit.
62
64
  - Security CI runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, and lychee.
63
65
  - GitHub release installer artifacts are checksummed and signed with Sigstore keyless signing.
64
- - npm releases use provenance through OIDC trusted publishing.
66
+ - npm releases use a generated prod-only shrinkwrap and release security audit; the publish token is fetched at runtime from Azure Key Vault through GitHub OIDC to Entra.
65
67
  - The npm package intentionally ships a generated, prod-only `npm-shrinkwrap.json` so registry installs resolve the audited release tree. Release gates regenerate it from `package-lock.json`, compare for parity, and run a registry-fidelity consumer install before publishing.
66
68
  - Socket behavioural alerts are documented in [`socket.yml`](./socket.yml) and under "Security Considerations" below. `shellAccess` and `shrinkwrap` are reviewed package capabilities/configuration for this CLI appliance, not hidden install behaviour.
67
69
 
68
70
  ## Personal MCP Appliance
69
71
 
70
- The personal-appliance contract keeps that surface intentionally narrow: one trusted user runs the gateway on a machine or volume they own, connects one MCP endpoint, and asks any connected client for cross-LLM validation.
72
+ The personal-appliance contract keeps that surface intentionally narrow: one trusted user runs the gateway on a machine or volume they own, connects one MCP endpoint, and lets supported clients operate local coding agents through workspace-scoped, approval-gated, auditable requests.
71
73
 
72
74
  The product contract is documented in [docs/personal-mcp/PRODUCT_CONTRACT.md](docs/personal-mcp/PRODUCT_CONTRACT.md). It defines the single-user scope, security posture, target support matrix, and provider-support verification gates. Public setup guides must not claim ChatGPT, Claude web, Claude Desktop, Codex, Gemini CLI, Gemini web, or Grok inbound support until the corresponding provider/client path has been verified.
73
75
 
@@ -190,15 +192,15 @@ Every `*_request` and `*_request_async` tool except `devin_request` / `devin_req
190
192
 
191
193
  Per-CLI capability matrix (prefix discipline is automatic via `promptParts` for all providers except Devin and Cursor, which have no `promptParts` surface; explicit levers are provider-specific):
192
194
 
193
- | CLI | Prefix discipline | Explicit lever(s) |
194
- | ------- | ----------------- | ----------------- |
195
+ | CLI | Prefix discipline | Explicit lever(s) |
196
+ | ------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
195
197
  | 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
- | devin | no | plain `prompt` only |
201
- | cursor | no | plain `prompt` only |
198
+ | codex | yes | none (OpenAI implicit) |
199
+ | gemini | yes | none (implicit server-side) |
200
+ | grok | yes | `compactionMode` / `compactionDetail` (context compaction: `summary | transcript | segments`; `segments` writes per-segment markdown) |
201
+ | mistral | yes | none (implicit) |
202
+ | devin | no | plain `prompt` only |
203
+ | cursor | no | plain `prompt` only |
202
204
 
203
205
  **Claude example (explicit cacheControl)**
204
206
 
@@ -208,10 +210,10 @@ claude_request({
208
210
  system: "You are a helpful code reviewer.",
209
211
  context: "<long stable file dump>",
210
212
  task: "Review the diff.",
211
- cacheControl: { system: true, context: true } // task is never marked
213
+ cacheControl: { system: true, context: true }, // task is never marked
212
214
  },
213
- outputFormat: "stream-json"
214
- })
215
+ outputFormat: "stream-json",
216
+ });
215
217
  ```
216
218
 
217
219
  Gateway emits the `stream-json` stdin path with `cache_control: {type:"ephemeral", ttl:"1h"}` on marked blocks only.
@@ -222,8 +224,8 @@ Gateway emits the `stream-json` stdin path with `cache_control: {type:"ephemeral
222
224
  grok_request({
223
225
  promptParts: { system: "...", context: "...", task: "..." },
224
226
  compactionMode: "segments",
225
- compactionDetail: "balanced"
226
- })
227
+ compactionDetail: "balanced",
228
+ });
227
229
  ```
228
230
 
229
231
  Emits `--compaction-mode segments --compaction-detail balanced`.
@@ -249,6 +251,26 @@ Opt-in flags (all default off) live under `[cache_awareness]` in `~/.llm-cli-gat
249
251
  - **Type Safety**: Strict TypeScript with comprehensive error handling
250
252
  - **Supply-chain hardening**: a dedicated `.github/workflows/security.yml` runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, and lychee on every push and PR (see `SECURITY.md` for the threat model)
251
253
 
254
+ ## Provider capability surface
255
+
256
+ Every provider is reachable through the same request, session, job, and validation machinery, but the underlying CLIs differ in what they natively expose. The table records what actually shipped per provider; discover the live surface at runtime with `provider_tool_capabilities`, `list_models`, and the `provider-acp://<provider>` / `provider-tools://<provider>` resources.
257
+
258
+ | Provider | CLI request tools | Native ACP | Live model discovery | Admin surface |
259
+ | --- | --- | --- | --- | --- |
260
+ | Claude Code (`claude`) | `claude_request` / `_async` | None (CLI-first; no ACP entrypoint at claude 2.1.198) | model aliases, reasoning-effort levels, fallback model | read-only via `provider_admin_list` / `provider_admin_run` |
261
+ | OpenAI Codex (`codex`) | `codex_request` / `_async`, `codex_fork_session` | None (codex-cli 0.142.4 advertises mcp-server / app-server transports, not native ACP) | `codex debug models` | read-only via `provider_admin_list` / `provider_admin_run` |
262
+ | Gemini / Antigravity (`gemini`, `agy`) | `gemini_request` / `_async` | None (agy 1.0.14 exposes no ACP entrypoint; legacy Gemini CLI ACP evidence does not transfer) | `agy models` | read-only via `provider_admin_list` / `provider_admin_run` |
263
+ | xAI Grok (`grok`) | `grok_request` (sync `transport: "acp"`) / `_async` | Native via `grok agent stdio` | `grok models` + `~/.grok/config.toml` | read-only via `provider_admin_list` / `provider_admin_run` |
264
+ | Mistral Vibe (`mistral`) | `mistral_request` (sync `transport: "acp"`) / `_async` | Native via `vibe-acp` | Vibe config plus the `VIBE_ACTIVE_MODEL` active model and agent profiles | read-only via `provider_admin_list` / `provider_admin_run` |
265
+ | Cognition Devin (`devin`) | `devin_request` (sync `transport: "acp"`, `agentType: summarizer\|review`) / `_async` | Native via `devin acp` | `--model` / `DEVIN_MODEL` | read-only via `provider_admin_list` / `provider_admin_run` |
266
+ | Cursor Agent (`cursor`) | `cursor_request` (sync `transport: "acp"`) / `_async` | Native via `cursor-agent acp` (companion-owned) | model aliases | read-only via `provider_admin_list` / `provider_admin_run` |
267
+
268
+ - **Native ACP** is reported honestly. `grok`, `mistral`, `devin`, and `cursor` expose a native ACP entrypoint, so `provider-acp://<provider>` carries the negotiated `initialize` capability set and the derived session-method availability, and the sync `*_request` accepts `transport: "acp"` (fails closed unless `[acp]` and the provider's `runtime_enabled` gate are set). ACP routing is sync-only: the `*_request_async` variants always run the CLI transport and do not accept `transport: "acp"` (nor Devin's `agentType`); async ACP parity is a later phase. `claude`, `codex`, and `gemini` have no native ACP entrypoint at their target CLI versions; their `provider-acp://` records report `native: false` with no methods and no adapter-as-native masquerade, and they expose no `transport: "acp"` selector.
269
+ - **Resources** are generated from the provider registry for every CLI provider: `models://<provider>`, `sessions://<provider>`, `provider-acp://<provider>`, `provider-tools://<provider>`, and `provider-subcommands://<provider>`.
270
+ - **Model discovery** is live and account-aware: the discovery listed above reaches `models://<provider>` and `list_models`, degrading to static registry facts when a live probe is unavailable (a resource read never spawns a CLI).
271
+ - **Admin surfaces** are discovery-driven and output-redacted. `provider_admin_list` and `provider_admin_run` are read-only for every provider. State-mutating admin operations are exposed only through `provider_admin_mutate`, gated behind `[admin] allow_mutating_cli_admin_ops`, the remote `cli:admin` scope, an approval gate, and an audit record. Mutating ACP session operations are likewise gated behind `[acp] allow_mutating_session_ops`.
272
+ - **Validation** commands work across every provider: `validate_with_models`, `second_opinion`, `compare_answers`, `red_team_review`, `consensus_check`, `ask_model`, and `synthesize_validation`, with durable signed receipts via `validation_receipt` and the `validation-receipt://{validationId}` resource.
273
+
252
274
  ## Prerequisites
253
275
 
254
276
  **Node.js >= 24.4.0** is required (`engines.node` in `package.json`). The gateway uses Node's built-in `node:sqlite` module for persistence — there is no native binding to compile and no install scripts run. The 24.4 floor is where `allowBareNamedParameters` defaults to `true`, which the persistence layer relies on.
@@ -389,7 +411,7 @@ The personal-appliance surface exposes simplified validation tools for non-devel
389
411
  - `synthesize_validation`: run an explicit judge model after provider results have been collected.
390
412
  - `list_available_models`: list the models each provider CLI exposes through the simplified surface.
391
413
  - `job_status` and `job_result`: poll and collect validation job outputs.
392
- - `validation_receipt`: retrieve the immutable receipt of a terminal cross-LLM validation run by `validationId` (returns `minted | pending | expired_unminted | not_found`, own-or-not-found). `format: "markdown"` renders a human-readable report; `includeRawResponses` inlines provider answer text. Registered only under the durable persistence gate (`backend = "sqlite"` with an attached validation-run store).
414
+ - `validation_receipt`: retrieve the immutable receipt of a terminal cross-LLM validation run by `validationId` (returns `minted | pending | expired_unminted | not_found`, own-or-not-found). `format: "markdown"` renders a human-readable report; `includeRawResponses` inlines provider answer text. Registered only when the attached job store provides the durable validation-run store capability (`sqlite` and `postgres`).
393
415
 
394
416
  The same receipt is also exposed as the `validation-receipt://{validationId}` MCP resource (same durable gate and own-or-not-found owner scoping).
395
417
 
@@ -574,6 +596,7 @@ Execute a Grok CLI (xAI) request with session support.
574
596
 
575
597
  - `prompt` (string, optional*): The prompt to send (1-100,000 chars). *Exactly one of `prompt` or `promptParts` is required (mutually exclusive)
576
598
  - `model` (string, optional): Model name or alias (e.g. `grok-build`, `latest`)
599
+ - `transport` (string, optional): `"cli"` (default) runs the Grok CLI; `"acp"` routes through Grok's native `grok agent stdio` transport when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise). Sync-only: `grok_request_async` always runs the CLI transport and does not accept `transport`
577
600
  - `outputFormat` (string, optional): `"plain"` (default), `"json"`, or `"streaming-json"`
578
601
  - `sessionId` (string, optional): Session ID to resume (`--resume <id>`)
579
602
  - `resumeLatest` (boolean, optional): Resume the most recent session in the current cwd (`--continue`)
@@ -636,7 +659,7 @@ Every async job is persisted to a job store as it transitions through running
636
659
 
637
660
  - **Re-issuing a request is safe.** Identical `*_request` / `*_request_async` calls within the dedup window (default 1 hour) short-circuit onto the existing running or completed job — the caller gets back the same job ID instead of starting a duplicate run. This directly fixes the "agent times out polling, re-issues, and the whole job starts over" failure mode.
638
661
  - **`llm_job_status` and `llm_job_result` work across gateway restarts.** Job rows live for 30 days by default; callers can collect results long after the in-memory cache has evicted them.
639
- - **Jobs running at shutdown are marked `orphaned`** on the next gateway boot (the detached child can't be reattached to). Their captured partial output remains readable.
662
+ - **A job is marked `orphaned` only when its owning gateway instance is provably gone**, never because another instance restarted. Each instance holds a periodic heartbeat lease and stamps every job it owns; the recovery sweep orphans a `queued`/`running` job only when that job's own lease has expired. On a shared store (`backend = "postgres"`) this means a fresh instance never orphans another live instance's in-flight jobs. The captured partial output of a genuinely orphaned job remains readable, and a stale-then-reviving owner that later finishes self-heals to the correct terminal state (issue #139).
640
663
  - **Pass `forceRefresh: true`** on any request tool to bypass dedup and force a fresh CLI run.
641
664
 
642
665
  ##### Persistence configuration
@@ -647,17 +670,29 @@ The job-store backend is configured by `~/.llm-cli-gateway/config.toml` (overrid
647
670
  [persistence]
648
671
  backend = "sqlite" # "sqlite" | "memory" | "postgres" | "none"
649
672
  path = "~/.llm-cli-gateway/logs.db" # for sqlite
650
- # dsn = "postgresql://user:pw@host/db" # for postgres (interface only — impl not yet shipped)
673
+ # dsn = "postgresql://user:pw@host/db" # for postgres
651
674
  retentionDays = 30
652
675
  dedupWindowMs = 3600000
653
676
  acknowledgeEphemeral = false # required to enable async tools with memory backend
677
+
678
+ # Issue #139 durable orphan-recovery lease (defaults shown). Each instance
679
+ # advances a per-job lease on every heartbeat; the sweep orphans a job only
680
+ # after its own lease expires, so a fresh instance never orphans another live
681
+ # instance's jobs on a shared store. Validated: leaseTtl >= 2*heartbeat and
682
+ # httpJobGrace >= leaseTtl.
683
+ instanceHeartbeatMs = 15000 # heartbeat cadence
684
+ instanceLeaseTtlMs = 90000 # per-job lease TTL (6x heartbeat)
685
+ httpJobGraceMs = 300000 # extra grace for no-pid http jobs (5 min)
686
+ orphanSweepIntervalMs = 30000 # reaper cadence
687
+ instanceGcMs = 3600000 # gateway_instances GC horizon
688
+ # ownsOrphanRecovery = false # DEPRECATED (#139): superseded by the lease; parsed + warned, no longer used
654
689
  ```
655
690
 
656
691
  Backends:
657
692
 
658
693
  - **`sqlite`** (default) — durable, file-backed. Safe for single-instance deployments.
694
+ - **`postgres`** — durable PostgreSQL-backed async job, dedup, orphan recovery, HTTP job, and validation receipt storage. Use this for multi-instance or service deployments. Requires the optional peer dependency `pg` to be installed alongside the gateway.
659
695
  - **`memory`** — in-process Map. Lost on gateway exit. Requires `acknowledgeEphemeral = true` to be loaded. Suitable for tests and ephemeral CI gateways.
660
- - **`postgres`** — interface only, implementation not yet shipped. Selecting this backend throws at startup.
661
696
  - **`none`** — no store. **`*_request_async`, `llm_job_status`, `llm_job_result`, and `llm_job_cancel` are NOT registered on the gateway.** This is a structural invariant: agents that try to call async tools against a gateway with `backend = "none"` get a clean "tool not found" at connect time instead of silent in-memory loss after the 1-hour TTL. Use `llm_process_health` to inspect the resolved persistence state programmatically.
662
697
 
663
698
  Legacy environment variables (deprecated; emit a warning at startup):
@@ -922,6 +957,7 @@ consumes = ["OUT:mcp-reconnected"]
922
957
  Run a Mistral Vibe agentic coding request. Like `grok_request` in shape, but with Vibe's specific surface:
923
958
 
924
959
  - `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.
960
+ - `transport` (string, optional): `"cli"` (default) runs the Vibe CLI; `"acp"` routes through Vibe's native `vibe-acp` transport when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise). Sync-only: `mistral_request_async` always runs the CLI transport and does not accept `transport`
925
961
  - `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.
926
962
  - `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry (allow-list only).
927
963
  - `disallowedTools` (string[], optional): Accepted for parity with the other providers; ignored at the CLI boundary with a logged warning.
@@ -951,7 +987,8 @@ Run a Cognition Devin CLI request synchronously (headless print mode, `devin -p`
951
987
 
952
988
  - `prompt` (string, optional*): Prompt text for Devin CLI (1-100,000 chars). Required in practice; `promptFile` is additive
953
989
  - `model` (string, optional): Model name or alias (e.g. `opus`, `latest`)
954
- - `transport` (string, optional): `"cli"` (default) runs the Devin CLI; `"acp"` routes through `devin acp` when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise)
990
+ - `transport` (string, optional): `"cli"` (default) runs the Devin CLI; `"acp"` routes through Devin's native `devin acp` transport when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise). Sync-only: `devin_request_async` always runs the CLI transport and accepts neither `transport` nor `agentType`
991
+ - `agentType` (string, optional): ACP agent variant for `transport: "acp"` (`devin acp --agent-type`): `"summarizer"` (no tools, text summary) or `"review"` (read-only plus shell code-review); ignored for the CLI transport
955
992
  - `permissionMode` (string, optional): Devin CLI permission mode (`--permission-mode`): `auto` (auto-approves read-only tools), `smart` (also auto-runs actions a fast model judges safe), `dangerous` (auto-approves all). Omit to use Devin's headless default
956
993
  - `promptFile` (string, optional): Load the initial prompt from a file (`--prompt-file`)
957
994
  - `sessionId` (string, optional): Devin session ID to resume (`--resume <id>`). The `gw-*` id minted for a brand-new session is not resumable via `sessionId`; continue with `resumeLatest: true`
@@ -972,7 +1009,7 @@ Run a Cursor Agent CLI request synchronously. Defaults to headless print mode (`
972
1009
  - `model` (string, optional): Model name or alias (for example `gpt-5`, `sonnet-4-thinking`, or `latest`)
973
1010
  - `mode` (string, optional): Cursor mode, `"plan"` or `"ask"` (`--mode`)
974
1011
  - `outputFormat` (string, optional): `"text"` (default), `"json"`, or `"stream-json"`
975
- - `transport` (string, optional): `"cli"` (default) or `"acp"`; ACP fails closed unless enabled in gateway config and rejects unsupported Cursor CLI-only controls instead of dropping them
1012
+ - `transport` (string, optional): `"cli"` (default) or `"acp"`; ACP fails closed unless enabled in gateway config and rejects unsupported Cursor CLI-only controls instead of dropping them. Sync-only: `cursor_request_async` always runs the CLI transport and does not accept `transport`
976
1013
  - `force` (boolean, optional): Emit `--force` for non-interactive operation
977
1014
  - `autoReview` (boolean, optional): Emit `--auto-review`
978
1015
  - `sandbox` (string, optional): `"enabled"` or `"disabled"` (`--sandbox`)
@@ -1317,7 +1354,7 @@ The API key value is never emitted on any of these surfaces (only the env var na
1317
1354
 
1318
1355
  ### Security note
1319
1356
 
1320
- The resolved API key is excluded from `payloadJson`, the dedup key, logs, and the flight recorder. However, the **request prompt is persisted in plaintext** in the async job store (the SQLite file at `[persistence].path`, default `~/.llm-cli-gateway/logs.db`) and is not covered by secret redaction. This mirrors the CLI tools, whose prompt is persisted in `argsJson` whenever it is passed as a command argument (a few CLI paths instead stream the prompt over stdin and so do not persist it). Treat the job store as sensitive at rest. See [Security Considerations](#security-considerations).
1357
+ The resolved API key is excluded from `payloadJson`, the dedup key, logs, and the flight recorder. However, the **request prompt is persisted in plaintext** in the async job store (SQLite at `[persistence].path`, default `~/.llm-cli-gateway/logs.db`, or Postgres rows under `backend = "postgres"`) and is not covered by secret redaction. This mirrors the CLI tools, whose prompt is persisted in `argsJson` whenever it is passed as a command argument (a few CLI paths instead stream the prompt over stdin and so do not persist it). Treat the job store as sensitive at rest. See [Security Considerations](#security-considerations).
1321
1358
 
1322
1359
  ## Session Management
1323
1360
 
@@ -1384,9 +1421,11 @@ await callTool("session_delete", {
1384
1421
  - **Gemini (Antigravity)** → `default` / prompted, i.e. **no** `--dangerously-skip-permissions` (the `agy` CLI has no accept-edits middle rung, so the safe default is prompted execution; without the opt-in, Gemini cannot auto-approve mutating tools under `mcp_managed`)
1385
1422
 
1386
1423
  Set to `1`/`true` to let the operator opt back in: this permits bypass requests through the approval gate **and** restores each provider's full auto-approve mode under `mcp_managed` (Claude `bypassPermissions`, Grok `--always-approve`, Mistral `--agent auto-approve`, Gemini `--dangerously-skip-permissions`). Sandboxed auto modes (e.g. codex `--sandbox workspace-write`) are unaffected.
1424
+
1387
1425
  ```bash
1388
1426
  LLM_GATEWAY_APPROVAL_ALLOW_BYPASS=1 node dist/index.js
1389
1427
  ```
1428
+
1390
1429
  - `LLM_GATEWAY_TRUSTED_PRINCIPAL_HEADER`: Name of an HTTP header carrying the authenticated user identity asserted by a **trusted front door** (any identity-aware reverse proxy / IdP). When set, the gateway adopts that header value as the request's ownership principal — but **only** for requests authenticated with the gateway's own static bearer token (i.e. the trusted upstream proxy), never from an arbitrary remote client. Off by default; IdP-agnostic. Lets a proxy-fronted multi-user deployment carry per-user identity into the gateway.
1391
1430
  ```bash
1392
1431
  LLM_GATEWAY_TRUSTED_PRINCIPAL_HEADER=x-gateway-principal node dist/index.js
@@ -1575,11 +1614,11 @@ The gateway supports concurrent requests across different CLIs. Each request spa
1575
1614
 
1576
1615
  - **Input Validation**: All prompts are validated (min 1 char, max 100k chars)
1577
1616
  - **API-provider keys**: For `[providers.<name>]` HTTP providers, the gateway reads the key from the named environment variable at request time only. The resolved key is excluded from the persisted `payloadJson`, the dedup key, logs, and the flight recorder, and is never surfaced on the discovery/diagnostic surfaces (which report only the env var name and a presence boolean). `base_url` userinfo is redacted on the diagnostic surfaces. See [API providers (HTTP)](#api-providers-http).
1578
- - **Prompt persistence at rest**: Async job rows store the request **prompt in plaintext** (HTTP `payloadJson`, and CLI `argsJson` whenever the prompt is passed as a command argument rather than streamed over stdin); this is not covered by secret redaction. The SQLite job-store file (default `~/.llm-cli-gateway/logs.db`, configurable via `[persistence].path`) is `chmod`ed to `0o600` on non-Windows hosts; treat it as sensitive and scope/rotate it like any prompt log. Set `[persistence].backend = "none"` to disable the async job store entirely (the `*_request_async` / `llm_job_*` tools are then not registered).
1617
+ - **Prompt persistence at rest**: Async job rows store the request **prompt in plaintext** (HTTP `payloadJson`, and CLI `argsJson` whenever the prompt is passed as a command argument rather than streamed over stdin); this is not covered by secret redaction. The SQLite job-store file (default `~/.llm-cli-gateway/logs.db`, configurable via `[persistence].path`) is `chmod`ed to `0o600` on non-Windows hosts; the Postgres backend stores the same fields in database rows. Treat either backend as sensitive and scope/rotate it like any prompt log. Set `[persistence].backend = "none"` to disable the async job store entirely (the `*_request_async` / `llm_job_*` tools are then not registered).
1579
1618
  - **Command Execution**: Uses `spawn` with separate arguments (not shell execution)
1580
1619
  - **No Eval**: No dynamic code evaluation in our source (see "Socket alerts" below for the transitive `ajv` codegen case)
1581
1620
  - **Sandboxing**: Consider running in containers for production use
1582
- - **Provenance**: Releases are published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) via OIDC trusted publishing from GitHub Actions
1621
+ - **npm publish control**: npm releases are gated by the generated prod-only shrinkwrap, release security audit, packed-consumer checks, and a publish token fetched at runtime from Azure Key Vault through GitHub OIDC to Entra
1583
1622
  - **Release signing**: GitHub release installer artifacts are signed with Sigstore keyless signing; verify `SHA256SUMS.sigstore.json` before trusting the checksum file
1584
1623
 
1585
1624
  ### Socket alerts — context for reviewers
@@ -1,6 +1,6 @@
1
1
  import { AcpError } from "./errors.js";
2
2
  import type { JsonRpcStdioTransport, JsonRpcId } from "./json-rpc-stdio.js";
3
- import { type ContentBlock, type InitializeResponse, type ReadTextFileRequest, type ReadTextFileResponse, type RequestPermissionRequest, type RequestPermissionResponse, type SessionLoadResponse, type SessionNewResponse, type SessionPromptResponse, type SessionUpdateNotification, type WriteTextFileRequest, type WriteTextFileResponse } from "./types.js";
3
+ import { type CloseSessionResponse, type ContentBlock, type DeleteSessionResponse, type InitializeResponse, type ListSessionsResponse, type ReadTextFileRequest, type ReadTextFileResponse, type RequestPermissionRequest, type RequestPermissionResponse, type SessionLoadResponse, type SessionNewResponse, type SessionPromptResponse, type SessionResumeResponse, type SessionUpdateNotification, type SetSessionConfigOptionResponse, type SetSessionModeResponse, type WriteTextFileRequest, type WriteTextFileResponse } from "./types.js";
4
4
  import type { Logger } from "../logger.js";
5
5
  import type { CliType } from "../session-manager.js";
6
6
  export declare const DEFAULT_ACP_PROTOCOL_VERSION = 1;
@@ -25,6 +25,7 @@ export interface AcpClientOptions {
25
25
  readonly logger?: Logger;
26
26
  readonly protocolVersion?: number;
27
27
  readonly timeouts?: AcpClientTimeouts;
28
+ readonly allowMutatingSessionOps?: boolean;
28
29
  }
29
30
  export interface AcpClientTimeouts {
30
31
  readonly initializeMs?: number;
@@ -48,6 +49,18 @@ export interface PromptParams {
48
49
  readonly sessionId: string;
49
50
  readonly prompt: ReadonlyArray<ContentBlock>;
50
51
  }
52
+ export interface ResumeSessionParams extends NewSessionParams {
53
+ readonly sessionId: string;
54
+ }
55
+ export interface SetSessionModeParams {
56
+ readonly sessionId: string;
57
+ readonly modeId: string;
58
+ }
59
+ export interface SetSessionConfigOptionParams {
60
+ readonly sessionId: string;
61
+ readonly configId: string;
62
+ readonly value: string;
63
+ }
51
64
  export declare class AcpClient {
52
65
  private readonly transport;
53
66
  private readonly provider;
@@ -56,16 +69,31 @@ export declare class AcpClient {
56
69
  private readonly logger;
57
70
  private readonly protocolVersion;
58
71
  private readonly timeouts;
72
+ private readonly allowMutatingSessionOps;
59
73
  private initialized;
60
74
  private initializeResult;
75
+ private methodAvailability;
76
+ private static readonly MUTATING_METHODS;
61
77
  constructor(options: AcpClientOptions);
62
78
  get isInitialized(): boolean;
63
79
  get agentInfo(): InitializeResponse | null;
80
+ get availableMethods(): ReadonlySet<string>;
81
+ supportsMethod(method: string): boolean;
64
82
  initialize(options?: InitializeOptions): Promise<InitializeResponse>;
65
83
  newSession(params: NewSessionParams): Promise<SessionNewResponse>;
66
84
  loadSession(params: LoadSessionParams): Promise<SessionLoadResponse>;
67
85
  prompt(params: PromptParams): Promise<SessionPromptResponse>;
68
86
  cancel(sessionId: string): void;
87
+ resumeSession(params: ResumeSessionParams): Promise<SessionResumeResponse>;
88
+ listSessions(params?: {
89
+ cursor?: string;
90
+ }): Promise<ListSessionsResponse>;
91
+ closeSession(sessionId: string): Promise<CloseSessionResponse>;
92
+ deleteSession(sessionId: string): Promise<DeleteSessionResponse>;
93
+ setSessionMode(params: SetSessionModeParams): Promise<SetSessionModeResponse>;
94
+ setSessionConfigOption(params: SetSessionConfigOptionParams): Promise<SetSessionConfigOptionResponse>;
95
+ private augmentSessionMethods;
96
+ private assertMethodAvailable;
69
97
  private send;
70
98
  private normalizeError;
71
99
  private assertInitialized;
@@ -1,5 +1,5 @@
1
- import { AcpProtocolError, isAcpError } from "./errors.js";
2
- import { parseInitializeResponse, parseReadTextFileRequest, parseRequestPermissionRequest, parseSessionLoadResponse, parseSessionNewResponse, parseSessionPromptResponse, parseSessionUpdateNotification, parseWriteTextFileRequest, } from "./types.js";
1
+ import { AcpMethodUnsupportedError, AcpMutatingDisabledError, AcpProtocolError, isAcpError, } from "./errors.js";
2
+ import { deriveAcpMethodAvailability, parseCloseSessionResponse, parseDeleteSessionResponse, parseInitializeResponse, parseListSessionsResponse, parseReadTextFileRequest, parseRequestPermissionRequest, parseSessionLoadResponse, parseSessionNewResponse, parseSessionPromptResponse, parseSessionResumeResponse, parseSessionUpdateNotification, parseSetSessionConfigOptionResponse, parseSetSessionModeResponse, parseWriteTextFileRequest, sessionResponseMethods, } from "./types.js";
3
3
  import { noopLogger } from "../logger.js";
4
4
  export const DEFAULT_ACP_PROTOCOL_VERSION = 1;
5
5
  export class AcpClient {
@@ -10,8 +10,15 @@ export class AcpClient {
10
10
  logger;
11
11
  protocolVersion;
12
12
  timeouts;
13
+ allowMutatingSessionOps;
13
14
  initialized = false;
14
15
  initializeResult = null;
16
+ methodAvailability = new Set();
17
+ static MUTATING_METHODS = new Set([
18
+ "session/delete",
19
+ "session/set_mode",
20
+ "session/set_config_option",
21
+ ]);
15
22
  constructor(options) {
16
23
  this.transport = options.transport;
17
24
  this.provider = options.provider;
@@ -20,6 +27,7 @@ export class AcpClient {
20
27
  this.logger = options.logger ?? noopLogger;
21
28
  this.protocolVersion = options.protocolVersion ?? DEFAULT_ACP_PROTOCOL_VERSION;
22
29
  this.timeouts = options.timeouts ?? {};
30
+ this.allowMutatingSessionOps = options.allowMutatingSessionOps ?? false;
23
31
  }
24
32
  get isInitialized() {
25
33
  return this.initialized;
@@ -27,6 +35,12 @@ export class AcpClient {
27
35
  get agentInfo() {
28
36
  return this.initializeResult;
29
37
  }
38
+ get availableMethods() {
39
+ return new Set(this.methodAvailability);
40
+ }
41
+ supportsMethod(method) {
42
+ return this.methodAvailability.has(method);
43
+ }
30
44
  async initialize(options = {}) {
31
45
  if (this.initialized && this.initializeResult) {
32
46
  return this.initializeResult;
@@ -45,12 +59,15 @@ export class AcpClient {
45
59
  const result = parseInitializeResponse(raw, this.provider);
46
60
  this.initialized = true;
47
61
  this.initializeResult = result;
62
+ this.methodAvailability = new Set(deriveAcpMethodAvailability(result));
48
63
  return result;
49
64
  }
50
65
  async newSession(params) {
51
66
  this.assertInitialized("session/new");
52
67
  const raw = await this.send("session/new", { cwd: params.cwd, mcpServers: params.mcpServers ?? [] }, this.timeouts.sessionNewMs);
53
- return parseSessionNewResponse(raw, this.provider);
68
+ const response = parseSessionNewResponse(raw, this.provider);
69
+ this.augmentSessionMethods(response);
70
+ return response;
54
71
  }
55
72
  async loadSession(params) {
56
73
  this.assertInitialized("session/load");
@@ -59,7 +76,9 @@ export class AcpClient {
59
76
  cwd: params.cwd,
60
77
  mcpServers: params.mcpServers ?? [],
61
78
  }, this.timeouts.sessionLoadMs);
62
- return parseSessionLoadResponse(raw, this.provider);
79
+ const response = parseSessionLoadResponse(raw, this.provider);
80
+ this.augmentSessionMethods(response);
81
+ return response;
63
82
  }
64
83
  async prompt(params) {
65
84
  this.assertInitialized("session/prompt");
@@ -69,6 +88,61 @@ export class AcpClient {
69
88
  cancel(sessionId) {
70
89
  this.transport.notify("session/cancel", { sessionId });
71
90
  }
91
+ async resumeSession(params) {
92
+ this.assertMethodAvailable("session/resume");
93
+ const raw = await this.send("session/resume", { sessionId: params.sessionId, cwd: params.cwd, mcpServers: params.mcpServers ?? [] }, this.timeouts.sessionLoadMs);
94
+ const response = parseSessionResumeResponse(raw, this.provider);
95
+ this.augmentSessionMethods(response);
96
+ return response;
97
+ }
98
+ async listSessions(params = {}) {
99
+ this.assertMethodAvailable("session/list");
100
+ const raw = await this.send("session/list", { cursor: params.cursor });
101
+ return parseListSessionsResponse(raw, this.provider);
102
+ }
103
+ async closeSession(sessionId) {
104
+ this.assertMethodAvailable("session/close");
105
+ const raw = await this.send("session/close", { sessionId });
106
+ return parseCloseSessionResponse(raw, this.provider);
107
+ }
108
+ async deleteSession(sessionId) {
109
+ this.assertMethodAvailable("session/delete");
110
+ const raw = await this.send("session/delete", { sessionId });
111
+ return parseDeleteSessionResponse(raw, this.provider);
112
+ }
113
+ async setSessionMode(params) {
114
+ this.assertMethodAvailable("session/set_mode");
115
+ const raw = await this.send("session/set_mode", {
116
+ sessionId: params.sessionId,
117
+ modeId: params.modeId,
118
+ });
119
+ return parseSetSessionModeResponse(raw, this.provider);
120
+ }
121
+ async setSessionConfigOption(params) {
122
+ this.assertMethodAvailable("session/set_config_option");
123
+ const raw = await this.send("session/set_config_option", {
124
+ sessionId: params.sessionId,
125
+ configId: params.configId,
126
+ value: params.value,
127
+ });
128
+ return parseSetSessionConfigOptionResponse(raw, this.provider);
129
+ }
130
+ augmentSessionMethods(response) {
131
+ for (const method of sessionResponseMethods(response)) {
132
+ this.methodAvailability.add(method);
133
+ }
134
+ }
135
+ assertMethodAvailable(method) {
136
+ this.assertInitialized(method);
137
+ if (!this.methodAvailability.has(method)) {
138
+ throw new AcpMethodUnsupportedError(this.provider, method, {
139
+ reason: "capability_not_advertised",
140
+ });
141
+ }
142
+ if (AcpClient.MUTATING_METHODS.has(method) && !this.allowMutatingSessionOps) {
143
+ throw new AcpMutatingDisabledError(this.provider, method, { reason: "config_gate_off" });
144
+ }
145
+ }
72
146
  async send(method, params, timeoutMs) {
73
147
  try {
74
148
  return await this.transport.request(method, params, timeoutMs);
@@ -1,5 +1,5 @@
1
1
  import type { CliType } from "../session-manager.js";
2
- export type AcpErrorKind = "acp_disabled" | "provider_acp_disabled" | "provider_acp_unsupported" | "provider_runtime_disabled" | "provider_unavailable" | "protocol" | "timeout" | "permission_denied" | "process_exit";
2
+ export type AcpErrorKind = "acp_disabled" | "provider_acp_disabled" | "provider_acp_unsupported" | "provider_runtime_disabled" | "provider_unavailable" | "method_unsupported" | "mutating_disabled" | "protocol" | "timeout" | "permission_denied" | "process_exit";
3
3
  export declare function redactAcpMessage(input: string): string;
4
4
  export declare function redactAcpDebug(value: unknown): unknown;
5
5
  export declare function redactAcpCause(cause: unknown): unknown;
@@ -32,6 +32,14 @@ export declare class ProviderRuntimeDisabledError extends AcpError {
32
32
  export declare class ProviderUnavailableError extends AcpError {
33
33
  constructor(provider: CliType, reason: string, debug?: AcpErrorDebug);
34
34
  }
35
+ export declare class AcpMethodUnsupportedError extends AcpError {
36
+ readonly method: string;
37
+ constructor(provider: CliType, method: string, debug?: AcpErrorDebug);
38
+ }
39
+ export declare class AcpMutatingDisabledError extends AcpError {
40
+ readonly method: string;
41
+ constructor(provider: CliType, method: string, debug?: AcpErrorDebug);
42
+ }
35
43
  export declare class AcpProtocolError extends AcpError {
36
44
  readonly code?: number;
37
45
  constructor(userMessage: string, options?: {