@gajae-code/coding-agent 0.16.4 → 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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- 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/helpers.d.ts +33 -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/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -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 +12 -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/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -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-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/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 +4 -2
- 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.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -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 +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- 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/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- 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 +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- 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 +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- 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/session-state-lock.ts +51 -8
- 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 +6 -6
- 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 +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- 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 +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- 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 +35 -5
- 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 +387 -48
- 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 +2 -2
- 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 +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- 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 +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- 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/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import * as path from "node:path";
|
|
4
8
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
5
9
|
import type { AttemptScope } from "@gajae-code/agent-core/attempt-scope";
|
|
6
10
|
import type {
|
|
@@ -18,6 +22,37 @@ import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/workflow
|
|
|
18
22
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
19
23
|
import type { AttemptRecordStore } from "../../session/attempt-record-store";
|
|
20
24
|
import { createReadonlySessionManager, type SessionManager } from "../../session/session-manager";
|
|
25
|
+
import {
|
|
26
|
+
attenuateFunctionHookGrant,
|
|
27
|
+
cloneFunctionHookDataStrict,
|
|
28
|
+
compatibilityPayloadForFunctionHook,
|
|
29
|
+
createFunctionHookCapabilities,
|
|
30
|
+
type FunctionHook,
|
|
31
|
+
type FunctionHookAuditRecord,
|
|
32
|
+
type FunctionHookCapability,
|
|
33
|
+
type FunctionHookCapabilityBindings,
|
|
34
|
+
type FunctionHookGrant,
|
|
35
|
+
type FunctionHookInvocation,
|
|
36
|
+
type FunctionHookNext,
|
|
37
|
+
type FunctionHookRegistration,
|
|
38
|
+
type FunctionHookResult,
|
|
39
|
+
functionHookDenyAllowed,
|
|
40
|
+
functionHookEventIdentityMatches,
|
|
41
|
+
functionHookGrantHash,
|
|
42
|
+
functionHookInspectAllowed,
|
|
43
|
+
functionHookPayloadHash,
|
|
44
|
+
functionHookTransformAllowed,
|
|
45
|
+
isPlainFunctionHookData,
|
|
46
|
+
isValidFunctionHookEventValue,
|
|
47
|
+
isValidFunctionHookReturnValue,
|
|
48
|
+
redactFunctionHookValue,
|
|
49
|
+
sanitizeFunctionHookReason,
|
|
50
|
+
} from "./function-hooks";
|
|
51
|
+
import {
|
|
52
|
+
getExtensionHandlerRegistrationOrder,
|
|
53
|
+
getFunctionHookRegistration,
|
|
54
|
+
readConstrainedFunctionHookFile,
|
|
55
|
+
} from "./function-hooks-internal";
|
|
21
56
|
import type {
|
|
22
57
|
AfterProviderResponseEvent,
|
|
23
58
|
BeforeAgentStartEvent,
|
|
@@ -67,6 +102,8 @@ import { createExtensionSettings } from "./types";
|
|
|
67
102
|
interface BeforeAgentStartCombinedResult {
|
|
68
103
|
messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
|
|
69
104
|
systemPrompt?: string[];
|
|
105
|
+
prompt?: string;
|
|
106
|
+
images?: ImageContent[];
|
|
70
107
|
}
|
|
71
108
|
|
|
72
109
|
const BEFORE_AGENT_START_RESULT_KEYS = new Set(["message", "systemPrompt"]);
|
|
@@ -153,7 +190,15 @@ type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "
|
|
|
153
190
|
? SessionCompactingResult | undefined
|
|
154
191
|
: undefined;
|
|
155
192
|
type Handler = Extension["handlers"] extends Map<string, Array<infer T>> ? T : never;
|
|
156
|
-
type IndexedHandler = { ext: Extension; handler: Handler };
|
|
193
|
+
type IndexedHandler = { ext: Extension; handler: Handler; registrationOrder: number };
|
|
194
|
+
type IndexedFunctionHook = IndexedHandler & { registration: FunctionHookRegistration };
|
|
195
|
+
|
|
196
|
+
export type FunctionHookDispatchResult<TEvent extends ExtensionEvent> =
|
|
197
|
+
| { action: "continue"; event: TEvent; transformed?: boolean }
|
|
198
|
+
| { action: "deny"; reason: string }
|
|
199
|
+
| { action: "return"; value: unknown };
|
|
200
|
+
|
|
201
|
+
export type FunctionHookAuditSink = (record: FunctionHookAuditRecord) => void;
|
|
157
202
|
|
|
158
203
|
export type NewSessionHandler = (options?: {
|
|
159
204
|
parentSession?: string;
|
|
@@ -218,6 +263,10 @@ export class ExtensionRunner {
|
|
|
218
263
|
#errorListeners: Set<ExtensionErrorListener> = new Set();
|
|
219
264
|
#handlersByEvent: Map<string, IndexedHandler[]> = new Map();
|
|
220
265
|
#attemptRecordStore: AttemptRecordStore | undefined;
|
|
266
|
+
#functionHookAuditSequence = 0;
|
|
267
|
+
#functionHookAudit: FunctionHookAuditRecord[] = [];
|
|
268
|
+
#functionHookAuditSink: FunctionHookAuditSink | undefined;
|
|
269
|
+
#functionHookDepth = new AsyncLocalStorage<number>();
|
|
221
270
|
|
|
222
271
|
#getModel: () => Model | undefined = () => undefined;
|
|
223
272
|
#getCredentialSessionId: () => string = () => "";
|
|
@@ -263,6 +312,7 @@ export class ExtensionRunner {
|
|
|
263
312
|
|
|
264
313
|
#getJobsFn: ExtensionContextActions["getJobs"] = undefined;
|
|
265
314
|
#onJobFoldFn: ExtensionContextActions["onJobFold"] = undefined;
|
|
315
|
+
#onSessionEventFn: ExtensionContextActions["onSessionEvent"] = undefined;
|
|
266
316
|
#sdkControlFn: ExtensionContextActions["sdkControl"] = undefined;
|
|
267
317
|
#setSdkPermissionProviderFn: ExtensionContextActions["setSdkPermissionProvider"] = undefined;
|
|
268
318
|
#setSdkClientBridgeFn: ExtensionContextActions["setSdkClientBridge"] = undefined;
|
|
@@ -305,16 +355,36 @@ export class ExtensionRunner {
|
|
|
305
355
|
|
|
306
356
|
static #indexHandlers(extensions: Extension[]): Map<string, IndexedHandler[]> {
|
|
307
357
|
const handlersByEvent = new Map<string, IndexedHandler[]>();
|
|
358
|
+
let registrationOrder = 0;
|
|
308
359
|
for (const ext of extensions) {
|
|
360
|
+
const extensionHandlers: Array<{
|
|
361
|
+
eventType: string;
|
|
362
|
+
handler: Handler;
|
|
363
|
+
index: number;
|
|
364
|
+
explicitOrder?: number;
|
|
365
|
+
}> = [];
|
|
366
|
+
let index = 0;
|
|
309
367
|
for (const [eventType, handlers] of ext.handlers) {
|
|
368
|
+
for (const handler of handlers) {
|
|
369
|
+
const explicitOrder =
|
|
370
|
+
getExtensionHandlerRegistrationOrder(handler) ??
|
|
371
|
+
getFunctionHookRegistration(handler)?.registrationOrder;
|
|
372
|
+
extensionHandlers.push({ eventType, handler, index: index++, explicitOrder });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
extensionHandlers.sort((a, b) => {
|
|
376
|
+
if (a.explicitOrder !== undefined && b.explicitOrder !== undefined)
|
|
377
|
+
return a.explicitOrder - b.explicitOrder;
|
|
378
|
+
return a.index - b.index;
|
|
379
|
+
});
|
|
380
|
+
for (const { eventType, handler } of extensionHandlers) {
|
|
310
381
|
let indexedHandlers = handlersByEvent.get(eventType);
|
|
311
382
|
if (!indexedHandlers) {
|
|
312
383
|
indexedHandlers = [];
|
|
313
384
|
handlersByEvent.set(eventType, indexedHandlers);
|
|
314
385
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
386
|
+
indexedHandlers.push({ ext, handler, registrationOrder });
|
|
387
|
+
registrationOrder += 1;
|
|
318
388
|
}
|
|
319
389
|
}
|
|
320
390
|
return handlersByEvent;
|
|
@@ -395,6 +465,7 @@ export class ExtensionRunner {
|
|
|
395
465
|
|
|
396
466
|
this.#getJobsFn = contextActions.getJobs;
|
|
397
467
|
this.#onJobFoldFn = contextActions.onJobFold;
|
|
468
|
+
this.#onSessionEventFn = contextActions.onSessionEvent;
|
|
398
469
|
this.#sdkControlFn = contextActions.sdkControl;
|
|
399
470
|
this.#setSdkPermissionProviderFn = contextActions.setSdkPermissionProvider;
|
|
400
471
|
this.#setSdkClientBridgeFn = contextActions.setSdkClientBridge;
|
|
@@ -556,7 +627,45 @@ export class ExtensionRunner {
|
|
|
556
627
|
}
|
|
557
628
|
|
|
558
629
|
hasHandlers(eventType: string): boolean {
|
|
559
|
-
return (
|
|
630
|
+
return (
|
|
631
|
+
(this.#handlersByEvent.get(eventType)?.length ?? 0) > 0 ||
|
|
632
|
+
(this.#handlersByEvent.get("*")?.some(({ handler }) => getFunctionHookRegistration(handler) !== undefined) ??
|
|
633
|
+
false)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
hasToolResultMediation(toolName: string): boolean {
|
|
638
|
+
for (const indexed of this.#matchingHandlers({ type: "tool_result", toolName } as ToolResultEvent, true)) {
|
|
639
|
+
const registration = getFunctionHookRegistration(indexed.handler);
|
|
640
|
+
if (!registration) return true;
|
|
641
|
+
const capabilities = new Set(registration.grant.capabilities);
|
|
642
|
+
if (capabilities.has("tool") || capabilities.has("tool.transform") || capabilities.has("tool.deny")) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Return immutable registration metadata in authoritative registration order. */
|
|
650
|
+
getFunctionHookRegistrations(): readonly FunctionHookRegistration[] {
|
|
651
|
+
const registrations: Array<{ order: number; registration: FunctionHookRegistration }> = [];
|
|
652
|
+
for (const handlers of this.#handlersByEvent.values()) {
|
|
653
|
+
for (const indexed of handlers) {
|
|
654
|
+
const registration = getFunctionHookRegistration(indexed.handler);
|
|
655
|
+
if (registration) registrations.push({ order: indexed.registrationOrder, registration });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
registrations.sort((a, b) => a.order - b.order);
|
|
659
|
+
return registrations.map(({ registration }) => registration);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Install the optional deterministic audit sink used by hosts and tests. */
|
|
663
|
+
setFunctionHookAuditSink(sink: FunctionHookAuditSink | undefined): void {
|
|
664
|
+
this.#functionHookAuditSink = sink;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
getFunctionHookAudit(): readonly FunctionHookAuditRecord[] {
|
|
668
|
+
return this.#functionHookAudit.map(record => cloneFunctionHookDataStrict(record));
|
|
560
669
|
}
|
|
561
670
|
|
|
562
671
|
setAttemptRecordStore(store: AttemptRecordStore): void {
|
|
@@ -680,6 +789,7 @@ export class ExtensionRunner {
|
|
|
680
789
|
|
|
681
790
|
getJobs: () => this.#getJobsFn?.(),
|
|
682
791
|
onJobFold: listener => this.#onJobFoldFn?.(listener) ?? (() => {}),
|
|
792
|
+
onSessionEvent: listener => this.#onSessionEventFn?.(listener) ?? (() => {}),
|
|
683
793
|
sdkControl: (operation, input) => this.#sdkControlFn?.(operation, input),
|
|
684
794
|
setSdkPermissionProvider: provider => this.#setSdkPermissionProviderFn?.(provider),
|
|
685
795
|
setSdkClientBridge: bridge => this.#setSdkClientBridgeFn?.(bridge),
|
|
@@ -732,6 +842,496 @@ export class ExtensionRunner {
|
|
|
732
842
|
};
|
|
733
843
|
}
|
|
734
844
|
|
|
845
|
+
#matchingFunctionHooks(event: ExtensionEvent): IndexedFunctionHook[] {
|
|
846
|
+
return this.#matchingHandlers(event, false).map(indexed => ({
|
|
847
|
+
...indexed,
|
|
848
|
+
registration: getFunctionHookRegistration(indexed.handler)!,
|
|
849
|
+
}));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#matchingHandlers(event: ExtensionEvent, includeLegacyToolHandlers: boolean): IndexedHandler[] {
|
|
853
|
+
const exact = this.#handlersByEvent.get(event.type) ?? [];
|
|
854
|
+
const wildcard = this.#handlersByEvent.get("*") ?? [];
|
|
855
|
+
const matches: IndexedHandler[] = [];
|
|
856
|
+
for (const indexed of [...exact, ...wildcard]) {
|
|
857
|
+
const registration = getFunctionHookRegistration(indexed.handler);
|
|
858
|
+
if (!registration) {
|
|
859
|
+
if (includeLegacyToolHandlers && indexed.registrationOrder >= 0 && exact.includes(indexed))
|
|
860
|
+
matches.push(indexed);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (registration.event !== "*" && registration.event !== event.type) continue;
|
|
864
|
+
if (
|
|
865
|
+
registration.target !== undefined &&
|
|
866
|
+
registration.target !== "*" &&
|
|
867
|
+
((event.type !== "tool_call" && event.type !== "tool_result") || event.toolName !== registration.target)
|
|
868
|
+
) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
matches.push(indexed);
|
|
872
|
+
}
|
|
873
|
+
matches.sort((a, b) => a.registrationOrder - b.registrationOrder);
|
|
874
|
+
return matches;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
#functionHookAuditPath(registration: FunctionHookRegistration): string {
|
|
878
|
+
return path.basename(registration.provenance.path).slice(0, 256);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
#appendFunctionHookAudit(
|
|
882
|
+
registration: FunctionHookRegistration,
|
|
883
|
+
invocation: FunctionHookInvocation,
|
|
884
|
+
action: FunctionHookAuditRecord["action"],
|
|
885
|
+
reason?: string,
|
|
886
|
+
effectiveGrant: FunctionHookGrant = registration.grant,
|
|
887
|
+
evidence?: unknown,
|
|
888
|
+
): void {
|
|
889
|
+
const record: FunctionHookAuditRecord = {
|
|
890
|
+
sequence: this.#functionHookAuditSequence,
|
|
891
|
+
eventId: invocation.eventId,
|
|
892
|
+
correlationId: invocation.correlationId,
|
|
893
|
+
eventType: invocation.eventType,
|
|
894
|
+
action,
|
|
895
|
+
registrationOrder: invocation.registrationOrder,
|
|
896
|
+
provenance: {
|
|
897
|
+
source: registration.provenance.source,
|
|
898
|
+
scope: registration.provenance.scope,
|
|
899
|
+
plugin: registration.provenance.plugin,
|
|
900
|
+
extensionId: registration.provenance.extensionId,
|
|
901
|
+
path: this.#functionHookAuditPath(registration),
|
|
902
|
+
},
|
|
903
|
+
payloadHash: functionHookPayloadHash(invocation.payload),
|
|
904
|
+
requestedCapabilities: [...registration.grant.capabilities],
|
|
905
|
+
effectiveCapabilities: [...effectiveGrant.capabilities],
|
|
906
|
+
capabilityHash: functionHookGrantHash(effectiveGrant),
|
|
907
|
+
...(registration.provenance.activationGeneration === undefined
|
|
908
|
+
? {}
|
|
909
|
+
: { activationGeneration: registration.provenance.activationGeneration }),
|
|
910
|
+
...(reason === undefined ? {} : { reason: sanitizeFunctionHookReason(reason, "Function hook decision") }),
|
|
911
|
+
...(evidence === undefined ? {} : { evidence: redactFunctionHookValue(evidence) }),
|
|
912
|
+
};
|
|
913
|
+
this.#functionHookAuditSequence += 1;
|
|
914
|
+
const frozenEvidence =
|
|
915
|
+
record.evidence !== null && typeof record.evidence === "object"
|
|
916
|
+
? Object.freeze(cloneFunctionHookDataStrict(record.evidence))
|
|
917
|
+
: record.evidence;
|
|
918
|
+
const frozenRecord = Object.freeze({
|
|
919
|
+
...record,
|
|
920
|
+
provenance: Object.freeze(record.provenance),
|
|
921
|
+
...(record.evidence === undefined ? {} : { evidence: frozenEvidence }),
|
|
922
|
+
});
|
|
923
|
+
this.#functionHookAudit.push(frozenRecord);
|
|
924
|
+
if (this.#functionHookAudit.length > 1024) this.#functionHookAudit.shift();
|
|
925
|
+
this.#functionHookAuditSink?.(frozenRecord);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
#functionHookErrorResult(event: ExtensionEvent, reason: string): FunctionHookDispatchResult<ExtensionEvent> {
|
|
929
|
+
if (
|
|
930
|
+
event.type === "tool_call" ||
|
|
931
|
+
event.type === "tool_result" ||
|
|
932
|
+
event.type === "before_provider_request" ||
|
|
933
|
+
event.type.startsWith("session_before_")
|
|
934
|
+
) {
|
|
935
|
+
return { action: "deny", reason };
|
|
936
|
+
}
|
|
937
|
+
return { action: "continue", event };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async #runFunctionHookWithTimeout(
|
|
941
|
+
hook: FunctionHook,
|
|
942
|
+
invocation: FunctionHookInvocation,
|
|
943
|
+
capabilities: ReturnType<typeof createFunctionHookCapabilities>,
|
|
944
|
+
next: FunctionHookNext,
|
|
945
|
+
parentSignal: AbortSignal,
|
|
946
|
+
ext: Extension,
|
|
947
|
+
controller: AbortController,
|
|
948
|
+
): Promise<
|
|
949
|
+
{ status: "ok"; value: unknown } | { status: "timeout" } | { status: "error"; error: string; stack?: string }
|
|
950
|
+
> {
|
|
951
|
+
const abortFromParent = () => controller.abort(parentSignal.reason);
|
|
952
|
+
if (parentSignal.aborted) abortFromParent();
|
|
953
|
+
else parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
954
|
+
const childInvocation = Object.freeze({ ...invocation, signal: controller.signal });
|
|
955
|
+
let timer: NodeJS.Timeout | undefined;
|
|
956
|
+
const timeout = new Promise<"timeout">(resolve => {
|
|
957
|
+
timer = setTimeout(() => {
|
|
958
|
+
controller.abort(new Error("Function hook timed out"));
|
|
959
|
+
resolve("timeout");
|
|
960
|
+
}, extensionHandlerTimeoutMs);
|
|
961
|
+
});
|
|
962
|
+
const { promise: aborted, resolve: resolveAborted } = Promise.withResolvers<"aborted">();
|
|
963
|
+
const resolveAbort = () => resolveAborted("aborted");
|
|
964
|
+
if (parentSignal.aborted) resolveAborted("aborted");
|
|
965
|
+
else parentSignal.addEventListener("abort", resolveAbort, { once: true });
|
|
966
|
+
try {
|
|
967
|
+
const value = await Promise.race([
|
|
968
|
+
Promise.resolve()
|
|
969
|
+
.then(() => hook(childInvocation, capabilities, next))
|
|
970
|
+
.then(result => ({ status: "ok" as const, value: result })),
|
|
971
|
+
timeout.then(status => ({ status })),
|
|
972
|
+
aborted.then(status => ({ status })),
|
|
973
|
+
]);
|
|
974
|
+
if (value.status === "timeout" || value.status === "aborted") {
|
|
975
|
+
const error =
|
|
976
|
+
value.status === "timeout"
|
|
977
|
+
? `handler timed out after ${extensionHandlerTimeoutMs}ms`
|
|
978
|
+
: "handler aborted";
|
|
979
|
+
this.emitError({ extensionPath: ext.path, event: invocation.eventType, error });
|
|
980
|
+
return { status: "timeout" };
|
|
981
|
+
}
|
|
982
|
+
return value;
|
|
983
|
+
} catch (error) {
|
|
984
|
+
const message = sanitizeFunctionHookReason(
|
|
985
|
+
error instanceof Error ? error.message : String(error),
|
|
986
|
+
"Function hook failed",
|
|
987
|
+
);
|
|
988
|
+
const stack =
|
|
989
|
+
error instanceof Error && error.stack
|
|
990
|
+
? sanitizeFunctionHookReason(error.stack, "Function hook stack unavailable")
|
|
991
|
+
: undefined;
|
|
992
|
+
this.emitError({ extensionPath: ext.path, event: invocation.eventType, error: message, stack });
|
|
993
|
+
return { status: "error", error: message, stack };
|
|
994
|
+
} finally {
|
|
995
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
996
|
+
parentSignal.removeEventListener("abort", abortFromParent);
|
|
997
|
+
parentSignal.removeEventListener("abort", resolveAbort);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Execute capability-scoped middleware stored in Extension.handlers. */
|
|
1002
|
+
async emitFunctionHooks<TEvent extends ExtensionEvent>(
|
|
1003
|
+
event: TEvent,
|
|
1004
|
+
options: {
|
|
1005
|
+
signal?: AbortSignal;
|
|
1006
|
+
correlationId?: string;
|
|
1007
|
+
scope?: AttemptScopeRef;
|
|
1008
|
+
legacyHandler?: (handler: IndexedHandler, event: TEvent) => Promise<FunctionHookDispatchResult<TEvent>>;
|
|
1009
|
+
} = {},
|
|
1010
|
+
): Promise<FunctionHookDispatchResult<TEvent>> {
|
|
1011
|
+
const handlers = options.legacyHandler ? this.#matchingHandlers(event, true) : this.#matchingFunctionHooks(event);
|
|
1012
|
+
if (handlers.length === 0) return { action: "continue", event };
|
|
1013
|
+
const includesLegacy = handlers.some(indexed => getFunctionHookRegistration(indexed.handler) === undefined);
|
|
1014
|
+
if (includesLegacy) this.#requireScopeOrFailClosed(options.scope, event.type);
|
|
1015
|
+
this.#markAttemptExecuted(options.scope);
|
|
1016
|
+
const functionHookDepth = this.#functionHookDepth.getStore() ?? 0;
|
|
1017
|
+
if (functionHookDepth >= 16) {
|
|
1018
|
+
const reason = "Function hook re-entry depth exceeded";
|
|
1019
|
+
this.emitError({ extensionPath: "<function-hooks>", event: event.type, error: reason });
|
|
1020
|
+
return this.#functionHookErrorResult(event, reason) as FunctionHookDispatchResult<TEvent>;
|
|
1021
|
+
}
|
|
1022
|
+
const parentSignal = options.signal ?? new AbortController().signal;
|
|
1023
|
+
const eventId = randomUUID();
|
|
1024
|
+
const correlationId = options.correlationId ?? eventId;
|
|
1025
|
+
return await this.#functionHookDepth.run(functionHookDepth + 1, async () => {
|
|
1026
|
+
const invoke = async (
|
|
1027
|
+
index: number,
|
|
1028
|
+
currentEvent: TEvent,
|
|
1029
|
+
removedCapabilities: readonly FunctionHookCapability[],
|
|
1030
|
+
chainSignal: AbortSignal,
|
|
1031
|
+
): Promise<FunctionHookDispatchResult<TEvent>> => {
|
|
1032
|
+
if (index >= handlers.length) return { action: "continue", event: currentEvent, transformed: false };
|
|
1033
|
+
if (chainSignal.aborted)
|
|
1034
|
+
return this.#functionHookErrorResult(
|
|
1035
|
+
currentEvent,
|
|
1036
|
+
"Function hook chain aborted",
|
|
1037
|
+
) as FunctionHookDispatchResult<TEvent>;
|
|
1038
|
+
const indexed = handlers[index];
|
|
1039
|
+
if (!indexed) return { action: "continue", event: currentEvent, transformed: false };
|
|
1040
|
+
const registration = getFunctionHookRegistration(indexed.handler);
|
|
1041
|
+
if (!registration) {
|
|
1042
|
+
const legacyDispatch = await options.legacyHandler!(indexed, currentEvent);
|
|
1043
|
+
if (legacyDispatch.action !== "continue") return legacyDispatch;
|
|
1044
|
+
const downstream = await invoke(index + 1, legacyDispatch.event, removedCapabilities, chainSignal);
|
|
1045
|
+
return legacyDispatch.transformed === true && downstream.action === "continue"
|
|
1046
|
+
? { ...downstream, transformed: true }
|
|
1047
|
+
: downstream;
|
|
1048
|
+
}
|
|
1049
|
+
const effectiveGrant = attenuateFunctionHookGrant(registration.grant, removedCapabilities);
|
|
1050
|
+
const downstreamRemoved = [
|
|
1051
|
+
...new Set([...removedCapabilities, ...(registration.grant.attenuateDownstream ?? [])]),
|
|
1052
|
+
];
|
|
1053
|
+
const wildcard = registration.event === "*" || registration.target === "*";
|
|
1054
|
+
let nextCalled = false;
|
|
1055
|
+
let nextFailureReason: string | undefined;
|
|
1056
|
+
let nextPromise: Promise<FunctionHookDispatchResult<TEvent>> | undefined;
|
|
1057
|
+
let nextReturnValue: FunctionHookResult | undefined;
|
|
1058
|
+
const failureResult = async (reason: string): Promise<FunctionHookDispatchResult<TEvent>> => {
|
|
1059
|
+
if (functionHookDenyAllowed(currentEvent, effectiveGrant)) return { action: "deny", reason };
|
|
1060
|
+
if (wildcard) {
|
|
1061
|
+
if (nextPromise) return await nextPromise;
|
|
1062
|
+
return await invoke(index + 1, currentEvent, downstreamRemoved, chainSignal);
|
|
1063
|
+
}
|
|
1064
|
+
return this.#functionHookErrorResult(currentEvent, reason) as FunctionHookDispatchResult<TEvent>;
|
|
1065
|
+
};
|
|
1066
|
+
let payload: Readonly<TEvent>;
|
|
1067
|
+
try {
|
|
1068
|
+
payload = Object.freeze(
|
|
1069
|
+
compatibilityPayloadForFunctionHook(currentEvent, effectiveGrant, wildcard),
|
|
1070
|
+
) as Readonly<TEvent>;
|
|
1071
|
+
} catch {
|
|
1072
|
+
const reason = "Function hook payload could not be snapshotted";
|
|
1073
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1074
|
+
return failureResult(reason);
|
|
1075
|
+
}
|
|
1076
|
+
const controller = new AbortController();
|
|
1077
|
+
const abortFromChain = () => controller.abort(chainSignal.reason);
|
|
1078
|
+
if (chainSignal.aborted) abortFromChain();
|
|
1079
|
+
else chainSignal.addEventListener("abort", abortFromChain, { once: true });
|
|
1080
|
+
const invocation = Object.freeze({
|
|
1081
|
+
eventId,
|
|
1082
|
+
correlationId,
|
|
1083
|
+
eventType: currentEvent.type,
|
|
1084
|
+
provenance: Object.freeze({ ...registration.provenance }),
|
|
1085
|
+
registrationOrder: indexed.registrationOrder,
|
|
1086
|
+
payload,
|
|
1087
|
+
signal: controller.signal,
|
|
1088
|
+
}) as FunctionHookInvocation<TEvent>;
|
|
1089
|
+
let active = true;
|
|
1090
|
+
const bindings: FunctionHookCapabilityBindings = {
|
|
1091
|
+
cwd: this.cwd,
|
|
1092
|
+
ui: this.#uiContext,
|
|
1093
|
+
sessionMetadata: this.sessionMetadata,
|
|
1094
|
+
isActive: () => active && !chainSignal.aborted && !controller.signal.aborted,
|
|
1095
|
+
emitMessage: (message, options) => {
|
|
1096
|
+
const { triggerTurn: _triggerTurn, deliverAs: _deliverAs, ...payload } = message;
|
|
1097
|
+
this.runtime.sendMessage(
|
|
1098
|
+
{
|
|
1099
|
+
...payload,
|
|
1100
|
+
display: message.display ?? false,
|
|
1101
|
+
},
|
|
1102
|
+
options,
|
|
1103
|
+
);
|
|
1104
|
+
},
|
|
1105
|
+
appendAudit: evidence => {
|
|
1106
|
+
this.#appendFunctionHookAudit(
|
|
1107
|
+
registration,
|
|
1108
|
+
invocation,
|
|
1109
|
+
"continue",
|
|
1110
|
+
undefined,
|
|
1111
|
+
effectiveGrant,
|
|
1112
|
+
evidence,
|
|
1113
|
+
);
|
|
1114
|
+
},
|
|
1115
|
+
fetch: (input, init) => fetch(input, init),
|
|
1116
|
+
readFile: (filePath, roots) => readConstrainedFunctionHookFile(filePath, this.cwd, roots),
|
|
1117
|
+
};
|
|
1118
|
+
const capabilities = createFunctionHookCapabilities(effectiveGrant, bindings);
|
|
1119
|
+
const next: FunctionHookNext = async nextEvent => {
|
|
1120
|
+
if (nextCalled) throw new Error("Function hook next() may only be called once");
|
|
1121
|
+
nextCalled = true;
|
|
1122
|
+
const candidate = (nextEvent ?? currentEvent) as TEvent;
|
|
1123
|
+
if (
|
|
1124
|
+
nextEvent !== undefined &&
|
|
1125
|
+
(!isPlainFunctionHookData(candidate) ||
|
|
1126
|
+
!functionHookEventIdentityMatches(currentEvent, candidate) ||
|
|
1127
|
+
!isValidFunctionHookEventValue(candidate) ||
|
|
1128
|
+
!functionHookTransformAllowed(currentEvent, effectiveGrant))
|
|
1129
|
+
) {
|
|
1130
|
+
nextFailureReason = "Function hook passed an invalid or unauthorized event to next()";
|
|
1131
|
+
throw new Error(nextFailureReason);
|
|
1132
|
+
}
|
|
1133
|
+
let candidateSnapshot = currentEvent;
|
|
1134
|
+
if (nextEvent !== undefined) {
|
|
1135
|
+
try {
|
|
1136
|
+
candidateSnapshot = cloneFunctionHookDataStrict(candidate);
|
|
1137
|
+
} catch {
|
|
1138
|
+
nextFailureReason = "Function hook replacement event could not be snapshotted";
|
|
1139
|
+
throw new Error(nextFailureReason);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
nextPromise = invoke(index + 1, candidateSnapshot, downstreamRemoved, controller.signal).then(
|
|
1143
|
+
downstreamResult =>
|
|
1144
|
+
nextEvent !== undefined && downstreamResult.action === "continue"
|
|
1145
|
+
? { ...downstreamResult, transformed: true }
|
|
1146
|
+
: downstreamResult,
|
|
1147
|
+
);
|
|
1148
|
+
const continuedResult = await nextPromise;
|
|
1149
|
+
try {
|
|
1150
|
+
const publicResult =
|
|
1151
|
+
continuedResult.action === "continue"
|
|
1152
|
+
? {
|
|
1153
|
+
action: "continue" as const,
|
|
1154
|
+
event: compatibilityPayloadForFunctionHook(
|
|
1155
|
+
continuedResult.event,
|
|
1156
|
+
effectiveGrant,
|
|
1157
|
+
wildcard,
|
|
1158
|
+
),
|
|
1159
|
+
}
|
|
1160
|
+
: continuedResult.action === "return" &&
|
|
1161
|
+
!functionHookInspectAllowed(currentEvent, effectiveGrant, wildcard)
|
|
1162
|
+
? { action: "return" as const, value: "<redacted>" }
|
|
1163
|
+
: continuedResult;
|
|
1164
|
+
nextReturnValue = cloneFunctionHookDataStrict(publicResult) as FunctionHookResult;
|
|
1165
|
+
return nextReturnValue;
|
|
1166
|
+
} catch {
|
|
1167
|
+
nextFailureReason = "Function hook continuation result could not be snapshotted";
|
|
1168
|
+
throw new Error(nextFailureReason);
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const outcome = await this.#runFunctionHookWithTimeout(
|
|
1172
|
+
registration.handler,
|
|
1173
|
+
invocation,
|
|
1174
|
+
capabilities,
|
|
1175
|
+
next as FunctionHookNext,
|
|
1176
|
+
chainSignal,
|
|
1177
|
+
indexed.ext,
|
|
1178
|
+
controller,
|
|
1179
|
+
);
|
|
1180
|
+
if (outcome.status === "timeout") {
|
|
1181
|
+
controller.abort(new Error("Function hook invocation timed out"));
|
|
1182
|
+
active = false;
|
|
1183
|
+
chainSignal.removeEventListener("abort", abortFromChain);
|
|
1184
|
+
this.#appendFunctionHookAudit(registration, invocation, "timeout");
|
|
1185
|
+
return failureResult("Function hook timed out");
|
|
1186
|
+
}
|
|
1187
|
+
if (outcome.status === "error") {
|
|
1188
|
+
controller.abort(new Error("Function hook invocation failed"));
|
|
1189
|
+
active = false;
|
|
1190
|
+
chainSignal.removeEventListener("abort", abortFromChain);
|
|
1191
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", outcome.error);
|
|
1192
|
+
return failureResult(outcome.error);
|
|
1193
|
+
}
|
|
1194
|
+
if (nextFailureReason) {
|
|
1195
|
+
controller.abort(new Error(nextFailureReason));
|
|
1196
|
+
active = false;
|
|
1197
|
+
chainSignal.removeEventListener("abort", abortFromChain);
|
|
1198
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", nextFailureReason);
|
|
1199
|
+
return failureResult(nextFailureReason);
|
|
1200
|
+
}
|
|
1201
|
+
if (nextCalled) {
|
|
1202
|
+
if (outcome.value !== undefined && outcome.value !== nextReturnValue) {
|
|
1203
|
+
const reason = "Function hook returned a conflicting decision after next()";
|
|
1204
|
+
controller.abort(new Error(reason));
|
|
1205
|
+
active = false;
|
|
1206
|
+
chainSignal.removeEventListener("abort", abortFromChain);
|
|
1207
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1208
|
+
return failureResult(reason);
|
|
1209
|
+
}
|
|
1210
|
+
const nextResult = (await nextPromise) as FunctionHookDispatchResult<TEvent>;
|
|
1211
|
+
controller.abort(new Error("Function hook invocation completed"));
|
|
1212
|
+
active = false;
|
|
1213
|
+
chainSignal.removeEventListener("abort", abortFromChain);
|
|
1214
|
+
this.#appendFunctionHookAudit(registration, invocation, "continue", undefined, effectiveGrant);
|
|
1215
|
+
return nextResult;
|
|
1216
|
+
}
|
|
1217
|
+
controller.abort(new Error("Function hook invocation completed"));
|
|
1218
|
+
active = false;
|
|
1219
|
+
chainSignal.removeEventListener("abort", abortFromChain);
|
|
1220
|
+
const rawResult = outcome.value;
|
|
1221
|
+
if (rawResult === undefined) {
|
|
1222
|
+
this.#appendFunctionHookAudit(registration, invocation, "continue");
|
|
1223
|
+
return await invoke(index + 1, currentEvent, downstreamRemoved, chainSignal);
|
|
1224
|
+
}
|
|
1225
|
+
if (
|
|
1226
|
+
rawResult === null ||
|
|
1227
|
+
!isPlainFunctionHookData(rawResult) ||
|
|
1228
|
+
typeof rawResult !== "object" ||
|
|
1229
|
+
Array.isArray(rawResult)
|
|
1230
|
+
) {
|
|
1231
|
+
const reason = "Function hook returned a non-plain result";
|
|
1232
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1233
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1234
|
+
return failureResult(reason);
|
|
1235
|
+
}
|
|
1236
|
+
const action = (rawResult as { action?: unknown }).action;
|
|
1237
|
+
const allowedResultKeys =
|
|
1238
|
+
action === "continue"
|
|
1239
|
+
? new Set(["action", "event"])
|
|
1240
|
+
: action === "deny"
|
|
1241
|
+
? new Set(["action", "reason"])
|
|
1242
|
+
: action === "return"
|
|
1243
|
+
? new Set(["action", "value"])
|
|
1244
|
+
: undefined;
|
|
1245
|
+
if (!allowedResultKeys || Object.keys(rawResult).some(key => !allowedResultKeys.has(key))) {
|
|
1246
|
+
const reason = "Function hook returned unknown result fields";
|
|
1247
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1248
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1249
|
+
return failureResult(reason);
|
|
1250
|
+
}
|
|
1251
|
+
if (action === "continue") {
|
|
1252
|
+
const candidate = (rawResult as { event?: unknown }).event;
|
|
1253
|
+
let nextEvent = currentEvent;
|
|
1254
|
+
if (candidate !== undefined) {
|
|
1255
|
+
if (
|
|
1256
|
+
!isPlainFunctionHookData(candidate) ||
|
|
1257
|
+
!functionHookEventIdentityMatches(currentEvent, candidate as ExtensionEvent) ||
|
|
1258
|
+
!isValidFunctionHookEventValue(candidate as ExtensionEvent)
|
|
1259
|
+
) {
|
|
1260
|
+
const reason = "Function hook returned an invalid transformed event";
|
|
1261
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1262
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1263
|
+
return failureResult(reason);
|
|
1264
|
+
}
|
|
1265
|
+
if (!functionHookTransformAllowed(currentEvent, effectiveGrant)) {
|
|
1266
|
+
const reason = "Function hook transform capability was not granted";
|
|
1267
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1268
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1269
|
+
return failureResult(reason);
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
nextEvent = cloneFunctionHookDataStrict(candidate as TEvent);
|
|
1273
|
+
} catch {
|
|
1274
|
+
const reason = "Function hook replacement event could not be snapshotted";
|
|
1275
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1276
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1277
|
+
return failureResult(reason);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
this.#appendFunctionHookAudit(registration, invocation, "continue");
|
|
1281
|
+
const downstreamResult = await invoke(index + 1, nextEvent, downstreamRemoved, chainSignal);
|
|
1282
|
+
return candidate !== undefined && downstreamResult.action === "continue"
|
|
1283
|
+
? { ...downstreamResult, transformed: true }
|
|
1284
|
+
: downstreamResult;
|
|
1285
|
+
}
|
|
1286
|
+
if (action === "deny") {
|
|
1287
|
+
const reason = sanitizeFunctionHookReason(
|
|
1288
|
+
(rawResult as { reason?: unknown }).reason,
|
|
1289
|
+
"Function hook denied the event",
|
|
1290
|
+
);
|
|
1291
|
+
if (!functionHookDenyAllowed(currentEvent, effectiveGrant)) {
|
|
1292
|
+
const invalidReason = "Function hook deny capability was not granted";
|
|
1293
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: invalidReason });
|
|
1294
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", invalidReason);
|
|
1295
|
+
return failureResult(invalidReason);
|
|
1296
|
+
}
|
|
1297
|
+
this.#appendFunctionHookAudit(registration, invocation, "deny", reason);
|
|
1298
|
+
return { action: "deny", reason } as FunctionHookDispatchResult<TEvent>;
|
|
1299
|
+
}
|
|
1300
|
+
if (action === "return" && Object.hasOwn(rawResult, "value")) {
|
|
1301
|
+
if (!functionHookDenyAllowed(currentEvent, effectiveGrant)) {
|
|
1302
|
+
const reason = "Function hook short-circuit capability was not granted";
|
|
1303
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1304
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1305
|
+
return failureResult(reason);
|
|
1306
|
+
}
|
|
1307
|
+
const value = (rawResult as { value: unknown }).value;
|
|
1308
|
+
if (value === undefined || !isValidFunctionHookReturnValue(currentEvent, value)) {
|
|
1309
|
+
const reason = "Function hook returned an invalid terminal value";
|
|
1310
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1311
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1312
|
+
return failureResult(reason);
|
|
1313
|
+
}
|
|
1314
|
+
let valueSnapshot: unknown;
|
|
1315
|
+
try {
|
|
1316
|
+
valueSnapshot = cloneFunctionHookDataStrict(value);
|
|
1317
|
+
} catch {
|
|
1318
|
+
const reason = "Function hook terminal value could not be snapshotted";
|
|
1319
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1320
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1321
|
+
return failureResult(reason);
|
|
1322
|
+
}
|
|
1323
|
+
this.#appendFunctionHookAudit(registration, invocation, "return");
|
|
1324
|
+
return { action: "return", value: valueSnapshot } as FunctionHookDispatchResult<TEvent>;
|
|
1325
|
+
}
|
|
1326
|
+
const reason = "Function hook returned an invalid action";
|
|
1327
|
+
this.emitError({ extensionPath: indexed.ext.path, event: currentEvent.type, error: reason });
|
|
1328
|
+
this.#appendFunctionHookAudit(registration, invocation, "error", reason);
|
|
1329
|
+
return failureResult(reason);
|
|
1330
|
+
};
|
|
1331
|
+
return await invoke(0, event, [], parentSignal);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
735
1335
|
#isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
|
|
736
1336
|
return (
|
|
737
1337
|
event.type === "session_before_switch" ||
|
|
@@ -800,84 +1400,88 @@ export class ExtensionRunner {
|
|
|
800
1400
|
continueWhile?: () => boolean,
|
|
801
1401
|
scope?: AttemptScopeRef,
|
|
802
1402
|
): Promise<RunnerEmitResult<TEvent>> {
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
this.#requireScopeOrFailClosed(scope, event.type);
|
|
806
|
-
|
|
807
|
-
const ctx = this.createContext();
|
|
1403
|
+
const eventSignal = "signal" in event && event.signal instanceof AbortSignal ? event.signal : undefined;
|
|
1404
|
+
let ctx: ExtensionContext | undefined;
|
|
808
1405
|
let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
this
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (this.#isSessionBeforeEvent(event) && handlerResult) {
|
|
827
|
-
result = handlerResult as SessionBeforeEventResult;
|
|
828
|
-
if (result.cancel) {
|
|
829
|
-
return result as RunnerEmitResult<TEvent>;
|
|
1406
|
+
const functionDispatch = await this.emitFunctionHooks(event, {
|
|
1407
|
+
signal: eventSignal,
|
|
1408
|
+
scope,
|
|
1409
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1410
|
+
if (continueWhile && !continueWhile()) return { action: "return", value: result };
|
|
1411
|
+
ctx ??= this.createContext();
|
|
1412
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
1413
|
+
indexed.handler,
|
|
1414
|
+
currentEvent,
|
|
1415
|
+
ctx,
|
|
1416
|
+
indexed.ext,
|
|
1417
|
+
currentEvent.type === "session_shutdown" ? sessionShutdownHandlerTimeoutMs : extensionHandlerTimeoutMs,
|
|
1418
|
+
);
|
|
1419
|
+
if (continueWhile && !continueWhile()) return { action: "return", value: result };
|
|
1420
|
+
if (this.#isSessionBeforeEvent(currentEvent) && handlerResult) {
|
|
1421
|
+
result = handlerResult as SessionBeforeEventResult;
|
|
1422
|
+
if (result.cancel) return { action: "return", value: result };
|
|
830
1423
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
1424
|
+
if (currentEvent.type === "session.compacting" && handlerResult) {
|
|
1425
|
+
result = handlerResult as SessionCompactingResult;
|
|
1426
|
+
}
|
|
1427
|
+
return { action: "continue", event: currentEvent };
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
if (functionDispatch.action === "deny") {
|
|
1431
|
+
if (this.#isSessionBeforeEvent(event)) return { cancel: true } as RunnerEmitResult<TEvent>;
|
|
1432
|
+
return undefined as RunnerEmitResult<TEvent>;
|
|
836
1433
|
}
|
|
837
|
-
|
|
1434
|
+
if (functionDispatch.action === "return") return functionDispatch.value as RunnerEmitResult<TEvent>;
|
|
838
1435
|
return result as RunnerEmitResult<TEvent>;
|
|
839
1436
|
}
|
|
840
1437
|
|
|
841
|
-
async emitToolResult(
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
1438
|
+
async emitToolResult(
|
|
1439
|
+
event: ToolResultEvent,
|
|
1440
|
+
scope?: AttemptScopeRef,
|
|
1441
|
+
options: { signal?: AbortSignal; correlationId?: string } = {},
|
|
1442
|
+
): Promise<ToolResultEventResult | undefined> {
|
|
1443
|
+
let ctx: ExtensionContext | undefined;
|
|
1444
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1445
|
+
{ ...event },
|
|
1446
|
+
{
|
|
1447
|
+
...options,
|
|
1448
|
+
scope,
|
|
1449
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1450
|
+
ctx ??= this.createContext();
|
|
1451
|
+
const result = (await this.#runHandlerWithTimeout(
|
|
1452
|
+
indexed.handler,
|
|
1453
|
+
currentEvent,
|
|
1454
|
+
ctx,
|
|
1455
|
+
indexed.ext,
|
|
1456
|
+
extensionHandlerTimeoutMs,
|
|
1457
|
+
)) as ToolResultEventResult | undefined;
|
|
1458
|
+
let transformed = false;
|
|
1459
|
+
if (result?.content !== undefined) {
|
|
1460
|
+
currentEvent.content = result.content;
|
|
1461
|
+
transformed = true;
|
|
1462
|
+
}
|
|
1463
|
+
if (result?.details !== undefined) {
|
|
1464
|
+
currentEvent.details = result.details;
|
|
1465
|
+
transformed = true;
|
|
1466
|
+
}
|
|
1467
|
+
if (result?.isError !== undefined) {
|
|
1468
|
+
currentEvent.isError = result.isError;
|
|
1469
|
+
transformed = true;
|
|
1470
|
+
}
|
|
1471
|
+
return { action: "continue", event: currentEvent, transformed };
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
);
|
|
1475
|
+
if (functionDispatch.action === "deny") {
|
|
1476
|
+
return {
|
|
1477
|
+
content: [{ type: "text", text: functionDispatch.reason }],
|
|
1478
|
+
details: event.details,
|
|
1479
|
+
isError: true,
|
|
1480
|
+
};
|
|
877
1481
|
}
|
|
878
|
-
|
|
879
|
-
if (
|
|
880
|
-
|
|
1482
|
+
if (functionDispatch.action === "return") return functionDispatch.value as ToolResultEventResult;
|
|
1483
|
+
if (functionDispatch.transformed !== true) return undefined;
|
|
1484
|
+
const currentEvent = functionDispatch.event;
|
|
881
1485
|
return {
|
|
882
1486
|
content: currentEvent.content,
|
|
883
1487
|
details: currentEvent.details,
|
|
@@ -885,43 +1489,36 @@ export class ExtensionRunner {
|
|
|
885
1489
|
};
|
|
886
1490
|
}
|
|
887
1491
|
|
|
888
|
-
async emitToolCall(
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1492
|
+
async emitToolCall(
|
|
1493
|
+
event: ToolCallEvent,
|
|
1494
|
+
scope?: AttemptScopeRef,
|
|
1495
|
+
options: { signal?: AbortSignal; correlationId?: string } = {},
|
|
1496
|
+
): Promise<ToolCallEventResult | undefined> {
|
|
893
1497
|
const ctx = this.createContext();
|
|
894
|
-
let
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
return
|
|
909
|
-
|
|
1498
|
+
let legacyResult: ToolCallEventResult | undefined;
|
|
1499
|
+
const functionDispatch = await this.emitFunctionHooks(event, {
|
|
1500
|
+
...options,
|
|
1501
|
+
scope,
|
|
1502
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1503
|
+
try {
|
|
1504
|
+
const result = (await indexed.handler(currentEvent, ctx)) as ToolCallEventResult | undefined;
|
|
1505
|
+
if (result?.block) return { action: "return", value: result };
|
|
1506
|
+
if (result) legacyResult = result;
|
|
1507
|
+
return { action: "continue", event: currentEvent };
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1510
|
+
this.emitError({ extensionPath: indexed.ext.path, event: "tool_call", error: message });
|
|
1511
|
+
return {
|
|
1512
|
+
action: "return",
|
|
1513
|
+
value: { block: true, reason: `Extension ${indexed.ext.path} failed: ${message}` },
|
|
1514
|
+
};
|
|
910
1515
|
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
error: message,
|
|
918
|
-
stack,
|
|
919
|
-
});
|
|
920
|
-
return { block: true, reason: `Extension ${ext.path} failed: ${message}` };
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
return result;
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
if (functionDispatch.action === "deny") return { block: true, reason: functionDispatch.reason };
|
|
1519
|
+
if (functionDispatch.action === "return") return functionDispatch.value as ToolCallEventResult;
|
|
1520
|
+
event.input = functionDispatch.event.input;
|
|
1521
|
+
return legacyResult;
|
|
925
1522
|
}
|
|
926
1523
|
|
|
927
1524
|
async emitUserBash(event: UserBashEvent): Promise<UserBashEventResult | undefined> {
|
|
@@ -936,18 +1533,25 @@ export class ExtensionRunner {
|
|
|
936
1533
|
event: UserBashEvent | UserPythonEvent,
|
|
937
1534
|
eventName: "user_bash" | "user_python",
|
|
938
1535
|
): Promise<R | undefined> {
|
|
939
|
-
const handlers = this.#handlersByEvent.get(eventName) ?? [];
|
|
940
|
-
if (handlers.length === 0) return undefined;
|
|
941
|
-
|
|
942
1536
|
const ctx = this.createContext();
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1537
|
+
const functionDispatch = await this.emitFunctionHooks(event, {
|
|
1538
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1539
|
+
const result = await this.#runHandlerWithTimeout(
|
|
1540
|
+
indexed.handler,
|
|
1541
|
+
currentEvent,
|
|
1542
|
+
ctx,
|
|
1543
|
+
indexed.ext,
|
|
1544
|
+
extensionHandlerTimeoutMs,
|
|
1545
|
+
);
|
|
1546
|
+
return result === undefined
|
|
1547
|
+
? { action: "continue", event: currentEvent }
|
|
1548
|
+
: { action: "return", value: result };
|
|
1549
|
+
},
|
|
1550
|
+
});
|
|
1551
|
+
if (functionDispatch.action === "deny" || functionDispatch.action === "return") {
|
|
1552
|
+
return functionDispatch.action === "return" ? (functionDispatch.value as R) : undefined;
|
|
949
1553
|
}
|
|
950
|
-
|
|
1554
|
+
void eventName;
|
|
951
1555
|
return undefined;
|
|
952
1556
|
}
|
|
953
1557
|
|
|
@@ -959,29 +1563,39 @@ export class ExtensionRunner {
|
|
|
959
1563
|
promptPaths: Array<{ path: string; extensionPath: string }>;
|
|
960
1564
|
themePaths: Array<{ path: string; extensionPath: string }>;
|
|
961
1565
|
}> {
|
|
962
|
-
|
|
963
|
-
if (handlers.length === 0) return { skillPaths: [], promptPaths: [], themePaths: [] };
|
|
964
|
-
const ctx = this.createContext();
|
|
1566
|
+
let ctx: ExtensionContext | undefined;
|
|
965
1567
|
const skillPaths: Array<{ path: string; extensionPath: string }> = [];
|
|
966
1568
|
const promptPaths: Array<{ path: string; extensionPath: string }> = [];
|
|
967
1569
|
const themePaths: Array<{ path: string; extensionPath: string }> = [];
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1570
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1571
|
+
{ type: "resources_discover", cwd, reason },
|
|
1572
|
+
{
|
|
1573
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1574
|
+
ctx ??= this.createContext();
|
|
1575
|
+
const result = (await this.#runHandlerWithTimeout(
|
|
1576
|
+
indexed.handler,
|
|
1577
|
+
currentEvent,
|
|
1578
|
+
ctx,
|
|
1579
|
+
indexed.ext,
|
|
1580
|
+
extensionHandlerTimeoutMs,
|
|
1581
|
+
)) as ResourcesDiscoverResult | undefined;
|
|
1582
|
+
if (result?.skillPaths?.length)
|
|
1583
|
+
skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: indexed.ext.path })));
|
|
1584
|
+
if (result?.promptPaths?.length)
|
|
1585
|
+
promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: indexed.ext.path })));
|
|
1586
|
+
if (result?.themePaths?.length)
|
|
1587
|
+
themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: indexed.ext.path })));
|
|
1588
|
+
return { action: "continue", event: currentEvent };
|
|
1589
|
+
},
|
|
1590
|
+
},
|
|
1591
|
+
);
|
|
1592
|
+
if (functionDispatch.action === "deny") return { skillPaths: [], promptPaths: [], themePaths: [] };
|
|
1593
|
+
if (functionDispatch.action === "return")
|
|
1594
|
+
return functionDispatch.value as {
|
|
1595
|
+
skillPaths: Array<{ path: string; extensionPath: string }>;
|
|
1596
|
+
promptPaths: Array<{ path: string; extensionPath: string }>;
|
|
1597
|
+
themePaths: Array<{ path: string; extensionPath: string }>;
|
|
1598
|
+
};
|
|
985
1599
|
return { skillPaths, promptPaths, themePaths };
|
|
986
1600
|
}
|
|
987
1601
|
|
|
@@ -991,165 +1605,182 @@ export class ExtensionRunner {
|
|
|
991
1605
|
images: ImageContent[] | undefined,
|
|
992
1606
|
source: "interactive" | "sdk" | "extension",
|
|
993
1607
|
): Promise<InputEventResult> {
|
|
994
|
-
const handlers = this.#handlersByEvent.get("input") ?? [];
|
|
995
|
-
if (handlers.length === 0) return {};
|
|
996
|
-
|
|
997
1608
|
const ctx = this.createContext();
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1609
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1610
|
+
{ type: "input", text, images, source },
|
|
1611
|
+
{
|
|
1612
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1613
|
+
const result = (await this.#runHandlerWithTimeout(
|
|
1614
|
+
indexed.handler,
|
|
1615
|
+
currentEvent,
|
|
1616
|
+
ctx,
|
|
1617
|
+
indexed.ext,
|
|
1618
|
+
extensionHandlerTimeoutMs,
|
|
1619
|
+
)) as InputEventResult | undefined;
|
|
1620
|
+
if (result?.handled) return { action: "return", value: result };
|
|
1621
|
+
if (result?.text !== undefined) {
|
|
1622
|
+
currentEvent.text = result.text;
|
|
1623
|
+
currentEvent.images = result.images ?? currentEvent.images;
|
|
1624
|
+
return { action: "continue", event: currentEvent, transformed: true };
|
|
1625
|
+
}
|
|
1626
|
+
return { action: "continue", event: currentEvent };
|
|
1627
|
+
},
|
|
1628
|
+
},
|
|
1629
|
+
);
|
|
1630
|
+
if (functionDispatch.action === "deny") return { handled: true };
|
|
1631
|
+
if (functionDispatch.action === "return") return functionDispatch.value as InputEventResult;
|
|
1632
|
+
const transformedInput = functionDispatch.event;
|
|
1633
|
+
return transformedInput.text !== text || transformedInput.images !== images
|
|
1634
|
+
? { text: transformedInput.text, images: transformedInput.images }
|
|
1635
|
+
: {};
|
|
1014
1636
|
}
|
|
1015
1637
|
|
|
1016
|
-
async emitContext(messages: AgentMessage[], scope?: AttemptScopeRef): Promise<AgentMessage[]> {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (handlerResult && (handlerResult as ContextEventResult).messages) {
|
|
1042
|
-
currentMessages = (handlerResult as ContextEventResult).messages!;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
return currentMessages;
|
|
1638
|
+
async emitContext(messages: AgentMessage[], scope?: AttemptScopeRef, signal?: AbortSignal): Promise<AgentMessage[]> {
|
|
1639
|
+
let ctx: ExtensionContext | undefined;
|
|
1640
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1641
|
+
{ type: "context", messages },
|
|
1642
|
+
{
|
|
1643
|
+
signal,
|
|
1644
|
+
scope,
|
|
1645
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1646
|
+
ctx ??= this.createContext();
|
|
1647
|
+
const result = (await this.#runHandlerWithTimeout(
|
|
1648
|
+
indexed.handler,
|
|
1649
|
+
currentEvent,
|
|
1650
|
+
ctx,
|
|
1651
|
+
indexed.ext,
|
|
1652
|
+
extensionHandlerTimeoutMs,
|
|
1653
|
+
)) as ContextEventResult | undefined;
|
|
1654
|
+
if (!result?.messages) return { action: "continue", event: currentEvent };
|
|
1655
|
+
currentEvent.messages = result.messages;
|
|
1656
|
+
return { action: "continue", event: currentEvent, transformed: true };
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
);
|
|
1660
|
+
if (functionDispatch.action === "deny") throw new Error(functionDispatch.reason);
|
|
1661
|
+
if (functionDispatch.action === "return") return functionDispatch.value as AgentMessage[];
|
|
1662
|
+
return functionDispatch.event.messages;
|
|
1047
1663
|
}
|
|
1048
1664
|
|
|
1049
1665
|
async emitBeforeProviderRequest(
|
|
1050
1666
|
payload: unknown,
|
|
1051
1667
|
scope?: AttemptScopeRef,
|
|
1668
|
+
signal?: AbortSignal,
|
|
1052
1669
|
): Promise<BeforeProviderRequestEventResult> {
|
|
1053
|
-
const handlers = this.#handlersByEvent.get("before_provider_request") ?? [];
|
|
1054
|
-
if (handlers.length === 0) return payload;
|
|
1055
|
-
this.#requireScopeOrFailClosed(scope, "before_provider_request");
|
|
1056
|
-
|
|
1057
1670
|
const ctx = this.createContext();
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1671
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1672
|
+
{ type: "before_provider_request", payload },
|
|
1673
|
+
{
|
|
1674
|
+
signal,
|
|
1675
|
+
scope,
|
|
1676
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1677
|
+
const result = await this.#runHandlerWithTimeout(
|
|
1678
|
+
indexed.handler,
|
|
1679
|
+
currentEvent,
|
|
1680
|
+
ctx,
|
|
1681
|
+
indexed.ext,
|
|
1682
|
+
extensionHandlerTimeoutMs,
|
|
1683
|
+
);
|
|
1684
|
+
if (result === undefined) return { action: "continue", event: currentEvent };
|
|
1685
|
+
currentEvent.payload = result;
|
|
1686
|
+
return { action: "continue", event: currentEvent, transformed: true };
|
|
1687
|
+
},
|
|
1688
|
+
},
|
|
1689
|
+
);
|
|
1690
|
+
if (functionDispatch.action === "deny") throw new Error(functionDispatch.reason);
|
|
1691
|
+
if (functionDispatch.action === "return") return functionDispatch.value;
|
|
1692
|
+
return functionDispatch.event.payload;
|
|
1077
1693
|
}
|
|
1078
1694
|
|
|
1079
1695
|
async emitAfterProviderResponse(
|
|
1080
1696
|
response: ProviderResponseMetadata,
|
|
1081
1697
|
_model?: Model,
|
|
1082
1698
|
scope?: AttemptScopeRef,
|
|
1699
|
+
signal?: AbortSignal,
|
|
1083
1700
|
): Promise<void> {
|
|
1084
|
-
const handlers = this.#handlersByEvent.get("after_provider_response") ?? [];
|
|
1085
|
-
if (handlers.length === 0) return;
|
|
1086
|
-
this.#requireScopeOrFailClosed(scope, "after_provider_response");
|
|
1087
|
-
|
|
1088
1701
|
const ctx = this.createContext();
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
for (const { ext, handler } of handlers) {
|
|
1092
|
-
if (!marked) {
|
|
1093
|
-
this.#markAttemptExecuted(scope);
|
|
1094
|
-
marked = true;
|
|
1095
|
-
}
|
|
1096
|
-
const event: AfterProviderResponseEvent = {
|
|
1702
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1703
|
+
{
|
|
1097
1704
|
type: "after_provider_response",
|
|
1098
1705
|
status: response.status,
|
|
1099
1706
|
headers: response.headers,
|
|
1100
1707
|
requestId: response.requestId,
|
|
1101
1708
|
metadata: response.metadata,
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
signal,
|
|
1712
|
+
scope,
|
|
1713
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1714
|
+
await this.#runHandlerWithTimeout(
|
|
1715
|
+
indexed.handler,
|
|
1716
|
+
currentEvent,
|
|
1717
|
+
ctx,
|
|
1718
|
+
indexed.ext,
|
|
1719
|
+
extensionHandlerTimeoutMs,
|
|
1720
|
+
);
|
|
1721
|
+
return { action: "continue", event: currentEvent };
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
);
|
|
1725
|
+
if (functionDispatch.action !== "continue") return;
|
|
1105
1726
|
}
|
|
1106
1727
|
|
|
1107
1728
|
async emitBeforeAgentStart(
|
|
1108
1729
|
prompt: string,
|
|
1109
1730
|
images: ImageContent[] | undefined,
|
|
1110
1731
|
systemPrompt: string[],
|
|
1732
|
+
signal?: AbortSignal,
|
|
1111
1733
|
): Promise<BeforeAgentStartCombinedResult | undefined> {
|
|
1112
|
-
const handlers = this.#handlersByEvent.get("before_agent_start") ?? [];
|
|
1113
|
-
if (handlers.length === 0) return undefined;
|
|
1114
|
-
|
|
1115
1734
|
const ctx = this.createContext();
|
|
1116
1735
|
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
for (const { ext, handler } of handlers) {
|
|
1121
|
-
const event: BeforeAgentStartEvent = {
|
|
1736
|
+
const functionDispatch = await this.emitFunctionHooks(
|
|
1737
|
+
{
|
|
1122
1738
|
type: "before_agent_start",
|
|
1123
1739
|
prompt,
|
|
1124
1740
|
images,
|
|
1125
|
-
systemPrompt
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1741
|
+
systemPrompt,
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
signal,
|
|
1745
|
+
legacyHandler: async (indexed, currentEvent) => {
|
|
1746
|
+
const result = await this.#runHandlerWithTimeout(
|
|
1747
|
+
indexed.handler,
|
|
1748
|
+
currentEvent,
|
|
1749
|
+
ctx,
|
|
1750
|
+
indexed.ext,
|
|
1751
|
+
extensionHandlerTimeoutMs,
|
|
1752
|
+
);
|
|
1753
|
+
if (!result) return { action: "continue", event: currentEvent };
|
|
1754
|
+
const unsupportedFields = unsupportedBeforeAgentStartResultFields(result);
|
|
1755
|
+
if (unsupportedFields.length > 0) {
|
|
1756
|
+
this.emitError({
|
|
1757
|
+
extensionPath: indexed.ext.path,
|
|
1758
|
+
event: "before_agent_start",
|
|
1759
|
+
error: `Unsupported before_agent_start result field(s): ${unsupportedFields.join(", ")}. Supported fields: message, systemPrompt.`,
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
const legacyResult = result as BeforeAgentStartEventResult;
|
|
1763
|
+
if (legacyResult.message) messages.push(legacyResult.message);
|
|
1764
|
+
if (legacyResult.systemPrompt === undefined) return { action: "continue", event: currentEvent };
|
|
1765
|
+
currentEvent.systemPrompt = legacyResult.systemPrompt;
|
|
1766
|
+
return { action: "continue", event: currentEvent, transformed: true };
|
|
1767
|
+
},
|
|
1768
|
+
},
|
|
1769
|
+
);
|
|
1770
|
+
if (functionDispatch.action === "deny") throw new Error(functionDispatch.reason);
|
|
1771
|
+
if (functionDispatch.action === "return") return functionDispatch.value as BeforeAgentStartCombinedResult;
|
|
1772
|
+
const transformedPrompt = functionDispatch.event.prompt;
|
|
1773
|
+
const transformedImages = functionDispatch.event.images;
|
|
1774
|
+
const transformedSystemPrompt = functionDispatch.event.systemPrompt;
|
|
1775
|
+
const promptModified = transformedPrompt !== prompt;
|
|
1776
|
+
const imagesModified = JSON.stringify(transformedImages) !== JSON.stringify(images);
|
|
1777
|
+
const systemPromptModified = JSON.stringify(transformedSystemPrompt) !== JSON.stringify(systemPrompt);
|
|
1778
|
+
if (messages.length > 0 || systemPromptModified || promptModified || imagesModified) {
|
|
1150
1779
|
return {
|
|
1151
|
-
messages
|
|
1152
|
-
|
|
1780
|
+
...(messages.length > 0 ? { messages } : {}),
|
|
1781
|
+
...(systemPromptModified ? { systemPrompt: transformedSystemPrompt } : {}),
|
|
1782
|
+
...(promptModified ? { prompt: transformedPrompt } : {}),
|
|
1783
|
+
...(imagesModified ? { images: transformedImages } : {}),
|
|
1153
1784
|
};
|
|
1154
1785
|
}
|
|
1155
1786
|
|