@sema-agent/core 7.40.1 → 7.41.0
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 +188 -0
- package/dist/agents/agent-transcript-tool.d.ts +10 -2
- package/dist/agents/agent-transcript-tool.js +52 -26
- package/dist/agents/list-agents-tool.d.ts +17 -2
- package/dist/agents/list-agents-tool.js +28 -16
- package/dist/agents/peer-session-drain.d.ts +3 -1
- package/dist/agents/peer-session-drain.js +1 -0
- package/dist/agents/subagent-steps.d.ts +12 -2
- package/dist/agents/subagent-steps.js +8 -4
- package/dist/agents/subagent.d.ts +1 -17
- package/dist/agents/subagent.js +12 -24
- package/dist/core/agent-definition.d.ts +19 -40
- package/dist/core/agent-state.d.ts +91 -0
- package/dist/core/agent-state.js +79 -0
- package/dist/core/ask-origin.d.ts +28 -6
- package/dist/core/ask-origin.js +9 -5
- package/dist/core/ask-unresolvable-notice.d.ts +27 -4
- package/dist/core/ask-unresolvable-notice.js +8 -0
- package/dist/core/auto-promote.d.ts +14 -27
- package/dist/core/auto-promote.js +5 -5
- package/dist/core/checkpoint-store.d.ts +11 -12
- package/dist/core/decision-receipt.d.ts +5 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/engine-notice.d.ts +7 -1
- package/dist/core/gate-crash-trace.d.ts +1 -1
- package/dist/core/gate-fold.js +7 -3
- package/dist/core/gate-lanes.js +22 -3
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +20 -6
- package/dist/core/hooks.js +28 -28
- package/dist/core/mcp-tool-meta.d.ts +24 -0
- package/dist/core/mcp-tool-meta.js +17 -0
- package/dist/core/mcp.d.ts +1 -6
- package/dist/core/mcp.js +6 -7
- package/dist/core/mode-policy.d.ts +25 -0
- package/dist/core/mode-policy.js +8 -0
- package/dist/core/protocol-table.d.ts +3 -0
- package/dist/core/protocol-table.js +3 -0
- package/dist/core/reminder-mint.d.ts +21 -1
- package/dist/core/reminder-mint.js +20 -0
- package/dist/core/retired-keys.d.ts +2 -1
- package/dist/core/retired-keys.js +13 -2
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/call-hook-stations.d.ts +106 -0
- package/dist/core/runner/call-hook-stations.js +118 -0
- package/dist/core/runner/contracts.d.ts +35 -25
- package/dist/core/runner/gate-exit.d.ts +5 -1
- package/dist/core/runner/gate-exit.js +10 -6
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
- package/dist/core/runner/prepare-delegation-surface.js +4 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
- package/dist/core/runner/prepare-gate-stations.js +13 -70
- package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
- package/dist/core/runner/prepare-policy-chain.js +14 -25
- package/dist/core/runner/prepare-project-context.d.ts +15 -16
- package/dist/core/runner/prepare-project-context.js +15 -11
- package/dist/core/runner/prepare-run-refs.d.ts +8 -8
- package/dist/core/runner/prepare-run-refs.js +3 -3
- package/dist/core/runner/prepare-safety-scan.js +3 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
- package/dist/core/runner/prepare-suspend-saga.js +2 -1
- package/dist/core/runner/prepare-task.js +14 -12
- package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
- package/dist/core/runner/prepare-turn-wiring.js +16 -13
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-apply.js +12 -23
- package/dist/core/runner/resume-checkpoint-screen.js +3 -0
- package/dist/core/runner/run-leg.js +61 -47
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/session-rule-policy.d.ts +7 -19
- package/dist/core/runner/session-rule-policy.js +1 -2
- package/dist/core/runner/skill-base-dir.d.ts +10 -0
- package/dist/core/runner/skill-base-dir.js +4 -0
- package/dist/core/runner/synthetic-tools.d.ts +11 -17
- package/dist/core/runner/synthetic-tools.js +7 -8
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +2 -0
- package/dist/core/session-policy-store.d.ts +63 -11
- package/dist/core/session-policy-store.js +52 -6
- package/dist/core/skill-grant.d.ts +239 -0
- package/dist/core/skill-grant.js +313 -0
- package/dist/core/skills-directory.d.ts +5 -34
- package/dist/core/skills-directory.js +9 -58
- package/dist/core/task-registry-agent.d.ts +14 -2
- package/dist/core/task-registry-agent.js +23 -8
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.d.ts +16 -0
- package/dist/core/task-registry.js +19 -6
- package/dist/core/task-spec.d.ts +10 -0
- package/dist/core/tool-face.d.ts +3 -2
- package/dist/core/tool-name-aliases.d.ts +38 -12
- package/dist/core/tool-name-aliases.js +16 -0
- package/dist/core/tool-name-readings.d.ts +6 -2
- package/dist/core/tool-name-readings.js +6 -4
- package/dist/core/tool-policy.d.ts +2 -2
- package/dist/core/tool-registry.d.ts +1 -4
- package/dist/core/tool-registry.js +0 -3
- package/dist/core/tool-spec.d.ts +11 -13
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/harness/agent-harness.d.ts +12 -1
- package/dist/engine/harness/agent-harness.js +17 -13
- package/dist/engine/loop/agent-loop.d.ts +16 -1
- package/dist/engine/loop/agent-loop.js +9 -15
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/stores/file/adoption/adopt.js +23 -16
- package/dist/stores/file/index.d.ts +8 -2
- package/dist/stores/file/index.js +2 -2
- package/dist/stores/file/mailbox-store.js +2 -2
- package/dist/stores/file/permission-rule-adopt.js +10 -1
- package/dist/stores/file/permission-rule-store.d.ts +6 -4
- package/dist/stores/file/permission-rule-store.js +127 -59
- package/dist/stores/file/session-policy-store.d.ts +86 -14
- package/dist/stores/file/session-policy-store.js +135 -118
- package/dist/stores/file/task-list-store.d.ts +5 -0
- package/dist/stores/file/task-list-store.js +18 -4
- package/dist/stores/file/usage-window-store.d.ts +9 -1
- package/dist/stores/file/usage-window-store.js +47 -33
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +45 -5
- package/wire-changes/core@7.41.0.json +1865 -0
- package/dist/core/runner/active-skill-scope.d.ts +0 -58
- package/dist/core/runner/active-skill-scope.js +0 -133
- package/dist/core/skill-tool-specifier.d.ts +0 -80
- package/dist/core/skill-tool-specifier.js +0 -57
package/dist/core/gate-lanes.js
CHANGED
|
@@ -7,13 +7,14 @@ import { inlineUntrusted } from "./untrusted-text.js";
|
|
|
7
7
|
import { isRuleBehavior } from "./permission-rule-model.js";
|
|
8
8
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
9
9
|
import { applyPersistedTightening, disclosedRuleSet } from "./persisted-rule-arms.js";
|
|
10
|
+
import { skillAllowDecision, skillRowRefusal } from "./skill-grant.js";
|
|
10
11
|
import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdict } from "./read-only-shell.js";
|
|
11
12
|
import { readShellCommand } from "./shell-lexer.js";
|
|
12
13
|
import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
|
|
13
14
|
import { catalogRuleFaceOf } from "./tool-registry.js";
|
|
14
15
|
import { callFaceSeat } from "./tool-roster.js";
|
|
15
16
|
import { heldBy, wireOf } from "./tool-name-readings.js";
|
|
16
|
-
import { asksUnstated, modeAcceptsFileCommand, unstatedAskMessage } from "./mode-policy.js";
|
|
17
|
+
import { asksUnstated, modeAcceptsFileCommand, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
|
|
17
18
|
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
18
19
|
import { exitGate } from "./runner/gate-exit.js";
|
|
19
20
|
import { traceHookCrash } from "./gate-crash-trace.js";
|
|
@@ -167,6 +168,7 @@ export async function runGateLanes(pass) {
|
|
|
167
168
|
egress: input.egress,
|
|
168
169
|
shellGated: input.shellGated,
|
|
169
170
|
irreversibility: input.irreversibility,
|
|
171
|
+
requiresUserInteraction: input.requiresUserInteraction,
|
|
170
172
|
probeUnanswered: pass.probeUnanswered === true,
|
|
171
173
|
writeProtected: pass.writeProtected === true,
|
|
172
174
|
writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
|
|
@@ -279,6 +281,12 @@ export async function runGateLanes(pass) {
|
|
|
279
281
|
pass.deniedBy = "persisted_rule";
|
|
280
282
|
if (tightened.tightened === "deny")
|
|
281
283
|
pass.denyCause = undefined;
|
|
284
|
+
const skillRefusal = pass.decision.action === "deny" ? undefined : skillRowRefusal(input.skillGrants, laneReq);
|
|
285
|
+
if (skillRefusal !== undefined) {
|
|
286
|
+
pass.decision = skillRefusal;
|
|
287
|
+
pass.deniedBy = "policy";
|
|
288
|
+
pass.denyCause = undefined;
|
|
289
|
+
}
|
|
282
290
|
}
|
|
283
291
|
if (pass.decision.action === "ask") {
|
|
284
292
|
pass.decision = { ...pass.decision };
|
|
@@ -350,6 +358,11 @@ export async function runGateLanes(pass) {
|
|
|
350
358
|
};
|
|
351
359
|
}
|
|
352
360
|
}
|
|
361
|
+
const skillHit = allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() ? input.skillGrants?.allowing(pass.req, input.permissionMode?.()) : undefined;
|
|
362
|
+
if (skillHit !== undefined) {
|
|
363
|
+
pass.decision = skillAllowDecision(skillHit, pass.policyRewrite);
|
|
364
|
+
await notifier.notifyAsync(() => input.skillGrants?.onAllowed?.({ toolName: pass.req.toolName, toolCallId, ...skillHit }), "toolGate.skillGrantResolved");
|
|
365
|
+
}
|
|
353
366
|
const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
|
|
354
367
|
if (pass.decision.action === "ask") {
|
|
355
368
|
const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
@@ -703,14 +716,20 @@ export async function runGateLanes(pass) {
|
|
|
703
716
|
break;
|
|
704
717
|
}
|
|
705
718
|
}
|
|
719
|
+
const editSkillRefusal = skillRowRefusal(input.skillGrants, { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
|
|
720
|
+
if (editSkillRefusal !== undefined) {
|
|
721
|
+
editDenied = editSkillRefusal;
|
|
722
|
+
pass.deniedBy = "policy";
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
706
725
|
if (recheck.action === "allow") {
|
|
707
726
|
if (recheck.updatedInput !== undefined) {
|
|
708
727
|
editArgs = recheck.updatedInput;
|
|
709
728
|
editRewrittenSinceHuman = true;
|
|
710
729
|
}
|
|
711
|
-
if (!editRewrittenSinceHuman || !asksUnstated(recheck, input
|
|
730
|
+
if (!editRewrittenSinceHuman || !asksUnstated(recheck, permissionSeatAt(input, editArgs)?.seat, input.permissionMode?.(), editHookAllowed && recheck.updatedInput === undefined))
|
|
712
731
|
break;
|
|
713
|
-
recheck = { action: "ask", message: unstatedAskMessage(toolName) };
|
|
732
|
+
recheck = { action: "ask", message: permissionSeatAt(input, editArgs)?.question ?? unstatedAskMessage(toolName) };
|
|
714
733
|
pass.tightenedBy = "unstated";
|
|
715
734
|
}
|
|
716
735
|
if (recheck.updatedInput !== undefined) {
|
|
@@ -136,7 +136,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
136
136
|
* the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
|
|
137
137
|
* so the catalog is not written a second time here and the two cannot drift.
|
|
138
138
|
*/
|
|
139
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
|
|
139
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "skills.allowed_tool_not_granted", "agents.listing_truncated", "tool_result.offload_put_failed", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
|
|
140
140
|
/** A catalogued code (see {@link ENGINE_NOTICE_CODES}); {@link NOTICE_MINTER} says who mints it. NOT the type of
|
|
141
141
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
142
142
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -233,6 +233,7 @@ declare const NOTICE_AUDIENCE_TABLE: {
|
|
|
233
233
|
readonly "route.tool_call_id_folded": "operator";
|
|
234
234
|
readonly "context.tool_turn_repaired": "operator";
|
|
235
235
|
readonly "skills.listing_truncated": "operator";
|
|
236
|
+
readonly "skills.allowed_tool_not_granted": "operator";
|
|
236
237
|
readonly "agents.listing_truncated": "operator";
|
|
237
238
|
readonly "tool_result.offload_put_failed": "operator";
|
|
238
239
|
readonly "config.forward_sink_failed": "operator";
|
|
@@ -343,6 +344,7 @@ declare const NOTICE_MINTER_TABLE: {
|
|
|
343
344
|
readonly "memory.capture_opted_out": "engine";
|
|
344
345
|
readonly "memory.capture_optout_unpersisted": "engine";
|
|
345
346
|
readonly "skills.listing_truncated": "engine";
|
|
347
|
+
readonly "skills.allowed_tool_not_granted": "engine";
|
|
346
348
|
readonly "agents.listing_truncated": "engine";
|
|
347
349
|
readonly "tool_result.offload_put_failed": "engine";
|
|
348
350
|
readonly "instructions.source_changed": "server";
|
|
@@ -193,6 +193,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
|
|
|
193
193
|
"memory.capture_opted_out",
|
|
194
194
|
"memory.capture_optout_unpersisted",
|
|
195
195
|
"skills.listing_truncated",
|
|
196
|
+
"skills.allowed_tool_not_granted",
|
|
196
197
|
"agents.listing_truncated",
|
|
197
198
|
"tool_result.offload_put_failed",
|
|
198
199
|
"instructions.source_changed",
|
|
@@ -279,6 +280,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
279
280
|
"route.tool_call_id_folded": "operator",
|
|
280
281
|
"context.tool_turn_repaired": "operator",
|
|
281
282
|
"skills.listing_truncated": "operator",
|
|
283
|
+
"skills.allowed_tool_not_granted": "operator",
|
|
282
284
|
"agents.listing_truncated": "operator",
|
|
283
285
|
"tool_result.offload_put_failed": "operator",
|
|
284
286
|
"config.forward_sink_failed": "operator",
|
|
@@ -371,6 +373,7 @@ const NOTICE_MINTER_TABLE = {
|
|
|
371
373
|
"memory.capture_opted_out": "engine",
|
|
372
374
|
"memory.capture_optout_unpersisted": "engine",
|
|
373
375
|
"skills.listing_truncated": "engine",
|
|
376
|
+
"skills.allowed_tool_not_granted": "engine",
|
|
374
377
|
"agents.listing_truncated": "engine",
|
|
375
378
|
"tool_result.offload_put_failed": "engine",
|
|
376
379
|
"instructions.source_changed": "server",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from
|
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import type { GateOutcome } from "./gate-outcome.js";
|
|
5
|
+
import { type PreToolUseScreening } from "./runner/gate-exit.js";
|
|
5
6
|
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
|
|
6
7
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
8
|
import { type AskClass } from "./ask-class.js";
|
|
@@ -9,6 +10,9 @@ import { type PermissionMode } from "./ask-origin.js";
|
|
|
9
10
|
import type { AutoModeDenialTracker } from "./auto-mode.js";
|
|
10
11
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
11
12
|
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
13
|
+
import { type SafeNotifier } from "./safe-notify.js";
|
|
14
|
+
import type { PermissionSeatInputs } from "./mode-policy.js";
|
|
15
|
+
import type { SkillGateSeats } from "./skill-grant.js";
|
|
12
16
|
/**
|
|
13
17
|
* In-process hook seam (design/37) — a provider-agnostic interception layer modeled on CC's hooks,
|
|
14
18
|
* kept process-internal (no shell/HTTP executors, no settings files). The original tool-call trio:
|
|
@@ -1128,7 +1132,7 @@ export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | Per
|
|
|
1128
1132
|
* signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
|
|
1129
1133
|
* additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
|
|
1130
1134
|
* interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
|
|
1131
|
-
export interface ToolGateInput {
|
|
1135
|
+
export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats {
|
|
1132
1136
|
/** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
|
|
1133
1137
|
* isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
|
|
1134
1138
|
* wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
|
|
@@ -1462,9 +1466,10 @@ export interface ToolGateInput {
|
|
|
1462
1466
|
/** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
|
|
1463
1467
|
* absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
|
|
1464
1468
|
toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
|
|
1465
|
-
/** The called tool's
|
|
1466
|
-
*
|
|
1467
|
-
|
|
1469
|
+
/** The called tool's seat carries its server's declaration that a call must be put to a person
|
|
1470
|
+
* ({@link import("../engine/loop/types.js").AgentTool.mcpRequiresUserInteraction}): a surviving `allow` is tightened to an `ask`, and every ask on the call carries
|
|
1471
|
+
* the class `requires_user_interaction` and the real-approval bit (no rule, grant, classifier, bypass mode, blanket seat or policy-settled approver answers it). Absent ⇒ not declared. */
|
|
1472
|
+
requiresUserInteraction?: true;
|
|
1468
1473
|
/** The deployment's A2A peer-isolation switch: a set switch makes the A2A egress default an operator mandate. */
|
|
1469
1474
|
a2aIsolatePeers?: boolean;
|
|
1470
1475
|
/** Observe a mode RELEASE (fired before the released allow leaves the arbitration point; `readUnestablished`: the read station could not establish what the call reads). Observe-only. */
|
|
@@ -1499,8 +1504,10 @@ export interface ToolGateInput {
|
|
|
1499
1504
|
* this leg) and no durable park took the call, so the fail-closed deny STANDS. Fired once per such call
|
|
1500
1505
|
* from the gate's single deny exit, after every park re-route has had its attempt, with the facts the
|
|
1501
1506
|
* `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
|
|
1502
|
-
* `parkLaneExisted` = a park lane was armed for this run
|
|
1503
|
-
*
|
|
1507
|
+
* `parkLaneExisted` = a park lane was armed for this run; `remedy` = the caller-side change that would have let the
|
|
1508
|
+
* call run, present for the causes that say nobody could be asked (`approver_unavailable`, `no_approver_wired`).
|
|
1509
|
+
* Observe-only: a throwing seat never alters the deny. Absent ⇒ the deny stands silently (a host driving the gate
|
|
1510
|
+
* directly, no notice sink).
|
|
1504
1511
|
*/
|
|
1505
1512
|
onAskUnresolvable?: (info: {
|
|
1506
1513
|
toolName: string;
|
|
@@ -1784,6 +1791,13 @@ export declare function askCarryRowMembers(carry: AskCarry | undefined): {
|
|
|
1784
1791
|
askClass?: import("./ask-origin.js").AskClass;
|
|
1785
1792
|
classes?: readonly import("./ask-origin.js").AskClass[];
|
|
1786
1793
|
};
|
|
1794
|
+
/**
|
|
1795
|
+
* The PreToolUse screening seat ({@link PreToolUseScreening}) for ONE call — seat runner, seat bound, per-screening
|
|
1796
|
+
* context and the fail-closed / deny sentences, built in ONE place for every site that screens a call: the gate's
|
|
1797
|
+
* phase 1 and approved-edit re-screen (through {@link runToolGate}'s pass) and the resumed leg's re-screen of an
|
|
1798
|
+
* approved call. Undefined when no face is wired. `hookSeatMs` is the caller's validated bound.
|
|
1799
|
+
*/
|
|
1800
|
+
export declare function createPreToolUseScreening(preToolUse: Hooks["preToolUse"] | undefined, input: Pick<ToolGateInput, "event" | "trackedCwd" | "hookEnv" | "identity" | "onHookError" | "abortSignal">, hookSeatMs: number, notifier: SafeNotifier): PreToolUseScreening | undefined;
|
|
1787
1801
|
/**
|
|
1788
1802
|
* The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
|
|
1789
1803
|
* structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
|
package/dist/core/hooks.js
CHANGED
|
@@ -247,11 +247,11 @@ export function askCarryRowMembers(carry) {
|
|
|
247
247
|
...(classed !== undefined ? classed : {}),
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
|
-
export
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const screenedName = wireOf("gate.preToolUse", event);
|
|
250
|
+
export function createPreToolUseScreening(preToolUse, input, hookSeatMs, notifier) {
|
|
251
|
+
if (preToolUse === undefined)
|
|
252
|
+
return undefined;
|
|
253
|
+
const { toolCallId, toolName } = input.event;
|
|
254
|
+
const screenedName = wireOf("gate.preToolUse", input.event);
|
|
255
255
|
const hookCtx = (seatSignal) => ({
|
|
256
256
|
toolCallId,
|
|
257
257
|
toolName: screenedName,
|
|
@@ -263,38 +263,38 @@ export async function runToolGate(input) {
|
|
|
263
263
|
...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
|
|
264
264
|
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
265
265
|
});
|
|
266
|
+
const bound = { timeoutMs: hookSeatMs, ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), abortEnds: true };
|
|
267
|
+
return {
|
|
268
|
+
screen: (args) => runHookSeat("preToolUse", bound, (sig) => preToolUse(screenedName, args, hookCtx(sig))),
|
|
269
|
+
screened: screenPreToolUseResult,
|
|
270
|
+
crashReason: preToolUseCrashReason,
|
|
271
|
+
unansweredReason: async (subject, cause) => {
|
|
272
|
+
if (cause === "timeout")
|
|
273
|
+
traceHookCrash(input, hookSeatExpiredError("preToolUse", hookSeatMs, cause, `${subject} was NOT executed (fail-closed) and the screening face was signalled to stop`), notifier);
|
|
274
|
+
return preToolUseUnansweredReason(subject, hookSeatMs, cause);
|
|
275
|
+
},
|
|
276
|
+
deniedReason: (answer) => decisionText(answer) ?? `tool "${toolName}" blocked by a PreToolUse hook`,
|
|
277
|
+
refusedReason: (answer) => String(answer.message ?? `the PreToolUse hook's result for "${toolName}" was refused by the engine`),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export async function runToolGate(input) {
|
|
281
|
+
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
282
|
+
const { toolCallId, toolName } = event;
|
|
283
|
+
const callSignal = input.callSignal;
|
|
266
284
|
const modelInput = event.input;
|
|
267
285
|
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
|
|
268
286
|
const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
|
|
269
|
-
const
|
|
270
|
-
timeoutMs: hookSeatMs,
|
|
271
|
-
...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}),
|
|
272
|
-
...(abortEnds ? { abortEnds: true } : {}),
|
|
273
|
-
});
|
|
274
|
-
const preToolUseUnansweredBlock = async (subject, cause) => {
|
|
275
|
-
const reason = preToolUseUnansweredReason(subject, hookSeatMs, cause);
|
|
276
|
-
if (cause === "timeout") {
|
|
277
|
-
traceHookCrash(input, hookSeatExpiredError("preToolUse", hookSeatMs, cause, `${subject} was NOT executed (fail-closed) and the screening face was signalled to stop`), notifier);
|
|
278
|
-
}
|
|
279
|
-
return reason;
|
|
280
|
-
};
|
|
287
|
+
const observerBound = { timeoutMs: hookSeatMs, ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}) };
|
|
281
288
|
const notifyPermissionDeniedSeat = async (payload) => {
|
|
282
289
|
if (input.permissionDenied === undefined)
|
|
283
290
|
return;
|
|
284
291
|
await notifier.notifyAsync(async () => {
|
|
285
|
-
const seat = await runHookSeat("permissionDenied",
|
|
292
|
+
const seat = await runHookSeat("permissionDenied", observerBound, (sig) => input.permissionDenied?.({ ...payload, signal: sig }));
|
|
286
293
|
if (seat.expired)
|
|
287
294
|
throw hookSeatExpiredError("permissionDenied", hookSeatMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged");
|
|
288
295
|
}, "toolGate.permissionDenied");
|
|
289
296
|
};
|
|
290
|
-
const screening = preToolUse
|
|
291
|
-
? {
|
|
292
|
-
screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(screenedName, args, hookCtx(sig))),
|
|
293
|
-
screened: screenPreToolUseResult,
|
|
294
|
-
crashReason: preToolUseCrashReason,
|
|
295
|
-
unansweredReason: preToolUseUnansweredBlock,
|
|
296
|
-
}
|
|
297
|
-
: undefined;
|
|
297
|
+
const screening = createPreToolUseScreening(preToolUse, input, hookSeatMs, notifier);
|
|
298
298
|
const pass = {
|
|
299
299
|
input,
|
|
300
300
|
event,
|
|
@@ -341,13 +341,13 @@ export async function runToolGate(input) {
|
|
|
341
341
|
}
|
|
342
342
|
if (r) {
|
|
343
343
|
if (isEngineRefusal(r)) {
|
|
344
|
-
return await engineFailClosedExit(pass,
|
|
344
|
+
return await engineFailClosedExit(pass, screening.refusedReason(r));
|
|
345
345
|
}
|
|
346
346
|
if (r.additionalContext) {
|
|
347
347
|
pass.preToolContext.push(r.additionalContext);
|
|
348
348
|
}
|
|
349
349
|
if (r.action === "deny") {
|
|
350
|
-
return hookDenyExit(pass, r);
|
|
350
|
+
return hookDenyExit(pass, screening.deniedReason(r));
|
|
351
351
|
}
|
|
352
352
|
if (r.updatedInput !== undefined) {
|
|
353
353
|
pass.currentInput = r.updatedInput;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-tool `_meta` declarations a server makes in its tools/list entry that the engine reads at intake: a result-size
|
|
3
|
+
* threshold (`anthropic/maxResultSizeChars`) and the requirement that a call be put to a person
|
|
4
|
+
* (`anthropic/requiresUserInteraction`). Pure readers over the entry's `_meta` — the intake (`mcp.ts`) stamps what they answer
|
|
5
|
+
* on the mounted tool.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* F4-② (CC :320015/:320061): resolve a server's self-declared per-tool result-size threshold from its
|
|
9
|
+
* `_meta`. Non-numeric/non-finite/non-positive → undefined (declaration ignored); valid → capped at
|
|
10
|
+
* {@link MCP_META_RESULT_SIZE_CAP}. Exported for tests.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* A server's own per-tool declaration that a call must be put to a person, read from its `_meta` by the reference
|
|
15
|
+
* implementation's rule: the boolean `true` declares it, nothing else does. `declared` is the seat fact the intake stamps on
|
|
16
|
+
* the mounted tool. A PRESENT value that is not a boolean (the string "true", the number 1, an object, null) is a declaration
|
|
17
|
+
* the server wrote and this reader cannot honour: `advisory` is the sentence for the operator warning lane saying so (the tool
|
|
18
|
+
* stays mounted and reads as undeclared), instead of letting a typo quietly read as "no interaction required". `false` and
|
|
19
|
+
* absence are well-formed and say nothing. Exported for tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveMcpRequiresUserInteraction(meta: unknown): {
|
|
22
|
+
declared: boolean;
|
|
23
|
+
advisory?: string;
|
|
24
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const MCP_META_RESULT_SIZE_CAP = 500_000;
|
|
2
|
+
export function resolveMcpDeclaredResultSize(meta) {
|
|
3
|
+
const declared = meta?.["anthropic/maxResultSizeChars"];
|
|
4
|
+
if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0)
|
|
5
|
+
return undefined;
|
|
6
|
+
return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
|
|
7
|
+
}
|
|
8
|
+
const MCP_REQUIRES_USER_INTERACTION_META_KEY = "anthropic/requiresUserInteraction";
|
|
9
|
+
export function resolveMcpRequiresUserInteraction(meta) {
|
|
10
|
+
const value = meta?.[MCP_REQUIRES_USER_INTERACTION_META_KEY];
|
|
11
|
+
if (value === true)
|
|
12
|
+
return { declared: true };
|
|
13
|
+
if (value === undefined || value === false)
|
|
14
|
+
return { declared: false };
|
|
15
|
+
const kind = value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
16
|
+
return { declared: false, advisory: `its _meta["${MCP_REQUIRES_USER_INTERACTION_META_KEY}"] is ${kind}, not the boolean true — the declaration is ignored, so this tool is judged like a server tool that did not declare it` };
|
|
17
|
+
}
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ImageContent, TextContent } from "../internal/llm.js";
|
|
|
5
5
|
import { type McpImageResizer } from "./image-downsample.js";
|
|
6
6
|
import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
|
|
7
7
|
import { type ReminderDisclosureCounts } from "./reminder-disclosure.js";
|
|
8
|
+
export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
|
|
8
9
|
/**
|
|
9
10
|
* The safety axes (design/77 §4 irreversibility, design/70 egress) derived from one materialized MCP
|
|
10
11
|
* tool's server-advertised `annotations`. These ride alongside the {@link AgentTool} (which is vendored
|
|
@@ -376,12 +377,6 @@ export interface McpServerStatus {
|
|
|
376
377
|
* silently re-narrowed by a cast.
|
|
377
378
|
*/
|
|
378
379
|
export declare const MCP_PREFIX: "mcp__";
|
|
379
|
-
/**
|
|
380
|
-
* F4-② (CC :320015/:320061): resolve a server's self-declared per-tool result-size threshold from its
|
|
381
|
-
* `_meta`. Non-numeric/non-finite/non-positive → undefined (declaration ignored); valid → capped at
|
|
382
|
-
* {@link MCP_META_RESULT_SIZE_CAP}. Exported for tests.
|
|
383
|
-
*/
|
|
384
|
-
export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
|
|
385
380
|
/**
|
|
386
381
|
* Apply the token gate to a mapped MCP content array (CC `$Mp`/`BMp` shape): text blocks consume a
|
|
387
382
|
* cumulative char budget of `tokens×4`; past the budget they are sliced/dropped head-keep, and the CC
|
package/dist/core/mcp.js
CHANGED
|
@@ -17,6 +17,8 @@ import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
|
|
|
17
17
|
import { truncateError } from "./tool-errors.js";
|
|
18
18
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
19
19
|
import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disclosure.js";
|
|
20
|
+
import { resolveMcpDeclaredResultSize, resolveMcpRequiresUserInteraction } from "./mcp-tool-meta.js";
|
|
21
|
+
export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
|
|
20
22
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
21
23
|
import { snapshotMcpEntries } from "./locked-config.js";
|
|
22
24
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
@@ -25,13 +27,6 @@ import { readMember, thrownIs, thrownName, thrownText } from "./thrown-value.js"
|
|
|
25
27
|
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
26
28
|
const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
|
|
27
29
|
const MCP_CHARS_PER_TOKEN = 4;
|
|
28
|
-
const MCP_META_RESULT_SIZE_CAP = 500_000;
|
|
29
|
-
export function resolveMcpDeclaredResultSize(meta) {
|
|
30
|
-
const declared = meta?.["anthropic/maxResultSizeChars"];
|
|
31
|
-
if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0)
|
|
32
|
-
return undefined;
|
|
33
|
-
return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
|
|
34
|
-
}
|
|
35
30
|
function mcpMaxOutputTokens() {
|
|
36
31
|
return resolveEnvBudget(process.env.MAX_MCP_OUTPUT_TOKENS, { name: "MAX_MCP_OUTPUT_TOKENS", unit: "tokens", default: MCP_OUTPUT_TOKENS_DEFAULT });
|
|
37
32
|
}
|
|
@@ -1663,6 +1658,9 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1663
1658
|
const writeEffect = (axis?.effect ?? "write") === "write";
|
|
1664
1659
|
const mcpToolMeta = t._meta;
|
|
1665
1660
|
const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(mcpToolMeta);
|
|
1661
|
+
const interaction = resolveMcpRequiresUserInteraction(mcpToolMeta);
|
|
1662
|
+
if (interaction.advisory !== undefined)
|
|
1663
|
+
advisories.push({ tool: inlineUntrusted(t.name), reason: interaction.advisory });
|
|
1666
1664
|
const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
|
|
1667
1665
|
const mounted = {
|
|
1668
1666
|
name: namespacedName,
|
|
@@ -1671,6 +1669,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1671
1669
|
parameters: (effectiveInputSchema ?? { type: "object" }),
|
|
1672
1670
|
...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
|
|
1673
1671
|
...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
|
|
1672
|
+
...(interaction.declared ? { mcpRequiresUserInteraction: true } : {}),
|
|
1674
1673
|
mcpRemoteName: remoteName, ...(t.annotations?.readOnlyHint === true ? { mcpReadOnlyHint: true } : {}), permissionSeat: "passthrough",
|
|
1675
1674
|
execute: async (_toolCallId, params, signal) => {
|
|
1676
1675
|
const what = `The call to tool ${inlineUntrusted(remoteName)}`;
|
|
@@ -83,10 +83,35 @@ export declare function mountedToolDeclaresReadOnly(tools: readonly AgentTool[],
|
|
|
83
83
|
export declare function asksUnstated(d: PermissionResult, seat: "passthrough" | undefined, mode: PermissionMode | undefined, hookStated: boolean): boolean;
|
|
84
84
|
/** The unstated question's sentence (the reference implementation's passthrough sentence, without its product name). */
|
|
85
85
|
export declare const unstatedAskMessage: (toolName: string) => string;
|
|
86
|
+
/** One call's permission seat: `passthrough` (an allow nobody stated meets the mode's unstated answer) and the question
|
|
87
|
+
* put when it does — absent ⇒ {@link unstatedAskMessage}. */
|
|
88
|
+
export interface PermissionSeatReading {
|
|
89
|
+
readonly seat: "passthrough";
|
|
90
|
+
readonly question?: string;
|
|
91
|
+
}
|
|
92
|
+
/** The gate's permission-seat inputs (a member group of `ToolGateInput`), read through {@link permissionSeatAt}. */
|
|
93
|
+
export interface PermissionSeatInputs {
|
|
94
|
+
/** The called tool's permission seat ({@link import("./tool-spec.js").ToolSpec.permissionSeat}): `"passthrough"` ⇒ an allow no layer stated
|
|
95
|
+
* meets the mode's answer for an unstated call ({@link ModePolicy.unstated}). Absent ⇒ the tool answers for itself. */
|
|
96
|
+
permissionSeat?: "passthrough";
|
|
97
|
+
/** The seat read off THIS call's arguments, for a tool whose seat depends on them (the Runner wires it for the `Skill`
|
|
98
|
+
* tool: a skill that declares a list asks `Execute skill: <name>`). Present ⇒ it is the reading. */
|
|
99
|
+
permissionSeatOf?: (args: unknown) => PermissionSeatReading | undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The ONE reading of a call's permission seat, taken on the arguments the gate is judging (the policy's rewrite and the
|
|
103
|
+
* hook's included): the per-call reader where the tool's seat depends on its arguments, else the mounted tool's static
|
|
104
|
+
* seat. Read at the fold's last station and at the approved-edit round, on that round's edited bytes.
|
|
105
|
+
*/
|
|
106
|
+
export declare function permissionSeatAt(input: PermissionSeatInputs, args: unknown): PermissionSeatReading | undefined;
|
|
86
107
|
/** The PERMISSION SEAT of the LAST mounted instance of this name (the one a call dispatches to) — `passthrough` when the
|
|
87
108
|
* instance states no permission of its own ({@link AgentTool.permissionSeat}), else `undefined` (the engine's stations and
|
|
88
109
|
* the deployment's declarations answer for it). Read per call off the leg's live mount array, so a refresh is seen. */
|
|
89
110
|
export declare function mountedToolPermissionSeat(tools: readonly AgentTool[], toolName: string): "passthrough" | undefined;
|
|
111
|
+
/** Does the LAST mounted instance of this name (the one a call dispatches to) carry its server's declaration that a call must be put to a
|
|
112
|
+
* person ({@link AgentTool.mcpRequiresUserInteraction})? Read per call off the leg's live mount array, so a refresh is seen. The gate's
|
|
113
|
+
* seat for the `requires_user_interaction` class. */
|
|
114
|
+
export declare function mountedToolRequiresUserInteraction(tools: readonly AgentTool[], toolName: string): boolean;
|
|
90
115
|
/**
|
|
91
116
|
* Is this call a WRITE the `plan` mode asks about? Two faces, one question — does the call write by its declaration:
|
|
92
117
|
* · a covered path write (the catalog's protective floor: a file-write tool, or a same-name tool wearing its slot), and
|
package/dist/core/mode-policy.js
CHANGED
|
@@ -46,9 +46,17 @@ export function asksUnstated(d, seat, mode, hookStated) {
|
|
|
46
46
|
return d.action === "allow" && d.decisionReason === undefined && !hookStated && seat === "passthrough" && mode !== undefined && modePolicyOf(mode).unstated === "ask";
|
|
47
47
|
}
|
|
48
48
|
export const unstatedAskMessage = (toolName) => `The agent requested permission to use ${toolName}, but it has not been granted yet.`;
|
|
49
|
+
export function permissionSeatAt(input, args) {
|
|
50
|
+
if (input.permissionSeatOf !== undefined)
|
|
51
|
+
return input.permissionSeatOf(args);
|
|
52
|
+
return input.permissionSeat === "passthrough" ? { seat: "passthrough" } : undefined;
|
|
53
|
+
}
|
|
49
54
|
export function mountedToolPermissionSeat(tools, toolName) {
|
|
50
55
|
return tools.findLast((t) => t.name === toolName)?.permissionSeat === "passthrough" ? "passthrough" : undefined;
|
|
51
56
|
}
|
|
57
|
+
export function mountedToolRequiresUserInteraction(tools, toolName) {
|
|
58
|
+
return tools.findLast((t) => t.name === toolName)?.mcpRequiresUserInteraction === true;
|
|
59
|
+
}
|
|
52
60
|
const ENGINE_ROSTER_SOURCES = new Set(["builtin", "synthetic"]);
|
|
53
61
|
export function planRefusesWrite(req, roster, readOnlyDeclared) {
|
|
54
62
|
if (isProtectedWrite(req))
|
|
@@ -80,6 +80,9 @@ export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
|
|
|
80
80
|
* table rather than written out: a message that hard-codes ONE protocol becomes wrong — while staying
|
|
81
81
|
* green — the moment a second one is appended, and both messages tell a deployment what to write. */
|
|
82
82
|
export declare const NAMESPACED_NAME_SHAPES: string;
|
|
83
|
+
/** Whether `name` carries the namespace separator — reserved for protocol tool names: a deployment tool's name or alias
|
|
84
|
+
* carrying it is refused, and a rule name carrying it with no protocol prefix names no tool. The one spelling of the test. */
|
|
85
|
+
export declare function carriesNamespaceSeparator(name: string): boolean;
|
|
83
86
|
/** The namespace owning `name`, or `undefined` for a caller/first-party tool. */
|
|
84
87
|
export declare function protocolOf(name: string): ProtocolNamespace | undefined;
|
|
85
88
|
export {};
|
|
@@ -38,6 +38,9 @@ export const MCP_NAMESPACE = makeProtocolNamespace("mcp", MCP_PREFIX_NAME);
|
|
|
38
38
|
export const A2A_NAMESPACE = makeProtocolNamespace("a2a", A2A_PREFIX_NAME);
|
|
39
39
|
export const PROTOCOL_TABLE = [MCP_NAMESPACE, A2A_NAMESPACE];
|
|
40
40
|
export const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
|
|
41
|
+
export function carriesNamespaceSeparator(name) {
|
|
42
|
+
return name.includes(NAME_SEP);
|
|
43
|
+
}
|
|
41
44
|
export function protocolOf(name) {
|
|
42
45
|
return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
|
|
43
46
|
}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - **declare** ({@link reminderMarkDeclaration}) — the system-prompt declaration copy that gives
|
|
20
20
|
* the mark its meaning to the model.
|
|
21
21
|
* plus the tag renderers ({@link openSystemReminder} / {@link mintSystemReminder}) mint sites call
|
|
22
|
-
* so no call site inlines the marking syntax
|
|
22
|
+
* so no call site inlines the marking syntax, and the block readers ({@link markedReminderBodiesOf} /
|
|
23
|
+
* {@link stripMarkedReminderBlocks}) for the same reason on the reading side.
|
|
23
24
|
*
|
|
24
25
|
* SCHEME EVOLUTION CONTRACT (ruled 2026-08-19): the marking scheme is REPLACEABLE. The current
|
|
25
26
|
* scheme is an open-tag attribute (`mark="<22-char base64url>"`), but nothing outside this module
|
|
@@ -116,3 +117,22 @@ export declare function engineRegionTextsOf(messages: readonly EngineRegionMessa
|
|
|
116
117
|
* that must judge what the engine actually SAID reads block bodies and decides per block.
|
|
117
118
|
*/
|
|
118
119
|
export declare function markedReminderBodiesOf(text: string, mark: string): string[];
|
|
120
|
+
/**
|
|
121
|
+
* STRIP port — the reading half of {@link openSystemReminder}: `text` without the blocks the engine marked.
|
|
122
|
+
*
|
|
123
|
+
* A marked block is an open tag whose mark attribute carries a mark of the current scheme, through the next close
|
|
124
|
+
* tag; the attribute's spacing and quote style are not part of that judgment (the engine mints one spelling, a model
|
|
125
|
+
* that relays a block rewrites it freely, and the mark is what must not travel). Everything in it
|
|
126
|
+
* goes, and so do the blank lines that stood between it and the text after it, so what followed a block starts
|
|
127
|
+
* where the block started and the rest of the text keeps its own lines. A marked open with no close (a result cut
|
|
128
|
+
* mid-block) goes to the end of the text — the open tag is the engine's own mint, and what follows it is the block.
|
|
129
|
+
*
|
|
130
|
+
* Only a tag carrying a valid mark is a block here. Reminder-shaped text without the mark, with a value the scheme
|
|
131
|
+
* does not produce, with whitespace around the `=` or another attribute beside the mark, or spelled in another case is data (the declaration says so) and is
|
|
132
|
+
* returned untouched; so is a text with no marked open at all — the same string back.
|
|
133
|
+
*
|
|
134
|
+
* For a caller that PROJECTS a tool result into a shorter view for a reader who does not hold the result's session
|
|
135
|
+
* (the parent of a child agent): the block is the engine's note to the context that ran the tool, not part of what
|
|
136
|
+
* the tool returned, and carries that context's mark.
|
|
137
|
+
*/
|
|
138
|
+
export declare function stripMarkedReminderBlocks(text: string): string;
|
|
@@ -70,3 +70,23 @@ export function markedReminderBodiesOf(text, mark) {
|
|
|
70
70
|
}
|
|
71
71
|
return out;
|
|
72
72
|
}
|
|
73
|
+
const MARKED_OPEN_TAG = /<system-reminder\s+mark=(["'])([^"'<>]*)\1\s*>/g;
|
|
74
|
+
const CLOSE_TAG = "</system-reminder>";
|
|
75
|
+
const BLANK_LINES = /(?:[ \t]*\r?\n)*/y;
|
|
76
|
+
export function stripMarkedReminderBlocks(text) {
|
|
77
|
+
let out = "";
|
|
78
|
+
let from = 0;
|
|
79
|
+
for (const open of text.matchAll(MARKED_OPEN_TAG)) {
|
|
80
|
+
const at = open.index;
|
|
81
|
+
if (at < from || !isValidReminderMark(open[2]))
|
|
82
|
+
continue;
|
|
83
|
+
out += text.slice(from, at);
|
|
84
|
+
const closeAt = text.indexOf(CLOSE_TAG, at + open[0].length);
|
|
85
|
+
if (closeAt < 0)
|
|
86
|
+
return out;
|
|
87
|
+
BLANK_LINES.lastIndex = closeAt + CLOSE_TAG.length;
|
|
88
|
+
BLANK_LINES.exec(text);
|
|
89
|
+
from = BLANK_LINES.lastIndex;
|
|
90
|
+
}
|
|
91
|
+
return from === 0 ? text : out + text.slice(from);
|
|
92
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* optional seat reads. Every retired-key door reads through here so no door grows its own idea of "present";
|
|
6
6
|
* each composes its own refusal from the answer. A table row is `{ key: replacement }` — the sentence that says
|
|
7
7
|
* what replaced the seat, quoted in the refusal. A dotted key names a member of a member bag (`attachments.x`),
|
|
8
|
-
* read the same way one step at a time; a step that is not an object or a function holds nothing.
|
|
8
|
+
* read the same way one step at a time; a step that is not an object or a function holds nothing. A step spelled
|
|
9
|
+
* `name[]` names an array member and reads every element it holds (`skills[].manifest`: any element carrying it).
|
|
9
10
|
*/
|
|
10
11
|
/** The first retired key present on `bag`, in table order, or undefined when none is. */
|
|
11
12
|
export declare function retiredKeyOf(bag: object, table: Readonly<Record<string, string>>): {
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
function presentAt(at, steps) {
|
|
2
|
+
if (steps.length === 0)
|
|
3
|
+
return at !== undefined;
|
|
4
|
+
if (at === null || (typeof at !== "object" && typeof at !== "function"))
|
|
5
|
+
return false;
|
|
6
|
+
const [step, ...rest] = steps;
|
|
7
|
+
if (!step.endsWith("[]"))
|
|
8
|
+
return presentAt(Reflect.get(at, step), rest);
|
|
9
|
+
const items = Reflect.get(at, step.slice(0, -2));
|
|
10
|
+
return Array.isArray(items) && items.some((item) => presentAt(item, rest));
|
|
11
|
+
}
|
|
1
12
|
export function retiredKeyOf(bag, table) {
|
|
2
13
|
for (const [key, replacement] of Object.entries(table)) {
|
|
3
|
-
|
|
4
|
-
if (value !== undefined)
|
|
14
|
+
if (presentAt(bag, key.split(".")))
|
|
5
15
|
return { key, replacement };
|
|
6
16
|
}
|
|
7
17
|
return undefined;
|
|
@@ -18,4 +28,5 @@ export const RETIRED_TASK_SPEC_KEYS = Object.freeze({
|
|
|
18
28
|
autoModeRequested: 'the permission mode is one key now — write `permissionMode: "auto"`',
|
|
19
29
|
approverPosture: 'a posture never authorized anything; drop the key (the engine renders the posture from the effective mode). Releasing asks is a separate decision: `permissionMode: "bypassPermissions"`',
|
|
20
30
|
"attachments.planModeReminder": "the read-only planning posture is stated whenever the leg's mode is `plan` (first turn, every 5 turns, and an exit frame when it ends) — drop the key",
|
|
31
|
+
"skills[].manifest": "a skill's tools are its `allowedTools` / `disallowedTools` now — an allow-rule and a deny-rule source for the run the skill is activated in, never a fence; move `manifest.allowTools` to `allowedTools`. To fence tools write deny rules (or a delegated agent definition's `allowTools`); to fence write paths use session `allowDirs`",
|
|
21
32
|
});
|
|
@@ -33,8 +33,8 @@ declare const RULE_SOURCE_PHRASE: {
|
|
|
33
33
|
readonly named: "permission rule";
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
|
-
* The deny-narrowing fold as a WHOLE — session rules, inherited ancestor rules
|
|
37
|
-
* projections
|
|
36
|
+
* The deny-narrowing fold as a WHOLE — session rules, inherited ancestor rules and the frozen constraint
|
|
37
|
+
* projections. Used where the refusing layer's own sentence is carried as the
|
|
38
38
|
* detail and the wrapper must not claim to know which of them spoke.
|
|
39
39
|
*/
|
|
40
40
|
readonly narrowing: {
|