@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.
- package/CHANGELOG.md +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -2
- package/src/web/insane/bridge.ts +113 -23
- 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
|
|
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
|
-
|
|
118
|
-
|
|
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,
|
|
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 (
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
47
|
-
let
|
|
145
|
+
async function readManifestSnapshot(pluginRoot: string, filePath: string): Promise<ManifestSnapshot> {
|
|
146
|
+
let bytes: Buffer;
|
|
48
147
|
try {
|
|
49
|
-
|
|
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(
|
|
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<
|
|
189
|
+
async function resolveDeclaredFile(pluginRoot: string, rel: string): Promise<FileAuthority> {
|
|
69
190
|
const resolved = resolveWithinRoot(pluginRoot, rel);
|
|
70
|
-
let
|
|
191
|
+
let realRoot: string;
|
|
192
|
+
let realPath: string;
|
|
193
|
+
let stat: nodeFs.BigIntStats;
|
|
71
194
|
try {
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
219
|
+
buf = await readBoundedFile(file, rel, PLUGIN_FILE_MAX_BYTES);
|
|
93
220
|
} catch (error) {
|
|
94
|
-
|
|
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
|
|
145
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
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
|
|
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
|
|
168
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
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
|
|
176
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
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
|
-
|
|
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:
|
|
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 ${
|
|
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,
|
|
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
|
|
216
|
-
const { sha256: toolDigest, bytes: toolBytes } = await hashFile(
|
|
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
|
|
242
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
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
|
|
398
|
+
let schemaFileSnapshot: { relativePath: string; bytes: Buffer } | undefined;
|
|
246
399
|
if (tool.schemaPath !== undefined) {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
files.set(tool.schemaPath,
|
|
250
|
-
|
|
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 =
|
|
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
|
|
271
|
-
const { sha256: digest, bytes } = await hashFile(
|
|
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
|
-
|
|
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
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 =
|
|
548
|
+
const manifestBytes = manifestSnapshot.bytes;
|
|
381
549
|
const manifestHash = sha256(manifestBytes);
|
|
382
550
|
|
|
383
551
|
const copiedFiles = [...files.entries()]
|