@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
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ImageContent, TextContent } from "@gajae-code/ai/core";
|
|
3
|
+
import { redactCrashSecrets } from "@gajae-code/utils";
|
|
4
|
+
import type { CustomMessage } from "../../session/messages";
|
|
5
|
+
import type { ExtensionEvent, ExtensionSessionMetadata, ExtensionUIContext } from "./types";
|
|
6
|
+
|
|
7
|
+
/** Function hooks use the existing extension event stream; `*` is observation middleware. */
|
|
8
|
+
export type FunctionHookEventType = ExtensionEvent["type"] | "*";
|
|
9
|
+
export type FunctionHookPayloadFor<T extends FunctionHookEventType> = T extends "*"
|
|
10
|
+
? ExtensionEvent
|
|
11
|
+
: Extract<ExtensionEvent, { type: T }>;
|
|
12
|
+
|
|
13
|
+
/** Narrow capability vocabulary. Parent names expand only to their children. */
|
|
14
|
+
export type FunctionHookCapability =
|
|
15
|
+
| "tool"
|
|
16
|
+
| "tool.inspect"
|
|
17
|
+
| "tool.transform"
|
|
18
|
+
| "tool.deny"
|
|
19
|
+
| "ui"
|
|
20
|
+
| "ui.status"
|
|
21
|
+
| "ui.widget"
|
|
22
|
+
| "ui.notify"
|
|
23
|
+
| "ui.transform"
|
|
24
|
+
| "session"
|
|
25
|
+
| "session.read"
|
|
26
|
+
| "session.message"
|
|
27
|
+
| "session.deny"
|
|
28
|
+
| "audit"
|
|
29
|
+
| "audit.append"
|
|
30
|
+
| "network"
|
|
31
|
+
| "network.fetch"
|
|
32
|
+
| "filesystem"
|
|
33
|
+
| "filesystem.read";
|
|
34
|
+
|
|
35
|
+
export const FUNCTION_HOOK_CAPABILITIES: readonly FunctionHookCapability[] = [
|
|
36
|
+
"tool",
|
|
37
|
+
"tool.inspect",
|
|
38
|
+
"tool.transform",
|
|
39
|
+
"tool.deny",
|
|
40
|
+
"ui",
|
|
41
|
+
"ui.status",
|
|
42
|
+
"ui.widget",
|
|
43
|
+
"ui.notify",
|
|
44
|
+
"ui.transform",
|
|
45
|
+
"session",
|
|
46
|
+
"session.read",
|
|
47
|
+
"session.message",
|
|
48
|
+
"session.deny",
|
|
49
|
+
"audit",
|
|
50
|
+
"audit.append",
|
|
51
|
+
"network",
|
|
52
|
+
"network.fetch",
|
|
53
|
+
"filesystem",
|
|
54
|
+
"filesystem.read",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export type FunctionHookOperation = Exclude<
|
|
58
|
+
FunctionHookCapability,
|
|
59
|
+
"tool" | "ui" | "session" | "audit" | "network" | "filesystem"
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
const CAPABILITY_OPERATIONS: Record<FunctionHookCapability, readonly FunctionHookOperation[]> = {
|
|
63
|
+
tool: ["tool.inspect", "tool.transform", "tool.deny"],
|
|
64
|
+
"tool.inspect": ["tool.inspect"],
|
|
65
|
+
"tool.transform": ["tool.transform"],
|
|
66
|
+
"tool.deny": ["tool.deny"],
|
|
67
|
+
ui: ["ui.status", "ui.widget", "ui.notify", "ui.transform"],
|
|
68
|
+
"ui.status": ["ui.status"],
|
|
69
|
+
"ui.widget": ["ui.widget"],
|
|
70
|
+
"ui.notify": ["ui.notify"],
|
|
71
|
+
"ui.transform": ["ui.transform"],
|
|
72
|
+
session: ["session.read", "session.message", "session.deny"],
|
|
73
|
+
"session.read": ["session.read"],
|
|
74
|
+
"session.message": ["session.message"],
|
|
75
|
+
"session.deny": ["session.deny"],
|
|
76
|
+
audit: ["audit.append"],
|
|
77
|
+
"audit.append": ["audit.append"],
|
|
78
|
+
network: ["network.fetch"],
|
|
79
|
+
"network.fetch": ["network.fetch"],
|
|
80
|
+
filesystem: ["filesystem.read"],
|
|
81
|
+
"filesystem.read": ["filesystem.read"],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const KNOWN_CAPABILITIES = new Set<string>(FUNCTION_HOOK_CAPABILITIES);
|
|
85
|
+
export interface FunctionHookGrantInput {
|
|
86
|
+
capabilities?: readonly FunctionHookCapability[];
|
|
87
|
+
networkDestinations?: readonly string[];
|
|
88
|
+
filesystemRoots?: readonly string[];
|
|
89
|
+
/** An attenuation boundary can only remove downstream operations. */
|
|
90
|
+
attenuateDownstream?: readonly FunctionHookCapability[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface FunctionHookGrant {
|
|
94
|
+
readonly capabilities: readonly FunctionHookCapability[];
|
|
95
|
+
readonly networkDestinations: readonly string[];
|
|
96
|
+
readonly filesystemRoots: readonly string[];
|
|
97
|
+
readonly attenuateDownstream?: readonly FunctionHookCapability[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface FunctionHookRegistrationOptions extends FunctionHookGrantInput {
|
|
101
|
+
target?: string;
|
|
102
|
+
registrationId?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FunctionHookProvenance {
|
|
106
|
+
readonly source: "builtin" | "user" | "project" | "extension" | "plugin-bundle";
|
|
107
|
+
readonly scope?: "user" | "project" | "native";
|
|
108
|
+
readonly plugin?: string;
|
|
109
|
+
readonly extensionId?: string;
|
|
110
|
+
readonly path: string;
|
|
111
|
+
readonly activationGeneration?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface FunctionHookInvocation<TEvent extends ExtensionEvent = ExtensionEvent> {
|
|
115
|
+
readonly eventId: string;
|
|
116
|
+
readonly correlationId: string;
|
|
117
|
+
readonly eventType: TEvent["type"];
|
|
118
|
+
readonly provenance: FunctionHookProvenance;
|
|
119
|
+
readonly registrationOrder: number;
|
|
120
|
+
readonly payload: Readonly<TEvent>;
|
|
121
|
+
readonly signal: AbortSignal;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface FunctionHookMessage {
|
|
125
|
+
customType: string;
|
|
126
|
+
content: string | (TextContent | ImageContent)[];
|
|
127
|
+
display?: boolean;
|
|
128
|
+
details?: unknown;
|
|
129
|
+
attribution?: CustomMessage["attribution"];
|
|
130
|
+
triggerTurn?: boolean;
|
|
131
|
+
deliverAs?: "steer" | "followUp";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface FunctionHookAuditRecord {
|
|
135
|
+
readonly sequence: number;
|
|
136
|
+
readonly eventId: string;
|
|
137
|
+
readonly correlationId: string;
|
|
138
|
+
readonly eventType: string;
|
|
139
|
+
readonly action: "continue" | "deny" | "return" | "error" | "timeout";
|
|
140
|
+
readonly registrationOrder: number;
|
|
141
|
+
readonly provenance: {
|
|
142
|
+
source: FunctionHookProvenance["source"];
|
|
143
|
+
scope?: FunctionHookProvenance["scope"];
|
|
144
|
+
plugin?: string;
|
|
145
|
+
extensionId?: string;
|
|
146
|
+
path: string;
|
|
147
|
+
};
|
|
148
|
+
readonly reason?: string;
|
|
149
|
+
readonly payloadHash?: string;
|
|
150
|
+
readonly evidence?: unknown;
|
|
151
|
+
readonly requestedCapabilities?: readonly FunctionHookCapability[];
|
|
152
|
+
readonly effectiveCapabilities?: readonly FunctionHookCapability[];
|
|
153
|
+
readonly capabilityHash?: string;
|
|
154
|
+
readonly activationGeneration?: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface FunctionHookCapabilities {
|
|
158
|
+
readonly tool?: {
|
|
159
|
+
readonly canInspect: boolean;
|
|
160
|
+
readonly canTransform: boolean;
|
|
161
|
+
readonly canDeny: boolean;
|
|
162
|
+
};
|
|
163
|
+
readonly ui?: {
|
|
164
|
+
readonly canTransform: boolean;
|
|
165
|
+
readonly setStatus: (key: string, text: string | undefined) => void;
|
|
166
|
+
readonly setWidget: (key: string, content: string[] | undefined) => void;
|
|
167
|
+
readonly notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
168
|
+
};
|
|
169
|
+
readonly session?: {
|
|
170
|
+
readonly canRead: boolean;
|
|
171
|
+
readonly canDeny: boolean;
|
|
172
|
+
readonly metadata: Readonly<ExtensionSessionMetadata> | undefined;
|
|
173
|
+
readonly emitMessage: (
|
|
174
|
+
message: FunctionHookMessage,
|
|
175
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
176
|
+
) => void;
|
|
177
|
+
};
|
|
178
|
+
readonly audit?: { readonly append: (evidence: Record<string, unknown>) => void };
|
|
179
|
+
readonly network?: { readonly fetch: (input: string | URL, init?: RequestInit) => Promise<Response> };
|
|
180
|
+
readonly filesystem?: { readonly readFile: (filePath: string) => Promise<string> };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type FunctionHookResult<TEvent extends ExtensionEvent = ExtensionEvent> =
|
|
184
|
+
| { action: "continue"; event?: TEvent }
|
|
185
|
+
| { action: "deny"; reason: string }
|
|
186
|
+
| { action: "return"; value: unknown };
|
|
187
|
+
|
|
188
|
+
export type FunctionHookNext<TEvent extends ExtensionEvent = ExtensionEvent> = (
|
|
189
|
+
event?: TEvent,
|
|
190
|
+
) => Promise<FunctionHookResult<TEvent>>;
|
|
191
|
+
|
|
192
|
+
export type FunctionHook<TEvent extends ExtensionEvent = ExtensionEvent> = (
|
|
193
|
+
invocation: FunctionHookInvocation<TEvent>,
|
|
194
|
+
capabilities: FunctionHookCapabilities,
|
|
195
|
+
next: FunctionHookNext<TEvent>,
|
|
196
|
+
) => FunctionHookResult<TEvent> | undefined | Promise<FunctionHookResult<TEvent> | undefined>;
|
|
197
|
+
|
|
198
|
+
export interface FunctionHookRegistration {
|
|
199
|
+
readonly event: FunctionHookEventType;
|
|
200
|
+
readonly target?: string;
|
|
201
|
+
readonly registrationId?: string;
|
|
202
|
+
readonly handler: FunctionHook;
|
|
203
|
+
readonly grant: FunctionHookGrant;
|
|
204
|
+
readonly provenance: FunctionHookProvenance;
|
|
205
|
+
readonly registrationOrder?: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function validateFunctionHookTarget(target: string): string {
|
|
209
|
+
if (
|
|
210
|
+
target.length === 0 ||
|
|
211
|
+
target.trim() !== target ||
|
|
212
|
+
target === "." ||
|
|
213
|
+
target === ".." ||
|
|
214
|
+
target.includes("/") ||
|
|
215
|
+
target.includes("\\") ||
|
|
216
|
+
target.includes("\0")
|
|
217
|
+
) {
|
|
218
|
+
throw new Error("Function hook target must be a non-empty logical tool name without path separators");
|
|
219
|
+
}
|
|
220
|
+
return target;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function expandedOperations(capabilities: readonly FunctionHookCapability[]): Set<FunctionHookOperation> {
|
|
224
|
+
const operations = new Set<FunctionHookOperation>();
|
|
225
|
+
for (const capability of capabilities) {
|
|
226
|
+
if (!KNOWN_CAPABILITIES.has(capability))
|
|
227
|
+
throw new Error(`Unknown function hook capability: ${String(capability)}`);
|
|
228
|
+
for (const operation of CAPABILITY_OPERATIONS[capability]) operations.add(operation);
|
|
229
|
+
}
|
|
230
|
+
return operations;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function uniqueSortedCapabilities(input: readonly FunctionHookCapability[] | undefined): FunctionHookCapability[] {
|
|
234
|
+
const values = [...new Set(input ?? [])];
|
|
235
|
+
for (const value of values) {
|
|
236
|
+
if (!KNOWN_CAPABILITIES.has(value)) throw new Error(`Unknown function hook capability: ${String(value)}`);
|
|
237
|
+
}
|
|
238
|
+
return values.sort();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateNetworkDestination(value: string): string {
|
|
242
|
+
let url: URL;
|
|
243
|
+
try {
|
|
244
|
+
url = new URL(value);
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error("Function hook network destinations must be valid HTTPS origins");
|
|
247
|
+
}
|
|
248
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
249
|
+
throw new Error("Function hook network destinations must be origin-only HTTPS URLs");
|
|
250
|
+
return url.origin;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateFilesystemRoot(value: string): string {
|
|
254
|
+
if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.includes("\0"))
|
|
255
|
+
throw new Error("Function hook filesystem roots must be non-empty paths without NUL or surrounding whitespace");
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function normalizeFunctionHookGrant(input: FunctionHookGrantInput = {}): FunctionHookGrant {
|
|
260
|
+
const capabilities = uniqueSortedCapabilities(input.capabilities);
|
|
261
|
+
const operations = expandedOperations(capabilities);
|
|
262
|
+
const networkDestinations = [...new Set(input.networkDestinations ?? [])].map(validateNetworkDestination).sort();
|
|
263
|
+
const filesystemRoots = [...new Set(input.filesystemRoots ?? [])].map(validateFilesystemRoot).sort();
|
|
264
|
+
if (networkDestinations.length > 0 && !operations.has("network.fetch"))
|
|
265
|
+
throw new Error("Function hook network destinations require network.fetch");
|
|
266
|
+
if (filesystemRoots.length > 0 && !operations.has("filesystem.read"))
|
|
267
|
+
throw new Error("Function hook filesystem roots require filesystem.read");
|
|
268
|
+
const attenuation =
|
|
269
|
+
input.attenuateDownstream === undefined ? undefined : uniqueSortedCapabilities(input.attenuateDownstream);
|
|
270
|
+
if (attenuation) expandedOperations(attenuation);
|
|
271
|
+
return Object.freeze({
|
|
272
|
+
capabilities: Object.freeze(capabilities),
|
|
273
|
+
networkDestinations: Object.freeze(networkDestinations),
|
|
274
|
+
filesystemRoots: Object.freeze(filesystemRoots),
|
|
275
|
+
...(attenuation ? { attenuateDownstream: Object.freeze(attenuation) } : {}),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function compatibilityFunctionHookGrant(event: string): FunctionHookGrant {
|
|
280
|
+
if (event === "tool_call" || event === "tool_result") return normalizeFunctionHookGrant({ capabilities: ["tool"] });
|
|
281
|
+
if (event.startsWith("session")) return normalizeFunctionHookGrant({ capabilities: ["session"] });
|
|
282
|
+
return normalizeFunctionHookGrant();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function functionHookGrantOperations(grant: FunctionHookGrant): ReadonlySet<FunctionHookOperation> {
|
|
286
|
+
return expandedOperations(grant.capabilities);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function intersectFunctionHookGrants(
|
|
290
|
+
grant: FunctionHookGrant,
|
|
291
|
+
allowed: FunctionHookGrant | undefined,
|
|
292
|
+
): FunctionHookGrant {
|
|
293
|
+
if (!allowed) return grant;
|
|
294
|
+
const allowedOperations = expandedOperations(allowed.capabilities);
|
|
295
|
+
const capabilities = [...expandedOperations(grant.capabilities)].filter(operation =>
|
|
296
|
+
allowedOperations.has(operation),
|
|
297
|
+
) as FunctionHookCapability[];
|
|
298
|
+
return normalizeFunctionHookGrant({
|
|
299
|
+
capabilities,
|
|
300
|
+
networkDestinations: grant.networkDestinations.filter(destination =>
|
|
301
|
+
allowed.networkDestinations.includes(destination),
|
|
302
|
+
),
|
|
303
|
+
filesystemRoots: grant.filesystemRoots.filter(root => allowed.filesystemRoots.includes(root)),
|
|
304
|
+
attenuateDownstream: grant.attenuateDownstream,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Host-owned ceiling for caller-registered in-process extension and hook middleware. */
|
|
309
|
+
export const DEFAULT_EXTENSION_FUNCTION_HOOK_GRANT = normalizeFunctionHookGrant({
|
|
310
|
+
capabilities: ["tool", "ui", "session", "audit"],
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
export function attenuateFunctionHookGrant(
|
|
314
|
+
grant: FunctionHookGrant,
|
|
315
|
+
removed: readonly FunctionHookCapability[] | undefined,
|
|
316
|
+
): FunctionHookGrant {
|
|
317
|
+
if (!removed || removed.length === 0) return grant;
|
|
318
|
+
const removedOperations = expandedOperations(removed);
|
|
319
|
+
const capabilities = [...expandedOperations(grant.capabilities)].filter(
|
|
320
|
+
operation => !removedOperations.has(operation),
|
|
321
|
+
) as FunctionHookCapability[];
|
|
322
|
+
const operations = expandedOperations(capabilities);
|
|
323
|
+
return normalizeFunctionHookGrant({
|
|
324
|
+
capabilities,
|
|
325
|
+
networkDestinations: operations.has("network.fetch") ? grant.networkDestinations : [],
|
|
326
|
+
filesystemRoots: operations.has("filesystem.read") ? grant.filesystemRoots : [],
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function functionHookGrantHash(grant: FunctionHookGrant): string {
|
|
331
|
+
const canonical = JSON.stringify({
|
|
332
|
+
capabilities: [...expandedOperations(grant.capabilities)].sort(),
|
|
333
|
+
networkDestinations: [...grant.networkDestinations].sort(),
|
|
334
|
+
filesystemRoots: [...grant.filesystemRoots].sort(),
|
|
335
|
+
attenuateDownstream: [...(grant.attenuateDownstream ?? [])].sort(),
|
|
336
|
+
});
|
|
337
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function boundedText(value: string, maxLength: number): string {
|
|
341
|
+
return redactCrashSecrets(value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "")).slice(0, maxLength);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function sensitiveKey(key: string): boolean {
|
|
345
|
+
return /(?:token|secret|password|passwd|credential|authorization|cookie|private|api[-_]?key|bearer)/i.test(key);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Bounded JSON-shaped redaction for diagnostics and audit sinks. Accessors are never invoked. */
|
|
349
|
+
export function redactFunctionHookValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
350
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
351
|
+
if (typeof value === "string") return boundedText(value, 512);
|
|
352
|
+
if (typeof value === "bigint") return "<bigint>";
|
|
353
|
+
if (typeof value !== "object") return `<${typeof value}>`;
|
|
354
|
+
if (seen.has(value)) return "<cycle>";
|
|
355
|
+
if (depth >= 5) return "<depth-limit>";
|
|
356
|
+
seen.add(value);
|
|
357
|
+
try {
|
|
358
|
+
if (Array.isArray(value)) return value.slice(0, 32).map(item => redactFunctionHookValue(item, depth + 1, seen));
|
|
359
|
+
const output: Record<string, unknown> = {};
|
|
360
|
+
for (const key of Object.keys(value).sort().slice(0, 32)) {
|
|
361
|
+
const safeKey = boundedText(key, 80);
|
|
362
|
+
if (sensitiveKey(key)) {
|
|
363
|
+
output[safeKey] = "<redacted>";
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
367
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
368
|
+
output[safeKey] = "<accessor>";
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
output[safeKey] = redactFunctionHookValue(descriptor.value, depth + 1, seen);
|
|
372
|
+
}
|
|
373
|
+
return output;
|
|
374
|
+
} catch {
|
|
375
|
+
return "<unreadable>";
|
|
376
|
+
} finally {
|
|
377
|
+
seen.delete(value);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function functionHookPayloadHash(value: unknown): string {
|
|
382
|
+
return createHash("sha256")
|
|
383
|
+
.update(JSON.stringify(redactFunctionHookValue(value)))
|
|
384
|
+
.digest("hex");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function isPlainFunctionHookData(value: unknown, seen = new WeakSet<object>()): boolean {
|
|
388
|
+
if (value === null || value === undefined || typeof value === "string" || typeof value === "boolean") return true;
|
|
389
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
390
|
+
if (typeof value !== "object" || seen.has(value)) return false;
|
|
391
|
+
seen.add(value);
|
|
392
|
+
try {
|
|
393
|
+
if (Array.isArray(value)) return value.every(item => isPlainFunctionHookData(item, seen));
|
|
394
|
+
const prototype = Object.getPrototypeOf(value);
|
|
395
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
396
|
+
return Object.keys(value).every(key => {
|
|
397
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
398
|
+
return Boolean(descriptor && "value" in descriptor && isPlainFunctionHookData(descriptor.value, seen));
|
|
399
|
+
});
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
} finally {
|
|
403
|
+
seen.delete(value);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function isSafeFunctionHookValue(value: unknown, seen = new WeakSet<object>()): boolean {
|
|
408
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
409
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
410
|
+
if (value === undefined) return true;
|
|
411
|
+
if (typeof value !== "object" || seen.has(value)) return false;
|
|
412
|
+
seen.add(value);
|
|
413
|
+
try {
|
|
414
|
+
if (Array.isArray(value)) return value.every(item => isSafeFunctionHookValue(item, seen));
|
|
415
|
+
if (!isPlainFunctionHookData(value)) return false;
|
|
416
|
+
return Object.keys(value).every(key => {
|
|
417
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
418
|
+
return descriptor !== undefined && "value" in descriptor && isSafeFunctionHookValue(descriptor.value, seen);
|
|
419
|
+
});
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
} finally {
|
|
423
|
+
seen.delete(value);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isFunctionHookContent(value: unknown): boolean {
|
|
428
|
+
if (!isPlainFunctionHookData(value) || value === null || typeof value !== "object") return false;
|
|
429
|
+
const content = value as Record<string, unknown>;
|
|
430
|
+
if (content.type === "text")
|
|
431
|
+
return (
|
|
432
|
+
typeof content.text === "string" &&
|
|
433
|
+
(content.textSignature === undefined || typeof content.textSignature === "string")
|
|
434
|
+
);
|
|
435
|
+
if (content.type === "image") return typeof content.data === "string" && typeof content.mimeType === "string";
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isFunctionHookImage(value: unknown): boolean {
|
|
440
|
+
return (
|
|
441
|
+
isPlainFunctionHookData(value) &&
|
|
442
|
+
value !== null &&
|
|
443
|
+
typeof value === "object" &&
|
|
444
|
+
(value as Record<string, unknown>).type === "image" &&
|
|
445
|
+
typeof (value as Record<string, unknown>).data === "string" &&
|
|
446
|
+
typeof (value as Record<string, unknown>).mimeType === "string"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isFunctionHookAssistantContent(value: unknown): boolean {
|
|
451
|
+
if (isFunctionHookContent(value)) return true;
|
|
452
|
+
if (!isPlainFunctionHookData(value) || value === null || typeof value !== "object") return false;
|
|
453
|
+
const content = value as Record<string, unknown>;
|
|
454
|
+
if (content.type === "thinking") return typeof content.thinking === "string";
|
|
455
|
+
if (content.type === "redactedThinking") return typeof content.data === "string";
|
|
456
|
+
if (content.type === "toolCall") {
|
|
457
|
+
return (
|
|
458
|
+
typeof content.id === "string" &&
|
|
459
|
+
typeof content.name === "string" &&
|
|
460
|
+
content.arguments !== null &&
|
|
461
|
+
typeof content.arguments === "object" &&
|
|
462
|
+
!Array.isArray(content.arguments) &&
|
|
463
|
+
isPlainFunctionHookData(content.arguments)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isFunctionHookMessage(value: unknown): boolean {
|
|
470
|
+
if (!isPlainFunctionHookData(value) || value === null || typeof value !== "object") return false;
|
|
471
|
+
const message = value as Record<string, unknown>;
|
|
472
|
+
if (typeof message.role !== "string") return false;
|
|
473
|
+
if (message.role === "user" || message.role === "developer") {
|
|
474
|
+
return (
|
|
475
|
+
(typeof message.content === "string" ||
|
|
476
|
+
(Array.isArray(message.content) && message.content.every(isFunctionHookContent))) &&
|
|
477
|
+
typeof message.timestamp === "number" &&
|
|
478
|
+
Number.isFinite(message.timestamp)
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (message.role === "toolResult") {
|
|
482
|
+
return (
|
|
483
|
+
typeof message.toolCallId === "string" &&
|
|
484
|
+
typeof message.toolName === "string" &&
|
|
485
|
+
Array.isArray(message.content) &&
|
|
486
|
+
message.content.every(isFunctionHookContent) &&
|
|
487
|
+
typeof message.isError === "boolean" &&
|
|
488
|
+
typeof message.timestamp === "number" &&
|
|
489
|
+
Number.isFinite(message.timestamp)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
if (message.role === "assistant") {
|
|
493
|
+
return (
|
|
494
|
+
Array.isArray(message.content) &&
|
|
495
|
+
message.content.every(isFunctionHookAssistantContent) &&
|
|
496
|
+
typeof message.api === "string" &&
|
|
497
|
+
typeof message.provider === "string" &&
|
|
498
|
+
typeof message.model === "string" &&
|
|
499
|
+
typeof message.stopReason === "string" &&
|
|
500
|
+
typeof message.timestamp === "number" &&
|
|
501
|
+
Number.isFinite(message.timestamp)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (message.role === "custom" || message.role === "hookMessage") {
|
|
505
|
+
return (
|
|
506
|
+
isFunctionHookCustomMessage(message) &&
|
|
507
|
+
typeof message.timestamp === "number" &&
|
|
508
|
+
Number.isFinite(message.timestamp)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (message.role === "bashExecution" || message.role === "pythonExecution") {
|
|
512
|
+
const sourceField = message.role === "bashExecution" ? "command" : "code";
|
|
513
|
+
return (
|
|
514
|
+
typeof message[sourceField] === "string" &&
|
|
515
|
+
typeof message.output === "string" &&
|
|
516
|
+
(message.exitCode === undefined ||
|
|
517
|
+
(typeof message.exitCode === "number" && Number.isFinite(message.exitCode))) &&
|
|
518
|
+
typeof message.cancelled === "boolean" &&
|
|
519
|
+
typeof message.truncated === "boolean" &&
|
|
520
|
+
(message.meta === undefined || isSafeFunctionHookValue(message.meta)) &&
|
|
521
|
+
(message.excludeFromContext === undefined || typeof message.excludeFromContext === "boolean") &&
|
|
522
|
+
typeof message.timestamp === "number" &&
|
|
523
|
+
Number.isFinite(message.timestamp)
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (message.role === "fileMention") {
|
|
527
|
+
return (
|
|
528
|
+
Array.isArray(message.files) &&
|
|
529
|
+
message.files.every(file => {
|
|
530
|
+
if (!isPlainFunctionHookData(file) || file === null || typeof file !== "object") return false;
|
|
531
|
+
const entry = file as Record<string, unknown>;
|
|
532
|
+
return (
|
|
533
|
+
typeof entry.path === "string" &&
|
|
534
|
+
typeof entry.content === "string" &&
|
|
535
|
+
(entry.lineCount === undefined ||
|
|
536
|
+
(typeof entry.lineCount === "number" && Number.isFinite(entry.lineCount))) &&
|
|
537
|
+
(entry.byteSize === undefined ||
|
|
538
|
+
(typeof entry.byteSize === "number" && Number.isFinite(entry.byteSize))) &&
|
|
539
|
+
(entry.skippedReason === undefined || entry.skippedReason === "tooLarge") &&
|
|
540
|
+
(entry.image === undefined || isFunctionHookImage(entry.image)) &&
|
|
541
|
+
(entry.duplicate === undefined || typeof entry.duplicate === "boolean") &&
|
|
542
|
+
(entry.pruned === undefined || typeof entry.pruned === "boolean")
|
|
543
|
+
);
|
|
544
|
+
}) &&
|
|
545
|
+
typeof message.timestamp === "number" &&
|
|
546
|
+
Number.isFinite(message.timestamp)
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (message.role === "branchSummary") {
|
|
550
|
+
return (
|
|
551
|
+
typeof message.summary === "string" &&
|
|
552
|
+
typeof message.fromId === "string" &&
|
|
553
|
+
typeof message.timestamp === "number" &&
|
|
554
|
+
Number.isFinite(message.timestamp)
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (message.role === "compactionSummary") {
|
|
558
|
+
return (
|
|
559
|
+
typeof message.summary === "string" &&
|
|
560
|
+
(message.shortSummary === undefined || typeof message.shortSummary === "string") &&
|
|
561
|
+
typeof message.tokensBefore === "number" &&
|
|
562
|
+
Number.isFinite(message.tokensBefore) &&
|
|
563
|
+
(message.providerPayload === undefined || isSafeFunctionHookValue(message.providerPayload)) &&
|
|
564
|
+
typeof message.timestamp === "number" &&
|
|
565
|
+
Number.isFinite(message.timestamp)
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isFunctionHookCustomMessage(value: unknown): boolean {
|
|
572
|
+
if (!isPlainFunctionHookData(value) || value === null || typeof value !== "object") return false;
|
|
573
|
+
const message = value as Record<string, unknown>;
|
|
574
|
+
return (
|
|
575
|
+
typeof message.customType === "string" &&
|
|
576
|
+
(typeof message.content === "string" ||
|
|
577
|
+
(Array.isArray(message.content) && message.content.every(isFunctionHookContent))) &&
|
|
578
|
+
typeof message.display === "boolean" &&
|
|
579
|
+
(message.details === undefined || isSafeFunctionHookValue(message.details)) &&
|
|
580
|
+
(message.attribution === undefined || message.attribution === "user" || message.attribution === "agent")
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function isValidFunctionHookEventValue(event: ExtensionEvent): boolean {
|
|
585
|
+
try {
|
|
586
|
+
if (!isPlainFunctionHookData(event) || typeof event.type !== "string") return false;
|
|
587
|
+
switch (event.type) {
|
|
588
|
+
case "tool_call":
|
|
589
|
+
return (
|
|
590
|
+
typeof event.toolName === "string" &&
|
|
591
|
+
typeof event.toolCallId === "string" &&
|
|
592
|
+
event.input !== null &&
|
|
593
|
+
typeof event.input === "object" &&
|
|
594
|
+
!Array.isArray(event.input) &&
|
|
595
|
+
isSafeFunctionHookValue(event.input)
|
|
596
|
+
);
|
|
597
|
+
case "tool_result":
|
|
598
|
+
return (
|
|
599
|
+
typeof event.toolName === "string" &&
|
|
600
|
+
typeof event.toolCallId === "string" &&
|
|
601
|
+
Array.isArray(event.content) &&
|
|
602
|
+
event.content.every(isFunctionHookContent) &&
|
|
603
|
+
typeof event.isError === "boolean" &&
|
|
604
|
+
(event.details === undefined || isSafeFunctionHookValue(event.details))
|
|
605
|
+
);
|
|
606
|
+
case "input":
|
|
607
|
+
return (
|
|
608
|
+
typeof event.text === "string" &&
|
|
609
|
+
(event.images === undefined ||
|
|
610
|
+
(Array.isArray(event.images) && event.images.every(isFunctionHookImage))) &&
|
|
611
|
+
(event.source === "interactive" || event.source === "sdk" || event.source === "extension")
|
|
612
|
+
);
|
|
613
|
+
case "context":
|
|
614
|
+
return Array.isArray(event.messages) && event.messages.every(isFunctionHookMessage);
|
|
615
|
+
case "before_agent_start":
|
|
616
|
+
return (
|
|
617
|
+
typeof event.prompt === "string" &&
|
|
618
|
+
Array.isArray(event.systemPrompt) &&
|
|
619
|
+
event.systemPrompt.every(item => typeof item === "string") &&
|
|
620
|
+
(event.images === undefined || (Array.isArray(event.images) && event.images.every(isFunctionHookImage)))
|
|
621
|
+
);
|
|
622
|
+
case "before_provider_request":
|
|
623
|
+
return isSafeFunctionHookValue(event.payload);
|
|
624
|
+
case "after_provider_response":
|
|
625
|
+
return (
|
|
626
|
+
typeof event.status === "number" &&
|
|
627
|
+
Number.isFinite(event.status) &&
|
|
628
|
+
isSafeFunctionHookValue(event.headers) &&
|
|
629
|
+
(event.requestId === undefined || event.requestId === null || typeof event.requestId === "string") &&
|
|
630
|
+
(event.metadata === undefined || isSafeFunctionHookValue(event.metadata))
|
|
631
|
+
);
|
|
632
|
+
default:
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function functionHookEventIdentityMatches(original: ExtensionEvent, candidate: ExtensionEvent): boolean {
|
|
641
|
+
if (original.type !== candidate.type) return false;
|
|
642
|
+
if (original.type === "tool_call" && candidate.type === "tool_call")
|
|
643
|
+
return original.toolName === candidate.toolName && original.toolCallId === candidate.toolCallId;
|
|
644
|
+
if (original.type === "tool_result" && candidate.type === "tool_result")
|
|
645
|
+
return original.toolName === candidate.toolName && original.toolCallId === candidate.toolCallId;
|
|
646
|
+
if (original.type === "input" && candidate.type === "input") return original.source === candidate.source;
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function cloneFunctionHookData<T>(value: T): T {
|
|
651
|
+
return cloneFunctionHookDataStrict(value);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Security-boundary clone for transformed events; never aliases caller-owned data. */
|
|
655
|
+
export function cloneFunctionHookDataStrict<T>(value: T): T {
|
|
656
|
+
const cloned = structuredClone(value);
|
|
657
|
+
if (!isPlainFunctionHookData(cloned)) throw new Error("Function hook snapshot is not plain data");
|
|
658
|
+
return cloned;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function boundedUiKey(value: string): string {
|
|
662
|
+
return boundedText(value, 80);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function boundedUiLines(value: string[]): string[] {
|
|
666
|
+
return value.slice(0, 32).map(line => boundedText(line, 512));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function allowedUrl(value: string | URL, destinations: readonly string[]): URL | undefined {
|
|
670
|
+
try {
|
|
671
|
+
const url = new URL(value);
|
|
672
|
+
return url.protocol === "https:" && destinations.includes(url.origin) ? url : undefined;
|
|
673
|
+
} catch {
|
|
674
|
+
return undefined;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export interface FunctionHookCapabilityBindings {
|
|
679
|
+
readonly cwd: string;
|
|
680
|
+
readonly ui: ExtensionUIContext;
|
|
681
|
+
readonly sessionMetadata?: ExtensionSessionMetadata;
|
|
682
|
+
readonly isActive?: () => boolean;
|
|
683
|
+
readonly emitMessage: (
|
|
684
|
+
message: FunctionHookMessage,
|
|
685
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
686
|
+
) => void;
|
|
687
|
+
readonly appendAudit: (evidence: Record<string, unknown>) => void;
|
|
688
|
+
readonly fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
689
|
+
readonly readFile: (filePath: string, roots: readonly string[]) => Promise<string>;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function assertActive(bindings: FunctionHookCapabilityBindings): void {
|
|
693
|
+
if (bindings.isActive && !bindings.isActive()) throw new Error("Function hook invocation is no longer active");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function createFunctionHookCapabilities(
|
|
697
|
+
grant: FunctionHookGrant,
|
|
698
|
+
bindings: FunctionHookCapabilityBindings,
|
|
699
|
+
): FunctionHookCapabilities {
|
|
700
|
+
const operations = expandedOperations(grant.capabilities);
|
|
701
|
+
const result: FunctionHookCapabilities = {};
|
|
702
|
+
if (operations.has("tool.inspect") || operations.has("tool.transform") || operations.has("tool.deny")) {
|
|
703
|
+
Object.assign(result, {
|
|
704
|
+
tool: Object.freeze({
|
|
705
|
+
canInspect: operations.has("tool.inspect"),
|
|
706
|
+
canTransform: operations.has("tool.transform"),
|
|
707
|
+
canDeny: operations.has("tool.deny"),
|
|
708
|
+
}),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (
|
|
712
|
+
operations.has("ui.status") ||
|
|
713
|
+
operations.has("ui.widget") ||
|
|
714
|
+
operations.has("ui.notify") ||
|
|
715
|
+
operations.has("ui.transform")
|
|
716
|
+
) {
|
|
717
|
+
Object.assign(result, {
|
|
718
|
+
ui: Object.freeze({
|
|
719
|
+
canTransform: operations.has("ui.transform"),
|
|
720
|
+
setStatus: (key: string, text: string | undefined) => {
|
|
721
|
+
assertActive(bindings);
|
|
722
|
+
if (!operations.has("ui.status")) throw new Error("Function hook ui.status capability is required");
|
|
723
|
+
bindings.ui.setStatus(boundedUiKey(key), text === undefined ? undefined : boundedText(text, 512));
|
|
724
|
+
},
|
|
725
|
+
setWidget: (key: string, content: string[] | undefined) => {
|
|
726
|
+
assertActive(bindings);
|
|
727
|
+
if (!operations.has("ui.widget")) throw new Error("Function hook ui.widget capability is required");
|
|
728
|
+
bindings.ui.setWidget(boundedUiKey(key), content === undefined ? undefined : boundedUiLines(content));
|
|
729
|
+
},
|
|
730
|
+
notify: (message: string, type?: "info" | "warning" | "error") => {
|
|
731
|
+
assertActive(bindings);
|
|
732
|
+
if (!operations.has("ui.notify")) throw new Error("Function hook ui.notify capability is required");
|
|
733
|
+
bindings.ui.notify(boundedText(message, 512), type);
|
|
734
|
+
},
|
|
735
|
+
}),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (operations.has("session.read") || operations.has("session.message") || operations.has("session.deny")) {
|
|
739
|
+
const metadata =
|
|
740
|
+
operations.has("session.read") && bindings.sessionMetadata
|
|
741
|
+
? Object.freeze(cloneFunctionHookData(bindings.sessionMetadata))
|
|
742
|
+
: undefined;
|
|
743
|
+
Object.assign(result, {
|
|
744
|
+
session: Object.freeze({
|
|
745
|
+
canRead: operations.has("session.read"),
|
|
746
|
+
canDeny: operations.has("session.deny"),
|
|
747
|
+
metadata,
|
|
748
|
+
emitMessage: (
|
|
749
|
+
message: FunctionHookMessage,
|
|
750
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
751
|
+
) => {
|
|
752
|
+
assertActive(bindings);
|
|
753
|
+
if (!operations.has("session.message"))
|
|
754
|
+
throw new Error("Function hook session.message capability is required");
|
|
755
|
+
bindings.emitMessage(
|
|
756
|
+
{
|
|
757
|
+
customType: boundedUiKey(message.customType),
|
|
758
|
+
content:
|
|
759
|
+
typeof message.content === "string"
|
|
760
|
+
? boundedText(message.content, 2048)
|
|
761
|
+
: cloneFunctionHookData(message.content),
|
|
762
|
+
display: message.display ?? false,
|
|
763
|
+
details: redactFunctionHookValue(message.details),
|
|
764
|
+
attribution: message.attribution,
|
|
765
|
+
},
|
|
766
|
+
options ?? { triggerTurn: message.triggerTurn, deliverAs: message.deliverAs },
|
|
767
|
+
);
|
|
768
|
+
},
|
|
769
|
+
}),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (operations.has("audit.append")) {
|
|
773
|
+
Object.assign(result, {
|
|
774
|
+
audit: Object.freeze({
|
|
775
|
+
append: (evidence: Record<string, unknown>) => {
|
|
776
|
+
assertActive(bindings);
|
|
777
|
+
bindings.appendAudit(evidence);
|
|
778
|
+
},
|
|
779
|
+
}),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (operations.has("network.fetch")) {
|
|
783
|
+
Object.assign(result, {
|
|
784
|
+
network: Object.freeze({
|
|
785
|
+
fetch: (input: string | URL, init?: RequestInit) => {
|
|
786
|
+
assertActive(bindings);
|
|
787
|
+
const url = allowedUrl(input, grant.networkDestinations);
|
|
788
|
+
if (!url) throw new Error("Function hook network destination is outside its declared grant");
|
|
789
|
+
return bindings.fetch(url, { ...init, redirect: "error" });
|
|
790
|
+
},
|
|
791
|
+
}),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
if (operations.has("filesystem.read")) {
|
|
795
|
+
Object.assign(result, {
|
|
796
|
+
filesystem: Object.freeze({
|
|
797
|
+
readFile: (filePath: string) => {
|
|
798
|
+
assertActive(bindings);
|
|
799
|
+
return bindings.readFile(filePath, grant.filesystemRoots);
|
|
800
|
+
},
|
|
801
|
+
}),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return Object.freeze(result);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function compatibilityPayloadForFunctionHook(
|
|
808
|
+
event: ExtensionEvent,
|
|
809
|
+
grant: FunctionHookGrant,
|
|
810
|
+
wildcard: boolean,
|
|
811
|
+
): ExtensionEvent {
|
|
812
|
+
const operations = expandedOperations(grant.capabilities);
|
|
813
|
+
if ((event.type === "tool_call" || event.type === "tool_result") && !operations.has("tool.inspect")) {
|
|
814
|
+
const redacted = redactFunctionHookValue(event) as Record<string, unknown>;
|
|
815
|
+
redacted.input = "<redacted>";
|
|
816
|
+
if (event.type === "tool_result") {
|
|
817
|
+
redacted.content = "<redacted>";
|
|
818
|
+
redacted.details = "<redacted>";
|
|
819
|
+
}
|
|
820
|
+
return deepFreezeFunctionHookData(redacted) as unknown as ExtensionEvent;
|
|
821
|
+
}
|
|
822
|
+
const canInspectNonTool =
|
|
823
|
+
(event.type.startsWith("session_") && operations.has("session.read")) ||
|
|
824
|
+
(!event.type.startsWith("session_") && operations.has("ui.transform"));
|
|
825
|
+
if ((wildcard || !canInspectNonTool) && event.type !== "tool_call" && event.type !== "tool_result")
|
|
826
|
+
return Object.freeze({ type: event.type }) as ExtensionEvent;
|
|
827
|
+
return deepFreezeFunctionHookData(cloneFunctionHookData(event));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function functionHookInspectAllowed(
|
|
831
|
+
event: ExtensionEvent,
|
|
832
|
+
grant: FunctionHookGrant,
|
|
833
|
+
wildcard: boolean,
|
|
834
|
+
): boolean {
|
|
835
|
+
const operations = expandedOperations(grant.capabilities);
|
|
836
|
+
if (event.type === "tool_call" || event.type === "tool_result") return operations.has("tool.inspect");
|
|
837
|
+
if (wildcard) return false;
|
|
838
|
+
if (event.type.startsWith("session_")) return operations.has("session.read");
|
|
839
|
+
return operations.has("ui.transform");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function deepFreezeFunctionHookData<T>(value: T, seen = new WeakSet<object>()): T {
|
|
843
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) return value;
|
|
844
|
+
if (seen.has(value)) return value;
|
|
845
|
+
seen.add(value);
|
|
846
|
+
try {
|
|
847
|
+
for (const key of Object.keys(value as object)) {
|
|
848
|
+
const descriptor = Object.getOwnPropertyDescriptor(value as object, key);
|
|
849
|
+
if (descriptor && "value" in descriptor) deepFreezeFunctionHookData(descriptor.value, seen);
|
|
850
|
+
}
|
|
851
|
+
return Object.freeze(value);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
throw new Error("Function hook snapshot could not be frozen", { cause: error });
|
|
854
|
+
} finally {
|
|
855
|
+
seen.delete(value);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export function functionHookTransformAllowed(event: ExtensionEvent, grant: FunctionHookGrant): boolean {
|
|
860
|
+
const operations = expandedOperations(grant.capabilities);
|
|
861
|
+
if (event.type === "tool_call" || event.type === "tool_result") return operations.has("tool.transform");
|
|
862
|
+
if (
|
|
863
|
+
event.type === "input" ||
|
|
864
|
+
event.type === "context" ||
|
|
865
|
+
event.type === "before_agent_start" ||
|
|
866
|
+
event.type === "before_provider_request"
|
|
867
|
+
)
|
|
868
|
+
return operations.has("ui.transform");
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export function functionHookDenyAllowed(event: ExtensionEvent, grant: FunctionHookGrant): boolean {
|
|
873
|
+
const operations = expandedOperations(grant.capabilities);
|
|
874
|
+
if (event.type === "tool_call" || event.type === "tool_result") return operations.has("tool.deny");
|
|
875
|
+
if (
|
|
876
|
+
event.type === "input" ||
|
|
877
|
+
event.type === "before_agent_start" ||
|
|
878
|
+
event.type === "before_provider_request" ||
|
|
879
|
+
event.type === "context"
|
|
880
|
+
)
|
|
881
|
+
return operations.has("ui.transform");
|
|
882
|
+
return event.type.startsWith("session_before_") && operations.has("session.deny");
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function sanitizeFunctionHookReason(reason: unknown, fallback: string): string {
|
|
886
|
+
return typeof reason === "string" && reason.trim() ? boundedText(reason, 512) : fallback;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function hasOnlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
890
|
+
const allowed = new Set(keys);
|
|
891
|
+
return Object.keys(value).every(key => allowed.has(key));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** Validate terminal short-circuit values before they enter event-specific host control flow. */
|
|
895
|
+
export function isValidFunctionHookReturnValue(event: ExtensionEvent, value: unknown): boolean {
|
|
896
|
+
if (!isPlainFunctionHookData(value)) return false;
|
|
897
|
+
if (event.type === "context") return Array.isArray(value) && value.every(isFunctionHookMessage);
|
|
898
|
+
if (event.type === "before_provider_request") return value !== undefined;
|
|
899
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
900
|
+
const result = value as Record<string, unknown>;
|
|
901
|
+
switch (event.type) {
|
|
902
|
+
case "tool_call":
|
|
903
|
+
return (
|
|
904
|
+
hasOnlyKeys(result, ["block", "reason"]) &&
|
|
905
|
+
(result.block === undefined || typeof result.block === "boolean") &&
|
|
906
|
+
(result.reason === undefined || typeof result.reason === "string")
|
|
907
|
+
);
|
|
908
|
+
case "tool_result":
|
|
909
|
+
return (
|
|
910
|
+
hasOnlyKeys(result, ["content", "details", "isError"]) &&
|
|
911
|
+
(result.content === undefined ||
|
|
912
|
+
(Array.isArray(result.content) && result.content.every(isFunctionHookContent))) &&
|
|
913
|
+
(result.details === undefined || isSafeFunctionHookValue(result.details)) &&
|
|
914
|
+
(result.isError === undefined || typeof result.isError === "boolean")
|
|
915
|
+
);
|
|
916
|
+
case "input":
|
|
917
|
+
return (
|
|
918
|
+
hasOnlyKeys(result, ["handled", "text", "images"]) &&
|
|
919
|
+
(result.handled === undefined || typeof result.handled === "boolean") &&
|
|
920
|
+
(result.text === undefined || typeof result.text === "string") &&
|
|
921
|
+
(result.images === undefined || (Array.isArray(result.images) && result.images.every(isFunctionHookImage)))
|
|
922
|
+
);
|
|
923
|
+
case "before_agent_start":
|
|
924
|
+
return (
|
|
925
|
+
hasOnlyKeys(result, ["messages", "systemPrompt", "prompt", "images"]) &&
|
|
926
|
+
(result.messages === undefined ||
|
|
927
|
+
(Array.isArray(result.messages) && result.messages.every(isFunctionHookCustomMessage))) &&
|
|
928
|
+
(result.systemPrompt === undefined ||
|
|
929
|
+
(Array.isArray(result.systemPrompt) && result.systemPrompt.every(item => typeof item === "string"))) &&
|
|
930
|
+
(result.prompt === undefined || typeof result.prompt === "string") &&
|
|
931
|
+
(result.images === undefined || (Array.isArray(result.images) && result.images.every(isFunctionHookImage)))
|
|
932
|
+
);
|
|
933
|
+
case "session_before_switch":
|
|
934
|
+
return hasOnlyKeys(result, ["cancel"]) && (result.cancel === undefined || typeof result.cancel === "boolean");
|
|
935
|
+
case "session_before_branch":
|
|
936
|
+
return (
|
|
937
|
+
hasOnlyKeys(result, ["cancel", "skipConversationRestore"]) &&
|
|
938
|
+
(result.cancel === undefined || typeof result.cancel === "boolean") &&
|
|
939
|
+
(result.skipConversationRestore === undefined || typeof result.skipConversationRestore === "boolean")
|
|
940
|
+
);
|
|
941
|
+
case "session_before_compact":
|
|
942
|
+
return hasOnlyKeys(result, ["cancel"]) && (result.cancel === undefined || typeof result.cancel === "boolean");
|
|
943
|
+
case "session_before_tree": {
|
|
944
|
+
if (!hasOnlyKeys(result, ["cancel", "summary"])) return false;
|
|
945
|
+
if (result.cancel !== undefined && typeof result.cancel !== "boolean") return false;
|
|
946
|
+
if (result.summary === undefined) return true;
|
|
947
|
+
if (result.summary === null || typeof result.summary !== "object" || Array.isArray(result.summary))
|
|
948
|
+
return false;
|
|
949
|
+
const summary = result.summary as Record<string, unknown>;
|
|
950
|
+
return (
|
|
951
|
+
hasOnlyKeys(summary, ["summary", "details"]) &&
|
|
952
|
+
typeof summary.summary === "string" &&
|
|
953
|
+
(summary.details === undefined || isSafeFunctionHookValue(summary.details))
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
default:
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
}
|