@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
@@ -2,12 +2,24 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { logger } from "@gajae-code/utils";
4
4
  import { normalizePluginHook } from "../../hooks/normalize";
5
+ import {
6
+ compatibilityFunctionHookGrant,
7
+ type FunctionHookGrant,
8
+ functionHookGrantHash,
9
+ normalizeFunctionHookGrant,
10
+ } from "../extensions/function-hooks";
11
+ import { compileGjcPluginBundle } from "./compiler";
5
12
  import { bundleIdentity } from "./lifecycle-reconciliation";
6
13
  import { verifyImplementationHash } from "./metadata";
7
14
  import { resolveWithinRoot } from "./paths";
8
15
  import { loadEffectiveGjcPluginRegistry } from "./registry";
9
16
  import { type SessionQuarantine, validateSessionBundles, verifyEntryHashes } from "./session-validation";
10
- import { GjcPluginLoadError, type GjcPluginRegistryEntry, type GjcPluginScope } from "./types";
17
+ import {
18
+ GjcPluginLoadError,
19
+ type GjcPluginRegistryEntry,
20
+ type GjcPluginScope,
21
+ type NormalizedHookSurface,
22
+ } from "./types";
11
23
 
12
24
  /**
13
25
  * Constrained plugin-hook loader.
@@ -21,10 +33,23 @@ import { GjcPluginLoadError, type GjcPluginRegistryEntry, type GjcPluginScope }
21
33
 
22
34
  export interface ConstrainedPluginHook {
23
35
  plugin: string;
36
+ scope?: GjcPluginScope;
37
+ extensionId?: string;
24
38
  event: string;
25
39
  target?: string;
26
40
  phase?: "before" | "after";
27
- handler: (...args: any[]) => unknown;
41
+ handler: (...args: never[]) => unknown;
42
+ grant?: FunctionHookGrant;
43
+ provenance?: {
44
+ source: "plugin-bundle";
45
+ scope: GjcPluginScope;
46
+ plugin: string;
47
+ path: string;
48
+ extensionId: string;
49
+ activationGeneration?: number;
50
+ };
51
+ functionHook?: boolean;
52
+ activationGeneration?: number;
28
53
  }
29
54
 
30
55
  export interface ConstrainedHookLoadResult {
@@ -52,16 +77,143 @@ async function resolveConstrainedHookFile(root: string, relativePath: string): P
52
77
  export interface DeclaredHook {
53
78
  plugin: string;
54
79
  scope: GjcPluginScope;
80
+ extensionId?: string;
55
81
  event: string;
56
82
  target?: string;
57
83
  phase?: "before" | "after";
58
84
  relativePath: string;
59
85
  implementationHash?: string;
86
+ capabilities?: string[];
87
+ networkDestinations?: string[];
88
+ filesystemRoots?: string[];
89
+ capabilityHash?: string;
90
+ pluginRoot?: string;
91
+ functionHook?: boolean;
92
+ activationGeneration?: number;
93
+ }
94
+
95
+ interface PersistedHookMetadata {
96
+ grant: FunctionHookGrant;
97
+ functionHook: boolean;
98
+ }
99
+
100
+ function sameValues(actual: readonly unknown[] | undefined, expected: readonly unknown[]): boolean {
101
+ return (
102
+ actual !== undefined &&
103
+ actual.length === expected.length &&
104
+ actual.every((value, index) => value === expected[index])
105
+ );
106
+ }
107
+
108
+ function invalidHookMetadata(plugin: string, detail: string): never {
109
+ throw new GjcPluginLoadError(
110
+ "security_policy",
111
+ `GJC plugin hook "${plugin}" has inconsistent function-hook metadata: ${detail}`,
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Registry hook metadata is attacker-editable between install and session
117
+ * startup. Legacy hooks have no grant metadata at all; every hook carrying a
118
+ * grant must instead carry the complete canonical grant, its hash, and the
119
+ * function-hook marker.
120
+ */
121
+ function readPersistedHookMetadata(input: {
122
+ plugin: string;
123
+ event: string;
124
+ capabilities?: unknown;
125
+ networkDestinations?: unknown;
126
+ filesystemRoots?: unknown;
127
+ capabilityHash?: unknown;
128
+ functionHook?: unknown;
129
+ }): PersistedHookMetadata {
130
+ const hasGrantField =
131
+ input.capabilities !== undefined ||
132
+ input.networkDestinations !== undefined ||
133
+ input.filesystemRoots !== undefined;
134
+ const hasHash = input.capabilityHash !== undefined;
135
+
136
+ if (!hasGrantField && !hasHash) {
137
+ if (input.functionHook !== undefined && typeof input.functionHook !== "boolean")
138
+ invalidHookMetadata(input.plugin, "functionHook must be boolean");
139
+ if (input.functionHook === true)
140
+ invalidHookMetadata(
141
+ input.plugin,
142
+ "functionHook requires capabilities, destinations, roots, and capabilityHash",
143
+ );
144
+ return { grant: compatibilityFunctionHookGrant(input.event), functionHook: false };
145
+ }
146
+
147
+ if (
148
+ input.functionHook !== true ||
149
+ !Array.isArray(input.capabilities) ||
150
+ !Array.isArray(input.networkDestinations) ||
151
+ !Array.isArray(input.filesystemRoots) ||
152
+ typeof input.capabilityHash !== "string"
153
+ )
154
+ invalidHookMetadata(input.plugin, "functionHook grants must include canonical arrays and capabilityHash");
155
+
156
+ let grant: FunctionHookGrant;
157
+ try {
158
+ grant = normalizeFunctionHookGrant({
159
+ capabilities: input.capabilities as FunctionHookGrant["capabilities"],
160
+ networkDestinations: input.networkDestinations as string[],
161
+ filesystemRoots: input.filesystemRoots as string[],
162
+ });
163
+ } catch {
164
+ invalidHookMetadata(input.plugin, "grant values are invalid");
165
+ }
166
+ if (
167
+ !sameValues(input.capabilities as unknown[], grant.capabilities) ||
168
+ !sameValues(input.networkDestinations as unknown[], grant.networkDestinations) ||
169
+ !sameValues(input.filesystemRoots as unknown[], grant.filesystemRoots)
170
+ )
171
+ invalidHookMetadata(input.plugin, "grant values are not normalized");
172
+ const expectedHash = functionHookGrantHash(grant);
173
+ if ((input.capabilityHash as string).toLowerCase() !== expectedHash)
174
+ invalidHookMetadata(input.plugin, "capabilityHash does not match the normalized grant");
175
+ return { grant, functionHook: true };
176
+ }
177
+
178
+ function assertCompiledHookMetadata(
179
+ declared: DeclaredHook,
180
+ expected: NormalizedHookSurface | undefined,
181
+ ): PersistedHookMetadata {
182
+ const persisted = readPersistedHookMetadata(declared);
183
+ if (!expected) {
184
+ invalidHookMetadata(
185
+ declared.plugin,
186
+ `hook ${declared.extensionId ?? declared.relativePath} is not in the installed manifest`,
187
+ );
188
+ }
189
+ const expectedFunctionHook = expected.functionHook === true;
190
+ if (
191
+ declared.extensionId !== expected.extensionId ||
192
+ declared.event !== expected.event ||
193
+ declared.target !== expected.target ||
194
+ declared.phase !== expected.phase ||
195
+ declared.relativePath !== expected.relativePath ||
196
+ declared.implementationHash !== expected.implementationHash
197
+ )
198
+ invalidHookMetadata(declared.plugin, "identity or implementation metadata does not match the installed manifest");
199
+ if (persisted.functionHook !== expectedFunctionHook)
200
+ invalidHookMetadata(declared.plugin, "functionHook does not match the installed manifest");
201
+ if (!expectedFunctionHook) return persisted;
202
+ if (
203
+ !sameValues(persisted.grant.capabilities, expected.capabilities ?? []) ||
204
+ !sameValues(persisted.grant.networkDestinations, expected.networkDestinations ?? []) ||
205
+ !sameValues(persisted.grant.filesystemRoots, expected.filesystemRoots ?? []) ||
206
+ typeof expected.capabilityHash !== "string" ||
207
+ functionHookGrantHash(persisted.grant).toLowerCase() !== expected.capabilityHash.toLowerCase()
208
+ )
209
+ invalidHookMetadata(declared.plugin, "grant metadata does not match the installed manifest");
210
+ return persisted;
60
211
  }
