@sema-agent/core 5.20.0 → 5.21.1
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 +19 -0
- package/dist/agents/agent-definition.d.ts +7 -0
- package/dist/agents/agent-transcript-tool.d.ts +30 -0
- package/dist/agents/builtin-agents.d.ts +60 -0
- package/dist/agents/cascade.d.ts +107 -0
- package/dist/agents/cumulative-stats.d.ts +56 -0
- package/dist/agents/observer.d.ts +162 -0
- package/dist/agents/peer-admission.d.ts +95 -0
- package/dist/agents/repair-loop.d.ts +226 -0
- package/dist/agents/retain-ledger.d.ts +141 -0
- package/dist/agents/roster-store.d.ts +105 -0
- package/dist/agents/send-message-tool.d.ts +92 -0
- package/dist/agents/session-util.d.ts +5 -0
- package/dist/agents/subagent-steps.d.ts +66 -0
- package/dist/agents/subagent.d.ts +600 -0
- package/dist/agents/suspend-guard.d.ts +29 -0
- package/dist/agents/teacher.d.ts +75 -0
- package/dist/agents/team.d.ts +120 -1
- package/dist/agents/tool-filter.d.ts +34 -0
- package/dist/agents/verify.d.ts +198 -0
- package/dist/bench/metrics.d.ts +455 -0
- package/dist/brain/anthropic.d.ts +30 -0
- package/dist/brain/circuit-breaker.d.ts +33 -0
- package/dist/brain/context-overflow.d.ts +60 -3
- package/dist/brain/degrading.d.ts +67 -0
- package/dist/brain/errors.d.ts +42 -0
- package/dist/brain/failover.d.ts +15 -0
- package/dist/brain/media-degrade.d.ts +39 -0
- package/dist/brain/model-presets.d.ts +31 -0
- package/dist/brain/open-responses.d.ts +19 -0
- package/dist/brain/openai.d.ts +46 -0
- package/dist/brain/reasoning.d.ts +106 -1
- package/dist/brain/repetition.d.ts +83 -0
- package/dist/brain/request-params.d.ts +56 -0
- package/dist/brain/retry.d.ts +46 -0
- package/dist/brain/routing.d.ts +12 -0
- package/dist/brain/status-sink.d.ts +30 -1
- package/dist/brain/stream-engine.d.ts +147 -0
- package/dist/brain/stream-shared.d.ts +34 -0
- package/dist/brain/terminal-cause.d.ts +31 -0
- package/dist/brain/timeout.d.ts +107 -0
- package/dist/brain/tool-call-id.d.ts +20 -0
- package/dist/brain/tool-call-repair.d.ts +13 -0
- package/dist/config/catalog.d.ts +47 -0
- package/dist/config/defaults.d.ts +33 -0
- package/dist/core/a2a-task-state.d.ts +53 -0
- package/dist/core/a2a.d.ts +51 -0
- package/dist/core/arg-summary.d.ts +62 -0
- package/dist/core/ask-question.d.ts +272 -2
- package/dist/core/auto-compaction.d.ts +467 -0
- package/dist/core/auto-compaction.js +20 -6
- package/dist/core/auto-mode-prompt-assets.d.ts +8 -0
- package/dist/core/auto-mode-prompt.d.ts +27 -0
- package/dist/core/auto-mode.d.ts +54 -3
- package/dist/core/auto-promote.d.ts +100 -0
- package/dist/core/background-agent-store.d.ts +292 -0
- package/dist/core/background-shell.d.ts +110 -0
- package/dist/core/cache-break-detector.d.ts +34 -0
- package/dist/core/canonical-json.d.ts +57 -0
- package/dist/core/checkpoint-store.d.ts +1574 -16
- package/dist/core/compliance.d.ts +30 -0
- package/dist/core/consolidate-scope.d.ts +75 -0
- package/dist/core/context-edit.d.ts +99 -0
- package/dist/core/context-guard.d.ts +46 -0
- package/dist/core/exec-gate.d.ts +44 -0
- package/dist/core/exec-output-tail.d.ts +61 -0
- package/dist/core/file-snapshot-store.d.ts +104 -0
- package/dist/core/fs-write-gate-policy.d.ts +36 -0
- package/dist/core/git-worktree-env.d.ts +31 -0
- package/dist/core/governance-codes.d.ts +19 -0
- package/dist/core/hooks.d.ts +608 -1
- package/dist/core/human-input-projection.d.ts +37 -0
- package/dist/core/human-input-projection.js +13 -0
- package/dist/core/image-downsample.d.ts +74 -0
- package/dist/core/locked-config.d.ts +37 -0
- package/dist/core/lsp-diagnostics.d.ts +77 -0
- package/dist/core/lsp-protocol.d.ts +29 -0
- package/dist/core/lsp-session.d.ts +60 -1
- package/dist/core/lsp.d.ts +150 -1
- package/dist/core/mailbox-store.d.ts +57 -0
- package/dist/core/mcp.d.ts +381 -0
- package/dist/core/media-byte-cap.d.ts +21 -0
- package/dist/core/memory-admission.d.ts +71 -0
- package/dist/core/memory-engine/content-origin.d.ts +64 -0
- package/dist/core/memory-engine/data-plane.d.ts +43 -0
- package/dist/core/memory-engine/dual-root.d.ts +24 -0
- package/dist/core/memory-engine/engine.d.ts +230 -0
- package/dist/core/memory-engine/engine.js +103 -35
- package/dist/core/memory-engine/file-backend.d.ts +155 -0
- package/dist/core/memory-engine/frontmatter.d.ts +16 -0
- package/dist/core/memory-engine/layout.d.ts +217 -0
- package/dist/core/memory-engine/memory-backend-contract.d.ts +12 -0
- package/dist/core/memory-engine/migrate.d.ts +9 -0
- package/dist/core/memory-engine/scan.d.ts +12 -0
- package/dist/core/memory-engine/scope-contract.d.ts +61 -1
- package/dist/core/memory-engine/sync-client.d.ts +87 -0
- package/dist/core/memory-engine/sync.d.ts +60 -0
- package/dist/core/memory-engine/tools.d.ts +41 -0
- package/dist/core/memory-engine/types.d.ts +188 -0
- package/dist/core/memory-recall.d.ts +141 -0
- package/dist/core/memory-vector.d.ts +20 -0
- package/dist/core/memory.d.ts +458 -0
- package/dist/core/message-utils.d.ts +6 -0
- package/dist/core/oracle-isolation.d.ts +69 -0
- package/dist/core/permission-rule-consent.d.ts +138 -0
- package/dist/core/permission-rule-model.d.ts +122 -0
- package/dist/core/permission-rule-store.d.ts +119 -3
- package/dist/core/permission-rules.d.ts +87 -1
- package/dist/core/present-plan-tool.d.ts +20 -0
- package/dist/core/pricing.d.ts +26 -0
- package/dist/core/property-harness.d.ts +86 -0
- package/dist/core/protocol-naming.d.ts +38 -0
- package/dist/core/protocol-table.d.ts +61 -0
- package/dist/core/push-queue.d.ts +1 -0
- package/dist/core/remote-env.d.ts +383 -1
- package/dist/core/retention-policy.d.ts +33 -0
- package/dist/core/retention.d.ts +51 -0
- package/dist/core/roles.d.ts +59 -0
- package/dist/core/runner/active-skill-scope.d.ts +34 -0
- package/dist/core/runner/assemble-result.d.ts +134 -0
- package/dist/core/runner/compaction-call-options.d.ts +97 -1
- package/dist/core/runner/grounding-signal.d.ts +10 -0
- package/dist/core/runner/image.d.ts +17 -0
- package/dist/core/runner/image.js +29 -15
- package/dist/core/runner/memory-consolidation.d.ts +60 -0
- package/dist/core/runner/prepare-memory.d.ts +59 -0
- package/dist/core/runner/prepare-task.d.ts +1011 -2
- package/dist/core/runner/prepare-task.js +12 -11
- package/dist/core/runner/prompt-suggestions.d.ts +7 -0
- package/dist/core/runner/runtask.d.ts +304 -3
- package/dist/core/runner/session-file-state-replay.d.ts +49 -0
- package/dist/core/runner/session-rule-policy.d.ts +58 -0
- package/dist/core/runner/strict-output-schema.d.ts +18 -3
- package/dist/core/runner/synthetic-tools.d.ts +113 -0
- package/dist/core/runner/teardown-bounded.d.ts +34 -0
- package/dist/core/runner/tool-disclosure.d.ts +227 -0
- package/dist/core/runner/tool-output-projection.d.ts +6 -0
- package/dist/core/runner/turn-attachments.d.ts +634 -0
- package/dist/core/runner/usage-accounting.d.ts +32 -0
- package/dist/core/runtime.d.ts +9 -0
- package/dist/core/safe-notify.d.ts +64 -0
- package/dist/core/safety-axis-vocab.d.ts +23 -0
- package/dist/core/safety-merge-corpus.d.ts +37 -0
- package/dist/core/scheduler.d.ts +121 -0
- package/dist/core/secret-env.d.ts +32 -0
- package/dist/core/select-model.d.ts +15 -0
- package/dist/core/sensitive-path-policy.d.ts +42 -0
- package/dist/core/session-policy-store.d.ts +94 -0
- package/dist/core/session-reconcile.d.ts +80 -0
- package/dist/core/session-store.d.ts +85 -0
- package/dist/core/session.d.ts +153 -0
- package/dist/core/shared-memory/contract.d.ts +22 -0
- package/dist/core/shared-memory/normalize.d.ts +123 -2
- package/dist/core/shared-memory/tools.d.ts +14 -0
- package/dist/core/shared-memory/types.d.ts +105 -0
- package/dist/core/shutdown-debug.d.ts +6 -0
- package/dist/core/side-query.d.ts +38 -0
- package/dist/core/side-query.js +6 -1
- package/dist/core/skill-tool-specifier.d.ts +72 -0
- package/dist/core/skills-directory.d.ts +100 -1
- package/dist/core/spec-contract.d.ts +89 -0
- package/dist/core/store-contracts/background-agent-store-contract.d.ts +31 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +26 -0
- package/dist/core/store-contracts/contract-harness.d.ts +20 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +27 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +8 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +42 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +13 -0
- package/dist/core/strategy-store.d.ts +37 -0
- package/dist/core/stub-env.d.ts +7 -0
- package/dist/core/surrogate-safe-slice.d.ts +35 -0
- package/dist/core/task-notification.d.ts +202 -0
- package/dist/core/task-outcome.d.ts +53 -0
- package/dist/core/task-registry-agent.d.ts +337 -1
- package/dist/core/task-registry-monitor.d.ts +12 -0
- package/dist/core/task-registry-shared.d.ts +540 -0
- package/dist/core/task-registry.d.ts +343 -0
- package/dist/core/task-registry.js +13 -2
- package/dist/core/task-tool-shape.d.ts +44 -0
- package/dist/core/tighten-task-spec.d.ts +21 -0
- package/dist/core/tool-detach.d.ts +21 -0
- package/dist/core/tool-errors.d.ts +131 -0
- package/dist/core/tool-errors.js +4 -0
- package/dist/core/tool-name-aliases.d.ts +27 -0
- package/dist/core/tool-policy.d.ts +555 -0
- package/dist/core/tool-result-budget.d.ts +32 -0
- package/dist/core/tool-result-store.d.ts +174 -1
- package/dist/core/tools.d.ts +45 -0
- package/dist/core/trace.d.ts +323 -0
- package/dist/core/types.d.ts +3859 -2
- package/dist/core/untrusted-egress.d.ts +8 -0
- package/dist/core/untrusted-text.d.ts +156 -0
- package/dist/core/usage-window-store.d.ts +95 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/warm-resume.d.ts +17 -0
- package/dist/core/wiring-manifest.d.ts +169 -0
- package/dist/core/with-retry.d.ts +24 -0
- package/dist/core/workflow-journal-store.d.ts +160 -0
- package/dist/core/workflow-run-store-contract.d.ts +25 -0
- package/dist/core/workflow-run-store.d.ts +119 -0
- package/dist/engine/compaction/compaction.d.ts +256 -1
- package/dist/engine/compaction/utils.d.ts +94 -0
- package/dist/engine/execution-env/kill-tree.d.ts +29 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +94 -0
- package/dist/engine/harness/agent-harness.d.ts +116 -0
- package/dist/engine/harness/agent-harness.js +3 -14
- package/dist/engine/harness/messages.d.ts +15 -0
- package/dist/engine/harness/types.d.ts +464 -2
- package/dist/engine/llm/diagnostics.d.ts +4 -0
- package/dist/engine/llm/event-stream.d.ts +3 -0
- package/dist/engine/llm/index.d.ts +7 -0
- package/dist/engine/llm/types.d.ts +500 -3
- package/dist/engine/llm/validation.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +87 -2
- package/dist/engine/loop/runtime-deps.d.ts +7 -0
- package/dist/engine/loop/types.d.ts +424 -0
- package/dist/engine/lsp/frame-decoder.d.ts +13 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +107 -0
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +7 -0
- package/dist/engine/session/import-validate.d.ts +27 -0
- package/dist/engine/session/log-digest.d.ts +93 -0
- package/dist/engine/session/memory-repo.d.ts +6 -0
- package/dist/engine/session/memory-storage.d.ts +2 -0
- package/dist/engine/session/session.d.ts +75 -0
- package/dist/engine/session/storage-base.d.ts +8 -0
- package/dist/fixtures/index.d.ts +36 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.js +0 -1
- package/dist/internal/harness-types.d.ts +6 -0
- package/dist/internal/harness.d.ts +11 -0
- package/dist/internal/llm.d.ts +6 -0
- package/dist/orchestration/builtin-workflows.d.ts +53 -0
- package/dist/orchestration/dev-vm-script-runner.d.ts +19 -0
- package/dist/orchestration/goal.d.ts +57 -0
- package/dist/orchestration/goal.js +3 -0
- package/dist/orchestration/run-spec.d.ts +42 -0
- package/dist/orchestration/run-workflow-tool.d.ts +169 -0
- package/dist/orchestration/workflow-governance.d.ts +61 -0
- package/dist/orchestration/workflow-meta.d.ts +28 -0
- package/dist/orchestration/workflow-observe.d.ts +60 -0
- package/dist/orchestration/workflow-primitives.d.ts +23 -1
- package/dist/orchestration/workflow-sandbox-conformance.d.ts +38 -0
- package/dist/orchestration/workflow-script-runner.d.ts +88 -0
- package/dist/orchestration/workflow-script-store.d.ts +98 -0
- package/dist/orchestration/workflow-size-guideline.d.ts +67 -0
- package/dist/orchestration/workflow-types.d.ts +169 -2
- package/dist/orchestration/workflow.d.ts +358 -0
- package/dist/prompt-assembly/artifact-store.d.ts +33 -0
- package/dist/prompt-assembly/artifact.d.ts +25 -0
- package/dist/prompt-assembly/assemble.d.ts +20 -0
- package/dist/prompt-assembly/composer.d.ts +29 -0
- package/dist/prompt-assembly/epoch.d.ts +55 -1
- package/dist/prompt-assembly/event-registry.d.ts +35 -0
- package/dist/prompt-assembly/explain.d.ts +12 -0
- package/dist/prompt-assembly/tool-catalog.d.ts +38 -0
- package/dist/prompt-assembly/turn-snapshot.d.ts +40 -0
- package/dist/prompt-assembly/types.d.ts +115 -0
- package/dist/prompts/coordinator.d.ts +27 -0
- package/dist/prompts/default.d.ts +539 -0
- package/dist/prompts/simple-sections.d.ts +45 -0
- package/dist/prompts/supervisor.d.ts +66 -0
- package/dist/scenarios/env.d.ts +28 -0
- package/dist/scenarios/full-body.d.ts +50 -0
- package/dist/scenarios/scenario-registry.d.ts +60 -0
- package/dist/scenarios/teacher-quickstart.d.ts +27 -0
- package/dist/server/http.d.ts +17 -0
- package/dist/stores/cc/lockfile.d.ts +6 -0
- package/dist/stores/cc/mailbox-store.d.ts +8 -0
- package/dist/stores/cc/roster-adapter.d.ts +4 -0
- package/dist/stores/cc/sidecar-transcript.d.ts +66 -0
- package/dist/stores/file/background-agent-store.d.ts +24 -0
- package/dist/stores/file/checkpoint-store.d.ts +38 -0
- package/dist/stores/file/file-snapshot-store.d.ts +25 -0
- package/dist/stores/file/fs-atomic.d.ts +155 -0
- package/dist/stores/file/index.d.ts +89 -0
- package/dist/stores/file/mailbox-store.d.ts +36 -0
- package/dist/stores/file/memory-store.d.ts +82 -0
- package/dist/stores/file/permission-rule-store.d.ts +79 -0
- package/dist/stores/file/session-policy-store.d.ts +28 -0
- package/dist/stores/file/session-store.d.ts +40 -0
- package/dist/stores/file/shared-ledger.d.ts +83 -0
- package/dist/stores/file/tool-result-store.d.ts +11 -0
- package/dist/stores/file/usage-window-store.d.ts +18 -0
- package/dist/stores/file/workflow-journal-store.d.ts +100 -0
- package/dist/stores/file/workflow-run-store.d.ts +11 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +282 -0
- package/dist/tools/fs/encoding.d.ts +60 -0
- package/dist/tools/fs/fs-bash.d.ts +133 -0
- package/dist/tools/fs/fs-pdf.d.ts +28 -0
- package/dist/tools/fs/fs-search-tools.d.ts +5 -0
- package/dist/tools/fs/fs-search-tools.js +0 -1
- package/dist/tools/fs/fs-shared.d.ts +360 -0
- package/dist/tools/fs/fs-write.d.ts +16 -0
- package/dist/tools/fs/gh-rate-limit.d.ts +22 -0
- package/dist/tools/fs/index.d.ts +79 -0
- package/dist/tools/fs/notebook.d.ts +56 -0
- package/dist/tools/fs/pdf.d.ts +70 -0
- package/dist/tools/fs/pdf.js +8 -2
- package/dist/tools/fs/repo-map.d.ts +10 -0
- package/dist/tools/fs/safety.d.ts +376 -0
- package/dist/tools/fs/search.d.ts +131 -0
- package/dist/tools/loop-tick.d.ts +24 -0
- package/dist/tools/monitor.d.ts +55 -0
- package/dist/tools/scheduler-tools.d.ts +51 -0
- package/dist/tools/sql-adapters.d.ts +18 -0
- package/dist/tools/sql.d.ts +25 -0
- package/dist/tools/task-list.d.ts +77 -0
- package/dist/tools/todo.d.ts +8 -0
- package/dist/tools/web.d.ts +184 -0
- package/dist/tools/worktree.d.ts +81 -0
- package/package.json +2 -2
- package/dist/tools/gitea-issue.d.ts +0 -13
- package/dist/tools/gitea-issue.js +0 -75
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1,45 +1,221 @@
|
|
|
1
1
|
import type { DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
|
+
/**
|
|
5
|
+
* In-process hook seam (design/37) — a small, provider-agnostic interception layer modeled on CC's
|
|
6
|
+
* hooks but reduced to three process-internal callbacks (no shell/HTTP executors, no settings files):
|
|
7
|
+
*
|
|
8
|
+
* - {@link Hooks.preToolUse} runs before a tool executes. It may **rewrite** the args (an `allow` with
|
|
9
|
+
* `updatedInput`), **restrict** the call (`deny`/`ask`), and/or inject `additionalContext`. It is a
|
|
10
|
+
* pre-filter only: a hook's `allow` does NOT bypass the tool-policy gate — the policy still runs on
|
|
11
|
+
* the (possibly rewritten) args and has the final say (load-bearing invariant; see {@link runToolGate}).
|
|
12
|
+
* - {@link Hooks.postToolUse} runs after a tool executes. It may replace the result content
|
|
13
|
+
* (`updatedOutput`) and/or append `additionalContext`.
|
|
14
|
+
* - {@link Hooks.userPromptSubmit} runs before the objective becomes a user message. It may `block`
|
|
15
|
+
* submission (the task fails with a model-readable reason) or inject `additionalContext` ahead of it.
|
|
16
|
+
*
|
|
17
|
+
* All feedback the model should see (a block reason, injected context) is wrapped as a
|
|
18
|
+
* `<system-reminder>` via {@link formatHookFeedback} so the model can read and self-correct.
|
|
19
|
+
*/
|
|
4
20
|
export interface Hooks {
|
|
5
21
|
preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise<PreToolUseResult | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Deployment SELF-DECLARATION: this deployment's {@link preToolUse} is a pure OBSERVER — a tracer, an
|
|
24
|
+
* audit sink, a metrics counter — that never judges a call. Declaring it changes exactly one thing:
|
|
25
|
+
* the face stops being folded into the DELEGATION chain (`InheritedGate.parentConstraints`), so a
|
|
26
|
+
* delegated child inherits no screening entry for it.
|
|
27
|
+
*
|
|
28
|
+
* Why the declaration exists. The delegation fold (issue #33) keys on PRESENCE — "a preToolUse is
|
|
29
|
+
* wired" — because a callback cannot be inspected for whether it will ever return a verdict. That
|
|
30
|
+
* reading is right for a screening face and wrong for an observer, and the difference is not free:
|
|
31
|
+
* every inherited entry is one more OPAQUE constraint on the child, recorded on a durable park as
|
|
32
|
+
* `parentConstraintCount`. A deployment that flips on a default-OFF diagnostic tracer therefore also
|
|
33
|
+
* changes the shape every parked child's resume has to re-supply. The declaration is how a deployment
|
|
34
|
+
* says which of the two it wired, on the same footing as every other thing the library trusts a
|
|
35
|
+
* deployment to state about its own wiring.
|
|
36
|
+
*
|
|
37
|
+
* What does NOT change: the callback still runs in THIS task's own tool gate, on every call, with the
|
|
38
|
+
* same context and the same crash posture (a throw is still the fail-closed deny). Observation is
|
|
39
|
+
* untouched; only the inherited constraint goes away.
|
|
40
|
+
*
|
|
41
|
+
* The face also receives a DETACHED copy of the arguments rather than the live object, because the
|
|
42
|
+
* gate reads a rewrite off reference identity and an in-place edit would otherwise be a second channel
|
|
43
|
+
* that outlives the dropped return value. That detachment is BEST-EFFORT, in the same sense (and by the
|
|
44
|
+
* same helper) as every other observe-only payload here: a graph `structuredClone` cannot take — a
|
|
45
|
+
* function, a live handle — falls back to a shallow copy whose nested objects still alias, and a
|
|
46
|
+
* `SharedArrayBuffer` shares its memory even when the clone succeeds. Phase-1 arguments are the model's
|
|
47
|
+
* JSON on the ordinary path, which clones whole; the residual is reachable only through values a
|
|
48
|
+
* DEPLOYMENT put there (its own rewrite, or driving `runToolGate` directly), and the face and the
|
|
49
|
+
* deployment are one trust principal — this seam helps a deployment keep its own declaration, it is not
|
|
50
|
+
* a boundary against an adversary.
|
|
51
|
+
*
|
|
52
|
+
* If a declared-observational face RETURNS SOMETHING ANYWAY, the return is REFUSED, not obeyed: the
|
|
53
|
+
* call proceeds as if the hook had no opinion (`undefined`), and the deployment's `onError` face is
|
|
54
|
+
* told (`phase:"hook"`, classification `observational-hook-verdict-ignored`). That covers the whole
|
|
55
|
+
* result object — an `additionalContext` injection is a run-affecting capability too, so a face that
|
|
56
|
+
* declared it only watches does not get to write into the model's transcript either. The refusal is
|
|
57
|
+
* loud on purpose: a declaration that could be quietly contradicted would be a way to keep a
|
|
58
|
+
* screening face's verdicts while shedding the delegation constraint they belong to.
|
|
59
|
+
*/
|
|
6
60
|
preToolUseObservational?: true;
|
|
7
61
|
postToolUse?(toolName: string, input: unknown, output: HookToolOutput, ctx: HookToolContext): PostToolUseResult | undefined | Promise<PostToolUseResult | undefined>;
|
|
8
62
|
userPromptSubmit?(prompt: string): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* roadmap #5 (CC 198 Stop hook, :473831-): runs when the agent WOULD OTHERWISE END its run (no
|
|
65
|
+
* more tool calls, steering and follow-up queues dry). Return `{ block: reason }` to PUSH BACK —
|
|
66
|
+
* the reason is injected as a model-readable message and the run continues another turn (external
|
|
67
|
+
* adjudication: "the tests still fail", "the deliverable is missing X"). Return `undefined` to let
|
|
68
|
+
* the run end. Guardrails (CC-exact): consecutive blocks are capped (default 8 — the loop then
|
|
69
|
+
* overrides and ends with a warning); `ctx.stopHookActive` is true when the run is ALREADY
|
|
70
|
+
* continuing due to a previous block — a hook that ignores it and blocks unconditionally loops
|
|
71
|
+
* until the cap. `maxTurns` still outranks everything.
|
|
72
|
+
*/
|
|
9
73
|
stop?(ctx: StopHookContext): StopHookResult | undefined | Promise<StopHookResult | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* design/134 (CC PostToolUseFailure parity, SDK 0.3.202): runs after a tool EXECUTION fails —
|
|
76
|
+
* mutually exclusive with {@link postToolUse} (success → postToolUse; failure → this). Fires ONLY
|
|
77
|
+
* for genuine execution failures: a gate/policy block, a plan-mode deny, a pre-execution abort or a
|
|
78
|
+
* durable suspend triggers NEITHER callback (CC scopes those to the PermissionDenied event domain,
|
|
79
|
+
* not implemented here). Capability is additionalContext-only (CC-exact): the text is appended to
|
|
80
|
+
* the error tool result as a `<system-reminder>`; the failure itself cannot be rewritten.
|
|
81
|
+
*/
|
|
10
82
|
postToolUseFailure?(toolName: string, input: unknown, failure: HookToolFailure, ctx: HookToolContext): PostToolUseFailureResult | undefined | Promise<PostToolUseFailureResult | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* design/134 (CC PostToolBatch parity, SDK 0.3.202): fires ONCE after every tool call in a batch
|
|
85
|
+
* (one assistant turn's calls, design/120 stream-inline included) has resolved, before the next
|
|
86
|
+
* model request. Per-tool postToolUse/postToolUseFailure fire first; the batch callback fires after,
|
|
87
|
+
* at the turn boundary. A zero-tool turn does not fire; an aborted/steered-away boundary does not
|
|
88
|
+
* fire (same gate family as design/133 attachments). additionalContext is injected once for the
|
|
89
|
+
* whole batch as a boundary `<system-reminder>` (sanitized, shares the attachment byte cap).
|
|
90
|
+
*
|
|
91
|
+
* RB-467 (test-AI black-box repro, board 2305: ONE injection ate a whole subsequent batch): a batch
|
|
92
|
+
* landing on a boundary where the engine itself injected (final-verification nudge / finalize
|
|
93
|
+
* write-out) is now DELIVERED with `meta.injectedThisTurn` set, instead of being suppressed — for a
|
|
94
|
+
* deployment observer, any suppressed boundary is lost audit data; attribution belongs to the
|
|
95
|
+
* observer. On such a boundary the hook's `additionalContext` is IGNORED (the boundary already
|
|
96
|
+
* carries an engine injection; stacking observer text there is the double-inject this gate
|
|
97
|
+
* originally guarded against — that half of the old behavior stands).
|
|
98
|
+
*/
|
|
11
99
|
postToolBatch?(batch: PostToolBatchCall[], meta?: {
|
|
12
100
|
injectedThisTurn: "final_verification" | "finalize";
|
|
13
101
|
}): PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>;
|
|
102
|
+
/**
|
|
103
|
+
* design/134 (CC PreCompact parity): runs before each compaction, AFTER the trigger gate and a
|
|
104
|
+
* valid cut point are confirmed (so every preCompact corresponds to a compaction that would
|
|
105
|
+
* actually happen) and before the summary LLM call. `trigger`: "auto" (threshold) | "manual"
|
|
106
|
+
* (/compact) | "forced" (promptTooLong recovery and trim-pressure — compaction is not optional
|
|
107
|
+
* there). Return `{ block }` to skip this compaction (honored on auto/manual ONLY; ignored with a
|
|
108
|
+
* trace on forced — blocking a compaction the provider already demanded would kill the run).
|
|
109
|
+
* `additionalInstructions` is merged (appended) into the summarization instructions (CC
|
|
110
|
+
* mergeHookInstructions shape). A THROWING preCompact is swallowed on every path (traced, treated
|
|
111
|
+
* as no-block): an observer bug must never feed the compaction breaker or kill PTL recovery.
|
|
112
|
+
*/
|
|
14
113
|
preCompact?(ctx: PreCompactContext): PreCompactResult | undefined | Promise<PreCompactResult | undefined>;
|
|
114
|
+
/**
|
|
115
|
+
* design/134 (CC PostCompact parity): observe-only, after the compaction landed (summary appended
|
|
116
|
+
* to the session). No return capability (CC-exact); a throw is swallowed and traced.
|
|
117
|
+
*/
|
|
15
118
|
postCompact?(ctx: PostCompactContext): void | Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* StopFailure (the last engine-owned event of the registry-core 0.1.51 hooks contract; CC semantics:
|
|
121
|
+
* "the turn ended because of an API error"). Fires ONCE, after the result is assembled, when the task
|
|
122
|
+
* terminates `failed` BECAUSE the model/API layer errored (a brain error terminal —
|
|
123
|
+
* auth / network / rate_limit / server_error / …). It does NOT fire for engine-owned failure classes:
|
|
124
|
+
* budget, timeout/abort, max-turns, degenerate/walltime cutoffs (deliberate engine cuts), storage
|
|
125
|
+
* conflict, blocked, or a durable suspend — those are not API errors. Observe-only (CC-exact: the
|
|
126
|
+
* contract defines no StopFailure output capability); a throwing callback is swallowed + traced
|
|
127
|
+
* (design/134 discipline — an observer bug must never rewrite a terminal state).
|
|
128
|
+
*/
|
|
16
129
|
stopFailure?(ctx: StopFailureContext): void | Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* design/134 R5 (CC PermissionDenied parity, SDK 0.3.202 — service [418]③): runs when the tool gate's
|
|
132
|
+
* adjudicate chain DENY-SHORT-CIRCUITS a tool call — a ToolPolicy deny, a gate tighten (egress /
|
|
133
|
+
* irreversibility / coarse shellGate) whose `ask` resolved to deny (headless auto-deny included), or the
|
|
134
|
+
* plan-mode write-deny. Closes the observation blind spot where a blocked call fires NEITHER postToolUse
|
|
135
|
+
* NOR postToolUseFailure (R3 scoping). Does NOT fire for:
|
|
136
|
+
* - a **durable suspend** (the ask path — CC surfaces that via a `can_use_tool` control_request);
|
|
137
|
+
* - a **PreToolUse hook deny** (CC-exact: "PreToolUse hook denies bypass canUseTool and are not
|
|
138
|
+
* covered here").
|
|
139
|
+
* DOES fire for a **PreToolUse hook CRASH** (RB-442, `source:"hook"`): a throwing hook is intercepted by
|
|
140
|
+
* the ENGINE, which then makes its OWN fail-closed deny decision. The R5 exclusion above covers the
|
|
141
|
+
* hook's own DENY DECISION — not an engine decision that merely happened near a hook.
|
|
142
|
+
* Observe-only v1 (CC-exact payload `{toolName, input, toolCallId, reason}` + our `source` enum standing
|
|
143
|
+
* in for CC `decision_reason_type`): no `retry` capability (recorded — retry semantics vs our adjudicate
|
|
144
|
+
* chain need their own design), no additionalContext. A throwing callback is swallowed + traced via
|
|
145
|
+
* `onError(phase:"hook")` — an observer bug must never alter the deny outcome (design/134 discipline).
|
|
146
|
+
*/
|
|
17
147
|
permissionDenied?(payload: PermissionDeniedPayload): void | Promise<void>;
|
|
18
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Where a {@link Hooks.permissionDenied} deny came from — our gate-source enum standing in for CC's
|
|
151
|
+
* `decision_reason_type` ('classifier'|'asyncAgent'|'mode'|'rule'), named after OUR adjudicate-chain
|
|
152
|
+
* sources (design/134 R5):
|
|
153
|
+
* - `"policy"` — the ToolPolicy denied directly, or a policy-raised `ask` resolved to deny
|
|
154
|
+
* (headless auto-deny / approver said no / approval aborted).
|
|
155
|
+
* - `"hook"` — a PreToolUse hook `ask` (folded allow→ask) resolved to deny, OR the engine's fail-closed
|
|
156
|
+
* interception of a hook that THREW (RB-442). (A hook's own `deny` short-circuit is EXCLUDED from the
|
|
157
|
+
* event, CC-exact — a decision the hook made; the crash interception is a decision the ENGINE made.)
|
|
158
|
+
* - `"safety"` — the gate's deterministic egress/irreversibility tighten (design/70 / design/77 §4)
|
|
159
|
+
* raised the `ask` that resolved to deny, from an explicit per-tool mark.
|
|
160
|
+
* - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE
|
|
161
|
+
* `TaskSpec.shellGate` doctrine (design/80 D-E), not an explicit per-tool mark.
|
|
162
|
+
* - `"planMode"` — the design/108 plan-mode write-deny short-circuit (a read-only fidelity gate).
|
|
163
|
+
*/
|
|
19
164
|
export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier";
|
|
165
|
+
/** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */
|
|
20
166
|
export interface PermissionDeniedPayload {
|
|
21
167
|
toolName: string;
|
|
168
|
+
/** The FINAL (post-hook-rewrite / post-policy-rewrite) args the chain adjudicated — what would have
|
|
169
|
+
* executed; not necessarily the model's original args. */
|
|
22
170
|
input: unknown;
|
|
23
171
|
toolCallId: string;
|
|
172
|
+
/** Human/model-readable deny reason (CC `reason`) — the RAW decision text, NOT `<system-reminder>`-wrapped. */
|
|
24
173
|
reason: string;
|
|
174
|
+
/** Which gate source produced the deny (our `decision_reason_type` analog). */
|
|
25
175
|
source: PermissionDeniedSource;
|
|
26
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* 1.256 复审 MED-1 — observe-only payload isolation for {@link Hooks.permissionDenied}: clone the tool
|
|
179
|
+
* args before they ride the observer payload, so a hook mutating `payload.input` can never pollute the
|
|
180
|
+
* LIVE args object (later events / audit records share it). Same posture as the postToolUseFailure
|
|
181
|
+
* details clone (prepare-task): `structuredClone` first; a non-structured-cloneable graph
|
|
182
|
+
* (functions/handles) falls back to a SHALLOW plain object/array copy (top-level mutation isolated);
|
|
183
|
+
* a non-object primitive passes through as-is (immutable anyway).
|
|
184
|
+
*/
|
|
27
185
|
export declare function cloneObserverInput(input: unknown): unknown;
|
|
186
|
+
/** Context for {@link Hooks.stopFailure} — aligned with the TaskResult error face (observe-only). */
|
|
28
187
|
export interface StopFailureContext {
|
|
188
|
+
/** Human-readable error message (the assembled `errorMessage`; any `[code]` prefix already stripped). */
|
|
29
189
|
error: string;
|
|
190
|
+
/** Machine-readable kind — the assembled `errorCode` lifted from the brain's `[code]` prefix
|
|
191
|
+
* (auth / network / rate_limit / server_error / …); absent when the provider stamped no code. */
|
|
30
192
|
errorKind?: string;
|
|
193
|
+
/** Turns completed when the failure ended the run. */
|
|
31
194
|
turns: number;
|
|
32
195
|
}
|
|
196
|
+
/** The failed tool execution a PostToolUseFailure callback inspects (design/134). */
|
|
33
197
|
export interface HookToolFailure {
|
|
198
|
+
/** Model-facing error text of the failed call (the error tool result's text content, joined). */
|
|
34
199
|
error: string;
|
|
200
|
+
/** Wide any-abort semantics (⊇ CC `is_interrupt`, which is user-interrupt specifically): true when
|
|
201
|
+
* the task abort signal fired — timeout, budget, cancel or suspend — not only a human interrupt. */
|
|
35
202
|
isInterrupt: boolean;
|
|
203
|
+
/** Tool execution time in ms when the engine has it (CC 0.3.202 `duration_ms` parity); else absent. */
|
|
36
204
|
durationMs?: number;
|
|
205
|
+
/** Element-level clone of the error result's content blocks (mutating them cannot rewrite the
|
|
206
|
+
* transcript — the failure is observe-only). */
|
|
37
207
|
content: Array<TextContent | ImageContent | DocumentContent>;
|
|
208
|
+
/** BEST-EFFORT deep clone (structuredClone) of the tool's raw details. When the details graph is
|
|
209
|
+
* not structured-cloneable (functions/handles), this is the LIVE reference — treat it as
|
|
210
|
+
* READ-ONLY: a mutation would corrupt the result the loop is about to commit. */
|
|
38
211
|
details: unknown;
|
|
39
212
|
}
|
|
213
|
+
/** A PostToolUseFailure result: append model-readable context to the error result (CC-exact, no rewrite). */
|
|
40
214
|
export interface PostToolUseFailureResult {
|
|
41
215
|
additionalContext?: string;
|
|
42
216
|
}
|
|
217
|
+
/** One resolved call in a postToolBatch payload (CC PostToolBatchToolCall shape, thin projection —
|
|
218
|
+
* `response` is a bounded text digest of the result, never the full details object). */
|
|
43
219
|
export interface PostToolBatchCall {
|
|
44
220
|
toolName: string;
|
|
45
221
|
input: unknown;
|
|
@@ -47,88 +223,307 @@ export interface PostToolBatchCall {
|
|
|
47
223
|
response?: string;
|
|
48
224
|
isError: boolean;
|
|
49
225
|
}
|
|
226
|
+
/** A PostToolBatch result: inject context once for the whole batch at the turn boundary. */
|
|
50
227
|
export interface PostToolBatchResult {
|
|
51
228
|
additionalContext?: string;
|
|
52
229
|
}
|
|
230
|
+
/** Context for {@link Hooks.preCompact} (design/134). */
|
|
53
231
|
export interface PreCompactContext {
|
|
232
|
+
/** "auto" = threshold-triggered; "manual" = /compact; "forced" = promptTooLong recovery or
|
|
233
|
+
* trim-pressure propagation (block is ignored on forced — the compaction is not optional). */
|
|
54
234
|
trigger: "auto" | "manual" | "forced";
|
|
235
|
+
/** The summarization instructions in effect (spec/deployment-level), when set. */
|
|
55
236
|
customInstructions?: string;
|
|
56
237
|
}
|
|
238
|
+
/** A PreCompact result: skip this compaction (auto/manual only) and/or extend the summary instructions. */
|
|
57
239
|
export interface PreCompactResult {
|
|
240
|
+
/** When set (and trigger is not "forced"), this compaction is skipped; the reason is traced. */
|
|
58
241
|
block?: string;
|
|
242
|
+
/** Appended to the summarization instructions for this compaction (never replaces them).
|
|
243
|
+
* LLM-summary path only: a compaction served from a `summaryProvider` reuse (Seam C) never runs
|
|
244
|
+
* the summarization call, so these instructions are not consumed there (recorded design gap —
|
|
245
|
+
* extending the provider contract is out of scope for design/134). */
|
|
59
246
|
additionalInstructions?: string;
|
|
60
247
|
}
|
|
248
|
+
/** Context for {@link Hooks.postCompact} (design/134, observe-only). */
|
|
61
249
|
export interface PostCompactContext {
|
|
62
250
|
trigger: "auto" | "manual" | "forced";
|
|
251
|
+
/** The conversation summary the compaction produced (CC `compact_summary` parity). */
|
|
63
252
|
summary: string;
|
|
64
253
|
tokensBefore?: number;
|
|
65
254
|
tokensAfter?: number;
|
|
66
255
|
}
|
|
256
|
+
/** Context for the {@link Hooks.stop} hook (CC `stop_hook_active` parity). */
|
|
67
257
|
export interface StopHookContext {
|
|
258
|
+
/** True when this run is already continuing because a previous stop() blocked — check it and
|
|
259
|
+
* return success (undefined) once your condition can't be improved, or you will loop to the cap. */
|
|
68
260
|
stopHookActive: boolean;
|
|
261
|
+
/** Consecutive blocks so far in this run (resets when a stop() call lets the run proceed). */
|
|
69
262
|
consecutiveBlocks: number;
|
|
263
|
+
/**
|
|
264
|
+
* RB-177 ([1730], measured downstream) — a view of the conversation this hook is being asked to judge.
|
|
265
|
+
*
|
|
266
|
+
* Until now a Stop hook received two counters and nothing else. That is enough for a hook that asks
|
|
267
|
+
* "have I blocked too often?", and structurally insufficient for the entire class of hooks that ask
|
|
268
|
+
* anything ABOUT THE RUN — which is what the prompt-shaped hooks a settings file can declare are for.
|
|
269
|
+
* Measured end to end downstream: the carrier was constructed, invoked, and returned without error, and
|
|
270
|
+
* the model answered "I don't have direct access to your file system…" — because nobody had given the
|
|
271
|
+
* evaluator the session. Every layer reported success and the feature could not work. A hook cannot
|
|
272
|
+
* judge a run it cannot see, so the gap belonged here rather than in any of the layers that were
|
|
273
|
+
* dutifully passing an empty view along.
|
|
274
|
+
*
|
|
275
|
+
* LAZY on purpose: a hook that only reads the counters pays nothing, and a hook that needs the
|
|
276
|
+
* conversation gets it in full rather than through a truncation policy the engine would have to invent
|
|
277
|
+
* on its behalf. Callers decide what to send to a model and how much — that decision has a budget and a
|
|
278
|
+
* trust tier attached, and both belong to the caller.
|
|
279
|
+
*
|
|
280
|
+
* Absent when the runner cannot supply one (a stop gate wired without a session).
|
|
281
|
+
*/
|
|
70
282
|
getBranch?: () => Promise<readonly SessionTreeEntry[]>;
|
|
71
283
|
}
|
|
284
|
+
/** A Stop hook result: block the run from ending, with a model-readable reason. */
|
|
72
285
|
export interface StopHookResult {
|
|
286
|
+
/** When set, the run does NOT end: this reason is injected (as a `<system-reminder>`) and the
|
|
287
|
+
* model gets another turn to address it. */
|
|
73
288
|
block?: string;
|
|
289
|
+
/**
|
|
290
|
+
* CC 2.1.201 parity (CC :472050-472077): extra model-readable context injected at the stop point —
|
|
291
|
+
* DECOUPLED from `block`. When present it is injected (as a `<system-reminder>`, neutral framing:
|
|
292
|
+
* "Stop hook additional context: …") whether or not the hook blocked; `additionalContext` WITHOUT
|
|
293
|
+
* `block` still continues the run one more turn, but does NOT count toward the consecutive-block
|
|
294
|
+
* cap (the cap is driven by `block` alone).
|
|
295
|
+
*/
|
|
74
296
|
additionalContext?: string;
|
|
75
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* A READ-ONLY path-resolution capability face over the task's {@link ExecutionEnv}, handed to tool hooks
|
|
300
|
+
* on {@link HookToolContext.env} (the hook-env capability seam).
|
|
301
|
+
*
|
|
302
|
+
* **Why it exists.** A hook that judges a path — "is this write inside the workspace", "does this
|
|
303
|
+
* innocent-looking name resolve onto a guarded file" — must resolve it in the filesystem the HANDS
|
|
304
|
+
* actually write to. For a sandboxed/remote deployment that filesystem is NOT the host process's `fs`:
|
|
305
|
+
* the env is minted by `RunnerDeps.executionEnvFactory` DURING prepare, i.e. after the spec (and any
|
|
306
|
+
* policy/hook the caller built with it) already exists, so a hook closed over a spec-time env resolves
|
|
307
|
+
* against the wrong disk and answers `exists`/symlink questions about a different machine. Reading the
|
|
308
|
+
* env off the CALL CONTEXT removes the ordering problem structurally: the context is built after the env
|
|
309
|
+
* is, and a resumed/revived run's rebuilt env is picked up with no re-instantiation protocol.
|
|
310
|
+
*
|
|
311
|
+
* **Capabilities, not the object.** Hooks are an extension boundary the deployment fills with code whose
|
|
312
|
+
* job is to DECIDE; the whole `ExecutionEnv` is a face that can ACT (`exec`, `writeFile`, `remove`,
|
|
313
|
+
* `cleanup`). So this is four INDEPENDENTLY BOUND read primitives and nothing else: no `env`
|
|
314
|
+
* back-reference, no prototype of the env object, and the face itself is frozen (a hook cannot re-point a
|
|
315
|
+
* member for a later hook in the same run). Widening it later is additive and cheap; taking a wide object
|
|
316
|
+
* back is not.
|
|
317
|
+
*
|
|
318
|
+
* The line this draws, stated so nobody reads more into it: it separates what a hook is HANDED from what
|
|
319
|
+
* the environment can do. It is NOT an isolation boundary against code that can already reach the env's own
|
|
320
|
+
* class — anything that can redefine `SomeExecutionEnv.prototype.exec` intercepts every call the ENGINE
|
|
321
|
+
* makes into that env, receiver included, and needs nothing from this face to do it. Deployments that must
|
|
322
|
+
* withstand that run their hooks somewhere else, not behind a narrower argument.
|
|
323
|
+
*
|
|
324
|
+
* **Names are the env's own** ({@link ExecutionEnv} / `FileSystem`), deliberately: a hook author reads one
|
|
325
|
+
* contract, not a translation of it. The seam sketch's `canonicalize`/`root` are `canonicalPath`/`cwd`
|
|
326
|
+
* here — `canonicalPath` IS the realpath-semantics primitive, and `ExecutionEnv` declares no "root" at all
|
|
327
|
+
* (the engine's task root is a RUNNER notion — `deps.rootPath`/worktree/`requestedCwd` — not something the
|
|
328
|
+
* environment reports about itself), so the honest anchor is the env's own working directory. `cwd` is a
|
|
329
|
+
* FUNCTION, not the snapshot string, so a hook reading it late sees where the env is NOW. Read {@link
|
|
330
|
+
* HookEnvCapabilities.cwd} before using it as a base for RELATIVE paths — the task's tracked cwd is a
|
|
331
|
+
* different value, and this face carries the env's, on purpose.
|
|
332
|
+
*
|
|
333
|
+
* **Every member is optional, and absence is the signal.** A member is present iff the underlying env
|
|
334
|
+
* exposes that primitive (`readLink` is optional in `FileSystem`; a transport env may implement less than
|
|
335
|
+
* it declares). A missing member is never a stub that throws or an emulation that guesses — the hook is
|
|
336
|
+
* told, in the only way that cannot be misread, that this environment cannot answer, and takes its own
|
|
337
|
+
* conservative branch. The face carries a NULL PROTOTYPE so that reading is a question about this object
|
|
338
|
+
* alone (see {@link createHookEnvCapabilities}). Errors that the env CAN report stay in the `Result`
|
|
339
|
+
* (these methods never throw, inheriting the `FileSystem` contract verbatim).
|
|
340
|
+
*/
|
|
76
341
|
export interface HookEnvCapabilities {
|
|
342
|
+
/** {@link ExecutionEnv.canonicalPath} — the canonical path of an EXISTING path, symlinks resolved
|
|
343
|
+
* (realpath semantics), in the env's own namespace. */
|
|
77
344
|
canonicalPath?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
345
|
+
/** {@link ExecutionEnv.exists} — `false` for a missing path; other failures (permission, transport)
|
|
346
|
+
* come back as a `FileError`, which is NOT an answer of "absent". */
|
|
78
347
|
exists?(path: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
348
|
+
/** {@link ExecutionEnv.readLink} — a symlink's raw target, ONE hop, not followed. Absent whenever the
|
|
349
|
+
* env omits the primitive: a DANGLING symlink's real target is then unknowable to the hook. */
|
|
79
350
|
readLink?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
351
|
+
/**
|
|
352
|
+
* {@link ExecutionEnv.cwd} — the ENVIRONMENT's own working directory, read live: the base the env's own
|
|
353
|
+
* primitives resolve a relative path against, and the only anchor it reports about itself.
|
|
354
|
+
*
|
|
355
|
+
* NOT the task's tracked cwd, and the difference is load-bearing for anyone judging a RELATIVE path.
|
|
356
|
+
* The engine keeps a separate per-task cwd — it starts at the resolved task root (which is
|
|
357
|
+
* `deps.rootPath` when that is set, not necessarily the env's cwd) and moves with the shell's `cd` and
|
|
358
|
+
* with worktree entry — and the TOOLS' relative paths resolve against THAT, while the environment is
|
|
359
|
+
* never told it moved. So a hook that must decide a relative target the way the tool will interpret it
|
|
360
|
+
* cannot take the base from here: judge absolute targets, or have the deployment hand the tracked cwd
|
|
361
|
+
* to the hook through its own channel. What this member is good for is the env-side anchor — which
|
|
362
|
+
* filesystem/mount the answers below are about.
|
|
363
|
+
*/
|
|
80
364
|
cwd?(): string;
|
|
81
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Build the {@link HookEnvCapabilities} face for `env`. One wrapper per primitive (never a bound method of
|
|
368
|
+
* the env, never the env itself), presence probed at build time so the face's shape reports what this env
|
|
369
|
+
* can actually do, and frozen so it cannot be re-pointed by whoever receives it.
|
|
370
|
+
*
|
|
371
|
+
* Two details the "absent means cannot" contract rests on, both cheap and both load-bearing:
|
|
372
|
+
* · **null prototype, both ways.** Absence must be a fact about THIS object — on the way OUT (a face
|
|
373
|
+
* behind `Object.prototype` would answer `ctx.env.readLink` from there, turning "this env cannot read
|
|
374
|
+
* links" into "here is some function"; freezing the face does not freeze what it inherits) and on the
|
|
375
|
+
* way IN ({@link findEnvSlot} stops before `Object.prototype`, so an ambient property of that name is
|
|
376
|
+
* never adopted as a capability and never gets called with the env as its receiver).
|
|
377
|
+
* · **capture, do not re-read.** Each primitive is read ONCE, checked, and that captured callable is what
|
|
378
|
+
* the wrapper invokes — through module-load-captured intrinsics, never a build-time or call-time global
|
|
379
|
+
* lookup. Re-reading the property, or the machinery that does the reading, would let something other
|
|
380
|
+
* than what was validated end up running, or the receiver end up somewhere it was never handed.
|
|
381
|
+
* {@link createCwdReader} carries the one live read under the same rules.
|
|
382
|
+
*/
|
|
82
383
|
export declare function createHookEnvCapabilities(env: ExecutionEnv): HookEnvCapabilities;
|
|
384
|
+
/** Identifying context passed to tool hooks. */
|
|
83
385
|
export interface HookToolContext {
|
|
84
386
|
toolCallId: string;
|
|
85
387
|
toolName: string;
|
|
388
|
+
/**
|
|
389
|
+
* The read-only path-resolution face over the env the hands run against (see
|
|
390
|
+
* {@link HookEnvCapabilities} for the why, and for why it is capabilities rather than the env object).
|
|
391
|
+
*
|
|
392
|
+
* ABSENT when the deployment wired no execution environment at all (no `executionEnv` /
|
|
393
|
+
* `executionEnvFactory`: the run has no hands, and the engine's internal no-I/O placeholder is not an
|
|
394
|
+
* environment to report). Present ⇒ inspect the individual members: each one is itself optional.
|
|
395
|
+
*/
|
|
86
396
|
env?: HookEnvCapabilities;
|
|
87
397
|
}
|
|
398
|
+
/** The executed tool result a PostToolUse hook inspects. */
|
|
88
399
|
export interface HookToolOutput {
|
|
89
400
|
content: Array<TextContent | ImageContent | DocumentContent>;
|
|
90
401
|
details: unknown;
|
|
91
402
|
isError: boolean;
|
|
92
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* A PreToolUse hook result: a {@link PermissionResult} (so a hook can `deny`/`ask`, or `allow` with an
|
|
406
|
+
* `updatedInput` rewrite) plus optional `additionalContext` injected into the eventual tool result.
|
|
407
|
+
*/
|
|
93
408
|
export type PreToolUseResult = PermissionResult & {
|
|
94
409
|
additionalContext?: string;
|
|
95
410
|
};
|
|
411
|
+
/** A PostToolUse hook result: replace the tool output and/or append context (both optional). */
|
|
96
412
|
export interface PostToolUseResult {
|
|
413
|
+
/** If provided, replaces the tool result content array in full. */
|
|
97
414
|
updatedOutput?: Array<TextContent | ImageContent | DocumentContent>;
|
|
415
|
+
/** Appended to the result as a `<system-reminder>` the model can read. */
|
|
98
416
|
additionalContext?: string;
|
|
99
417
|
}
|
|
418
|
+
/** A UserPromptSubmit hook result: block the submission, or inject context ahead of the prompt. */
|
|
100
419
|
export interface UserPromptSubmitResult {
|
|
420
|
+
/** Block submission entirely; the task fails with this model-readable reason. */
|
|
101
421
|
block?: string;
|
|
422
|
+
/** Injected ahead of the user's prompt (wrapped as a `<system-reminder>`). */
|
|
102
423
|
additionalContext?: string;
|
|
103
424
|
}
|
|
425
|
+
/** Wrap model-facing hook/gate feedback in a `<system-reminder>` so it reads as guidance, not data.
|
|
426
|
+
* NOTE (council design/74 #6): this does NOT escape a literal `</system-reminder>` in `text` — callers MUST
|
|
427
|
+
* pass trusted, first-party strings (every current caller does: fixed gate/limit messages). If a future
|
|
428
|
+
* caller needs to relay UNTRUSTED content (tool output, user data), it must sanitize the close tag first
|
|
429
|
+
* (or use the `delimitUntrusted` fence), or a crafted payload could break out of the reminder framing. */
|
|
104
430
|
export declare function formatHookFeedback(text: string): string;
|
|
431
|
+
/** The outcome of the two-phase tool gate, mapped onto the harness `tool_call` hook return shape. */
|
|
105
432
|
export interface ToolGateResult {
|
|
433
|
+
/** Block execution (the loop emits an error tool result with `reason`). */
|
|
106
434
|
block?: boolean;
|
|
435
|
+
/** Model-readable block reason (already `<system-reminder>`-wrapped). */
|
|
107
436
|
reason?: string;
|
|
437
|
+
/**
|
|
438
|
+
* What ended the approval this call was waiting on, when this gate pass settled one (the ask
|
|
439
|
+
* resolution's own {@link import("./tool-policy.js").ApprovalSettledBy}, carried out verbatim — this
|
|
440
|
+
* layer classifies nothing). Present on BOTH terminal exits, the block and the allow: an approved
|
|
441
|
+
* call is a settled approval too, and a caller that saw the source only on refusals would read its
|
|
442
|
+
* absence on an execution as "nobody approved this". Absent on everything that settled no approval —
|
|
443
|
+
* a straight policy deny, a hook deny, a posture verdict where nobody was asked, every ordinary
|
|
444
|
+
* allow. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
|
|
445
|
+
* frame: the machine-readable half of the disclosure `reason` makes in words, on a channel no tool
|
|
446
|
+
* and no post-tool hook can write.
|
|
447
|
+
*/
|
|
108
448
|
settledBy?: import("./tool-policy.js").ApprovalSettledBy;
|
|
449
|
+
/** Rewritten args to execute with (re-validated by the loop); omitted when nothing rewrote. */
|
|
109
450
|
updatedInput?: unknown;
|
|
451
|
+
/**
|
|
452
|
+
* design/45: the gate routed a policy `ask` to a **durable suspension** (no synchronous approver).
|
|
453
|
+
* `suspendAsk` already persisted the checkpoint and aborted the run; the caller records this so the
|
|
454
|
+
* task assembles as `status:"suspended"` with the token. Suspend lives ONLY here in the gate's `ask`
|
|
455
|
+
* branch — it never becomes a `PermissionResult` action (policy stays pure allow/ask/deny, §15.2).
|
|
456
|
+
*/
|
|
110
457
|
suspend?: {
|
|
111
458
|
token: import("./checkpoint-store.js").CheckpointToken;
|
|
112
459
|
gate: import("./checkpoint-store.js").CheckpointGate;
|
|
113
460
|
};
|
|
461
|
+
/** PreToolUse `additionalContext` to append to this call's eventual tool result (correlated by id). */
|
|
114
462
|
preToolContext: string[];
|
|
115
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* The gate's content-ask routing outcome — deliberately a channel of its OWN, not a
|
|
466
|
+
* field on the permission verdict. A permission verdict is folded, re-adjudicated and re-presented by
|
|
467
|
+
* layers that know nothing about answers; an answer riding one of those would be dropped by the fold or
|
|
468
|
+
* carried into a second adjudication. Three arms, one per thing that can happen:
|
|
469
|
+
* · `answered` — a person answered. The gate lets the call execute and the answer is delivered to the
|
|
470
|
+
* tool through the run's own one-shot binding (the gate never carries the answer itself).
|
|
471
|
+
* · `unavailable` — nobody was reachable and NOTHING was shown (the {@link
|
|
472
|
+
* import("./ask-question.js").QuestionUnavailable} contract). Only this arm may be re-routed — the
|
|
473
|
+
* gate parks the question for a later human, or answers the honest "it was NOT shown" refusal.
|
|
474
|
+
* · `delivery_failure` — delivery was attempted and failed. NEVER re-routed: the question may already
|
|
475
|
+
* have been rendered to a person, so parking it would put one question in front of two deciders. It
|
|
476
|
+
* is reported to the model through the tool's own coded-failure / degrade exits.
|
|
477
|
+
*/
|
|
116
478
|
export type ContentAskOutcome = {
|
|
117
479
|
kind: "answered";
|
|
480
|
+
/** The EXACT batch that was put in front of the person, retained by the resolver as an isolated
|
|
481
|
+
* snapshot. The gate executes this and nothing else, so "what was shown" and "what runs" are the
|
|
482
|
+
* same object graph by construction rather than by two parties agreeing not to mutate one. */
|
|
118
483
|
presentedInput: unknown;
|
|
119
484
|
} | {
|
|
120
485
|
kind: "unavailable";
|
|
486
|
+
/** The retained snapshot, when the resolver got far enough to make one (i.e. it actually waited
|
|
487
|
+
* on a callback). The park persists THIS, not the caller's live object: the wait is a window in
|
|
488
|
+
* which whoever owns the rewrite can change it, and a checkpoint holding bytes the gate never
|
|
489
|
+
* adjudicated is one a later human would answer as a different question. Absent when no wait
|
|
490
|
+
* happened, in which case there is no window and the live object is still the validated one. */
|
|
121
491
|
presentedInput?: unknown;
|
|
492
|
+
/** True iff the park closure took its SYNC-FIRST DECLINE for this call — it has not yet made a
|
|
493
|
+
* real suspend attempt, so re-entering it now IS that attempt rather than a second one. False
|
|
494
|
+
* when the park already had its attempt (no live face to prefer, or a durable mandate): the gate
|
|
495
|
+
* must not re-enter, because a suspend and a checkpoint write are not idempotent, and a backend
|
|
496
|
+
* that committed before surfacing a transport error would be left an orphan row under a second
|
|
497
|
+
* token. Exactly one real attempt per call, on every path. */
|
|
122
498
|
parkDeclined: boolean;
|
|
123
499
|
} | {
|
|
124
500
|
kind: "delivery_failure";
|
|
125
501
|
code: string;
|
|
502
|
+
/** The retained snapshot, when the resolver got far enough to make one. Executed for the same
|
|
503
|
+
* reason the answered arm's is: the bound failure names the batch it belongs to, and executing a
|
|
504
|
+
* different one would drop it and re-ask the person. Absent when the refusal happened before a
|
|
505
|
+
* snapshot could be taken. */
|
|
126
506
|
presentedInput?: unknown;
|
|
127
507
|
};
|
|
508
|
+
/**
|
|
509
|
+
* A durable park that was ATTEMPTED for this call and could not be minted, carrying the reason in the
|
|
510
|
+
* words of whatever refused it. Distinct from the `undefined` return, which means "no park attempt
|
|
511
|
+
* happened" (no facility armed, or the closure took its sync-first decline) — that case hides nothing,
|
|
512
|
+
* while this one does: the gate falls back to the synchronous approver chain either way (the
|
|
513
|
+
* compensation contract — an uncommitted checkpoint is a safe fallback — is unchanged), but on a
|
|
514
|
+
* headless deployment that fallback is the auto-deny whose text is "no approver is wired". True as far
|
|
515
|
+
* as it goes, and the whole reason the call did not park lives somewhere else entirely (the host's
|
|
516
|
+
* `onError` face). So the reason travels with the fall-through and the deny quotes it.
|
|
517
|
+
*/
|
|
128
518
|
export interface ParkAttemptFailed {
|
|
519
|
+
/** Why the park could not be minted — a sentence, already worded for a reader. */
|
|
129
520
|
parkFailed: string;
|
|
130
521
|
}
|
|
522
|
+
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort signal. */
|
|
131
523
|
export interface ToolGateInput {
|
|
524
|
+
/** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
|
|
525
|
+
* isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
|
|
526
|
+
* wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
|
|
132
527
|
onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
|
|
133
528
|
event: {
|
|
134
529
|
toolCallId: string;
|
|
@@ -136,29 +531,181 @@ export interface ToolGateInput {
|
|
|
136
531
|
input: Record<string, unknown>;
|
|
137
532
|
};
|
|
138
533
|
preToolUse?: Hooks["preToolUse"];
|
|
534
|
+
/** The read-only env capability face put on every {@link HookToolContext} this gate call
|
|
535
|
+
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
536
|
+
* passed through unchanged; omitted when the deployment wired no execution environment. */
|
|
139
537
|
hookEnv?: HookEnvCapabilities;
|
|
538
|
+
/** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow). */
|
|
140
539
|
adjudicate?: (req: ToolCallRequest) => Promise<PermissionResult>;
|
|
540
|
+
/** Resolve an `ask` to allow/deny via `onAsk` (abort-bound). Required iff a decision can be `ask`.
|
|
541
|
+
* G1 three-value ([871]①): the result may carry `approverUnavailable` — the live approver affirmed no
|
|
542
|
+
* operator is reachable for THIS ask; the gate then re-routes onto the durable park leg (below),
|
|
543
|
+
* keeping the carried fail-closed deny when no park is armed. */
|
|
141
544
|
resolveAsk: (decision: PermissionResult, req: ToolCallRequest) => Promise<ResolvedAsk>;
|
|
142
|
-
|
|
545
|
+
/**
|
|
546
|
+
* design/45: route a policy `ask` to a **durable suspension** instead of the synchronous `resolveAsk`
|
|
547
|
+
* (onAsk) path. Called in the `ask` branch with the FINAL post-hook args; if it returns a suspend
|
|
548
|
+
* directive it has already persisted the checkpoint + aborted the run, and the gate short-circuits to
|
|
549
|
+
* `{ suspend }`. Returns `undefined` to fall through to `resolveAsk` (non-durable / declined). A
|
|
550
|
+
* {@link ParkAttemptFailed} ALSO falls through to `resolveAsk` — same fallback, same compensation —
|
|
551
|
+
* but carries why the mint failed so a resulting deny can name it instead of leaving the model and
|
|
552
|
+
* the operator with the fallback's own reason as if it were the cause.
|
|
553
|
+
* Omitted entirely → no durable mode (the 1.63 synchronous path, unchanged).
|
|
554
|
+
*
|
|
555
|
+
* The armed closure itself may also DECLINE (return `undefined` up front) to prefer the live
|
|
556
|
+
* synchronous approver: the Runner's closure does so for a plain AND for a SAFETY ask whenever the
|
|
557
|
+
* resolved seat is a FUNCTION-valued `onAsk` — even under `durableApproval`, which is the FALLBACK for
|
|
558
|
+
* when no live approver is present, not an "always park" mandate — an interactive per-edit ask must
|
|
559
|
+
* resolve in-stream, not cost a park/rebuild round-trip. The gate stays a pure fall-through consumer of
|
|
560
|
+
* that choice. What KEEPS the durable leg is no function seat (absent, or a blanket `"allow"`/`"deny"`
|
|
561
|
+
* string mode — a policy setting is not a live approver), a control-plane `forceDurableGate`, a live
|
|
562
|
+
* face that already answered `"unavailable"` for this ask, or an inherited-unavailable marked call.
|
|
563
|
+
* `safety` no longer participates in the park/sync decision at all — it only selects the gate KIND
|
|
564
|
+
* (`irreversible_ask`) once a park is minted.
|
|
565
|
+
*/
|
|
566
|
+
suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown,
|
|
567
|
+
/** design/80 D-2: present (non-undefined) ONLY when this ask is a SAFETY tighten (the egress/
|
|
568
|
+
* irreversibility gate tightened a surviving `allow` to `ask`, `decisionReason === "safety"`). Drives the
|
|
569
|
+
* mint-site gate-kind choice — a safety tighten mints `irreversible_ask` (non-budgetable) EVEN WHEN
|
|
570
|
+
* `durableApproval` is wired. The object records which axis(es) tightened (persisted as the gate's
|
|
571
|
+
* `safetyAxis`). Absent for a plain policy/hook `ask` (→ the normal `human`/`irreversible_ask` choice). */
|
|
572
|
+
safety?: import("./checkpoint-store.js").SafetyAxis,
|
|
573
|
+
/** The three-valued live-face verdict, widened by design/174 to cover BOTH live faces: true ⇒ the live face this
|
|
574
|
+
* ask would have been routed to already answered `"unavailable"` for it — the permission approver for
|
|
575
|
+
* a permission ask, the question face for a content ask. Either way there is no live leg left to
|
|
576
|
+
* prefer, so the closure must NOT take its sync-first decline and should park durably. All other
|
|
577
|
+
* decline/pre-commit-failure paths keep their existing `undefined` fallbacks (the gate then keeps the
|
|
578
|
+
* fail-closed deny / typed refusal the caller carries). */
|
|
579
|
+
liveFaceUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
580
|
+
/**
|
|
581
|
+
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
582
|
+
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
|
583
|
+
* post-policy request (the same args that will execute), and it owns everything about delivery:
|
|
584
|
+
* validating the batch before anything is put in front of a person, calling the live face, and binding
|
|
585
|
+
* the answer to this call id for the tool to pick up. It returns only WHICH of the three outcomes
|
|
586
|
+
* happened — never the answer, which travels the run's own binding.
|
|
587
|
+
*
|
|
588
|
+
* CONTRACT: it must not throw (a delivery failure is a returned value, so the gate can tell "failed"
|
|
589
|
+
* from "nobody home" — the two have opposite re-routing rules). Passed UNCONDITIONALLY by the runner,
|
|
590
|
+
* including on runs with no live face: it then answers `unavailable` without contacting anything, which
|
|
591
|
+
* is exactly the pre-174 park/refusal behavior. Absent entirely ⇒ the pre-174 path verbatim.
|
|
592
|
+
*/
|
|
143
593
|
resolveContentAsk?: (req: ToolCallRequest) => Promise<ContentAskOutcome>;
|
|
594
|
+
/**
|
|
595
|
+
* design/70: the called tool is egress-marked (`ToolSpec.egress` — an external write: open a PR,
|
|
596
|
+
* push, send). The gate tightens a surviving `allow` to `ask` so an egress tool is NEVER
|
|
597
|
+
* auto-allowed — execution always passes an explicit ask-resolution (onAsk / durable suspend);
|
|
598
|
+
* headless resolves to deny. Filled by the caller from the tool's spec; policies stay egress-blind.
|
|
599
|
+
*/
|
|
144
600
|
egress?: boolean;
|
|
601
|
+
/**
|
|
602
|
+
* design/77 §4 (Gate 4): the called tool's irreversibility tier (`ToolSpec.irreversibility`), filled by
|
|
603
|
+
* the caller from the tool's spec. After the egress tighten, the gate runs a SECOND deterministic tighten:
|
|
604
|
+
* `"always"` tightens a surviving `allow` to `ask`; `"maybe"` calls {@link reversibilityProbe} and tightens
|
|
605
|
+
* UNLESS the probe reports reversible (fail-closed on timeout/throw); `"never"`/undefined → untouched.
|
|
606
|
+
* Policies stay irreversibility-blind; the gate is the single chokepoint.
|
|
607
|
+
*/
|
|
145
608
|
irreversibility?: "never" | "maybe" | "always";
|
|
609
|
+
/**
|
|
610
|
+
* design/77 §4: the `"maybe"`-tier probe from the tool's spec (read at prepare-time, captured here — not a
|
|
611
|
+
* tool argument). Called ONLY when `irreversibility === "maybe"` && the surviving decision is `allow`,
|
|
612
|
+
* time-bounded by {@link approvalTimeoutMs}. Fail-closed: anything other than `{ reversible: true }`
|
|
613
|
+
* (including a timeout or a throw) tightens to `ask`. A probe is never trusted to AUTO-ALLOW past the gate.
|
|
614
|
+
*/
|
|
146
615
|
reversibilityProbe?: (args: unknown) => {
|
|
147
616
|
reversible: boolean;
|
|
148
617
|
} | Promise<{
|
|
149
618
|
reversible: boolean;
|
|
150
619
|
}>;
|
|
620
|
+
/** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`. */
|
|
151
621
|
approvalTimeoutMs?: number;
|
|
622
|
+
/** design/77 §4: the task abort signal — bounds {@link reversibilityProbe} by the task's real deadline
|
|
623
|
+
* (timeout/cancel) in addition to {@link approvalTimeoutMs}; an abort while probing fails closed to `ask`. */
|
|
152
624
|
abortSignal?: AbortSignal;
|
|
625
|
+
/**
|
|
626
|
+
* design/134 R5: observer for the gate's DENY short-circuit ({@link Hooks.permissionDenied}), pre-wrapped
|
|
627
|
+
* by the caller (swallow + onError) so it never throws. Fired ONCE, at the single deny exit of the
|
|
628
|
+
* adjudicate chain — a policy deny or a resolved-ask deny. NOT fired for a PreToolUse hook deny
|
|
629
|
+
* (CC-exact exclusion) or a durable suspend (the ask path). runToolGate still try/catches defensively:
|
|
630
|
+
* an observer must never alter the deny outcome.
|
|
631
|
+
*
|
|
632
|
+
* RB-442: ALSO fired for the engine's fail-closed interception of a THROWING PreToolUse hook, with
|
|
633
|
+
* `source:"hook"` — the CC-exact exclusion is the hook's own deny DECISION, not an engine decision.
|
|
634
|
+
*/
|
|
153
635
|
permissionDenied?: (payload: PermissionDeniedPayload) => void | Promise<void>;
|
|
636
|
+
/**
|
|
637
|
+
* RB-442: trace sink for a PreToolUse hook that THREW — the deployment-facing half of the crash
|
|
638
|
+
* (`onError(phase:"hook")`, wired by the caller). The model-facing half is the fail-closed block reason;
|
|
639
|
+
* this one carries the exception object itself to whoever runs the deployment, because a crashing hook is
|
|
640
|
+
* a bug someone has to fix and the model-facing summary is bounded/sanitized. Never affects the outcome
|
|
641
|
+
* (a throwing sink is swallowed).
|
|
642
|
+
*/
|
|
154
643
|
onHookError?: (err: unknown) => void;
|
|
644
|
+
/**
|
|
645
|
+
* design/134 R5: true when this tool's `irreversibility` tier was installed by the COARSE
|
|
646
|
+
* `TaskSpec.shellGate` doctrine (design/80 D-E `shellGatedBash`/`shellGatedMonitor`), not an explicit
|
|
647
|
+
* per-tool mark — attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
|
|
648
|
+
*/
|
|
155
649
|
shellGated?: boolean;
|
|
650
|
+
/**
|
|
651
|
+
* design/143 批2 ([672]-A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
|
|
652
|
+
* small-model policy CLASSIFIER before any human/durable resolution:
|
|
653
|
+
* - verdict `allow` → the ask resolves to allow (`decisionReason:"classifier"`) — no suspend, no onAsk;
|
|
654
|
+
* - verdict `block` → deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`);
|
|
655
|
+
* - `unavailable`/`parse_error` → FAIL-CLOSED: the ask continues down the ORIGINAL chain
|
|
656
|
+
* (durable suspend / onAsk / headless auto-deny) exactly as if auto mode were absent
|
|
657
|
+
* (CC `automode-unavailable`/`automode-parsing-error` — "NOT a policy decision").
|
|
658
|
+
* SCOPE (declared semantic): this covers EVERY surviving ask, INCLUDING the deterministic egress/
|
|
659
|
+
* irreversibility safety tightens — auto mode is the deployment's explicit choice to let the
|
|
660
|
+
* classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
|
|
661
|
+
* action classes: outbound writes, irreversible operations, and sensitive-value handling). A
|
|
662
|
+
* deployment that wants human-only
|
|
663
|
+
* resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by
|
|
664
|
+
* failing — only an affirmative `<block>no</block>` allows.
|
|
665
|
+
*/
|
|
156
666
|
autoMode?: {
|
|
157
667
|
decider: import("./auto-mode.js").AutoModeDecider;
|
|
158
668
|
};
|
|
669
|
+
/**
|
|
670
|
+
* design/153 §2/§7.4 (件4 复审, MED): true means this call's ask is MARKED — an inherited ancestor
|
|
671
|
+
* constraint already determined "no synchronous layer may resolve this ask" (the ancestor's frozen
|
|
672
|
+
* approver was unreachable, OR an ancestor durable mandate was floated back because THIS task's own
|
|
673
|
+
* park facility can honor it). The marker's whole contract is that the ask reaches EITHER the durable
|
|
674
|
+
* park leg OR the fail-closed `resolveAskBound` intercept, with no synchronous decision-maker in
|
|
675
|
+
* between — the auto-mode classifier is exactly such a decision-maker (`decide()` can `allow`, executing
|
|
676
|
+
* the call) and sat strictly BEFORE both marker consumers, so a marked call under an armed classifier
|
|
677
|
+
* bypassed the marker's contract entirely. When true, the classifier step is skipped (falls through to
|
|
678
|
+
* `suspendAsk`/`resolveAsk` untouched) so a marked ask can only ever be resolved by an operator (durable
|
|
679
|
+
* park) or fail closed — never by the classifier.
|
|
680
|
+
*/
|
|
159
681
|
isMarkedUnresolvable?: (toolCallId: string) => boolean;
|
|
682
|
+
/**
|
|
683
|
+
* design/179 — the persisted allow-rule lane. Present only when a deployment wired a rule store; absent
|
|
684
|
+
* it, this whole step does not exist and the gate's decision path is byte-identical to what it was.
|
|
685
|
+
*
|
|
686
|
+
* A persisted rule is the standing form of ONE recorded human approval, so it resolves the asks a real
|
|
687
|
+
* person saying "allow" would have resolved — and nothing else. It sits post-fold, at the entry of the
|
|
688
|
+
* `ask` branch, BEFORE the classifier and AFTER every tighten: earlier and a tighten would simply put
|
|
689
|
+
* the ask back; later and the classifier would already have spoken.
|
|
690
|
+
*
|
|
691
|
+
* Its consumption predicate is its own, not the classifier's blanket flip:
|
|
692
|
+
* · `requiresRealApproval` asks are never consumed — the two integrity gates are the floor, and the
|
|
693
|
+
* same flag is what a v2 organization-level ask will carry;
|
|
694
|
+
* · a PreToolUse hook's ask is never consumed — that is the deployment's own adjudication, and a rule
|
|
695
|
+
* minted for a command does not overrule it;
|
|
696
|
+
* · the reserved question tool and a call MARKED unresolvable are excluded for the same reason the
|
|
697
|
+
* classifier excludes them: both contracts require that no synchronous decision-maker stands between
|
|
698
|
+
* the ask and the park / content route, and this lane is one.
|
|
699
|
+
* What it DOES consume is the egress and irreversibility/shellGate tightens and unmarked bare asks —
|
|
700
|
+
* deliberately, because a deployment forcing shell classification is exactly where the feature is for.
|
|
701
|
+
* Consuming such an ask also skips the park it would have minted; that IS what a standing approval
|
|
702
|
+
* means, and the first bullet is what keeps an integrity ask out of that set.
|
|
703
|
+
*/
|
|
160
704
|
persistedRules?: {
|
|
705
|
+
/** The canonical text of the rule that admits this call, or `undefined`. Must not throw: a store that
|
|
706
|
+
* cannot be read answers "no rule" and discloses on its own channel (fail-closed = more asks). */
|
|
161
707
|
admits: (req: ToolCallRequest) => Promise<string | undefined>;
|
|
708
|
+
/** Observation sink for an ask a rule resolved — the allow-side disclosure. Never affects the outcome. */
|
|
162
709
|
onResolved?: (info: {
|
|
163
710
|
toolName: string;
|
|
164
711
|
toolCallId: string;
|
|
@@ -166,5 +713,65 @@ export interface ToolGateInput {
|
|
|
166
713
|
}) => void;
|
|
167
714
|
};
|
|
168
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Fold a {@link Hooks.preToolUse} callback into a {@link ToolPolicy} layer, so a screening face a task
|
|
718
|
+
* installed for ITSELF can also travel down a delegation chain as one more inherited constraint.
|
|
719
|
+
*
|
|
720
|
+
* Why a policy and not a second hook channel: a delegated child already folds each ancestor's caller
|
|
721
|
+
* policy (`InheritedGate.parentConstraints`), and that fold is where `deny > ask > allow`, the rewrite
|
|
722
|
+
* cascade, the ancestor-frozen approver and the durable mandate all already live. Expressing the
|
|
723
|
+
* screening face in the same shape means the child gains no new decision channel — the existing fold
|
|
724
|
+
* consumes it — and the tighten-only property is structural rather than restated.
|
|
725
|
+
*
|
|
726
|
+
* The mapping mirrors the gate's own phase 1 (see {@link runToolGate}) decision for decision:
|
|
727
|
+
* - no opinion (`undefined`) → `allow`, contributing nothing;
|
|
728
|
+
* - `deny` → `deny` in the fold's own words (the fold short-circuits, and the gate's deny exit supplies
|
|
729
|
+
* the same `formatHookFeedback` envelope phase 1 would have);
|
|
730
|
+
* - `ask` → `ask`, stamped `decisionReason:"hook"` so every consumer that distinguishes a screening ask
|
|
731
|
+
* from a policy ask reads the same fact one level down — notably the persisted-rule lane, which must
|
|
732
|
+
* not let a standing allow-rule resolve a screening ask;
|
|
733
|
+
* - `allow`/`ask` with `updatedInput` → the same rewrite, which the fold cascades to later layers and
|
|
734
|
+
* re-checks against the narrowing layers;
|
|
735
|
+
* - a THROW → the fail-closed deny with the RB-442 attribution text (naming the hook and the
|
|
736
|
+
* non-execution), because a screening face that cannot answer must not pass the call.
|
|
737
|
+
*
|
|
738
|
+
* **The callback inherits the fold's idempotency requirement** ({@link combinePolicies}): a layer's
|
|
739
|
+
* `check` may run more than once for a single tool call — the fold scans past an `ask` looking for a
|
|
740
|
+
* later `deny`, a rewrite emitted by a LATER layer re-runs the prefix that never saw the final args, and
|
|
741
|
+
* an approved operator edit is re-adjudicated. A callback whose consultation has a side effect (an audit
|
|
742
|
+
* row, a counter) will therefore see that call more than once on those paths. Its own gate's phase 1 has
|
|
743
|
+
* always had the second of these (an approved edit is re-screened there too).
|
|
744
|
+
*
|
|
745
|
+
* Two properties are deliberately NOT carried, and neither is reconstructible in this shape:
|
|
746
|
+
* - `additionalContext` — a policy verdict has no channel for model-facing text. Context a screening
|
|
747
|
+
* face injects is a property of the run whose gate ran it; a descendant's transcript does not receive
|
|
748
|
+
* it. (The decision half — the part that restricts — is what travels.)
|
|
749
|
+
* - the phase-1 observer exclusion — a deny raised here is a fold deny, so the descendant's
|
|
750
|
+
* `permissionDenied` observer sees it with `source:"policy"`. That exclusion is about a hook deciding
|
|
751
|
+
* in ITS OWN gate; here the descendant's policy fold is what refused the call.
|
|
752
|
+
*
|
|
753
|
+
* `env` is the capability face of the environment the callback was installed against — captured at the
|
|
754
|
+
* install site, not rebuilt in the descendant, for the same reason an inherited policy closure keeps its
|
|
755
|
+
* own captured environment (the descendant may run against a different root). `onCrash` is the install
|
|
756
|
+
* site's deployment error lane: the gate's phase 1 routes a crashing callback to `onError(phase:"hook")`
|
|
757
|
+
* so a broken screening face is distinguishable from a deliberate refusal, and the folded form has to
|
|
758
|
+
* reach the same lane from inside a descendant's fold.
|
|
759
|
+
*/
|
|
169
760
|
export declare function createPreToolUseConstraintPolicy(preToolUse: NonNullable<Hooks["preToolUse"]>, env?: HookEnvCapabilities, onCrash?: (err: unknown) => void): ToolPolicy;
|
|
761
|
+
/**
|
|
762
|
+
* The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
|
|
763
|
+
* structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
|
|
764
|
+
*
|
|
765
|
+
* 1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput`
|
|
766
|
+
* and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook
|
|
767
|
+
* `ask` is remembered (it does not short-circuit — a later policy `deny` outranks it).
|
|
768
|
+
* 2. **adjudicate** — run the tool policy on the FINAL `currentInput` (never the model's stale args),
|
|
769
|
+
* fold it with any remembered hook-ask via `deny > ask > allow`, and resolve a surviving `ask`
|
|
770
|
+
* through `onAsk`. The policy ALWAYS runs regardless of the hook's verdict. A policy `allow` may
|
|
771
|
+
* itself carry an `updatedInput` rewrite (redact/clamp), applied last over any hook rewrite.
|
|
772
|
+
*
|
|
773
|
+
* Because both phases run in this one linear function, the ordering — and the "policy is final"
|
|
774
|
+
* invariant — is enforced by the call stack, not by registration convention. Returns the gate result
|
|
775
|
+
* for the harness `tool_call` hook plus the PreToolUse context to attach to the tool result.
|
|
776
|
+
*/
|
|
170
777
|
export declare function runToolGate(input: ToolGateInput): Promise<ToolGateResult>;
|