@gajae-code/coding-agent 0.7.3 → 0.7.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 +48 -0
- package/bin/gjc.js +4 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +20 -1
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -0
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +60 -0
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +10 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/utils/changelog.d.ts +1 -0
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +181 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +21 -4
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/model-profile-activation.ts +55 -7
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +3 -3
- package/src/defaults/gjc/skills/team/SKILL.md +5 -4
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/gjc-plugins/compiler.ts +351 -0
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +109 -0
- package/src/extensibility/gjc-plugins/installer.ts +434 -0
- package/src/extensibility/gjc-plugins/loader.ts +3 -1
- package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/gjc-plugins/observability.ts +84 -0
- package/src/extensibility/gjc-plugins/paths.ts +1 -1
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/gjc-plugins/registry.ts +180 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/gjc-plugins/schema.ts +250 -20
- package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
- package/src/extensibility/gjc-plugins/types.ts +199 -3
- package/src/extensibility/gjc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/gjc-runtime/launch-tmux.ts +61 -7
- package/src/gjc-runtime/psmux-detect.ts +239 -0
- package/src/gjc-runtime/team-runtime.ts +56 -23
- package/src/gjc-runtime/tmux-common.ts +27 -2
- package/src/gjc-runtime/tmux-sessions.ts +51 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/main.ts +14 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +44 -11
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +50 -11
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +228 -0
- package/src/notifications/lifecycle-control-runtime.ts +400 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +433 -8
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +18 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +166 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +678 -7
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/index.ts +3 -1
- package/src/utils/changelog.ts +8 -0
|
@@ -1,6 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
GJC_PLUGIN_KIND,
|
|
3
|
+
GJC_SUBSKILL_PARENT_AGENTS,
|
|
4
|
+
type GjcPluginAgentAppendixManifestEntry,
|
|
5
|
+
type GjcPluginAppendixManifestEntry,
|
|
6
|
+
type GjcPluginHookManifestEntry,
|
|
7
|
+
GjcPluginLoadError,
|
|
8
|
+
type GjcPluginManifest,
|
|
9
|
+
type GjcPluginMcpManifestEntry,
|
|
10
|
+
type GjcPluginMcpTransport,
|
|
11
|
+
type GjcPluginToolManifestEntry,
|
|
12
|
+
type GjcSubskillParentAgent,
|
|
13
|
+
type SubskillFrontmatter,
|
|
14
|
+
} from "./types";
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Top-level surfaces that may never appear in a GJC plugin bundle: bundles may
|
|
18
|
+
* only EXTEND existing skills/agents, never register new top-level ones.
|
|
19
|
+
*/
|
|
20
|
+
const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "agents"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ambiguous legacy aliases. `mcps` is the only canonical MCP key; these are
|
|
24
|
+
* rejected as `unsupported_surface` to avoid accidental legacy shape ambiguity.
|
|
25
|
+
*/
|
|
26
|
+
const UNSUPPORTED_ALIAS_KEYS = ["mcp", "mcpServers"];
|
|
27
|
+
|
|
28
|
+
const KNOWN_MANIFEST_KEYS = new Set([
|
|
29
|
+
"kind",
|
|
30
|
+
"name",
|
|
31
|
+
"version",
|
|
32
|
+
"subskills",
|
|
33
|
+
"tools",
|
|
34
|
+
"hooks",
|
|
35
|
+
"mcps",
|
|
36
|
+
"system_appendix",
|
|
37
|
+
"agent-appendix",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const MCP_TRANSPORTS: readonly GjcPluginMcpTransport[] = ["stdio", "http", "sse"];
|
|
4
41
|
|
|
5
42
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
43
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -16,14 +53,194 @@ function requireNonEmptyString(value: unknown, field: string, filePath: string):
|
|
|
16
53
|
return value;
|
|
17
54
|
}
|
|
18
55
|
|
|
19
|
-
function
|
|
56
|
+
function manifestString(value: unknown, field: string, manifestPath: string): string {
|
|
57
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
58
|
+
throw new GjcPluginLoadError(
|
|
59
|
+
"invalid_manifest",
|
|
60
|
+
`Invalid GJC plugin manifest at ${manifestPath}: ${field} must be a non-empty string`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function optionalStringArray(value: unknown, field: string, manifestPath: string): string[] {
|
|
67
|
+
if (value === undefined) return [];
|
|
20
68
|
if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
|
|
21
69
|
throw new GjcPluginLoadError(
|
|
22
70
|
"invalid_manifest",
|
|
23
71
|
`Invalid GJC plugin manifest at ${manifestPath}: ${field} must be a string array`,
|
|
24
72
|
);
|
|
25
73
|
}
|
|
26
|
-
return [...value];
|
|
74
|
+
return [...(value as string[])];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function optionalArray(value: unknown, field: string, manifestPath: string): unknown[] {
|
|
78
|
+
if (value === undefined) return [];
|
|
79
|
+
if (!Array.isArray(value)) {
|
|
80
|
+
throw new GjcPluginLoadError(
|
|
81
|
+
"invalid_manifest",
|
|
82
|
+
`Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an array`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deriveToolName(toolPath: string): string {
|
|
89
|
+
const base = toolPath.split("/").pop() ?? toolPath;
|
|
90
|
+
return base.replace(/\.[^.]+$/, "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseTools(value: unknown, manifestPath: string): GjcPluginToolManifestEntry[] {
|
|
94
|
+
const raw = optionalArray(value, "tools", manifestPath);
|
|
95
|
+
return raw.map((entry, index) => {
|
|
96
|
+
// Legacy string shorthand: subskill-scoped tool path only.
|
|
97
|
+
if (typeof entry === "string") {
|
|
98
|
+
if (entry.trim().length === 0) {
|
|
99
|
+
throw new GjcPluginLoadError(
|
|
100
|
+
"invalid_manifest",
|
|
101
|
+
`Invalid GJC plugin manifest at ${manifestPath}: tools[${index}] must be a non-empty path`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return { name: deriveToolName(entry), path: entry, surface: "subskill" };
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(entry)) {
|
|
107
|
+
throw new GjcPluginLoadError(
|
|
108
|
+
"invalid_manifest",
|
|
109
|
+
`Invalid GJC plugin manifest at ${manifestPath}: tools[${index}] must be a string or object`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const name = manifestString(entry.name, `tools[${index}].name`, manifestPath);
|
|
113
|
+
const path = manifestString(entry.path, `tools[${index}].path`, manifestPath);
|
|
114
|
+
const description =
|
|
115
|
+
entry.description === undefined
|
|
116
|
+
? undefined
|
|
117
|
+
: manifestString(entry.description, `tools[${index}].description`, manifestPath);
|
|
118
|
+
const sha256 =
|
|
119
|
+
entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `tools[${index}].sha256`, manifestPath);
|
|
120
|
+
return { name, path, description, sha256, surface: "always-on" };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseHooks(value: unknown, manifestPath: string): GjcPluginHookManifestEntry[] {
|
|
125
|
+
const raw = optionalArray(value, "hooks", manifestPath);
|
|
126
|
+
return raw.map((entry, index) => {
|
|
127
|
+
if (!isRecord(entry)) {
|
|
128
|
+
throw new GjcPluginLoadError(
|
|
129
|
+
"invalid_manifest",
|
|
130
|
+
`Invalid GJC plugin manifest at ${manifestPath}: hooks[${index}] must be an object`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const name = manifestString(entry.name, `hooks[${index}].name`, manifestPath);
|
|
134
|
+
const event = manifestString(entry.event, `hooks[${index}].event`, manifestPath);
|
|
135
|
+
const path = manifestString(entry.path, `hooks[${index}].path`, manifestPath);
|
|
136
|
+
const target =
|
|
137
|
+
entry.target === undefined ? undefined : manifestString(entry.target, `hooks[${index}].target`, manifestPath);
|
|
138
|
+
let phase: "before" | "after" | undefined;
|
|
139
|
+
if (entry.phase !== undefined) {
|
|
140
|
+
if (entry.phase !== "before" && entry.phase !== "after") {
|
|
141
|
+
throw new GjcPluginLoadError(
|
|
142
|
+
"invalid_manifest",
|
|
143
|
+
`Invalid GJC plugin manifest at ${manifestPath}: hooks[${index}].phase must be "before" or "after"`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
phase = entry.phase;
|
|
147
|
+
}
|
|
148
|
+
const sha256 =
|
|
149
|
+
entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `hooks[${index}].sha256`, manifestPath);
|
|
150
|
+
return { name, event, target, phase, path, sha256 };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseMcps(value: unknown, manifestPath: string): GjcPluginMcpManifestEntry[] {
|
|
155
|
+
const raw = optionalArray(value, "mcps", manifestPath);
|
|
156
|
+
return raw.map((entry, index) => {
|
|
157
|
+
if (!isRecord(entry)) {
|
|
158
|
+
throw new GjcPluginLoadError(
|
|
159
|
+
"invalid_manifest",
|
|
160
|
+
`Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}] must be an object`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const name = manifestString(entry.name, `mcps[${index}].name`, manifestPath);
|
|
164
|
+
const transport = entry.transport;
|
|
165
|
+
if (typeof transport !== "string" || !MCP_TRANSPORTS.includes(transport as GjcPluginMcpTransport)) {
|
|
166
|
+
throw new GjcPluginLoadError(
|
|
167
|
+
"invalid_manifest",
|
|
168
|
+
`Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}].transport must be one of ${MCP_TRANSPORTS.join(", ")}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const command =
|
|
172
|
+
entry.command === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: manifestString(entry.command, `mcps[${index}].command`, manifestPath);
|
|
175
|
+
const url = entry.url === undefined ? undefined : manifestString(entry.url, `mcps[${index}].url`, manifestPath);
|
|
176
|
+
const cwd = entry.cwd === undefined ? undefined : manifestString(entry.cwd, `mcps[${index}].cwd`, manifestPath);
|
|
177
|
+
let args: string[] | undefined;
|
|
178
|
+
if (entry.args !== undefined) {
|
|
179
|
+
if (!Array.isArray(entry.args) || !entry.args.every(item => typeof item === "string")) {
|
|
180
|
+
throw new GjcPluginLoadError(
|
|
181
|
+
"invalid_manifest",
|
|
182
|
+
`Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}].args must be a string array`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
args = [...(entry.args as string[])];
|
|
186
|
+
}
|
|
187
|
+
let headers: Record<string, string> | undefined;
|
|
188
|
+
if (entry.headers !== undefined) {
|
|
189
|
+
if (!isRecord(entry.headers) || !Object.values(entry.headers).every(v => typeof v === "string")) {
|
|
190
|
+
throw new GjcPluginLoadError(
|
|
191
|
+
"invalid_manifest",
|
|
192
|
+
`Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}].headers must be a string map`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
headers = { ...(entry.headers as Record<string, string>) };
|
|
196
|
+
}
|
|
197
|
+
const sha256 =
|
|
198
|
+
entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `mcps[${index}].sha256`, manifestPath);
|
|
199
|
+
return { name, transport: transport as GjcPluginMcpTransport, command, args, cwd, url, headers, sha256 };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseAppendixEntry(entry: unknown, field: string, manifestPath: string): GjcPluginAppendixManifestEntry {
|
|
204
|
+
if (!isRecord(entry)) {
|
|
205
|
+
throw new GjcPluginLoadError(
|
|
206
|
+
"invalid_manifest",
|
|
207
|
+
`Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an object`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const name = manifestString(entry.name, `${field}.name`, manifestPath);
|
|
211
|
+
const path = entry.path === undefined ? undefined : manifestString(entry.path, `${field}.path`, manifestPath);
|
|
212
|
+
// Content may be empty/whitespace here; the compiler enforces non-empty and
|
|
213
|
+
// maps emptiness to invalid_appendix (not invalid_manifest).
|
|
214
|
+
if (entry.content !== undefined && typeof entry.content !== "string") {
|
|
215
|
+
throw new GjcPluginLoadError(
|
|
216
|
+
"invalid_manifest",
|
|
217
|
+
`Invalid GJC plugin manifest at ${manifestPath}: ${field}.content must be a string`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const content = entry.content as string | undefined;
|
|
221
|
+
const sha256 =
|
|
222
|
+
entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `${field}.sha256`, manifestPath);
|
|
223
|
+
return { name, path, content, sha256 };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseSystemAppendix(value: unknown, manifestPath: string): GjcPluginAppendixManifestEntry[] {
|
|
227
|
+
const raw = optionalArray(value, "system_appendix", manifestPath);
|
|
228
|
+
return raw.map((entry, index) => parseAppendixEntry(entry, `system_appendix[${index}]`, manifestPath));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseAgentAppendix(value: unknown, manifestPath: string): GjcPluginAgentAppendixManifestEntry[] {
|
|
232
|
+
const raw = optionalArray(value, "agent-appendix", manifestPath);
|
|
233
|
+
return raw.map((entry, index) => {
|
|
234
|
+
const base = parseAppendixEntry(entry, `agent-appendix[${index}]`, manifestPath);
|
|
235
|
+
const agent = (entry as Record<string, unknown>).agent;
|
|
236
|
+
if (typeof agent !== "string" || !GJC_SUBSKILL_PARENT_AGENTS.includes(agent as GjcSubskillParentAgent)) {
|
|
237
|
+
throw new GjcPluginLoadError(
|
|
238
|
+
"invalid_parent",
|
|
239
|
+
`Invalid GJC plugin manifest at ${manifestPath}: agent-appendix[${index}].agent must be one of ${GJC_SUBSKILL_PARENT_AGENTS.join(", ")}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return { ...base, agent: agent as GjcSubskillParentAgent };
|
|
243
|
+
});
|
|
27
244
|
}
|
|
28
245
|
|
|
29
246
|
export function parseManifest(raw: unknown, manifestPath: string): GjcPluginManifest {
|
|
@@ -40,31 +257,44 @@ export function parseManifest(raw: unknown, manifestPath: string): GjcPluginMani
|
|
|
40
257
|
}
|
|
41
258
|
}
|
|
42
259
|
|
|
260
|
+
for (const key of UNSUPPORTED_ALIAS_KEYS) {
|
|
261
|
+
if (Object.hasOwn(raw, key)) {
|
|
262
|
+
throw new GjcPluginLoadError(
|
|
263
|
+
"unsupported_surface",
|
|
264
|
+
`Unsupported GJC plugin surface in ${manifestPath}: ${key} (use the canonical "mcps" key)`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const key of Object.keys(raw)) {
|
|
270
|
+
if (!KNOWN_MANIFEST_KEYS.has(key)) {
|
|
271
|
+
throw new GjcPluginLoadError(
|
|
272
|
+
"unsupported_surface",
|
|
273
|
+
`Unsupported GJC plugin surface in ${manifestPath}: ${key}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
43
278
|
if (raw.kind !== GJC_PLUGIN_KIND) {
|
|
44
279
|
throw new GjcPluginLoadError(
|
|
45
280
|
"invalid_kind",
|
|
46
281
|
`Invalid GJC plugin kind in ${manifestPath}: expected ${GJC_PLUGIN_KIND}`,
|
|
47
282
|
);
|
|
48
283
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`Invalid GJC plugin manifest at ${manifestPath}: name must be a non-empty string`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
if (typeof raw.version !== "string" || raw.version.trim().length === 0) {
|
|
56
|
-
throw new GjcPluginLoadError(
|
|
57
|
-
"invalid_manifest",
|
|
58
|
-
`Invalid GJC plugin manifest at ${manifestPath}: version must be a non-empty string`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
284
|
+
|
|
285
|
+
const name = manifestString(raw.name, "name", manifestPath);
|
|
286
|
+
const version = manifestString(raw.version, "version", manifestPath);
|
|
61
287
|
|
|
62
288
|
return {
|
|
63
|
-
name
|
|
64
|
-
version
|
|
289
|
+
name,
|
|
290
|
+
version,
|
|
65
291
|
kind: GJC_PLUGIN_KIND,
|
|
66
|
-
subskills:
|
|
67
|
-
tools:
|
|
292
|
+
subskills: optionalStringArray(raw.subskills, "subskills", manifestPath),
|
|
293
|
+
tools: parseTools(raw.tools, manifestPath),
|
|
294
|
+
hooks: parseHooks(raw.hooks, manifestPath),
|
|
295
|
+
mcps: parseMcps(raw.mcps, manifestPath),
|
|
296
|
+
systemAppendix: parseSystemAppendix(raw.system_appendix, manifestPath),
|
|
297
|
+
agentAppendix: parseAgentAppendix(raw["agent-appendix"], manifestPath),
|
|
68
298
|
};
|
|
69
299
|
}
|
|
70
300
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { GjcPluginLoadErrorCode, GjcPluginRegistryEntry } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Session-start validation: the registry is the collision authority. Capability
|
|
8
|
+
* provider output is supplied as EVIDENCE only; plugin surfaces are never
|
|
9
|
+
* resolved by capability first-wins. Offending surfaces are quarantined
|
|
10
|
+
* fail-closed with a stable error code rather than silently shadowed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface SessionCapabilityEvidence {
|
|
14
|
+
/** Built-in + provider tool names already present before plugin insertion. */
|
|
15
|
+
toolNames?: Iterable<string>;
|
|
16
|
+
/** Non-plugin MCP server names from providers/built-ins. */
|
|
17
|
+
mcpNames?: Iterable<string>;
|
|
18
|
+
/** Non-plugin hook keys (event:phase:target:name) from providers/built-ins. */
|
|
19
|
+
hookKeys?: Iterable<string>;
|
|
20
|
+
/** Existing appendix extension ids already present. */
|
|
21
|
+
appendixIds?: Iterable<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SessionQuarantine {
|
|
25
|
+
plugin: string;
|
|
26
|
+
surfaceId: string;
|
|
27
|
+
code: GjcPluginLoadErrorCode;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SessionValidationResult {
|
|
32
|
+
/** Registry entries (enabled, non-quarantined) whose surfaces may activate. */
|
|
33
|
+
active: GjcPluginRegistryEntry[];
|
|
34
|
+
/** Per-surface quarantine records (fail-closed). */
|
|
35
|
+
quarantine: SessionQuarantine[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(buf: Buffer): string {
|
|
39
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Re-verify that the installed files still match the registry's recorded hashes.
|
|
44
|
+
* Drift (manual edits, partial writes) quarantines the whole plugin with
|
|
45
|
+
* runtime_mismatch.
|
|
46
|
+
*/
|
|
47
|
+
export async function verifyEntryHashes(entry: GjcPluginRegistryEntry): Promise<SessionQuarantine | null> {
|
|
48
|
+
for (const file of entry.copiedFiles) {
|
|
49
|
+
const abs = path.join(entry.pluginRoot, file.relativePath);
|
|
50
|
+
let buf: Buffer;
|
|
51
|
+
try {
|
|
52
|
+
buf = await fs.readFile(abs);
|
|
53
|
+
} catch {
|
|
54
|
+
return {
|
|
55
|
+
plugin: entry.name,
|
|
56
|
+
surfaceId: `plugin:${entry.name}`,
|
|
57
|
+
code: "runtime_mismatch",
|
|
58
|
+
message: `Installed file missing: ${file.relativePath}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (sha256(buf) !== file.sha256) {
|
|
62
|
+
return {
|
|
63
|
+
plugin: entry.name,
|
|
64
|
+
surfaceId: `plugin:${entry.name}`,
|
|
65
|
+
code: "runtime_mismatch",
|
|
66
|
+
message: `Installed file hash drift: ${file.relativePath}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function activeSurfaceIds(entry: GjcPluginRegistryEntry): {
|
|
74
|
+
tools: { id: string; name: string }[];
|
|
75
|
+
mcps: { id: string; name: string }[];
|
|
76
|
+
hooks: { id: string }[];
|
|
77
|
+
appendices: { id: string }[];
|
|
78
|
+
} {
|
|
79
|
+
const disabled = new Set(entry.disabledSurfaceIds);
|
|
80
|
+
return {
|
|
81
|
+
tools: entry.surfaces.tools
|
|
82
|
+
.filter(t => !disabled.has(t.extensionId))
|
|
83
|
+
.map(t => ({ id: t.extensionId, name: t.name })),
|
|
84
|
+
mcps: entry.surfaces.mcps
|
|
85
|
+
.filter(m => !disabled.has(m.extensionId))
|
|
86
|
+
.map(m => ({ id: m.extensionId, name: m.name })),
|
|
87
|
+
hooks: entry.surfaces.hooks.filter(h => !disabled.has(h.extensionId)).map(h => ({ id: h.extensionId })),
|
|
88
|
+
appendices: [...entry.surfaces.systemAppendices, ...entry.surfaces.agentAppendices]
|
|
89
|
+
.filter(a => !disabled.has(a.extensionId))
|
|
90
|
+
.map(a => ({ id: a.extensionId })),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate the effective installed registry against the current capability
|
|
96
|
+
* universe (evidence) and across plugins. Returns the entries that may activate
|
|
97
|
+
* plus per-surface quarantine records. Disabled entries/surfaces are skipped
|
|
98
|
+
* (not an error).
|
|
99
|
+
*/
|
|
100
|
+
export function validateSessionBundles(
|
|
101
|
+
entries: readonly GjcPluginRegistryEntry[],
|
|
102
|
+
evidence: SessionCapabilityEvidence = {},
|
|
103
|
+
preQuarantined: readonly SessionQuarantine[] = [],
|
|
104
|
+
): SessionValidationResult {
|
|
105
|
+
const quarantine: SessionQuarantine[] = [...preQuarantined];
|
|
106
|
+
const quarantinedPlugins = new Set(preQuarantined.map(q => q.plugin));
|
|
107
|
+
|
|
108
|
+
const seenTools = new Set<string>(evidence.toolNames ?? []);
|
|
109
|
+
const seenMcps = new Set<string>(evidence.mcpNames ?? []);
|
|
110
|
+
const seenHooks = new Set<string>(evidence.hookKeys ?? []);
|
|
111
|
+
const seenAppendices = new Set<string>(evidence.appendixIds ?? []);
|
|
112
|
+
|
|
113
|
+
const active: GjcPluginRegistryEntry[] = [];
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (!entry.enabled) continue; // user-disabled, not an error
|
|
116
|
+
if (quarantinedPlugins.has(entry.name)) continue;
|
|
117
|
+
const surfaces = activeSurfaceIds(entry);
|
|
118
|
+
let collided = false;
|
|
119
|
+
const recordCollision = (surfaceId: string, what: string): void => {
|
|
120
|
+
collided = true;
|
|
121
|
+
quarantine.push({
|
|
122
|
+
plugin: entry.name,
|
|
123
|
+
surfaceId,
|
|
124
|
+
code: "session_collision",
|
|
125
|
+
message: `${what} collides with an existing capability/plugin; fail-closed (no shadowing)`,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
for (const t of surfaces.tools) if (seenTools.has(t.name)) recordCollision(t.id, `tool "${t.name}"`);
|
|
129
|
+
for (const m of surfaces.mcps) if (seenMcps.has(m.name)) recordCollision(m.id, `mcp "${m.name}"`);
|
|
130
|
+
for (const h of surfaces.hooks) if (seenHooks.has(h.id)) recordCollision(h.id, `hook "${h.id}"`);
|
|
131
|
+
for (const a of surfaces.appendices) if (seenAppendices.has(a.id)) recordCollision(a.id, `appendix "${a.id}"`);
|
|
132
|
+
|
|
133
|
+
if (collided) {
|
|
134
|
+
// Fail-closed for the whole plugin entry so partial activation cannot
|
|
135
|
+
// leave a half-applied bundle.
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Reserve this plugin's surfaces so a later plugin colliding with it is
|
|
139
|
+
// also caught (deterministic order => earlier plugin wins reservation).
|
|
140
|
+
for (const t of surfaces.tools) seenTools.add(t.name);
|
|
141
|
+
for (const m of surfaces.mcps) seenMcps.add(m.name);
|
|
142
|
+
for (const h of surfaces.hooks) seenHooks.add(h.id);
|
|
143
|
+
for (const a of surfaces.appendices) seenAppendices.add(a.id);
|
|
144
|
+
active.push(entry);
|
|
145
|
+
}
|
|
146
|
+
return { active, quarantine };
|
|
147
|
+
}
|
|
@@ -19,12 +19,62 @@ export const GJC_AGENT_SUBSKILL_PHASES: Record<GjcSubskillParentAgent, string[]>
|
|
|
19
19
|
critic: ["prompt"],
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
export interface GjcPluginToolManifestEntry {
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
sha256?: string;
|
|
27
|
+
/**
|
|
28
|
+
* "always-on" object entries are activated for the whole session; legacy
|
|
29
|
+
* string shorthand stays "subskill"-scoped and is only attached to subskill
|
|
30
|
+
* bindings (never registered as an always-on tool surface).
|
|
31
|
+
*/
|
|
32
|
+
surface: "subskill" | "always-on";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface GjcPluginHookManifestEntry {
|
|
36
|
+
name: string;
|
|
37
|
+
event: string;
|
|
38
|
+
target?: string;
|
|
39
|
+
phase?: "before" | "after";
|
|
40
|
+
path: string;
|
|
41
|
+
sha256?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type GjcPluginMcpTransport = "stdio" | "http" | "sse";
|
|
45
|
+
|
|
46
|
+
export interface GjcPluginMcpManifestEntry {
|
|
47
|
+
name: string;
|
|
48
|
+
transport: GjcPluginMcpTransport;
|
|
49
|
+
command?: string;
|
|
50
|
+
args?: string[];
|
|
51
|
+
cwd?: string;
|
|
52
|
+
url?: string;
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
sha256?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GjcPluginAppendixManifestEntry {
|
|
58
|
+
name: string;
|
|
59
|
+
path?: string;
|
|
60
|
+
content?: string;
|
|
61
|
+
sha256?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GjcPluginAgentAppendixManifestEntry extends GjcPluginAppendixManifestEntry {
|
|
65
|
+
agent: GjcSubskillParentAgent;
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
export interface GjcPluginManifest {
|
|
23
69
|
name: string;
|
|
24
70
|
version: string;
|
|
25
71
|
kind: "gajae-code-plugin";
|
|
26
72
|
subskills: string[];
|
|
27
|
-
tools:
|
|
73
|
+
tools: GjcPluginToolManifestEntry[];
|
|
74
|
+
hooks: GjcPluginHookManifestEntry[];
|
|
75
|
+
mcps: GjcPluginMcpManifestEntry[];
|
|
76
|
+
systemAppendix: GjcPluginAppendixManifestEntry[];
|
|
77
|
+
agentAppendix: GjcPluginAgentAppendixManifestEntry[];
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
export interface SubskillFrontmatter {
|
|
@@ -76,15 +126,33 @@ export interface LoadedGjcPlugin {
|
|
|
76
126
|
}
|
|
77
127
|
|
|
78
128
|
export type GjcPluginLoadErrorCode =
|
|
129
|
+
// Parse-time
|
|
79
130
|
| "forbidden_surface"
|
|
80
131
|
| "invalid_manifest"
|
|
132
|
+
| "invalid_kind"
|
|
133
|
+
| "unsupported_surface"
|
|
134
|
+
// Compile-time
|
|
81
135
|
| "invalid_frontmatter"
|
|
82
136
|
| "invalid_parent"
|
|
83
137
|
| "invalid_phase"
|
|
138
|
+
| "missing_file"
|
|
139
|
+
| "hash_mismatch"
|
|
140
|
+
| "invalid_appendix"
|
|
141
|
+
| "invalid_hook"
|
|
142
|
+
| "invalid_mcp"
|
|
143
|
+
// Install-time
|
|
84
144
|
| "duplicate_arg"
|
|
85
145
|
| "duplicate_parent_phase"
|
|
86
|
-
| "
|
|
87
|
-
| "
|
|
146
|
+
| "duplicate_tool"
|
|
147
|
+
| "duplicate_hook"
|
|
148
|
+
| "duplicate_mcp"
|
|
149
|
+
| "duplicate_appendix"
|
|
150
|
+
| "security_policy"
|
|
151
|
+
| "install_conflict"
|
|
152
|
+
// Session-start / runtime
|
|
153
|
+
| "session_collision"
|
|
154
|
+
| "runtime_mismatch"
|
|
155
|
+
| "quarantined_surface";
|
|
88
156
|
|
|
89
157
|
export class GjcPluginLoadError extends Error {
|
|
90
158
|
readonly code: GjcPluginLoadErrorCode;
|
|
@@ -95,3 +163,131 @@ export class GjcPluginLoadError extends Error {
|
|
|
95
163
|
this.code = code;
|
|
96
164
|
}
|
|
97
165
|
}
|
|
166
|
+
|
|
167
|
+
export type GjcPluginScope = "user" | "project";
|
|
168
|
+
|
|
169
|
+
export type GjcPluginSourceKind = "path" | "git" | "tarball";
|
|
170
|
+
|
|
171
|
+
export interface GjcPluginCopiedFile {
|
|
172
|
+
relativePath: string;
|
|
173
|
+
sha256: string;
|
|
174
|
+
bytes: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface NormalizedSubskillSurface {
|
|
178
|
+
extensionId: string;
|
|
179
|
+
name: string;
|
|
180
|
+
description: string;
|
|
181
|
+
parent: string;
|
|
182
|
+
phase: string;
|
|
183
|
+
activationArg: string;
|
|
184
|
+
relativePath: string;
|
|
185
|
+
sha256: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface NormalizedToolSurface {
|
|
189
|
+
extensionId: string;
|
|
190
|
+
name: string;
|
|
191
|
+
relativePath: string;
|
|
192
|
+
sha256: string;
|
|
193
|
+
description?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface NormalizedHookSurface {
|
|
197
|
+
extensionId: string;
|
|
198
|
+
name: string;
|
|
199
|
+
event: string;
|
|
200
|
+
target?: string;
|
|
201
|
+
phase?: "before" | "after";
|
|
202
|
+
relativePath: string;
|
|
203
|
+
sha256: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface NormalizedMcpSurface {
|
|
207
|
+
extensionId: string;
|
|
208
|
+
name: string;
|
|
209
|
+
transport: GjcPluginMcpTransport;
|
|
210
|
+
configHash: string;
|
|
211
|
+
config: GjcPluginMcpManifestEntry;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface NormalizedAppendixSurface {
|
|
215
|
+
extensionId: string;
|
|
216
|
+
name: string;
|
|
217
|
+
relativePath?: string;
|
|
218
|
+
/** Inline appendix body (when the manifest used `content` instead of `path`). */
|
|
219
|
+
content?: string;
|
|
220
|
+
contentHash: string;
|
|
221
|
+
bytes: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface NormalizedAgentAppendixSurface extends NormalizedAppendixSurface {
|
|
225
|
+
agent: GjcSubskillParentAgent;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface NormalizedGjcPluginSurfaces {
|
|
229
|
+
subskills: NormalizedSubskillSurface[];
|
|
230
|
+
tools: NormalizedToolSurface[];
|
|
231
|
+
hooks: NormalizedHookSurface[];
|
|
232
|
+
mcps: NormalizedMcpSurface[];
|
|
233
|
+
systemAppendices: NormalizedAppendixSurface[];
|
|
234
|
+
agentAppendices: NormalizedAgentAppendixSurface[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Result of the pure compile step. Computed from manifest, frontmatter, and
|
|
239
|
+
* declared files read as bytes only — never by importing plugin code.
|
|
240
|
+
*/
|
|
241
|
+
export interface NormalizedGjcPluginBundle {
|
|
242
|
+
name: string;
|
|
243
|
+
version: string;
|
|
244
|
+
root: string;
|
|
245
|
+
manifestPath: string;
|
|
246
|
+
manifestHash: string;
|
|
247
|
+
surfaces: NormalizedGjcPluginSurfaces;
|
|
248
|
+
files: GjcPluginCopiedFile[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface GjcPluginQuarantineEntry {
|
|
252
|
+
surfaceId: string;
|
|
253
|
+
code: GjcPluginLoadErrorCode;
|
|
254
|
+
message: string;
|
|
255
|
+
detectedAt: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface GjcPluginRegistrySource {
|
|
259
|
+
kind: GjcPluginSourceKind;
|
|
260
|
+
uri: string;
|
|
261
|
+
ref?: string;
|
|
262
|
+
sha?: string;
|
|
263
|
+
resolvedAt: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface GjcPluginRegistryEntry {
|
|
267
|
+
name: string;
|
|
268
|
+
version: string;
|
|
269
|
+
scope: GjcPluginScope;
|
|
270
|
+
enabled: boolean;
|
|
271
|
+
pluginRoot: string;
|
|
272
|
+
manifestPath: string;
|
|
273
|
+
manifestHash: string;
|
|
274
|
+
source: GjcPluginRegistrySource;
|
|
275
|
+
installedAt: string;
|
|
276
|
+
updatedAt: string;
|
|
277
|
+
copiedFiles: GjcPluginCopiedFile[];
|
|
278
|
+
surfaces: NormalizedGjcPluginSurfaces;
|
|
279
|
+
disabledSurfaceIds: string[];
|
|
280
|
+
quarantine?: GjcPluginQuarantineEntry[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface GjcPluginRegistry {
|
|
284
|
+
version: 1;
|
|
285
|
+
scope: GjcPluginScope;
|
|
286
|
+
plugins: GjcPluginRegistryEntry[];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Stable identifiers for plugin-contributed surfaces used by observability,
|
|
291
|
+
* disabledSurfaceIds, and quarantine bookkeeping.
|
|
292
|
+
*/
|
|
293
|
+
export type GjcPluginSurfaceExtensionId = string;
|