61
212
 
62
213
  async function collectDeclaredHooks(
63
214
  entries: readonly GjcPluginRegistryEntry[],
64
215
  invalidHookIds = new Set<string>(),
216
+ activationGeneration?: number,
65
217
  ): Promise<DeclaredHook[]> {
66
218
  const out: DeclaredHook[] = [];
67
219
  for (const entry of entries) {
@@ -70,16 +222,23 @@ async function collectDeclaredHooks(
70
222
  for (const h of entry.surfaces.hooks) {
71
223
  if (disabled.has(h.extensionId) || invalidHookIds.has(`${entry.scope}:${entry.name}:${h.extensionId}`))
72
224
  continue;
73
- const implementationPath = await resolveConstrainedHookFile(entry.pluginRoot, h.relativePath);
74
225
  out.push({
75
226
  plugin: entry.name,
76
227
  scope: entry.scope,
228
+ extensionId: h.extensionId,
77
229
  event: h.event,
78
230
  target: h.target,
79
231
  phase: h.phase,
80
- relativePath: implementationPath,
232
+ relativePath: h.relativePath,
81
233
  implementationHash:
82
234
  "implementationHash" in h && typeof h.implementationHash === "string" ? h.implementationHash : undefined,
235
+ capabilities: h.capabilities,
236
+ networkDestinations: h.networkDestinations,
237
+ filesystemRoots: h.filesystemRoots,
238
+ capabilityHash: h.capabilityHash,
239
+ pluginRoot: entry.pluginRoot,
240
+ functionHook: h.functionHook,
241
+ activationGeneration,
83
242
  });
84
243
  }
85
244
  }
@@ -89,24 +248,68 @@ async function collectDeclaredHooks(
89
248
  /** Lazy declaration for one constrained hook. Importing this descriptor is metadata-only. */
90
249
  export class ConstrainedPluginHookDescriptor {
91
250
  readonly plugin: string;
92
- readonly scope: GjcPluginScope;
251
+ readonly scope?: GjcPluginScope;
252
+ readonly extensionId?: string;
93
253
  readonly event: string;
94
254
  readonly target?: string;
95
255
  readonly phase?: "before" | "after";
96
256
  readonly relativePath: string;
97
257
  readonly implementationHash?: string;
258
+ readonly capabilities?: string[];
259
+ readonly networkDestinations?: string[];
260
+ readonly filesystemRoots?: string[];
261
+ readonly capabilityHash?: string;
262
+ readonly pluginRoot?: string;
263
+ readonly grant: FunctionHookGrant;
264
+ readonly functionHook: boolean;
265
+ readonly activationGeneration?: number;
266
+ readonly persistedFunctionHook?: unknown;
98
267
 
99
268
  constructor(input: DeclaredHook) {
100
269
  this.plugin = input.plugin;
101
270
  this.scope = input.scope;
271
+ this.extensionId = input.extensionId;
102
272
  this.event = input.event;
103
273
  this.target = input.target;
104
274
  this.phase = input.phase;
105
275
  this.relativePath = input.relativePath;
106
276
  this.implementationHash = input.implementationHash;
277
+ this.pluginRoot = input.pluginRoot;
278
+ this.functionHook = input.functionHook === true;
279
+ this.persistedFunctionHook = input.functionHook;
280
+ this.capabilities = input.capabilities;
281
+ this.networkDestinations = input.networkDestinations;
282
+ this.filesystemRoots = input.filesystemRoots;
283
+ this.capabilityHash = input.capabilityHash;
284
+ this.activationGeneration = input.activationGeneration;
285
+ try {
286
+ this.grant =
287
+ input.capabilities === undefined &&
288
+ input.networkDestinations === undefined &&
289
+ input.filesystemRoots === undefined
290
+ ? compatibilityFunctionHookGrant(input.event)
291
+ : normalizeFunctionHookGrant({
292
+ capabilities: input.capabilities as FunctionHookGrant["capabilities"] | undefined,
293
+ networkDestinations: input.networkDestinations,
294
+ filesystemRoots: input.filesystemRoots,
295
+ });
296
+ } catch {
297
+ // Keep construction metadata-only; load() performs the fail-closed
298
+ // validation and reports a quarantine-safe error.
299
+ this.grant = compatibilityFunctionHookGrant(input.event);
300
+ }
107
301
  }
108
302
 
109
303
  async load(): Promise<ConstrainedPluginHook> {
304
+ const metadata = readPersistedHookMetadata({
305
+ plugin: this.plugin,
306
+ event: this.event,
307
+ capabilities: this.capabilities,
308
+ networkDestinations: this.networkDestinations,
309
+ filesystemRoots: this.filesystemRoots,
310
+ capabilityHash: this.capabilityHash,
311
+ functionHook: this.persistedFunctionHook,
312
+ });
110
313
  const normalized = normalizePluginHook({
111
314
  declaredEvent: this.event,
112
315
  target: this.target,
@@ -120,8 +323,21 @@ export class ConstrainedPluginHookDescriptor {
120
323
  normalized.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join("; "),
121
324
  );
122
325
  }
123
- if (this.implementationHash) await verifyImplementationHash(this.relativePath, this.implementationHash);
124
- const registered: { event: string; handler: (...a: any[]) => unknown }[] = [];
326
+ if (!this.pluginRoot) {
327
+ throw new GjcPluginLoadError(
328
+ "security_policy",
329
+ `GJC plugin hook "${this.plugin}" has no installed root; refusing to import an unconfined path`,
330
+ );
331
+ }
332
+ if (metadata.functionHook) {
333
+ throw new GjcPluginLoadError(
334
+ "security_policy",
335
+ `Plugin function hook "${this.plugin}" requires an isolated runtime and cannot execute in the host realm`,
336
+ );
337
+ }
338
+ const resolvedPath = await resolveConstrainedHookFile(this.pluginRoot, this.relativePath);
339
+ if (this.implementationHash) await verifyImplementationHash(resolvedPath, this.implementationHash);
340
+ const registered: { event: string; handler: (...a: never[]) => unknown }[] = [];
125
341
  const deny = (method: string) => () => {
126
342
  throw new GjcPluginLoadError(
127
343
  "security_policy",
@@ -129,16 +345,21 @@ export class ConstrainedPluginHookDescriptor {
129
345
  );
130
346
  };
131
347
  const constrainedApi: Record<string, unknown> = {
132
- on: (event: string, handler: (...a: any[]) => unknown) => registered.push({ event, handler }),
348
+ on: (event: string, handler: (...a: never[]) => unknown) => registered.push({ event, handler }),
133
349
  logger,
134
350
  };
135
351
  for (const method of DENIED_API_METHODS) constrainedApi[method] = deny(method);
136
- const mod = await import(this.relativePath);
352
+ const mod = await import(resolvedPath);
137
353
  const factory = mod.default ?? mod;
138
354
  if (typeof factory !== "function")
139
355
  throw new GjcPluginLoadError("invalid_hook", "Plugin hook must export a default function");
140
356
  await (factory as (api: unknown) => unknown)(constrainedApi);
141
- if (registered.length !== 1 || registered[0]?.event !== this.event) {
357
+ if (this.implementationHash) await verifyImplementationHash(resolvedPath, this.implementationHash);
358
+ if (
359
+ registered.length !== 1 ||
360
+ registered[0]?.event !== this.event ||
361
+ typeof registered[0]?.handler !== "function"
362
+ ) {
142
363
  throw new GjcPluginLoadError(
143
364
  "runtime_mismatch",
144
365
  `Plugin hook registered ${JSON.stringify(registered.map(r => r.event))}, expected exactly ["${this.event}"]`,
@@ -146,10 +367,28 @@ export class ConstrainedPluginHookDescriptor {
146
367
  }
147
368
  return {
148
369
  plugin: this.plugin,
370
+ scope: this.scope,
371
+ extensionId: this.extensionId,
149
372
  event: this.event,
150
373
  target: this.target,
151
374
  phase: this.phase,
152
375
  handler: registered[0].handler,
376
+ grant: metadata.grant,
377
+ functionHook: metadata.functionHook,
378
+ ...(this.scope && this.pluginRoot && this.extensionId
379
+ ? {
380
+ provenance: {
381
+ source: "plugin-bundle" as const,
382
+ scope: this.scope,
383
+ plugin: this.plugin,
384
+ path: this.relativePath,
385
+ extensionId: this.extensionId,
386
+ ...(this.activationGeneration === undefined
387
+ ? {}
388
+ : { activationGeneration: this.activationGeneration }),
389
+ },
390
+ }
391
+ : {}),
153
392
  };
154
393
  }
155
394
  }
@@ -165,7 +404,9 @@ async function loadOneHook(
165
404
  quarantine: {
166
405
  identity: bundleIdentity(declared.scope, declared.plugin),
167
406
  plugin: declared.plugin,
168
- surfaceId: `hook:${declared.event}:${declared.target ?? ""}`,
407
+ surfaceId:
408
+ declared.extensionId ??
409
+ `hook:${declared.event}:${declared.phase ?? ""}:${declared.target ?? "*"}:${path.basename(declared.relativePath)}`,
169
410
  code,
170
411
  message: error instanceof Error ? error.message : String(error),
171
412
  },
@@ -178,13 +419,32 @@ async function loadOneHook(
178
419
  * `cwd`, applying hash-drift + collision quarantine first. Returns empty when
179
420
  * no plugins are installed.
180
421
  */
181
- export async function loadConstrainedPluginHooks(input: { cwd: string }): Promise<ConstrainedHookLoadResult> {
422
+ export async function loadConstrainedPluginHooks(input: {
423
+ cwd: string;
424
+ activationGeneration?: number;
425
+ }): Promise<ConstrainedHookLoadResult> {
182
426
  const effective = await loadEffectiveGjcPluginRegistry(input.cwd);
183
427
  if (effective.length === 0) return { hooks: [], quarantine: [] };
184
428
  const preQuarantine: SessionQuarantine[] = [];
185
429
  const invalidHookIds = new Set<string>();
186
430
  for (const entry of effective) {
187
431
  if (!entry.enabled) continue;
432
+ let compiledHooks = new Map<string, NormalizedHookSurface>();
433
+ if (entry.surfaces.hooks.length > 0) {
434
+ try {
435
+ const compiled = await compileGjcPluginBundle(entry.pluginRoot);
436
+ compiledHooks = new Map(compiled.surfaces.hooks.map(hook => [hook.extensionId, hook]));
437
+ } catch (error) {
438
+ preQuarantine.push({
439
+ identity: bundleIdentity(entry.scope, entry.name),
440
+ plugin: entry.name,
441
+ surfaceId: `plugin:${entry.name}`,
442
+ code: error instanceof GjcPluginLoadError ? error.code : "invalid_hook",
443
+ message: error instanceof Error ? error.message : String(error),
444
+ });
445
+ continue;
446
+ }
447
+ }
188
448
  for (const hook of entry.surfaces.hooks) {
189
449
  if (entry.disabledSurfaceIds.includes(hook.extensionId)) continue;
190
450
  const normalized = normalizePluginHook({
@@ -194,20 +454,58 @@ export async function loadConstrainedPluginHooks(input: { cwd: string }): Promis
194
454
  plugin: entry.name,
195
455
  source: hook.relativePath,
196
456
  });
197
- if (normalized.hook) continue;
198
- invalidHookIds.add(`${entry.scope}:${entry.name}:${hook.extensionId}`);
199
- preQuarantine.push({
200
- identity: bundleIdentity(entry.scope, entry.name),
201
- plugin: entry.name,
202
- surfaceId: hook.extensionId,
203
- code: "invalid_hook",
204
- message: normalized.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join("; "),
205
- });
457
+ if (!normalized.hook) {
458
+ invalidHookIds.add(`${entry.scope}:${entry.name}:${hook.extensionId}`);
459
+ preQuarantine.push({
460
+ identity: bundleIdentity(entry.scope, entry.name),
461
+ plugin: entry.name,
462
+ surfaceId: hook.extensionId,
463
+ code: "invalid_hook",
464
+ message: normalized.diagnostics
465
+ .map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`)
466
+ .join("; "),
467
+ });
468
+ continue;
469
+ }
470
+ try {
471
+ assertCompiledHookMetadata(
472
+ {
473
+ plugin: entry.name,
474
+ scope: entry.scope,
475
+ extensionId: hook.extensionId,
476
+ event: hook.event,
477
+ target: hook.target,
478
+ phase: hook.phase,
479
+ relativePath: hook.relativePath,
480
+ implementationHash: hook.implementationHash,
481
+ capabilities: hook.capabilities,
482
+ networkDestinations: hook.networkDestinations,
483
+ filesystemRoots: hook.filesystemRoots,
484
+ capabilityHash: hook.capabilityHash,
485
+ pluginRoot: entry.pluginRoot,
486
+ functionHook: hook.functionHook,
487
+ },
488
+ compiledHooks.get(hook.extensionId),
489
+ );
490
+ } catch (error) {
491
+ invalidHookIds.add(`${entry.scope}:${entry.name}:${hook.extensionId}`);
492
+ preQuarantine.push({
493
+ identity: bundleIdentity(entry.scope, entry.name),
494
+ plugin: entry.name,
495
+ surfaceId: hook.extensionId,
496
+ code: error instanceof GjcPluginLoadError ? error.code : "invalid_hook",
497
+ message: error instanceof Error ? error.message : String(error),
498
+ });
499
+ }
206
500
  }
207
501
  const drift = await verifyEntryHashes(entry);
208
502
  if (drift) preQuarantine.push(drift);
209
503
  for (const hook of entry.surfaces.hooks) {
210
- if (entry.disabledSurfaceIds.includes(hook.extensionId)) continue;
504
+ if (
505
+ entry.disabledSurfaceIds.includes(hook.extensionId) ||
506
+ invalidHookIds.has(`${entry.scope}:${entry.name}:${hook.extensionId}`)
507
+ )
508
+ continue;
211
509
  try {
212
510
  await resolveConstrainedHookFile(entry.pluginRoot, hook.relativePath);
213
511
  } catch (error) {
@@ -223,7 +521,7 @@ export async function loadConstrainedPluginHooks(input: { cwd: string }): Promis
223
521
  }
224
522
  }
225
523
  const { active, quarantine } = validateSessionBundles(effective, {}, preQuarantine);
226
- const declared = await collectDeclaredHooks(active, invalidHookIds);
524
+ const declared = await collectDeclaredHooks(active, invalidHookIds, input.activationGeneration);
227
525
  const hooks: ConstrainedPluginHook[] = [];
228
526
  for (const d of declared) {
229
527
  const { hook, quarantine: q } = await loadOneHook(d);
@@ -5,7 +5,8 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { gunzipSync } from "node:zlib";
7
7
  import { compileGjcPluginBundle } from "./compiler";
8
- import { gjcPluginProjectRoot, gjcPluginUserRoot } from "./paths";
8
+ import { classifyStdioInvocation } from "./mcp-policy";
9
+ import { gjcPluginInstallRoot, gjcPluginProjectRoot, gjcPluginSafeDirectoryName, gjcPluginUserRoot } from "./paths";
9
10
  import { readRegistry, sortRegistryEntries, withRegistryLock, writeRegistryUnlocked } from "./registry";
10
11
  import {
11
12
  GJC_PLUGIN_MANIFEST_FILENAME,
@@ -64,14 +65,6 @@ function scopeRoot(scope: GjcPluginScope, cwd: string): string {
64
65
  return scope === "user" ? gjcPluginUserRoot() : gjcPluginProjectRoot(cwd);
65
66
  }
66
67
 
67
- function safeDirSegment(name: string): string {
68
- const seg = name.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
69
- if (!seg || seg === "." || seg === "..") {
70
- throw new GjcPluginLoadError("invalid_manifest", `GJC plugin name is not a safe directory segment: ${name}`);
71
- }
72
- return seg;
73
- }
74
-
75
68
  async function isDirectory(p: string): Promise<boolean> {
76
69
  try {
77
70
  return (await fs.stat(p)).isDirectory();
@@ -392,6 +385,14 @@ function sha256(buf: Buffer): string {
392
385
  * copied, so the installed tree equals the validated set.
393
386
  */
394
387
  async function copyValidatedFiles(bundle: NormalizedGjcPluginBundle, stagingDir: string): Promise<void> {
388
+ const cwdPaths = new Set(
389
+ bundle.surfaces.mcps.flatMap(surface => {
390
+ if (surface.transport !== "stdio") return [];
391
+ const invocation = classifyStdioInvocation(surface.config, { pluginRoot: bundle.root });
392
+ return [path.relative(bundle.root, invocation.cwd)];
393
+ }),
394
+ );
395
+ for (const cwdPath of cwdPaths) await fs.mkdir(path.join(stagingDir, cwdPath), { recursive: true });
395
396
  for (const file of bundle.files) {
396
397
  const src = path.join(bundle.root, file.relativePath);
397
398
  const lst = await fs.lstat(src);
@@ -438,9 +439,9 @@ export async function runGjcBundleTransaction(
438
439
  // lock roots or sweep installation remnants.
439
440
  const bundle = await compileGjcPluginBundle(resolved.dir);
440
441
  validateInstallPlan(bundle, []);
441
- const dirName = safeDirSegment(bundle.name);
442
+ const dirName = gjcPluginSafeDirectoryName(bundle.name);
442
443
  const root = scopeRoot(options.scope, options.cwd);
443
- const finalDir = path.join(root, dirName);
444
+ const finalDir = gjcPluginInstallRoot(options.scope, options.cwd, bundle.name);
444
445
 
445
446
  // Lock-free refusal preflight. Acquiring a scope lock creates the scope
446
447
  // root and mutates directory metadata, so a create-only refusal must be
@@ -578,7 +579,7 @@ export function candidateRegistryEntry(
578
579
  source: GjcPluginRegistrySource,
579
580
  now: string,
580
581
  ): GjcPluginRegistryEntry {
581
- const finalDir = path.join(scopeRoot(scope, cwd), safeDirSegment(bundle.name));
582
+ const finalDir = gjcPluginInstallRoot(scope, cwd, bundle.name);
582
583
  return bundleToRegistryEntry(bundle, finalDir, scope, source, now);
583
584
  }
584
585