@gajae-code/coding-agent 0.12.0 → 0.12.2
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 +111 -0
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +56 -2
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +40 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +7 -1
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +9 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +290 -0
- package/src/async/job-manager.ts +99 -39
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +53 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +491 -27
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
- package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
- package/src/gjc-runtime/workflow-manifest.ts +21 -70
- package/src/internal-urls/docs-index.generated.ts +16 -16
- package/src/modes/DESIGN.md +104 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -15
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +42 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +193 -34
- package/src/modes/controllers/selector-controller.ts +160 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/runtime-init.ts +3 -0
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +7 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +89 -9
- package/src/session/agent-session.ts +844 -173
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +200 -111
- package/src/session/internal/managed-session-storage.ts +492 -62
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +38 -45
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +280 -45
- package/src/task/output-manager.ts +24 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +34 -8
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +6 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -6,16 +6,12 @@ import * as path from "node:path";
|
|
|
6
6
|
import { gunzipSync } from "node:zlib";
|
|
7
7
|
import { compileGjcPluginBundle } from "./compiler";
|
|
8
8
|
import { gjcPluginProjectRoot, gjcPluginUserRoot } from "./paths";
|
|
9
|
-
import {
|
|
10
|
-
readRegistry,
|
|
11
|
-
registryEntryFingerprint,
|
|
12
|
-
sortRegistryEntries,
|
|
13
|
-
withRegistryLock,
|
|
14
|
-
writeRegistryUnlocked,
|
|
15
|
-
} from "./registry";
|
|
9
|
+
import { readRegistry, sortRegistryEntries, withRegistryLock, writeRegistryUnlocked } from "./registry";
|
|
16
10
|
import {
|
|
17
11
|
GJC_PLUGIN_MANIFEST_FILENAME,
|
|
12
|
+
type GjcLifecycleError,
|
|
18
13
|
GjcPluginLoadError,
|
|
14
|
+
type GjcPluginRegistry,
|
|
19
15
|
type GjcPluginRegistryEntry,
|
|
20
16
|
type GjcPluginRegistrySource,
|
|
21
17
|
type GjcPluginScope,
|
|
@@ -23,17 +19,37 @@ import {
|
|
|
23
19
|
} from "./types";
|
|
24
20
|
import { validateInstallPlan } from "./validation";
|
|
25
21
|
|
|
26
|
-
export interface
|
|
22
|
+
export interface GjcBundleTransactionOptions {
|
|
27
23
|
scope: GjcPluginScope;
|
|
28
24
|
cwd: string;
|
|
29
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Policy hook evaluated while both scope locks are held. It decides whether
|
|
27
|
+
* to commit the candidate, report an already-satisfied no-op, or abort with
|
|
28
|
+
* a typed lifecycle error. Only the lifecycle service supplies this.
|
|
29
|
+
*/
|
|
30
|
+
decide: (input: GjcBundleTransactionContext) => Promise<GjcBundleTransactionDecision>;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export interface
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
export interface GjcBundleTransactionContext {
|
|
34
|
+
targetRegistry: GjcPluginRegistry;
|
|
35
|
+
/** Both scopes, deterministically sorted, for cross-scope decisions. */
|
|
36
|
+
effective: GjcPluginRegistryEntry[];
|
|
37
|
+
existing: GjcPluginRegistryEntry | undefined;
|
|
38
|
+
bundle: NormalizedGjcPluginBundle;
|
|
39
|
+
/** Entry the candidate would produce if committed as-is. */
|
|
40
|
+
candidate: GjcPluginRegistryEntry;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
export type GjcBundleTransactionDecision =
|
|
44
|
+
| { kind: "commit"; entry: GjcPluginRegistryEntry }
|
|
45
|
+
| { kind: "noop"; entry: GjcPluginRegistryEntry }
|
|
46
|
+
| { kind: "abort"; error: GjcLifecycleError };
|
|
47
|
+
|
|
48
|
+
export type GjcBundleTransactionResult =
|
|
49
|
+
| { status: "committed"; entry: GjcPluginRegistryEntry; remnants: string[] }
|
|
50
|
+
| { status: "noop"; entry: GjcPluginRegistryEntry; remnants: string[] }
|
|
51
|
+
| { status: "aborted"; error: GjcLifecycleError; remnants: string[] };
|
|
52
|
+
|
|
37
53
|
// Resource limits for the in-house tar extractor (third-party security boundary).
|
|
38
54
|
const TAR_MAX_FILES = 8192;
|
|
39
55
|
const TAR_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
@@ -113,8 +129,21 @@ function tarHeaderChecksumOk(header: Uint8Array): boolean {
|
|
|
113
129
|
|
|
114
130
|
/** Minimal, traversal/symlink-safe, resource-bounded extraction of a tar(.gz). */
|
|
115
131
|
async function extractTarball(tarPath: string, destRoot: string): Promise<void> {
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
// A missing or corrupt archive surfaces as a native fs/zlib error. Translate
|
|
133
|
+
// it here so callers see the same typed source failure they get for every
|
|
134
|
+
// other unreachable source, instead of a raw errno escaping the lifecycle.
|
|
135
|
+
let raw: Buffer;
|
|
136
|
+
try {
|
|
137
|
+
raw = await fs.readFile(tarPath);
|
|
138
|
+
} catch {
|
|
139
|
+
throw new GjcPluginLoadError("missing_file", "GJC plugin tarball could not be read");
|
|
140
|
+
}
|
|
141
|
+
let buf: Buffer;
|
|
142
|
+
try {
|
|
143
|
+
buf = /\.(tgz|tar\.gz)$/i.test(tarPath) ? gunzipSync(raw) : raw;
|
|
144
|
+
} catch {
|
|
145
|
+
throw new GjcPluginLoadError("invalid_manifest", "GJC plugin tarball could not be decompressed");
|
|
146
|
+
}
|
|
118
147
|
const resolvedRoot = path.resolve(destRoot);
|
|
119
148
|
const decoder = new TextDecoder();
|
|
120
149
|
let offset = 0;
|
|
@@ -215,17 +244,23 @@ function runGit(args: string[], cwd?: string): Promise<string> {
|
|
|
215
244
|
// argv array (no shell) — repo/ref are passed as discrete args, not interpolated.
|
|
216
245
|
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
217
246
|
let stdout = "";
|
|
218
|
-
let stderr = "";
|
|
219
247
|
child.stdout.on("data", d => {
|
|
220
248
|
stdout += d;
|
|
221
249
|
});
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
// stderr is drained but never surfaced: git writes the remote URL into it,
|
|
251
|
+
// which can carry credentials.
|
|
252
|
+
child.stderr.resume();
|
|
253
|
+
// A spawn failure (git missing, ENOENT, EACCES) arrives as a raw system
|
|
254
|
+
// error. Convert it so the lifecycle can report a typed, sanitized source
|
|
255
|
+
// failure instead of letting an errno escape to the CLI.
|
|
256
|
+
child.on("error", () => {
|
|
257
|
+
reject(new GjcPluginLoadError("missing_file", "git is unavailable or could not be started"));
|
|
224
258
|
});
|
|
225
|
-
child.on("error", reject);
|
|
226
259
|
child.on("close", code => {
|
|
227
260
|
if (code === 0) resolve(stdout.trim());
|
|
228
|
-
|
|
261
|
+
// git writes the remote URL into stderr, which can carry credentials, so
|
|
262
|
+
// the operation is named without echoing the underlying output.
|
|
263
|
+
else reject(new GjcPluginLoadError("install_conflict", `git ${args[0]} failed`));
|
|
229
264
|
});
|
|
230
265
|
return promise;
|
|
231
266
|
}
|
|
@@ -334,29 +369,61 @@ async function cleanupOrphans(root: string, dirName: string): Promise<void> {
|
|
|
334
369
|
}
|
|
335
370
|
}
|
|
336
371
|
|
|
337
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Serialized bundle transaction: prepare outside the locks, then hold the
|
|
374
|
+
* user->project locks in a fixed order so the decision sees a consistent
|
|
375
|
+
* cross-scope view. Only the target scope is ever committed.
|
|
376
|
+
*/
|
|
377
|
+
export async function runGjcBundleTransaction(
|
|
338
378
|
source: string,
|
|
339
|
-
options:
|
|
340
|
-
): Promise<
|
|
379
|
+
options: GjcBundleTransactionOptions,
|
|
380
|
+
): Promise<GjcBundleTransactionResult> {
|
|
341
381
|
const resolved = await resolveSource(source);
|
|
342
382
|
try {
|
|
343
|
-
//
|
|
383
|
+
// Compile + validate outside every lock (never imports plugin code).
|
|
344
384
|
const bundle = await compileGjcPluginBundle(resolved.dir);
|
|
345
385
|
const dirName = safeDirSegment(bundle.name);
|
|
346
386
|
const root = scopeRoot(options.scope, options.cwd);
|
|
347
387
|
const finalDir = path.join(root, dirName);
|
|
348
388
|
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
389
|
+
// Lock-free refusal preflight. Acquiring a scope lock creates the scope
|
|
390
|
+
// root and mutates directory metadata, so a create-only refusal must be
|
|
391
|
+
// decided before any lock is taken; otherwise "zero mutation" is false.
|
|
392
|
+
// The locked decision below re-checks, so this is an early-out only.
|
|
393
|
+
const preflightTarget = await readRegistry(options.scope, options.cwd);
|
|
394
|
+
const preexisting = preflightTarget.plugins.find(p => p.name === bundle.name);
|
|
395
|
+
if (preexisting) {
|
|
396
|
+
// The decision may compare a cross-scope fingerprint, so it must see the
|
|
397
|
+
// same complete universe the locked decision sees.
|
|
398
|
+
const preflightOther = await readRegistry(options.scope === "user" ? "project" : "user", options.cwd);
|
|
399
|
+
const early = await options.decide({
|
|
400
|
+
targetRegistry: preflightTarget,
|
|
401
|
+
effective: sortRegistryEntries([...preflightTarget.plugins, ...preflightOther.plugins]),
|
|
402
|
+
existing: preexisting,
|
|
403
|
+
bundle,
|
|
404
|
+
candidate: bundleToRegistryEntry(
|
|
405
|
+
bundle,
|
|
406
|
+
finalDir,
|
|
407
|
+
options.scope,
|
|
408
|
+
resolved.source,
|
|
409
|
+
new Date().toISOString(),
|
|
410
|
+
),
|
|
411
|
+
});
|
|
412
|
+
// Only an abort is honoured here; everything else is re-decided under
|
|
413
|
+
// the lock, so this can never short-circuit a commit.
|
|
414
|
+
if (early.kind === "abort") return { status: "aborted", error: early.error, remnants: [] };
|
|
415
|
+
}
|
|
354
416
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
417
|
+
const critical = async (): Promise<GjcBundleTransactionResult> => {
|
|
418
|
+
// Read-only until the policy decision resolves. A refusal must not create
|
|
419
|
+
// the scope root or sweep orphans, so an existing-target refusal leaves
|
|
420
|
+
// the filesystem byte-for-byte untouched.
|
|
421
|
+
|
|
422
|
+
const targetRegistry = await readRegistry(options.scope, options.cwd);
|
|
423
|
+
const otherScope: GjcPluginScope = options.scope === "user" ? "project" : "user";
|
|
424
|
+
const otherRegistry = await readRegistry(otherScope, options.cwd);
|
|
425
|
+
const effective = sortRegistryEntries([...targetRegistry.plugins, ...otherRegistry.plugins]);
|
|
426
|
+
const existing = targetRegistry.plugins.find(p => p.name === bundle.name);
|
|
360
427
|
const candidate = bundleToRegistryEntry(
|
|
361
428
|
bundle,
|
|
362
429
|
finalDir,
|
|
@@ -364,18 +431,21 @@ export async function installGjcPluginBundle(
|
|
|
364
431
|
resolved.source,
|
|
365
432
|
new Date().toISOString(),
|
|
366
433
|
);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
434
|
+
|
|
435
|
+
const decision = await options.decide({ targetRegistry, effective, existing, bundle, candidate });
|
|
436
|
+
if (decision.kind === "abort") return { status: "aborted", error: decision.error, remnants: [] };
|
|
437
|
+
if (decision.kind === "noop") return { status: "noop", entry: decision.entry, remnants: [] };
|
|
438
|
+
|
|
439
|
+
// The decision committed, so mutation may begin.
|
|
440
|
+
await fs.mkdir(root, { recursive: true });
|
|
441
|
+
await cleanupOrphans(root, dirName);
|
|
442
|
+
|
|
443
|
+
// Hard install-time collision + MCP security validation against the
|
|
444
|
+
// effective registry across BOTH scopes. Surface IDs derive from the
|
|
445
|
+
// surface name, not the bundle name, so a differently named bundle in
|
|
446
|
+
// the opposite scope can claim the same ID; only the exact target
|
|
447
|
+
// identity is excluded, since that is the entry being replaced.
|
|
448
|
+
validateInstallPlan(bundle, effective);
|
|
379
449
|
|
|
380
450
|
const unique = `${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
381
451
|
const stagingDir = `${finalDir}.installing-${unique}`;
|
|
@@ -394,8 +464,8 @@ export async function installGjcPluginBundle(
|
|
|
394
464
|
// Registry write last; on failure, roll the filesystem back.
|
|
395
465
|
try {
|
|
396
466
|
const next = sortRegistryEntries([
|
|
397
|
-
...
|
|
398
|
-
|
|
467
|
+
...targetRegistry.plugins.filter(p => p.name !== bundle.name),
|
|
468
|
+
decision.entry,
|
|
399
469
|
]);
|
|
400
470
|
await writeRegistryUnlocked({ version: 1, scope: options.scope, plugins: next }, options.cwd);
|
|
401
471
|
} catch (error) {
|
|
@@ -403,17 +473,86 @@ export async function installGjcPluginBundle(
|
|
|
403
473
|
if (hadFinal) await fs.rename(backupDir, finalDir);
|
|
404
474
|
throw error;
|
|
405
475
|
}
|
|
406
|
-
|
|
407
|
-
|
|
476
|
+
const remnants: string[] = [];
|
|
477
|
+
if (hadFinal) {
|
|
478
|
+
try {
|
|
479
|
+
await fs.rm(backupDir, { recursive: true, force: true });
|
|
480
|
+
} catch {
|
|
481
|
+
remnants.push(backupDir);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { status: "committed", entry: decision.entry, remnants };
|
|
408
485
|
} finally {
|
|
409
486
|
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
410
487
|
}
|
|
411
|
-
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// Surface IDs are globally unique, so the collision decision spans both
|
|
491
|
+
// scopes and must be serialized against every other writer. Both locks are
|
|
492
|
+
// therefore held, in a fixed user->project order to avoid deadlock,
|
|
493
|
+
// regardless of which scope commits. Refusal purity is preserved by the
|
|
494
|
+
// pre-lock preflight above, which returns before any lock is acquired.
|
|
495
|
+
return await withRegistryLock("user", options.cwd, () => withRegistryLock("project", options.cwd, critical));
|
|
496
|
+
} finally {
|
|
497
|
+
await resolved.cleanup();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Compile a source into a validated candidate bundle without touching disk state. */
|
|
502
|
+
export async function resolveGjcBundleCandidate<T>(
|
|
503
|
+
source: string,
|
|
504
|
+
fn: (input: { bundle: NormalizedGjcPluginBundle; source: GjcPluginRegistrySource }) => Promise<T>,
|
|
505
|
+
): Promise<T> {
|
|
506
|
+
const resolved = await resolveSource(source);
|
|
507
|
+
try {
|
|
508
|
+
const bundle = await compileGjcPluginBundle(resolved.dir);
|
|
509
|
+
return await fn({ bundle, source: resolved.source });
|
|
412
510
|
} finally {
|
|
413
511
|
await resolved.cleanup();
|
|
414
512
|
}
|
|
415
513
|
}
|
|
416
514
|
|
|
515
|
+
/** Build the registry entry a candidate bundle would produce at a target path. */
|
|
516
|
+
export function candidateRegistryEntry(
|
|
517
|
+
bundle: NormalizedGjcPluginBundle,
|
|
518
|
+
scope: GjcPluginScope,
|
|
519
|
+
cwd: string,
|
|
520
|
+
source: GjcPluginRegistrySource,
|
|
521
|
+
now: string,
|
|
522
|
+
): GjcPluginRegistryEntry {
|
|
523
|
+
const finalDir = path.join(scopeRoot(scope, cwd), safeDirSegment(bundle.name));
|
|
524
|
+
return bundleToRegistryEntry(bundle, finalDir, scope, source, now);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* True when a spec has the SHAPE of a GJC bundle source: a filesystem path, a
|
|
529
|
+
* git locator, or a tarball. This is a pure string test that never touches the
|
|
530
|
+
* filesystem or the network, so a deleted or unreachable source is still
|
|
531
|
+
* recognised as GJC-intent and can reach the lifecycle's typed refusal instead
|
|
532
|
+
* of falling through to npm.
|
|
533
|
+
*
|
|
534
|
+
* npm and marketplace specs are never path/git/tarball shaped, so this cleanly
|
|
535
|
+
* separates the two install worlds.
|
|
536
|
+
*/
|
|
537
|
+
export function isGjcPluginSourceShape(source: string): boolean {
|
|
538
|
+
if (looksLikeGit(source)) return true;
|
|
539
|
+
// Explicit path forms, POSIX and Windows.
|
|
540
|
+
const isPathShaped =
|
|
541
|
+
source.startsWith("/") ||
|
|
542
|
+
source.startsWith("./") ||
|
|
543
|
+
source.startsWith("../") ||
|
|
544
|
+
source.startsWith("~/") ||
|
|
545
|
+
source.startsWith(".\\") ||
|
|
546
|
+
source.startsWith("..\\") ||
|
|
547
|
+
/^[a-zA-Z]:[\\/]/.test(source) ||
|
|
548
|
+
source.startsWith("\\\\");
|
|
549
|
+
if (isPathShaped) return true;
|
|
550
|
+
// A tarball SUFFIX alone is not enough: npm package names may contain dots,
|
|
551
|
+
// so `foo.tgz` and `@scope/foo.tar.gz` are legal npm specs. Only claim an
|
|
552
|
+
// archive when the locator is also path- or URL-shaped.
|
|
553
|
+
return isTarball(source) && /^[a-z][a-z0-9+.-]*:\/\//i.test(source);
|
|
554
|
+
}
|
|
555
|
+
|
|
417
556
|
/** True only when the source actually resolves to a GJC plugin bundle (root gajae-plugin.json). */
|
|
418
557
|
export async function isGjcPluginBundleSource(source: string): Promise<boolean> {
|
|
419
558
|
if (!isTarball(source) && !looksLikeGit(source)) {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
GJC_BUNDLE_KIND,
|
|
4
|
+
type GjcBundleIdentity,
|
|
5
|
+
type GjcPluginQuarantineEntry,
|
|
6
|
+
type GjcPluginRegistryEntry,
|
|
7
|
+
type GjcPluginScope,
|
|
8
|
+
type NormalizedGjcPluginBundle,
|
|
9
|
+
type NormalizedGjcPluginSurfaces,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pure fingerprint and reconciliation helpers for the GJC bundle lifecycle.
|
|
14
|
+
* Nothing here reads or writes the filesystem: every function is a
|
|
15
|
+
* deterministic projection of its inputs so preview/apply can compare-and-swap
|
|
16
|
+
* on stable hashes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function sha256(text: string): string {
|
|
20
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function bundleIdentity(scope: GjcPluginScope, name: string): GjcBundleIdentity {
|
|
24
|
+
return { kind: GJC_BUNDLE_KIND, scope, name };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function identityKey(identity: GjcBundleIdentity): string {
|
|
28
|
+
return `${identity.kind}\u0000${identity.scope}\u0000${identity.name}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function identityEquals(a: GjcBundleIdentity, b: GjcBundleIdentity): boolean {
|
|
32
|
+
return a.kind === b.kind && a.scope === b.scope && a.name === b.name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** All stable surface IDs of a surface set, sorted and de-duplicated. */
|
|
36
|
+
export function surfaceIdsOf(surfaces: NormalizedGjcPluginSurfaces): string[] {
|
|
37
|
+
const ids = [
|
|
38
|
+
...surfaces.subskills.map(s => s.extensionId),
|
|
39
|
+
...surfaces.tools.map(t => t.extensionId),
|
|
40
|
+
...surfaces.hooks.map(h => h.extensionId),
|
|
41
|
+
...surfaces.mcps.map(m => m.extensionId),
|
|
42
|
+
...surfaces.systemAppendices.map(a => a.extensionId),
|
|
43
|
+
...surfaces.agentAppendices.map(a => a.extensionId),
|
|
44
|
+
];
|
|
45
|
+
return [...new Set(ids)].sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fingerprint of the exact installed target: identity plus the persisted bytes
|
|
50
|
+
* that any update must not silently replace.
|
|
51
|
+
*/
|
|
52
|
+
export function targetFingerprint(entry: GjcPluginRegistryEntry): string {
|
|
53
|
+
const files = [...entry.copiedFiles]
|
|
54
|
+
.map(f => `${f.relativePath}:${f.sha256}:${f.bytes}`)
|
|
55
|
+
.sort()
|
|
56
|
+
.join("\n");
|
|
57
|
+
return sha256(
|
|
58
|
+
[
|
|
59
|
+
identityKey(bundleIdentity(entry.scope, entry.name)),
|
|
60
|
+
entry.version,
|
|
61
|
+
entry.manifestHash,
|
|
62
|
+
surfaceIdsOf(entry.surfaces).join(","),
|
|
63
|
+
files,
|
|
64
|
+
].join("\u0000"),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fingerprint of the exact installed baseline an update compares against: the
|
|
70
|
+
* installed content, the persisted enablement intent it must carry forward,
|
|
71
|
+
* and the stored source descriptor the update re-resolves from.
|
|
72
|
+
*
|
|
73
|
+
* Toggling the bundle or any surface changes this, so a preview taken before
|
|
74
|
+
* the toggle can no longer be applied. Binding the source matters just as much:
|
|
75
|
+
* apply re-resolves `entry.source.uri` before taking the locks, so a descriptor
|
|
76
|
+
* that changed in between must invalidate the reviewed baseline rather than let
|
|
77
|
+
* an update be committed from a locator the reviewer never saw.
|
|
78
|
+
*/
|
|
79
|
+
export function baselineFingerprint(entry: GjcPluginRegistryEntry): string {
|
|
80
|
+
const source = [
|
|
81
|
+
entry.source.kind,
|
|
82
|
+
entry.source.uri,
|
|
83
|
+
entry.source.ref ?? "",
|
|
84
|
+
entry.source.sha ?? "",
|
|
85
|
+
entry.source.resolvedAt,
|
|
86
|
+
].join("\u0001");
|
|
87
|
+
return sha256(
|
|
88
|
+
[
|
|
89
|
+
targetFingerprint(entry),
|
|
90
|
+
entry.enabled ? "1" : "0",
|
|
91
|
+
[...new Set(entry.disabledSurfaceIds)].sort().join(","),
|
|
92
|
+
[...new Set((entry.quarantine ?? []).map(q => `${q.surfaceId}:${q.code}`))].sort().join(","),
|
|
93
|
+
source,
|
|
94
|
+
].join("\u0000"),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Fingerprint of an update candidate before it is written anywhere. */
|
|
99
|
+
export function candidateFingerprint(scope: GjcPluginScope, bundle: NormalizedGjcPluginBundle): string {
|
|
100
|
+
const files = [...bundle.files]
|
|
101
|
+
.map(f => `${f.relativePath}:${f.sha256}:${f.bytes}`)
|
|
102
|
+
.sort()
|
|
103
|
+
.join("\n");
|
|
104
|
+
return sha256(
|
|
105
|
+
[
|
|
106
|
+
identityKey(bundleIdentity(scope, bundle.name)),
|
|
107
|
+
bundle.version,
|
|
108
|
+
bundle.manifestHash,
|
|
109
|
+
surfaceIdsOf(bundle.surfaces).join(","),
|
|
110
|
+
files,
|
|
111
|
+
].join("\u0000"),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fingerprint of everything besides the target and candidate bytes that the
|
|
117
|
+
* update decision depended on: opposite-scope same-name entries and the
|
|
118
|
+
* effective collision universe.
|
|
119
|
+
*/
|
|
120
|
+
export function decisionContextFingerprint(
|
|
121
|
+
target: GjcBundleIdentity,
|
|
122
|
+
effectiveEntries: readonly GjcPluginRegistryEntry[],
|
|
123
|
+
): string {
|
|
124
|
+
// The target's own state is covered by the baseline fingerprint; this hash
|
|
125
|
+
// only tracks the surrounding universe (notably same-name opposite-scope
|
|
126
|
+
// entries) so drift is attributed to the correct gate.
|
|
127
|
+
const parts = effectiveEntries
|
|
128
|
+
.filter(e => !identityEquals(bundleIdentity(e.scope, e.name), target))
|
|
129
|
+
.map(e => `${e.scope}\u0000${e.name}\u0000${e.version}\u0000${e.manifestHash}\u0000${e.enabled ? "1" : "0"}`)
|
|
130
|
+
.sort();
|
|
131
|
+
return sha256([identityKey(target), ...parts].join("\n"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Fingerprint of the inputs that decide live activation. Changes here (and only
|
|
136
|
+
* here) advance the activation generation.
|
|
137
|
+
*/
|
|
138
|
+
export function activationFingerprint(entries: readonly GjcPluginRegistryEntry[]): string {
|
|
139
|
+
const parts = entries
|
|
140
|
+
.filter(e => e.enabled)
|
|
141
|
+
.map(e => {
|
|
142
|
+
const disabled = [...new Set(e.disabledSurfaceIds)].sort().join(",");
|
|
143
|
+
const quarantined = [...new Set((e.quarantine ?? []).map(q => q.surfaceId))].sort().join(",");
|
|
144
|
+
return [identityKey(bundleIdentity(e.scope, e.name)), e.manifestHash, disabled, quarantined].join("\u0000");
|
|
145
|
+
})
|
|
146
|
+
.sort();
|
|
147
|
+
return sha256(parts.join("\n"));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ReconciledEnablement {
|
|
151
|
+
disabledSurfaceIds: string[];
|
|
152
|
+
quarantine: GjcPluginQuarantineEntry[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Carry persisted enablement intent across an update:
|
|
157
|
+
* - surviving disabled IDs stay disabled,
|
|
158
|
+
* - IDs whose surface disappeared are dropped,
|
|
159
|
+
* - new surface IDs are enabled by omission,
|
|
160
|
+
* - quarantine is recomputed from candidateQuarantine against the candidate's
|
|
161
|
+
* surface set; omitted input means no candidate quarantine is justified.
|
|
162
|
+
*/
|
|
163
|
+
export function reconcileEnablement(
|
|
164
|
+
previousDisabledSurfaceIds: readonly string[],
|
|
165
|
+
candidateSurfaceIds: readonly string[],
|
|
166
|
+
candidateQuarantine: readonly GjcPluginQuarantineEntry[] = [],
|
|
167
|
+
): ReconciledEnablement {
|
|
168
|
+
const surviving = new Set(candidateSurfaceIds);
|
|
169
|
+
const disabledSurfaceIds = [...new Set(previousDisabledSurfaceIds)].filter(id => surviving.has(id)).sort();
|
|
170
|
+
const seen = new Set<string>();
|
|
171
|
+
const quarantine = candidateQuarantine
|
|
172
|
+
.filter(q => {
|
|
173
|
+
if (!surviving.has(q.surfaceId)) return false;
|
|
174
|
+
if (seen.has(q.surfaceId)) return false;
|
|
175
|
+
seen.add(q.surfaceId);
|
|
176
|
+
return true;
|
|
177
|
+
})
|
|
178
|
+
.sort((a, b) => a.surfaceId.localeCompare(b.surfaceId));
|
|
179
|
+
return { disabledSurfaceIds, quarantine };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface SurfaceDelta {
|
|
183
|
+
addedSurfaceIds: string[];
|
|
184
|
+
removedSurfaceIds: string[];
|
|
185
|
+
retainedSurfaceIds: string[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function diffSurfaceIds(
|
|
189
|
+
currentSurfaceIds: readonly string[],
|
|
190
|
+
candidateSurfaceIds: readonly string[],
|
|
191
|
+
): SurfaceDelta {
|
|
192
|
+
const current = new Set(currentSurfaceIds);
|
|
193
|
+
const candidate = new Set(candidateSurfaceIds);
|
|
194
|
+
return {
|
|
195
|
+
addedSurfaceIds: [...candidate].filter(id => !current.has(id)).sort(),
|
|
196
|
+
removedSurfaceIds: [...current].filter(id => !candidate.has(id)).sort(),
|
|
197
|
+
retainedSurfaceIds: [...candidate].filter(id => current.has(id)).sort(),
|
|
198
|
+
};
|
|
199
|
+
}
|