@gajae-code/coding-agent 0.16.1 → 0.16.4
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 +70 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -2
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +94 -6
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/acp/adapter.ts +1 -1
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +47 -23
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +36 -25
- package/src/sdk/host/session-runtime.ts +279 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -30
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +50 -21
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -2
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +9 -2
- package/src/utils/external-editor.ts +16 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -7,7 +7,12 @@ import { Effort } from '@gajae-code/ai/model-thinking';
|
|
|
7
7
|
import type { OAuthCredentials, OAuthLoginCallbacks } from '@gajae-code/ai/utils/oauth/types';
|
|
8
8
|
import { loginXai, refreshXaiToken, XAI_OAUTH_SCOPE } from '@gajae-code/ai/utils/oauth/xai';
|
|
9
9
|
import type { ExtensionAPI, ProviderConfig } from '@gajae-code/coding-agent';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type GrokCliModelConfig,
|
|
12
|
+
getMaxReasoningEffort,
|
|
13
|
+
resolveModels,
|
|
14
|
+
supportsReasoningEffort,
|
|
15
|
+
} from '../models/catalog.js';
|
|
11
16
|
import { sanitizePayload } from '../payload/sanitize.js';
|
|
12
17
|
import { getBaseUrl, isGrokBuildBaseUrlOverrideIgnored } from '../shared/base-url.js';
|
|
13
18
|
import { streamGrokCli } from './stream.js';
|
|
@@ -38,7 +43,7 @@ export default function registerGrokCli(api: ExtensionAPI) {
|
|
|
38
43
|
thinking: m.reasoning
|
|
39
44
|
? {
|
|
40
45
|
minLevel: Effort.Low,
|
|
41
|
-
maxLevel: m.
|
|
46
|
+
maxLevel: getMaxReasoningEffort(m.id) ?? Effort.XHigh,
|
|
42
47
|
mode: 'effort',
|
|
43
48
|
}
|
|
44
49
|
: undefined,
|
|
@@ -46,6 +51,12 @@ export default function registerGrokCli(api: ExtensionAPI) {
|
|
|
46
51
|
cost: m.cost,
|
|
47
52
|
contextWindow: m.contextWindow,
|
|
48
53
|
maxTokens: m.maxTokens,
|
|
54
|
+
// cli-chat-proxy is not a generic audited OpenAI origin. Carry only the
|
|
55
|
+
// catalog's model-scoped evidence into core reasoning request gating.
|
|
56
|
+
compat:
|
|
57
|
+
m.reasoning && supportsReasoningEffort(m.id)
|
|
58
|
+
? { supportsReasoningEffort: true }
|
|
59
|
+
: undefined,
|
|
49
60
|
})),
|
|
50
61
|
oauth: {
|
|
51
62
|
name: 'Grok Build',
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -321,7 +321,9 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
|
|
|
321
321
|
// Skills
|
|
322
322
|
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
323
323
|
// Walk up from cwd finding .gjc/skills/ in ancestors (closest first)
|
|
324
|
-
const ancestors = getAncestorDirs(ctx.cwd, ctx.repoRoot ?? ctx.home)
|
|
324
|
+
const ancestors = getAncestorDirs(ctx.cwd, ctx.repoRoot ?? ctx.home).filter(
|
|
325
|
+
({ dir }) => path.resolve(dir) !== path.resolve(ctx.home),
|
|
326
|
+
);
|
|
325
327
|
const projectScans = ancestors.flatMap(({ dir }) =>
|
|
326
328
|
getProjectConfigDirs().map(projectConfigDir =>
|
|
327
329
|
scanSkillsFromDir(ctx, {
|
|
@@ -334,14 +336,21 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
334
336
|
);
|
|
335
337
|
|
|
336
338
|
// User-level scan from the active agent-directory profile.
|
|
337
|
-
const
|
|
339
|
+
const userSkillDirs = [
|
|
340
|
+
...new Set([
|
|
341
|
+
path.join(resolveUserAgentDir(ctx), "skills"),
|
|
342
|
+
path.join(ctx.home, PATHS.userBase, "skills"),
|
|
343
|
+
path.join(ctx.home, ".gjc", "skills"),
|
|
344
|
+
]),
|
|
345
|
+
];
|
|
346
|
+
const userScans = userSkillDirs.map(dir =>
|
|
338
347
|
scanSkillsFromDir(ctx, {
|
|
339
|
-
dir
|
|
348
|
+
dir,
|
|
340
349
|
providerId: PROVIDER_ID,
|
|
341
350
|
level: "user",
|
|
342
351
|
requireDescription: true,
|
|
343
352
|
}),
|
|
344
|
-
|
|
353
|
+
);
|
|
345
354
|
|
|
346
355
|
const results = await Promise.all([...projectScans, ...userScans]);
|
|
347
356
|
|
|
@@ -17,6 +17,7 @@ import { rootContainsGjcManifest } from "../extensibility/gjc-plugins";
|
|
|
17
17
|
import {
|
|
18
18
|
type ClaudePluginRoot,
|
|
19
19
|
createSourceMeta,
|
|
20
|
+
invalidateClaudePluginRoots,
|
|
20
21
|
listClaudePluginRoots,
|
|
21
22
|
loadFilesFromDir,
|
|
22
23
|
scanSkillsFromDir,
|
|
@@ -95,12 +96,15 @@ async function resolvePluginDir(
|
|
|
95
96
|
async function listNonGjcPluginRoots(
|
|
96
97
|
home: string,
|
|
97
98
|
cwd: string,
|
|
99
|
+
allowedLevels: ReadonlySet<"user" | "project"> = new Set(["user", "project"]),
|
|
98
100
|
): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
|
|
101
|
+
await invalidateClaudePluginRoots(home, cwd);
|
|
99
102
|
const { roots, warnings } = await listClaudePluginRoots(home, cwd);
|
|
100
103
|
const filteredRoots: ClaudePluginRoot[] = [];
|
|
101
|
-
const filteredWarnings = [...warnings];
|
|
104
|
+
const filteredWarnings = allowedLevels.size === 2 ? [...warnings] : [];
|
|
102
105
|
|
|
103
106
|
for (const root of roots) {
|
|
107
|
+
if (!allowedLevels.has(root.scope)) continue;
|
|
104
108
|
if (await rootContainsGjcManifest(root.path)) {
|
|
105
109
|
filteredWarnings.push(`[claude-plugins] Skipping gajae-code plugin root (binding-only): ${root.path}`);
|
|
106
110
|
continue;
|
|
@@ -115,11 +119,14 @@ async function listNonGjcPluginRoots(
|
|
|
115
119
|
// Skills
|
|
116
120
|
// =============================================================================
|
|
117
121
|
|
|
118
|
-
async function
|
|
122
|
+
export async function loadMarketplaceSkills(
|
|
123
|
+
ctx: LoadContext,
|
|
124
|
+
allowedLevels: ReadonlySet<"user" | "project"> = new Set(["user", "project"]),
|
|
125
|
+
): Promise<LoadResult<Skill>> {
|
|
119
126
|
const items: Skill[] = [];
|
|
120
127
|
const warnings: string[] = [];
|
|
121
128
|
|
|
122
|
-
const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd);
|
|
129
|
+
const { roots, warnings: rootWarnings } = await listNonGjcPluginRoots(ctx.home, ctx.cwd, allowedLevels);
|
|
123
130
|
warnings.push(...rootWarnings);
|
|
124
131
|
|
|
125
132
|
const results = await Promise.all(
|
|
@@ -129,6 +136,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
129
136
|
dir: skillsDir,
|
|
130
137
|
providerId: PROVIDER_ID,
|
|
131
138
|
level: root.scope,
|
|
139
|
+
requireDescription: true,
|
|
132
140
|
});
|
|
133
141
|
return { root, result, warning };
|
|
134
142
|
}),
|
|
@@ -371,7 +379,7 @@ registerProvider<Skill>(skillCapability.id, {
|
|
|
371
379
|
displayName: DISPLAY_NAME,
|
|
372
380
|
description: "Load skills from GJC marketplace plugins",
|
|
373
381
|
priority: PRIORITY,
|
|
374
|
-
load:
|
|
382
|
+
load: loadMarketplaceSkills,
|
|
375
383
|
});
|
|
376
384
|
|
|
377
385
|
registerProvider<SlashCommand>(slashCommandCapability.id, {
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -852,6 +852,14 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
|
|
|
852
852
|
|
|
853
853
|
const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
|
|
854
854
|
|
|
855
|
+
/** Invalidate the exact user/project registry pair and their resolved-root cache entry. */
|
|
856
|
+
export async function invalidateClaudePluginRoots(home: string, cwd?: string): Promise<void> {
|
|
857
|
+
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
|
858
|
+
invalidateFsCache(path.join(getPluginsDir(home), "installed_plugins.json"));
|
|
859
|
+
if (resolvedProjectPath) invalidateFsCache(resolvedProjectPath);
|
|
860
|
+
pluginRootsCache.delete(`${home}:${resolvedProjectPath ?? ""}`);
|
|
861
|
+
}
|
|
862
|
+
|
|
855
863
|
/**
|
|
856
864
|
* List installed GJC plugin roots from the GJC plugin registry and, when present,
|
|
857
865
|
* the nearest project-scoped registry resolved from `cwd`.
|
|
@@ -885,7 +893,7 @@ export async function listClaudePluginRoots(
|
|
|
885
893
|
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
886
894
|
|
|
887
895
|
const atIndex = pluginId.lastIndexOf("@");
|
|
888
|
-
if (atIndex === -1) {
|
|
896
|
+
if (atIndex <= 0 || atIndex === pluginId.length - 1) {
|
|
889
897
|
warnings.push(`Invalid plugin ID format (missing @marketplace): ${pluginId}`);
|
|
890
898
|
continue;
|
|
891
899
|
}
|
|
@@ -927,7 +935,7 @@ export async function listClaudePluginRoots(
|
|
|
927
935
|
for (const [pluginId, entries] of Object.entries(projectRegistry.plugins)) {
|
|
928
936
|
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
929
937
|
const atIndex = pluginId.lastIndexOf("@");
|
|
930
|
-
if (atIndex === -1) {
|
|
938
|
+
if (atIndex <= 0 || atIndex === pluginId.length - 1) {
|
|
931
939
|
warnings.push(`Invalid plugin ID format (missing @marketplace): ${pluginId}`);
|
|
932
940
|
continue;
|
|
933
941
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import * as nodeFs from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { parseFrontmatter, pathIsWithin } from "@gajae-code/utils";
|
|
5
|
-
import {
|
|
6
|
+
import { classifyStdioInvocation } from "./mcp-policy";
|
|
7
|
+
import { canonicalizeJsonSchema, extractDeclaredToolSchema, schemaHash } from "./metadata";
|
|
6
8
|
import { resolveWithinRoot } from "./paths";
|
|
7
9
|
import { parseManifest, parseSubskillFrontmatter } from "./schema";
|
|
8
10
|
import {
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
type GjcPluginAppendixManifestEntry,
|
|
11
13
|
GjcPluginLoadError,
|
|
12
14
|
type GjcPluginMcpManifestEntry,
|
|
15
|
+
type JsonSchema202012,
|
|
13
16
|
type NormalizedAgentAppendixSurface,
|
|
14
17
|
type NormalizedAppendixSurface,
|
|
15
18
|
type NormalizedGjcPluginBundle,
|
|
@@ -22,6 +25,101 @@ import {
|
|
|
22
25
|
} from "./types";
|
|
23
26
|
import { validateBinding } from "./validation";
|
|
24
27
|
|
|
28
|
+
const PLUGIN_FILE_MAX_BYTES = 16 * 1024 * 1024;
|
|
29
|
+
const PLUGIN_MANIFEST_MAX_BYTES = PLUGIN_FILE_MAX_BYTES;
|
|
30
|
+
const PLUGIN_FILE_OPEN_FLAGS =
|
|
31
|
+
nodeFs.constants.O_RDONLY | (process.platform === "win32" ? 0 : (nodeFs.constants.O_NOFOLLOW ?? 0));
|
|
32
|
+
|
|
33
|
+
interface FileIdentity {
|
|
34
|
+
dev: bigint;
|
|
35
|
+
ino: bigint;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface FileAuthority {
|
|
39
|
+
path: string;
|
|
40
|
+
realPath: string;
|
|
41
|
+
identity: FileIdentity;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ManifestSnapshot {
|
|
45
|
+
bytes: Buffer;
|
|
46
|
+
json: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sameIdentity(left: FileIdentity, right: FileIdentity): boolean {
|
|
50
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function errorCode(error: unknown): string | undefined {
|
|
54
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
55
|
+
? error.code
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tooLarge(rel: string, maxBytes: number): GjcPluginLoadError {
|
|
60
|
+
return new GjcPluginLoadError("security_policy", `GJC plugin file exceeds ${maxBytes} bytes: ${rel}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readBoundedFile(file: FileAuthority, rel: string, maxBytes: number): Promise<Buffer> {
|
|
64
|
+
let handle: fs.FileHandle;
|
|
65
|
+
try {
|
|
66
|
+
handle = await fs.open(file.path, PLUGIN_FILE_OPEN_FLAGS);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (errorCode(error) === "ELOOP") {
|
|
69
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin path became a symlink before opening: ${rel}`, {
|
|
70
|
+
cause: error instanceof Error ? error : undefined,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const before = await handle.stat({ bigint: true });
|
|
77
|
+
if (!before.isFile()) {
|
|
78
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin path is not a regular file: ${rel}`);
|
|
79
|
+
}
|
|
80
|
+
if (!sameIdentity(file.identity, before)) {
|
|
81
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin file changed before opening: ${rel}`);
|
|
82
|
+
}
|
|
83
|
+
const [settledRealPath, settledPath] = await Promise.all([
|
|
84
|
+
fs.realpath(file.path),
|
|
85
|
+
fs.lstat(file.path, { bigint: true }),
|
|
86
|
+
]);
|
|
87
|
+
if (
|
|
88
|
+
settledRealPath !== file.realPath ||
|
|
89
|
+
settledPath.isSymbolicLink() ||
|
|
90
|
+
!settledPath.isFile() ||
|
|
91
|
+
!sameIdentity(settledPath, before)
|
|
92
|
+
) {
|
|
93
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin file changed before reading: ${rel}`);
|
|
94
|
+
}
|
|
95
|
+
if (before.size > BigInt(maxBytes)) throw tooLarge(rel, maxBytes);
|
|
96
|
+
const chunks: Buffer[] = [];
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const remaining = maxBytes + 1 - offset;
|
|
100
|
+
if (remaining <= 0) throw tooLarge(rel, maxBytes);
|
|
101
|
+
const chunk = Buffer.allocUnsafe(Math.min(1024 * 1024, remaining));
|
|
102
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, offset);
|
|
103
|
+
if (bytesRead === 0) break;
|
|
104
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
105
|
+
offset += bytesRead;
|
|
106
|
+
}
|
|
107
|
+
const after = await handle.stat({ bigint: true });
|
|
108
|
+
if (
|
|
109
|
+
!sameIdentity(before, after) ||
|
|
110
|
+
before.size !== after.size ||
|
|
111
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
112
|
+
before.ctimeNs !== after.ctimeNs ||
|
|
113
|
+
BigInt(offset) !== after.size
|
|
114
|
+
) {
|
|
115
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin file changed while reading: ${rel}`);
|
|
116
|
+
}
|
|
117
|
+
return Buffer.concat(chunks, offset);
|
|
118
|
+
} finally {
|
|
119
|
+
await handle.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
25
123
|
function sha256(bytes: Buffer | string): string {
|
|
26
124
|
return createHash("sha256").update(bytes).digest("hex");
|
|
27
125
|
}
|
|
@@ -43,17 +141,19 @@ export const surfaceIds = {
|
|
|
43
141
|
`subskill-tool:${parent}:${phase}:${activationArg}:${relativePath}`,
|
|
44
142
|
} as const;
|
|
45
143
|
|
|
46
|
-
async function
|
|
47
|
-
let
|
|
144
|
+
async function readManifestSnapshot(pluginRoot: string, filePath: string): Promise<ManifestSnapshot> {
|
|
145
|
+
let bytes: Buffer;
|
|
48
146
|
try {
|
|
49
|
-
|
|
147
|
+
const file = await resolveDeclaredFile(pluginRoot, GJC_PLUGIN_MANIFEST_FILENAME);
|
|
148
|
+
bytes = await readBoundedFile(file, GJC_PLUGIN_MANIFEST_FILENAME, PLUGIN_MANIFEST_MAX_BYTES);
|
|
50
149
|
} catch (error) {
|
|
150
|
+
if (error instanceof GjcPluginLoadError && error.code !== "missing_file") throw error;
|
|
51
151
|
throw new GjcPluginLoadError("missing_file", `Missing GJC plugin manifest at ${filePath}`, {
|
|
52
152
|
cause: error instanceof Error ? error : undefined,
|
|
53
153
|
});
|
|
54
154
|
}
|
|
55
155
|
try {
|
|
56
|
-
return JSON.parse(
|
|
156
|
+
return { bytes, json: JSON.parse(bytes.toString("utf8")) as unknown };
|
|
57
157
|
} catch (error) {
|
|
58
158
|
throw new GjcPluginLoadError("invalid_manifest", `Invalid GJC plugin manifest JSON at ${filePath}`, {
|
|
59
159
|
cause: error instanceof Error ? error : undefined,
|
|
@@ -61,37 +161,64 @@ async function readManifestJson(filePath: string): Promise<unknown> {
|
|
|
61
161
|
}
|
|
62
162
|
}
|
|
63
163
|
|
|
164
|
+
async function resolveDeclaredDirectory(pluginRoot: string, rel: string): Promise<string> {
|
|
165
|
+
const resolved = resolveWithinRoot(pluginRoot, rel || ".");
|
|
166
|
+
let stat: Awaited<ReturnType<typeof fs.lstat>>;
|
|
167
|
+
try {
|
|
168
|
+
stat = await fs.lstat(resolved);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new GjcPluginLoadError("missing_file", `Missing GJC plugin directory at ${resolved}`, {
|
|
171
|
+
cause: error instanceof Error ? error : undefined,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (!stat.isDirectory()) {
|
|
175
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin cwd must be a real directory: ${rel || "."}`);
|
|
176
|
+
}
|
|
177
|
+
const [realRoot, real] = await Promise.all([fs.realpath(pluginRoot), fs.realpath(resolved)]);
|
|
178
|
+
if (real !== realRoot && !pathIsWithin(realRoot, real)) {
|
|
179
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin cwd escapes root via symlink: ${rel || "."}`);
|
|
180
|
+
}
|
|
181
|
+
return resolved;
|
|
182
|
+
}
|
|
183
|
+
|
|
64
184
|
/**
|
|
65
185
|
* Resolve a declared relative path, rejecting lexical escapes AND symlink
|
|
66
186
|
* escapes out of the plugin root. Never imports the file.
|
|
67
187
|
*/
|
|
68
|
-
async function resolveDeclaredFile(pluginRoot: string, rel: string): Promise<
|
|
188
|
+
async function resolveDeclaredFile(pluginRoot: string, rel: string): Promise<FileAuthority> {
|
|
69
189
|
const resolved = resolveWithinRoot(pluginRoot, rel);
|
|
70
|
-
let
|
|
190
|
+
let realRoot: string;
|
|
191
|
+
let realPath: string;
|
|
192
|
+
let stat: nodeFs.BigIntStats;
|
|
71
193
|
try {
|
|
72
|
-
|
|
194
|
+
realRoot = await fs.realpath(pluginRoot);
|
|
195
|
+
realPath = await fs.realpath(resolved);
|
|
196
|
+
stat = await fs.lstat(resolved, { bigint: true });
|
|
73
197
|
} catch (error) {
|
|
74
198
|
throw new GjcPluginLoadError("missing_file", `Missing GJC plugin file at ${resolved}`, {
|
|
75
199
|
cause: error instanceof Error ? error : undefined,
|
|
76
200
|
});
|
|
77
201
|
}
|
|
78
|
-
|
|
79
|
-
if (!pathIsWithin(realRoot, real)) {
|
|
202
|
+
if (!pathIsWithin(realRoot, realPath)) {
|
|
80
203
|
throw new GjcPluginLoadError("security_policy", `GJC plugin file escapes root via symlink: ${rel}`);
|
|
81
204
|
}
|
|
82
|
-
|
|
205
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
206
|
+
throw new GjcPluginLoadError("security_policy", `GJC plugin path must be a real file: ${rel}`);
|
|
207
|
+
}
|
|
208
|
+
return { path: resolved, realPath, identity: stat };
|
|
83
209
|
}
|
|
84
210
|
|
|
85
211
|
async function hashFile(
|
|
86
|
-
|
|
212
|
+
file: FileAuthority,
|
|
87
213
|
rel: string,
|
|
88
214
|
declaredSha?: string,
|
|
89
|
-
): Promise<{ sha256: string; bytes: number }> {
|
|
215
|
+
): Promise<{ sha256: string; bytes: number; content: Buffer }> {
|
|
90
216
|
let buf: Buffer;
|
|
91
217
|
try {
|
|
92
|
-
buf = await
|
|
218
|
+
buf = await readBoundedFile(file, rel, PLUGIN_FILE_MAX_BYTES);
|
|
93
219
|
} catch (error) {
|
|
94
|
-
|
|
220
|
+
if (error instanceof GjcPluginLoadError) throw error;
|
|
221
|
+
throw new GjcPluginLoadError("missing_file", `Missing GJC plugin file at ${file.path}`, {
|
|
95
222
|
cause: error instanceof Error ? error : undefined,
|
|
96
223
|
});
|
|
97
224
|
}
|
|
@@ -99,7 +226,38 @@ async function hashFile(
|
|
|
99
226
|
if (declaredSha !== undefined && declaredSha.toLowerCase() !== digest) {
|
|
100
227
|
throw new GjcPluginLoadError("hash_mismatch", `GJC plugin file hash mismatch for ${rel}`);
|
|
101
228
|
}
|
|
102
|
-
return { sha256: digest, bytes: buf.byteLength };
|
|
229
|
+
return { sha256: digest, bytes: buf.byteLength, content: buf };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function schemaFromSnapshots(
|
|
233
|
+
declaration: unknown,
|
|
234
|
+
sourceBytes: Buffer,
|
|
235
|
+
schemaFile?: { relativePath: string; bytes: Buffer },
|
|
236
|
+
): JsonSchema202012 {
|
|
237
|
+
if (schemaFile !== undefined) {
|
|
238
|
+
try {
|
|
239
|
+
return canonicalizeJsonSchema(JSON.parse(schemaFile.bytes.toString("utf8")) as unknown);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof GjcPluginLoadError) throw error;
|
|
242
|
+
throw new GjcPluginLoadError(
|
|
243
|
+
"invalid_schema",
|
|
244
|
+
`Invalid JSON Schema declaration at ${schemaFile.relativePath}`,
|
|
245
|
+
{
|
|
246
|
+
cause: error instanceof Error ? error : undefined,
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (declaration !== undefined) return canonicalizeJsonSchema(declaration);
|
|
252
|
+
try {
|
|
253
|
+
return extractDeclaredToolSchema(sourceBytes.toString("utf8"));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof GjcPluginLoadError) throw error;
|
|
256
|
+
throw new GjcPluginLoadError(
|
|
257
|
+
"invalid_schema",
|
|
258
|
+
`Tool parameters schema is not statically readable: ${error instanceof Error ? error.message : String(error)}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
103
261
|
}
|
|
104
262
|
|
|
105
263
|
function mcpConfigHash(entry: GjcPluginMcpManifestEntry): string {
|
|
@@ -141,8 +299,8 @@ async function compileAppendix(
|
|
|
141
299
|
return { contentHash: digest, bytes: Buffer.byteLength(content), content };
|
|
142
300
|
}
|
|
143
301
|
const rel = entry.path as string;
|
|
144
|
-
const
|
|
145
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
302
|
+
const file = await resolveDeclaredFile(pluginRoot, rel);
|
|
303
|
+
const { sha256: digest, bytes } = await hashFile(file, rel, entry.sha256);
|
|
146
304
|
if (bytes === 0) {
|
|
147
305
|
throw new GjcPluginLoadError("invalid_appendix", `Invalid GJC plugin ${field}: file is empty`);
|
|
148
306
|
}
|
|
@@ -158,40 +316,34 @@ async function compileAppendix(
|
|
|
158
316
|
export async function compileGjcPluginBundle(root: string): Promise<NormalizedGjcPluginBundle> {
|
|
159
317
|
const pluginRoot = path.resolve(root);
|
|
160
318
|
const manifestPath = path.join(pluginRoot, GJC_PLUGIN_MANIFEST_FILENAME);
|
|
161
|
-
const
|
|
319
|
+
const manifestSnapshot = await readManifestSnapshot(pluginRoot, manifestPath);
|
|
320
|
+
const manifest = parseManifest(manifestSnapshot.json, manifestPath);
|
|
162
321
|
|
|
163
322
|
const files = new Map<string, { sha256: string; bytes: number }>();
|
|
164
323
|
const manifestSubskillTools = manifest.tools.filter(tool => tool.surface === "subskill");
|
|
165
324
|
const manifestSubskillFiles = new Map<string, { name: string; sha256: string }>();
|
|
166
325
|
for (const tool of manifestSubskillTools) {
|
|
167
|
-
const
|
|
168
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
326
|
+
const file = await resolveDeclaredFile(pluginRoot, tool.path);
|
|
327
|
+
const { sha256: digest, bytes } = await hashFile(file, tool.path, tool.sha256);
|
|
169
328
|
files.set(tool.path, { sha256: digest, bytes });
|
|
170
329
|
manifestSubskillFiles.set(tool.path, { name: tool.name, sha256: digest });
|
|
171
330
|
}
|
|
172
331
|
|
|
173
332
|
const subskills: NormalizedSubskillSurface[] = [];
|
|
174
333
|
for (const rel of manifest.subskills) {
|
|
175
|
-
const
|
|
176
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
334
|
+
const file = await resolveDeclaredFile(pluginRoot, rel);
|
|
335
|
+
const { sha256: digest, bytes, content: contentBytes } = await hashFile(file, rel);
|
|
177
336
|
files.set(rel, { sha256: digest, bytes });
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
content = await fs.readFile(abs, "utf8");
|
|
181
|
-
} catch (error) {
|
|
182
|
-
throw new GjcPluginLoadError("missing_file", `Missing GJC sub-skill file at ${abs}`, {
|
|
183
|
-
cause: error instanceof Error ? error : undefined,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
337
|
+
const content = contentBytes.toString("utf8");
|
|
186
338
|
let parsed: { frontmatter: Record<string, unknown>; body: string };
|
|
187
339
|
try {
|
|
188
|
-
parsed = parseFrontmatter(content, { source:
|
|
340
|
+
parsed = parseFrontmatter(content, { source: file.path, level: "fatal" });
|
|
189
341
|
} catch (error) {
|
|
190
|
-
throw new GjcPluginLoadError("invalid_frontmatter", `Invalid GJC sub-skill frontmatter at ${
|
|
342
|
+
throw new GjcPluginLoadError("invalid_frontmatter", `Invalid GJC sub-skill frontmatter at ${file.path}`, {
|
|
191
343
|
cause: error instanceof Error ? error : undefined,
|
|
192
344
|
});
|
|
193
345
|
}
|
|
194
|
-
const fm = parseSubskillFrontmatter(parsed.frontmatter,
|
|
346
|
+
const fm = parseSubskillFrontmatter(parsed.frontmatter, file.path);
|
|
195
347
|
validateBinding(fm);
|
|
196
348
|
// Subskill-scoped frontmatter tools are hashed for copy-ownership and
|
|
197
349
|
// escape checks (the loader resolves these at runtime).
|
|
@@ -212,8 +364,8 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
|
|
|
212
364
|
}
|
|
213
365
|
for (const toolRel of fmToolPaths) {
|
|
214
366
|
if (toolRel.trim().length === 0) continue;
|
|
215
|
-
const
|
|
216
|
-
const { sha256: toolDigest, bytes: toolBytes } = await hashFile(
|
|
367
|
+
const toolFile = await resolveDeclaredFile(pluginRoot, toolRel);
|
|
368
|
+
const { sha256: toolDigest, bytes: toolBytes } = await hashFile(toolFile, toolRel);
|
|
217
369
|
files.set(toolRel, { sha256: toolDigest, bytes: toolBytes });
|
|
218
370
|
const extensionId = surfaceIds.subskillTool(fm.binds_to, fm.phase, fm.activation_arg, toolRel);
|
|
219
371
|
if (seenToolRefs.has(extensionId)) continue;
|
|
@@ -238,18 +390,18 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
|
|
|
238
390
|
// surface; legacy string shorthand stays subskill-scoped (loader-handled).
|
|
239
391
|
const tools: NormalizedToolSurface[] = [];
|
|
240
392
|
for (const tool of manifest.tools) {
|
|
241
|
-
const
|
|
242
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
393
|
+
const file = await resolveDeclaredFile(pluginRoot, tool.path);
|
|
394
|
+
const { sha256: digest, bytes, content: sourceBytes } = await hashFile(file, tool.path, tool.sha256);
|
|
243
395
|
files.set(tool.path, { sha256: digest, bytes });
|
|
244
396
|
if (tool.surface !== "always-on") continue;
|
|
245
|
-
let
|
|
397
|
+
let schemaFileSnapshot: { relativePath: string; bytes: Buffer } | undefined;
|
|
246
398
|
if (tool.schemaPath !== undefined) {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
files.set(tool.schemaPath,
|
|
250
|
-
|
|
399
|
+
const schemaFile = await resolveDeclaredFile(pluginRoot, tool.schemaPath);
|
|
400
|
+
const { sha256: schemaDigest, bytes: schemaBytes, content } = await hashFile(schemaFile, tool.schemaPath);
|
|
401
|
+
files.set(tool.schemaPath, { sha256: schemaDigest, bytes: schemaBytes });
|
|
402
|
+
schemaFileSnapshot = { relativePath: tool.schemaPath, bytes: content };
|
|
251
403
|
}
|
|
252
|
-
const schema =
|
|
404
|
+
const schema = schemaFromSnapshots(tool.schema, sourceBytes, schemaFileSnapshot);
|
|
253
405
|
tools.push({
|
|
254
406
|
extensionId: surfaceIds.tool(tool.name),
|
|
255
407
|
name: tool.name,
|
|
@@ -267,8 +419,8 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
|
|
|
267
419
|
for (const hook of manifest.hooks) {
|
|
268
420
|
// Path safety first: resolve/hash before semantic checks so traversal and
|
|
269
421
|
// missing-file failures take precedence over contract validation.
|
|
270
|
-
const
|
|
271
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
422
|
+
const file = await resolveDeclaredFile(pluginRoot, hook.path);
|
|
423
|
+
const { sha256: digest, bytes } = await hashFile(file, hook.path, hook.sha256);
|
|
272
424
|
files.set(hook.path, { sha256: digest, bytes });
|
|
273
425
|
// Minimal compile-time hook contract: tool_call hooks must name a target
|
|
274
426
|
// and a before/after phase so the constrained runner (M3/M4) can bind them.
|
|
@@ -320,17 +472,15 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
|
|
|
320
472
|
`GJC plugin MCP "${entry.name}": ${entry.transport} requires a url`,
|
|
321
473
|
);
|
|
322
474
|
}
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const { sha256: digest, bytes } = await hashFile(abs, arg, undefined);
|
|
333
|
-
files.set(arg, { sha256: digest, bytes });
|
|
475
|
+
// Derive ownership from the same cwd-aware bare Node/Bun grammar used at runtime.
|
|
476
|
+
if (entry.transport === "stdio") {
|
|
477
|
+
const invocation = classifyStdioInvocation(entry, { pluginRoot });
|
|
478
|
+
await resolveDeclaredDirectory(pluginRoot, path.relative(pluginRoot, invocation.cwd));
|
|
479
|
+
for (const relativePath of new Set(invocation.ownedRelativePaths)) {
|
|
480
|
+
const file = await resolveDeclaredFile(pluginRoot, relativePath);
|
|
481
|
+
const { sha256: digest, bytes } = await hashFile(file, relativePath, undefined);
|
|
482
|
+
files.set(relativePath, { sha256: digest, bytes });
|
|
483
|
+
}
|
|
334
484
|
}
|
|
335
485
|
mcps.push({
|
|
336
486
|
extensionId: surfaceIds.mcp(entry.name),
|
|
@@ -377,7 +527,7 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
|
|
|
377
527
|
agentAppendices,
|
|
378
528
|
};
|
|
379
529
|
|
|
380
|
-
const manifestBytes =
|
|
530
|
+
const manifestBytes = manifestSnapshot.bytes;
|
|
381
531
|
const manifestHash = sha256(manifestBytes);
|
|
382
532
|
|
|
383
533
|
const copiedFiles = [...files.entries()]
|
|
@@ -5,7 +5,8 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { gunzipSync } from "node:zlib";
|
|
7
7
|
import { compileGjcPluginBundle } from "./compiler";
|
|
8
|
-
import {
|
|
8
|
+
import { classifyStdioInvocation } from "./mcp-policy";
|
|
9
|
+
import { gjcPluginInstallRoot, gjcPluginProjectRoot, gjcPluginSafeDirectoryName, gjcPluginUserRoot } from "./paths";
|
|
9
10
|
import { readRegistry, sortRegistryEntries, withRegistryLock, writeRegistryUnlocked } from "./registry";
|
|
10
11
|
import {
|
|
11
12
|
GJC_PLUGIN_MANIFEST_FILENAME,
|
|
@@ -64,14 +65,6 @@ function scopeRoot(scope: GjcPluginScope, cwd: string): string {
|
|
|
64
65
|
return scope === "user" ? gjcPluginUserRoot() : gjcPluginProjectRoot(cwd);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function safeDirSegment(name: string): string {
|
|
68
|
-
const seg = name.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
|
|
69
|
-
if (!seg || seg === "." || seg === "..") {
|
|
70
|
-
throw new GjcPluginLoadError("invalid_manifest", `GJC plugin name is not a safe directory segment: ${name}`);
|
|
71
|
-
}
|
|
72
|
-
return seg;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
68
|
async function isDirectory(p: string): Promise<boolean> {
|
|
76
69
|
try {
|
|
77
70
|
return (await fs.stat(p)).isDirectory();
|
|
@@ -392,6 +385,14 @@ function sha256(buf: Buffer): string {
|
|
|
392
385
|
* copied, so the installed tree equals the validated set.
|
|
393
386
|
*/
|
|
394
387
|
async function copyValidatedFiles(bundle: NormalizedGjcPluginBundle, stagingDir: string): Promise<void> {
|
|
388
|
+
const cwdPaths = new Set(
|
|
389
|
+
bundle.surfaces.mcps.flatMap(surface => {
|
|
390
|
+
if (surface.transport !== "stdio") return [];
|
|
391
|
+
const invocation = classifyStdioInvocation(surface.config, { pluginRoot: bundle.root });
|
|
392
|
+
return [path.relative(bundle.root, invocation.cwd)];
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
for (const cwdPath of cwdPaths) await fs.mkdir(path.join(stagingDir, cwdPath), { recursive: true });
|
|
395
396
|
for (const file of bundle.files) {
|
|
396
397
|
const src = path.join(bundle.root, file.relativePath);
|
|
397
398
|
const lst = await fs.lstat(src);
|
|
@@ -438,9 +439,9 @@ export async function runGjcBundleTransaction(
|
|
|
438
439
|
// lock roots or sweep installation remnants.
|
|
439
440
|
const bundle = await compileGjcPluginBundle(resolved.dir);
|
|
440
441
|
validateInstallPlan(bundle, []);
|
|
441
|
-
const dirName =
|
|
442
|
+
const dirName = gjcPluginSafeDirectoryName(bundle.name);
|
|
442
443
|
const root = scopeRoot(options.scope, options.cwd);
|
|
443
|
-
const finalDir =
|
|
444
|
+
const finalDir = gjcPluginInstallRoot(options.scope, options.cwd, bundle.name);
|
|
444
445
|
|
|
445
446
|
// Lock-free refusal preflight. Acquiring a scope lock creates the scope
|
|
446
447
|
// root and mutates directory metadata, so a create-only refusal must be
|
|
@@ -578,7 +579,7 @@ export function candidateRegistryEntry(
|
|
|
578
579
|
source: GjcPluginRegistrySource,
|
|
579
580
|
now: string,
|
|
580
581
|
): GjcPluginRegistryEntry {
|
|
581
|
-
const finalDir =
|
|
582
|
+
const finalDir = gjcPluginInstallRoot(scope, cwd, bundle.name);
|
|
582
583
|
return bundleToRegistryEntry(bundle, finalDir, scope, source, now);
|
|
583
584
|
}
|
|
584
585
|
|