@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
@@ -49,6 +49,7 @@ import type { ExecOptions, ExecResult } from "../../exec/exec";
49
49
  import type { CustomEditor } from "../../modes/components/custom-editor";
50
50
  import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/workflow-gate-broker";
51
51
  import type { Theme } from "../../modes/theme/theme";
52
+ import type { AgentSessionEventListener } from "../../session/agent-session";
52
53
  import type {
53
54
  ClientBridge,
54
55
  ClientBridgePermissionOption,
@@ -103,6 +104,12 @@ import type {
103
104
  TurnStartEvent,
104
105
  } from "../shared-events";
105
106
  import type { SlashCommandInfo } from "../slash-commands";
107
+ import type {
108
+ FunctionHook,
109
+ FunctionHookEventType,
110
+ FunctionHookPayloadFor,
111
+ FunctionHookRegistrationOptions,
112
+ } from "./function-hooks";
106
113
 
107
114
  export type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
108
115
  export type { ExecOptions, ExecResult } from "../../exec/exec";
@@ -530,6 +537,8 @@ export interface ExtensionContext {
530
537
  getJobs(): unknown;
531
538
  /** Subscribe to foreground-wait folds (`bash_folded`). Returns an unsubscribe function. */
532
539
  onJobFold?(listener: (event: JobFoldEvent) => void): () => void;
540
+ /** Observe session events synchronously in producer order. Returns an unsubscribe function. */
541
+ onSessionEvent?(listener: AgentSessionEventListener): () => void;
533
542
  /** Typed skill and mode controls exposed to the SDK host. */
534
543
  invokeSkill?(
535
544
  name: string,
@@ -750,6 +759,8 @@ export type { AgentEndEvent, TurnEndEvent, TurnStartEvent } from "../shared-even
750
759
  /** Fired when an agent loop starts. `sdkRunToken` is an internal SDK queue-owner binding. */
751
760
  export interface AgentStartEvent extends SharedAgentStartEvent {
752
761
  sdkRunToken?: string;
762
+ /** Complete set of SDK queue owners consumed together by this run. */
763
+ sdkRunTokens?: string[];
753
764
  }
754
765
 
755
766
  /** Fired when an agent run fails before emitting agent_end. The error is the
@@ -1234,6 +1245,18 @@ export interface ExtensionAPI {
1234
1245
  on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
1235
1246
  on(event: "user_python", handler: ExtensionHandler<UserPythonEvent, UserPythonEventResult>): void;
1236
1247
 
1248
+ /**
1249
+ * Register capability-scoped middleware on the authoritative ExtensionRunner.
1250
+ * The host supplies grants; an extension cannot self-grant capabilities.
1251
+ * Wildcard registrations receive a redacted payload unless the host explicitly
1252
+ * grants a tool inspection/transform capability.
1253
+ */
1254
+ registerFunctionHook<T extends FunctionHookEventType>(
1255
+ event: T,
1256
+ handler: FunctionHook<FunctionHookPayloadFor<T>>,
1257
+ options?: FunctionHookRegistrationOptions,
1258
+ ): void;
1259
+
1237
1260
  // =========================================================================
1238
1261
  // Tool Registration
1239
1262
  // =========================================================================
@@ -1692,6 +1715,8 @@ export interface ExtensionContextActions {
1692
1715
  getJobs?: () => unknown;
1693
1716
  /** Subscribe to foreground-wait folds so the SDK host can publish `bash_folded`. Returns an unsubscribe. */
1694
1717
  onJobFold?: (listener: (event: JobFoldEvent) => void) => () => void;
1718
+ /** Ordered, synchronous AgentSession subscription, independent of async extension hooks. */
1719
+ onSessionEvent?: (listener: AgentSessionEventListener) => () => void;
1695
1720
  setSdkPermissionProvider?: (
1696
1721
  provider:
1697
1722
  | ((
@@ -1752,6 +1777,23 @@ export interface Extension {
1752
1777
  shortcuts: Map<KeyId, ExtensionShortcut>;
1753
1778
  }
1754
1779
 
1780
+ export type {
1781
+ FunctionHook,
1782
+ FunctionHookAuditRecord,
1783
+ FunctionHookCapabilities,
1784
+ FunctionHookCapability,
1785
+ FunctionHookEventType,
1786
+ FunctionHookGrant,
1787
+ FunctionHookGrantInput,
1788
+ FunctionHookNext,
1789
+ FunctionHookOperation,
1790
+ FunctionHookPayloadFor,
1791
+ FunctionHookProvenance,
1792
+ FunctionHookRegistration,
1793
+ FunctionHookRegistrationOptions,
1794
+ FunctionHookResult,
1795
+ } from "./function-hooks";
1796
+
1755
1797
  /** Result of loading extensions. */
1756
1798
  export interface LoadExtensionsResult {
1757
1799
  extensions: Extension[];
@@ -2,12 +2,24 @@
2
2
  * Tool wrappers for extensions.
3
3
  */
4
4
  import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@gajae-code/agent-core";
5
- import type { ImageContent, Static, TextContent, TSchema } from "@gajae-code/ai/core";
5
+ import {
6
+ type ImageContent,
7
+ type Static,
8
+ type TextContent,
9
+ type ToolCall,
10
+ type TSchema,
11
+ validateToolArguments,
12
+ } from "@gajae-code/ai/core";
6
13
  import type { Theme } from "../../modes/theme/theme";
14
+ import { ToolAbortError } from "../../tools/tool-errors";
7
15
  import { applyToolProxy } from "../tool-proxy";
8
16
  import type { ExtensionRunner } from "./runner";
9
17
  import type { RegisteredTool, ToolCallEventResult } from "./types";
10
18
 
19
+ function toolAbortReason(signal: AbortSignal | undefined, fallback: string): Error {
20
+ return signal?.reason instanceof Error ? signal.reason : new ToolAbortError(fallback);
21
+ }
22
+
11
23
  /**
12
24
  * Adapts a RegisteredTool into an AgentTool.
13
25
  */
@@ -92,6 +104,11 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
92
104
  applyToolProxy(tool, this);
93
105
  }
94
106
 
107
+ /** Host composition hook: guards must run inside Function Hook preflight. */
108
+ getInnerTool(): AgentTool<TParameters, TDetails> {
109
+ return this.tool;
110
+ }
111
+
95
112
  /**
96
113
  * Forward browser mode changes when available.
97
114
  */
@@ -109,19 +126,21 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
109
126
  context?: AgentToolContext,
110
127
  ) {
111
128
  const scope = context?.attemptScope;
129
+ const toolCallEvent = {
130
+ type: "tool_call" as const,
131
+ toolName: this.tool.name,
132
+ toolCallId,
133
+ input: params as Record<string, unknown>,
134
+ };
112
135
  // Emit tool_call event - extensions can block execution
113
136
  if (this.runner.hasHandlers("tool_call")) {
114
137
  try {
115
- const callResult = (await this.runner.emitToolCall(
116
- {
117
- type: "tool_call",
118
- toolName: this.tool.name,
119
- toolCallId,
120
- input: params as Record<string, unknown>,
121
- },
122
- scope,
123
- )) as ToolCallEventResult | undefined;
138
+ const callResult = (await this.runner.emitToolCall(toolCallEvent, scope, {
139
+ signal,
140
+ correlationId: toolCallId,
141
+ })) as ToolCallEventResult | undefined;
124
142
 
143
+ if (signal?.aborted) throw toolAbortReason(signal, "Tool call aborted during Function Hook mediation");
125
144
  if (callResult?.block) {
126
145
  const reason = callResult.reason || "Tool execution was blocked by an extension";
127
146
  throw new Error(reason);
@@ -133,14 +152,28 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
133
152
  throw new Error(`Extension failed, blocking execution: ${String(err)}`);
134
153
  }
135
154
  }
155
+ if (toolCallEvent.input !== params || !this.tool.lenientArgValidation) {
156
+ params = validateToolArguments(this.tool, {
157
+ type: "toolCall",
158
+ id: toolCallId,
159
+ name: this.tool.name,
160
+ arguments: toolCallEvent.input,
161
+ } satisfies ToolCall) as Static<TParameters>;
162
+ }
136
163
 
137
164
  // Execute the actual tool
138
165
  let result: { content: any; details?: TDetails };
139
166
  let executionError: Error | undefined;
167
+ const mediatesToolResult = this.runner.hasToolResultMediation(this.tool.name);
168
+ const deliversToolResult = this.runner.hasHandlers("tool_result");
169
+ const effectiveOnUpdate = mediatesToolResult ? undefined : onUpdate;
140
170
 
141
171
  try {
142
- result = await this.tool.execute(toolCallId, params, signal, onUpdate, context);
172
+ result = await this.tool.execute(toolCallId, params, signal, effectiveOnUpdate, context);
143
173
  } catch (err) {
174
+ if (err instanceof ToolAbortError || signal?.aborted) {
175
+ throw err instanceof ToolAbortError ? err : toolAbortReason(signal, "Tool execution aborted");
176
+ }
144
177
  executionError = err instanceof Error ? err : new Error(String(err));
145
178
  result = {
146
179
  content: [{ type: "text", text: executionError.message }],
@@ -149,7 +182,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
149
182
  }
150
183
 
151
184
  // Emit tool_result event - extensions can modify the result and error status
152
- if (this.runner.hasHandlers("tool_result")) {
185
+ if (signal?.aborted) throw toolAbortReason(signal, "Tool execution aborted");
186
+ if (deliversToolResult) {
153
187
  const resultResult = await this.runner.emitToolResult(
154
188
  {
155
189
  type: "tool_result",
@@ -161,11 +195,17 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
161
195
  isError: !!executionError,
162
196
  },
163
197
  scope,
198
+ { signal, correlationId: toolCallId },
164
199
  );
200
+ if (signal?.aborted) throw toolAbortReason(signal, "Tool result mediation aborted");
165
201
 
166
202
  if (resultResult) {
167
- const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
168
- const modifiedDetails = (resultResult.details ?? result.details) as TDetails;
203
+ const modifiedContent: (TextContent | ImageContent)[] = Object.hasOwn(resultResult, "content")
204
+ ? (resultResult.content ?? [])
205
+ : result.content;
206
+ const modifiedDetails = (
207
+ Object.hasOwn(resultResult, "details") ? resultResult.details : result.details
208
+ ) as TDetails;
169
209
 
170
210
  // Extension can override error status
171
211
  if (resultResult.isError === true && !executionError) {
@@ -181,7 +221,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
181
221
 
182
222
  // Error status unchanged, but content/details may be modified
183
223
  if (executionError) {
184
- throw executionError;
224
+ const textBlocks = modifiedContent.filter((content): content is TextContent => content.type === "text");
225
+ throw new Error(textBlocks.map(content => content.text).join("\n") || "Tool execution failed");
185
226
  }
186
227
  return { content: modifiedContent, details: modifiedDetails };
187
228
  }
@@ -1,8 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
+ import * as nodeFs from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
4
5
  import { parseFrontmatter, pathIsWithin } from "@gajae-code/utils";
5
- import { readSchemaDeclaration, schemaHash } from "./metadata";
6
+ import { functionHookGrantHash, normalizeFunctionHookGrant } from "../extensions/function-hooks";
7
+ import { classifyStdioInvocation } from "./mcp-policy";
8
+ import { canonicalizeJsonSchema, extractDeclaredToolSchema, schemaHash } from "./metadata";
6
9
  import { resolveWithinRoot } from "./paths";
7
10
  import { parseManifest, parseSubskillFrontmatter } from "./schema";
8
11
  import {
@@ -10,6 +13,7 @@ import {
10
13
  type GjcPluginAppendixManifestEntry,
11
14
  GjcPluginLoadError,
12
15
  type GjcPluginMcpManifestEntry,
16
+ type JsonSchema202012,
13
17
  type NormalizedAgentAppendixSurface,
14
18
  type NormalizedAppendixSurface,
15
19
  type NormalizedGjcPluginBundle,
@@ -22,6 +26,101 @@ import {
22
26
  } from "./types";
23
27
  import { validateBinding } from "./validation";
24
28
 
29
+ const PLUGIN_FILE_MAX_BYTES = 16 * 1024 * 1024;
30
+ const PLUGIN_MANIFEST_MAX_BYTES = PLUGIN_FILE_MAX_BYTES;
31
+ const PLUGIN_FILE_OPEN_FLAGS =
32
+ nodeFs.constants.O_RDONLY | (process.platform === "win32" ? 0 : (nodeFs.constants.O_NOFOLLOW ?? 0));
33
+
34
+ interface FileIdentity {
35
+ dev: bigint;
36
+ ino: bigint;
37
+ }
38
+
39
+ interface FileAuthority {
40
+ path: string;
41
+ realPath: string;
42
+ identity: FileIdentity;
43
+ }
44
+
45
+ interface ManifestSnapshot {
46
+ bytes: Buffer;
47
+ json: unknown;
48
+ }
49
+
50
+ function sameIdentity(left: FileIdentity, right: FileIdentity): boolean {
51
+ return left.dev === right.dev && left.ino === right.ino;
52
+ }
53
+
54
+ function errorCode(error: unknown): string | undefined {
55
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
56
+ ? error.code
57
+ : undefined;
58
+ }
59
+
60
+ function tooLarge(rel: string, maxBytes: number): GjcPluginLoadError {
61
+ return new GjcPluginLoadError("security_policy", `GJC plugin file exceeds ${maxBytes} bytes: ${rel}`);
62
+ }
63
+
64
+ async function readBoundedFile(file: FileAuthority, rel: string, maxBytes: number): Promise<Buffer> {
65
+ let handle: fs.FileHandle;
66
+ try {
67
+ handle = await fs.open(file.path, PLUGIN_FILE_OPEN_FLAGS);
68
+ } catch (error) {
69
+ if (errorCode(error) === "ELOOP") {
70
+ throw new GjcPluginLoadError("security_policy", `GJC plugin path became a symlink before opening: ${rel}`, {
71
+ cause: error instanceof Error ? error : undefined,
72
+ });
73
+ }
74
+ throw error;
75
+ }
76
+ try {
77
+ const before = await handle.stat({ bigint: true });
78
+ if (!before.isFile()) {
79
+ throw new GjcPluginLoadError("security_policy", `GJC plugin path is not a regular file: ${rel}`);
80
+ }
81
+ if (!sameIdentity(file.identity, before)) {
82
+ throw new GjcPluginLoadError("security_policy", `GJC plugin file changed before opening: ${rel}`);
83
+ }
84
+ const [settledRealPath, settledPath] = await Promise.all([
85
+ fs.realpath(file.path),
86
+ fs.lstat(file.path, { bigint: true }),
87
+ ]);
88
+ if (
89
+ settledRealPath !== file.realPath ||
90
+ settledPath.isSymbolicLink() ||
91
+ !settledPath.isFile() ||
92
+ !sameIdentity(settledPath, before)
93
+ ) {
94
+ throw new GjcPluginLoadError("security_policy", `GJC plugin file changed before reading: ${rel}`);
95
+ }
96
+ if (before.size > BigInt(maxBytes)) throw tooLarge(rel, maxBytes);
97
+ const chunks: Buffer[] = [];
98
+ let offset = 0;
99
+ for (;;) {
100
+ const remaining = maxBytes + 1 - offset;
101
+ if (remaining <= 0) throw tooLarge(rel, maxBytes);
102
+ const chunk = Buffer.allocUnsafe(Math.min(1024 * 1024, remaining));
103
+ const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, offset);
104
+ if (bytesRead === 0) break;
105
+ chunks.push(chunk.subarray(0, bytesRead));
106
+ offset += bytesRead;
107
+ }
108
+ const after = await handle.stat({ bigint: true });
109
+ if (
110
+ !sameIdentity(before, after) ||
111
+ before.size !== after.size ||
112
+ before.mtimeNs !== after.mtimeNs ||
113
+ before.ctimeNs !== after.ctimeNs ||
114
+ BigInt(offset) !== after.size
115
+ ) {
116
+ throw new GjcPluginLoadError("security_policy", `GJC plugin file changed while reading: ${rel}`);
117
+ }
118
+ return Buffer.concat(chunks, offset);
119
+ } finally {
120
+ await handle.close();
121
+ }
122
+ }
123
+
25
124
  function sha256(bytes: Buffer | string): string {
26
125
  return createHash("sha256").update(bytes).digest("hex");
27
126
  }
@@ -43,17 +142,19 @@ export const surfaceIds = {
43
142
  `subskill-tool:${parent}:${phase}:${activationArg}:${relativePath}`,
44
143
  } as const;
45
144
 
46
- async function readManifestJson(filePath: string): Promise<unknown> {
47
- let text: string;
145
+ async function readManifestSnapshot(pluginRoot: string, filePath: string): Promise<ManifestSnapshot> {
146
+ let bytes: Buffer;
48
147
  try {
49
- text = await fs.readFile(filePath, "utf8");
148
+ const file = await resolveDeclaredFile(pluginRoot, GJC_PLUGIN_MANIFEST_FILENAME);
149
+ bytes = await readBoundedFile(file, GJC_PLUGIN_MANIFEST_FILENAME, PLUGIN_MANIFEST_MAX_BYTES);
50
150
  } catch (error) {
151
+ if (error instanceof GjcPluginLoadError && error.code !== "missing_file") throw error;
51
152
  throw new GjcPluginLoadError("missing_file", `Missing GJC plugin manifest at ${filePath}`, {
52
153
  cause: error instanceof Error ? error : undefined,
53
154
  });
54
155
  }
55
156
  try {
56
- return JSON.parse(text) as unknown;
157
+ return { bytes, json: JSON.parse(bytes.toString("utf8")) as unknown };
57
158
  } catch (error) {
58
159
  throw new GjcPluginLoadError("invalid_manifest", `Invalid GJC plugin manifest JSON at ${filePath}`, {
59
160
  cause: error instanceof Error ? error : undefined,
@@ -61,37 +162,64 @@ async function readManifestJson(filePath: string): Promise<unknown> {
61
162
  }
62
163
  }
63
164
 
165
+ async function resolveDeclaredDirectory(pluginRoot: string, rel: string): Promise<string> {
166
+ const resolved = resolveWithinRoot(pluginRoot, rel || ".");
167
+ let stat: Awaited<ReturnType<typeof fs.lstat>>;
168
+ try {
169
+ stat = await fs.lstat(resolved);
170
+ } catch (error) {
171
+ throw new GjcPluginLoadError("missing_file", `Missing GJC plugin directory at ${resolved}`, {
172
+ cause: error instanceof Error ? error : undefined,
173
+ });
174
+ }
175
+ if (!stat.isDirectory()) {
176
+ throw new GjcPluginLoadError("security_policy", `GJC plugin cwd must be a real directory: ${rel || "."}`);
177
+ }
178
+ const [realRoot, real] = await Promise.all([fs.realpath(pluginRoot), fs.realpath(resolved)]);
179
+ if (real !== realRoot && !pathIsWithin(realRoot, real)) {
180
+ throw new GjcPluginLoadError("security_policy", `GJC plugin cwd escapes root via symlink: ${rel || "."}`);
181
+ }
182
+ return resolved;
183
+ }
184
+
64
185
  /**
65
186
  * Resolve a declared relative path, rejecting lexical escapes AND symlink
66
187
  * escapes out of the plugin root. Never imports the file.
67
188
  */
68
- async function resolveDeclaredFile(pluginRoot: string, rel: string): Promise<string> {
189
+ async function resolveDeclaredFile(pluginRoot: string, rel: string): Promise<FileAuthority> {
69
190
  const resolved = resolveWithinRoot(pluginRoot, rel);
70
- let real: string;
191
+ let realRoot: string;
192
+ let realPath: string;
193
+ let stat: nodeFs.BigIntStats;
71
194
  try {
72
- real = await fs.realpath(resolved);
195
+ realRoot = await fs.realpath(pluginRoot);
196
+ realPath = await fs.realpath(resolved);
197
+ stat = await fs.lstat(resolved, { bigint: true });
73
198
  } catch (error) {
74
199
  throw new GjcPluginLoadError("missing_file", `Missing GJC plugin file at ${resolved}`, {
75
200
  cause: error instanceof Error ? error : undefined,
76
201
  });
77
202
  }
78
- const realRoot = await fs.realpath(pluginRoot);
79
- if (!pathIsWithin(realRoot, real)) {
203
+ if (!pathIsWithin(realRoot, realPath)) {
80
204
  throw new GjcPluginLoadError("security_policy", `GJC plugin file escapes root via symlink: ${rel}`);
81
205
  }
82
- return resolved;
206
+ if (stat.isSymbolicLink() || !stat.isFile()) {
207
+ throw new GjcPluginLoadError("security_policy", `GJC plugin path must be a real file: ${rel}`);
208
+ }
209
+ return { path: resolved, realPath, identity: stat };
83
210
  }
84
211
 
85
212
  async function hashFile(
86
- absPath: string,
213
+ file: FileAuthority,
87
214
  rel: string,
88
215
  declaredSha?: string,
89
- ): Promise<{ sha256: string; bytes: number }> {
216
+ ): Promise<{ sha256: string; bytes: number; content: Buffer }> {
90
217
  let buf: Buffer;
91
218
  try {
92
- buf = await fs.readFile(absPath);
219
+ buf = await readBoundedFile(file, rel, PLUGIN_FILE_MAX_BYTES);
93
220
  } catch (error) {
94
- throw new GjcPluginLoadError("missing_file", `Missing GJC plugin file at ${absPath}`, {
221
+ if (error instanceof GjcPluginLoadError) throw error;
222
+ throw new GjcPluginLoadError("missing_file", `Missing GJC plugin file at ${file.path}`, {
95
223
  cause: error instanceof Error ? error : undefined,
96
224
  });
97
225
  }
@@ -99,7 +227,38 @@ async function hashFile(
99
227
  if (declaredSha !== undefined && declaredSha.toLowerCase() !== digest) {
100
228
  throw new GjcPluginLoadError("hash_mismatch", `GJC plugin file hash mismatch for ${rel}`);
101
229
  }
102
- return { sha256: digest, bytes: buf.byteLength };
230
+ return { sha256: digest, bytes: buf.byteLength, content: buf };
231
+ }
232
+
233
+ function schemaFromSnapshots(
234
+ declaration: unknown,
235
+ sourceBytes: Buffer,
236
+ schemaFile?: { relativePath: string; bytes: Buffer },
237
+ ): JsonSchema202012 {
238
+ if (schemaFile !== undefined) {
239
+ try {
240
+ return canonicalizeJsonSchema(JSON.parse(schemaFile.bytes.toString("utf8")) as unknown);
241
+ } catch (error) {
242
+ if (error instanceof GjcPluginLoadError) throw error;
243
+ throw new GjcPluginLoadError(
244
+ "invalid_schema",
245
+ `Invalid JSON Schema declaration at ${schemaFile.relativePath}`,
246
+ {
247
+ cause: error instanceof Error ? error : undefined,
248
+ },
249
+ );
250
+ }
251
+ }
252
+ if (declaration !== undefined) return canonicalizeJsonSchema(declaration);
253
+ try {
254
+ return extractDeclaredToolSchema(sourceBytes.toString("utf8"));
255
+ } catch (error) {
256
+ if (error instanceof GjcPluginLoadError) throw error;
257
+ throw new GjcPluginLoadError(
258
+ "invalid_schema",
259
+ `Tool parameters schema is not statically readable: ${error instanceof Error ? error.message : String(error)}`,
260
+ );
261
+ }
103
262
  }
104
263
 
105
264
  function mcpConfigHash(entry: GjcPluginMcpManifestEntry): string {
@@ -141,8 +300,8 @@ async function compileAppendix(
141
300
  return { contentHash: digest, bytes: Buffer.byteLength(content), content };
142
301
  }
143
302
  const rel = entry.path as string;
144
- const abs = await resolveDeclaredFile(pluginRoot, rel);
145
- const { sha256: digest, bytes } = await hashFile(abs, rel, entry.sha256);
303
+ const file = await resolveDeclaredFile(pluginRoot, rel);
304
+ const { sha256: digest, bytes } = await hashFile(file, rel, entry.sha256);
146
305
  if (bytes === 0) {
147
306
  throw new GjcPluginLoadError("invalid_appendix", `Invalid GJC plugin ${field}: file is empty`);
148
307
  }
@@ -158,40 +317,34 @@ async function compileAppendix(
158
317
  export async function compileGjcPluginBundle(root: string): Promise<NormalizedGjcPluginBundle> {
159
318
  const pluginRoot = path.resolve(root);
160
319
  const manifestPath = path.join(pluginRoot, GJC_PLUGIN_MANIFEST_FILENAME);
161
- const manifest = parseManifest(await readManifestJson(manifestPath), manifestPath);
320
+ const manifestSnapshot = await readManifestSnapshot(pluginRoot, manifestPath);
321
+ const manifest = parseManifest(manifestSnapshot.json, manifestPath);
162
322
 
163
323
  const files = new Map<string, { sha256: string; bytes: number }>();
164
324
  const manifestSubskillTools = manifest.tools.filter(tool => tool.surface === "subskill");
165
325
  const manifestSubskillFiles = new Map<string, { name: string; sha256: string }>();
166
326
  for (const tool of manifestSubskillTools) {
167
- const abs = await resolveDeclaredFile(pluginRoot, tool.path);
168
- const { sha256: digest, bytes } = await hashFile(abs, tool.path, tool.sha256);
327
+ const file = await resolveDeclaredFile(pluginRoot, tool.path);
328
+ const { sha256: digest, bytes } = await hashFile(file, tool.path, tool.sha256);
169
329
  files.set(tool.path, { sha256: digest, bytes });
170
330
  manifestSubskillFiles.set(tool.path, { name: tool.name, sha256: digest });
171
331
  }
172
332
 
173
333
  const subskills: NormalizedSubskillSurface[] = [];
174
334
  for (const rel of manifest.subskills) {
175
- const abs = await resolveDeclaredFile(pluginRoot, rel);
176
- const { sha256: digest, bytes } = await hashFile(abs, rel);
335
+ const file = await resolveDeclaredFile(pluginRoot, rel);
336
+ const { sha256: digest, bytes, content: contentBytes } = await hashFile(file, rel);
177
337
  files.set(rel, { sha256: digest, bytes });
178
- let content: string;
179
- try {
180
- content = await fs.readFile(abs, "utf8");
181
- } catch (error) {
182
- throw new GjcPluginLoadError("missing_file", `Missing GJC sub-skill file at ${abs}`, {
183
- cause: error instanceof Error ? error : undefined,
184
- });
185
- }
338
+ const content = contentBytes.toString("utf8");
186
339
  let parsed: { frontmatter: Record<string, unknown>; body: string };
187
340
  try {
188
- parsed = parseFrontmatter(content, { source: abs, level: "fatal" });
341
+ parsed = parseFrontmatter(content, { source: file.path, level: "fatal" });
189
342
  } catch (error) {
190
- throw new GjcPluginLoadError("invalid_frontmatter", `Invalid GJC sub-skill frontmatter at ${abs}`, {
343
+ throw new GjcPluginLoadError("invalid_frontmatter", `Invalid GJC sub-skill frontmatter at ${file.path}`, {
191
344
  cause: error instanceof Error ? error : undefined,
192
345
  });
193
346
  }
194
- const fm = parseSubskillFrontmatter(parsed.frontmatter, abs);
347
+ const fm = parseSubskillFrontmatter(parsed.frontmatter, file.path);
195
348
  validateBinding(fm);
196
349
  // Subskill-scoped frontmatter tools are hashed for copy-ownership and
197
350
  // escape checks (the loader resolves these at runtime).
@@ -212,8 +365,8 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
212
365
  }
213
366
  for (const toolRel of fmToolPaths) {
214
367
  if (toolRel.trim().length === 0) continue;
215
- const toolAbs = await resolveDeclaredFile(pluginRoot, toolRel);
216
- const { sha256: toolDigest, bytes: toolBytes } = await hashFile(toolAbs, toolRel);
368
+ const toolFile = await resolveDeclaredFile(pluginRoot, toolRel);
369
+ const { sha256: toolDigest, bytes: toolBytes } = await hashFile(toolFile, toolRel);
217
370
  files.set(toolRel, { sha256: toolDigest, bytes: toolBytes });
218
371
  const extensionId = surfaceIds.subskillTool(fm.binds_to, fm.phase, fm.activation_arg, toolRel);
219
372
  if (seenToolRefs.has(extensionId)) continue;
@@ -238,18 +391,18 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
238
391
  // surface; legacy string shorthand stays subskill-scoped (loader-handled).
239
392
  const tools: NormalizedToolSurface[] = [];
240
393
  for (const tool of manifest.tools) {
241
- const abs = await resolveDeclaredFile(pluginRoot, tool.path);
242
- const { sha256: digest, bytes } = await hashFile(abs, tool.path, tool.sha256);
394
+ const file = await resolveDeclaredFile(pluginRoot, tool.path);
395
+ const { sha256: digest, bytes, content: sourceBytes } = await hashFile(file, tool.path, tool.sha256);
243
396
  files.set(tool.path, { sha256: digest, bytes });
244
397
  if (tool.surface !== "always-on") continue;
245
- let schemaPath: string | undefined;
398
+ let schemaFileSnapshot: { relativePath: string; bytes: Buffer } | undefined;
246
399
  if (tool.schemaPath !== undefined) {
247
- const schemaAbs = await resolveDeclaredFile(pluginRoot, tool.schemaPath);
248
- const schemaFile = await hashFile(schemaAbs, tool.schemaPath);
249
- files.set(tool.schemaPath, schemaFile);
250
- schemaPath = tool.schemaPath;
400
+ const schemaFile = await resolveDeclaredFile(pluginRoot, tool.schemaPath);
401
+ const { sha256: schemaDigest, bytes: schemaBytes, content } = await hashFile(schemaFile, tool.schemaPath);
402
+ files.set(tool.schemaPath, { sha256: schemaDigest, bytes: schemaBytes });
403
+ schemaFileSnapshot = { relativePath: tool.schemaPath, bytes: content };
251
404
  }
252
- const schema = await readSchemaDeclaration(pluginRoot, abs, tool.schema, schemaPath);
405
+ const schema = schemaFromSnapshots(tool.schema, sourceBytes, schemaFileSnapshot);
253
406
  tools.push({
254
407
  extensionId: surfaceIds.tool(tool.name),
255
408
  name: tool.name,
@@ -267,8 +420,8 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
267
420
  for (const hook of manifest.hooks) {
268
421
  // Path safety first: resolve/hash before semantic checks so traversal and
269
422
  // missing-file failures take precedence over contract validation.
270
- const abs = await resolveDeclaredFile(pluginRoot, hook.path);
271
- const { sha256: digest, bytes } = await hashFile(abs, hook.path, hook.sha256);
423
+ const file = await resolveDeclaredFile(pluginRoot, hook.path);
424
+ const { sha256: digest, bytes } = await hashFile(file, hook.path, hook.sha256);
272
425
  files.set(hook.path, { sha256: digest, bytes });
273
426
  // Minimal compile-time hook contract: tool_call hooks must name a target
274
427
  // and a before/after phase so the constrained runner (M3/M4) can bind them.
@@ -295,7 +448,16 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
295
448
  `GJC plugin hook "${hook.name}": tool_result requires the after phase`,
296
449
  );
297
450
  }
298
- hooks.push({
451
+ const grant = normalizeFunctionHookGrant({
452
+ capabilities: hook.capabilities,
453
+ networkDestinations: hook.networkDestinations,
454
+ filesystemRoots: hook.filesystemRoots,
455
+ });
456
+ const functionHook =
457
+ hook.capabilities !== undefined ||
458
+ hook.networkDestinations !== undefined ||
459
+ hook.filesystemRoots !== undefined;
460
+ const normalizedHook: NormalizedHookSurface = {
299
461
  extensionId: surfaceIds.hook(hook.event, hook.phase, hook.target, hook.name),
300
462
  name: hook.name,
301
463
  event: hook.event,
@@ -304,7 +466,15 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
304
466
  relativePath: hook.path,
305
467
  sha256: digest,
306
468
  implementationHash: digest,
307
- });
469
+ };
470
+ if (functionHook) {
471
+ normalizedHook.capabilities = [...grant.capabilities];
472
+ normalizedHook.networkDestinations = [...grant.networkDestinations];
473
+ normalizedHook.filesystemRoots = [...grant.filesystemRoots];
474
+ normalizedHook.capabilityHash = functionHookGrantHash(grant);
475
+ normalizedHook.functionHook = true;
476
+ }
477
+ hooks.push(normalizedHook);
308
478
  }
309
479
 
310
480
  const mcps: NormalizedMcpSurface[] = [];
@@ -320,17 +490,15 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
320
490
  `GJC plugin MCP "${entry.name}": ${entry.transport} requires a url`,
321
491
  );
322
492
  }
323
- // Hash bundled stdio script args (relative file paths) so the registry owns
324
- // their copied-file boundary. Path/security failures must propagate, not be
325
- // swallowed, so traversal/symlink-escape/missing bundled files fail compile.
326
- for (const arg of entry.args ?? []) {
327
- // Skip flags (e.g. "--port", "-v"); only treat clearly path-like args as
328
- // bundled files subject to root confinement.
329
- if (arg.startsWith("-")) continue;
330
- if (!arg.startsWith(".") && !arg.includes("/")) continue;
331
- const abs = await resolveDeclaredFile(pluginRoot, arg);
332
- const { sha256: digest, bytes } = await hashFile(abs, arg, undefined);
333
- files.set(arg, { sha256: digest, bytes });
493
+ // Derive ownership from the same cwd-aware bare Node/Bun grammar used at runtime.
494
+ if (entry.transport === "stdio") {
495
+ const invocation = classifyStdioInvocation(entry, { pluginRoot });
496
+ await resolveDeclaredDirectory(pluginRoot, path.relative(pluginRoot, invocation.cwd));
497
+ for (const relativePath of new Set(invocation.ownedRelativePaths)) {
498
+ const file = await resolveDeclaredFile(pluginRoot, relativePath);
499
+ const { sha256: digest, bytes } = await hashFile(file, relativePath, undefined);
500
+ files.set(relativePath, { sha256: digest, bytes });
501
+ }
334
502
  }
335
503
  mcps.push({
336
504
  extensionId: surfaceIds.mcp(entry.name),
@@ -377,7 +545,7 @@ export async function compileGjcPluginBundle(root: string): Promise<NormalizedGj
377
545
  agentAppendices,
378
546
  };
379
547
 
380
- const manifestBytes = await fs.readFile(manifestPath);
548
+ const manifestBytes = manifestSnapshot.bytes;
381
549
  const manifestHash = sha256(manifestBytes);
382
550
 
383
551
  const copiedFiles = [...files.entries()]