@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
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import * as nodeFs from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { type GjcBundleTransactionDecision, resolveGjcBundleCandidate, runGjcBundleTransaction } from "./installer";
|
|
5
|
+
import {
|
|
6
|
+
activationFingerprint,
|
|
7
|
+
baselineFingerprint,
|
|
8
|
+
bundleIdentity,
|
|
9
|
+
candidateFingerprint,
|
|
10
|
+
decisionContextFingerprint,
|
|
11
|
+
diffSurfaceIds,
|
|
12
|
+
identityEquals,
|
|
13
|
+
reconcileEnablement,
|
|
14
|
+
surfaceIdsOf,
|
|
15
|
+
targetFingerprint,
|
|
16
|
+
} from "./lifecycle-reconciliation";
|
|
17
|
+
import { readRegistry, sortRegistryEntries, withRegistryLock, writeRegistryUnlocked } from "./registry";
|
|
18
|
+
import type {
|
|
19
|
+
GjcBundleIdentity,
|
|
20
|
+
GjcBundleSafeSource,
|
|
21
|
+
GjcBundleSummary,
|
|
22
|
+
GjcBundleSurfaceSummary,
|
|
23
|
+
GjcInstallResult,
|
|
24
|
+
GjcLifecycleError,
|
|
25
|
+
GjcLifecycleResult,
|
|
26
|
+
GjcPluginRegistryEntry,
|
|
27
|
+
GjcPluginRegistrySource,
|
|
28
|
+
GjcPluginScope,
|
|
29
|
+
GjcReviewedUpdateToken,
|
|
30
|
+
GjcToggleResult,
|
|
31
|
+
GjcUpdateApplyResult,
|
|
32
|
+
GjcUpdatePreview,
|
|
33
|
+
} from "./types";
|
|
34
|
+
import { GJC_PLUGIN_MANIFEST_FILENAME, GjcPluginLoadError } from "./types";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* GJC bundle lifecycle service.
|
|
38
|
+
*
|
|
39
|
+
* This module is the ONLY policy and persistence writer for GJC bundles: fresh
|
|
40
|
+
* install, update preview/apply, bundle enable/disable, and surface
|
|
41
|
+
* enable/disable. Callers (CLI, Settings) never touch the registry writers or
|
|
42
|
+
* the installer transaction directly.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export interface GjcLifecycleContext {
|
|
46
|
+
cwd: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fail(code: GjcLifecycleError["code"], message: string, recovery?: string): GjcLifecycleError {
|
|
50
|
+
return recovery ? { code, message, recovery } : { code, message };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const UNSUPPORTED_UPDATE_REASON: Partial<Record<GjcPluginRegistrySource["kind"], string>> = {};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Redact a stored locator to a display form: host + path only. No userinfo,
|
|
57
|
+
* query, fragment, credentials, or parent-directory segments reach output.
|
|
58
|
+
*/
|
|
59
|
+
const SAFE_LOCATOR_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
60
|
+
const SAFE_HOST = /^[A-Za-z0-9.-]+$/;
|
|
61
|
+
const SAFE_REF = /^[A-Za-z0-9._/-]{1,128}$/;
|
|
62
|
+
const SAFE_SHA = /^[A-Fa-f0-9]{1,128}$/;
|
|
63
|
+
|
|
64
|
+
function safePathSegments(value: string): string[] {
|
|
65
|
+
return value
|
|
66
|
+
.replace(/\\/g, "/")
|
|
67
|
+
.split("/")
|
|
68
|
+
.flatMap(segment => {
|
|
69
|
+
try {
|
|
70
|
+
return [decodeURIComponent(segment)];
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.filter(segment => segment !== "." && !segment.includes("..") && SAFE_LOCATOR_SEGMENT.test(segment));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function displayPath(segments: string[]): string {
|
|
79
|
+
return segments.join("/").replace(/\.git$/i, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function safeRef(value: string | undefined): string | undefined {
|
|
83
|
+
if (value === undefined || !SAFE_REF.test(value) || value.startsWith("/") || value.includes("..")) return undefined;
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function safeSha(value: string | undefined): string | undefined {
|
|
88
|
+
return value !== undefined && SAFE_SHA.test(value) ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function localPathDisplay(value: string, fallback: string): string {
|
|
92
|
+
const segments = safePathSegments(value);
|
|
93
|
+
return segments.at(-1) ?? fallback;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function redactSourceLocator(source: GjcPluginRegistrySource): string {
|
|
97
|
+
const unc = /^(?:\\\\|\/\/)(.+)$/.exec(source.uri);
|
|
98
|
+
if (unc) {
|
|
99
|
+
const [host, ...segments] = safePathSegments(unc[1] ?? "");
|
|
100
|
+
if (host && SAFE_HOST.test(host)) {
|
|
101
|
+
const safePath = displayPath(segments);
|
|
102
|
+
return safePath ? `${host}/${safePath}` : host;
|
|
103
|
+
}
|
|
104
|
+
return source.kind;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (/^[A-Za-z]:[\\/]/.test(source.uri)) return localPathDisplay(source.uri, source.kind);
|
|
108
|
+
if (source.kind === "path" || /^(?:\.{1,2}[\\/]|[\\/])/.test(source.uri)) {
|
|
109
|
+
return localPathDisplay(source.uri, source.kind);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(source.uri);
|
|
114
|
+
if (!SAFE_HOST.test(url.hostname)) return source.kind;
|
|
115
|
+
const safePath = displayPath(safePathSegments(url.pathname));
|
|
116
|
+
return safePath ? `${url.hostname}/${safePath}` : url.hostname;
|
|
117
|
+
} catch {
|
|
118
|
+
const scp = /^[^@/:]+@([A-Za-z0-9.-]+):(.+)$/.exec(source.uri);
|
|
119
|
+
if (scp) {
|
|
120
|
+
const safePath = displayPath(safePathSegments(scp[2] ?? ""));
|
|
121
|
+
return safePath ? `${scp[1]}/${safePath}` : (scp[1] ?? source.kind);
|
|
122
|
+
}
|
|
123
|
+
return source.kind;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toSafeSource(source: GjcPluginRegistrySource): GjcBundleSafeSource {
|
|
128
|
+
const unsupportedReason = UNSUPPORTED_UPDATE_REASON[source.kind];
|
|
129
|
+
const safe: GjcBundleSafeSource = {
|
|
130
|
+
kind: source.kind,
|
|
131
|
+
display: redactSourceLocator(source),
|
|
132
|
+
resolvedAt: source.resolvedAt,
|
|
133
|
+
updatable: unsupportedReason === undefined,
|
|
134
|
+
};
|
|
135
|
+
const ref = safeRef(source.ref);
|
|
136
|
+
const sha = safeSha(source.sha);
|
|
137
|
+
if (ref !== undefined) safe.ref = ref;
|
|
138
|
+
if (sha !== undefined) safe.sha = sha;
|
|
139
|
+
if (unsupportedReason !== undefined) safe.unsupportedReason = unsupportedReason;
|
|
140
|
+
return safe;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function surfaceSummaries(entry: GjcPluginRegistryEntry): GjcBundleSurfaceSummary[] {
|
|
144
|
+
const disabled = new Set(entry.disabledSurfaceIds);
|
|
145
|
+
const quarantined = new Map((entry.quarantine ?? []).map(q => [q.surfaceId, q.code]));
|
|
146
|
+
const rows: GjcBundleSurfaceSummary[] = [
|
|
147
|
+
...entry.surfaces.subskills.map(s => ({ extensionId: s.extensionId, kind: "subskill" as const, name: s.name })),
|
|
148
|
+
...entry.surfaces.tools.map(t => ({ extensionId: t.extensionId, kind: "tool" as const, name: t.name })),
|
|
149
|
+
...entry.surfaces.hooks.map(h => ({ extensionId: h.extensionId, kind: "hook" as const, name: h.name })),
|
|
150
|
+
...entry.surfaces.mcps.map(m => ({ extensionId: m.extensionId, kind: "mcp" as const, name: m.name })),
|
|
151
|
+
...entry.surfaces.systemAppendices.map(a => ({
|
|
152
|
+
extensionId: a.extensionId,
|
|
153
|
+
kind: "system-appendix" as const,
|
|
154
|
+
name: a.name,
|
|
155
|
+
})),
|
|
156
|
+
...entry.surfaces.agentAppendices.map(a => ({
|
|
157
|
+
extensionId: a.extensionId,
|
|
158
|
+
kind: "agent-appendix" as const,
|
|
159
|
+
name: a.name,
|
|
160
|
+
})),
|
|
161
|
+
].map(row => {
|
|
162
|
+
const code = quarantined.get(row.extensionId);
|
|
163
|
+
const summary: GjcBundleSurfaceSummary = {
|
|
164
|
+
...row,
|
|
165
|
+
enabled: !disabled.has(row.extensionId),
|
|
166
|
+
quarantined: code !== undefined,
|
|
167
|
+
};
|
|
168
|
+
if (code !== undefined) summary.quarantineCode = code;
|
|
169
|
+
return summary;
|
|
170
|
+
});
|
|
171
|
+
return rows.sort((a, b) => a.extensionId.localeCompare(b.extensionId));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Safe, redacted DTO for one installed bundle. */
|
|
175
|
+
export function toBundleSummary(entry: GjcPluginRegistryEntry): GjcBundleSummary {
|
|
176
|
+
const surfaces = surfaceSummaries(entry);
|
|
177
|
+
return {
|
|
178
|
+
identity: bundleIdentity(entry.scope, entry.name),
|
|
179
|
+
version: entry.version,
|
|
180
|
+
enabled: entry.enabled,
|
|
181
|
+
source: toSafeSource(entry.source),
|
|
182
|
+
installedAt: entry.installedAt,
|
|
183
|
+
updatedAt: entry.updatedAt,
|
|
184
|
+
manifestHash: entry.manifestHash,
|
|
185
|
+
targetFingerprint: targetFingerprint(entry),
|
|
186
|
+
surfaces,
|
|
187
|
+
quarantined: surfaces.some(s => s.quarantined),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Rebuild the exact locator an update must re-resolve from. A git ref is stored
|
|
193
|
+
* separately from the URI, so re-resolving the bare URI would silently drop the
|
|
194
|
+
* reviewed branch or tag and update from the default branch instead.
|
|
195
|
+
*/
|
|
196
|
+
function storedSourceLocator(source: GjcPluginRegistrySource): string {
|
|
197
|
+
return source.kind === "git" && source.ref ? `${source.uri}#${source.ref}` : source.uri;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Exposed for locator-reconstruction tests; not part of the lifecycle API. */
|
|
201
|
+
export const storedSourceLocatorForTest = storedSourceLocator;
|
|
202
|
+
|
|
203
|
+
/** Exposed so a test can pin parity with the installer's source predicates. */
|
|
204
|
+
export const isLocalDirectorySourceForTest = isLocalDirectorySource;
|
|
205
|
+
|
|
206
|
+
async function readEffective(cwd: string): Promise<GjcPluginRegistryEntry[]> {
|
|
207
|
+
const [user, project] = await Promise.all([readRegistry("user", cwd), readRegistry("project", cwd)]);
|
|
208
|
+
return sortRegistryEntries([...user.plugins, ...project.plugins]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** All installed bundles across both scopes, deterministically ordered. */
|
|
212
|
+
export async function listGjcBundles(ctx: GjcLifecycleContext): Promise<GjcBundleSummary[]> {
|
|
213
|
+
return (await readEffective(ctx.cwd)).map(toBundleSummary);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** One bundle by exact (scope, name) identity. Opposite scope never matches. */
|
|
217
|
+
export async function getGjcBundle(
|
|
218
|
+
ctx: GjcLifecycleContext,
|
|
219
|
+
identity: GjcBundleIdentity,
|
|
220
|
+
): Promise<GjcLifecycleResult<GjcBundleSummary>> {
|
|
221
|
+
const registry = await readRegistry(identity.scope, ctx.cwd);
|
|
222
|
+
const entry = registry.plugins.find(p => p.name === identity.name);
|
|
223
|
+
if (!entry) return { ok: false, error: notInstalled(identity) };
|
|
224
|
+
return { ok: true, value: toBundleSummary(entry) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function notInstalled(identity: GjcBundleIdentity): GjcLifecycleError {
|
|
228
|
+
return fail(
|
|
229
|
+
"not_installed",
|
|
230
|
+
`GJC bundle "${identity.name}" is not installed in the ${identity.scope} scope`,
|
|
231
|
+
`gjc plugin install <source> --${identity.scope}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function alreadyInstalled(name: string, scope: GjcPluginScope): GjcLifecycleError {
|
|
236
|
+
return fail(
|
|
237
|
+
"already_installed_use_upgrade",
|
|
238
|
+
`GJC bundle "${name}" is already installed in the ${scope} scope`,
|
|
239
|
+
`gjc plugin upgrade ${name} --${scope}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Run a source-resolving operation and convert a resolution failure into the
|
|
245
|
+
* typed `source_unavailable` result the lifecycle contract promises.
|
|
246
|
+
*
|
|
247
|
+
* Re-resolution reaches the network and the filesystem, so it can throw with a
|
|
248
|
+
* message carrying the stored locator. Letting that escape would both crash the
|
|
249
|
+
* CLI with an unhandled rejection and echo an absolute path, which is the exact
|
|
250
|
+
* leak class the safe DTOs exist to prevent.
|
|
251
|
+
*/
|
|
252
|
+
async function withSourceAvailability<T>(
|
|
253
|
+
identity: GjcBundleIdentity,
|
|
254
|
+
run: () => Promise<GjcLifecycleResult<T>>,
|
|
255
|
+
): Promise<GjcLifecycleResult<T>> {
|
|
256
|
+
try {
|
|
257
|
+
return await run();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// Only a source-resolution failure becomes `source_unavailable`. A
|
|
260
|
+
// programming bug, an out-of-memory, or a write/rollback fault must keep
|
|
261
|
+
// propagating: mislabelling those as an unreachable source would hide real
|
|
262
|
+
// failures behind a benign-looking, retryable error.
|
|
263
|
+
if (!(error instanceof GjcPluginLoadError)) throw error;
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
error: fail(
|
|
267
|
+
"source_unavailable",
|
|
268
|
+
`The stored source for GJC bundle "${identity.name}" could not be resolved`,
|
|
269
|
+
`gjc plugin install <source> --${identity.scope}`,
|
|
270
|
+
),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* True only for strings the installer would treat as a local path rather than
|
|
277
|
+
* a remote locator. This deliberately mirrors the installer's own `looksLikeGit`
|
|
278
|
+
* predicate: if the two ever disagreed, a remote locator could be read as a
|
|
279
|
+
* relative path and shadowed by a local directory of the same shape.
|
|
280
|
+
*/
|
|
281
|
+
function isLocalDirectorySource(source: string): boolean {
|
|
282
|
+
// Mirrors installer `looksLikeGit`.
|
|
283
|
+
if (/^(https?|ssh|git):\/\//i.test(source)) return false;
|
|
284
|
+
if (/^git@/.test(source)) return false;
|
|
285
|
+
if (source.startsWith("git:")) return false;
|
|
286
|
+
// Mirrors installer `isTarball`: a local archive is extracted, not read in
|
|
287
|
+
// place, so its manifest is not at `<source>/gajae-plugin.json`.
|
|
288
|
+
if (/\.(tgz|tar\.gz|tar)$/i.test(source)) return false;
|
|
289
|
+
// Any other scheme-qualified locator is likewise not a local directory.
|
|
290
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(source)) return false;
|
|
291
|
+
// scp-style `user@host:path` and bare `host:port/path` forms are remote.
|
|
292
|
+
if (/^[^@/\s]+@[^:/\s]+:/.test(source)) return false;
|
|
293
|
+
if (/^[^/\s:]+:\d+\//.test(source)) return false;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Largest manifest this preflight will read before giving up. */
|
|
298
|
+
const PREFLIGHT_MANIFEST_MAX_BYTES = 64 * 1024;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Bundle name declared by a local directory source, read without resolving or
|
|
302
|
+
* compiling it so a create-only refusal does not depend on the source being
|
|
303
|
+
* fetchable. Returns undefined for anything that is not a plain local
|
|
304
|
+
* directory, for symlinked or oversized manifests, and for unreadable or
|
|
305
|
+
* malformed content, in which case the caller falls through to the
|
|
306
|
+
* transaction's own pre-lock preflight.
|
|
307
|
+
*
|
|
308
|
+
* This can only cause a refusal, never a commit, and the locked decision
|
|
309
|
+
* re-derives the identity from the compiled bundle.
|
|
310
|
+
*/
|
|
311
|
+
async function declaredBundleName(source: string): Promise<string | undefined> {
|
|
312
|
+
// Classify the locator BEFORE touching the filesystem. On POSIX a remote
|
|
313
|
+
// locator like `https://host/repo` is also a valid relative path
|
|
314
|
+
// (`https:/host/repo`), so statting first would let a locally created
|
|
315
|
+
// directory shadow a remote source and refuse an install that should have
|
|
316
|
+
// resolved remotely.
|
|
317
|
+
if (!isLocalDirectorySource(source)) return undefined;
|
|
318
|
+
let handle: fs.FileHandle | undefined;
|
|
319
|
+
try {
|
|
320
|
+
const dir = await fs.stat(source);
|
|
321
|
+
if (!dir.isDirectory()) return undefined;
|
|
322
|
+
const manifestPath = path.join(source, GJC_PLUGIN_MANIFEST_FILENAME);
|
|
323
|
+
// Open without following a final symlink, then stat the OPEN handle so a
|
|
324
|
+
// concurrent rename cannot swap in a symlink or an oversized file between
|
|
325
|
+
// the check and the read.
|
|
326
|
+
handle = await fs.open(manifestPath, nodeFs.constants.O_RDONLY | nodeFs.constants.O_NOFOLLOW);
|
|
327
|
+
const manifest = await handle.stat();
|
|
328
|
+
if (!manifest.isFile() || manifest.size > PREFLIGHT_MANIFEST_MAX_BYTES) return undefined;
|
|
329
|
+
const parsed: unknown = JSON.parse(await handle.readFile("utf8"));
|
|
330
|
+
const name = (parsed as { name?: unknown }).name;
|
|
331
|
+
return typeof name === "string" && name.length > 0 ? name : undefined;
|
|
332
|
+
} catch {
|
|
333
|
+
return undefined;
|
|
334
|
+
} finally {
|
|
335
|
+
await handle?.close().catch(() => {});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Fresh install only. An existing target in the same scope is create-only and
|
|
341
|
+
* is refused identically with or without force; upgrading is a separate,
|
|
342
|
+
* scope-qualified operation.
|
|
343
|
+
*/
|
|
344
|
+
export async function installGjcBundle(
|
|
345
|
+
ctx: GjcLifecycleContext,
|
|
346
|
+
scope: GjcPluginScope,
|
|
347
|
+
source: string,
|
|
348
|
+
): Promise<GjcLifecycleResult<GjcInstallResult>> {
|
|
349
|
+
// A create-only refusal must not depend on the source being reachable, so
|
|
350
|
+
// identify the target before resolving anything. Only the declared manifest
|
|
351
|
+
// name can do that: it IS the canonical identity component.
|
|
352
|
+
//
|
|
353
|
+
// A stored-locator match deliberately does NOT qualify. One locator can
|
|
354
|
+
// resolve to different content over time, and the same URI can back two
|
|
355
|
+
// differently named bundles, so matching on it would refuse installs that
|
|
356
|
+
// should proceed. When the name cannot be read the transaction's own
|
|
357
|
+
// pre-lock preflight refuses after resolving.
|
|
358
|
+
const declared = await declaredBundleName(source);
|
|
359
|
+
if (declared) {
|
|
360
|
+
const registry = await readRegistry(scope, ctx.cwd);
|
|
361
|
+
const existing = registry.plugins.find(p => p.name === declared);
|
|
362
|
+
if (existing) return { ok: false, error: alreadyInstalled(existing.name, scope) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const result = await runGjcBundleTransaction(source, {
|
|
366
|
+
scope,
|
|
367
|
+
cwd: ctx.cwd,
|
|
368
|
+
decide: async ({ existing, candidate }): Promise<GjcBundleTransactionDecision> => {
|
|
369
|
+
if (existing) {
|
|
370
|
+
return {
|
|
371
|
+
kind: "abort",
|
|
372
|
+
error: fail(
|
|
373
|
+
"already_installed_use_upgrade",
|
|
374
|
+
`GJC bundle "${existing.name}" is already installed in the ${scope} scope`,
|
|
375
|
+
`gjc plugin upgrade ${existing.name} --${scope}`,
|
|
376
|
+
),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return { kind: "commit", entry: candidate };
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
if (result.status === "aborted") return { ok: false, error: result.error };
|
|
383
|
+
return { ok: true, value: { status: "installed", summary: toBundleSummary(result.entry) } };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Re-resolve the stored source descriptor and describe what an update would do.
|
|
388
|
+
* The returned token binds the candidate, the exact installed baseline, and the
|
|
389
|
+
* deterministic decision context; apply is a compare-and-swap on all three.
|
|
390
|
+
*/
|
|
391
|
+
export async function previewGjcBundleUpdate(
|
|
392
|
+
ctx: GjcLifecycleContext,
|
|
393
|
+
identity: GjcBundleIdentity,
|
|
394
|
+
): Promise<GjcLifecycleResult<GjcUpdatePreview>> {
|
|
395
|
+
const registry = await readRegistry(identity.scope, ctx.cwd);
|
|
396
|
+
const entry = registry.plugins.find(p => p.name === identity.name);
|
|
397
|
+
if (!entry) return { ok: false, error: notInstalled(identity) };
|
|
398
|
+
const safeSource = toSafeSource(entry.source);
|
|
399
|
+
if (!safeSource.updatable) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
error: fail(
|
|
403
|
+
"source_unsupported",
|
|
404
|
+
`GJC bundle "${identity.name}" was installed from a ${entry.source.kind} source that cannot be re-resolved`,
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const effective = await readEffective(ctx.cwd);
|
|
410
|
+
// Re-resolution reaches the network and the filesystem, so it can throw with
|
|
411
|
+
// a cause carrying the raw locator. Convert that into the typed
|
|
412
|
+
// `source_unavailable` result the contract promises, rather than letting an
|
|
413
|
+
// exception escape and echo the stored path.
|
|
414
|
+
return await withSourceAvailability(identity, async () =>
|
|
415
|
+
resolveGjcBundleCandidate(storedSourceLocator(entry.source), async ({ bundle }) => {
|
|
416
|
+
if (bundle.name !== entry.name) {
|
|
417
|
+
return {
|
|
418
|
+
ok: false as const,
|
|
419
|
+
error: fail(
|
|
420
|
+
"identity_mismatch",
|
|
421
|
+
`Source now declares "${bundle.name}" but "${entry.name}" is installed; install the new bundle and uninstall the old one`,
|
|
422
|
+
`gjc plugin install <source> --${identity.scope}`,
|
|
423
|
+
),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const candidateIds = surfaceIdsOf(bundle.surfaces);
|
|
427
|
+
const delta = diffSurfaceIds(surfaceIdsOf(entry.surfaces), candidateIds);
|
|
428
|
+
const candidateHash = candidateFingerprint(identity.scope, bundle);
|
|
429
|
+
const baselineHash = baselineFingerprint(entry);
|
|
430
|
+
const contextHash = decisionContextFingerprint(identity, effective);
|
|
431
|
+
const token: GjcReviewedUpdateToken = {
|
|
432
|
+
identity,
|
|
433
|
+
candidateFingerprint: candidateHash,
|
|
434
|
+
baselineFingerprint: baselineHash,
|
|
435
|
+
decisionContextFingerprint: contextHash,
|
|
436
|
+
reviewedAt: new Date().toISOString(),
|
|
437
|
+
};
|
|
438
|
+
return {
|
|
439
|
+
ok: true as const,
|
|
440
|
+
value: {
|
|
441
|
+
identity,
|
|
442
|
+
current: toBundleSummary(entry),
|
|
443
|
+
candidateVersion: bundle.version,
|
|
444
|
+
candidateManifestHash: bundle.manifestHash,
|
|
445
|
+
addedSurfaceIds: delta.addedSurfaceIds,
|
|
446
|
+
removedSurfaceIds: delta.removedSurfaceIds,
|
|
447
|
+
retainedSurfaceIds: delta.retainedSurfaceIds,
|
|
448
|
+
changed: candidateHash !== targetFingerprint(entry),
|
|
449
|
+
token,
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Apply a previously reviewed update. Any drift in the candidate bytes, the
|
|
458
|
+
* installed baseline, or the decision context returns a typed stale error with
|
|
459
|
+
* zero mutation.
|
|
460
|
+
*/
|
|
461
|
+
export async function applyGjcBundleUpdate(
|
|
462
|
+
ctx: GjcLifecycleContext,
|
|
463
|
+
token: GjcReviewedUpdateToken,
|
|
464
|
+
): Promise<GjcLifecycleResult<GjcUpdateApplyResult>> {
|
|
465
|
+
const identity = token.identity;
|
|
466
|
+
const registry = await readRegistry(identity.scope, ctx.cwd);
|
|
467
|
+
const entry = registry.plugins.find(p => p.name === identity.name);
|
|
468
|
+
if (!entry) return { ok: false, error: notInstalled(identity) };
|
|
469
|
+
if (!toSafeSource(entry.source).updatable) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
error: fail(
|
|
473
|
+
"source_unsupported",
|
|
474
|
+
`GJC bundle "${identity.name}" was installed from a ${entry.source.kind} source that cannot be re-resolved`,
|
|
475
|
+
),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return await withSourceAvailability<GjcUpdateApplyResult>(identity, async () => {
|
|
480
|
+
const result = await runGjcBundleTransaction(storedSourceLocator(entry.source), {
|
|
481
|
+
scope: identity.scope,
|
|
482
|
+
cwd: ctx.cwd,
|
|
483
|
+
decide: async ({ existing, effective, bundle, candidate }): Promise<GjcBundleTransactionDecision> => {
|
|
484
|
+
if (!existing) return { kind: "abort", error: notInstalled(identity) };
|
|
485
|
+
if (
|
|
486
|
+
bundle.name !== existing.name ||
|
|
487
|
+
!identityEquals(bundleIdentity(identity.scope, bundle.name), identity)
|
|
488
|
+
) {
|
|
489
|
+
return {
|
|
490
|
+
kind: "abort",
|
|
491
|
+
error: fail(
|
|
492
|
+
"identity_mismatch",
|
|
493
|
+
`Source now declares "${bundle.name}" but "${existing.name}" is installed; install the new bundle and uninstall the old one`,
|
|
494
|
+
`gjc plugin install <source> --${identity.scope}`,
|
|
495
|
+
),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const candidateHash = candidateFingerprint(identity.scope, bundle);
|
|
499
|
+
if (candidateHash !== token.candidateFingerprint) {
|
|
500
|
+
return {
|
|
501
|
+
kind: "abort",
|
|
502
|
+
error: fail("stale_candidate", "The source changed since it was reviewed; preview the update again"),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const baselineHash = baselineFingerprint(existing);
|
|
506
|
+
if (baselineHash !== token.baselineFingerprint) {
|
|
507
|
+
return {
|
|
508
|
+
kind: "abort",
|
|
509
|
+
error: fail(
|
|
510
|
+
"stale_baseline",
|
|
511
|
+
"The installed bundle changed since it was reviewed; preview the update again",
|
|
512
|
+
),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const contextHash = decisionContextFingerprint(identity, effective);
|
|
516
|
+
if (contextHash !== token.decisionContextFingerprint) {
|
|
517
|
+
return {
|
|
518
|
+
kind: "abort",
|
|
519
|
+
error: fail(
|
|
520
|
+
"stale_decision_context",
|
|
521
|
+
"Installed bundles changed since the update was reviewed; preview the update again",
|
|
522
|
+
),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
if (candidateHash === targetFingerprint(existing)) return { kind: "noop", entry: existing };
|
|
526
|
+
|
|
527
|
+
// Quarantine is recomputed against the candidate, never carried forward,
|
|
528
|
+
// so a surface the update fixes is not left permanently blocked.
|
|
529
|
+
const reconciled = reconcileEnablement(existing.disabledSurfaceIds, surfaceIdsOf(bundle.surfaces));
|
|
530
|
+
const next: GjcPluginRegistryEntry = {
|
|
531
|
+
...candidate,
|
|
532
|
+
enabled: existing.enabled,
|
|
533
|
+
installedAt: existing.installedAt,
|
|
534
|
+
disabledSurfaceIds: reconciled.disabledSurfaceIds,
|
|
535
|
+
};
|
|
536
|
+
if (reconciled.quarantine.length > 0) next.quarantine = reconciled.quarantine;
|
|
537
|
+
else delete next.quarantine;
|
|
538
|
+
return { kind: "commit", entry: next };
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
if (result.status === "aborted") return { ok: false, error: result.error };
|
|
543
|
+
if (result.status === "noop") {
|
|
544
|
+
return { ok: true, value: { status: "unchanged", summary: toBundleSummary(result.entry), remnantCount: 0 } };
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
ok: true,
|
|
548
|
+
value: { status: "updated", summary: toBundleSummary(result.entry), remnantCount: result.remnants.length },
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function mutateEntry(
|
|
554
|
+
ctx: GjcLifecycleContext,
|
|
555
|
+
identity: GjcBundleIdentity,
|
|
556
|
+
mutate: (entry: GjcPluginRegistryEntry) => GjcLifecycleResult<GjcPluginRegistryEntry | null>,
|
|
557
|
+
): Promise<GjcLifecycleResult<GjcToggleResult>> {
|
|
558
|
+
return await withRegistryLock(identity.scope, ctx.cwd, async () => {
|
|
559
|
+
const registry = await readRegistry(identity.scope, ctx.cwd);
|
|
560
|
+
const entry = registry.plugins.find(p => p.name === identity.name);
|
|
561
|
+
if (!entry) return { ok: false, error: notInstalled(identity) };
|
|
562
|
+
const outcome = mutate(entry);
|
|
563
|
+
if (!outcome.ok) return { ok: false, error: outcome.error };
|
|
564
|
+
if (outcome.value === null) return { ok: true, value: { summary: toBundleSummary(entry), mutated: false } };
|
|
565
|
+
const next = sortRegistryEntries([...registry.plugins.filter(p => p.name !== identity.name), outcome.value]);
|
|
566
|
+
await writeRegistryUnlocked({ version: 1, scope: identity.scope, plugins: next }, ctx.cwd);
|
|
567
|
+
return { ok: true, value: { summary: toBundleSummary(outcome.value), mutated: true } };
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Enable or disable a whole bundle. Deterministic quarantine blocks enabling;
|
|
573
|
+
* disabling is always allowed so operators can always de-escalate.
|
|
574
|
+
*/
|
|
575
|
+
export async function setGjcBundleEnabled(
|
|
576
|
+
ctx: GjcLifecycleContext,
|
|
577
|
+
identity: GjcBundleIdentity,
|
|
578
|
+
enabled: boolean,
|
|
579
|
+
): Promise<GjcLifecycleResult<GjcToggleResult>> {
|
|
580
|
+
return await mutateEntry(ctx, identity, entry => {
|
|
581
|
+
if (enabled && (entry.quarantine?.length ?? 0) > 0) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
error: fail("quarantined", `GJC bundle "${identity.name}" is quarantined and cannot be enabled`),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (entry.enabled === enabled) return { ok: true, value: null };
|
|
588
|
+
return { ok: true, value: { ...entry, enabled, updatedAt: new Date().toISOString() } };
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Enable or disable one surface of a bundle by its stable extension ID. */
|
|
593
|
+
export async function setGjcBundleSurfaceEnabled(
|
|
594
|
+
ctx: GjcLifecycleContext,
|
|
595
|
+
identity: GjcBundleIdentity,
|
|
596
|
+
surfaceId: string,
|
|
597
|
+
enabled: boolean,
|
|
598
|
+
): Promise<GjcLifecycleResult<GjcToggleResult>> {
|
|
599
|
+
return await mutateEntry(ctx, identity, entry => {
|
|
600
|
+
if (!surfaceIdsOf(entry.surfaces).includes(surfaceId)) {
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
error: fail("surface_unknown", `GJC bundle "${identity.name}" has no surface "${surfaceId}"`),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (enabled && (entry.quarantine ?? []).some(q => q.surfaceId === surfaceId)) {
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
error: fail("quarantined", `Surface "${surfaceId}" is quarantined and cannot be enabled`),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const disabled = new Set(entry.disabledSurfaceIds);
|
|
613
|
+
if (enabled ? !disabled.has(surfaceId) : disabled.has(surfaceId)) return { ok: true, value: null };
|
|
614
|
+
if (enabled) disabled.delete(surfaceId);
|
|
615
|
+
else disabled.add(surfaceId);
|
|
616
|
+
return {
|
|
617
|
+
ok: true,
|
|
618
|
+
value: { ...entry, disabledSurfaceIds: [...disabled].sort(), updatedAt: new Date().toISOString() },
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Deterministic activation generation for the current persisted state. */
|
|
624
|
+
export async function currentActivationFingerprint(ctx: GjcLifecycleContext): Promise<string> {
|
|
625
|
+
return activationFingerprint(await readEffective(ctx.cwd));
|
|
626
|
+
}
|
|
@@ -33,8 +33,8 @@ function statusFor(
|
|
|
33
33
|
quarantinedIds: Map<string, string>,
|
|
34
34
|
): { status: PluginSurfaceStatus; quarantineCode?: string } {
|
|
35
35
|
const q =
|
|
36
|
-
quarantinedIds.get(`${entry.name}\u0000${extensionId}`) ??
|
|
37
|
-
quarantinedIds.get(`${entry.name}\u0000plugin:${entry.name}`);
|
|
36
|
+
quarantinedIds.get(`${entry.scope}\u0000${entry.name}\u0000${extensionId}`) ??
|
|
37
|
+
quarantinedIds.get(`${entry.scope}\u0000${entry.name}\u0000plugin:${entry.name}`);
|
|
38
38
|
if (q) return { status: "quarantined", quarantineCode: q };
|
|
39
39
|
if (!entry.enabled || entry.disabledSurfaceIds.includes(extensionId)) return { status: "disabled" };
|
|
40
40
|
return { status: "enabled" };
|
|
@@ -76,7 +76,7 @@ export async function summarizeGjcPluginObservability(cwd: string): Promise<Plug
|
|
|
76
76
|
}
|
|
77
77
|
const { quarantine } = validateSessionBundles(effective, {}, preQuarantine);
|
|
78
78
|
const quarantinedIds = new Map<string, string>();
|
|
79
|
-
for (const q of quarantine) quarantinedIds.set(`${q.plugin}\u0000${q.surfaceId}`, q.code);
|
|
79
|
+
for (const q of quarantine) quarantinedIds.set(`${q.identity.scope}\u0000${q.plugin}\u0000${q.surfaceId}`, q.code);
|
|
80
80
|
|
|
81
81
|
const surfaces: PluginSurfaceRow[] = [];
|
|
82
82
|
for (const entry of effective) surfaces.push(...rowsForEntry(entry, quarantinedIds));
|
|
@@ -23,7 +23,11 @@ function sanitizeBody(text: string): string {
|
|
|
23
23
|
// Strip control chars (except tab/newline), then XML-escape &, <, > so a
|
|
24
24
|
// malicious body can NEVER emit a closing delimiter or fake <system>/
|
|
25
25
|
// <developer>/<gjc-subskill> tag that escapes the lower-authority block.
|
|
26
|
-
|
|
26
|
+
//
|
|
27
|
+
// The set spans C0, DEL, and C1. Carriage return can rewrite a rendered line,
|
|
28
|
+
// and U+009B is a single-byte CSI that introduces an escape sequence without
|
|
29
|
+
// any preceding ESC, so omitting either leaves the same injection open.
|
|
30
|
+
const stripped = text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
27
31
|
return stripped.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28
32
|
}
|
|
29
33
|
|