@gajae-code/coding-agent 0.16.7 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for reading/writing .gjc/mcp.json files at user or project level.
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
6
8
|
import * as fs from "node:fs";
|
|
7
9
|
import * as path from "node:path";
|
|
10
|
+
import { exactReplacePath, type NativeExactFileIdentity } from "@gajae-code/natives";
|
|
8
11
|
import { isEnoent } from "@gajae-code/utils";
|
|
9
12
|
import { invalidate as invalidateFsCache } from "../capability/fs";
|
|
13
|
+
import { withFileLock } from "../config/file-lock";
|
|
10
14
|
|
|
11
15
|
import { validateServerConfig } from "./config";
|
|
16
|
+
import { readMcpBooleanField, restoreMcpBooleanField, writeMcpBooleanField } from "./policy-json-edit";
|
|
12
17
|
import { MCP_CONFIG_SCHEMA_URL, type MCPConfigFile, type MCPServerConfig } from "./types";
|
|
13
18
|
|
|
14
19
|
function withSchema(config: MCPConfigFile): MCPConfigFile {
|
|
@@ -298,3 +303,226 @@ export async function setServerDisabled(filePath: string, name: string, disabled
|
|
|
298
303
|
|
|
299
304
|
await writeMCPConfigFile(filePath, updated);
|
|
300
305
|
}
|
|
306
|
+
|
|
307
|
+
export interface ExactMcpBooleanUpdate {
|
|
308
|
+
readonly beforeRaw?: string;
|
|
309
|
+
readonly beforeIdentity?: NativeExactFileIdentity;
|
|
310
|
+
readonly beforeValue?: unknown;
|
|
311
|
+
readonly afterValue: boolean;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
interface McpFileSnapshot {
|
|
315
|
+
readonly raw: string;
|
|
316
|
+
readonly identity: NativeExactFileIdentity;
|
|
317
|
+
}
|
|
318
|
+
async function exactSnapshot(filePath: string): Promise<McpFileSnapshot | null> {
|
|
319
|
+
let handle: fs.promises.FileHandle | undefined;
|
|
320
|
+
try {
|
|
321
|
+
// O_NONBLOCK prevents `open()` itself from blocking forever if the
|
|
322
|
+
// lexical path was raced to a FIFO between an earlier lstat and this
|
|
323
|
+
// open; the immediate fstat below still rejects any non-regular file.
|
|
324
|
+
handle = await fs.promises.open(
|
|
325
|
+
filePath,
|
|
326
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
|
|
327
|
+
);
|
|
328
|
+
const stat = await handle.stat({ bigint: true });
|
|
329
|
+
const parent = await fs.promises.lstat(path.dirname(filePath), { bigint: true });
|
|
330
|
+
if (!stat.isFile() || stat.nlink !== 1n || parent.isSymbolicLink() || !parent.isDirectory())
|
|
331
|
+
throw new Error("MCP config is not an owned regular file");
|
|
332
|
+
const limit = 1024 * 1024;
|
|
333
|
+
if (stat.size > BigInt(limit)) throw new Error("MCP config size limit");
|
|
334
|
+
const bytes = new Uint8Array(limit + 1);
|
|
335
|
+
let length = 0;
|
|
336
|
+
while (length < bytes.length) {
|
|
337
|
+
const next = await handle.read(bytes, length, bytes.length - length, length);
|
|
338
|
+
if (next.bytesRead === 0) break;
|
|
339
|
+
length += next.bytesRead;
|
|
340
|
+
}
|
|
341
|
+
if (length > limit) throw new Error("MCP config size limit");
|
|
342
|
+
const after = await handle.stat({ bigint: true });
|
|
343
|
+
const lexical = await fs.promises.lstat(filePath, { bigint: true });
|
|
344
|
+
const parentAfter = await fs.promises.lstat(path.dirname(filePath), { bigint: true });
|
|
345
|
+
if (
|
|
346
|
+
after.dev !== stat.dev ||
|
|
347
|
+
after.ino !== stat.ino ||
|
|
348
|
+
after.mtimeNs !== stat.mtimeNs ||
|
|
349
|
+
after.ctimeNs !== stat.ctimeNs ||
|
|
350
|
+
after.size !== stat.size ||
|
|
351
|
+
after.nlink !== 1n ||
|
|
352
|
+
lexical.isSymbolicLink() ||
|
|
353
|
+
lexical.dev !== after.dev ||
|
|
354
|
+
lexical.ino !== after.ino ||
|
|
355
|
+
lexical.mtimeNs !== after.mtimeNs ||
|
|
356
|
+
lexical.ctimeNs !== after.ctimeNs ||
|
|
357
|
+
parentAfter.isSymbolicLink() ||
|
|
358
|
+
parentAfter.dev !== parent.dev ||
|
|
359
|
+
parentAfter.ino !== parent.ino
|
|
360
|
+
)
|
|
361
|
+
throw new Error("MCP config identity changed while reading");
|
|
362
|
+
const contents = bytes.subarray(0, length);
|
|
363
|
+
const identity = {
|
|
364
|
+
dev: stat.dev,
|
|
365
|
+
ino: stat.ino,
|
|
366
|
+
nlink: stat.nlink,
|
|
367
|
+
size: stat.size,
|
|
368
|
+
mtimeNs: stat.mtimeNs,
|
|
369
|
+
parentDev: parent.dev,
|
|
370
|
+
parentIno: parent.ino,
|
|
371
|
+
sha256: crypto.createHash("sha256").update(contents).digest("hex"),
|
|
372
|
+
};
|
|
373
|
+
return { raw: new TextDecoder("utf-8", { fatal: true }).decode(contents), identity };
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
376
|
+
if (code === "ENOENT") return null;
|
|
377
|
+
throw error;
|
|
378
|
+
} finally {
|
|
379
|
+
await handle?.close();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function exactIdentity(filePath: string): Promise<NativeExactFileIdentity | null> {
|
|
384
|
+
return (await exactSnapshot(filePath))?.identity ?? null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Exact-field JSON CAS used by doctor; server names are object keys and may contain dots. */
|
|
388
|
+
export async function updateMCPBooleanExact(
|
|
389
|
+
filePath: string,
|
|
390
|
+
name: string,
|
|
391
|
+
field: "autoload" | "enabled",
|
|
392
|
+
value: boolean,
|
|
393
|
+
expected: { readonly raw: string; readonly identity: NativeExactFileIdentity },
|
|
394
|
+
): Promise<{ beforeValue: unknown; afterValue: boolean; changed: boolean; afterIdentity?: NativeExactFileIdentity }> {
|
|
395
|
+
return await withFileLock(filePath, async () => {
|
|
396
|
+
const snapshot = await exactSnapshot(filePath);
|
|
397
|
+
const raw = snapshot?.raw;
|
|
398
|
+
const identity = snapshot?.identity;
|
|
399
|
+
if (
|
|
400
|
+
!identity ||
|
|
401
|
+
raw !== expected.raw ||
|
|
402
|
+
identity.dev !== expected.identity.dev ||
|
|
403
|
+
identity.ino !== expected.identity.ino ||
|
|
404
|
+
identity.size !== expected.identity.size ||
|
|
405
|
+
identity.mtimeNs !== expected.identity.mtimeNs ||
|
|
406
|
+
identity.parentDev !== expected.identity.parentDev ||
|
|
407
|
+
identity.parentIno !== expected.identity.parentIno ||
|
|
408
|
+
identity.sha256 !== expected.identity.sha256
|
|
409
|
+
)
|
|
410
|
+
throw new Error("MCP config compare-and-swap conflict");
|
|
411
|
+
const edit = writeMcpBooleanField(raw, name, field, value);
|
|
412
|
+
if (edit.status === "not_found") throw new Error("MCP server not found");
|
|
413
|
+
if (edit.status === "duplicate_key") throw new Error(`MCP config has a duplicate key at ${edit.path}`);
|
|
414
|
+
if (edit.status === "malformed") throw new Error("MCP config is not valid JSON");
|
|
415
|
+
const beforeValue = edit.beforeValue;
|
|
416
|
+
if (beforeValue === value) return { beforeValue, afterValue: value, changed: false };
|
|
417
|
+
const tmpPath = `${filePath}.doctor-${crypto.randomUUID()}.tmp`;
|
|
418
|
+
const handle = await fs.promises.open(
|
|
419
|
+
tmpPath,
|
|
420
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
421
|
+
0o600,
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
await handle.writeFile(edit.text, "utf8");
|
|
425
|
+
await handle.sync();
|
|
426
|
+
} finally {
|
|
427
|
+
await handle.close();
|
|
428
|
+
}
|
|
429
|
+
const source = await exactIdentity(tmpPath);
|
|
430
|
+
if (!source) throw new Error("MCP config identity unavailable");
|
|
431
|
+
const replaced = exactReplacePath(tmpPath, filePath, source, expected.identity);
|
|
432
|
+
if (!replaced.ok) throw new Error(`MCP config replacement failed: ${replaced.code ?? "unknown"}`);
|
|
433
|
+
const parentHandle = await fs.promises.open(path.dirname(filePath), "r");
|
|
434
|
+
try {
|
|
435
|
+
await parentHandle.sync();
|
|
436
|
+
} finally {
|
|
437
|
+
await parentHandle.close();
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
beforeValue,
|
|
441
|
+
afterValue: value,
|
|
442
|
+
changed: true,
|
|
443
|
+
afterIdentity: (await exactIdentity(filePath)) ?? undefined,
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export type RestoreMCPBooleanResult =
|
|
449
|
+
| { readonly status: "restored" }
|
|
450
|
+
| { readonly status: "conflict" }
|
|
451
|
+
| { readonly status: "failed"; readonly reason: string };
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Roll back exactly one `mcpServers.<name>.<field>` token to its captured
|
|
455
|
+
* `value` (which may be `undefined` to restore an originally-absent field, or
|
|
456
|
+
* an originally-invalid non-boolean scalar). Binds to the PUBLISHED file's
|
|
457
|
+
* dev/ino/parent identity captured right after the mutating replace — not the
|
|
458
|
+
* pre-mutation identity — and to the field's known current (post-mutation)
|
|
459
|
+
* value, so an unrelated in-place sibling edit made after publication is
|
|
460
|
+
* preserved rather than reverted by a stale whole-file snapshot. A CAS
|
|
461
|
+
* precondition mismatch (identity or field drifted) is a `conflict`; any
|
|
462
|
+
* other unexpected error is reported as `failed` and must never be silently
|
|
463
|
+
* folded into `conflict`, since only `conflict` means "an independent editor
|
|
464
|
+
* already resolved this and no action is required".
|
|
465
|
+
*/
|
|
466
|
+
export async function restoreMCPBooleanExact(
|
|
467
|
+
filePath: string,
|
|
468
|
+
name: string,
|
|
469
|
+
field: "autoload" | "enabled",
|
|
470
|
+
originalRaw: string,
|
|
471
|
+
expected: { readonly identity: NativeExactFileIdentity; readonly currentValue: boolean },
|
|
472
|
+
): Promise<RestoreMCPBooleanResult> {
|
|
473
|
+
return await withFileLock(filePath, async () => {
|
|
474
|
+
let current: McpFileSnapshot | null;
|
|
475
|
+
try {
|
|
476
|
+
current = await exactSnapshot(filePath);
|
|
477
|
+
} catch {
|
|
478
|
+
return { status: "failed", reason: "restore_snapshot_failed" };
|
|
479
|
+
}
|
|
480
|
+
if (
|
|
481
|
+
!current ||
|
|
482
|
+
current.identity.dev !== expected.identity.dev ||
|
|
483
|
+
current.identity.ino !== expected.identity.ino ||
|
|
484
|
+
current.identity.parentDev !== expected.identity.parentDev ||
|
|
485
|
+
current.identity.parentIno !== expected.identity.parentIno
|
|
486
|
+
)
|
|
487
|
+
return { status: "conflict" };
|
|
488
|
+
const read = readMcpBooleanField(current.raw, name, field);
|
|
489
|
+
if (read.status === "duplicate_key") return { status: "failed", reason: `duplicate key at ${read.path}` };
|
|
490
|
+
if (read.status === "malformed") return { status: "failed", reason: "MCP config is not valid JSON" };
|
|
491
|
+
if (read.status === "not_found" || read.value !== expected.currentValue) return { status: "conflict" };
|
|
492
|
+
const edit = restoreMcpBooleanField(current.raw, originalRaw, name, field);
|
|
493
|
+
if (edit.status !== "ok") return { status: "failed", reason: edit.status };
|
|
494
|
+
const tmpPath = `${filePath}.doctor-restore-${crypto.randomUUID()}.tmp`;
|
|
495
|
+
const restoreHandle = await fs.promises.open(
|
|
496
|
+
tmpPath,
|
|
497
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
498
|
+
0o600,
|
|
499
|
+
);
|
|
500
|
+
try {
|
|
501
|
+
await restoreHandle.writeFile(edit.text, "utf8");
|
|
502
|
+
await restoreHandle.sync();
|
|
503
|
+
} finally {
|
|
504
|
+
await restoreHandle.close();
|
|
505
|
+
}
|
|
506
|
+
const source = await exactIdentity(tmpPath);
|
|
507
|
+
if (!source) return { status: "failed", reason: "restore identity unavailable" };
|
|
508
|
+
const replaced = exactReplacePath(tmpPath, filePath, source, current.identity);
|
|
509
|
+
if (!replaced.ok) return { status: "failed", reason: "restore_publication_unverified" };
|
|
510
|
+
const verified = await exactSnapshot(filePath);
|
|
511
|
+
if (!verified) return { status: "failed", reason: "post-restore verification unavailable" };
|
|
512
|
+
if (
|
|
513
|
+
verified.raw !== edit.text ||
|
|
514
|
+
verified.identity.dev !== source.dev ||
|
|
515
|
+
verified.identity.ino !== source.ino ||
|
|
516
|
+
verified.identity.parentDev !== source.parentDev ||
|
|
517
|
+
verified.identity.parentIno !== source.parentIno
|
|
518
|
+
)
|
|
519
|
+
return { status: "failed", reason: "post_restore_identity_mismatch" };
|
|
520
|
+
const parentHandle = await fs.promises.open(path.dirname(filePath), "r");
|
|
521
|
+
try {
|
|
522
|
+
await parentHandle.sync();
|
|
523
|
+
} finally {
|
|
524
|
+
await parentHandle.close();
|
|
525
|
+
}
|
|
526
|
+
return { status: "restored" };
|
|
527
|
+
});
|
|
528
|
+
}
|
|
@@ -73,6 +73,108 @@ export function detectAuthError(error: Error): boolean {
|
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
const CHALLENGE_KEY_MAX_LENGTH = 64;
|
|
77
|
+
|
|
78
|
+
function isChallengeKeyStart(code: number): boolean {
|
|
79
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isChallengeKeyPart(code: number): boolean {
|
|
83
|
+
return isChallengeKeyStart(code) || (code >= 48 && code <= 57) || code === 45;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan challenge parameters without retrying an entire identifier run at every
|
|
88
|
+
* offset. Skipping a failed run is important: a bounded regex could reinterpret
|
|
89
|
+
* the suffix of an oversized key as a different, recognized parameter.
|
|
90
|
+
*/
|
|
91
|
+
function extractChallengeEntries(errorMessage: string): Array<[string, string]> {
|
|
92
|
+
const entries: Array<[string, string]> = [];
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
|
|
95
|
+
while (cursor < errorMessage.length) {
|
|
96
|
+
const keyStart = cursor;
|
|
97
|
+
if (!isChallengeKeyStart(errorMessage.charCodeAt(keyStart))) {
|
|
98
|
+
cursor++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let keyEnd = keyStart + 1;
|
|
103
|
+
while (isChallengeKeyPart(errorMessage.charCodeAt(keyEnd))) keyEnd++;
|
|
104
|
+
|
|
105
|
+
if (errorMessage.charCodeAt(keyEnd) !== 61 || errorMessage.charCodeAt(keyEnd + 1) !== 34) {
|
|
106
|
+
cursor = keyEnd + 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const valueStart = keyEnd + 2;
|
|
111
|
+
const valueEnd = errorMessage.indexOf('"', valueStart);
|
|
112
|
+
if (valueEnd === -1) break;
|
|
113
|
+
if (valueEnd > valueStart && keyEnd - keyStart <= CHALLENGE_KEY_MAX_LENGTH) {
|
|
114
|
+
entries.push([errorMessage.slice(keyStart, keyEnd).toLowerCase(), errorMessage.slice(valueStart, valueEnd)]);
|
|
115
|
+
}
|
|
116
|
+
cursor = valueEnd + 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface QuotedParameter {
|
|
123
|
+
markerStart: number;
|
|
124
|
+
valueEnd: number;
|
|
125
|
+
value: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRegexLineTerminator(code: number): boolean {
|
|
129
|
+
return code === 10 || code === 13 || code === 0x2028 || code === 0x2029;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function collectQuotedParameters(errorMessage: string, prefix: string): QuotedParameter[] {
|
|
133
|
+
const entries: QuotedParameter[] = [];
|
|
134
|
+
for (
|
|
135
|
+
let markerStart = errorMessage.indexOf(prefix);
|
|
136
|
+
markerStart !== -1;
|
|
137
|
+
markerStart = errorMessage.indexOf(prefix, markerStart + 1)
|
|
138
|
+
) {
|
|
139
|
+
const valueStart = markerStart + prefix.length;
|
|
140
|
+
const valueEnd = errorMessage.indexOf('"', valueStart);
|
|
141
|
+
if (valueEnd === -1) break;
|
|
142
|
+
if (valueEnd > valueStart)
|
|
143
|
+
entries.push({ markerStart, valueEnd, value: errorMessage.slice(valueStart, valueEnd) });
|
|
144
|
+
}
|
|
145
|
+
return entries;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Preserve the legacy `realm="...".*token_url="..."` fallback without the
|
|
150
|
+
* greedy wildcard rescanning every later `realm` marker. The wildcard cannot
|
|
151
|
+
* cross a JavaScript regex line terminator and selects the last valid token on
|
|
152
|
+
* the matching line.
|
|
153
|
+
*/
|
|
154
|
+
function extractLegacyWwwAuth(errorMessage: string): { authorizationUrl: string; tokenUrl: string } | null {
|
|
155
|
+
const lineAt = new Uint32Array(errorMessage.length + 1);
|
|
156
|
+
let line = 0;
|
|
157
|
+
for (let index = 0; index < errorMessage.length; index++) {
|
|
158
|
+
lineAt[index] = line;
|
|
159
|
+
if (isRegexLineTerminator(errorMessage.charCodeAt(index))) line++;
|
|
160
|
+
}
|
|
161
|
+
lineAt[errorMessage.length] = line;
|
|
162
|
+
|
|
163
|
+
const realms = collectQuotedParameters(errorMessage, 'realm="');
|
|
164
|
+
const tokens = collectQuotedParameters(errorMessage, 'token_url="');
|
|
165
|
+
const lastTokenByLine = new Map<number, QuotedParameter>();
|
|
166
|
+
for (const token of tokens) lastTokenByLine.set(lineAt[token.markerStart]!, token);
|
|
167
|
+
|
|
168
|
+
for (const realm of realms) {
|
|
169
|
+
const token = lastTokenByLine.get(lineAt[realm.valueEnd + 1]!);
|
|
170
|
+
if (token && token.markerStart > realm.valueEnd) {
|
|
171
|
+
return { authorizationUrl: realm.value, tokenUrl: token.value };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
76
178
|
/**
|
|
77
179
|
* Extract OAuth endpoints from error response.
|
|
78
180
|
* Looks for WWW-Authenticate header format or JSON error bodies.
|
|
@@ -130,9 +232,10 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
130
232
|
try {
|
|
131
233
|
// Try to parse as JSON error response
|
|
132
234
|
// Many MCP servers return JSON with OAuth endpoints in error body
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
235
|
+
const jsonStart = errorMsg.indexOf("{");
|
|
236
|
+
const jsonEnd = errorMsg.lastIndexOf("}");
|
|
237
|
+
if (jsonEnd > jsonStart) {
|
|
238
|
+
const errorBody = JSON.parse(errorMsg.slice(jsonStart, jsonEnd + 1)) as Record<string, unknown>;
|
|
136
239
|
|
|
137
240
|
// Check for OAuth endpoints in error body
|
|
138
241
|
if (errorBody.oauth || errorBody.authorization || errorBody.auth) {
|
|
@@ -160,11 +263,11 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
160
263
|
// Not JSON, continue with other detection methods
|
|
161
264
|
}
|
|
162
265
|
|
|
163
|
-
const challengeEntries =
|
|
266
|
+
const challengeEntries = extractChallengeEntries(errorMsg);
|
|
164
267
|
if (challengeEntries.length > 0) {
|
|
165
268
|
const challengeValues = new Map<string, string>();
|
|
166
|
-
for (const [,
|
|
167
|
-
challengeValues.set(
|
|
269
|
+
for (const [key, value] of challengeEntries) {
|
|
270
|
+
challengeValues.set(key, value);
|
|
168
271
|
}
|
|
169
272
|
|
|
170
273
|
const authorizationUrl =
|
|
@@ -186,15 +289,12 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
186
289
|
}
|
|
187
290
|
}
|
|
188
291
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const wwwAuthMatch = errorMsg.match(/realm="([^"]+)".*token_url="([^"]+)"/);
|
|
192
|
-
if (wwwAuthMatch) {
|
|
292
|
+
const wwwAuth = extractLegacyWwwAuth(errorMsg);
|
|
293
|
+
if (wwwAuth) {
|
|
193
294
|
return {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
scopes: scopeFromAuthUrl(wwwAuthMatch[1]),
|
|
295
|
+
...wwwAuth,
|
|
296
|
+
clientId: clientIdFromAuthUrl(wwwAuth.authorizationUrl),
|
|
297
|
+
scopes: scopeFromAuthUrl(wwwAuth.authorizationUrl),
|
|
198
298
|
};
|
|
199
299
|
}
|
|
200
300
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless JSON policy-field editing for `.gjc/mcp.json`.
|
|
3
|
+
*
|
|
4
|
+
* Uses `jsonc-parser`'s AST/edit API instead of `JSON.parse`/`JSON.stringify`
|
|
5
|
+
* so only the selected `mcpServers.<name>.<field>` token is rewritten. Every
|
|
6
|
+
* other token in the document (unrelated fields, large
|
|
7
|
+
* integer literals, `-0`, secrets) survives untouched. `JSON.parse` would
|
|
8
|
+
* silently normalize those values and collapse duplicate keys to "last one
|
|
9
|
+
* wins", hiding a real authoring bug — so duplicate keys along the selected
|
|
10
|
+
* path are rejected instead of being silently resolved.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
applyEdits,
|
|
14
|
+
findNodeAtLocation,
|
|
15
|
+
getNodeValue,
|
|
16
|
+
modify,
|
|
17
|
+
type Node,
|
|
18
|
+
type ParseError,
|
|
19
|
+
parseTree,
|
|
20
|
+
} from "jsonc-parser";
|
|
21
|
+
|
|
22
|
+
export type McpBooleanField = "autoload" | "enabled";
|
|
23
|
+
|
|
24
|
+
export type McpFieldReadResult =
|
|
25
|
+
| { readonly status: "ok"; readonly exists: boolean; readonly value: unknown }
|
|
26
|
+
| { readonly status: "duplicate_key"; readonly path: string }
|
|
27
|
+
| { readonly status: "malformed" }
|
|
28
|
+
| { readonly status: "not_found" };
|
|
29
|
+
|
|
30
|
+
export type McpFieldWriteResult =
|
|
31
|
+
| { readonly status: "ok"; readonly text: string; readonly beforeValue: unknown }
|
|
32
|
+
| { readonly status: "duplicate_key"; readonly path: string }
|
|
33
|
+
| { readonly status: "malformed" }
|
|
34
|
+
| { readonly status: "not_found" };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect a duplicate property name at each level while descending the given
|
|
38
|
+
* path. Scoped to the selected policy path only — this is not a whole-document
|
|
39
|
+
* scan; it visits only the objects on the selected path.
|
|
40
|
+
*/
|
|
41
|
+
export function findDuplicateKeyAlongPath(root: Node, path: readonly string[]): string | undefined {
|
|
42
|
+
let node: Node | undefined = root;
|
|
43
|
+
for (let depth = 0; depth < path.length; depth++) {
|
|
44
|
+
if (node?.type !== "object" || !node.children) return undefined;
|
|
45
|
+
let matched: Node | undefined;
|
|
46
|
+
for (const property of node.children) {
|
|
47
|
+
const keyNode = property.children?.[0];
|
|
48
|
+
if (!keyNode || keyNode.value !== path[depth]) continue;
|
|
49
|
+
if (matched) return path.slice(0, depth + 1).join(".");
|
|
50
|
+
matched = property;
|
|
51
|
+
}
|
|
52
|
+
node = matched?.children?.[1];
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parsePolicyTree(raw: string): Node | undefined {
|
|
58
|
+
const errors: ParseError[] = [];
|
|
59
|
+
const root = parseTree(raw, errors, { disallowComments: true, allowTrailingComma: false });
|
|
60
|
+
return root && errors.length === 0 ? root : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Read one selected `mcpServers.<name>.<field>` boolean without collapsing duplicate keys. */
|
|
64
|
+
export function readMcpBooleanField(raw: string, serverName: string, field: McpBooleanField): McpFieldReadResult {
|
|
65
|
+
const root = parsePolicyTree(raw);
|
|
66
|
+
if (!root) return { status: "malformed" };
|
|
67
|
+
const path = ["mcpServers", serverName, field];
|
|
68
|
+
const duplicate = findDuplicateKeyAlongPath(root, path);
|
|
69
|
+
if (duplicate) return { status: "duplicate_key", path: duplicate };
|
|
70
|
+
const serverNode = findNodeAtLocation(root, ["mcpServers", serverName]);
|
|
71
|
+
if (serverNode?.type !== "object") return { status: "not_found" };
|
|
72
|
+
const fieldNode = findNodeAtLocation(root, path);
|
|
73
|
+
return { status: "ok", exists: fieldNode !== undefined, value: fieldNode ? getNodeValue(fieldNode) : undefined };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compute the lossless text after setting `mcpServers.<name>.<field>` to
|
|
78
|
+
* `value`, or removing it when `value` is `undefined` (used to restore an
|
|
79
|
+
* originally-absent field). Only the selected token's edit range changes.
|
|
80
|
+
*/
|
|
81
|
+
export function writeMcpBooleanField(
|
|
82
|
+
raw: string,
|
|
83
|
+
serverName: string,
|
|
84
|
+
field: McpBooleanField,
|
|
85
|
+
/**
|
|
86
|
+
* The new field value. `undefined` removes the key, restoring an
|
|
87
|
+
* originally-absent field on rollback. A rollback may also need to restore
|
|
88
|
+
* an originally-invalid non-boolean scalar, so this intentionally accepts
|
|
89
|
+
* any JSON value rather than only `boolean`.
|
|
90
|
+
*/
|
|
91
|
+
value: unknown,
|
|
92
|
+
): McpFieldWriteResult {
|
|
93
|
+
const before = readMcpBooleanField(raw, serverName, field);
|
|
94
|
+
if (before.status !== "ok") return before;
|
|
95
|
+
const edits = modify(raw, ["mcpServers", serverName, field], value, {});
|
|
96
|
+
return { status: "ok", text: applyEdits(raw, edits), beforeValue: before.value };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Restore the original token, rather than reserializing its possibly-invalid value. */
|
|
100
|
+
export function restoreMcpBooleanField(
|
|
101
|
+
raw: string,
|
|
102
|
+
original: string,
|
|
103
|
+
serverName: string,
|
|
104
|
+
field: McpBooleanField,
|
|
105
|
+
): McpFieldWriteResult {
|
|
106
|
+
const current = readMcpBooleanField(raw, serverName, field);
|
|
107
|
+
const before = readMcpBooleanField(original, serverName, field);
|
|
108
|
+
if (current.status !== "ok") return current;
|
|
109
|
+
if (before.status !== "ok") return before;
|
|
110
|
+
if (!before.exists) return writeMcpBooleanField(raw, serverName, field, undefined);
|
|
111
|
+
const currentTree = parsePolicyTree(raw);
|
|
112
|
+
const originalTree = parsePolicyTree(original);
|
|
113
|
+
if (!currentTree || !originalTree) return { status: "malformed" };
|
|
114
|
+
const selected = ["mcpServers", serverName, field];
|
|
115
|
+
const currentNode = findNodeAtLocation(currentTree, selected);
|
|
116
|
+
const originalNode = findNodeAtLocation(originalTree, selected);
|
|
117
|
+
if (!currentNode || !originalNode) return { status: "not_found" };
|
|
118
|
+
return {
|
|
119
|
+
status: "ok",
|
|
120
|
+
beforeValue: current.value,
|
|
121
|
+
text: applyEdits(raw, [
|
|
122
|
+
{
|
|
123
|
+
offset: currentNode.offset,
|
|
124
|
+
length: currentNode.length,
|
|
125
|
+
content: original.slice(originalNode.offset, originalNode.offset + originalNode.length),
|
|
126
|
+
},
|
|
127
|
+
]),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MCPServerConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
export type AutoloadStatus = "autoload" | "autoload-off" | "disabled";
|
|
4
|
+
|
|
5
|
+
/** Pure startup policy shared by runtime-facing CLI inspection and doctor. */
|
|
6
|
+
export function computeAutoloadStatus(
|
|
7
|
+
name: string,
|
|
8
|
+
config: Pick<MCPServerConfig, "enabled" | "autoload">,
|
|
9
|
+
disabledServers: ReadonlySet<string>,
|
|
10
|
+
): AutoloadStatus {
|
|
11
|
+
if (config.enabled === false || disabledServers.has(name)) return "disabled";
|
|
12
|
+
if (config.autoload === false) return "autoload-off";
|
|
13
|
+
return "autoload";
|
|
14
|
+
}
|
package/src/runtime-mcp/types.ts
CHANGED
|
@@ -67,8 +67,8 @@ export interface MCPServerConfigBase {
|
|
|
67
67
|
* Whether an explicit runtime MCP consumer should connect this server
|
|
68
68
|
* automatically when that consumer starts (default: true). Ordinary
|
|
69
69
|
* standalone `gjc`, `gjc --tmux`, and print-mode sessions honor autoload for
|
|
70
|
-
* conventional registrations; `autoload: false` keeps a server
|
|
71
|
-
*
|
|
70
|
+
* conventional registrations; `autoload: false` keeps a server stored but
|
|
71
|
+
* unloaded at startup. `--no-mcp` opts a session out entirely.
|
|
72
72
|
*/
|
|
73
73
|
autoload?: boolean;
|
|
74
74
|
/** Connection timeout in milliseconds (default: 30000) */
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -198,6 +198,8 @@ export class AcpSdkAdapter {
|
|
|
198
198
|
#reverseCancelTtlMs: number;
|
|
199
199
|
#closed = false;
|
|
200
200
|
#started = false;
|
|
201
|
+
/** Reverse-provider leases are enabled only after host origin is authenticated. */
|
|
202
|
+
#providerActivationAuthorized = false;
|
|
201
203
|
constructor(options: AcpSdkAdapterOptions) {
|
|
202
204
|
if ((options.client === undefined) === (options.router === undefined))
|
|
203
205
|
throw new AcpSdkAdapterError(
|
|
@@ -225,6 +227,11 @@ export class AcpSdkAdapter {
|
|
|
225
227
|
return adapter;
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
/** Authorize provider registration after the host startup origin is known. */
|
|
231
|
+
authorizeProviderActivation(): void {
|
|
232
|
+
this.#providerActivationAuthorized = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
228
235
|
acceptAttachment(attachment: SessionAttachment): void {
|
|
229
236
|
if (!this.#router || attachment.sessionId !== this.#sessionId)
|
|
230
237
|
throw new AcpSdkAdapterError("invalid_input", "ACP attachment does not match this session adapter.");
|
|
@@ -238,7 +245,8 @@ export class AcpSdkAdapter {
|
|
|
238
245
|
this.#connectionId = undefined;
|
|
239
246
|
this.#routerConnectionReady = false;
|
|
240
247
|
this.#providersActivated = false;
|
|
241
|
-
if (attachment.isCurrent()
|
|
248
|
+
if (attachment.isCurrent() && this.#providerActivationAuthorized)
|
|
249
|
+
void this.#activateProviders().catch(error => this.#reportReconnectFailure(error));
|
|
242
250
|
}
|
|
243
251
|
|
|
244
252
|
revokeAttachment(attachment: SessionAttachment): void {
|
|
@@ -264,7 +272,7 @@ export class AcpSdkAdapter {
|
|
|
264
272
|
this.#providersActivated = false;
|
|
265
273
|
}
|
|
266
274
|
if (!attachment.isCurrent()) throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
267
|
-
await this.#activateProviders();
|
|
275
|
+
if (this.#providerActivationAuthorized) await this.#activateProviders();
|
|
268
276
|
}
|
|
269
277
|
/**
|
|
270
278
|
* Re-register reverse providers on a live attachment without aborting in-flight
|
|
@@ -273,7 +281,7 @@ export class AcpSdkAdapter {
|
|
|
273
281
|
* place for the life of the session (#4909).
|
|
274
282
|
*/
|
|
275
283
|
async ensureProviders(): Promise<void> {
|
|
276
|
-
if (this.#closed || this.#providers.length === 0) return;
|
|
284
|
+
if (!this.#providerActivationAuthorized || this.#closed || this.#providers.length === 0) return;
|
|
277
285
|
if (this.#ensuring) return await this.#ensuring;
|
|
278
286
|
const run = (async () => {
|
|
279
287
|
if (this.#closed) return;
|
|
@@ -307,9 +315,15 @@ export class AcpSdkAdapter {
|
|
|
307
315
|
return () => this.#frameHandlers.delete(handler);
|
|
308
316
|
}
|
|
309
317
|
|
|
310
|
-
async start(): Promise<void> {
|
|
318
|
+
async start(options: { activateProviders?: boolean } = {}): Promise<void> {
|
|
319
|
+
const activateProviders = options.activateProviders !== false;
|
|
311
320
|
if (this.#closed) throw new AcpSdkAdapterError("connection_closed");
|
|
321
|
+
if (this.#router && this.#attachment) assertMaintenanceCapability(this.#attachment);
|
|
312
322
|
if (!this.#started) {
|
|
323
|
+
// Direct adapter startup owns providers; ACP attachment defers this
|
|
324
|
+
// authority until runtime origin discovery. Repeated startup cannot
|
|
325
|
+
// promote a secondary attachment.
|
|
326
|
+
this.#providerActivationAuthorized = activateProviders;
|
|
313
327
|
this.#started = true;
|
|
314
328
|
if (this.#client) {
|
|
315
329
|
this.#unsubscribe ??= this.#client.onFrame(frame => void this.#onFrame(frame));
|
|
@@ -323,12 +337,13 @@ export class AcpSdkAdapter {
|
|
|
323
337
|
this.#connectionId = this.#client.connectionId;
|
|
324
338
|
}
|
|
325
339
|
}
|
|
326
|
-
if (
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
if (activateProviders) {
|
|
341
|
+
if (this.#router) {
|
|
342
|
+
if (!this.#attachment?.isCurrent()) return;
|
|
343
|
+
if (this.#providerActivationAuthorized) await this.#activateProviders();
|
|
344
|
+
} else {
|
|
345
|
+
if (this.#providerActivationAuthorized) await this.#activateProviders();
|
|
346
|
+
}
|
|
332
347
|
}
|
|
333
348
|
this.#heartbeat ??= setInterval(
|
|
334
349
|
() => void this.#heartbeatLeases().catch(error => this.#reportReconnectFailure(error)),
|
|
@@ -575,7 +590,7 @@ export class AcpSdkAdapter {
|
|
|
575
590
|
}
|
|
576
591
|
|
|
577
592
|
async #activateProviders(force = false): Promise<void> {
|
|
578
|
-
if (this.#providers.length === 0) return;
|
|
593
|
+
if (!this.#providerActivationAuthorized || this.#providers.length === 0) return;
|
|
579
594
|
if (this.#providerActivation) {
|
|
580
595
|
await this.#providerActivation;
|
|
581
596
|
if (this.#closed) return;
|
|
@@ -656,7 +671,7 @@ export class AcpSdkAdapter {
|
|
|
656
671
|
}
|
|
657
672
|
|
|
658
673
|
async #reclaimProviders(): Promise<void> {
|
|
659
|
-
if (this.#closed || !this.#providers.length) return;
|
|
674
|
+
if (!this.#providerActivationAuthorized || this.#closed || !this.#providers.length) return;
|
|
660
675
|
if (this.#reclaiming) return await this.#reclaiming;
|
|
661
676
|
this.#reclaiming = (async () => {
|
|
662
677
|
this.#abortActiveReverseRequests();
|