@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
package/src/capability/index.ts
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* - Registering providers (where to find it)
|
|
7
7
|
* - Loading items for a capability across all providers
|
|
8
8
|
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
9
11
|
import * as path from "node:path";
|
|
10
12
|
import {
|
|
13
|
+
CONFIG_DIR_NAME,
|
|
11
14
|
getAgentDir,
|
|
12
15
|
getAgentProfileAuthority,
|
|
16
|
+
getConfigDirName,
|
|
17
|
+
getPluginsDir,
|
|
13
18
|
getProjectDir,
|
|
14
19
|
getTrustedHomeDir,
|
|
15
20
|
logger,
|
|
@@ -45,6 +50,157 @@ const providerMeta = new Map<string, { displayName: string; description: string
|
|
|
45
50
|
/** Disabled providers (by ID) */
|
|
46
51
|
const disabledProviders = new Set<string>();
|
|
47
52
|
|
|
53
|
+
/** User/project roots used by providers that derive paths from LoadContext.home/cwd. */
|
|
54
|
+
const EXPLICIT_HOME_PROVIDER_ROOTS = [
|
|
55
|
+
// `claude` and `codex` are import aliases for the registered hook providers.
|
|
56
|
+
{ providerIds: ["claude", "claude-hooks"], user: null, project: ".claude" },
|
|
57
|
+
{ providerIds: ["codex", "codex-hooks"], user: null, project: ".codex" },
|
|
58
|
+
{ providerIds: ["gemini"], user: ".gemini", project: ".gemini" },
|
|
59
|
+
{ providerIds: ["opencode"], user: path.join(".config", "opencode"), project: ".opencode" },
|
|
60
|
+
{ providerIds: ["opencode"], user: null, project: "opencode.json" },
|
|
61
|
+
{ providerIds: ["cursor"], user: ".cursor", project: ".cursor" },
|
|
62
|
+
{ providerIds: ["windsurf"], user: path.join(".codeium", "windsurf"), project: ".windsurf" },
|
|
63
|
+
{ providerIds: ["cline"], user: null, project: ".clinerules" },
|
|
64
|
+
{ providerIds: ["github"], user: null, project: ".github" },
|
|
65
|
+
{ providerIds: ["agents"], user: ".agent", project: ".agent" },
|
|
66
|
+
{ providerIds: ["agents"], user: ".agents", project: ".agents" },
|
|
67
|
+
{ providerIds: ["agents-md"], user: null, project: "AGENTS.md" },
|
|
68
|
+
{ providerIds: ["mcp-json"], user: null, project: "mcp.json" },
|
|
69
|
+
{ providerIds: ["mcp-json"], user: null, project: ".mcp.json" },
|
|
70
|
+
{ providerIds: ["ssh-json"], user: null, project: "ssh.json" },
|
|
71
|
+
{ providerIds: ["ssh-json"], user: null, project: ".ssh.json" },
|
|
72
|
+
] as const;
|
|
73
|
+
|
|
74
|
+
const ANCESTOR_SCOPED_PROJECT_PROVIDERS = new Set(["agents", "agents-md", "cline"]);
|
|
75
|
+
|
|
76
|
+
function isWithinOrEqual(root: string, candidate: string): boolean {
|
|
77
|
+
const relative = path.relative(root, candidate);
|
|
78
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a path through the real path of its deepest existing ancestor,
|
|
83
|
+
* re-appending any not-yet-created suffix. Explicit-home discovery accepts
|
|
84
|
+
* fresh project paths, but must not let a symlinked existing ancestor alias a
|
|
85
|
+
* directory outside the supplied home.
|
|
86
|
+
*/
|
|
87
|
+
async function canonicalizeThroughExistingAncestor(target: string): Promise<string> {
|
|
88
|
+
const resolved = path.resolve(target);
|
|
89
|
+
const suffix: string[] = [];
|
|
90
|
+
let current = resolved;
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
try {
|
|
94
|
+
const real = await fs.realpath(current);
|
|
95
|
+
return suffix.length > 0 ? path.join(real, ...suffix.reverse()) : real;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
98
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
99
|
+
const parent = path.dirname(current);
|
|
100
|
+
if (parent === current) return resolved;
|
|
101
|
+
suffix.push(path.basename(current));
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function canonicalizeContainedPath(root: string, target: string, label: string): Promise<string> {
|
|
108
|
+
const canonical = await canonicalizeThroughExistingAncestor(target);
|
|
109
|
+
if (!isWithinOrEqual(root, canonical)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`loadCapabilityForHome ${label} escapes the supplied home through a symlink: "${target}" resolves to "${canonical}" outside "${root}".`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return canonical;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate every config root that explicit-home discovery can use before any
|
|
119
|
+
* provider is invoked. A symlinked `.gjc`/`agent`/`plugins` directory would
|
|
120
|
+
* otherwise make a path that is lexically inside the supplied home read an
|
|
121
|
+
* unrelated profile.
|
|
122
|
+
*/
|
|
123
|
+
async function assertExplicitHomeRoots(
|
|
124
|
+
canonicalHome: string,
|
|
125
|
+
cwd: string,
|
|
126
|
+
explicitAgentDir: boolean,
|
|
127
|
+
providerIds: ReadonlySet<string>,
|
|
128
|
+
isolatedHome: boolean,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const userRoots = new Map<string, string>();
|
|
131
|
+
const projectRoots = new Map<string, { label: string; walksAncestors: boolean }>();
|
|
132
|
+
const addUserRoot = (root: string, label: string): void => {
|
|
133
|
+
if (!userRoots.has(root)) userRoots.set(root, label);
|
|
134
|
+
};
|
|
135
|
+
const addProjectRoot = (root: string, label: string, walksAncestors = false): void => {
|
|
136
|
+
if (!projectRoots.has(root)) projectRoots.set(root, { label, walksAncestors });
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Resolve aliases before selecting roots. For example, the registered
|
|
140
|
+
// `claude-hooks` provider uses the `.claude` root shared by the `claude`
|
|
141
|
+
// import convention, while only its project scope is actually loaded.
|
|
142
|
+
for (const source of EXPLICIT_HOME_PROVIDER_ROOTS) {
|
|
143
|
+
if (!source.providerIds.some(providerId => providerIds.has(providerId))) continue;
|
|
144
|
+
const label = source.providerIds.join("/");
|
|
145
|
+
if (source.user) addUserRoot(path.join(canonicalHome, source.user), `${label} user root`);
|
|
146
|
+
if (source.project)
|
|
147
|
+
addProjectRoot(
|
|
148
|
+
source.project,
|
|
149
|
+
`${label} project root`,
|
|
150
|
+
source.providerIds.some(providerId => ANCESTOR_SCOPED_PROJECT_PROVIDERS.has(providerId)),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Native and SSH providers use the managed agent directory for user scope,
|
|
155
|
+
// while native, SSH, and project-scoped SSH config also read the project
|
|
156
|
+
// config root. An explicit agent directory is intentionally allowed outside
|
|
157
|
+
// the supplied home and is validated separately by the caller.
|
|
158
|
+
const usesNative = providerIds.has("native");
|
|
159
|
+
const usesSsh = providerIds.has("ssh-json");
|
|
160
|
+
if ((usesNative || usesSsh) && !explicitAgentDir)
|
|
161
|
+
addUserRoot(path.join(canonicalHome, getConfigDirName(), "agent"), "user agent directory");
|
|
162
|
+
if (usesNative || usesSsh) {
|
|
163
|
+
// Native and SSH project discovery are fixed to `.gjc`; configured project
|
|
164
|
+
// roots are validated only by adapters that actually consume them below.
|
|
165
|
+
addProjectRoot(".gjc", "native/SSH project registry root");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The marketplace provider derives its user registry from the XDG-aware
|
|
169
|
+
// helper rather than a fixed `<home>/.gjc/plugins` path.
|
|
170
|
+
if (providerIds.has("claude-plugins")) {
|
|
171
|
+
addProjectRoot(path.join(CONFIG_DIR_NAME, "plugins"), "project plugin registry root", true);
|
|
172
|
+
addUserRoot(getPluginsDir(canonicalHome), "plugin registry root");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const trustedHome = await canonicalizeThroughExistingAncestor(getTrustedHomeDir());
|
|
176
|
+
const currentProfilePluginRoot =
|
|
177
|
+
canonicalHome === trustedHome ? await canonicalizeThroughExistingAncestor(getPluginsDir(canonicalHome)) : null;
|
|
178
|
+
await Promise.all(
|
|
179
|
+
[...userRoots.entries()].map(async ([root, label]) => {
|
|
180
|
+
const canonicalRoot = await canonicalizeThroughExistingAncestor(root);
|
|
181
|
+
if (label === "plugin registry root" && canonicalRoot === currentProfilePluginRoot) return;
|
|
182
|
+
await canonicalizeContainedPath(canonicalHome, root, label);
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Validate only the roots at the boundaries their providers actually inspect.
|
|
187
|
+
// Cwd-only providers must not fail because an unrelated ancestor contains a
|
|
188
|
+
// symlinked config file; ancestor-scoped providers retain the full walk.
|
|
189
|
+
await Promise.all(
|
|
190
|
+
[...projectRoots.entries()].map(async ([relativeRoot, { label, walksAncestors }]) => {
|
|
191
|
+
if (!isolatedHome) return;
|
|
192
|
+
let current = cwd;
|
|
193
|
+
while (true) {
|
|
194
|
+
await canonicalizeContainedPath(canonicalHome, path.join(current, relativeRoot), label);
|
|
195
|
+
if (!walksAncestors || current === canonicalHome) break;
|
|
196
|
+
const parent = path.dirname(current);
|
|
197
|
+
if (parent === current) break;
|
|
198
|
+
current = parent;
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
48
204
|
/** Settings manager for persistence (if set) */
|
|
49
205
|
let settings: Settings | null = null;
|
|
50
206
|
/** Session-local settings keyed by normalized working directory. */
|
|
@@ -120,7 +276,7 @@ async function loadImpl<T>(
|
|
|
120
276
|
: new Set<string>(
|
|
121
277
|
options.disabledExtensions ??
|
|
122
278
|
options.settings?.get("disabledExtensions") ??
|
|
123
|
-
settings?.get("disabledExtensions") ??
|
|
279
|
+
(options.isolatedHome ? undefined : settings?.get("disabledExtensions")) ??
|
|
124
280
|
[],
|
|
125
281
|
);
|
|
126
282
|
|
|
@@ -221,8 +377,12 @@ async function loadImpl<T>(
|
|
|
221
377
|
* Filter providers based on options and disabled state.
|
|
222
378
|
*/
|
|
223
379
|
function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Provider<T>[] {
|
|
224
|
-
const activeSettings = options.
|
|
225
|
-
|
|
380
|
+
const activeSettings = options.isolatedHome
|
|
381
|
+
? options.settings
|
|
382
|
+
: (options.settings ?? settingsByCwd.get(path.normalize(options.cwd ?? getProjectDir())));
|
|
383
|
+
const activeDisabledProviders = new Set(
|
|
384
|
+
activeSettings?.get("disabledProviders") ?? (options.isolatedHome ? [] : disabledProviders),
|
|
385
|
+
);
|
|
226
386
|
let providers = (capability.providers as Provider<T>[]).filter(
|
|
227
387
|
p => options.includeDisabledProviders === true || !activeDisabledProviders.has(p.id),
|
|
228
388
|
);
|
|
@@ -247,19 +407,122 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
|
|
|
247
407
|
throw new Error(`Unknown capability: "${capabilityId}"`);
|
|
248
408
|
}
|
|
249
409
|
|
|
250
|
-
|
|
410
|
+
// `isolatedHome` is an internal option used only by loadCapabilityForHome.
|
|
411
|
+
// Public ordinary loads must always apply the active process settings and
|
|
412
|
+
// disabled-provider/extension policy, even if a caller supplies that field.
|
|
413
|
+
const ordinaryOptions: LoadOptions = options.isolatedHome ? { ...options, isolatedHome: false } : options;
|
|
414
|
+
const cwd = ordinaryOptions.cwd ?? getProjectDir();
|
|
251
415
|
const home = getTrustedHomeDir();
|
|
252
|
-
const userAgentDir =
|
|
416
|
+
const userAgentDir = ordinaryOptions.agentDir ? path.resolve(ordinaryOptions.agentDir) : getAgentDir();
|
|
253
417
|
const profileAuthority =
|
|
254
|
-
|
|
255
|
-
(
|
|
418
|
+
ordinaryOptions.profileAuthority ??
|
|
419
|
+
(ordinaryOptions.agentDir &&
|
|
420
|
+
normalizePathForComparison(userAgentDir) !== normalizePathForComparison(getAgentDir())
|
|
256
421
|
? "custom"
|
|
257
422
|
: getAgentProfileAuthority());
|
|
258
423
|
const repoRoot = await findRepoRoot(cwd);
|
|
259
|
-
const ctx: LoadContext = {
|
|
260
|
-
|
|
424
|
+
const ctx: LoadContext = {
|
|
425
|
+
cwd,
|
|
426
|
+
home,
|
|
427
|
+
userAgentDir,
|
|
428
|
+
profileAuthority,
|
|
429
|
+
repoRoot,
|
|
430
|
+
isolatedHome: false,
|
|
431
|
+
settings: ordinaryOptions.settings,
|
|
432
|
+
};
|
|
433
|
+
const providers = filterProviders(capability, ordinaryOptions);
|
|
434
|
+
|
|
435
|
+
return await loadImpl(capability, providers, ctx, ordinaryOptions);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Load a capability against an explicitly supplied home directory.
|
|
440
|
+
*
|
|
441
|
+
* Cross-profile authority contract: when an explicit `home` is supplied, the
|
|
442
|
+
* load must resolve every user-scope surface from that home and NEVER fall
|
|
443
|
+
* back to the process-global `getAgentDir()`. The agent directory is derived
|
|
444
|
+
* from the supplied home (`<home>/<configDir>/agent`) unless the caller
|
|
445
|
+
* supplies an explicit `agentDir`; there is no implicit process-profile
|
|
446
|
+
* fallback. Consumers that cannot derive a user scope must fail closed.
|
|
447
|
+
*/
|
|
448
|
+
export async function loadCapabilityForHome<T>(
|
|
449
|
+
capabilityId: string,
|
|
450
|
+
home: string,
|
|
451
|
+
options: LoadOptions = {},
|
|
452
|
+
): Promise<CapabilityResult<T>> {
|
|
453
|
+
const capability = capabilities.get(capabilityId) as Capability<T> | undefined;
|
|
454
|
+
if (!capability) {
|
|
455
|
+
throw new Error(`Unknown capability: "${capabilityId}"`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const resolvedHome = path.resolve(home);
|
|
459
|
+
if (!path.isAbsolute(home)) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`loadCapabilityForHome requires an absolute home directory; received "${home}". Refusing to fall back to the process profile.`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
// Derive the user agent directory from the SUPPLIED home. An explicit
|
|
465
|
+
// options.agentDir is honored; otherwise `<home>/<configDirName>/agent`.
|
|
466
|
+
// getAgentDir() is deliberately not consulted: an explicit home must never
|
|
467
|
+
// read another profile's SYSTEM/RULES/AGENTS, skills, commands, hooks,
|
|
468
|
+
// settings, or executable descriptors.
|
|
469
|
+
const canonicalHome = await canonicalizeThroughExistingAncestor(resolvedHome);
|
|
470
|
+
const homeStats = await fs.stat(canonicalHome);
|
|
471
|
+
const homeIdentity = { dev: homeStats.dev, ino: homeStats.ino };
|
|
472
|
+
const trustedHome = await canonicalizeThroughExistingAncestor(getTrustedHomeDir());
|
|
473
|
+
const isolatedHome = canonicalHome !== trustedHome;
|
|
474
|
+
const lexicalCwd = options.cwd === undefined ? canonicalHome : path.resolve(options.cwd);
|
|
475
|
+
const cwd = await canonicalizeThroughExistingAncestor(lexicalCwd);
|
|
476
|
+
// Every explicit-home cwd must resolve inside the supplied physical home.
|
|
477
|
+
// Allowing an outside cwd would let repository and plugin-registry discovery
|
|
478
|
+
// walk unrelated ancestors before the isolated boundary can take effect.
|
|
479
|
+
if (isolatedHome && !isWithinOrEqual(canonicalHome, cwd)) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`loadCapabilityForHome cwd is outside the supplied home: "${lexicalCwd}" resolves to "${cwd}" outside "${canonicalHome}".`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const isolatedOptions: LoadOptions = { ...options, isolatedHome };
|
|
486
|
+
const providers = filterProviders(capability, isolatedOptions);
|
|
487
|
+
const effectiveProviderIds = new Set(providers.map(provider => provider.id));
|
|
488
|
+
const usesAgentScope = effectiveProviderIds.has("native") || effectiveProviderIds.has("ssh-json");
|
|
489
|
+
const userAgentDir = options.agentDir
|
|
490
|
+
? await canonicalizeThroughExistingAncestor(path.resolve(options.agentDir))
|
|
491
|
+
: usesAgentScope
|
|
492
|
+
? await canonicalizeContainedPath(
|
|
493
|
+
canonicalHome,
|
|
494
|
+
path.join(canonicalHome, getConfigDirName(), "agent"),
|
|
495
|
+
"user agent directory",
|
|
496
|
+
)
|
|
497
|
+
: path.join(canonicalHome, getConfigDirName(), "agent");
|
|
498
|
+
const userAgentStats = await fs.stat(userAgentDir).catch(() => null);
|
|
499
|
+
const userAgentIdentity = userAgentStats ? { dev: userAgentStats.dev, ino: userAgentStats.ino } : null;
|
|
500
|
+
await assertExplicitHomeRoots(canonicalHome, cwd, Boolean(options.agentDir), effectiveProviderIds, isolatedHome);
|
|
501
|
+
const repoRootCandidate = await findRepoRoot(cwd, canonicalHome, {
|
|
502
|
+
isolatedHome,
|
|
503
|
+
homeIdentity,
|
|
504
|
+
home: canonicalHome,
|
|
505
|
+
bypassCache: true,
|
|
506
|
+
});
|
|
507
|
+
const canonicalRepoRoot = repoRootCandidate ? await canonicalizeThroughExistingAncestor(repoRootCandidate) : null;
|
|
508
|
+
const repoRoot =
|
|
509
|
+
canonicalRepoRoot &&
|
|
510
|
+
canonicalRepoRoot !== canonicalHome &&
|
|
511
|
+
(!isolatedHome || isWithinOrEqual(canonicalHome, canonicalRepoRoot))
|
|
512
|
+
? canonicalRepoRoot
|
|
513
|
+
: null;
|
|
514
|
+
const ctx: LoadContext = {
|
|
515
|
+
cwd,
|
|
516
|
+
home: canonicalHome,
|
|
517
|
+
userAgentDir,
|
|
518
|
+
userAgentIdentity,
|
|
519
|
+
repoRoot,
|
|
520
|
+
isolatedHome,
|
|
521
|
+
homeIdentity,
|
|
522
|
+
settings: isolatedOptions.settings,
|
|
523
|
+
};
|
|
261
524
|
|
|
262
|
-
return await loadImpl(capability, providers, ctx,
|
|
525
|
+
return await loadImpl(capability, providers, ctx, isolatedOptions);
|
|
263
526
|
}
|
|
264
527
|
|
|
265
528
|
// =============================================================================
|
package/src/capability/types.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Settings } from "../config/settings";
|
|
10
|
+
import type { FileIdentity } from "./fs";
|
|
10
11
|
/**
|
|
11
12
|
* Context passed to every provider loader.
|
|
12
13
|
*/
|
|
@@ -31,8 +32,14 @@ export interface LoadContext {
|
|
|
31
32
|
userAgentDir?: string;
|
|
32
33
|
/** Resolver-owned classification for the selected user agent directory. */
|
|
33
34
|
profileAuthority?: "default" | "custom";
|
|
35
|
+
/** Device/inode identity captured for the explicit home root. */
|
|
36
|
+
homeIdentity?: FileIdentity;
|
|
37
|
+
/** Device/inode identity captured for the isolated native agent root. */
|
|
38
|
+
userAgentIdentity?: FileIdentity | null;
|
|
34
39
|
/** Git repository root (directory containing .git), or null if not in a repo */
|
|
35
40
|
repoRoot: string | null;
|
|
41
|
+
/** Whether discovery must remain isolated to the supplied home boundary. */
|
|
42
|
+
isolatedHome?: boolean;
|
|
36
43
|
/** Owning session settings for provider policy decisions. */
|
|
37
44
|
settings?: Settings;
|
|
38
45
|
}
|
|
@@ -107,6 +114,8 @@ export interface LoadOptions {
|
|
|
107
114
|
disabledExtensions?: string[];
|
|
108
115
|
/** Session settings whose provider policy applies to this load. */
|
|
109
116
|
settings?: Settings;
|
|
117
|
+
/** Internal: prevent explicit-home loads from consulting process-global policy. */
|
|
118
|
+
isolatedHome?: boolean;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/**
|
|
@@ -297,10 +297,11 @@ const TRUST_BY_KIND: Record<CustomizeSurfaceKind, string> = {
|
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
/** Whether a restart/new session is required for changes to take effect. */
|
|
300
|
-
function restartRequiredFor(
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
300
|
+
function restartRequiredFor(_kind: CustomizeSurfaceKind): boolean {
|
|
301
|
+
// Every customization projection describes startup-owned state. MCP is not
|
|
302
|
+
// an in-session public reload surface, so it follows the same contract as
|
|
303
|
+
// every other customization kind.
|
|
304
|
+
return true;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
function sourceClassFor(provider: string): CustomizeSourceClass {
|
|
@@ -556,7 +557,7 @@ async function collectMcps(cwd: string, activeSettings: SettingsInstance): Promi
|
|
|
556
557
|
const disabledExts = disabledExtensionIds(activeSettings);
|
|
557
558
|
const disabledProviders = new Set(activeSettings.get("disabledProviders"));
|
|
558
559
|
// The startup projection: `loadAllMCPConfigs` is what a session uses when
|
|
559
|
-
// connecting MCP servers
|
|
560
|
+
// connecting MCP servers at startup (`--mcp-config` names one exact file). A single
|
|
560
561
|
// policy-violating endpoint (for example userinfo in a URL) fails the whole
|
|
561
562
|
// projection closed at startup, so catch it and report instead of losing
|
|
562
563
|
// the surface.
|
|
@@ -644,8 +645,10 @@ async function collectMcps(cwd: string, activeSettings: SettingsInstance): Promi
|
|
|
644
645
|
base,
|
|
645
646
|
"stored-only",
|
|
646
647
|
"storage-only",
|
|
647
|
-
"Discovered but never auto-connected by standalone sessions.
|
|
648
|
-
[
|
|
648
|
+
"Discovered but never auto-connected by standalone sessions at startup. Enable it for startup loading when you need it.",
|
|
649
|
+
[
|
|
650
|
+
`gjc --mcp-config ${entry.path} to load one exact config file (autoload: false servers stay unloaded; flip autoload and start a new session)`,
|
|
651
|
+
],
|
|
649
652
|
{ mcp: safeMcpSummary(server, connectable) },
|
|
650
653
|
);
|
|
651
654
|
});
|
|
@@ -962,8 +965,11 @@ async function collectExtensions(cwd: string, activeSettings: SettingsInstance):
|
|
|
962
965
|
nameOf: ext => ext.name,
|
|
963
966
|
pathOf: ext => ext.path,
|
|
964
967
|
notExecutedDetail:
|
|
965
|
-
"
|
|
966
|
-
notExecutedRemediation: [
|
|
968
|
+
"Trusted filesystem extension module discovered for session-start loading. Doctor does not execute the module, so its runtime input handlers, tools, commands, and flags remain opaque until a session loads it.",
|
|
969
|
+
notExecutedRemediation: [
|
|
970
|
+
"Review the trusted extension source before session startup",
|
|
971
|
+
"Disable one module with gjc config set disabledExtensions '[\"extension-module:<name>\"]', or set disableExtensionDiscovery in the SDK",
|
|
972
|
+
],
|
|
967
973
|
});
|
|
968
974
|
}
|
|
969
975
|
|
|
@@ -1404,7 +1410,7 @@ function foreignMcpItem(
|
|
|
1404
1410
|
scope: "project",
|
|
1405
1411
|
path: server._source.path,
|
|
1406
1412
|
trust: TRUST_BY_KIND.mcp,
|
|
1407
|
-
restartRequired:
|
|
1413
|
+
restartRequired: restartRequiredFor("mcp"),
|
|
1408
1414
|
precedence: { priority: 0, shadowedBy },
|
|
1409
1415
|
};
|
|
1410
1416
|
return {
|
|
@@ -1414,7 +1420,7 @@ function foreignMcpItem(
|
|
|
1414
1420
|
detail: `${desc.providerName} project convention (${desc.dirName}/) is not part of the GJC load path; sessions never discover this MCP server.`,
|
|
1415
1421
|
remediation: [
|
|
1416
1422
|
`Move the server definition to .gjc/mcp.json to make it discoverable`,
|
|
1417
|
-
"
|
|
1423
|
+
"then enable it for startup loading (autoload: false servers stay unloaded; flip autoload and start a new session)",
|
|
1418
1424
|
],
|
|
1419
1425
|
mcp: safeMcpSummary(server, false),
|
|
1420
1426
|
};
|
|
@@ -1598,7 +1604,7 @@ export async function runCustomizeDoctor(
|
|
|
1598
1604
|
skillScopeNotes,
|
|
1599
1605
|
disabledProviders: settings.get("disabledProviders"),
|
|
1600
1606
|
mcpNote:
|
|
1601
|
-
"Standalone sessions never auto-connect MCP servers.
|
|
1607
|
+
"Standalone sessions never auto-connect undiscoverable MCP servers at startup. Discoverable servers load from GJC's own config scopes, or name one exact file with gjc --mcp-config <path> (autoload: false servers stay unloaded; flip autoload and start a new session).",
|
|
1602
1608
|
conventionsNotLoaded: foreignPolicyNote ? [foreignPolicyNote] : [],
|
|
1603
1609
|
globalImportCandidateDirs,
|
|
1604
1610
|
sourceClasses: SOURCE_CLASS_DESCRIPTIONS,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { isDoctorTargetId } from "./ids";
|
|
2
|
+
import type { DoctorMode, DoctorRepair, DoctorRiskClass, DoctorScope } from "./types";
|
|
3
|
+
|
|
4
|
+
export type DoctorAction =
|
|
5
|
+
| "config.set-validated"
|
|
6
|
+
| "mcp.set-startup-policy"
|
|
7
|
+
| "permissions.restrict-owned-config"
|
|
8
|
+
| "install.restore-binary"
|
|
9
|
+
| "install.repair-managed-link"
|
|
10
|
+
| "plugin.restore-known-artifact"
|
|
11
|
+
| "plugin.quarantine-selected"
|
|
12
|
+
| "service.restart-owned"
|
|
13
|
+
| "service.detach-owned-stale-artifact";
|
|
14
|
+
export interface DoctorCliOptions {
|
|
15
|
+
readonly mode: DoctorMode;
|
|
16
|
+
readonly json: boolean;
|
|
17
|
+
readonly help: boolean;
|
|
18
|
+
readonly checks: readonly string[];
|
|
19
|
+
readonly scope?: DoctorScope;
|
|
20
|
+
readonly timeoutMs?: number;
|
|
21
|
+
readonly repair?: DoctorAction;
|
|
22
|
+
readonly targetId?: string;
|
|
23
|
+
readonly setValue?: boolean;
|
|
24
|
+
readonly ref?: string;
|
|
25
|
+
readonly sha256?: string;
|
|
26
|
+
readonly allowRisks: readonly DoctorRiskClass[];
|
|
27
|
+
readonly yes: boolean;
|
|
28
|
+
readonly drainSeconds?: number;
|
|
29
|
+
}
|
|
30
|
+
export type DoctorConfirmationResult = "confirmed" | "confirmation_declined" | "confirmation_timeout" | "cancelled";
|
|
31
|
+
export interface DoctorOptions extends DoctorCliOptions {
|
|
32
|
+
readonly cwd?: string;
|
|
33
|
+
readonly tty?: boolean;
|
|
34
|
+
readonly signal?: AbortSignal;
|
|
35
|
+
readonly runId?: string;
|
|
36
|
+
readonly deadline?: number;
|
|
37
|
+
/** Internal supervisor channel; never populated from CLI data. */
|
|
38
|
+
readonly confirmRepair?: (repair: DoctorRepair) => Promise<DoctorConfirmationResult>;
|
|
39
|
+
/** Synchronous notification before entering an effect-capable transaction. */
|
|
40
|
+
readonly onRepairAdmitted?: () => void;
|
|
41
|
+
}
|
|
42
|
+
export interface DoctorParseResult {
|
|
43
|
+
readonly options?: DoctorOptions;
|
|
44
|
+
readonly error?: string;
|
|
45
|
+
}
|
|
46
|
+
const ACTION_TARGET_KINDS = {
|
|
47
|
+
"config.set-validated": "config",
|
|
48
|
+
"mcp.set-startup-policy": "mcp",
|
|
49
|
+
"permissions.restrict-owned-config": "permission",
|
|
50
|
+
"install.restore-binary": "binary",
|
|
51
|
+
"install.repair-managed-link": "link",
|
|
52
|
+
"plugin.restore-known-artifact": "plugin",
|
|
53
|
+
"plugin.quarantine-selected": "plugin",
|
|
54
|
+
"service.restart-owned": "service",
|
|
55
|
+
"service.detach-owned-stale-artifact": "artifact",
|
|
56
|
+
} as const satisfies Record<DoctorAction, string>;
|
|
57
|
+
const ACTIONS = new Set(Object.keys(ACTION_TARGET_KINDS));
|
|
58
|
+
export function isDoctorAction(value: unknown): value is DoctorAction {
|
|
59
|
+
return typeof value === "string" && ACTIONS.has(value);
|
|
60
|
+
}
|
|
61
|
+
const RISKS = new Set<DoctorRiskClass>([
|
|
62
|
+
"config-change",
|
|
63
|
+
"permission-change",
|
|
64
|
+
"install-replace",
|
|
65
|
+
"plugin-change",
|
|
66
|
+
"service-interruption",
|
|
67
|
+
"artifact-detach",
|
|
68
|
+
"network",
|
|
69
|
+
"external-execution",
|
|
70
|
+
]);
|
|
71
|
+
function value(argv: readonly string[], i: number, _flag: string): [string | undefined, number] {
|
|
72
|
+
const v = argv[i + 1];
|
|
73
|
+
return !v || v.startsWith("-") ? [undefined, i] : [v, i + 1];
|
|
74
|
+
}
|
|
75
|
+
export function parseDoctorArgs(argv: readonly string[]): DoctorParseResult {
|
|
76
|
+
let mode: DoctorMode = "diagnose",
|
|
77
|
+
json = false,
|
|
78
|
+
help = false,
|
|
79
|
+
scope: DoctorScope | undefined,
|
|
80
|
+
timeoutMs: number | undefined;
|
|
81
|
+
let repair: DoctorAction | undefined,
|
|
82
|
+
targetId: string | undefined,
|
|
83
|
+
setValue: boolean | undefined,
|
|
84
|
+
ref: string | undefined,
|
|
85
|
+
sha256: string | undefined,
|
|
86
|
+
drainSeconds: number | undefined,
|
|
87
|
+
yes = false;
|
|
88
|
+
const checks = new Set<string>(),
|
|
89
|
+
allowRisks = new Set<DoctorRiskClass>();
|
|
90
|
+
const singleton = new Set([
|
|
91
|
+
"--repair",
|
|
92
|
+
"--target",
|
|
93
|
+
"--set-value-json",
|
|
94
|
+
"--ref",
|
|
95
|
+
"--sha256",
|
|
96
|
+
"--scope",
|
|
97
|
+
"--timeout-ms",
|
|
98
|
+
]);
|
|
99
|
+
for (let i = 0; i < argv.length; i++) {
|
|
100
|
+
const arg = argv[i];
|
|
101
|
+
if (arg === "--json") {
|
|
102
|
+
json = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === "--help" || arg === "-h") {
|
|
106
|
+
help = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (arg === "--fix") {
|
|
110
|
+
if (mode === "dry-run") continue;
|
|
111
|
+
mode = "fix";
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg === "--dry-run") {
|
|
115
|
+
mode = "dry-run";
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (arg === "--yes") {
|
|
119
|
+
yes = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const equals = arg.indexOf("=");
|
|
123
|
+
const flag = equals < 0 ? arg : arg.slice(0, equals);
|
|
124
|
+
const raw = equals < 0 ? undefined : arg.slice(equals + 1);
|
|
125
|
+
const hasInline = raw !== undefined;
|
|
126
|
+
if (flag === "--check" || flag === "--allow-risk") {
|
|
127
|
+
const v = hasInline ? raw : value(argv, i, flag)[0];
|
|
128
|
+
if (!v) return { error: `${flag} requires a value` };
|
|
129
|
+
if (!hasInline) i++;
|
|
130
|
+
if (flag === "--check") checks.add(v);
|
|
131
|
+
else {
|
|
132
|
+
if (!RISKS.has(v as DoctorRiskClass)) return { error: "unknown risk class" };
|
|
133
|
+
allowRisks.add(v as DoctorRiskClass);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (singleton.has(flag)) {
|
|
138
|
+
if (
|
|
139
|
+
(flag === "--repair" && repair) ||
|
|
140
|
+
(flag === "--target" && targetId) ||
|
|
141
|
+
(flag === "--set-value-json" && setValue !== undefined) ||
|
|
142
|
+
(flag === "--ref" && ref) ||
|
|
143
|
+
(flag === "--sha256" && sha256) ||
|
|
144
|
+
(flag === "--scope" && scope) ||
|
|
145
|
+
(flag === "--timeout-ms" && timeoutMs !== undefined)
|
|
146
|
+
)
|
|
147
|
+
return { error: `${flag} may be specified only once` };
|
|
148
|
+
const v = hasInline ? raw : value(argv, i, flag)[0];
|
|
149
|
+
if (!v) return { error: `${flag} requires a value` };
|
|
150
|
+
if (!hasInline) i++;
|
|
151
|
+
if (flag === "--repair") {
|
|
152
|
+
if (!isDoctorAction(v)) return { error: "unknown repair action" };
|
|
153
|
+
repair = v;
|
|
154
|
+
} else if (flag === "--target") targetId = v;
|
|
155
|
+
else if (flag === "--set-value-json") {
|
|
156
|
+
if (v !== "true" && v !== "false") return { error: "--set-value-json accepts only true or false" };
|
|
157
|
+
setValue = v === "true";
|
|
158
|
+
} else if (flag === "--ref") ref = v;
|
|
159
|
+
else if (flag === "--sha256") {
|
|
160
|
+
if (!/^[a-fA-F0-9]{64}$/.test(v)) return { error: "--sha256 must be 64 hexadecimal characters" };
|
|
161
|
+
sha256 = v.toLowerCase();
|
|
162
|
+
} else if (flag === "--scope") {
|
|
163
|
+
if (v !== "user" && v !== "project") return { error: "--scope must be user or project" };
|
|
164
|
+
scope = v;
|
|
165
|
+
} else {
|
|
166
|
+
const n = Number(v);
|
|
167
|
+
if (!Number.isInteger(n) || n < 1000 || n > 120000)
|
|
168
|
+
return { error: "--timeout-ms must be an integer from 1000 to 120000" };
|
|
169
|
+
timeoutMs = n;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (arg === "--drain") {
|
|
174
|
+
if (drainSeconds !== undefined) return { error: "--drain may be specified only once" };
|
|
175
|
+
drainSeconds = 30;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (arg.startsWith("--")) return { error: "unknown option" };
|
|
179
|
+
return { error: "unexpected argument" };
|
|
180
|
+
}
|
|
181
|
+
if (mode === "fix" && (!repair || !targetId)) return { error: "--fix requires --repair and --target" };
|
|
182
|
+
if (mode === "dry-run" && repair !== undefined && targetId === undefined)
|
|
183
|
+
return { error: "--repair requires --target" };
|
|
184
|
+
if (drainSeconds !== undefined && repair !== "service.restart-owned")
|
|
185
|
+
return { error: "--drain is only valid for service.restart-owned" };
|
|
186
|
+
if (targetId !== undefined && !isDoctorTargetId(targetId))
|
|
187
|
+
return { error: "target must be a stable doctor target ID" };
|
|
188
|
+
if (repair && targetId && targetId.split(":")[1] !== ACTION_TARGET_KINDS[repair])
|
|
189
|
+
return { error: "repair action does not match target kind" };
|
|
190
|
+
if (scope && targetId) {
|
|
191
|
+
const parts = targetId.split(":");
|
|
192
|
+
const targetScope = parts.includes("user") ? "user" : parts.includes("project") ? "project" : undefined;
|
|
193
|
+
if (!targetScope) return { error: "--scope is unsupported for this target" };
|
|
194
|
+
if (targetScope !== scope) return { error: "--scope does not match target scope" };
|
|
195
|
+
}
|
|
196
|
+
if (repair === "config.set-validated" || repair === "mcp.set-startup-policy") {
|
|
197
|
+
if (setValue === undefined) return { error: "this action requires --set-value-json true|false" };
|
|
198
|
+
}
|
|
199
|
+
if (repair !== "config.set-validated" && repair !== "mcp.set-startup-policy" && setValue !== undefined)
|
|
200
|
+
return { error: "--set-value-json is only valid for config or MCP repairs" };
|
|
201
|
+
if (repair !== "install.restore-binary" && repair !== "plugin.restore-known-artifact" && sha256 !== undefined)
|
|
202
|
+
return { error: "--sha256 is only valid for binary or plugin restore" };
|
|
203
|
+
if (
|
|
204
|
+
repair !== "install.restore-binary" &&
|
|
205
|
+
repair !== "plugin.restore-known-artifact" &&
|
|
206
|
+
ref !== undefined &&
|
|
207
|
+
repair !== "install.repair-managed-link"
|
|
208
|
+
)
|
|
209
|
+
return { error: "--ref is not valid for this repair" };
|
|
210
|
+
if (repair === "install.repair-managed-link" && mode === "fix" && !ref)
|
|
211
|
+
return { error: "install.repair-managed-link requires --ref" };
|
|
212
|
+
if (
|
|
213
|
+
repair === "install.repair-managed-link" &&
|
|
214
|
+
ref !== undefined &&
|
|
215
|
+
!/^c1:link:r[0-9a-f]{64}:(source|wrapper|binary):p[0-9a-f]{64}$/.test(ref)
|
|
216
|
+
)
|
|
217
|
+
return { error: "--ref must be a c1 link candidate ID" };
|
|
218
|
+
return {
|
|
219
|
+
options: {
|
|
220
|
+
mode,
|
|
221
|
+
json,
|
|
222
|
+
help,
|
|
223
|
+
checks: [...checks],
|
|
224
|
+
scope,
|
|
225
|
+
timeoutMs,
|
|
226
|
+
repair,
|
|
227
|
+
targetId,
|
|
228
|
+
setValue,
|
|
229
|
+
ref,
|
|
230
|
+
sha256,
|
|
231
|
+
allowRisks: [...allowRisks],
|
|
232
|
+
yes,
|
|
233
|
+
drainSeconds,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|