ast-mcp-server 0.7.2 → 0.8.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 +33 -1
- package/README.md +36 -14
- package/SECURITY.md +8 -8
- package/dist/cli.js +18 -14
- package/dist/services/agent-setup.js +248 -27
- package/dist/services/agent-targets.js +209 -63
- package/dist/services/checkbox-state.js +31 -0
- package/dist/services/managed-file.js +709 -0
- package/dist/services/managed-guidance.js +229 -0
- package/dist/services/opencode-config.js +135 -0
- package/dist/services/raw-tty.js +75 -0
- package/dist/services/setup-wizard.js +45 -93
- package/dist/services/skill-installer.js +145 -84
- package/docs/support.md +13 -13
- package/package.json +6 -3
- package/skills/structural-code-editing/SKILL.md +9 -3
- package/skills/structural-code-editing/guidance.md +3 -0
- package/skills/structural-code-editing/releases.json +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,45 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.8.0] — release candidate (unreleased, 2026-08-14)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- OpenCode effective-config discovery and verification now run against disposable routed copies because the client normalizes both configured files even for diagnostic commands; the real planned destination remains snapshot-protected.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added managed `structural-code-editing` activation guidance for the verified Claude, OpenCode, Codex, and Gemini global instruction surfaces; Hermes and Copilot remain explicitly skill-only.
|
|
18
|
+
- Added an exact SHA-256 release manifest for safe upgrades from registry-proven skill bytes and packaged the canonical marker-free guidance payload.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Setup now preflights MCP, skill, and guidance across every selected client before mutation, preserves user-owned instruction bytes, applies snapshot-checked descriptor-bound file writes, and reports schema v2 with logical outcomes plus completed, possibly committed, rolled-back, rollback-failed, pending, and successful physical asset states.
|
|
23
|
+
- `install-skill` uses the same official-byte manifest but remains skill-only; explicit force never bypasses guidance ownership, route, or race checks.
|
|
24
|
+
- Managed file writes hold staged and preimage inodes through publication, use no-clobber descriptor links for creation and atomic exchange with exact-pair rollback for replacement, reject same-inode content or mode races before cleanup, then reauthenticate unchanged/completed postimages before later asset or MCP writes.
|
|
25
|
+
|
|
26
|
+
### Release status
|
|
27
|
+
|
|
28
|
+
- This version is bound to exact-SHA CI and the repository's staged `next` publication, public-registry verification, `latest` promotion, annotated-tag, and hosted-release sequence. npm registry and GitHub readbacks remain authoritative for those external transitions.
|
|
29
|
+
|
|
30
|
+
## [0.7.2] — published to `latest` and `next`, registry verified (2026-08-13)
|
|
8
31
|
|
|
9
32
|
### Fixed
|
|
10
33
|
|
|
11
34
|
- Promotion preflight now validates raw npm registry metadata exactly once, compares live integrity with the verified artifact before and after mutation, and classifies the normalized readback without reinterpreting its field schema.
|
|
12
35
|
- Added a composed regression covering eligible, already-promoted, and integrity-drift promotion readbacks.
|
|
13
36
|
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Expanded convergent setup to Claude Code, Hermes, OpenCode, Codex CLI, Gemini CLI, and GitHub Copilot CLI with fail-closed versioned contracts.
|
|
40
|
+
- Added native checkbox selection, shared `.agents/skills` planning, routed JSONC-safe OpenCode configuration, bounded correlated diagnostics, and fixture admission gates.
|
|
41
|
+
|
|
42
|
+
### Release status
|
|
43
|
+
|
|
44
|
+
- npm exposes the immutable published `0.7.2` bytes under both `latest` and `next`. The `0.8.0` section above describes distinct release bytes and cannot be attributed to that package version.
|
|
45
|
+
|
|
14
46
|
## [0.7.1] — published to `next`, verified, not promoted (2026-08-12)
|
|
15
47
|
|
|
16
48
|
### Fixed
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ The included batch benchmark records a 50% reduction in model round-trips and a
|
|
|
82
82
|
|
|
83
83
|
## Supported environment and trust boundary
|
|
84
84
|
|
|
85
|
-
The
|
|
85
|
+
The package engine floor remains Node.js `>=22.5.0`. The v0.8.0 release matrix covers Linux x64 under exact Node.js 22.5.0 and the current Node.js 24 line, including managed setup-file publication. That mutation additionally requires GNU coreutils 9.7 `mv` supporting `--update=none-fail`, `--exchange`, `--no-copy`, and `--no-target-directory`, GNU coreutils `ln -L -T`, procfs descriptor paths at `/proc/self/fd`, and `O_DIRECTORY`/`O_NOFOLLOW`. Other Linux architectures or systems without those filesystem primitives, macOS, and Windows remain unverified until equivalent exact-tree gates pass.
|
|
86
86
|
|
|
87
87
|
This is a local stdio server. It runs with the invoking user's filesystem permissions, and clients may request any `project_root` that user can access. It does not provide HTTP authentication, sandboxing, tenant isolation, or a remote-service security boundary. Remote, untrusted, and multi-tenant operation is unsupported.
|
|
88
88
|
|
|
@@ -134,24 +134,44 @@ From a source checkout, use the Yarn script; it builds first and then opens the
|
|
|
134
134
|
yarn setup
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
The wizard
|
|
137
|
+
The wizard supports exactly six CLI clients in this order: Claude Code, Hermes, OpenCode, Codex CLI, Gemini CLI, and GitHub Copilot CLI. Cursor, Windsurf, Cline, and other editor-integrated clients are intentionally excluded. Compatible detected clients start checked; unavailable or incompatible clients are disabled with a reason. Use Up/Down to move, Space to toggle, Enter to submit, or Escape/Ctrl-C to cancel.
|
|
138
138
|
|
|
139
|
-
1. preflights
|
|
140
|
-
2. installs the bundled `structural-code-editing` skill;
|
|
141
|
-
3.
|
|
142
|
-
4.
|
|
139
|
+
1. preflights every selected client's existing `ast` MCP registration, skill destination, and effective managed-guidance destination;
|
|
140
|
+
2. installs or safely upgrades the bundled `structural-code-editing` skill;
|
|
141
|
+
3. adds one marker-owned activation block to each verified global instruction surface while preserving all user-owned bytes;
|
|
142
|
+
4. registers this package's MCP server through the agent's official CLI;
|
|
143
|
+
5. reconnects and verifies the expected tools.
|
|
143
144
|
|
|
144
|
-
Existing matching registrations
|
|
145
|
+
Existing matching registrations, skill files, and managed blocks are unchanged. Conflicting MCP registrations or malformed/unknown managed guidance fail before any write; resolve them explicitly instead of letting a setup script guess. Skill upgrades are automatic only when the installed bytes match an exact SHA-256 admitted from a published npm tarball. Unknown or customized skill bytes fail closed unless `--force-skill` is explicit. That flag applies only to the skill and cannot override guidance conflicts, unsafe routes, or filesystem races.
|
|
146
|
+
|
|
147
|
+
Guidance uses each client's verified global instruction contract rather than one universal filename:
|
|
148
|
+
|
|
149
|
+
| Client | Managed guidance destination |
|
|
150
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| Claude | `$CLAUDE_CONFIG_DIR/CLAUDE.md`, or `~/.claude/CLAUDE.md` |
|
|
152
|
+
| OpenCode | Effective native `AGENTS.md`; an existing Claude fallback may be shared or preserved when OpenCode has no native file |
|
|
153
|
+
| Codex | Non-empty `$CODEX_HOME/AGENTS.override.md`, otherwise `$CODEX_HOME/AGENTS.md`; `CODEX_HOME` defaults to `~/.codex` |
|
|
154
|
+
| Gemini | The one supported safe `context.fileName` from `~/.gemini/settings.json`, otherwise `~/.gemini/GEMINI.md` |
|
|
155
|
+
| Hermes | `skill_only`; setup does not modify `SOUL.md` or invent a global instruction destination |
|
|
156
|
+
| Copilot | `skill_only`; setup does not invent a personal global instruction destination |
|
|
157
|
+
|
|
158
|
+
The managed range is delimited by `ast-tool:structural-code-editing guidance v1` begin/end markers. Setup updates only that range, preserves the file's UTF-8 BOM, newline style, mode, and all content outside the range, and rejects duplicate, partial, reordered, unknown, symlinked, or non-regular destinations. Writes pin the parent chain, preimage, held temporary inode, and destination. New files use descriptor-bound no-clobber publication; replacements use an atomic same-directory exchange, validate both exchanged identities plus the pinned preimage bytes and mode, and roll the exact pair back when an in-call substitution or same-inode edit is detected. Every completed postimage is reauthenticated before later asset or MCP mutation. Cross-client setup is convergent rather than globally transactional.
|
|
159
|
+
|
|
160
|
+
Successful setup output uses schema `version: 2`. Each agent reports `mcp`, `skill`, and `guidance`; physical writes include an `asset` of `skill`, `guidance`, or `mcp_config`. A complete replay returns every applicable item as `unchanged`/`skill_only` and an empty `physical_writes` array. A failed managed publication separates `completed_writes`, `possibly_committed`, `rolled_back`, `rollback_failed`, and `pending`; an uncertain commit or failed rollback is never reported as untouched and requires inspection plus a fresh replan.
|
|
145
161
|
|
|
146
162
|
For automation, make the target set and confirmation explicit:
|
|
147
163
|
|
|
148
164
|
```bash
|
|
149
165
|
ast-tool setup --agents all --yes
|
|
150
|
-
ast-tool setup --agents claude --yes
|
|
166
|
+
ast-tool setup --agents claude,codex --yes
|
|
151
167
|
```
|
|
152
168
|
|
|
153
169
|
From a source checkout, replace `ast-tool` with `yarn setup` in those commands.
|
|
154
170
|
|
|
171
|
+
`--agents all` is resolved only after detection and means every detected compatible client. If any detected client has unknown or incompatible output, setup fails before writes. Explicit IDs are strict and reject unavailable clients. Non-interactive setup requires both `--agents` and `--yes`.
|
|
172
|
+
|
|
173
|
+
OpenCode 1.18.18 or newer is required. Because `opencode mcp add` ignores custom config routing, setup updates only `mcp.ast` in `OPENCODE_CONFIG`, then `OPENCODE_CONFIG_DIR/opencode.json`, then `~/.config/opencode/opencode.json`. JSONC comments, unrelated keys, and file mode are preserved. OpenCode's nominally diagnostic config command normalizes both routed config files, so setup runs discovery and verification against disposable copies while retaining the selected config bytes and fails closed if the planned real destination changes. Gemini setup may require trusting the current folder before registration. Diagnostics use a correlation ID and omit command arguments, environment, credentials, and raw provider output; setup failures may include a bounded destination path so the operator can inspect an uncertain or pending write.
|
|
174
|
+
|
|
155
175
|
## Install the agent skill
|
|
156
176
|
|
|
157
177
|
The package bundles a `structural-code-editing` skill that teaches an agent when to use the AST tools, how to minimize context, and how to review mutations safely. Install it for both Claude Code and Hermes with one command:
|
|
@@ -169,10 +189,11 @@ ast-tool install-skill hermes
|
|
|
169
189
|
|
|
170
190
|
The default is user scope. It writes to Claude Code's personal skill directory and to the active `HERMES_HOME`:
|
|
171
191
|
|
|
172
|
-
| Target
|
|
173
|
-
|
|
|
174
|
-
| Claude Code
|
|
175
|
-
| Hermes
|
|
192
|
+
| Target | Destination |
|
|
193
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
194
|
+
| Claude Code | `$CLAUDE_CONFIG_DIR/skills/structural-code-editing/SKILL.md`, or `~/.claude/skills/...` by default |
|
|
195
|
+
| Hermes | `$HERMES_HOME/skills/software-development/structural-code-editing/SKILL.md`, or `~/.hermes/...` by default |
|
|
196
|
+
| OpenCode, Codex, Gemini, Copilot | `~/.agents/skills/structural-code-editing/SKILL.md` (one physical write, four logical outcomes) |
|
|
176
197
|
|
|
177
198
|
To commit the skill into one project for Claude Code, use project scope:
|
|
178
199
|
|
|
@@ -182,7 +203,7 @@ ast-tool install-skill claude --scope project --project-root /absolute/project
|
|
|
182
203
|
|
|
183
204
|
This writes `.claude/skills/structural-code-editing/SKILL.md` below that project. Project scope is intentionally rejected for Hermes because Hermes skills belong to a profile, not a source repository.
|
|
184
205
|
|
|
185
|
-
Installation is idempotent. Existing
|
|
206
|
+
Installation is idempotent. Existing current bytes are left untouched; exact predecessor bytes admitted by the bundled npm-provenance manifest are upgraded safely. Unknown or customized bytes fail closed unless `--force` is explicit. `install-skill` never writes global guidance or configures MCP. From an unlinked source checkout, replace `ast-tool` with `yarn node /absolute/path/to/ast-mcp-server/dist/cli.js`.
|
|
186
207
|
|
|
187
208
|
Claude Code detects changes in an existing skill directory live; restart it if the top-level skills directory did not exist when the session started. In Hermes, run `/reload-skills` or start a new session, then verify with `hermes skills list`.
|
|
188
209
|
|
|
@@ -421,11 +442,12 @@ yarn build
|
|
|
421
442
|
yarn test:mcp
|
|
422
443
|
yarn test:cli
|
|
423
444
|
yarn test:package
|
|
445
|
+
yarn test:installed-agents
|
|
424
446
|
yarn npm audit --all --recursive
|
|
425
447
|
yarn pack --dry-run
|
|
426
448
|
```
|
|
427
449
|
|
|
428
|
-
`test:mcp` exercises the built stdio server. `test:cli` runs a read pipeline and a prepare/apply/replay workflow across separate Node processes.
|
|
450
|
+
`test:mcp` exercises the built stdio server. `test:cli` runs a read pipeline and a prepare/apply/replay workflow across separate Node processes. `test:installed-agents` is a host-dependent manual gate: it builds first, detects locally installed supported clients, uses only disposable homes/config roots, verifies deterministic effective discovery without model calls, reports unavailable clients, and removes the disposable state. It is not a portable CI requirement because CI does not install every external client.
|
|
429
451
|
|
|
430
452
|
## Benchmarks
|
|
431
453
|
|
package/SECURITY.md
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported versions
|
|
4
4
|
|
|
5
|
-
Security fixes are provided for the
|
|
5
|
+
Security fixes are provided for the exact version currently identified by the npm registry's `latest` dist-tag and for the current `main` development line. The support window is registry-authoritative:
|
|
6
6
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| `main` /
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
7
|
+
| Release state | Supported |
|
|
8
|
+
| ------------------------------------------- | --------- |
|
|
9
|
+
| `main` / `Unreleased` | Yes |
|
|
10
|
+
| Exact npm version currently tagged `latest` | Yes |
|
|
11
|
+
| Earlier npm versions | No |
|
|
12
|
+
| Unpublished release candidates | No |
|
|
13
13
|
|
|
14
|
-
This project supports trusted single-user local stdio use
|
|
14
|
+
This project supports trusted single-user local stdio use; the supported operating-system and architecture boundary remains Linux x64 only. The package engine floor is Node.js `>=22.5.0`, and the v0.8.0 release matrix covers Linux x64 under exact Node.js 22.5.0 and the current Node.js 24 line, including managed setup-file publication. That mutation requires GNU coreutils 9.7 `mv --update=none-fail --exchange --no-copy --no-target-directory`, GNU coreutils `ln -L -T`, and procfs descriptor-relative paths at `/proc/self/fd` with `O_DIRECTORY`/`O_NOFOLLOW`. Other Linux architectures and systems without those primitives are unverified, as are macOS and Windows. Remote, untrusted, and multi-tenant use is outside the supported security boundary.
|
|
15
15
|
|
|
16
16
|
## Reporting a vulnerability
|
|
17
17
|
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { createInterface } from "node:readline/promises";
|
|
5
4
|
import { ZodError } from "zod";
|
|
6
5
|
import { BatchExecutionError, parseBatchDocument, runBatchDocument } from "./batch/runner.js";
|
|
7
6
|
import { MAX_BATCH_INPUT_BYTES, isPrepareBatchTool } from "./batch/schema.js";
|
|
8
7
|
import { CliOutputError, serializeCliSuccess } from "./cli-output.js";
|
|
9
8
|
import { applyPersistedOperation, persistOperationPlan } from "./services/operation-plan-file.js";
|
|
10
9
|
import { AgentSetupError, detectInstalledAgents, resolveServerEntryPath, runAgentSetup, } from "./services/agent-setup.js";
|
|
11
|
-
import { installBundledSkill,
|
|
12
|
-
import { parseAgentsArgument, promptForAgentSelection, } from "./services/setup-wizard.js";
|
|
10
|
+
import { installBundledSkill, resolveBundledSkillAssets, } from "./services/skill-installer.js";
|
|
11
|
+
import { parseAgentsArgument, promptForAgentSelection, resolveAgentSelection, } from "./services/setup-wizard.js";
|
|
13
12
|
class CliError extends Error {
|
|
14
13
|
code;
|
|
15
14
|
exitCode;
|
|
@@ -241,30 +240,33 @@ export async function runCli(args) {
|
|
|
241
240
|
const options = parseSetupArgs(commandArgs);
|
|
242
241
|
try {
|
|
243
242
|
const detections = await detectInstalledAgents();
|
|
244
|
-
let agents
|
|
245
|
-
if (agents === undefined) {
|
|
243
|
+
let agents;
|
|
244
|
+
if (options.agents === undefined) {
|
|
246
245
|
if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) {
|
|
247
246
|
throw new CliError("Interactive setup requires a TTY. Use --agents claude,hermes --yes for automation.", "USAGE", 2, command);
|
|
248
247
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
prompt.close();
|
|
255
|
-
}
|
|
248
|
+
agents = await promptForAgentSelection(detections, {
|
|
249
|
+
input: process.stdin,
|
|
250
|
+
output: process.stderr,
|
|
251
|
+
signals: process,
|
|
252
|
+
});
|
|
256
253
|
if (agents.length === 0) {
|
|
257
254
|
return { status: "cancelled", command: "setup", agents: [] };
|
|
258
255
|
}
|
|
259
256
|
}
|
|
257
|
+
else
|
|
258
|
+
agents = resolveAgentSelection(options.agents, detections);
|
|
260
259
|
const executablePath = process.argv[1];
|
|
261
260
|
if (!executablePath) {
|
|
262
261
|
throw new Error("Cannot resolve the ast-tool executable path.");
|
|
263
262
|
}
|
|
263
|
+
const assets = await resolveBundledSkillAssets(executablePath);
|
|
264
264
|
return await runAgentSetup({
|
|
265
265
|
agents,
|
|
266
266
|
detections,
|
|
267
|
-
sourceSkillPath:
|
|
267
|
+
sourceSkillPath: assets.skillPath,
|
|
268
|
+
sourceGuidancePath: assets.guidancePath,
|
|
269
|
+
releaseManifestPath: assets.releasesPath,
|
|
268
270
|
serverEntryPath: await resolveServerEntryPath(executablePath),
|
|
269
271
|
forceSkill: options.forceSkill,
|
|
270
272
|
});
|
|
@@ -286,9 +288,11 @@ export async function runCli(args) {
|
|
|
286
288
|
if (!executablePath) {
|
|
287
289
|
throw new Error("Cannot resolve the ast-tool executable path.");
|
|
288
290
|
}
|
|
291
|
+
const assets = await resolveBundledSkillAssets(executablePath);
|
|
289
292
|
return await installBundledSkill({
|
|
290
293
|
...options,
|
|
291
|
-
sourceSkillPath:
|
|
294
|
+
sourceSkillPath: assets.skillPath,
|
|
295
|
+
releaseManifestPath: assets.releasesPath,
|
|
292
296
|
});
|
|
293
297
|
}
|
|
294
298
|
catch (error) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import { access, realpath, stat } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import process from "node:process";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { planBundledSkillInstallation, SkillConflictError, } from "./skill-installer.js";
|
|
9
|
+
import { planManagedGuidance, } from "./managed-guidance.js";
|
|
10
|
+
import { applyManagedFilePlan, createManagedFileApplyContext, ManagedFileApplyError, ManagedFileRollbackError, revalidateManagedFilePlan, verifyManagedFilePostimage, } from "./managed-file.js";
|
|
11
|
+
import { AGENT_IDS, classifyAgentVersion, getAgentTarget, MCP_SERVER_NAME, } from "./agent-targets.js";
|
|
12
|
+
import { applyOpenCodeConfigPlan, planOpenCodeConfig, resolveOpenCodeConfigPath, withIsolatedOpenCodeConfig, } from "./opencode-config.js";
|
|
8
13
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
9
14
|
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
|
|
10
15
|
export class AgentSetupError extends Error {
|
|
@@ -17,6 +22,87 @@ export class AgentSetupError extends Error {
|
|
|
17
22
|
this.details = details;
|
|
18
23
|
}
|
|
19
24
|
}
|
|
25
|
+
export async function applyManagedAssetPlans(skillPlan, guidancePlan, context = createManagedFileApplyContext(), hooks = {}) {
|
|
26
|
+
const physicalWrites = [];
|
|
27
|
+
const possiblyCommitted = [];
|
|
28
|
+
const rolledBack = [];
|
|
29
|
+
const rollbackFailed = [];
|
|
30
|
+
const authenticated = [];
|
|
31
|
+
let activePlan;
|
|
32
|
+
try {
|
|
33
|
+
for (const file of [...skillPlan.files, ...guidancePlan.files]) {
|
|
34
|
+
await revalidateManagedFilePlan(file);
|
|
35
|
+
}
|
|
36
|
+
const plans = [
|
|
37
|
+
...skillPlan.files.map((file) => ({ file, asset: "skill" })),
|
|
38
|
+
...guidancePlan.files.map((file) => ({ file, asset: "guidance" })),
|
|
39
|
+
];
|
|
40
|
+
for (const { file, asset } of plans) {
|
|
41
|
+
activePlan = { file, asset };
|
|
42
|
+
for (const current of authenticated) {
|
|
43
|
+
await verifyManagedFilePostimage(current, context);
|
|
44
|
+
}
|
|
45
|
+
await applyManagedFilePlan(file, context, hooks);
|
|
46
|
+
authenticated.push(file);
|
|
47
|
+
if (file.status !== "unchanged") {
|
|
48
|
+
physicalWrites.push({ path: file.path, asset, status: file.status });
|
|
49
|
+
}
|
|
50
|
+
activePlan = undefined;
|
|
51
|
+
}
|
|
52
|
+
for (const current of authenticated) {
|
|
53
|
+
await verifyManagedFilePostimage(current, context);
|
|
54
|
+
}
|
|
55
|
+
return physicalWrites;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (activePlan !== undefined && activePlan.file.status !== "unchanged") {
|
|
59
|
+
const write = {
|
|
60
|
+
path: activePlan.file.path,
|
|
61
|
+
asset: activePlan.asset,
|
|
62
|
+
status: activePlan.file.status,
|
|
63
|
+
};
|
|
64
|
+
if (error instanceof ManagedFileRollbackError) {
|
|
65
|
+
rolledBack.push(write);
|
|
66
|
+
}
|
|
67
|
+
else if (error instanceof ManagedFileApplyError) {
|
|
68
|
+
if (error.commitState === "committed") {
|
|
69
|
+
physicalWrites.push(write);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
possiblyCommitted.push(write);
|
|
73
|
+
}
|
|
74
|
+
if (error.rollbackState === "failed") {
|
|
75
|
+
rollbackFailed.push(write);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new AgentSetupError(error instanceof Error ? error.message : "Managed asset apply failed.", "AGENT_ASSET_APPLY_FAILED", {
|
|
80
|
+
completed_writes: physicalWrites,
|
|
81
|
+
possibly_committed: possiblyCommitted,
|
|
82
|
+
rolled_back: rolledBack,
|
|
83
|
+
rollback_failed: rollbackFailed,
|
|
84
|
+
pending: [
|
|
85
|
+
...skillPlan.files
|
|
86
|
+
.filter((file) => file.status !== "unchanged")
|
|
87
|
+
.map((file) => ({ path: file.path, asset: "skill", status: file.status })),
|
|
88
|
+
...guidancePlan.files
|
|
89
|
+
.filter((file) => file.status !== "unchanged")
|
|
90
|
+
.map((file) => ({ path: file.path, asset: "guidance", status: file.status })),
|
|
91
|
+
].filter((pending) => !physicalWrites.some((completed) => completed.path === pending.path && completed.asset === pending.asset) &&
|
|
92
|
+
!possiblyCommitted.some((uncertain) => uncertain.path === pending.path && uncertain.asset === pending.asset)),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function verifyManagedAssetPlans(skillPlan, guidancePlan, context) {
|
|
97
|
+
try {
|
|
98
|
+
for (const file of [...skillPlan.files, ...guidancePlan.files]) {
|
|
99
|
+
await verifyManagedFilePostimage(file, context);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new AgentSetupError(error instanceof Error ? error.message : "Managed asset postimage verification failed.", "AGENT_ASSET_APPLY_FAILED", { cause: error instanceof Error ? error.message : String(error) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
20
106
|
function environmentPath(environment) {
|
|
21
107
|
return environment.PATH ?? environment.Path ?? environment.path ?? "";
|
|
22
108
|
}
|
|
@@ -76,7 +162,7 @@ async function runCommand(executable, args, options) {
|
|
|
76
162
|
const appendOutput = (target, chunk) => {
|
|
77
163
|
outputBytes += chunk.byteLength;
|
|
78
164
|
if (outputBytes > MAX_COMMAND_OUTPUT_BYTES) {
|
|
79
|
-
finishWithError(new AgentSetupError(`Agent command exceeded ${MAX_COMMAND_OUTPUT_BYTES} output bytes.`, "AGENT_COMMAND_OUTPUT_LIMIT"
|
|
165
|
+
finishWithError(new AgentSetupError(`Agent command exceeded ${MAX_COMMAND_OUTPUT_BYTES} output bytes.`, "AGENT_COMMAND_OUTPUT_LIMIT"));
|
|
80
166
|
return;
|
|
81
167
|
}
|
|
82
168
|
if (target === "stdout") {
|
|
@@ -87,15 +173,12 @@ async function runCommand(executable, args, options) {
|
|
|
87
173
|
}
|
|
88
174
|
};
|
|
89
175
|
const timeout = setTimeout(() => {
|
|
90
|
-
finishWithError(new AgentSetupError(`Agent command timed out after ${options.timeoutMs}ms.`, "AGENT_COMMAND_TIMEOUT"
|
|
176
|
+
finishWithError(new AgentSetupError(`Agent command timed out after ${options.timeoutMs}ms.`, "AGENT_COMMAND_TIMEOUT"));
|
|
91
177
|
}, options.timeoutMs);
|
|
92
178
|
child.stdout.on("data", (chunk) => appendOutput("stdout", chunk));
|
|
93
179
|
child.stderr.on("data", (chunk) => appendOutput("stderr", chunk));
|
|
94
|
-
child.on("error", (
|
|
95
|
-
finishWithError(new AgentSetupError(
|
|
96
|
-
executable,
|
|
97
|
-
args,
|
|
98
|
-
}));
|
|
180
|
+
child.on("error", () => {
|
|
181
|
+
finishWithError(new AgentSetupError("Failed to start the agent command.", "AGENT_COMMAND_START"));
|
|
99
182
|
});
|
|
100
183
|
child.on("close", (exitCode) => {
|
|
101
184
|
if (settled) {
|
|
@@ -109,8 +192,7 @@ async function runCommand(executable, args, options) {
|
|
|
109
192
|
});
|
|
110
193
|
}
|
|
111
194
|
function commandFailure(agent, operation, result) {
|
|
112
|
-
|
|
113
|
-
return new AgentSetupError(`${getAgentTarget(agent).label} ${operation} failed with exit code ${result.exitCode}.`, "AGENT_COMMAND_FAILED", { agent, operation, output });
|
|
195
|
+
return new AgentSetupError(`${getAgentTarget(agent).label} ${operation} failed with exit code ${result.exitCode}.`, "AGENT_COMMAND_FAILED", { agent, operation, reason: `Command exited with code ${result.exitCode}.` });
|
|
114
196
|
}
|
|
115
197
|
function targetRuntime(detection, environment, timeoutMs) {
|
|
116
198
|
return {
|
|
@@ -136,6 +218,33 @@ async function inspectMcp(agent, detection, serverEntryPath, nodeExecutable, env
|
|
|
136
218
|
...(inspection.detail ? { detail: inspection.detail } : {}),
|
|
137
219
|
};
|
|
138
220
|
}
|
|
221
|
+
async function verifyOpenCode(detection, environment, timeoutMs, context) {
|
|
222
|
+
await withIsolatedOpenCodeConfig(environment, environment.HOME ?? os.homedir(), async (isolatedEnvironment) => {
|
|
223
|
+
const runtime = targetRuntime(detection, isolatedEnvironment, timeoutMs);
|
|
224
|
+
const resolved = await runtime.run(["debug", "config", "--pure"]);
|
|
225
|
+
if (resolved.exitCode !== 0)
|
|
226
|
+
throw commandFailure("opencode", "resolved configuration verification", resolved);
|
|
227
|
+
let config;
|
|
228
|
+
try {
|
|
229
|
+
config = JSON.parse(resolved.stdout);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
throw new AgentSetupError("OpenCode resolved configuration output is unknown.", "AGENT_VERIFICATION_FAILED", { agent: "opencode" });
|
|
233
|
+
}
|
|
234
|
+
const command = config?.mcp?.ast?.command;
|
|
235
|
+
if (!Array.isArray(command) ||
|
|
236
|
+
command.length !== 2 ||
|
|
237
|
+
command[0] !== context.nodeExecutable ||
|
|
238
|
+
command[1] !== context.serverEntryPath) {
|
|
239
|
+
throw new AgentSetupError("OpenCode resolved configuration does not contain the expected ast server.", "AGENT_VERIFICATION_FAILED", { agent: "opencode" });
|
|
240
|
+
}
|
|
241
|
+
const listed = await runtime.run(["mcp", "list", "--pure"]);
|
|
242
|
+
const plainList = listed.stdout.replaceAll("\u001b", "");
|
|
243
|
+
if (listed.exitCode !== 0 || !/ast[\s\S]*connected/i.test(plainList)) {
|
|
244
|
+
throw new AgentSetupError("OpenCode did not report the ast server as connected.", "AGENT_VERIFICATION_FAILED", { agent: "opencode" });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
139
248
|
async function configureMcp(agent, detection, serverEntryPath, nodeExecutable, environment, timeoutMs) {
|
|
140
249
|
const target = getAgentTarget(agent);
|
|
141
250
|
const added = await target.mcp.register(targetRuntime(detection, environment, timeoutMs), {
|
|
@@ -175,7 +284,12 @@ export async function detectInstalledAgents(options = {}) {
|
|
|
175
284
|
const definition = getAgentTarget(id);
|
|
176
285
|
const executable = await findExecutable(definition.command, environment);
|
|
177
286
|
if (executable === undefined) {
|
|
178
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
id,
|
|
289
|
+
label: definition.label,
|
|
290
|
+
installed: false,
|
|
291
|
+
compatibility: { status: "unavailable", reason: "Not found in PATH." },
|
|
292
|
+
};
|
|
179
293
|
}
|
|
180
294
|
let version;
|
|
181
295
|
try {
|
|
@@ -191,10 +305,17 @@ export async function detectInstalledAgents(options = {}) {
|
|
|
191
305
|
catch {
|
|
192
306
|
version = undefined;
|
|
193
307
|
}
|
|
194
|
-
|
|
308
|
+
const compatibility = version === undefined
|
|
309
|
+
? {
|
|
310
|
+
status: "incompatible",
|
|
311
|
+
reason: "Version command did not produce admitted evidence.",
|
|
312
|
+
}
|
|
313
|
+
: classifyAgentVersion(id, version);
|
|
314
|
+
return { id, label: definition.label, installed: true, executable, version, compatibility };
|
|
195
315
|
}));
|
|
196
316
|
}
|
|
197
317
|
export async function runAgentSetup(options) {
|
|
318
|
+
const correlationId = randomUUID();
|
|
198
319
|
const environment = options.environment ?? process.env;
|
|
199
320
|
const nodeExecutable = options.nodeExecutable ?? process.execPath;
|
|
200
321
|
const timeoutMs = options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
@@ -208,42 +329,137 @@ export async function runAgentSetup(options) {
|
|
|
208
329
|
if (!detection?.installed || detection.executable === undefined) {
|
|
209
330
|
throw new AgentSetupError(`${getAgentTarget(agent).label} is not installed or is not available in PATH.`, "AGENT_NOT_INSTALLED", { agent });
|
|
210
331
|
}
|
|
332
|
+
if (detection.compatibility && detection.compatibility.status !== "compatible") {
|
|
333
|
+
throw new AgentSetupError(`${getAgentTarget(agent).label} is incompatible: ${detection.compatibility.reason}`, detection.compatibility.status === "blocked_untrusted_folder"
|
|
334
|
+
? "AGENT_TRUST_REQUIRED"
|
|
335
|
+
: "AGENT_INCOMPATIBLE", { agent, correlation_id: correlationId });
|
|
336
|
+
}
|
|
211
337
|
}
|
|
212
338
|
await assertRegularFile(options.serverEntryPath, "MCP server entrypoint");
|
|
213
339
|
await assertRegularFile(options.sourceSkillPath, "Bundled skill");
|
|
214
|
-
|
|
340
|
+
let openCodePlan;
|
|
341
|
+
if (requested.includes("opencode")) {
|
|
342
|
+
openCodePlan = await planOpenCodeConfig({
|
|
343
|
+
filePath: resolveOpenCodeConfigPath(environment, environment.HOME ?? os.homedir()),
|
|
344
|
+
nodeExecutable,
|
|
345
|
+
serverEntryPath: options.serverEntryPath,
|
|
346
|
+
}).catch((error) => {
|
|
347
|
+
throw new AgentSetupError(error instanceof Error ? error.message : "OpenCode configuration preflight failed.", "AGENT_MCP_CONFLICT", { agent: "opencode", correlation_id: correlationId });
|
|
348
|
+
});
|
|
349
|
+
const effective = await withIsolatedOpenCodeConfig(environment, environment.HOME ?? os.homedir(), (isolatedEnvironment) => targetRuntime(detectionMap.get("opencode"), isolatedEnvironment, timeoutMs).run([
|
|
350
|
+
"debug",
|
|
351
|
+
"config",
|
|
352
|
+
"--pure",
|
|
353
|
+
]));
|
|
354
|
+
if (effective.exitCode !== 0) {
|
|
355
|
+
throw new AgentSetupError(`OpenCode effective configuration preflight failed with exit code ${effective.exitCode}.`, "AGENT_MCP_CONFLICT", { agent: "opencode", correlation_id: correlationId });
|
|
356
|
+
}
|
|
357
|
+
let effectiveConfig;
|
|
358
|
+
try {
|
|
359
|
+
effectiveConfig = JSON.parse(effective.stdout);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
throw new AgentSetupError("OpenCode effective configuration output is unknown.", "AGENT_MCP_CONFLICT", { agent: "opencode", correlation_id: correlationId });
|
|
363
|
+
}
|
|
364
|
+
const effectiveAst = effectiveConfig?.mcp?.ast;
|
|
365
|
+
const desiredAst = {
|
|
366
|
+
type: "local",
|
|
367
|
+
command: [nodeExecutable, options.serverEntryPath],
|
|
368
|
+
enabled: true,
|
|
369
|
+
};
|
|
370
|
+
if (effectiveAst !== undefined && JSON.stringify(effectiveAst) !== JSON.stringify(desiredAst)) {
|
|
371
|
+
throw new AgentSetupError("OpenCode effective configuration conflict at mcp.ast.", "AGENT_MCP_CONFLICT", { agent: "opencode", correlation_id: correlationId });
|
|
372
|
+
}
|
|
373
|
+
if (effectiveAst !== undefined && openCodePlan.status === "installed") {
|
|
374
|
+
openCodePlan = { ...openCodePlan, status: "unchanged" };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const inspections = await Promise.all(requested.map((agent) => agent === "opencode"
|
|
378
|
+
? Promise.resolve({
|
|
379
|
+
agent,
|
|
380
|
+
status: openCodePlan?.status === "unchanged" ? "current" : "missing",
|
|
381
|
+
})
|
|
382
|
+
: inspectMcp(agent, detectionMap.get(agent), options.serverEntryPath, nodeExecutable, environment, timeoutMs)));
|
|
215
383
|
const conflict = inspections.find((item) => item.status === "conflict");
|
|
216
384
|
if (conflict !== undefined) {
|
|
217
385
|
throw new AgentSetupError(`Conflicting ${getAgentTarget(conflict.agent).label} MCP registration: ${conflict.detail}`, "AGENT_MCP_CONFLICT", { agent: conflict.agent });
|
|
218
386
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
387
|
+
const blocked = inspections.find((item) => item.status === "blocked_untrusted_folder");
|
|
388
|
+
if (blocked !== undefined) {
|
|
389
|
+
throw new AgentSetupError(`${getAgentTarget(blocked.agent).label} requires the working folder to be trusted before setup.`, "AGENT_TRUST_REQUIRED", { agent: blocked.agent, correlation_id: correlationId });
|
|
390
|
+
}
|
|
391
|
+
let skillPlan;
|
|
392
|
+
try {
|
|
393
|
+
skillPlan = await planBundledSkillInstallation({
|
|
394
|
+
target: requested,
|
|
395
|
+
scope: "user",
|
|
396
|
+
force: options.forceSkill ?? false,
|
|
397
|
+
sourceSkillPath: options.sourceSkillPath,
|
|
398
|
+
releaseManifestPath: options.releaseManifestPath,
|
|
399
|
+
environment,
|
|
400
|
+
homeDirectory: environment.HOME ?? os.homedir(),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
227
404
|
if (error instanceof SkillConflictError) {
|
|
228
405
|
throw new AgentSetupError(`Skill already exists with different content at ${error.destination}; pass --force-skill to replace it.`, "AGENT_SKILL_CONFLICT", { path: error.destination });
|
|
229
406
|
}
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
|
-
|
|
407
|
+
throw new AgentSetupError(error instanceof Error ? error.message : "Skill preflight failed.", "SETUP_ASSET_INVALID", { asset: "skill", correlation_id: correlationId });
|
|
408
|
+
}
|
|
409
|
+
let guidancePlan;
|
|
410
|
+
try {
|
|
411
|
+
guidancePlan = await planManagedGuidance({
|
|
412
|
+
agents: requested,
|
|
413
|
+
sourceGuidancePath: options.sourceGuidancePath,
|
|
414
|
+
environment,
|
|
415
|
+
homeDirectory: environment.HOME ?? os.homedir(),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
throw new AgentSetupError(error instanceof Error ? error.message : "Managed guidance preflight failed.", "AGENT_GUIDANCE_CONFLICT", { asset: "guidance", correlation_id: correlationId });
|
|
420
|
+
}
|
|
421
|
+
const skillStatuses = new Map(skillPlan.installations.map((installation) => [installation.target, installation.status]));
|
|
422
|
+
const guidanceStatuses = new Map(guidancePlan.installations.map((installation) => [installation.agent, installation.status]));
|
|
423
|
+
const applyContext = createManagedFileApplyContext();
|
|
424
|
+
const physicalWrites = await applyManagedAssetPlans(skillPlan, guidancePlan, applyContext);
|
|
233
425
|
const completed = [];
|
|
234
426
|
for (const agent of requested) {
|
|
235
427
|
const detection = detectionMap.get(agent);
|
|
236
428
|
const inspection = inspections.find((item) => item.agent === agent);
|
|
237
429
|
try {
|
|
430
|
+
await verifyManagedAssetPlans(skillPlan, guidancePlan, applyContext);
|
|
238
431
|
if (inspection.status === "missing") {
|
|
239
|
-
|
|
432
|
+
if (agent === "opencode") {
|
|
433
|
+
await applyOpenCodeConfigPlan(openCodePlan);
|
|
434
|
+
if (openCodePlan.status === "installed") {
|
|
435
|
+
physicalWrites.push({
|
|
436
|
+
path: openCodePlan.filePath,
|
|
437
|
+
asset: "mcp_config",
|
|
438
|
+
status: "installed",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
await verifyOpenCode(detection, environment, timeoutMs, {
|
|
442
|
+
nodeExecutable,
|
|
443
|
+
serverEntryPath: options.serverEntryPath,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else
|
|
447
|
+
await configureMcp(agent, detection, options.serverEntryPath, nodeExecutable, environment, timeoutMs);
|
|
448
|
+
}
|
|
449
|
+
else if (agent === "opencode") {
|
|
450
|
+
await verifyOpenCode(detection, environment, timeoutMs, {
|
|
451
|
+
nodeExecutable,
|
|
452
|
+
serverEntryPath: options.serverEntryPath,
|
|
453
|
+
});
|
|
240
454
|
}
|
|
455
|
+
await verifyManagedAssetPlans(skillPlan, guidancePlan, applyContext);
|
|
241
456
|
completed.push({
|
|
242
457
|
agent,
|
|
243
458
|
executable: detection.executable,
|
|
244
459
|
version: detection.version,
|
|
245
460
|
mcp: inspection.status === "current" ? "unchanged" : "configured",
|
|
246
461
|
skill: skillStatuses.get(agent),
|
|
462
|
+
guidance: guidanceStatuses.get(agent),
|
|
247
463
|
});
|
|
248
464
|
}
|
|
249
465
|
catch (error) {
|
|
@@ -251,16 +467,21 @@ export async function runAgentSetup(options) {
|
|
|
251
467
|
throw new AgentSetupError(error.message, error.code, {
|
|
252
468
|
cause: error.details,
|
|
253
469
|
completed,
|
|
254
|
-
skills:
|
|
470
|
+
skills: skillPlan.installations,
|
|
471
|
+
guidance: guidancePlan.installations,
|
|
472
|
+
completed_writes: physicalWrites,
|
|
255
473
|
});
|
|
256
474
|
}
|
|
257
475
|
throw error;
|
|
258
476
|
}
|
|
259
477
|
}
|
|
260
478
|
return {
|
|
479
|
+
version: 2,
|
|
261
480
|
status: "ok",
|
|
262
481
|
command: "setup",
|
|
263
482
|
server: { name: MCP_SERVER_NAME, command: nodeExecutable, args: [options.serverEntryPath] },
|
|
264
483
|
agents: completed,
|
|
484
|
+
correlation_id: correlationId,
|
|
485
|
+
physical_writes: physicalWrites,
|
|
265
486
|
};
|
|
266
487
|
}
|