@yitom/agy-acp-map 0.1.14 → 0.1.17
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/README.md +6 -6
- package/dist/agy-headless.exe +0 -0
- package/dist/bin.js +246 -21
- package/dist/core/session-core.d.ts +28 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/index.js +248 -22
- package/dist/lib/agy-args.d.ts +7 -4
- package/dist/lib/mcp-servers.d.ts +91 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ Configure on **`session/new`** (preferred) and/or env fallbacks. Stored on the S
|
|
|
95
95
|
| `--sandbox` | `sandbox: true` | `AGY_ACP_SANDBOX=1` |
|
|
96
96
|
| `--json-schema` | `jsonSchema` (string or object→stringify) | `AGY_ACP_JSON_SCHEMA` (string or path) |
|
|
97
97
|
| `--conversation` | `conversationId` (resume / switch-model flow) | _(from prior turn)_ |
|
|
98
|
-
| `--dangerously-skip-permissions` | `safety: 'autonomous' \| 'autonomous-unsandboxed'` or `skipPermissions: true` | `AGY_ACP_SAFETY=…` / `AGY_ACP_SKIP_PERMISSIONS=1` if safety unset (**default
|
|
98
|
+
| `--dangerously-skip-permissions` | `safety: 'autonomous' \| 'autonomous-unsandboxed'` or `skipPermissions: true` | `AGY_ACP_SAFETY=…` / `AGY_ACP_SKIP_PERMISSIONS=1` if safety unset (**default: skip on, unsandboxed**) |
|
|
99
99
|
| `--disable-slash-commands` | `disableSlashCommands` (default **true**) | `AGY_ACP_DISABLE_SLASH_COMMANDS=0` to omit |
|
|
100
100
|
| `--print-timeout` | `printTimeout` (e.g. `30m`, `120s`, `0`) | `AGY_ACP_PRINT_TIMEOUT` (default `0`) |
|
|
101
101
|
|
|
@@ -159,7 +159,7 @@ Display-history path: `~/.agy-acp-map/history/`. Override with `AGY_ACP_HISTORY_
|
|
|
159
159
|
| Env | Default | Meaning |
|
|
160
160
|
|-----|---------|---------|
|
|
161
161
|
| `AGY_ACP_SAFETY` | `safe` | `safe` \| `autonomous` \| `autonomous-unsandboxed` (aliases: `unsandboxed`, `autonomous_unsandboxed`). See Safety modes table. |
|
|
162
|
-
| `AGY_ACP_SKIP_PERMISSIONS` | `0` | If **safety unset**: `1` ≈ treat as `autonomous`; `0` ≈ `safe`. Explicit `AGY_ACP_SAFETY` / `session.safety` wins. |
|
|
162
|
+
| `AGY_ACP_SKIP_PERMISSIONS` | `0` | If **safety unset**: `1` ≈ treat as `autonomous-unsandboxed`; `0` ≈ `safe`. Explicit `AGY_ACP_SAFETY` / `session.safety` wins. |
|
|
163
163
|
| `AGY_ACP_DISABLE_SLASH_COMMANDS` | `1` | When `1` (default), pass `--disable-slash-commands`. Set `0` to omit. |
|
|
164
164
|
| `AGY_ACP_PRINT_TIMEOUT` | `0` | Passed as `--print-timeout` (e.g. `30m`, `120s`, `0` = wait until turn completes). |
|
|
165
165
|
| `AGY_BIN` | `agy` | Override binary |
|
|
@@ -167,7 +167,7 @@ Display-history path: `~/.agy-acp-map/history/`. Override with `AGY_ACP_HISTORY_
|
|
|
167
167
|
| `AGY_ACP_EFFORT` | — | Default `--effort` |
|
|
168
168
|
| `AGY_ACP_MODE` | — | Default `--mode` |
|
|
169
169
|
| `AGY_ACP_AGENT` | — | Default `--agent` |
|
|
170
|
-
| `AGY_ACP_SANDBOX` | — | `1`/`0` → force sandbox on/off for `safe`/`autonomous
|
|
170
|
+
| `AGY_ACP_SANDBOX` | — | `1`/`0` → force sandbox on/off for `safe`/`autonomous`/defaulted tier. Ignored only when `autonomous-unsandboxed` was explicitly selected. |
|
|
171
171
|
| `AGY_ACP_JSON_SCHEMA` | — | Schema string or file path for `--json-schema` |
|
|
172
172
|
| `AGY_ACP_KEEP_STAGING` | — | `1` → keep `.agy-acp-staging` files after turn/close (debug) |
|
|
173
173
|
| `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` | `~/.agy-acp-map/sessions.json` | Disk session index path (`SESSION_STORE` wins if both set) |
|
|
@@ -209,15 +209,15 @@ No interactive ACP permission UI — **launch strategies only** (`resolveSafety`
|
|
|
209
209
|
|
|
210
210
|
| Mode / 模式 | Skip permissions / 跳过权限 | Sandbox / 沙箱 |
|
|
211
211
|
|-------------|------------------------------|----------------|
|
|
212
|
-
| **safe**
|
|
212
|
+
| **safe** | no — rely on agy `settings.json` allow/deny + soft-deny messages | only if user sets `sandbox: true` / `AGY_ACP_SANDBOX=1` |
|
|
213
213
|
| **autonomous** | yes (`--dangerously-skip-permissions`) | **default `--sandbox`** unless user sets `sandbox: false` / `AGY_ACP_SANDBOX=0` |
|
|
214
|
-
| **autonomous-unsandboxed** (aliases: `autonomous_unsandboxed`, `unsandboxed`) | yes (`--dangerously-skip-permissions`) | **never**
|
|
214
|
+
| **autonomous-unsandboxed** (default; aliases: `autonomous_unsandboxed`, `unsandboxed`) | yes (`--dangerously-skip-permissions`) | **never** when explicitly selected (ignores sandbox overrides); the defaulted tier honors explicit `sandbox: true` / `AGY_ACP_SANDBOX=1` |
|
|
215
215
|
|
|
216
216
|
Accept via:
|
|
217
217
|
- `session/new` field `safety`: `'safe' | 'autonomous' | 'autonomous-unsandboxed'`
|
|
218
218
|
- env `AGY_ACP_SAFETY` (same values)
|
|
219
219
|
- idle `session/set_config_option` with `configId: "safety"`
|
|
220
|
-
- Backward compat: `AGY_ACP_SKIP_PERMISSIONS=1` ≈ `autonomous` **if safety unset**; `=0` ≈ `safe`
|
|
220
|
+
- Backward compat: `AGY_ACP_SKIP_PERMISSIONS=1` ≈ `autonomous-unsandboxed` **if safety unset**; `=0` ≈ `safe`
|
|
221
221
|
|
|
222
222
|
输入:`session/new.safety` / `AGY_ACP_SAFETY` / `set_config_option`;兼容 `AGY_ACP_SKIP_PERMISSIONS`(仅在未设 safety 时生效)。
|
|
223
223
|
|
package/dist/agy-headless.exe
CHANGED
|
Binary file
|
package/dist/bin.js
CHANGED
|
@@ -10750,14 +10750,15 @@ function normalizePrintTimeout(v) {
|
|
|
10750
10750
|
return s === "" ? undefined : s;
|
|
10751
10751
|
}
|
|
10752
10752
|
function resolveSafety(session = {}, env = process.env) {
|
|
10753
|
-
|
|
10753
|
+
const explicitSafety = normalizeSafety(session.safety) ?? normalizeSafety(env.AGY_ACP_SAFETY) ?? undefined;
|
|
10754
|
+
let safety = explicitSafety;
|
|
10754
10755
|
if (!safety) {
|
|
10755
10756
|
const envSkip = env.AGY_ACP_SKIP_PERMISSIONS;
|
|
10756
10757
|
if (envSkip !== undefined && envSkip !== "") {
|
|
10757
10758
|
const skip = !(envSkip === "0" || envSkip === "false" || envSkip === "no");
|
|
10758
|
-
safety = skip ? "autonomous" : "safe";
|
|
10759
|
+
safety = skip ? "autonomous-unsandboxed" : "safe";
|
|
10759
10760
|
} else {
|
|
10760
|
-
safety = "autonomous";
|
|
10761
|
+
safety = "autonomous-unsandboxed";
|
|
10761
10762
|
}
|
|
10762
10763
|
}
|
|
10763
10764
|
let skipPermissions = safety === "autonomous" || safety === "autonomous-unsandboxed";
|
|
@@ -10765,7 +10766,7 @@ function resolveSafety(session = {}, env = process.env) {
|
|
|
10765
10766
|
skipPermissions = Boolean(session.skipPermissions);
|
|
10766
10767
|
}
|
|
10767
10768
|
let sandbox;
|
|
10768
|
-
if (safety === "autonomous-unsandboxed") {
|
|
10769
|
+
if (safety === "autonomous-unsandboxed" && explicitSafety === "autonomous-unsandboxed") {
|
|
10769
10770
|
sandbox = false;
|
|
10770
10771
|
} else if (session.sandbox !== undefined) {
|
|
10771
10772
|
sandbox = Boolean(session.sandbox);
|
|
@@ -11692,7 +11693,7 @@ function formatSoftDenyMessage(denies) {
|
|
|
11692
11693
|
bits.push(`(via ${d.source})`);
|
|
11693
11694
|
lines.push(bits.join(" "));
|
|
11694
11695
|
}
|
|
11695
|
-
lines.push("Or set AGY_ACP_SAFETY=safe / AGY_ACP_SKIP_PERMISSIONS=0 to re-enable permission checks (autonomous
|
|
11696
|
+
lines.push("Or set AGY_ACP_SAFETY=safe / AGY_ACP_SKIP_PERMISSIONS=0 to re-enable permission checks (autonomous-unsandboxed — full permissions, no sandbox — is the bridge default; AGY_ACP_SAFETY=autonomous re-enables sandbox containment).");
|
|
11696
11697
|
if (denies.some((d) => d.source?.includes("sandbox"))) {
|
|
11697
11698
|
lines.push("Sandbox note: --sandbox = terminal restrictions, not verified offline. Built-in search may still work; if `curl`/`Invoke-WebRequest` failed, retry once with safety=autonomous-unsandboxed (or sandbox:false) to confirm it is a sandbox network block.");
|
|
11698
11699
|
}
|
|
@@ -12540,11 +12541,183 @@ class SessionHistoryStore {
|
|
|
12540
12541
|
}
|
|
12541
12542
|
}
|
|
12542
12543
|
|
|
12544
|
+
// src/lib/mcp-servers.ts
|
|
12545
|
+
import { execFile } from "node:child_process";
|
|
12546
|
+
function fail(msg) {
|
|
12547
|
+
throw new RequestError(-32602, msg);
|
|
12548
|
+
}
|
|
12549
|
+
function nonEmptyString(v) {
|
|
12550
|
+
return typeof v === "string" && v.length > 0;
|
|
12551
|
+
}
|
|
12552
|
+
function strArray(v, what) {
|
|
12553
|
+
if (v === undefined)
|
|
12554
|
+
return [];
|
|
12555
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
12556
|
+
fail(`mcpServers[].${what} must be an array of strings`);
|
|
12557
|
+
}
|
|
12558
|
+
return v;
|
|
12559
|
+
}
|
|
12560
|
+
function nameValueList(v, what) {
|
|
12561
|
+
if (v === undefined)
|
|
12562
|
+
return [];
|
|
12563
|
+
if (!Array.isArray(v))
|
|
12564
|
+
fail(`mcpServers[].${what} must be an array`);
|
|
12565
|
+
return v.map((e, i) => {
|
|
12566
|
+
if (!e || typeof e !== "object")
|
|
12567
|
+
fail(`mcpServers[].${what}[${i}] must be {name, value}`);
|
|
12568
|
+
const rec = e;
|
|
12569
|
+
if (!nonEmptyString(rec.name) || typeof rec.value !== "string") {
|
|
12570
|
+
fail(`mcpServers[].${what}[${i}] must be {name: string, value: string}`);
|
|
12571
|
+
}
|
|
12572
|
+
return { name: rec.name, value: rec.value };
|
|
12573
|
+
});
|
|
12574
|
+
}
|
|
12575
|
+
function normalizeMcpServer(input) {
|
|
12576
|
+
if (!input || typeof input !== "object")
|
|
12577
|
+
fail("mcpServers[] must be an object");
|
|
12578
|
+
const s = input;
|
|
12579
|
+
if (!nonEmptyString(s.name))
|
|
12580
|
+
fail("mcpServers[].name must be a non-empty string");
|
|
12581
|
+
const name = s.name.trim();
|
|
12582
|
+
if (name.length > 64 || /[\s\x00-\x1f]/.test(name)) {
|
|
12583
|
+
fail(`mcpServers[].name must be ≤64 chars with no whitespace: ${JSON.stringify(name)}`);
|
|
12584
|
+
}
|
|
12585
|
+
const t = typeof s.type === "string" ? s.type.trim().toLowerCase() : "";
|
|
12586
|
+
if (t === "sse" || t === "acp") {
|
|
12587
|
+
fail(`mcpServers[] '${name}': type '${t}' has no 'agy mcp add' equivalent ` + `(agy supports stdio|http only). Resend as stdio or http.`);
|
|
12588
|
+
}
|
|
12589
|
+
if (t !== "" && t !== "stdio" && t !== "http") {
|
|
12590
|
+
fail(`mcpServers[] '${name}': unknown type ${JSON.stringify(s.type)} (want stdio|http)`);
|
|
12591
|
+
}
|
|
12592
|
+
const hasCommand = nonEmptyString(s.command);
|
|
12593
|
+
const hasUrl = nonEmptyString(s.url);
|
|
12594
|
+
if (hasCommand && hasUrl) {
|
|
12595
|
+
fail(`mcpServers[] '${name}': ambiguous (both command and url set); send one`);
|
|
12596
|
+
}
|
|
12597
|
+
if (t === "http" || !hasCommand && hasUrl) {
|
|
12598
|
+
if (!hasUrl)
|
|
12599
|
+
fail(`mcpServers[] '${name}': http server needs a url`);
|
|
12600
|
+
const url = s.url;
|
|
12601
|
+
if (!/^https?:\/\//i.test(url))
|
|
12602
|
+
fail(`mcpServers[] '${name}': url must start with http(s)://`);
|
|
12603
|
+
return { name, kind: "http", args: [], env: [], url, headers: nameValueList(s.headers, "headers") };
|
|
12604
|
+
}
|
|
12605
|
+
if (!hasCommand) {
|
|
12606
|
+
fail(`mcpServers[] '${name}': stdio server needs a command (or send type:"http" with a url)`);
|
|
12607
|
+
}
|
|
12608
|
+
return {
|
|
12609
|
+
name,
|
|
12610
|
+
kind: "stdio",
|
|
12611
|
+
command: s.command,
|
|
12612
|
+
args: strArray(s.args, "args"),
|
|
12613
|
+
env: nameValueList(s.env, "env"),
|
|
12614
|
+
headers: []
|
|
12615
|
+
};
|
|
12616
|
+
}
|
|
12617
|
+
function validateMcpServers(input) {
|
|
12618
|
+
if (input === undefined)
|
|
12619
|
+
return [];
|
|
12620
|
+
if (!Array.isArray(input))
|
|
12621
|
+
fail("mcpServers must be an array");
|
|
12622
|
+
const seen = new Set;
|
|
12623
|
+
return input.map((e) => {
|
|
12624
|
+
const n = normalizeMcpServer(e);
|
|
12625
|
+
if (seen.has(n.name))
|
|
12626
|
+
fail(`mcpServers[] duplicate name: '${n.name}'`);
|
|
12627
|
+
seen.add(n.name);
|
|
12628
|
+
return n;
|
|
12629
|
+
});
|
|
12630
|
+
}
|
|
12631
|
+
function mcpServerToAgyAddArgs(s) {
|
|
12632
|
+
const argv = ["mcp", "add"];
|
|
12633
|
+
if (s.kind === "stdio") {
|
|
12634
|
+
for (const e of s.env)
|
|
12635
|
+
argv.push("--env", `${e.name}=${e.value}`);
|
|
12636
|
+
argv.push(s.name, "--", s.command, ...s.args);
|
|
12637
|
+
return argv;
|
|
12638
|
+
}
|
|
12639
|
+
for (const h of s.headers)
|
|
12640
|
+
argv.push("--header", `${h.name}: ${h.value}`);
|
|
12641
|
+
argv.push("--type", "http", s.name, s.url);
|
|
12642
|
+
return argv;
|
|
12643
|
+
}
|
|
12644
|
+
function execFileAsync(bin, args, timeoutMs) {
|
|
12645
|
+
return new Promise((resolve) => {
|
|
12646
|
+
execFile(bin, args, { timeout: timeoutMs, maxBuffer: 512 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
12647
|
+
const e = err;
|
|
12648
|
+
resolve({
|
|
12649
|
+
status: typeof e?.code === "number" ? e.code : e ? 1 : 0,
|
|
12650
|
+
stdout: String(stdout ?? ""),
|
|
12651
|
+
stderr: e?.message ? `${e.message}
|
|
12652
|
+
${String(stderr ?? "")}` : String(stderr ?? "")
|
|
12653
|
+
});
|
|
12654
|
+
});
|
|
12655
|
+
});
|
|
12656
|
+
}
|
|
12657
|
+
var defaultMcpRunFn = (bin, args, opts) => execFileAsync(bin, args, opts.timeoutMs);
|
|
12658
|
+
function tail(text, n = 600) {
|
|
12659
|
+
const t = String(text || "").trim();
|
|
12660
|
+
return t.length > n ? "…" + t.slice(-n) : t;
|
|
12661
|
+
}
|
|
12662
|
+
async function syncMcpServers(bin, servers, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12663
|
+
const added = [];
|
|
12664
|
+
for (const s of servers) {
|
|
12665
|
+
let r;
|
|
12666
|
+
try {
|
|
12667
|
+
r = await runFn(bin, mcpServerToAgyAddArgs(s), { timeoutMs });
|
|
12668
|
+
} catch (err) {
|
|
12669
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}': ${err?.message || err}`);
|
|
12670
|
+
}
|
|
12671
|
+
if (r.status !== 0) {
|
|
12672
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}' (exit ${r.status}): ${tail(`${r.stdout}
|
|
12673
|
+
${r.stderr}`)}`);
|
|
12674
|
+
}
|
|
12675
|
+
added.push(s.name);
|
|
12676
|
+
}
|
|
12677
|
+
if (added.length) {
|
|
12678
|
+
let listed = "";
|
|
12679
|
+
try {
|
|
12680
|
+
const lr = await runFn(bin, ["mcp", "list"], { timeoutMs });
|
|
12681
|
+
listed = String(lr.stdout ?? "");
|
|
12682
|
+
} catch (err) {
|
|
12683
|
+
throw new RequestError(-32603, `registered MCP server(s) [${added.join(", ")}] but 'mcp list' verification failed: ${err?.message || err}`);
|
|
12684
|
+
}
|
|
12685
|
+
const lines = listed.split(`
|
|
12686
|
+
`);
|
|
12687
|
+
const missing = added.filter((n) => !lines.some((ln) => ln.includes(n)));
|
|
12688
|
+
if (missing.length) {
|
|
12689
|
+
throw new RequestError(-32603, `MCP server(s) [${missing.join(", ")}] exited 0 on 'mcp add' but are absent from 'mcp list' ` + `(agy swallows its global --version/--help flags even after '--'; check server args). ` + `List tail: ${tail(listed, 300)}`);
|
|
12690
|
+
}
|
|
12691
|
+
}
|
|
12692
|
+
return { added };
|
|
12693
|
+
}
|
|
12694
|
+
async function removeMcpServers(bin, names, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12695
|
+
const removed = [];
|
|
12696
|
+
const warnings = [];
|
|
12697
|
+
for (const name of names) {
|
|
12698
|
+
try {
|
|
12699
|
+
const r = await runFn(bin, ["mcp", "remove", name], { timeoutMs });
|
|
12700
|
+
if (r.status !== 0) {
|
|
12701
|
+
warnings.push(`mcp remove '${name}' exit ${r.status}: ${tail(`${r.stdout}
|
|
12702
|
+
${r.stderr}`, 200)}`);
|
|
12703
|
+
} else {
|
|
12704
|
+
removed.push(name);
|
|
12705
|
+
}
|
|
12706
|
+
} catch (err) {
|
|
12707
|
+
warnings.push(`mcp remove '${name}' threw: ${err?.message || err}`);
|
|
12708
|
+
}
|
|
12709
|
+
}
|
|
12710
|
+
if (warnings.length) {
|
|
12711
|
+
console.warn(`[ACP-MCP] cleanup warnings: ${warnings.join(" | ")}`);
|
|
12712
|
+
}
|
|
12713
|
+
return { removed, warnings };
|
|
12714
|
+
}
|
|
12715
|
+
|
|
12543
12716
|
// src/core/types.ts
|
|
12544
12717
|
var AGENT_INFO = {
|
|
12545
12718
|
name: "agy-acp",
|
|
12546
12719
|
title: "agy ACP (stream-json)",
|
|
12547
|
-
version: "0.1.
|
|
12720
|
+
version: "0.1.17"
|
|
12548
12721
|
};
|
|
12549
12722
|
var BRIDGE_CAPABILITIES = {
|
|
12550
12723
|
prompt: true,
|
|
@@ -12652,6 +12825,17 @@ function debugLog(msg) {
|
|
|
12652
12825
|
|
|
12653
12826
|
// src/core/session-core.ts
|
|
12654
12827
|
var EMPTY_SESSION_MAX_AGE_MS = 60 * 60 * 1000;
|
|
12828
|
+
function resolveAgyBin() {
|
|
12829
|
+
const rawBin = process.env.AGY_BIN;
|
|
12830
|
+
let bin = rawBin && rawBin !== "undefined" && rawBin !== "null" ? rawBin : "agy";
|
|
12831
|
+
if (bin === "agy" || bin === "agy.exe") {
|
|
12832
|
+
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12833
|
+
if (fs9.existsSync(geminiBin)) {
|
|
12834
|
+
bin = geminiBin;
|
|
12835
|
+
}
|
|
12836
|
+
}
|
|
12837
|
+
return bin;
|
|
12838
|
+
}
|
|
12655
12839
|
function deriveTitle(text, maxLen = 60) {
|
|
12656
12840
|
const line = String(text || "").split(/\r?\n/).map((s) => s.trim()).find(Boolean) || "";
|
|
12657
12841
|
const flat = line.replace(/\s+/g, " ");
|
|
@@ -12663,7 +12847,10 @@ class AgySessionCore {
|
|
|
12663
12847
|
sessionStore;
|
|
12664
12848
|
historyStore;
|
|
12665
12849
|
catalogPromise = null;
|
|
12850
|
+
mcpRunner;
|
|
12851
|
+
mcpRefs = new Map;
|
|
12666
12852
|
constructor(options) {
|
|
12853
|
+
this.mcpRunner = options?.mcpRunner ?? defaultMcpRunFn;
|
|
12667
12854
|
if (options?.sessionStore instanceof SessionStore) {
|
|
12668
12855
|
this.sessionStore = options.sessionStore;
|
|
12669
12856
|
} else if (typeof options?.sessionStore === "string") {
|
|
@@ -12712,14 +12899,8 @@ class AgySessionCore {
|
|
|
12712
12899
|
disableSlashCommands: disableSlash,
|
|
12713
12900
|
printTimeout
|
|
12714
12901
|
});
|
|
12715
|
-
const rawBin =
|
|
12716
|
-
|
|
12717
|
-
if (bin === "agy" || bin === "agy.exe") {
|
|
12718
|
-
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12719
|
-
if (fs9.existsSync(geminiBin)) {
|
|
12720
|
-
bin = geminiBin;
|
|
12721
|
-
}
|
|
12722
|
-
}
|
|
12902
|
+
const rawBin = resolveAgyBin();
|
|
12903
|
+
const bin = rawBin;
|
|
12723
12904
|
let execBin = bin;
|
|
12724
12905
|
let execArgs = args;
|
|
12725
12906
|
if (/\.(js|cjs|mjs|ts)$/i.test(bin)) {
|
|
@@ -12850,6 +13031,43 @@ class AgySessionCore {
|
|
|
12850
13031
|
if (!session.agent && agents.length)
|
|
12851
13032
|
session.agent = agents[0].value;
|
|
12852
13033
|
}
|
|
13034
|
+
async applySessionMcpServers(sessionId, input) {
|
|
13035
|
+
const servers = validateMcpServers(input ?? []);
|
|
13036
|
+
if (!servers.length)
|
|
13037
|
+
return [];
|
|
13038
|
+
const live = this.sessions.get(sessionId);
|
|
13039
|
+
const hadLiveProc = live ? live.proc.isWritable() : false;
|
|
13040
|
+
await syncMcpServers(resolveAgyBin(), servers, this.mcpRunner);
|
|
13041
|
+
const names = servers.map((s) => ({ name: s.name }));
|
|
13042
|
+
if (live)
|
|
13043
|
+
live.mcpServers = names;
|
|
13044
|
+
this.trackMcpServers(sessionId, names.map((n) => n.name));
|
|
13045
|
+
if (hadLiveProc) {
|
|
13046
|
+
console.warn(`[ACP-MCP] session ${sessionId}: MCP servers [${names.map((n) => n.name).join(", ")}] ` + `registered while the session process is live; they apply to fresh spawns (reconnect to use them).`);
|
|
13047
|
+
}
|
|
13048
|
+
return names;
|
|
13049
|
+
}
|
|
13050
|
+
trackMcpServers(sessionId, names) {
|
|
13051
|
+
for (const name of names) {
|
|
13052
|
+
let set = this.mcpRefs.get(name);
|
|
13053
|
+
if (!set) {
|
|
13054
|
+
set = new Set;
|
|
13055
|
+
this.mcpRefs.set(name, set);
|
|
13056
|
+
}
|
|
13057
|
+
set.add(sessionId);
|
|
13058
|
+
}
|
|
13059
|
+
}
|
|
13060
|
+
untrackMcpServers(sessionId) {
|
|
13061
|
+
const freed = [];
|
|
13062
|
+
for (const [name, set] of this.mcpRefs) {
|
|
13063
|
+
set.delete(sessionId);
|
|
13064
|
+
if (set.size === 0) {
|
|
13065
|
+
this.mcpRefs.delete(name);
|
|
13066
|
+
freed.push(name);
|
|
13067
|
+
}
|
|
13068
|
+
}
|
|
13069
|
+
return freed;
|
|
13070
|
+
}
|
|
12853
13071
|
async createSession(params, protocolVersion = 1) {
|
|
12854
13072
|
const cwd = params?.cwd;
|
|
12855
13073
|
debugLog(`createSession v${protocolVersion} cwd=${cwd}`);
|
|
@@ -12872,6 +13090,7 @@ class AgySessionCore {
|
|
|
12872
13090
|
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
12873
13091
|
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12874
13092
|
}
|
|
13093
|
+
validateMcpServers(params?.mcpServers);
|
|
12875
13094
|
const launch = extractLaunchConfig(params);
|
|
12876
13095
|
const discovery = await this.getDiscovery();
|
|
12877
13096
|
const sessionId = randomUUID3();
|
|
@@ -12914,6 +13133,7 @@ class AgySessionCore {
|
|
|
12914
13133
|
};
|
|
12915
13134
|
this.applyCatalogDefaults(session, discovery);
|
|
12916
13135
|
this.sessions.set(sessionId, session);
|
|
13136
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
12917
13137
|
this.persistSession(session);
|
|
12918
13138
|
this.warmupSession(sessionId);
|
|
12919
13139
|
return {
|
|
@@ -12942,14 +13162,10 @@ class AgySessionCore {
|
|
|
12942
13162
|
throw new RequestError(-32602, protocolVersion === 2 ? 'only replayFrom.type="start" is supported by this agent' : "session/resume with replayFrom is only supported by ACP v2");
|
|
12943
13163
|
}
|
|
12944
13164
|
}
|
|
12945
|
-
if (params?.mcpServers !== undefined) {
|
|
12946
|
-
|
|
12947
|
-
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12948
|
-
}
|
|
12949
|
-
if (params.mcpServers.length > 0) {
|
|
12950
|
-
throw new RequestError(-32602, "mcpServers are not supported by this agent");
|
|
12951
|
-
}
|
|
13165
|
+
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
13166
|
+
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12952
13167
|
}
|
|
13168
|
+
validateMcpServers(params?.mcpServers);
|
|
12953
13169
|
let additionalDirectories = [];
|
|
12954
13170
|
if (params?.additionalDirectories !== undefined) {
|
|
12955
13171
|
if (!Array.isArray(params.additionalDirectories)) {
|
|
@@ -13024,6 +13240,7 @@ class AgySessionCore {
|
|
|
13024
13240
|
}
|
|
13025
13241
|
const discovery = await this.getDiscovery();
|
|
13026
13242
|
this.applyCatalogDefaults(session, discovery);
|
|
13243
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
13027
13244
|
if (!session.title) {
|
|
13028
13245
|
try {
|
|
13029
13246
|
const first = this.historyStore.firstUserText(sessionId);
|
|
@@ -13200,6 +13417,14 @@ class AgySessionCore {
|
|
|
13200
13417
|
}
|
|
13201
13418
|
this.sessionStore.delete(sessionId);
|
|
13202
13419
|
this.historyStore.delete(sessionId);
|
|
13420
|
+
try {
|
|
13421
|
+
const freed = this.untrackMcpServers(sessionId);
|
|
13422
|
+
if (freed.length) {
|
|
13423
|
+
await removeMcpServers(resolveAgyBin(), freed, this.mcpRunner);
|
|
13424
|
+
}
|
|
13425
|
+
} catch (err) {
|
|
13426
|
+
console.warn(`[ACP-MCP] cleanup after delete ${sessionId} failed: ${err?.message || err}`);
|
|
13427
|
+
}
|
|
13203
13428
|
return {};
|
|
13204
13429
|
}
|
|
13205
13430
|
async closeSession(params) {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { type DiscoveryResult } from '../lib/agy-discovery.ts';
|
|
2
2
|
import { SessionStore } from '../lib/session-store.ts';
|
|
3
3
|
import { SessionHistoryStore } from '../lib/session-history.ts';
|
|
4
|
+
import { type McpRunFn } from '../lib/mcp-servers.ts';
|
|
4
5
|
import { type SdkSession, type ProtocolVersion } from './types.ts';
|
|
5
6
|
export interface SessionCoreOptions {
|
|
6
7
|
sessionStore?: SessionStore | string;
|
|
7
8
|
historyStore?: SessionHistoryStore | string;
|
|
9
|
+
/**
|
|
10
|
+
* `agy mcp ...` runner override (tests inject a fake; prod spawns agy).
|
|
11
|
+
* Keeps MCP sync unit-testable without touching the real global config.
|
|
12
|
+
*/
|
|
13
|
+
mcpRunner?: McpRunFn;
|
|
8
14
|
}
|
|
9
15
|
/** Rows without any completed turn older than this are hidden from
|
|
10
16
|
* session/list (still resumable/deletable by id — the store keeps them). */
|
|
11
17
|
export declare const EMPTY_SESSION_MAX_AGE_MS: number;
|
|
18
|
+
/** Resolve the agy binary (AGY_BIN, else ~/.gemini/bin/agy). Shared by spawn and `agy mcp ...` sync. */
|
|
19
|
+
export declare function resolveAgyBin(): string;
|
|
12
20
|
/** First user prompt collapsed to one line, capped for list display. */
|
|
13
21
|
export declare function deriveTitle(text: string, maxLen?: number): string;
|
|
14
22
|
export declare class AgySessionCore {
|
|
@@ -16,6 +24,13 @@ export declare class AgySessionCore {
|
|
|
16
24
|
readonly sessionStore: SessionStore;
|
|
17
25
|
readonly historyStore: SessionHistoryStore;
|
|
18
26
|
private catalogPromise;
|
|
27
|
+
private readonly mcpRunner;
|
|
28
|
+
/**
|
|
29
|
+
* MCP server name → sessionIds that registered it (this process only).
|
|
30
|
+
* Drives delete-time cleanup: a server is `agy mcp remove`d only when its
|
|
31
|
+
* last referencing session goes away.
|
|
32
|
+
*/
|
|
33
|
+
private readonly mcpRefs;
|
|
19
34
|
constructor(options?: SessionCoreOptions);
|
|
20
35
|
getDiscovery(): Promise<DiscoveryResult>;
|
|
21
36
|
/** Warm-up toggle: AGY_ACP_WARMUP=0/false/no disables connect-time pre-spawn. */
|
|
@@ -41,6 +56,19 @@ export declare class AgySessionCore {
|
|
|
41
56
|
replayHistory(sessionId: string, protocolVersion: ProtocolVersion, notifyClient: (update: any) => Promise<void> | void): Promise<void>;
|
|
42
57
|
sessionMeta(session: SdkSession): Record<string, unknown> | undefined;
|
|
43
58
|
applyCatalogDefaults(session: SdkSession, discovery: DiscoveryResult): void;
|
|
59
|
+
/**
|
|
60
|
+
* Sync ACP mcpServers into agy's MCP config BEFORE the session process
|
|
61
|
+
* spawns, so tools are listed from the first turn. Throws loud on any
|
|
62
|
+
* failure (a client that asked for MCP must never get a silent
|
|
63
|
+
* tools-less session). Tracks names per session for delete-time cleanup.
|
|
64
|
+
*/
|
|
65
|
+
private applySessionMcpServers;
|
|
66
|
+
private trackMcpServers;
|
|
67
|
+
/**
|
|
68
|
+
* Drop one session's references; returns names no other live session
|
|
69
|
+
* references anymore (safe to `agy mcp remove`).
|
|
70
|
+
*/
|
|
71
|
+
private untrackMcpServers;
|
|
44
72
|
createSession(params: any, protocolVersion?: ProtocolVersion): Promise<{
|
|
45
73
|
session: SdkSession;
|
|
46
74
|
discovery: DiscoveryResult;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface SdkSession {
|
|
|
58
58
|
disableSlashCommands?: boolean;
|
|
59
59
|
printTimeout?: string;
|
|
60
60
|
deleted?: boolean;
|
|
61
|
+
/** MCP servers this session registered (tracked for delete-time cleanup). */
|
|
62
|
+
mcpServers?: Array<{
|
|
63
|
+
name: string;
|
|
64
|
+
}>;
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Sequential FIFO execution queue to eliminate notification ordering races.
|
package/dist/index.js
CHANGED
|
@@ -10421,14 +10421,15 @@ function normalizePrintTimeout(v) {
|
|
|
10421
10421
|
return s === "" ? undefined : s;
|
|
10422
10422
|
}
|
|
10423
10423
|
function resolveSafety(session = {}, env = process.env) {
|
|
10424
|
-
|
|
10424
|
+
const explicitSafety = normalizeSafety(session.safety) ?? normalizeSafety(env.AGY_ACP_SAFETY) ?? undefined;
|
|
10425
|
+
let safety = explicitSafety;
|
|
10425
10426
|
if (!safety) {
|
|
10426
10427
|
const envSkip = env.AGY_ACP_SKIP_PERMISSIONS;
|
|
10427
10428
|
if (envSkip !== undefined && envSkip !== "") {
|
|
10428
10429
|
const skip = !(envSkip === "0" || envSkip === "false" || envSkip === "no");
|
|
10429
|
-
safety = skip ? "autonomous" : "safe";
|
|
10430
|
+
safety = skip ? "autonomous-unsandboxed" : "safe";
|
|
10430
10431
|
} else {
|
|
10431
|
-
safety = "autonomous";
|
|
10432
|
+
safety = "autonomous-unsandboxed";
|
|
10432
10433
|
}
|
|
10433
10434
|
}
|
|
10434
10435
|
let skipPermissions = safety === "autonomous" || safety === "autonomous-unsandboxed";
|
|
@@ -10436,7 +10437,7 @@ function resolveSafety(session = {}, env = process.env) {
|
|
|
10436
10437
|
skipPermissions = Boolean(session.skipPermissions);
|
|
10437
10438
|
}
|
|
10438
10439
|
let sandbox;
|
|
10439
|
-
if (safety === "autonomous-unsandboxed") {
|
|
10440
|
+
if (safety === "autonomous-unsandboxed" && explicitSafety === "autonomous-unsandboxed") {
|
|
10440
10441
|
sandbox = false;
|
|
10441
10442
|
} else if (session.sandbox !== undefined) {
|
|
10442
10443
|
sandbox = Boolean(session.sandbox);
|
|
@@ -11402,7 +11403,7 @@ function formatSoftDenyMessage(denies) {
|
|
|
11402
11403
|
bits.push(`(via ${d.source})`);
|
|
11403
11404
|
lines.push(bits.join(" "));
|
|
11404
11405
|
}
|
|
11405
|
-
lines.push("Or set AGY_ACP_SAFETY=safe / AGY_ACP_SKIP_PERMISSIONS=0 to re-enable permission checks (autonomous
|
|
11406
|
+
lines.push("Or set AGY_ACP_SAFETY=safe / AGY_ACP_SKIP_PERMISSIONS=0 to re-enable permission checks (autonomous-unsandboxed — full permissions, no sandbox — is the bridge default; AGY_ACP_SAFETY=autonomous re-enables sandbox containment).");
|
|
11406
11407
|
if (denies.some((d) => d.source?.includes("sandbox"))) {
|
|
11407
11408
|
lines.push("Sandbox note: --sandbox = terminal restrictions, not verified offline. Built-in search may still work; if `curl`/`Invoke-WebRequest` failed, retry once with safety=autonomous-unsandboxed (or sandbox:false) to confirm it is a sandbox network block.");
|
|
11408
11409
|
}
|
|
@@ -12327,11 +12328,183 @@ class SessionHistoryStore {
|
|
|
12327
12328
|
}
|
|
12328
12329
|
}
|
|
12329
12330
|
|
|
12331
|
+
// src/lib/mcp-servers.ts
|
|
12332
|
+
import { execFile } from "node:child_process";
|
|
12333
|
+
function fail(msg) {
|
|
12334
|
+
throw new RequestError(-32602, msg);
|
|
12335
|
+
}
|
|
12336
|
+
function nonEmptyString(v) {
|
|
12337
|
+
return typeof v === "string" && v.length > 0;
|
|
12338
|
+
}
|
|
12339
|
+
function strArray(v, what) {
|
|
12340
|
+
if (v === undefined)
|
|
12341
|
+
return [];
|
|
12342
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
12343
|
+
fail(`mcpServers[].${what} must be an array of strings`);
|
|
12344
|
+
}
|
|
12345
|
+
return v;
|
|
12346
|
+
}
|
|
12347
|
+
function nameValueList(v, what) {
|
|
12348
|
+
if (v === undefined)
|
|
12349
|
+
return [];
|
|
12350
|
+
if (!Array.isArray(v))
|
|
12351
|
+
fail(`mcpServers[].${what} must be an array`);
|
|
12352
|
+
return v.map((e, i) => {
|
|
12353
|
+
if (!e || typeof e !== "object")
|
|
12354
|
+
fail(`mcpServers[].${what}[${i}] must be {name, value}`);
|
|
12355
|
+
const rec = e;
|
|
12356
|
+
if (!nonEmptyString(rec.name) || typeof rec.value !== "string") {
|
|
12357
|
+
fail(`mcpServers[].${what}[${i}] must be {name: string, value: string}`);
|
|
12358
|
+
}
|
|
12359
|
+
return { name: rec.name, value: rec.value };
|
|
12360
|
+
});
|
|
12361
|
+
}
|
|
12362
|
+
function normalizeMcpServer(input) {
|
|
12363
|
+
if (!input || typeof input !== "object")
|
|
12364
|
+
fail("mcpServers[] must be an object");
|
|
12365
|
+
const s = input;
|
|
12366
|
+
if (!nonEmptyString(s.name))
|
|
12367
|
+
fail("mcpServers[].name must be a non-empty string");
|
|
12368
|
+
const name = s.name.trim();
|
|
12369
|
+
if (name.length > 64 || /[\s\x00-\x1f]/.test(name)) {
|
|
12370
|
+
fail(`mcpServers[].name must be ≤64 chars with no whitespace: ${JSON.stringify(name)}`);
|
|
12371
|
+
}
|
|
12372
|
+
const t = typeof s.type === "string" ? s.type.trim().toLowerCase() : "";
|
|
12373
|
+
if (t === "sse" || t === "acp") {
|
|
12374
|
+
fail(`mcpServers[] '${name}': type '${t}' has no 'agy mcp add' equivalent ` + `(agy supports stdio|http only). Resend as stdio or http.`);
|
|
12375
|
+
}
|
|
12376
|
+
if (t !== "" && t !== "stdio" && t !== "http") {
|
|
12377
|
+
fail(`mcpServers[] '${name}': unknown type ${JSON.stringify(s.type)} (want stdio|http)`);
|
|
12378
|
+
}
|
|
12379
|
+
const hasCommand = nonEmptyString(s.command);
|
|
12380
|
+
const hasUrl = nonEmptyString(s.url);
|
|
12381
|
+
if (hasCommand && hasUrl) {
|
|
12382
|
+
fail(`mcpServers[] '${name}': ambiguous (both command and url set); send one`);
|
|
12383
|
+
}
|
|
12384
|
+
if (t === "http" || !hasCommand && hasUrl) {
|
|
12385
|
+
if (!hasUrl)
|
|
12386
|
+
fail(`mcpServers[] '${name}': http server needs a url`);
|
|
12387
|
+
const url = s.url;
|
|
12388
|
+
if (!/^https?:\/\//i.test(url))
|
|
12389
|
+
fail(`mcpServers[] '${name}': url must start with http(s)://`);
|
|
12390
|
+
return { name, kind: "http", args: [], env: [], url, headers: nameValueList(s.headers, "headers") };
|
|
12391
|
+
}
|
|
12392
|
+
if (!hasCommand) {
|
|
12393
|
+
fail(`mcpServers[] '${name}': stdio server needs a command (or send type:"http" with a url)`);
|
|
12394
|
+
}
|
|
12395
|
+
return {
|
|
12396
|
+
name,
|
|
12397
|
+
kind: "stdio",
|
|
12398
|
+
command: s.command,
|
|
12399
|
+
args: strArray(s.args, "args"),
|
|
12400
|
+
env: nameValueList(s.env, "env"),
|
|
12401
|
+
headers: []
|
|
12402
|
+
};
|
|
12403
|
+
}
|
|
12404
|
+
function validateMcpServers(input) {
|
|
12405
|
+
if (input === undefined)
|
|
12406
|
+
return [];
|
|
12407
|
+
if (!Array.isArray(input))
|
|
12408
|
+
fail("mcpServers must be an array");
|
|
12409
|
+
const seen = new Set;
|
|
12410
|
+
return input.map((e) => {
|
|
12411
|
+
const n = normalizeMcpServer(e);
|
|
12412
|
+
if (seen.has(n.name))
|
|
12413
|
+
fail(`mcpServers[] duplicate name: '${n.name}'`);
|
|
12414
|
+
seen.add(n.name);
|
|
12415
|
+
return n;
|
|
12416
|
+
});
|
|
12417
|
+
}
|
|
12418
|
+
function mcpServerToAgyAddArgs(s) {
|
|
12419
|
+
const argv = ["mcp", "add"];
|
|
12420
|
+
if (s.kind === "stdio") {
|
|
12421
|
+
for (const e of s.env)
|
|
12422
|
+
argv.push("--env", `${e.name}=${e.value}`);
|
|
12423
|
+
argv.push(s.name, "--", s.command, ...s.args);
|
|
12424
|
+
return argv;
|
|
12425
|
+
}
|
|
12426
|
+
for (const h of s.headers)
|
|
12427
|
+
argv.push("--header", `${h.name}: ${h.value}`);
|
|
12428
|
+
argv.push("--type", "http", s.name, s.url);
|
|
12429
|
+
return argv;
|
|
12430
|
+
}
|
|
12431
|
+
function execFileAsync(bin, args, timeoutMs) {
|
|
12432
|
+
return new Promise((resolve) => {
|
|
12433
|
+
execFile(bin, args, { timeout: timeoutMs, maxBuffer: 512 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
12434
|
+
const e = err;
|
|
12435
|
+
resolve({
|
|
12436
|
+
status: typeof e?.code === "number" ? e.code : e ? 1 : 0,
|
|
12437
|
+
stdout: String(stdout ?? ""),
|
|
12438
|
+
stderr: e?.message ? `${e.message}
|
|
12439
|
+
${String(stderr ?? "")}` : String(stderr ?? "")
|
|
12440
|
+
});
|
|
12441
|
+
});
|
|
12442
|
+
});
|
|
12443
|
+
}
|
|
12444
|
+
var defaultMcpRunFn = (bin, args, opts) => execFileAsync(bin, args, opts.timeoutMs);
|
|
12445
|
+
function tail(text, n = 600) {
|
|
12446
|
+
const t = String(text || "").trim();
|
|
12447
|
+
return t.length > n ? "…" + t.slice(-n) : t;
|
|
12448
|
+
}
|
|
12449
|
+
async function syncMcpServers(bin, servers, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12450
|
+
const added = [];
|
|
12451
|
+
for (const s of servers) {
|
|
12452
|
+
let r;
|
|
12453
|
+
try {
|
|
12454
|
+
r = await runFn(bin, mcpServerToAgyAddArgs(s), { timeoutMs });
|
|
12455
|
+
} catch (err) {
|
|
12456
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}': ${err?.message || err}`);
|
|
12457
|
+
}
|
|
12458
|
+
if (r.status !== 0) {
|
|
12459
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}' (exit ${r.status}): ${tail(`${r.stdout}
|
|
12460
|
+
${r.stderr}`)}`);
|
|
12461
|
+
}
|
|
12462
|
+
added.push(s.name);
|
|
12463
|
+
}
|
|
12464
|
+
if (added.length) {
|
|
12465
|
+
let listed = "";
|
|
12466
|
+
try {
|
|
12467
|
+
const lr = await runFn(bin, ["mcp", "list"], { timeoutMs });
|
|
12468
|
+
listed = String(lr.stdout ?? "");
|
|
12469
|
+
} catch (err) {
|
|
12470
|
+
throw new RequestError(-32603, `registered MCP server(s) [${added.join(", ")}] but 'mcp list' verification failed: ${err?.message || err}`);
|
|
12471
|
+
}
|
|
12472
|
+
const lines = listed.split(`
|
|
12473
|
+
`);
|
|
12474
|
+
const missing = added.filter((n) => !lines.some((ln) => ln.includes(n)));
|
|
12475
|
+
if (missing.length) {
|
|
12476
|
+
throw new RequestError(-32603, `MCP server(s) [${missing.join(", ")}] exited 0 on 'mcp add' but are absent from 'mcp list' ` + `(agy swallows its global --version/--help flags even after '--'; check server args). ` + `List tail: ${tail(listed, 300)}`);
|
|
12477
|
+
}
|
|
12478
|
+
}
|
|
12479
|
+
return { added };
|
|
12480
|
+
}
|
|
12481
|
+
async function removeMcpServers(bin, names, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12482
|
+
const removed = [];
|
|
12483
|
+
const warnings = [];
|
|
12484
|
+
for (const name of names) {
|
|
12485
|
+
try {
|
|
12486
|
+
const r = await runFn(bin, ["mcp", "remove", name], { timeoutMs });
|
|
12487
|
+
if (r.status !== 0) {
|
|
12488
|
+
warnings.push(`mcp remove '${name}' exit ${r.status}: ${tail(`${r.stdout}
|
|
12489
|
+
${r.stderr}`, 200)}`);
|
|
12490
|
+
} else {
|
|
12491
|
+
removed.push(name);
|
|
12492
|
+
}
|
|
12493
|
+
} catch (err) {
|
|
12494
|
+
warnings.push(`mcp remove '${name}' threw: ${err?.message || err}`);
|
|
12495
|
+
}
|
|
12496
|
+
}
|
|
12497
|
+
if (warnings.length) {
|
|
12498
|
+
console.warn(`[ACP-MCP] cleanup warnings: ${warnings.join(" | ")}`);
|
|
12499
|
+
}
|
|
12500
|
+
return { removed, warnings };
|
|
12501
|
+
}
|
|
12502
|
+
|
|
12330
12503
|
// src/core/types.ts
|
|
12331
12504
|
var AGENT_INFO = {
|
|
12332
12505
|
name: "agy-acp",
|
|
12333
12506
|
title: "agy ACP (stream-json)",
|
|
12334
|
-
version: "0.1.
|
|
12507
|
+
version: "0.1.17"
|
|
12335
12508
|
};
|
|
12336
12509
|
var BRIDGE_CAPABILITIES = {
|
|
12337
12510
|
prompt: true,
|
|
@@ -12452,6 +12625,17 @@ function installFileLogging() {
|
|
|
12452
12625
|
|
|
12453
12626
|
// src/core/session-core.ts
|
|
12454
12627
|
var EMPTY_SESSION_MAX_AGE_MS = 60 * 60 * 1000;
|
|
12628
|
+
function resolveAgyBin() {
|
|
12629
|
+
const rawBin = process.env.AGY_BIN;
|
|
12630
|
+
let bin = rawBin && rawBin !== "undefined" && rawBin !== "null" ? rawBin : "agy";
|
|
12631
|
+
if (bin === "agy" || bin === "agy.exe") {
|
|
12632
|
+
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12633
|
+
if (fs9.existsSync(geminiBin)) {
|
|
12634
|
+
bin = geminiBin;
|
|
12635
|
+
}
|
|
12636
|
+
}
|
|
12637
|
+
return bin;
|
|
12638
|
+
}
|
|
12455
12639
|
function deriveTitle(text, maxLen = 60) {
|
|
12456
12640
|
const line = String(text || "").split(/\r?\n/).map((s) => s.trim()).find(Boolean) || "";
|
|
12457
12641
|
const flat = line.replace(/\s+/g, " ");
|
|
@@ -12463,7 +12647,10 @@ class AgySessionCore {
|
|
|
12463
12647
|
sessionStore;
|
|
12464
12648
|
historyStore;
|
|
12465
12649
|
catalogPromise = null;
|
|
12650
|
+
mcpRunner;
|
|
12651
|
+
mcpRefs = new Map;
|
|
12466
12652
|
constructor(options) {
|
|
12653
|
+
this.mcpRunner = options?.mcpRunner ?? defaultMcpRunFn;
|
|
12467
12654
|
if (options?.sessionStore instanceof SessionStore) {
|
|
12468
12655
|
this.sessionStore = options.sessionStore;
|
|
12469
12656
|
} else if (typeof options?.sessionStore === "string") {
|
|
@@ -12512,14 +12699,8 @@ class AgySessionCore {
|
|
|
12512
12699
|
disableSlashCommands: disableSlash,
|
|
12513
12700
|
printTimeout
|
|
12514
12701
|
});
|
|
12515
|
-
const rawBin =
|
|
12516
|
-
|
|
12517
|
-
if (bin === "agy" || bin === "agy.exe") {
|
|
12518
|
-
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12519
|
-
if (fs9.existsSync(geminiBin)) {
|
|
12520
|
-
bin = geminiBin;
|
|
12521
|
-
}
|
|
12522
|
-
}
|
|
12702
|
+
const rawBin = resolveAgyBin();
|
|
12703
|
+
const bin = rawBin;
|
|
12523
12704
|
let execBin = bin;
|
|
12524
12705
|
let execArgs = args;
|
|
12525
12706
|
if (/\.(js|cjs|mjs|ts)$/i.test(bin)) {
|
|
@@ -12650,6 +12831,43 @@ class AgySessionCore {
|
|
|
12650
12831
|
if (!session.agent && agents.length)
|
|
12651
12832
|
session.agent = agents[0].value;
|
|
12652
12833
|
}
|
|
12834
|
+
async applySessionMcpServers(sessionId, input) {
|
|
12835
|
+
const servers = validateMcpServers(input ?? []);
|
|
12836
|
+
if (!servers.length)
|
|
12837
|
+
return [];
|
|
12838
|
+
const live = this.sessions.get(sessionId);
|
|
12839
|
+
const hadLiveProc = live ? live.proc.isWritable() : false;
|
|
12840
|
+
await syncMcpServers(resolveAgyBin(), servers, this.mcpRunner);
|
|
12841
|
+
const names = servers.map((s) => ({ name: s.name }));
|
|
12842
|
+
if (live)
|
|
12843
|
+
live.mcpServers = names;
|
|
12844
|
+
this.trackMcpServers(sessionId, names.map((n) => n.name));
|
|
12845
|
+
if (hadLiveProc) {
|
|
12846
|
+
console.warn(`[ACP-MCP] session ${sessionId}: MCP servers [${names.map((n) => n.name).join(", ")}] ` + `registered while the session process is live; they apply to fresh spawns (reconnect to use them).`);
|
|
12847
|
+
}
|
|
12848
|
+
return names;
|
|
12849
|
+
}
|
|
12850
|
+
trackMcpServers(sessionId, names) {
|
|
12851
|
+
for (const name of names) {
|
|
12852
|
+
let set = this.mcpRefs.get(name);
|
|
12853
|
+
if (!set) {
|
|
12854
|
+
set = new Set;
|
|
12855
|
+
this.mcpRefs.set(name, set);
|
|
12856
|
+
}
|
|
12857
|
+
set.add(sessionId);
|
|
12858
|
+
}
|
|
12859
|
+
}
|
|
12860
|
+
untrackMcpServers(sessionId) {
|
|
12861
|
+
const freed = [];
|
|
12862
|
+
for (const [name, set] of this.mcpRefs) {
|
|
12863
|
+
set.delete(sessionId);
|
|
12864
|
+
if (set.size === 0) {
|
|
12865
|
+
this.mcpRefs.delete(name);
|
|
12866
|
+
freed.push(name);
|
|
12867
|
+
}
|
|
12868
|
+
}
|
|
12869
|
+
return freed;
|
|
12870
|
+
}
|
|
12653
12871
|
async createSession(params, protocolVersion = 1) {
|
|
12654
12872
|
const cwd = params?.cwd;
|
|
12655
12873
|
debugLog(`createSession v${protocolVersion} cwd=${cwd}`);
|
|
@@ -12672,6 +12890,7 @@ class AgySessionCore {
|
|
|
12672
12890
|
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
12673
12891
|
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12674
12892
|
}
|
|
12893
|
+
validateMcpServers(params?.mcpServers);
|
|
12675
12894
|
const launch = extractLaunchConfig(params);
|
|
12676
12895
|
const discovery = await this.getDiscovery();
|
|
12677
12896
|
const sessionId = randomUUID3();
|
|
@@ -12714,6 +12933,7 @@ class AgySessionCore {
|
|
|
12714
12933
|
};
|
|
12715
12934
|
this.applyCatalogDefaults(session, discovery);
|
|
12716
12935
|
this.sessions.set(sessionId, session);
|
|
12936
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
12717
12937
|
this.persistSession(session);
|
|
12718
12938
|
this.warmupSession(sessionId);
|
|
12719
12939
|
return {
|
|
@@ -12742,14 +12962,10 @@ class AgySessionCore {
|
|
|
12742
12962
|
throw new RequestError(-32602, protocolVersion === 2 ? 'only replayFrom.type="start" is supported by this agent' : "session/resume with replayFrom is only supported by ACP v2");
|
|
12743
12963
|
}
|
|
12744
12964
|
}
|
|
12745
|
-
if (params?.mcpServers !== undefined) {
|
|
12746
|
-
|
|
12747
|
-
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12748
|
-
}
|
|
12749
|
-
if (params.mcpServers.length > 0) {
|
|
12750
|
-
throw new RequestError(-32602, "mcpServers are not supported by this agent");
|
|
12751
|
-
}
|
|
12965
|
+
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
12966
|
+
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12752
12967
|
}
|
|
12968
|
+
validateMcpServers(params?.mcpServers);
|
|
12753
12969
|
let additionalDirectories = [];
|
|
12754
12970
|
if (params?.additionalDirectories !== undefined) {
|
|
12755
12971
|
if (!Array.isArray(params.additionalDirectories)) {
|
|
@@ -12824,6 +13040,7 @@ class AgySessionCore {
|
|
|
12824
13040
|
}
|
|
12825
13041
|
const discovery = await this.getDiscovery();
|
|
12826
13042
|
this.applyCatalogDefaults(session, discovery);
|
|
13043
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
12827
13044
|
if (!session.title) {
|
|
12828
13045
|
try {
|
|
12829
13046
|
const first = this.historyStore.firstUserText(sessionId);
|
|
@@ -13000,6 +13217,14 @@ class AgySessionCore {
|
|
|
13000
13217
|
}
|
|
13001
13218
|
this.sessionStore.delete(sessionId);
|
|
13002
13219
|
this.historyStore.delete(sessionId);
|
|
13220
|
+
try {
|
|
13221
|
+
const freed = this.untrackMcpServers(sessionId);
|
|
13222
|
+
if (freed.length) {
|
|
13223
|
+
await removeMcpServers(resolveAgyBin(), freed, this.mcpRunner);
|
|
13224
|
+
}
|
|
13225
|
+
} catch (err) {
|
|
13226
|
+
console.warn(`[ACP-MCP] cleanup after delete ${sessionId} failed: ${err?.message || err}`);
|
|
13227
|
+
}
|
|
13003
13228
|
return {};
|
|
13004
13229
|
}
|
|
13005
13230
|
async closeSession(params) {
|
|
@@ -17669,7 +17894,8 @@ __export(exports_core3, {
|
|
|
17669
17894
|
BRIDGE_CAPABILITIES: () => BRIDGE_CAPABILITIES,
|
|
17670
17895
|
EMPTY_SESSION_MAX_AGE_MS: () => EMPTY_SESSION_MAX_AGE_MS,
|
|
17671
17896
|
catalogChoices: () => catalogChoices,
|
|
17672
|
-
deriveTitle: () => deriveTitle
|
|
17897
|
+
deriveTitle: () => deriveTitle,
|
|
17898
|
+
resolveAgyBin: () => resolveAgyBin
|
|
17673
17899
|
});
|
|
17674
17900
|
// src/v1/index.ts
|
|
17675
17901
|
var exports_v1 = {};
|
package/dist/lib/agy-args.d.ts
CHANGED
|
@@ -114,14 +114,17 @@ export type SafetyResolveInput = {
|
|
|
114
114
|
*
|
|
115
115
|
* Tiers:
|
|
116
116
|
* safe — no skip-permissions; sandbox only if explicitly true
|
|
117
|
-
* autonomous
|
|
118
|
-
*
|
|
117
|
+
* autonomous — skip-permissions; default --sandbox unless sandbox:false
|
|
118
|
+
* (opt-in containment; explicit safety/autonomous only)
|
|
119
|
+
* autonomous-unsandboxed — skip-permissions; NEVER --sandbox (the default:
|
|
120
|
+
* full permissions, no terminal restrictions)
|
|
119
121
|
*
|
|
120
122
|
* Inputs:
|
|
121
123
|
* session.safety / AGY_ACP_SAFETY
|
|
122
|
-
* AGY_ACP_SKIP_PERMISSIONS=0 ≈ safe if safety unset; otherwise autonomous
|
|
124
|
+
* AGY_ACP_SKIP_PERMISSIONS=0 ≈ safe if safety unset; otherwise autonomous-unsandboxed
|
|
123
125
|
* explicit session.skipPermissions overrides the skip flag only
|
|
124
|
-
* explicit session.sandbox / AGY_ACP_SANDBOX override sandbox except
|
|
126
|
+
* explicit session.sandbox / AGY_ACP_SANDBOX override sandbox except when the
|
|
127
|
+
* unsandboxed tier was explicitly selected (defaulted tier honors opt-in)
|
|
125
128
|
*/
|
|
126
129
|
export declare function resolveSafety(session?: SafetyResolveInput, env?: NodeJS.ProcessEnv): ResolvedSafety;
|
|
127
130
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP `mcpServers` → `agy mcp add/remove` bridging.
|
|
3
|
+
*
|
|
4
|
+
* The bridge owns MCP registration end-to-end:
|
|
5
|
+
* - `session/new|resume` accept the standard ACP `mcpServers` list and sync
|
|
6
|
+
* it into agy's MCP config (`~/.gemini/config/mcp_config.json`) via
|
|
7
|
+
* `agy mcp add` BEFORE the session process spawns, so tools are listed
|
|
8
|
+
* from the first turn.
|
|
9
|
+
* - `session/delete` removes the servers this session registered (refcounted
|
|
10
|
+
* per bridge process; best-effort, never breaks close/delete).
|
|
11
|
+
* - Permissions stay under the bridge safety policy: MCP tool calls run with
|
|
12
|
+
* the session's `--dangerously-skip-permissions`/`--sandbox` flags, and
|
|
13
|
+
* headless soft-denies still surface `permissions.allow` guidance.
|
|
14
|
+
*
|
|
15
|
+
* Supported ACP shapes: stdio `{name, command, args, env[]}` and http
|
|
16
|
+
* `{name, url, headers[], type:"http"}`. `sse`/`acp` transports have no
|
|
17
|
+
* `agy mcp add` equivalent and fail fast (no silent downgrade).
|
|
18
|
+
*/
|
|
19
|
+
export interface AcpMcpServerInput {
|
|
20
|
+
name?: unknown;
|
|
21
|
+
type?: unknown;
|
|
22
|
+
command?: unknown;
|
|
23
|
+
args?: unknown;
|
|
24
|
+
env?: unknown;
|
|
25
|
+
url?: unknown;
|
|
26
|
+
headers?: unknown;
|
|
27
|
+
[k: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface NormalizedMcpServer {
|
|
30
|
+
name: string;
|
|
31
|
+
kind: 'stdio' | 'http';
|
|
32
|
+
command?: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
env: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}>;
|
|
38
|
+
url?: string;
|
|
39
|
+
headers: Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate one ACP McpServer entry. Throws RequestError(-32602) — loud,
|
|
46
|
+
* so a client that sent an unusable server never gets a silent no-tools
|
|
47
|
+
* session.
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeMcpServer(input: unknown): NormalizedMcpServer;
|
|
50
|
+
/** Validate a full session/new|resume mcpServers list (non-array → loud). */
|
|
51
|
+
export declare function validateMcpServers(input: unknown): NormalizedMcpServer[];
|
|
52
|
+
/**
|
|
53
|
+
* Build `agy mcp add` argv. agy rejects flags placed after <name>, so all
|
|
54
|
+
* flags come first: mcp add [--env K=V] [--header K:V] [--type t] <name>
|
|
55
|
+
* <commandOrUrl> [args...].
|
|
56
|
+
* stdio ALWAYS inserts `--` after <name>: agy consumes a global `--version`
|
|
57
|
+
* (and may misparse other dash-args) even in post-positional slots, which
|
|
58
|
+
* exits 0 while registering nothing (verified against agy 1.2.8). `--` ends
|
|
59
|
+
* flag parsing so command/args pass through verbatim.
|
|
60
|
+
*/
|
|
61
|
+
export declare function mcpServerToAgyAddArgs(s: NormalizedMcpServer): string[];
|
|
62
|
+
export interface McpRunResult {
|
|
63
|
+
status: number | null;
|
|
64
|
+
stdout: string;
|
|
65
|
+
stderr: string;
|
|
66
|
+
}
|
|
67
|
+
/** Injectable `agy mcp ...` runner (tests stub it; prod spawns agy). */
|
|
68
|
+
export type McpRunFn = (bin: string, args: string[], opts: {
|
|
69
|
+
timeoutMs: number;
|
|
70
|
+
}) => Promise<McpRunResult>;
|
|
71
|
+
/** Production runner: real `agy mcp ...` subprocess. */
|
|
72
|
+
export declare const defaultMcpRunFn: McpRunFn;
|
|
73
|
+
/**
|
|
74
|
+
* Register every server via `agy mcp add` (idempotent: add == upsert).
|
|
75
|
+
* Any failure throws RequestError — a client that asked for MCP must never
|
|
76
|
+
* get a silent tools-less session.
|
|
77
|
+
* Verify-after-add: agy exits 0 while registering NOTHING when argv contains
|
|
78
|
+
* its global flags (`--version`/`--help` are swallowed even after `--`),
|
|
79
|
+
* so every add is confirmed via `mcp list`. Absent-after-add throws loud.
|
|
80
|
+
*/
|
|
81
|
+
export declare function syncMcpServers(bin: string, servers: NormalizedMcpServer[], runFn?: McpRunFn, timeoutMs?: number): Promise<{
|
|
82
|
+
added: string[];
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Remove servers by name. Best-effort by design: returns per-name warnings
|
|
86
|
+
* instead of throwing, so session close/delete can never break on cleanup.
|
|
87
|
+
*/
|
|
88
|
+
export declare function removeMcpServers(bin: string, names: string[], runFn?: McpRunFn, timeoutMs?: number): Promise<{
|
|
89
|
+
removed: string[];
|
|
90
|
+
warnings: string[];
|
|
91
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yitom/agy-acp-map",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -48,8 +48,11 @@
|
|
|
48
48
|
"smoke:permissions": "bun tests/smoke/smoke-permissions.ts",
|
|
49
49
|
"smoke:image-in": "bun tests/smoke/smoke-image-in.ts",
|
|
50
50
|
"smoke:image-out": "bun tests/smoke/smoke-image-out.ts",
|
|
51
|
+
"smoke:mcp": "bun tests/smoke/smoke-mcp.ts",
|
|
52
|
+
"smoke:mcp-resume": "bun tests/smoke/smoke-mcp-resume.ts",
|
|
53
|
+
"smoke:mcp-e2e": "bun tests/smoke/smoke-mcp-e2e.ts",
|
|
51
54
|
"smoke:robustness": "bun tests/smoke/smoke-robustness.ts",
|
|
52
|
-
"smoke:all": "bun test src/lib && bun tests/test-agy-args.ts && bun tests/smoke/smoke-basic.ts && bun tests/smoke/smoke-flags.ts && bun tests/smoke/smoke-permissions.ts && bun tests/smoke/smoke-image-in.ts && bun tests/smoke/smoke-image-out.ts && bun tests/smoke/smoke-robustness.ts"
|
|
55
|
+
"smoke:all": "bun test src/lib && bun tests/test-agy-args.ts && bun tests/smoke/smoke-mcp.ts && bun tests/smoke/smoke-mcp-resume.ts && bun tests/smoke/smoke-basic.ts && bun tests/smoke/smoke-flags.ts && bun tests/smoke/smoke-permissions.ts && bun tests/smoke/smoke-image-in.ts && bun tests/smoke/smoke-image-out.ts && bun tests/smoke/smoke-robustness.ts"
|
|
53
56
|
},
|
|
54
57
|
"keywords": [
|
|
55
58
|
"antigravity",
|