@gajae-code/coding-agent 0.16.3 → 0.16.6

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.
Files changed (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -1,3 +1,4 @@
1
+ import { redactCrashSecrets } from "@gajae-code/utils";
1
2
  import { identityEquals, identityKey } from "./lifecycle-reconciliation";
2
3
  import type { GjcBundleIdentity, GjcRuntimeFinding, GjcRuntimeSnapshot, GjcRuntimeSnapshotState } from "./types";
3
4
 
@@ -28,7 +29,11 @@ export class GjcRuntimeFindingAccumulator {
28
29
  constructor(readonly generation: number) {}
29
30
 
30
31
  add(finding: GjcRuntimeFinding): void {
31
- this.findings.push(finding);
32
+ this.findings.push({
33
+ ...finding,
34
+ message: redactCrashSecrets(finding.message.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?")).slice(0, 2048),
35
+ generation: this.generation,
36
+ });
32
37
  }
33
38
 
34
39
  addAll(findings: readonly GjcRuntimeFinding[]): void {
@@ -40,7 +45,20 @@ export class GjcRuntimeFindingAccumulator {
40
45
  const seen = new Set<string>();
41
46
  const unique: GjcRuntimeFinding[] = [];
42
47
  for (const finding of this.findings) {
43
- const key = [identityKey(finding.identity), finding.surfaceId, finding.code, finding.message].join("\u0000");
48
+ const key = [
49
+ identityKey(finding.identity),
50
+ finding.surfaceId,
51
+ finding.code,
52
+ finding.message,
53
+ finding.decision ?? "",
54
+ finding.event ?? "",
55
+ finding.phase ?? "",
56
+ JSON.stringify(finding.requestedCapabilities ?? []),
57
+ JSON.stringify(finding.effectiveCapabilities ?? []),
58
+ finding.capabilityHash ?? "",
59
+ JSON.stringify(finding.provenance ?? {}),
60
+ JSON.stringify(finding.audit ?? {}),
61
+ ].join("\u0000");
44
62
  if (seen.has(key)) continue;
45
63
  seen.add(key);
46
64
  unique.push(finding);
@@ -1,3 +1,8 @@
1
+ import {
2
+ FUNCTION_HOOK_CAPABILITIES,
3
+ type FunctionHookCapability,
4
+ normalizeFunctionHookGrant,
5
+ } from "../extensions/function-hooks";
1
6
  import {
2
7
  GJC_PLUGIN_KIND,
3
8
  GJC_SUBSKILL_PARENT_AGENTS,
@@ -94,6 +99,23 @@ function isRecord(value: unknown): value is Record<string, unknown> {
94
99
  return typeof value === "object" && value !== null && !Array.isArray(value);
95
100
  }
96
101
 
102
+ function rejectUnknownKeys(
103
+ value: Record<string, unknown>,
104
+ allowed: readonly string[],
105
+ context: string,
106
+ manifestPath: string,
107
+ ): void {
108
+ const allowedSet = new Set(allowed);
109
+ for (const key of Object.keys(value)) {
110
+ if (!allowedSet.has(key)) {
111
+ throw new GjcPluginLoadError(
112
+ "unsupported_surface",
113
+ `Unsupported GJC plugin field "${key}" at ${context} (${manifestPath}); supported fields are ${[...allowed].sort().join(", ")}`,
114
+ );
115
+ }
116
+ }
117
+ }
118
+
97
119
  function requireNonEmptyString(value: unknown, field: string, filePath: string): string {
98
120
  if (typeof value !== "string" || value.trim().length === 0) {
99
121
  throw new GjcPluginLoadError(
@@ -221,6 +243,23 @@ function parseTools(value: unknown, manifestPath: string): GjcPluginToolManifest
221
243
  `Invalid GJC plugin manifest at ${manifestPath}: tools[${index}] must be a string or object`,
222
244
  );
223
245
  }
246
+ rejectUnknownKeys(
247
+ entry,
248
+ [
249
+ "name",
250
+ "path",
251
+ "description",
252
+ "sha256",
253
+ "schemaPath",
254
+ "schema_path",
255
+ "schema",
256
+ "inputSchema",
257
+ "input_schema",
258
+ "parameters",
259
+ ],
260
+ `tools[${index}]`,
261
+ manifestPath,
262
+ );
224
263
  const name = manifestSafeName(entry.name, `tools[${index}].name`, manifestPath);
225
264
  const path = manifestString(entry.path, `tools[${index}].path`, manifestPath);
226
265
  const description =
@@ -258,6 +297,25 @@ function parseHooks(value: unknown, manifestPath: string): GjcPluginHookManifest
258
297
  `Invalid GJC plugin manifest at ${manifestPath}: hooks[${index}] must be an object`,
259
298
  );
260
299
  }
300
+ const allowedKeys = new Set([
301
+ "name",
302
+ "event",
303
+ "target",
304
+ "phase",
305
+ "path",
306
+ "sha256",
307
+ "capabilities",
308
+ "networkDestinations",
309
+ "filesystemRoots",
310
+ ]);
311
+ for (const key of Object.keys(entry)) {
312
+ if (!allowedKeys.has(key)) {
313
+ throw new GjcPluginLoadError(
314
+ "unsupported_surface",
315
+ `Unsupported GJC plugin hook field "${key}" at ${manifestPath}; supported fields are ${[...allowedKeys].sort().join(", ")}`,
316
+ );
317
+ }
318
+ }
261
319
  const name = manifestSafeName(entry.name, `hooks[${index}].name`, manifestPath);
262
320
  // event/target become part of the hook surface ID
263
321
  // (`hook:<event>:<phase>:<target>:<name>`), which is rendered and printed.
@@ -266,7 +324,9 @@ function parseHooks(value: unknown, manifestPath: string): GjcPluginHookManifest
266
324
  const target =
267
325
  entry.target === undefined
268
326
  ? undefined
269
- : manifestSafeName(entry.target, `hooks[${index}].target`, manifestPath);
327
+ : entry.target === "*"
328
+ ? "*"
329
+ : manifestSafeName(entry.target, `hooks[${index}].target`, manifestPath);
270
330
  let phase: "before" | "after" | undefined;
271
331
  if (entry.phase !== undefined) {
272
332
  if (entry.phase !== "before" && entry.phase !== "after") {
@@ -279,7 +339,41 @@ function parseHooks(value: unknown, manifestPath: string): GjcPluginHookManifest
279
339
  }
280
340
  const sha256 =
281
341
  entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `hooks[${index}].sha256`, manifestPath);
282
- return { name, event, target, phase, path, sha256 };
342
+ let capabilities: FunctionHookCapability[] | undefined;
343
+ if (entry.capabilities !== undefined) {
344
+ if (
345
+ !Array.isArray(entry.capabilities) ||
346
+ !entry.capabilities.every(
347
+ capability =>
348
+ typeof capability === "string" &&
349
+ FUNCTION_HOOK_CAPABILITIES.includes(capability as FunctionHookCapability),
350
+ )
351
+ ) {
352
+ throw new GjcPluginLoadError(
353
+ "unsupported_surface",
354
+ `Invalid GJC plugin hook capabilities at ${manifestPath}: use only ${FUNCTION_HOOK_CAPABILITIES.join(", ")}`,
355
+ );
356
+ }
357
+ capabilities = [...(entry.capabilities as FunctionHookCapability[])];
358
+ }
359
+ const parseStringArray = (field: "networkDestinations" | "filesystemRoots"): string[] | undefined => {
360
+ const value = entry[field];
361
+ if (value === undefined) return undefined;
362
+ if (!Array.isArray(value) || !value.every(item => typeof item === "string"))
363
+ throw new GjcPluginLoadError(
364
+ "invalid_manifest",
365
+ `Invalid ${field} at ${manifestPath}: expected a string array`,
366
+ );
367
+ return [...(value as string[])];
368
+ };
369
+ const networkDestinations = parseStringArray("networkDestinations");
370
+ const filesystemRoots = parseStringArray("filesystemRoots");
371
+ try {
372
+ normalizeFunctionHookGrant({ capabilities, networkDestinations, filesystemRoots });
373
+ } catch (error) {
374
+ throw new GjcPluginLoadError("invalid_manifest", error instanceof Error ? error.message : String(error));
375
+ }
376
+ return { name, event, target, phase, path, sha256, capabilities, networkDestinations, filesystemRoots };
283
377
  });
284
378
  }
285
379
 
@@ -290,6 +384,12 @@ function parseMcpEntry(entry: unknown, field: string, manifestPath: string): Gjc
290
384
  `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an object`,
291
385
  );
292
386
  }
387
+ rejectUnknownKeys(
388
+ entry,
389
+ ["name", "transport", "command", "args", "cwd", "url", "headers", "sha256"],
390
+ field,
391
+ manifestPath,
392
+ );
293
393
  const name = manifestSafeName(entry.name, `${field}.name`, manifestPath);
294
394
  const transport = entry.transport;
295
395
  if (typeof transport !== "string" || !MCP_TRANSPORTS.includes(transport as GjcPluginMcpTransport)) {
@@ -430,13 +530,24 @@ function pickOptional(entry: Record<string, unknown>, keys: readonly string[]):
430
530
  return out;
431
531
  }
432
532
 
433
- function parseAppendixEntry(entry: unknown, field: string, manifestPath: string): GjcPluginAppendixManifestEntry {
533
+ function parseAppendixEntry(
534
+ entry: unknown,
535
+ field: string,
536
+ manifestPath: string,
537
+ allowAgent = false,
538
+ ): GjcPluginAppendixManifestEntry {
434
539
  if (!isRecord(entry)) {
435
540
  throw new GjcPluginLoadError(
436
541
  "invalid_manifest",
437
542
  `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an object`,
438
543
  );
439
544
  }
545
+ rejectUnknownKeys(
546
+ entry,
547
+ allowAgent ? ["name", "path", "content", "sha256", "agent"] : ["name", "path", "content", "sha256"],
548
+ field,
549
+ manifestPath,
550
+ );
440
551
  const name = manifestSafeName(entry.name, `${field}.name`, manifestPath);
441
552
  const path = entry.path === undefined ? undefined : manifestString(entry.path, `${field}.path`, manifestPath);
442
553
  // Content may be empty/whitespace here; the compiler enforces non-empty and
@@ -461,7 +572,7 @@ function parseSystemAppendix(value: unknown, manifestPath: string): GjcPluginApp
461
572
  function parseAgentAppendix(value: unknown, manifestPath: string): GjcPluginAgentAppendixManifestEntry[] {
462
573
  const raw = optionalArray(value, "agent-appendix", manifestPath);
463
574
  return raw.map((entry, index) => {
464
- const base = parseAppendixEntry(entry, `agent-appendix[${index}]`, manifestPath);
575
+ const base = parseAppendixEntry(entry, `agent-appendix[${index}]`, manifestPath, true);
465
576
  const agent = (entry as Record<string, unknown>).agent;
466
577
  if (typeof agent !== "string" || !GJC_SUBSKILL_PARENT_AGENTS.includes(agent as GjcSubskillParentAgent)) {
467
578
  throw new GjcPluginLoadError(
@@ -553,6 +664,12 @@ export function parseManifest(raw: unknown, manifestPath: string): GjcPluginMani
553
664
  }
554
665
 
555
666
  export function parseSubskillFrontmatter(fm: Record<string, unknown>, filePath: string): SubskillFrontmatter {
667
+ rejectUnknownKeys(
668
+ fm,
669
+ ["name", "binds_to", "phase", "activation_arg", "description", "tools"],
670
+ "frontmatter",
671
+ filePath,
672
+ );
556
673
  return {
557
674
  // Name and activation_arg become part of the surface ID
558
675
  // (`subskill:<parent>:<phase>:<arg>`) and are rendered, so they share the
@@ -1,5 +1,6 @@
1
1
  import type { CanonicalGjcWorkflowSkill } from "../../skill-state/active-state";
2
2
  import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../../skill-state/active-state";
3
+ import type { FunctionHookCapability } from "../extensions/function-hooks";
3
4
 
4
5
  export const GJC_PLUGIN_MANIFEST_FILENAME = "gajae-plugin.json";
5
6
  export const GJC_PLUGIN_KIND = "gajae-code-plugin";
@@ -48,6 +49,10 @@ export interface GjcPluginHookManifestEntry {
48
49
  phase?: "before" | "after";
49
50
  path: string;
50
51
  sha256?: string;
52
+ /** Requested capability families; the runtime never grants more than declared. */
53
+ capabilities?: FunctionHookCapability[];
54
+ networkDestinations?: string[];
55
+ filesystemRoots?: string[];
51
56
  }
52
57
 
53
58
  export type GjcPluginMcpTransport = "stdio" | "http" | "sse";
@@ -290,6 +295,11 @@ export interface NormalizedHookSurface {
290
295
  relativePath: string;
291
296
  sha256: string;
292
297
  implementationHash?: string;
298
+ capabilities?: FunctionHookCapability[];
299
+ networkDestinations?: string[];
300
+ filesystemRoots?: string[];
301
+ capabilityHash?: string;
302
+ functionHook?: boolean;
293
303
  }
294
304
 
295
305
  export interface NormalizedMcpSurface {
@@ -522,6 +532,21 @@ export interface GjcRuntimeFinding {
522
532
  surfaceId: string;
523
533
  code: GjcPluginLoadErrorCode;
524
534
  message: string;
535
+ decision?: "loaded" | "quarantined" | "blocked" | "error";
536
+ event?: string;
537
+ phase?: "before" | "after";
538
+ requestedCapabilities?: readonly FunctionHookCapability[];
539
+ effectiveCapabilities?: readonly FunctionHookCapability[];
540
+ capabilityHash?: string;
541
+ provenance?: {
542
+ source: "builtin" | "user" | "project" | "extension" | "plugin-bundle";
543
+ scope?: "user" | "project" | "native";
544
+ plugin?: string;
545
+ path?: string;
546
+ extensionId?: string;
547
+ };
548
+ audit?: Record<string, unknown>;
549
+ generation?: number;
525
550
  }
526
551
 
527
552
  export interface GjcRuntimeSnapshot {
@@ -11,6 +11,24 @@ import { HookSourceConvention } from "../../hooks/events";
11
11
  import { type NormalizedHook, normalizeDirectoryHook } from "../../hooks/normalize";
12
12
  import type { HookMessage } from "../../session/messages";
13
13
  import type { SessionManager } from "../../session/session-manager";
14
+ import {
15
+ DEFAULT_EXTENSION_FUNCTION_HOOK_GRANT,
16
+ type FunctionHook,
17
+ type FunctionHookEventType,
18
+ type FunctionHookPayloadFor,
19
+ type FunctionHookRegistration,
20
+ type FunctionHookRegistrationOptions,
21
+ intersectFunctionHookGrants,
22
+ normalizeFunctionHookGrant,
23
+ validateFunctionHookTarget,
24
+ } from "../extensions/function-hooks";
25
+ import {
26
+ getExtensionHandlerRegistrationOrder,
27
+ getFunctionHookRegistration,
28
+ tagExtensionHandlerRegistrationOrder,
29
+ tagFunctionHookHandler,
30
+ wrapExtensionHandlerRegistration,
31
+ } from "../extensions/function-hooks-internal";
14
32
  import type {
15
33
  ExtensionAPI,
16
34
  ExtensionCommandContext,
@@ -114,6 +132,7 @@ export interface LoadHookExtensionsResult {
114
132
  async function createHookAPI(
115
133
  handlers: Map<string, HandlerFn[]>,
116
134
  cwd: string,
135
+ sourcePath: string,
117
136
  ): Promise<{
118
137
  api: HookAPI;
119
138
  messageRenderers: Map<string, HookMessageRenderer>;
@@ -125,6 +144,7 @@ async function createHookAPI(
125
144
  let appendEntryHandler: AppendEntryHandler | null = null;
126
145
  const messageRenderers = new Map<string, HookMessageRenderer>();
127
146
  const commands = new Map<string, RegisteredCommand>();
147
+ let handlerRegistrationOrder = 0;
128
148
 
129
149
  // Cast to HookAPI - the implementation is more general (string event names)
130
150
  // but the interface has specific overloads for type safety in hooks
@@ -133,7 +153,7 @@ async function createHookAPI(
133
153
  if (!handlers.has(event)) {
134
154
  handlers.set(event, []);
135
155
  }
136
- handlers.get(event)!.push(handler);
156
+ handlers.get(event)!.push(wrapExtensionHandlerRegistration(handler, handlerRegistrationOrder++));
137
157
  },
138
158
  sendMessage<T = unknown>(
139
159
  message: HookMessage<T>,
@@ -156,6 +176,39 @@ async function createHookAPI(
156
176
  registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void {
157
177
  commands.set(name, { name, ...options });
158
178
  },
179
+ registerFunctionHook<T extends FunctionHookEventType>(
180
+ event: T,
181
+ handler: FunctionHook<FunctionHookPayloadFor<T>>,
182
+ options: FunctionHookRegistrationOptions = {},
183
+ ): void {
184
+ if (options.target !== undefined) {
185
+ if (event === "*") throw new Error("Wildcard function hooks cannot declare a target");
186
+ if (event !== "tool_call" && event !== "tool_result")
187
+ throw new Error("Function hook targets are only valid for tool_call and tool_result events");
188
+ if (options.target !== "*") validateFunctionHookTarget(options.target);
189
+ }
190
+ const registrationOrder = handlerRegistrationOrder++;
191
+ const registration: FunctionHookRegistration = {
192
+ event,
193
+ ...(options.target === undefined ? {} : { target: options.target }),
194
+ ...(options.registrationId === undefined ? {} : { registrationId: options.registrationId }),
195
+ registrationOrder,
196
+ handler: handler as unknown as FunctionHook,
197
+ grant: intersectFunctionHookGrants(
198
+ normalizeFunctionHookGrant(options),
199
+ DEFAULT_EXTENSION_FUNCTION_HOOK_GRANT,
200
+ ),
201
+ provenance: { source: "extension", path: sourcePath, extensionId: sourcePath },
202
+ };
203
+ const list = handlers.get(event) ?? [];
204
+ list.push(
205
+ tagExtensionHandlerRegistrationOrder(
206
+ tagFunctionHookHandler(registration) as unknown as HandlerFn,
207
+ registrationOrder,
208
+ ),
209
+ );
210
+ handlers.set(event, list);
211
+ },
159
212
  exec(command: string, args: string[], options?: ExecOptions) {
160
213
  return execCommand(command, args, options?.cwd ?? cwd, options);
161
214
  },
@@ -198,6 +251,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
198
251
  const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = await createHookAPI(
199
252
  handlers,
200
253
  cwd,
254
+ resolvedPath,
201
255
  );
202
256
 
203
257
  // Call factory to register handlers
@@ -258,6 +312,27 @@ export async function discoverAndLoadHooks(configuredPaths: string[], cwd: strin
258
312
  const seen = new Set<string>();
259
313
  const normalizationErrors: Array<{ path: string; error: string }> = [];
260
314
  const normalizationByPath = new Map<string, NormalizedHook>();
315
+ const normalizeExplicitPath = (inputPath: string): NormalizedHook | undefined => {
316
+ const resolved = resolvePath(inputPath, cwd);
317
+ const phase = path.basename(path.dirname(resolved));
318
+ if (phase !== "pre" && phase !== "post") return undefined;
319
+ const fileName = path.basename(resolved);
320
+ const toolName = fileName.includes(".") ? fileName.slice(0, fileName.lastIndexOf(".")) : fileName;
321
+ const normalized = normalizeDirectoryHook({
322
+ convention: HookSourceConvention.NativeGjc,
323
+ phase,
324
+ toolName: toolName === "*" ? "*" : toolName,
325
+ source: resolved,
326
+ externalName: fileName,
327
+ });
328
+ if (!normalized.hook) {
329
+ normalizationErrors.push({
330
+ path: resolved,
331
+ error: normalized.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join("; "),
332
+ });
333
+ }
334
+ return normalized.hook ?? undefined;
335
+ };
261
336
 
262
337
  // Helper to add paths without duplicates
263
338
  const addPaths = (paths: string[]) => {
@@ -297,13 +372,18 @@ export async function discoverAndLoadHooks(configuredPaths: string[], cwd: strin
297
372
  });
298
373
  continue;
299
374
  }
300
- normalizationByPath.set(path.resolve(hook.path), normalized.hook);
375
+ normalizationByPath.set(resolvePath(hook.path, cwd), normalized.hook);
301
376
  }
302
377
  addPaths([hook.path]);
303
378
  }
304
379
 
305
380
  // 2. Explicitly configured paths (can override/add)
306
- addPaths(configuredPaths.map(p => resolvePath(p, cwd)));
381
+ for (const configuredPath of configuredPaths) {
382
+ const resolved = resolvePath(configuredPath, cwd);
383
+ const normalized = normalizeExplicitPath(resolved);
384
+ if (normalized) normalizationByPath.set(resolved, normalized);
385
+ addPaths([resolved]);
386
+ }
307
387
 
308
388
  const loaded = await loadHooks(allPaths, cwd);
309
389
  for (const hook of loaded.hooks) {
@@ -352,23 +432,60 @@ function createHookExtensionFactory(hook: LoadedHook): ExtensionFactory {
352
432
  hook.setSendMessageHandler((message, options) => api.sendMessage(message, options));
353
433
  hook.setAppendEntryHandler((customType, data) => api.appendEntry(customType, data));
354
434
 
435
+ const orderedHandlers: Array<{ event: string; handler: HandlerFn; order: number; fallback: number }> = [];
436
+ let fallback = 0;
355
437
  for (const [event, handlers] of hook.handlers) {
356
438
  for (const handler of handlers) {
439
+ orderedHandlers.push({
440
+ event,
441
+ handler,
442
+ order:
443
+ getExtensionHandlerRegistrationOrder(handler) ??
444
+ getFunctionHookRegistration(handler)?.registrationOrder ??
445
+ Number.MAX_SAFE_INTEGER,
446
+ fallback: fallback++,
447
+ });
448
+ }
449
+ }
450
+ orderedHandlers.sort((a, b) => a.order - b.order || a.fallback - b.fallback);
451
+
452
+ for (const { event, handler } of orderedHandlers) {
453
+ const functionRegistration = getFunctionHookRegistration(handler);
454
+ if (functionRegistration) {
357
455
  const normalized = hook.normalization;
358
- const adapted: HandlerFn = async (...args: unknown[]) => {
359
- const payload = args[0] as { toolName?: string };
360
- if (
361
- normalized &&
362
- event === normalized.runtimeEvent &&
363
- normalized.toolName !== "*" &&
364
- payload.toolName !== normalized.toolName
365
- ) {
366
- return undefined;
367
- }
368
- return await handler(payload, toHookContext(args[1] as ExtensionContext));
369
- };
370
- (api.on as (event: string, handler: HandlerFn) => void)(event, adapted);
456
+ const normalizedTarget = normalized && normalized.toolName !== "*" ? normalized.toolName : undefined;
457
+ if (
458
+ normalized &&
459
+ (functionRegistration.event !== normalized.runtimeEvent ||
460
+ (functionRegistration.target !== undefined && functionRegistration.target !== normalizedTarget))
461
+ ) {
462
+ throw new Error("Function hook registration conflicts with its normalized discovery target");
463
+ }
464
+ api.registerFunctionHook(functionRegistration.event, functionRegistration.handler, {
465
+ ...(normalizedTarget === undefined && functionRegistration.target === undefined
466
+ ? {}
467
+ : { target: normalizedTarget ?? functionRegistration.target }),
468
+ ...(functionRegistration.registrationId === undefined
469
+ ? {}
470
+ : { registrationId: functionRegistration.registrationId }),
471
+ ...functionRegistration.grant,
472
+ });
473
+ continue;
371
474
  }
475
+ const normalized = hook.normalization;
476
+ const adapted: HandlerFn = async (...args: unknown[]) => {
477
+ const payload = args[0] as { toolName?: string };
478
+ if (
479
+ normalized &&
480
+ event === normalized.runtimeEvent &&
481
+ normalized.toolName !== "*" &&
482
+ payload.toolName !== normalized.toolName
483
+ ) {
484
+ return undefined;
485
+ }
486
+ return await handler(payload, toHookContext(args[1] as ExtensionContext));
487
+ };
488
+ (api.on as (event: string, handler: HandlerFn) => void)(event, adapted);
372
489
  }
373
490
 
374
491
  for (const [customType, renderer] of hook.messageRenderers) {
@@ -7,6 +7,12 @@ import type { Theme } from "../../modes/theme/theme";
7
7
  import type { HookMessage } from "../../session/messages";
8
8
  import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
9
9
  import type { BashToolDetails, FindToolDetails, ReadToolDetails, SearchToolDetails } from "../../tools";
10
+ import type {
11
+ FunctionHook,
12
+ FunctionHookEventType,
13
+ FunctionHookPayloadFor,
14
+ FunctionHookRegistrationOptions,
15
+ } from "../extensions/function-hooks";
10
16
  import type {
11
17
  AgentEndEvent,
12
18
  AgentStartEvent,
@@ -42,6 +48,23 @@ import type {
42
48
 
43
49
  // Re-export for backward compatibility
44
50
  export type { ExecOptions, ExecResult } from "../../exec/exec";
51
+ /** Capability-scoped Function Hooks share the ExtensionRunner event contract. */
52
+ export type {
53
+ FunctionHook,
54
+ FunctionHookAuditRecord,
55
+ FunctionHookCapabilities,
56
+ FunctionHookCapability,
57
+ FunctionHookEventType,
58
+ FunctionHookGrant,
59
+ FunctionHookGrantInput,
60
+ FunctionHookNext,
61
+ FunctionHookOperation,
62
+ FunctionHookPayloadFor,
63
+ FunctionHookProvenance,
64
+ FunctionHookRegistration,
65
+ FunctionHookRegistrationOptions,
66
+ FunctionHookResult,
67
+ } from "../extensions/function-hooks";
45
68
 
46
69
  /**
47
70
  * UI context for hooks to request interactive UI from the harness.
@@ -499,6 +522,11 @@ export interface HookAPI {
499
522
  on(event: "todo_reminder", handler: HookHandler<TodoReminderEvent>): void;
500
523
  on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;
501
524
  on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult>): void;
525
+ registerFunctionHook<T extends FunctionHookEventType>(
526
+ event: T,
527
+ handler: FunctionHook<FunctionHookPayloadFor<T>>,
528
+ options?: FunctionHookRegistrationOptions,
529
+ ): void;
502
530
 
503
531
  /**
504
532
  * Send a custom message to the session. Creates a CustomMessageEntry that