@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/discovery/helpers.ts
CHANGED
|
@@ -15,7 +15,16 @@ import {
|
|
|
15
15
|
tryParseJson,
|
|
16
16
|
} from "@gajae-code/utils";
|
|
17
17
|
import type { ExtensionModule } from "../capability/extension-module";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
capturePathIdentity,
|
|
20
|
+
type FileIdentity,
|
|
21
|
+
invalidate as invalidateFsCache,
|
|
22
|
+
isSingleLinkRegularFileAt,
|
|
23
|
+
type ReadFileOptions,
|
|
24
|
+
type ReadScope,
|
|
25
|
+
readDirEntries,
|
|
26
|
+
readFile,
|
|
27
|
+
} from "../capability/fs";
|
|
19
28
|
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
20
29
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
21
30
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
@@ -52,7 +61,9 @@ export const SOURCE_PATHS = {
|
|
|
52
61
|
get userAgent() {
|
|
53
62
|
return `${getConfigDirName()}/agent`;
|
|
54
63
|
},
|
|
55
|
-
projectDir
|
|
64
|
+
get projectDir() {
|
|
65
|
+
return CONFIG_DIR_NAME;
|
|
66
|
+
},
|
|
56
67
|
},
|
|
57
68
|
claude: {
|
|
58
69
|
userBase: ".claude",
|
|
@@ -190,6 +201,35 @@ export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: stri
|
|
|
190
201
|
return path.join(ctx.cwd, paths.projectDir, subpath);
|
|
191
202
|
}
|
|
192
203
|
|
|
204
|
+
/** Build the filesystem authority for a provider read. */
|
|
205
|
+
export function getReadOptions(
|
|
206
|
+
ctx: Pick<LoadContext, "home" | "isolatedHome" | "userAgentDir" | "homeIdentity" | "userAgentIdentity">,
|
|
207
|
+
scope: ReadScope,
|
|
208
|
+
): ReadFileOptions | undefined {
|
|
209
|
+
if (!ctx.isolatedHome) return undefined;
|
|
210
|
+
return {
|
|
211
|
+
isolatedHome: true,
|
|
212
|
+
home: ctx.home,
|
|
213
|
+
homeIdentity: ctx.homeIdentity,
|
|
214
|
+
userAgentDir: scope === "native" ? ctx.userAgentDir : undefined,
|
|
215
|
+
userAgentIdentity: scope === "native" ? ctx.userAgentIdentity : undefined,
|
|
216
|
+
scope,
|
|
217
|
+
bypassCache: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function getReadOptionsForContainment(
|
|
222
|
+
ctx: Pick<LoadContext, "home" | "isolatedHome" | "userAgentDir" | "homeIdentity" | "userAgentIdentity">,
|
|
223
|
+
scope: ReadScope,
|
|
224
|
+
containmentRoot?: string,
|
|
225
|
+
): Promise<ReadFileOptions | undefined> {
|
|
226
|
+
const options = getReadOptions(ctx, scope);
|
|
227
|
+
if (!options || !containmentRoot) return options;
|
|
228
|
+
const containmentRootIdentity = await capturePathIdentity(containmentRoot);
|
|
229
|
+
if (!containmentRootIdentity) return undefined;
|
|
230
|
+
return { ...options, containmentRoot, containmentRootIdentity };
|
|
231
|
+
}
|
|
232
|
+
|
|
193
233
|
/**
|
|
194
234
|
* Create source metadata for an item.
|
|
195
235
|
*/
|
|
@@ -413,6 +453,13 @@ async function globIf(
|
|
|
413
453
|
}
|
|
414
454
|
}
|
|
415
455
|
|
|
456
|
+
async function isAllowedIsolatedExtensionPath(
|
|
457
|
+
ctx: Pick<LoadContext, "isolatedHome">,
|
|
458
|
+
filePath: string,
|
|
459
|
+
): Promise<boolean> {
|
|
460
|
+
return !ctx.isolatedHome || (await isSingleLinkRegularFileAt(filePath));
|
|
461
|
+
}
|
|
462
|
+
|
|
416
463
|
export interface ScanSkillsFromDirOptions {
|
|
417
464
|
dir: string;
|
|
418
465
|
/** Selected authority root whose own identity must not be a symlink. */
|
|
@@ -420,6 +467,12 @@ export interface ScanSkillsFromDirOptions {
|
|
|
420
467
|
providerId: string;
|
|
421
468
|
level: "user" | "project";
|
|
422
469
|
requireDescription?: boolean;
|
|
470
|
+
/** Optional physical root that every discovered skill must remain within. */
|
|
471
|
+
containmentRoot?: string;
|
|
472
|
+
/** Physical repository root permitting the project scan directory itself to be a symlink. */
|
|
473
|
+
linkContainmentRoot?: string;
|
|
474
|
+
/** Filesystem authority for explicit-home reads. */
|
|
475
|
+
scope?: ReadScope;
|
|
423
476
|
}
|
|
424
477
|
|
|
425
478
|
// Stable ordering used for skill lists in prompts: name (case-insensitive), then name, then path.
|
|
@@ -442,6 +495,7 @@ export const SkillDiscoveryTestHooks: {
|
|
|
442
495
|
afterAuthorityRootValidated?: (root: string) => void | Promise<void>;
|
|
443
496
|
afterAuthorityRootMissing?: (root: string) => void | Promise<void>;
|
|
444
497
|
afterContainedRootValidated?: (root: string) => void | Promise<void>;
|
|
498
|
+
afterScanRootValidated?: (root: string) => void | Promise<void>;
|
|
445
499
|
} = {};
|
|
446
500
|
|
|
447
501
|
interface DirectoryIdentity {
|
|
@@ -450,18 +504,62 @@ interface DirectoryIdentity {
|
|
|
450
504
|
realPath: string;
|
|
451
505
|
}
|
|
452
506
|
|
|
453
|
-
|
|
507
|
+
interface ScanRootLinkIdentity {
|
|
508
|
+
dev: bigint;
|
|
509
|
+
ino: bigint;
|
|
510
|
+
linkText: string;
|
|
511
|
+
target: DirectoryIdentity;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function captureScanRootLinkIdentity(root: string): Promise<ScanRootLinkIdentity | null> {
|
|
454
515
|
const observed = await fs.promises.lstat(root, { bigint: true });
|
|
455
|
-
if (observed.isSymbolicLink()
|
|
516
|
+
if (!observed.isSymbolicLink()) return null;
|
|
517
|
+
const linkText = await fs.promises.readlink(root);
|
|
518
|
+
let targetPath: string;
|
|
519
|
+
try {
|
|
520
|
+
targetPath = await fs.promises.realpath(root);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
523
|
+
throw new Error(`Dangling skills directory link: ${root}`);
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
const target = await captureDirectoryIdentity(targetPath);
|
|
527
|
+
if (!target) return null;
|
|
528
|
+
return { dev: observed.dev, ino: observed.ino, linkText, target };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function scanRootLinkIdentityIsCurrent(root: string, expected: ScanRootLinkIdentity): Promise<boolean> {
|
|
532
|
+
try {
|
|
533
|
+
const observed = await fs.promises.lstat(root, { bigint: true });
|
|
534
|
+
if (!observed.isSymbolicLink() || observed.dev !== expected.dev || observed.ino !== expected.ino) return false;
|
|
535
|
+
const linkText = await fs.promises.readlink(root);
|
|
536
|
+
if (linkText !== expected.linkText) return false;
|
|
537
|
+
const targetPath = await fs.promises.realpath(root);
|
|
538
|
+
if (normalizePathForComparison(targetPath) !== normalizePathForComparison(expected.target.realPath)) return false;
|
|
539
|
+
return await directoryIdentityIsCurrent(targetPath, expected.target);
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function captureDirectoryIdentity(root: string, allowSymlink = false): Promise<DirectoryIdentity | null> {
|
|
546
|
+
const observed = allowSymlink
|
|
547
|
+
? await fs.promises.stat(root, { bigint: true })
|
|
548
|
+
: await fs.promises.lstat(root, { bigint: true });
|
|
549
|
+
if ((!allowSymlink && observed.isSymbolicLink()) || !observed.isDirectory()) return null;
|
|
456
550
|
const realPath = await fs.promises.realpath(root);
|
|
457
551
|
const resolved = await fs.promises.stat(realPath, { bigint: true });
|
|
458
552
|
if (!resolved.isDirectory() || observed.dev !== resolved.dev || observed.ino !== resolved.ino) return null;
|
|
459
|
-
return { dev:
|
|
553
|
+
return { dev: resolved.dev, ino: resolved.ino, realPath };
|
|
460
554
|
}
|
|
461
555
|
|
|
462
|
-
async function directoryIdentityIsCurrent(
|
|
556
|
+
async function directoryIdentityIsCurrent(
|
|
557
|
+
root: string,
|
|
558
|
+
expected: DirectoryIdentity,
|
|
559
|
+
allowSymlink = false,
|
|
560
|
+
): Promise<boolean> {
|
|
463
561
|
try {
|
|
464
|
-
const current = await captureDirectoryIdentity(root);
|
|
562
|
+
const current = await captureDirectoryIdentity(root, allowSymlink);
|
|
465
563
|
return (
|
|
466
564
|
current !== null &&
|
|
467
565
|
current.dev === expected.dev &&
|
|
@@ -479,12 +577,61 @@ export async function scanSkillsFromDir(
|
|
|
479
577
|
): Promise<LoadResult<Skill>> {
|
|
480
578
|
const items: Skill[] = [];
|
|
481
579
|
const warnings: string[] = [];
|
|
482
|
-
const { dir, level, providerId, requireDescription = false } = options;
|
|
580
|
+
const { dir, level, providerId, requireDescription = false, containmentRoot, linkContainmentRoot } = options;
|
|
581
|
+
const scope = options.scope ?? (level === "user" ? "user" : "project");
|
|
582
|
+
if (linkContainmentRoot && (level !== "project" || scope !== "project")) {
|
|
583
|
+
warnings.push(`Refusing link containment root outside project scope: ${linkContainmentRoot}`);
|
|
584
|
+
return { items, warnings };
|
|
585
|
+
}
|
|
586
|
+
const readOptions = await getReadOptionsForContainment(_ctx, scope, containmentRoot);
|
|
587
|
+
if (_ctx.isolatedHome && containmentRoot && !readOptions) return { items, warnings };
|
|
588
|
+
const canonicalScanDir = await canonicalizePathWithinHome(_ctx, dir, containmentRoot, scope);
|
|
589
|
+
if (!canonicalScanDir) return { items, warnings };
|
|
590
|
+
// Preserve the lexical project root so a replacement link cannot be hidden
|
|
591
|
+
// by isolated-home canonicalization.
|
|
592
|
+
const scanDir = linkContainmentRoot ? path.resolve(dir) : canonicalScanDir;
|
|
593
|
+
let repositoryIdentity: DirectoryIdentity | null = null;
|
|
594
|
+
let repositoryLexicalRoot: string | undefined;
|
|
595
|
+
if (linkContainmentRoot) {
|
|
596
|
+
try {
|
|
597
|
+
repositoryLexicalRoot = path.resolve(linkContainmentRoot);
|
|
598
|
+
const repositoryRealPath = await fs.promises.realpath(repositoryLexicalRoot);
|
|
599
|
+
repositoryIdentity = await captureDirectoryIdentity(repositoryRealPath);
|
|
600
|
+
if (!repositoryIdentity) {
|
|
601
|
+
warnings.push(`Refusing unsafe repository root: ${linkContainmentRoot}`);
|
|
602
|
+
return { items, warnings };
|
|
603
|
+
}
|
|
604
|
+
const lexicalRelative = path.relative(repositoryLexicalRoot, path.resolve(dir));
|
|
605
|
+
if (lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
606
|
+
warnings.push(`Refusing skills directory outside repository root: ${dir}`);
|
|
607
|
+
return { items, warnings };
|
|
608
|
+
}
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { items, warnings };
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
483
614
|
|
|
484
615
|
let entries: fs.Dirent[];
|
|
485
616
|
let realRoot: string;
|
|
486
617
|
let authorityIdentity: DirectoryIdentity | null = null;
|
|
487
|
-
let scanRootIdentity: DirectoryIdentity;
|
|
618
|
+
let scanRootIdentity: DirectoryIdentity | null = null;
|
|
619
|
+
let scanRootLinkIdentity: ScanRootLinkIdentity | null = null;
|
|
620
|
+
const scanRootIsCurrent = async (): Promise<boolean> => {
|
|
621
|
+
if (!repositoryIdentity || !linkContainmentRoot) {
|
|
622
|
+
return scanRootIdentity !== null && (await directoryIdentityIsCurrent(scanDir, scanRootIdentity));
|
|
623
|
+
}
|
|
624
|
+
if (
|
|
625
|
+
!repositoryLexicalRoot ||
|
|
626
|
+
normalizePathForComparison(await fs.promises.realpath(repositoryLexicalRoot)) !==
|
|
627
|
+
normalizePathForComparison(repositoryIdentity.realPath) ||
|
|
628
|
+
!(await directoryIdentityIsCurrent(repositoryIdentity.realPath, repositoryIdentity))
|
|
629
|
+
) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
if (scanRootLinkIdentity) return await scanRootLinkIdentityIsCurrent(scanDir, scanRootLinkIdentity);
|
|
633
|
+
return scanRootIdentity !== null && (await directoryIdentityIsCurrent(scanDir, scanRootIdentity));
|
|
634
|
+
};
|
|
488
635
|
try {
|
|
489
636
|
if (options.authorityRoot) {
|
|
490
637
|
try {
|
|
@@ -504,22 +651,36 @@ export async function scanSkillsFromDir(
|
|
|
504
651
|
return { items, warnings };
|
|
505
652
|
}
|
|
506
653
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
654
|
+
if (linkContainmentRoot) {
|
|
655
|
+
scanRootLinkIdentity = await captureScanRootLinkIdentity(scanDir);
|
|
656
|
+
if (!scanRootLinkIdentity) scanRootIdentity = await captureDirectoryIdentity(scanDir);
|
|
657
|
+
if (!scanRootLinkIdentity && !scanRootIdentity) {
|
|
658
|
+
warnings.push(`Refusing unsafe skills directory: ${dir}`);
|
|
659
|
+
return { items, warnings };
|
|
660
|
+
}
|
|
661
|
+
const capturedRealRoot = scanRootLinkIdentity?.target.realPath ?? scanRootIdentity!.realPath;
|
|
662
|
+
if (!isWithinOrEqual(repositoryIdentity!.realPath, capturedRealRoot)) {
|
|
663
|
+
warnings.push(`Refusing skills directory outside repository root: ${dir}`);
|
|
664
|
+
return { items, warnings };
|
|
665
|
+
}
|
|
666
|
+
realRoot = capturedRealRoot;
|
|
667
|
+
} else {
|
|
668
|
+
scanRootIdentity = await captureDirectoryIdentity(scanDir);
|
|
669
|
+
if (!scanRootIdentity) {
|
|
670
|
+
warnings.push(`Refusing unsafe skills directory: ${dir}`);
|
|
671
|
+
return { items, warnings };
|
|
672
|
+
}
|
|
673
|
+
realRoot = scanRootIdentity.realPath;
|
|
511
674
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (!(await directoryIdentityIsCurrent(dir, scanRootIdentity))) {
|
|
675
|
+
entries = await fs.promises.readdir(scanDir, { withFileTypes: true });
|
|
676
|
+
await SkillDiscoveryTestHooks.afterScanRootValidated?.(scanDir);
|
|
677
|
+
if (!(await scanRootIsCurrent())) {
|
|
516
678
|
warnings.push(`Refusing changed skills directory: ${dir}`);
|
|
517
679
|
return { items, warnings };
|
|
518
680
|
}
|
|
519
681
|
} catch (error) {
|
|
520
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
521
|
-
warnings.push(`Failed to read skills directory: ${
|
|
522
|
-
}
|
|
682
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
683
|
+
warnings.push(`Failed to read skills directory: ${scanDir} (${String(error)})`);
|
|
523
684
|
return { items, warnings };
|
|
524
685
|
}
|
|
525
686
|
const isWithinRoot = (candidate: string): boolean => {
|
|
@@ -528,7 +689,7 @@ export async function scanSkillsFromDir(
|
|
|
528
689
|
};
|
|
529
690
|
const openSkillFileSafely = async (skillPath: string): Promise<FileHandle> => {
|
|
530
691
|
if (
|
|
531
|
-
!(await
|
|
692
|
+
!(await scanRootIsCurrent()) ||
|
|
532
693
|
(options.authorityRoot &&
|
|
533
694
|
authorityIdentity &&
|
|
534
695
|
!(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)))
|
|
@@ -544,7 +705,7 @@ export async function scanSkillsFromDir(
|
|
|
544
705
|
handle.stat({ bigint: true }),
|
|
545
706
|
fs.promises.lstat(skillPath, { bigint: true }),
|
|
546
707
|
fs.promises.realpath(skillPath),
|
|
547
|
-
|
|
708
|
+
scanRootIsCurrent(),
|
|
548
709
|
options.authorityRoot && authorityIdentity
|
|
549
710
|
? directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)
|
|
550
711
|
: true,
|
|
@@ -571,7 +732,16 @@ export async function scanSkillsFromDir(
|
|
|
571
732
|
const readSkillContentSafely = async (skillPath: string): Promise<string> => {
|
|
572
733
|
const handle = await openSkillFileSafely(skillPath);
|
|
573
734
|
try {
|
|
574
|
-
|
|
735
|
+
const content = await handle.readFile({ encoding: "utf8" });
|
|
736
|
+
if (
|
|
737
|
+
!(await scanRootIsCurrent()) ||
|
|
738
|
+
(options.authorityRoot &&
|
|
739
|
+
authorityIdentity &&
|
|
740
|
+
!(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)))
|
|
741
|
+
) {
|
|
742
|
+
throw new Error(`Unsafe skill authority root for: ${skillPath}`);
|
|
743
|
+
}
|
|
744
|
+
return content;
|
|
575
745
|
} finally {
|
|
576
746
|
await handle.close();
|
|
577
747
|
}
|
|
@@ -665,7 +835,13 @@ export async function scanSkillsFromDir(
|
|
|
665
835
|
for (const entry of entries) {
|
|
666
836
|
if (entry.name.startsWith(".")) continue;
|
|
667
837
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
668
|
-
const skillPath =
|
|
838
|
+
const skillPath = await canonicalizePathWithinHome(
|
|
839
|
+
_ctx,
|
|
840
|
+
path.join(scanDir, entry.name, "SKILL.md"),
|
|
841
|
+
containmentRoot,
|
|
842
|
+
scope,
|
|
843
|
+
);
|
|
844
|
+
if (!skillPath) continue;
|
|
669
845
|
work.push(loadSkill(skillPath));
|
|
670
846
|
}
|
|
671
847
|
await Promise.all(work);
|
|
@@ -676,24 +852,33 @@ export async function scanSkillsFromDir(
|
|
|
676
852
|
return { items, warnings };
|
|
677
853
|
}
|
|
678
854
|
|
|
679
|
-
/** Read a regular
|
|
680
|
-
export async function readContainedFile(
|
|
855
|
+
/** Read a regular file that remains contained by its configured root. */
|
|
856
|
+
export async function readContainedFile(
|
|
857
|
+
root: string,
|
|
858
|
+
filePath: string,
|
|
859
|
+
expectedRootIdentity?: FileIdentity | null,
|
|
860
|
+
): Promise<string | null> {
|
|
861
|
+
if (expectedRootIdentity === null) return null;
|
|
681
862
|
let rootIdentity: DirectoryIdentity;
|
|
682
863
|
try {
|
|
683
|
-
const capturedRoot = await captureDirectoryIdentity(root);
|
|
864
|
+
const capturedRoot = await captureDirectoryIdentity(root, true);
|
|
684
865
|
if (!capturedRoot) return null;
|
|
685
866
|
rootIdentity = capturedRoot;
|
|
867
|
+
if (
|
|
868
|
+
expectedRootIdentity !== undefined &&
|
|
869
|
+
(rootIdentity.dev !== BigInt(expectedRootIdentity.dev) ||
|
|
870
|
+
rootIdentity.ino !== BigInt(expectedRootIdentity.ino))
|
|
871
|
+
)
|
|
872
|
+
return null;
|
|
686
873
|
await SkillDiscoveryTestHooks.afterContainedRootValidated?.(root);
|
|
687
|
-
if (!(await directoryIdentityIsCurrent(root, rootIdentity))) return null;
|
|
874
|
+
if (!(await directoryIdentityIsCurrent(root, rootIdentity, true))) return null;
|
|
688
875
|
} catch (error) {
|
|
689
876
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
690
877
|
throw error;
|
|
691
878
|
}
|
|
692
879
|
const relative = path.relative(root, filePath);
|
|
693
880
|
if (relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
|
694
|
-
const flags =
|
|
695
|
-
fs.constants.O_RDONLY |
|
|
696
|
-
(process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
881
|
+
const flags = fs.constants.O_RDONLY | (process.platform === "win32" ? 0 : (fs.constants.O_NONBLOCK ?? 0));
|
|
697
882
|
let handle: FileHandle;
|
|
698
883
|
try {
|
|
699
884
|
handle = await fs.promises.open(filePath, flags);
|
|
@@ -702,20 +887,19 @@ export async function readContainedFile(root: string, filePath: string): Promise
|
|
|
702
887
|
return null;
|
|
703
888
|
}
|
|
704
889
|
try {
|
|
705
|
-
const [opened,
|
|
890
|
+
const [opened, currentPath, currentTarget, currentRoot] = await Promise.all([
|
|
706
891
|
handle.stat({ bigint: true }),
|
|
707
|
-
fs.promises.lstat(filePath, { bigint: true }),
|
|
708
892
|
fs.promises.realpath(filePath),
|
|
709
|
-
|
|
893
|
+
fs.promises.stat(filePath, { bigint: true }),
|
|
894
|
+
directoryIdentityIsCurrent(root, rootIdentity, true),
|
|
710
895
|
]);
|
|
711
896
|
const currentRelative = path.relative(rootIdentity.realPath, currentPath);
|
|
712
897
|
if (
|
|
713
898
|
!opened.isFile() ||
|
|
714
899
|
opened.nlink !== 1n ||
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
opened.
|
|
718
|
-
opened.ino !== observed.ino ||
|
|
900
|
+
!currentTarget.isFile() ||
|
|
901
|
+
opened.dev !== currentTarget.dev ||
|
|
902
|
+
opened.ino !== currentTarget.ino ||
|
|
719
903
|
!currentRoot ||
|
|
720
904
|
currentRelative.startsWith("..") ||
|
|
721
905
|
path.isAbsolute(currentRelative)
|
|
@@ -723,7 +907,7 @@ export async function readContainedFile(root: string, filePath: string): Promise
|
|
|
723
907
|
return null;
|
|
724
908
|
}
|
|
725
909
|
const content = await handle.readFile({ encoding: "utf8" });
|
|
726
|
-
return (await directoryIdentityIsCurrent(root, rootIdentity)) ? content : null;
|
|
910
|
+
return (await directoryIdentityIsCurrent(root, rootIdentity, true)) ? content : null;
|
|
727
911
|
} finally {
|
|
728
912
|
await handle.close();
|
|
729
913
|
}
|
|
@@ -778,10 +962,19 @@ export async function loadFilesFromDir<T>(
|
|
|
778
962
|
transform: (name: string, content: string, path: string, source: SourceMeta) => T | null;
|
|
779
963
|
/** Whether to recurse into subdirectories (default: false) */
|
|
780
964
|
recursive?: boolean;
|
|
965
|
+
/** Optional physical root that every discovered file must remain within. */
|
|
966
|
+
containmentRoot?: string;
|
|
967
|
+
/** Filesystem authority for explicit-home reads. */
|
|
968
|
+
scope?: ReadScope;
|
|
781
969
|
},
|
|
782
970
|
): Promise<LoadResult<T>> {
|
|
783
971
|
const items: T[] = [];
|
|
784
972
|
const warnings: string[] = [];
|
|
973
|
+
const scope = options.scope ?? (level === "user" ? "user" : "project");
|
|
974
|
+
const readOptions = await getReadOptionsForContainment(_ctx, scope, options.containmentRoot);
|
|
975
|
+
if (_ctx.isolatedHome && options.containmentRoot && !readOptions) return { items, warnings };
|
|
976
|
+
const scanDir = await canonicalizePathWithinHome(_ctx, dir, options.containmentRoot, scope);
|
|
977
|
+
if (!scanDir) return { items, warnings };
|
|
785
978
|
// Build glob pattern based on extensions and recursion
|
|
786
979
|
const { extensions, recursive = false } = options;
|
|
787
980
|
|
|
@@ -799,7 +992,7 @@ export async function loadFilesFromDir<T>(
|
|
|
799
992
|
const { glob, FileType } = await discoveryNatives();
|
|
800
993
|
const result = await glob({
|
|
801
994
|
pattern,
|
|
802
|
-
path:
|
|
995
|
+
path: scanDir,
|
|
803
996
|
gitignore: true,
|
|
804
997
|
hidden: false,
|
|
805
998
|
fileType: FileType.File,
|
|
@@ -813,13 +1006,21 @@ export async function loadFilesFromDir<T>(
|
|
|
813
1006
|
// Read all matching files in parallel
|
|
814
1007
|
const fileResults = await Promise.all(
|
|
815
1008
|
matches.map(async match => {
|
|
816
|
-
const filePath =
|
|
817
|
-
|
|
1009
|
+
const filePath = await canonicalizePathWithinHome(
|
|
1010
|
+
_ctx,
|
|
1011
|
+
path.join(scanDir, match.path),
|
|
1012
|
+
options.containmentRoot,
|
|
1013
|
+
scope,
|
|
1014
|
+
);
|
|
1015
|
+
if (!filePath) return null;
|
|
1016
|
+
const content = await readFile(filePath, readOptions);
|
|
818
1017
|
return { filePath, content };
|
|
819
1018
|
}),
|
|
820
1019
|
);
|
|
821
1020
|
|
|
822
|
-
for (const
|
|
1021
|
+
for (const result of fileResults) {
|
|
1022
|
+
if (!result) continue;
|
|
1023
|
+
const { filePath, content } = result;
|
|
823
1024
|
if (content === null) {
|
|
824
1025
|
warnings.push(`Failed to read file: ${filePath}`);
|
|
825
1026
|
continue;
|
|
@@ -858,8 +1059,11 @@ interface ExtensionModuleManifest {
|
|
|
858
1059
|
async function readExtensionModuleManifest(
|
|
859
1060
|
_ctx: LoadContext,
|
|
860
1061
|
packageJsonPath: string,
|
|
1062
|
+
scope: ReadScope,
|
|
861
1063
|
): Promise<ExtensionModuleManifest | null> {
|
|
862
|
-
const
|
|
1064
|
+
const resolvedPackageJsonPath = await canonicalizePathWithinHome(_ctx, packageJsonPath, undefined, scope);
|
|
1065
|
+
if (!resolvedPackageJsonPath) return null;
|
|
1066
|
+
const content = await readFile(resolvedPackageJsonPath, getReadOptions(_ctx, scope));
|
|
863
1067
|
if (!content) return null;
|
|
864
1068
|
|
|
865
1069
|
const pkg = tryParseJson<{ gjc?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content);
|
|
@@ -881,47 +1085,66 @@ async function readExtensionModuleManifest(
|
|
|
881
1085
|
* No recursion beyond one level. Complex packages must use package.json manifest.
|
|
882
1086
|
* Uses native glob for fast filesystem scanning with gitignore support.
|
|
883
1087
|
*/
|
|
884
|
-
export async function discoverExtensionModulePaths(
|
|
1088
|
+
export async function discoverExtensionModulePaths(
|
|
1089
|
+
ctx: LoadContext,
|
|
1090
|
+
dir: string,
|
|
1091
|
+
options: { scope?: ReadScope } = {},
|
|
1092
|
+
): Promise<string[]> {
|
|
885
1093
|
const discovered = new Set<string>();
|
|
1094
|
+
const scope = options.scope ?? "project";
|
|
1095
|
+
const readOptions = getReadOptions(ctx, scope);
|
|
1096
|
+
const discoveryDir = await canonicalizePathWithinHome(ctx, dir, undefined, scope);
|
|
1097
|
+
if (!discoveryDir) return [];
|
|
886
1098
|
const { FileType } = await discoveryNatives();
|
|
887
1099
|
// Find all candidate files in parallel using glob
|
|
888
1100
|
const [directFiles, indexFiles, packageJsonFiles] = await Promise.all([
|
|
889
1101
|
// 1. Direct *.ts or *.js files
|
|
890
|
-
globIf(
|
|
1102
|
+
globIf(discoveryDir, "*.{ts,js}", FileType.File, false),
|
|
891
1103
|
// 2. Subdirectory index files
|
|
892
|
-
globIf(
|
|
1104
|
+
globIf(discoveryDir, "*/index.{ts,js}", FileType.File, false),
|
|
893
1105
|
// 3. Subdirectory package.json files
|
|
894
|
-
globIf(
|
|
1106
|
+
globIf(discoveryDir, "*/package.json", FileType.File, false),
|
|
895
1107
|
]);
|
|
896
1108
|
|
|
897
1109
|
// Process direct files
|
|
898
1110
|
for (const match of directFiles) {
|
|
899
1111
|
if (match.path.includes("/")) continue;
|
|
900
|
-
|
|
1112
|
+
const candidatePath = await canonicalizePathWithinHome(
|
|
1113
|
+
ctx,
|
|
1114
|
+
path.join(discoveryDir, match.path),
|
|
1115
|
+
undefined,
|
|
1116
|
+
scope,
|
|
1117
|
+
);
|
|
1118
|
+
if (candidatePath && (await isAllowedIsolatedExtensionPath(ctx, candidatePath))) discovered.add(candidatePath);
|
|
901
1119
|
}
|
|
902
1120
|
// Track which subdirectories have package.json manifests with declared extensions
|
|
903
1121
|
const subdirsWithDeclaredExtensions = new Set<string>();
|
|
904
1122
|
for (const match of packageJsonFiles) {
|
|
905
1123
|
const subdir = path.dirname(match.path); // e.g., "my-extension"
|
|
906
|
-
const packageJsonPath = path.join(
|
|
907
|
-
const manifest = await readExtensionModuleManifest(
|
|
1124
|
+
const packageJsonPath = path.join(discoveryDir, match.path);
|
|
1125
|
+
const manifest = await readExtensionModuleManifest(ctx, packageJsonPath, scope);
|
|
908
1126
|
const declaredExtensions =
|
|
909
1127
|
manifest?.extensions?.filter((extPath): extPath is string => typeof extPath === "string") ?? [];
|
|
910
1128
|
if (declaredExtensions.length === 0) continue;
|
|
911
1129
|
subdirsWithDeclaredExtensions.add(subdir);
|
|
912
|
-
const subdirPath = path.join(
|
|
1130
|
+
const subdirPath = path.join(discoveryDir, subdir);
|
|
913
1131
|
for (const extPath of declaredExtensions) {
|
|
914
|
-
|
|
915
|
-
const
|
|
1132
|
+
const configuredPath = path.resolve(subdirPath, extPath);
|
|
1133
|
+
const resolvedConfiguredPath = await canonicalizePathWithinHome(ctx, configuredPath, undefined, scope);
|
|
1134
|
+
if (!resolvedConfiguredPath) continue;
|
|
1135
|
+
let resolvedExtPath = resolvedConfiguredPath;
|
|
1136
|
+
const entries = await readDirEntries(resolvedExtPath, readOptions);
|
|
916
1137
|
if (entries.length !== 0) {
|
|
917
1138
|
const pluginFilePath = entries.find(
|
|
918
1139
|
e => e.isFile() && (e.name === "index.ts" || e.name === "index.js"),
|
|
919
1140
|
)?.name;
|
|
920
1141
|
resolvedExtPath = pluginFilePath ? path.join(resolvedExtPath, pluginFilePath) : resolvedExtPath;
|
|
921
1142
|
}
|
|
922
|
-
const
|
|
1143
|
+
const canonicalExtPath = await canonicalizePathWithinHome(ctx, resolvedExtPath, undefined, scope);
|
|
1144
|
+
if (!canonicalExtPath || !(await isAllowedIsolatedExtensionPath(ctx, canonicalExtPath))) continue;
|
|
1145
|
+
const content = await readFile(canonicalExtPath, readOptions);
|
|
923
1146
|
if (content !== null) {
|
|
924
|
-
discovered.add(
|
|
1147
|
+
discovered.add(canonicalExtPath);
|
|
925
1148
|
}
|
|
926
1149
|
}
|
|
927
1150
|
}
|
|
@@ -936,7 +1159,13 @@ export async function discoverExtensionModulePaths(_ctx: LoadContext, dir: strin
|
|
|
936
1159
|
}
|
|
937
1160
|
}
|
|
938
1161
|
for (const preferredPath of preferredIndexBySubdir.values()) {
|
|
939
|
-
|
|
1162
|
+
const candidatePath = await canonicalizePathWithinHome(
|
|
1163
|
+
ctx,
|
|
1164
|
+
path.join(discoveryDir, preferredPath),
|
|
1165
|
+
undefined,
|
|
1166
|
+
scope,
|
|
1167
|
+
);
|
|
1168
|
+
if (candidatePath && (await isAllowedIsolatedExtensionPath(ctx, candidatePath))) discovered.add(candidatePath);
|
|
940
1169
|
}
|
|
941
1170
|
return [...discovered];
|
|
942
1171
|
}
|
|
@@ -1058,34 +1287,62 @@ export function parseClaudePluginsRegistry(content: string): ClaudePluginsRegist
|
|
|
1058
1287
|
* This is the single source of truth for "active project root" used by install,
|
|
1059
1288
|
* uninstall, list, upgrade, discovery, and doctor. Deterministic for a given `cwd`.
|
|
1060
1289
|
*/
|
|
1061
|
-
export async function resolveActiveProjectRegistryPath(
|
|
1290
|
+
export async function resolveActiveProjectRegistryPath(
|
|
1291
|
+
cwd: string,
|
|
1292
|
+
homeDir?: string,
|
|
1293
|
+
isolatedHome = false,
|
|
1294
|
+
): Promise<string | null> {
|
|
1062
1295
|
// Pass 1: walk up looking for an existing .gjc/ directory (nearest wins).
|
|
1063
|
-
// Stop before the
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1296
|
+
// Stop before the caller's authoritative home — its .gjc/ is the user-level
|
|
1297
|
+
// config dir, not a project root. Explicit-home capability loading passes
|
|
1298
|
+
// that supplied home instead of leaking the process-global trusted home.
|
|
1299
|
+
const canonicalize = async (value: string): Promise<string> => {
|
|
1067
1300
|
try {
|
|
1068
|
-
|
|
1301
|
+
return await fs.promises.realpath(value);
|
|
1302
|
+
} catch {
|
|
1303
|
+
return path.resolve(value);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
const explicitHome = homeDir !== undefined;
|
|
1307
|
+
const trustedHome = isolatedHome ? null : await canonicalize(getTrustedHomeDir()).catch(() => null);
|
|
1308
|
+
homeDir = await canonicalize(homeDir ?? getTrustedHomeDir());
|
|
1309
|
+
const canonicalCwd = await canonicalize(cwd);
|
|
1310
|
+
const relativeHome = path.relative(homeDir, canonicalCwd);
|
|
1311
|
+
const explicitBoundary = isolatedHome || (explicitHome && homeDir !== trustedHome);
|
|
1312
|
+
const effectiveStop =
|
|
1313
|
+
!explicitBoundary ||
|
|
1314
|
+
relativeHome === "" ||
|
|
1315
|
+
(relativeHome !== ".." && !relativeHome.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeHome))
|
|
1316
|
+
? homeDir
|
|
1317
|
+
: canonicalCwd;
|
|
1318
|
+
let dir = canonicalCwd;
|
|
1319
|
+
while (true) {
|
|
1320
|
+
if (dir === effectiveStop && effectiveStop === homeDir) break;
|
|
1321
|
+
try {
|
|
1322
|
+
const stat = await fs.promises.stat(path.join(dir, CONFIG_DIR_NAME));
|
|
1069
1323
|
if (stat.isDirectory()) {
|
|
1070
|
-
return path.join(dir,
|
|
1324
|
+
return path.join(dir, CONFIG_DIR_NAME, "plugins", "installed_plugins.json");
|
|
1071
1325
|
}
|
|
1072
1326
|
} catch {
|
|
1073
1327
|
// not found at this level — continue up
|
|
1074
1328
|
}
|
|
1329
|
+
if (dir === effectiveStop) break;
|
|
1075
1330
|
const parent = path.dirname(dir);
|
|
1076
1331
|
if (parent === dir) break; // filesystem root
|
|
1077
1332
|
dir = parent;
|
|
1078
1333
|
}
|
|
1079
1334
|
|
|
1080
1335
|
// Pass 2: walk up looking for .git as a fallback anchor.
|
|
1081
|
-
dir =
|
|
1082
|
-
while (
|
|
1336
|
+
dir = canonicalCwd;
|
|
1337
|
+
while (true) {
|
|
1338
|
+
if (dir === effectiveStop && effectiveStop === homeDir) break;
|
|
1083
1339
|
try {
|
|
1084
1340
|
await fs.promises.stat(path.join(dir, ".git"));
|
|
1085
|
-
return path.join(dir,
|
|
1341
|
+
return path.join(dir, CONFIG_DIR_NAME, "plugins", "installed_plugins.json");
|
|
1086
1342
|
} catch {
|
|
1087
1343
|
// not found at this level — continue up
|
|
1088
1344
|
}
|
|
1345
|
+
if (dir === effectiveStop) break;
|
|
1089
1346
|
const parent = path.dirname(dir);
|
|
1090
1347
|
if (parent === dir) break; // filesystem root
|
|
1091
1348
|
dir = parent;
|
|
@@ -1111,46 +1368,154 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
|
|
|
1111
1368
|
// Home directory must not be treated as a project root: the fallback path would alias
|
|
1112
1369
|
// getInstalledPluginsRegistryPath(), causing MarketplaceManager to load the same file
|
|
1113
1370
|
// as both user and project registry and producing duplicates / disambiguation errors.
|
|
1114
|
-
|
|
1115
|
-
|
|
1371
|
+
const [canonicalCwd, canonicalHome] = await Promise.all([
|
|
1372
|
+
fs.promises.realpath(cwd).catch(() => path.resolve(cwd)),
|
|
1373
|
+
fs.promises.realpath(getTrustedHomeDir()).catch(() => path.resolve(getTrustedHomeDir())),
|
|
1374
|
+
]);
|
|
1375
|
+
if (canonicalCwd === canonicalHome) return undefined;
|
|
1376
|
+
return path.join(cwd, CONFIG_DIR_NAME, "plugins", "installed_plugins.json");
|
|
1116
1377
|
}
|
|
1117
1378
|
|
|
1118
1379
|
const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
|
|
1119
1380
|
|
|
1120
1381
|
/** Invalidate the exact user/project registry pair and their resolved-root cache entry. */
|
|
1121
|
-
export async function invalidateClaudePluginRoots(home: string, cwd?: string): Promise<void> {
|
|
1122
|
-
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1382
|
+
export async function invalidateClaudePluginRoots(home: string, cwd?: string, isolatedHome = false): Promise<void> {
|
|
1383
|
+
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd, home, isolatedHome) : null;
|
|
1384
|
+
const canonicalHome = await canonicalizeThroughExistingAncestor(home);
|
|
1385
|
+
const rawGjcRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
|
|
1386
|
+
const gjcRegistryPath = await canonicalizePluginRegistryPath(canonicalHome, rawGjcRegistryPath, isolatedHome);
|
|
1387
|
+
const projectRegistryPath = resolvedProjectPath
|
|
1388
|
+
? await canonicalizePluginRegistryPath(canonicalHome, resolvedProjectPath, isolatedHome)
|
|
1389
|
+
: undefined;
|
|
1390
|
+
for (const registryPath of new Set([
|
|
1391
|
+
rawGjcRegistryPath,
|
|
1392
|
+
gjcRegistryPath,
|
|
1393
|
+
resolvedProjectPath,
|
|
1394
|
+
projectRegistryPath,
|
|
1395
|
+
])) {
|
|
1396
|
+
if (registryPath) invalidateFsCache(registryPath);
|
|
1397
|
+
}
|
|
1398
|
+
pluginRootsCache.delete(`${canonicalHome}:${gjcRegistryPath ?? ""}:${projectRegistryPath ?? ""}`);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function canonicalizeThroughExistingAncestor(target: string): Promise<string> {
|
|
1402
|
+
const resolved = path.resolve(target);
|
|
1403
|
+
const suffix: string[] = [];
|
|
1404
|
+
let current = resolved;
|
|
1405
|
+
|
|
1406
|
+
while (true) {
|
|
1407
|
+
try {
|
|
1408
|
+
const real = await fs.promises.realpath(current);
|
|
1409
|
+
return suffix.length > 0 ? path.join(real, ...suffix.reverse()) : real;
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1412
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
1413
|
+
const parent = path.dirname(current);
|
|
1414
|
+
if (parent === current) return resolved;
|
|
1415
|
+
suffix.push(path.basename(current));
|
|
1416
|
+
current = parent;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function isWithinOrEqual(root: string, candidate: string): boolean {
|
|
1422
|
+
const relative = path.relative(root, candidate);
|
|
1423
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
async function canonicalizePluginRegistryPath(
|
|
1427
|
+
home: string,
|
|
1428
|
+
registryPath: string,
|
|
1429
|
+
isolatedHome: boolean,
|
|
1430
|
+
): Promise<string | undefined> {
|
|
1431
|
+
const canonical = await canonicalizeThroughExistingAncestor(registryPath);
|
|
1432
|
+
if (isolatedHome) {
|
|
1433
|
+
const canonicalHome = await canonicalizeThroughExistingAncestor(home);
|
|
1434
|
+
if (!isWithinOrEqual(canonicalHome, canonical)) return undefined;
|
|
1435
|
+
}
|
|
1436
|
+
return canonical;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Resolve a configured path through existing symlinks when an explicit-home
|
|
1441
|
+
* load is active. Ordinary discovery keeps its historical lexical path
|
|
1442
|
+
* handling; isolated discovery must reject paths that leave the supplied
|
|
1443
|
+
* physical home (or an explicitly selected agent directory), including paths
|
|
1444
|
+
* that escape through an existing symlink. When provided, containmentRoot is
|
|
1445
|
+
* an additional physical boundary, such as a plugin root.
|
|
1446
|
+
*/
|
|
1447
|
+
export async function canonicalizePathWithinHome(
|
|
1448
|
+
ctx: Pick<LoadContext, "home" | "isolatedHome" | "userAgentDir" | "homeIdentity">,
|
|
1449
|
+
target: string,
|
|
1450
|
+
containmentRoot?: string,
|
|
1451
|
+
scope: ReadScope = "project",
|
|
1452
|
+
): Promise<string | undefined> {
|
|
1453
|
+
if (!ctx.isolatedHome) return target;
|
|
1454
|
+
const roots = scope === "native" ? [ctx.home, ctx.userAgentDir] : [ctx.home];
|
|
1455
|
+
const canonicalRoots = await Promise.all(
|
|
1456
|
+
roots.filter((root): root is string => typeof root === "string").map(canonicalizeThroughExistingAncestor),
|
|
1457
|
+
);
|
|
1458
|
+
const canonicalTarget = await canonicalizeThroughExistingAncestor(target);
|
|
1459
|
+
if (!canonicalRoots.some(root => isWithinOrEqual(root, canonicalTarget))) return undefined;
|
|
1460
|
+
if (containmentRoot) {
|
|
1461
|
+
const canonicalContainmentRoot = await canonicalizeThroughExistingAncestor(containmentRoot);
|
|
1462
|
+
if (!isWithinOrEqual(canonicalContainmentRoot, canonicalTarget)) return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
return canonicalTarget;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async function resolveIsolatedPluginPath(home: string, value: string): Promise<string | undefined> {
|
|
1468
|
+
const canonicalHome = await canonicalizeThroughExistingAncestor(home);
|
|
1469
|
+
const resolved = path.resolve(canonicalHome, value);
|
|
1470
|
+
const canonical = await canonicalizeThroughExistingAncestor(resolved);
|
|
1471
|
+
if (!isWithinOrEqual(canonicalHome, canonical)) return undefined;
|
|
1472
|
+
return canonical;
|
|
1126
1473
|
}
|
|
1127
1474
|
|
|
1128
1475
|
/**
|
|
1129
1476
|
* List installed GJC plugin roots from the GJC plugin registry and, when present,
|
|
1130
1477
|
* the nearest project-scoped registry resolved from `cwd`.
|
|
1131
1478
|
*
|
|
1132
|
-
*
|
|
1479
|
+
* Ordinary results are cached per `home:resolvedProjectPath` key to avoid
|
|
1480
|
+
* repeated parsing. Isolated results intentionally bypass the shared cache:
|
|
1481
|
+
* an ordinary load may contain external install roots that an isolated load
|
|
1482
|
+
* must reject, and reusing that result would cross the home boundary.
|
|
1133
1483
|
*/
|
|
1134
1484
|
|
|
1135
1485
|
export async function listClaudePluginRoots(
|
|
1136
1486
|
home: string,
|
|
1137
1487
|
cwd?: string,
|
|
1488
|
+
isolatedHome = false,
|
|
1489
|
+
homeIdentity?: FileIdentity,
|
|
1138
1490
|
): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
|
|
1139
|
-
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1491
|
+
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd, home, isolatedHome) : null;
|
|
1492
|
+
const canonicalHome = await canonicalizeThroughExistingAncestor(home);
|
|
1493
|
+
const rawGjcRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
|
|
1494
|
+
const gjcRegistryPath = await canonicalizePluginRegistryPath(canonicalHome, rawGjcRegistryPath, isolatedHome);
|
|
1495
|
+
const projectRegistryPath = resolvedProjectPath
|
|
1496
|
+
? await canonicalizePluginRegistryPath(canonicalHome, resolvedProjectPath, isolatedHome)
|
|
1497
|
+
: undefined;
|
|
1498
|
+
const cacheKey = `${canonicalHome}:${gjcRegistryPath ?? ""}:${projectRegistryPath ?? ""}`;
|
|
1499
|
+
if (!isolatedHome) {
|
|
1500
|
+
const cached = pluginRootsCache.get(cacheKey);
|
|
1501
|
+
if (cached) return cached;
|
|
1502
|
+
}
|
|
1143
1503
|
|
|
1144
1504
|
const roots: ClaudePluginRoot[] = [];
|
|
1145
1505
|
const warnings: string[] = [];
|
|
1146
1506
|
const projectRoots: ClaudePluginRoot[] = [];
|
|
1507
|
+
const registryReadOptions: ReadFileOptions | undefined = isolatedHome
|
|
1508
|
+
? { isolatedHome: true, home: canonicalHome, homeIdentity, scope: "project", bypassCache: true }
|
|
1509
|
+
: undefined;
|
|
1147
1510
|
|
|
1148
1511
|
// ── GJC installed plugins registry ───────────────────────────────────────
|
|
1149
1512
|
// In production `home` is the provenance-checked home, so `getPluginsDir(home)` resolves to the
|
|
1150
1513
|
// same XDG-aware path the marketplace writer uses (reads and writes always agree).
|
|
1151
1514
|
// Tests pass a temp dir, which short-circuits the resolver for deterministic isolation.
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1515
|
+
const gjcContent = gjcRegistryPath ? await readFile(gjcRegistryPath, registryReadOptions) : null;
|
|
1516
|
+
if (isolatedHome && !gjcRegistryPath) {
|
|
1517
|
+
warnings.push(`Ignoring GJC plugin registry outside the isolated home: ${rawGjcRegistryPath}`);
|
|
1518
|
+
}
|
|
1154
1519
|
if (gjcContent) {
|
|
1155
1520
|
const gjcRegistry = parseClaudePluginsRegistry(gjcContent);
|
|
1156
1521
|
if (gjcRegistry) {
|
|
@@ -1166,20 +1531,33 @@ export async function listClaudePluginRoots(
|
|
|
1166
1531
|
const marketplace = pluginId.slice(atIndex + 1);
|
|
1167
1532
|
|
|
1168
1533
|
for (const entry of entries) {
|
|
1169
|
-
if (
|
|
1534
|
+
if (
|
|
1535
|
+
!entry ||
|
|
1536
|
+
typeof entry !== "object" ||
|
|
1537
|
+
Array.isArray(entry) ||
|
|
1538
|
+
!entry.installPath ||
|
|
1539
|
+
typeof entry.installPath !== "string"
|
|
1540
|
+
) {
|
|
1170
1541
|
warnings.push(`Plugin ${pluginId} entry has no installPath`);
|
|
1171
1542
|
continue;
|
|
1172
1543
|
}
|
|
1173
1544
|
if (entry.enabled === false) continue;
|
|
1545
|
+
const installPath = isolatedHome
|
|
1546
|
+
? await resolveIsolatedPluginPath(home, entry.installPath)
|
|
1547
|
+
: entry.installPath;
|
|
1548
|
+
if (!installPath) {
|
|
1549
|
+
warnings.push(`Plugin ${pluginId} installPath escapes the isolated home`);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1174
1552
|
// Deduplicate by installPath within same ID
|
|
1175
|
-
if (roots.some(r => r.id === pluginId && r.path ===
|
|
1553
|
+
if (roots.some(r => r.id === pluginId && r.path === installPath)) continue;
|
|
1176
1554
|
|
|
1177
1555
|
roots.push({
|
|
1178
1556
|
id: pluginId,
|
|
1179
1557
|
marketplace,
|
|
1180
1558
|
plugin: pluginName,
|
|
1181
1559
|
version: entry.version || "unknown",
|
|
1182
|
-
path:
|
|
1560
|
+
path: installPath,
|
|
1183
1561
|
scope: entry.scope || "user",
|
|
1184
1562
|
});
|
|
1185
1563
|
}
|
|
@@ -1193,7 +1571,10 @@ export async function listClaudePluginRoots(
|
|
|
1193
1571
|
// Loaded from the nearest .gjc/plugins/installed_plugins.json relative to cwd.
|
|
1194
1572
|
// Project entries take precedence over user entries for the same plugin ID.
|
|
1195
1573
|
if (resolvedProjectPath) {
|
|
1196
|
-
const projectContent = await readFile(
|
|
1574
|
+
const projectContent = projectRegistryPath ? await readFile(projectRegistryPath, registryReadOptions) : null;
|
|
1575
|
+
if (isolatedHome && !projectRegistryPath) {
|
|
1576
|
+
warnings.push(`Ignoring project plugin registry outside the isolated home: ${resolvedProjectPath}`);
|
|
1577
|
+
}
|
|
1197
1578
|
if (projectContent) {
|
|
1198
1579
|
const projectRegistry = parseClaudePluginsRegistry(projectContent);
|
|
1199
1580
|
if (projectRegistry) {
|
|
@@ -1207,23 +1588,36 @@ export async function listClaudePluginRoots(
|
|
|
1207
1588
|
const pluginName = pluginId.slice(0, atIndex);
|
|
1208
1589
|
const marketplace = pluginId.slice(atIndex + 1);
|
|
1209
1590
|
for (const entry of entries) {
|
|
1210
|
-
if (
|
|
1591
|
+
if (
|
|
1592
|
+
!entry ||
|
|
1593
|
+
typeof entry !== "object" ||
|
|
1594
|
+
Array.isArray(entry) ||
|
|
1595
|
+
!entry.installPath ||
|
|
1596
|
+
typeof entry.installPath !== "string"
|
|
1597
|
+
) {
|
|
1211
1598
|
warnings.push(`Plugin ${pluginId} entry has no installPath`);
|
|
1212
1599
|
continue;
|
|
1213
1600
|
}
|
|
1214
1601
|
if (entry.enabled === false) continue;
|
|
1602
|
+
const installPath = isolatedHome
|
|
1603
|
+
? await resolveIsolatedPluginPath(home, entry.installPath)
|
|
1604
|
+
: entry.installPath;
|
|
1605
|
+
if (!installPath) {
|
|
1606
|
+
warnings.push(`Plugin ${pluginId} installPath escapes the isolated home`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1215
1609
|
projectRoots.push({
|
|
1216
1610
|
id: pluginId,
|
|
1217
1611
|
marketplace,
|
|
1218
1612
|
plugin: pluginName,
|
|
1219
1613
|
version: entry.version || "unknown",
|
|
1220
|
-
path:
|
|
1614
|
+
path: installPath,
|
|
1221
1615
|
scope: "project",
|
|
1222
1616
|
});
|
|
1223
1617
|
}
|
|
1224
1618
|
}
|
|
1225
1619
|
} else {
|
|
1226
|
-
warnings.push(`Failed to parse project plugin registry: ${
|
|
1620
|
+
warnings.push(`Failed to parse project plugin registry: ${projectRegistryPath}`);
|
|
1227
1621
|
}
|
|
1228
1622
|
}
|
|
1229
1623
|
}
|
|
@@ -1237,7 +1631,7 @@ export async function listClaudePluginRoots(
|
|
|
1237
1631
|
}
|
|
1238
1632
|
|
|
1239
1633
|
const result = { roots, warnings };
|
|
1240
|
-
pluginRootsCache.set(cacheKey, result);
|
|
1634
|
+
if (!isolatedHome) pluginRootsCache.set(cacheKey, result);
|
|
1241
1635
|
return result;
|
|
1242
1636
|
}
|
|
1243
1637
|
|