@sema-agent/core 7.42.1 → 7.43.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -3
  5. package/dist/agents/teacher.js +1 -1
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/allow-entry.d.ts +50 -31
  8. package/dist/core/allow-entry.js +100 -37
  9. package/dist/core/ask-origin.d.ts +16 -5
  10. package/dist/core/ask-origin.js +11 -3
  11. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  12. package/dist/core/ask-unresolvable-notice.js +1 -1
  13. package/dist/core/auto-mode.d.ts +5 -4
  14. package/dist/core/auto-mode.js +3 -1
  15. package/dist/core/checkpoint-store.d.ts +18 -7
  16. package/dist/core/checkpoint-store.js +6 -3
  17. package/dist/core/child-clamp-seats.d.ts +15 -1
  18. package/dist/core/child-clamp-seats.js +6 -2
  19. package/dist/core/classifier-unavailable-arm.d.ts +42 -23
  20. package/dist/core/classifier-unavailable-arm.js +14 -3
  21. package/dist/core/decision-receipt.js +2 -0
  22. package/dist/core/gate-lanes.js +37 -29
  23. package/dist/core/hooks.d.ts +6 -1
  24. package/dist/core/hooks.js +3 -0
  25. package/dist/core/mode-policy.d.ts +5 -4
  26. package/dist/core/mode-policy.js +4 -0
  27. package/dist/core/permission-rule-consent.d.ts +3 -2
  28. package/dist/core/permission-rule-consent.js +25 -20
  29. package/dist/core/permission-rule-model.d.ts +47 -7
  30. package/dist/core/permission-rule-model.js +80 -6
  31. package/dist/core/permission-rules.d.ts +8 -2
  32. package/dist/core/permission-rules.js +26 -9
  33. package/dist/core/runner/classifier-ladder.d.ts +10 -8
  34. package/dist/core/runner/classifier-ladder.js +3 -1
  35. package/dist/core/runner/denial-limit-arms.d.ts +25 -8
  36. package/dist/core/runner/denial-limit-arms.js +24 -10
  37. package/dist/core/runner/gate-exit.js +2 -1
  38. package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
  39. package/dist/core/runner/permission-rule-lanes.js +8 -2
  40. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  41. package/dist/core/runner/prepare-ask-lane.js +2 -1
  42. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
  43. package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
  44. package/dist/core/runner/prepare-config-doors.d.ts +3 -0
  45. package/dist/core/runner/prepare-config-doors.js +6 -5
  46. package/dist/core/runner/prepare-gate-stations.js +1 -1
  47. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  48. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  49. package/dist/core/runner/prepare-park-ask.d.ts +4 -4
  50. package/dist/core/runner/prepare-park-ask.js +2 -4
  51. package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
  52. package/dist/core/runner/prepare-policy-chain.js +59 -38
  53. package/dist/core/runner/prepare-project-context.js +6 -2
  54. package/dist/core/runner/prepare-task.js +6 -6
  55. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
  56. package/dist/core/runner/prepare-wiring-manifest.js +8 -9
  57. package/dist/core/runner/resume-checkpoint-screen.js +1 -1
  58. package/dist/core/runner/synthetic-tools.d.ts +3 -2
  59. package/dist/core/runner/synthetic-tools.js +1 -1
  60. package/dist/core/session-policy-store.d.ts +3 -3
  61. package/dist/core/session-policy-store.js +18 -4
  62. package/dist/core/skill-grant-binding.d.ts +44 -0
  63. package/dist/core/skill-grant-binding.js +22 -0
  64. package/dist/core/skill-grant.d.ts +59 -28
  65. package/dist/core/skill-grant.js +96 -23
  66. package/dist/core/task-spec.d.ts +14 -1
  67. package/dist/core/tighten-task-spec.d.ts +2 -0
  68. package/dist/core/tighten-task-spec.js +3 -0
  69. package/dist/core/tool-name-aliases.d.ts +35 -5
  70. package/dist/core/tool-name-aliases.js +29 -4
  71. package/dist/core/tool-policy.d.ts +10 -0
  72. package/dist/core/tool-policy.js +9 -2
  73. package/dist/core/tool-spec.d.ts +7 -0
  74. package/dist/core/wiring-manifest.d.ts +56 -14
  75. package/dist/core/wiring-manifest.js +29 -4
  76. package/dist/index.d.ts +3 -2
  77. package/dist/index.js +2 -2
  78. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  79. package/dist/orchestration/run-workflow-tool.js +1 -1
  80. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  81. package/dist/orchestration/workflow-primitives.js +6 -6
  82. package/package.json +1 -1
  83. package/test/export-surface.snapshot.json +11 -1
  84. package/wire-changes/core@7.43.0.json +985 -0
@@ -11,7 +11,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
11
11
  import { mountWriteProtection } from "./write-protection-station.js";
12
12
  import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
13
13
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
14
- import { LSP_CONTRACT_ID, askSeatEffective, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
14
+ import { LSP_CONTRACT_ID, askSeatEffective, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
15
15
  import { modePolicyOf } from "../mode-policy.js";
16
16
  import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
17
17
  export function mcpManifestEntries(entries, statuses) {
@@ -102,9 +102,9 @@ function announceDurableGateUnavailable(args) {
102
102
  message: `Durable approval gate mandated but unavailable: ${source === "force_durable_gate" ? "the runtime entitlement forceDurableGate is in force for this principal" : "this task opted into durable approval (durableApproval) with no live approver seat"}, ` +
103
103
  `but ${cause === "task_store_disabled" ? 'this task disabled the checkpoint store (checkpointStore: "disabled")' : "this deployment wired no checkpoint store"}, ` +
104
104
  `so no interactive ask of this task can be parked for a durable answer. Asks resolve on the live chain instead ` +
105
- `(${args.liveApprover ? "a live approver answers permission asks in-stream" : args.blanketAllow ? 'a blanket "allow" posture answers ordinary permission asks without judgment (a mandated ask is refused)' : "permission asks are denied fail-closed — no live approver is wired"}; ` +
105
+ `(${args.liveApprover ? "a live approver answers permission asks in-stream" : args.blanketAllow ? 'a blanket "allow" posture answers ordinary permission asks without judgment (a mandated ask is refused)' : "permission asks are denied fail-closed — nobody in this run can be asked to approve them"}; ` +
106
106
  `${liveQuestionFace ? "a live question face answers AskUserQuestion in-stream" : args.liveQuestionFace ? "AskUserQuestion is withheld from the live face by an inherited durable mandate and refused" : "AskUserQuestion has no live face and is refused"}) ` +
107
- `and NO durable approval record is written. ${cause === "task_store_disabled" ? "Run this task with its store" : "Wire a checkpoint store on this deployment"}, or ${source === "force_durable_gate" ? "withdraw the entitlement for principals served here" : "give the task a live approver seat (or drop durableApproval)"}.`,
107
+ `and NO durable approval record is written. ${cause === "task_store_disabled" ? "Run this task with its store" : "Wire a checkpoint store on this deployment"}, or ${source === "force_durable_gate" ? "withdraw the entitlement for principals served here" : args.declaredNone ? 'run the task where a person can be asked (without approvalReach: "none"), or drop durableApproval' : "give the task a live approver seat (or drop durableApproval)"}.`,
108
108
  detail: {
109
109
  sessionId: args.sessionId,
110
110
  runId: args.runId,
@@ -117,8 +117,8 @@ function announceDurableGateUnavailable(args) {
117
117
  });
118
118
  }
119
119
  export function prepareWiringManifest(input) {
120
- const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, sessionBox, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, readDenyBuiltinTiersActive, lspManagerWired, shellReadStationArmed, lspDiagnosticsLive, fileHistoryFact } = input;
121
- const onAsk = spec.onAsk ?? deps.onAsk;
120
+ const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, sessionBox, harness, pausedRef, askSeat, liveInheritedGate, delegation, toolRoster, readDenyBuiltinTiersActive, lspManagerWired, shellReadStationArmed, lspDiagnosticsLive, fileHistoryFact } = input;
121
+ const onAsk = askSeat.value;
122
122
  const handWriteTools = toolRoster.entries.filter((e) => e.mountedBy === "hands" && (e.effect === "write" || e.effect === "idempotent")).map((e) => e.name);
123
123
  const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
124
124
  const peerWriteUngated = modePolicyOf(permissionMode.current).unstated === "ask"
@@ -181,14 +181,13 @@ export function prepareWiringManifest(input) {
181
181
  const writeProtection = mountWriteProtection({ deps, env: handsEnabled ? executionEnv : undefined, rootPath: taskRootFinal, roster: toolRoster, sessionId, runId });
182
182
  const wiringQuestionSeam = resolveQuestionSeam(spec, deps);
183
183
  const wiringQuestionStripped = internals?.questionFaceStripped === true;
184
- const wiringAskSeam = resolveAskSeamForm(spec, deps);
185
184
  const wiringManifest = deriveWiringManifest({
186
185
  half: "effective",
187
186
  leg: resume !== undefined ? "resume" : internals?.isDelegatedChild === true ? "child" : "root",
188
187
  teamRole: teamRoleOf({ isDelegatedChild: delegation.isDelegatedChild, ...(internals?.explicitAgentName !== undefined ? { spawnName: internals.explicitAgentName } : {}) }),
189
188
  ...(spec.peerPresence !== undefined ? { peerPresence: spec.peerPresence } : {}),
190
- askForm: wiringAskSeam.form,
191
- ...(wiringAskSeam.provenance !== undefined ? { askProvenance: wiringAskSeam.provenance } : {}),
189
+ askForm: askSeat.form,
190
+ ...(askSeat.provenance !== undefined ? { askProvenance: askSeat.provenance } : {}),
192
191
  questionWired: wiringQuestionSeam.wired,
193
192
  ...(wiringQuestionSeam.provenance !== undefined ? { questionProvenance: wiringQuestionSeam.provenance } : {}),
194
193
  ...(wiringQuestionStripped ? { questionStrippedByEngine: true } : {}),
@@ -254,7 +253,7 @@ export function prepareWiringManifest(input) {
254
253
  resourceSuspendEligible ||
255
254
  platformSuspendArmed ||
256
255
  spec.enablePlanMode === true;
257
- announceDurableGateUnavailable({ onNotice: deps.onNotice, mandate: durableMandateSourceOf({ forceDurableGate: runtimeCaps?.forceDurableGate === true, durableApprovalDeclared: spec.durableApproval !== undefined, onAsk: frozenOnAsk }), storeWired: checkpointStore !== undefined, taskStoreDisabled: spec.checkpointStore === "disabled", liveApprover: isLiveApproverSeat(frozenOnAsk), blanketAllow: frozenOnAsk === "allow", liveQuestionFace: liveQuestionFace !== undefined, parentConstraints: liveInheritedGate?.parentConstraints, sessionId, runId, principal: spec.principal });
256
+ announceDurableGateUnavailable({ onNotice: deps.onNotice, mandate: durableMandateSourceOf({ forceDurableGate: runtimeCaps?.forceDurableGate === true, durableApprovalDeclared: spec.durableApproval !== undefined, onAsk }), storeWired: checkpointStore !== undefined, taskStoreDisabled: spec.checkpointStore === "disabled", liveApprover: isLiveApproverSeat(onAsk), blanketAllow: onAsk === "allow", declaredNone: askSeat.reach === "none", liveQuestionFace: liveQuestionFace !== undefined, parentConstraints: liveInheritedGate?.parentConstraints, sessionId, runId, principal: spec.principal });
258
257
  const hookIdentity = mintHookInvocationIdentity({
259
258
  sessionId,
260
259
  taskId: hostTaskId,
@@ -48,7 +48,7 @@ export function resumeCheckpointScreen(input) {
48
48
  }
49
49
  }
50
50
  if (readFrozenSkillGrants(cp.state.skillGrants) === undefined)
51
- throw new CheckpointError("checkpoint.invalid_outcome", "this checkpoint carries a skill-grant section (state.skillGrants) that does not read as grant rows — refusing to resume without the grant rows and refusals it carries (corruption guard), the checkpoint stays pending", { reason: "skill_grants_damaged" });
51
+ throw new CheckpointError("checkpoint.invalid_outcome", "this checkpoint carries a skill-grant section (state.skillGrants) that does not read as grant rows under this engine — a damaged section, or a row an earlier release wrote whose entry this release reads as nothing (an entry with a surrounding space, say) — refusing to resume without the grant rows and refusals it carries; the checkpoint stays pending: finish it on a worker of the release that parked it, or expire it", { reason: "skill_grants_damaged" });
52
52
  }
53
53
  if (realApprovalOrgFact(preCasGateBit) !== undefined && runner.deps.permissionRuleStore?.partitions.org !== true) {
54
54
  throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (org ${String(realApprovalOrgFact(preCasGateBit))}, routed as ${String(preCasGateBit?.origin)}) and this worker has no org adjudication wiring (its permission-rule store carries no org partition) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
@@ -169,9 +169,10 @@ export declare function renderSkillLinesWithin(skills: ReadonlyArray<SkillListin
169
169
  *
170
170
  * A SUCCESSFUL activation writes the skill's two lists into the run's grant table (`grants`, origin `model`): its
171
171
  * `allowed-tools` then run without asking and its `disallowed-tools` are refused for the rest of the run; an unknown skill
172
- * throws first and writes nothing. Activating a skill that declares a list was itself a question (the gate reads this
172
+ * throws first and writes nothing, as does one whose `disallowed-tools` holds an entry that reads as nothing (the table
173
+ * refuses it: the body is not served, nothing is granted). Activating a skill that declares a list was itself a question (the gate reads this
173
174
  * tool's seat off the call's arguments — `Execute skill: <name>`), so a write here always follows a statement. The tool
174
175
  * runs ALONE in its batch: a call after it in the same message is judged after the rows exist.
175
176
  */
176
- export declare function createSkillTool(skills: SkillLike[], grants?: SkillGrantTable): AgentTool;
177
+ export declare function createSkillTool(skills: SkillLike[], grants: SkillGrantTable): AgentTool;
177
178
  export {};
@@ -188,7 +188,7 @@ export function createSkillTool(skills, grants) {
188
188
  `(Only skills registered with this deployment are loadable — other products' skills directories are not read. ` +
189
189
  `If the user wants one of those here, read its source file and recreate its content for this deployment instead.)`);
190
190
  }
191
- grants?.activate(skill, "model");
191
+ grants.activate(skill, "model");
192
192
  const baseDir = skill.baseDir;
193
193
  let body = baseDir !== undefined ? substituteSkillRoot(skill.content, baseDir) : skill.content;
194
194
  if (skill.files !== undefined && skill.files.length > 0) {
@@ -53,7 +53,7 @@ export interface PutRulesOptions {
53
53
  operator?: boolean;
54
54
  }
55
55
  /** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, and the
56
- * 1:1 successor when the retired name has one. */
56
+ * spelling to write instead when the judgment has one (a retired name's 1:1 successor, an unmintable segment's minted name). */
57
57
  export interface SessionRuleNameRefusal {
58
58
  field: SessionRuleToolNameField;
59
59
  name: string;
@@ -63,8 +63,8 @@ export interface SessionRuleNameRefusal {
63
63
  /** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
64
64
  * = a non-operator write tried to relax the rules; `corrupt` = a backend could not read the erase-surviving
65
65
  * rev mark ({@link SessionPolicyStore.deleteBySession}) and refuses rather than stamp an unproven rev; the fourth
66
- * code = a tool-name bucket names a retired or prefix-less name ({@link assertSessionRuleNamesCurrent}) — `detail` is
67
- * present on that code and only on it. */
66
+ * code = a tool-name bucket names a retired name, a prefix-less `__` name or a peer segment no mint spells
67
+ * ({@link assertSessionRuleNamesCurrent}) — `detail` is present on that code and only on it. */
68
68
  export declare class SessionPolicyError extends Error {
69
69
  readonly code: "conflict" | "loosen_forbidden" | "corrupt" | "legacy_tool_name";
70
70
  readonly detail?: SessionRuleNameRefusal | undefined;
@@ -40,10 +40,24 @@ export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLA
40
40
  if (verdict.kind === "current")
41
41
  continue;
42
42
  const quoted = JSON.stringify(name);
43
- const successor = verdict.kind === "retired" ? verdict.currentName : undefined;
44
- throw new SessionPolicyError("legacy_tool_name", verdict.kind === "retired"
45
- ? `session rules ${field} names ${quoted}, a retired tool name (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Use the current name ${JSON.stringify(successor)}.` : ""} If this deployment mounts its own tool under ${quoted}, declare it in the store's mountedToolNames.`
46
- : `session rules ${field} names ${quoted}, which carries the "__" namespace separator but no protocol prefix — no tool is named that way (protocol tools are named ${NAMESPACED_NAME_SHAPES}), and with it in place no run of this session could start, so nothing was written.`, { field, name, verdict: verdict.kind, ...(successor !== undefined ? { currentName: successor } : {}) });
43
+ const successor = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
44
+ let message;
45
+ switch (verdict.kind) {
46
+ case "retired":
47
+ message = `session rules ${field} names ${quoted}, a retired tool name (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Use the current name ${JSON.stringify(successor)}.` : ""} If this deployment mounts its own tool under ${quoted}, declare it in the store's mountedToolNames.`;
48
+ break;
49
+ case "unprefixed_namespace":
50
+ message = `session rules ${field} names ${quoted}, which carries the "__" namespace separator but no protocol prefix — no tool is named that way (protocol tools are named ${NAMESPACED_NAME_SHAPES}), and with it in place no run of this session could start, so nothing was written.`;
51
+ break;
52
+ case "unmintable_segment":
53
+ message = `session rules ${field} names ${quoted}, which can match no tool (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Write ${JSON.stringify(successor)}.` : ""}`;
54
+ break;
55
+ default: {
56
+ const exhaustive = verdict;
57
+ return exhaustive;
58
+ }
59
+ }
60
+ throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(successor !== undefined ? { currentName: successor } : {}) });
47
61
  }
48
62
  }
49
63
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * What a Skill card's approval is bound to, as ONE value: the skill the call names and the two lists approving it releases,
3
+ * in the rule grammar's canonical spelling (de-duplicated, sorted). Its hash is the `boundGrantDigest` a parked approval is
4
+ * judged by, so a reader holds the list a person approved and the binding that covers it as the same fact — the live
5
+ * `AskRequest`, the parked row and the inbox projection all carry it (minted by `skillGrantBindingMembers`).
6
+ *
7
+ * A leaf on purpose: the checkpoint projection reads it at its boundary and may not import the module that mints it (it imports only
8
+ * the hash).
9
+ */
10
+ export interface BoundSkillGrants {
11
+ /** The skill's name as the loaded table spells it, or `null` when the call names no loaded skill (it grants nothing). */
12
+ readonly skill: string | null;
13
+ /** The skill's `allowed-tools` entries — what approving the activation lets be used without asking. */
14
+ readonly allowedTools: readonly string[];
15
+ /** The skill's `disallowed-tools` entries — what activating it refuses for the rest of the run. */
16
+ readonly disallowedTools: readonly string[];
17
+ }
18
+ /**
19
+ * A stored value read as a {@link BoundSkillGrants}, whole or not at all: a fresh copy of the three members and nothing else,
20
+ * or `undefined` when any member is not of its type. Every member is read ONCE, so the value screened is the value published.
21
+ */
22
+ export declare function readBoundSkillGrants(raw: unknown): BoundSkillGrants | undefined;
23
+ /** The card members a call to the engine's `Skill` tool carries on every face its card has — empty for any other call. */
24
+ export interface SkillGrantBindingMembers {
25
+ /** The hash of {@link boundGrants}: the binding a parked approval of the card is judged by. */
26
+ boundGrantDigest?: string;
27
+ boundGrants?: BoundSkillGrants;
28
+ }
29
+ /** The leg's binding factory ({@link SkillGrantBindingMembers} for a call over `args`), the one every ask-mint site spreads. */
30
+ export type GrantBindingOf = (call: {
31
+ readonly toolName: string;
32
+ }, args: unknown) => SkillGrantBindingMembers;
33
+ /**
34
+ * The binding members of a stored parked row, as the inbox projection publishes them: the digest when it is a string, and the lists
35
+ * only as the value that digest hashes — {@link readBoundSkillGrants} whole, and equal to the digest by hash. A pair that
36
+ * disagrees, or lists beside no digest, would show an approver one list while the approval binds another: the lists read as absent.
37
+ */
38
+ export declare function readBoundBinding(row: {
39
+ readonly boundGrantDigest?: unknown;
40
+ readonly boundGrants?: unknown;
41
+ } | undefined): {
42
+ boundGrantDigest?: string;
43
+ boundGrants?: BoundSkillGrants;
44
+ };
@@ -0,0 +1,22 @@
1
+ import { boundInputHashOf } from "./canonical-json.js";
2
+ export function readBoundSkillGrants(raw) {
3
+ if (typeof raw !== "object" || raw === null)
4
+ return undefined;
5
+ const { skill, allowedTools: allowed, disallowedTools: disallowed } = raw;
6
+ if (skill !== null && typeof skill !== "string")
7
+ return undefined;
8
+ const entries = (list) => {
9
+ if (!Array.isArray(list))
10
+ return undefined;
11
+ const copy = Array.from(list);
12
+ return copy.every((e) => typeof e === "string") ? copy : undefined;
13
+ };
14
+ const allowedTools = entries(allowed);
15
+ const disallowedTools = entries(disallowed);
16
+ return allowedTools !== undefined && disallowedTools !== undefined ? { skill, allowedTools, disallowedTools } : undefined;
17
+ }
18
+ export function readBoundBinding(row) {
19
+ const digest = row?.boundGrantDigest;
20
+ const grants = readBoundSkillGrants(row?.boundGrants);
21
+ return { ...(typeof digest === "string" ? { boundGrantDigest: digest } : {}), ...(grants !== undefined && typeof digest === "string" && boundInputHashOf(grants) === digest ? { boundGrants: grants } : {}) };
22
+ }
@@ -2,7 +2,8 @@ import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
2
2
  import type { PermissionMode } from "./ask-origin.js";
3
3
  import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
4
4
  import type { AgentTool } from "../internal/harness-types.js";
5
- import { type AllowEntryGateSeats, type AllowEntryInertReason, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
5
+ import type { SkillGrantBindingMembers } from "./skill-grant-binding.js";
6
+ import { type AllowEntryGateSeats, type AllowEntryInert, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
6
7
  /** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
7
8
  * delegated child's seed from its parent. */
8
9
  export declare const SKILL_GRANT_ORIGINS: readonly ["model", "user", "inherited"];
@@ -25,12 +26,12 @@ export interface SkillGrantRow {
25
26
  readonly compiled: CompiledAllowEntry;
26
27
  }
27
28
  /** An entry the grammar reads as granting or refusing nothing. */
28
- export interface SkillInertEntry {
29
+ export type SkillInertEntry = {
29
30
  readonly skill: string;
30
31
  readonly list: "allowed-tools" | "disallowed-tools";
31
- readonly entry: string;
32
- readonly reason: AllowEntryInertReason;
33
- }
32
+ } & AllowEntryInert;
33
+ /** The names the run mounts, read when a list is — a skill is activated later than prepare, and the roster is read as it stands. */
34
+ export type MountedToolNames = () => ReadonlySet<string>;
34
35
  /** The two lists a skill declares (the shape of a `SkillSpec`, read by name). */
35
36
  export interface SkillGrantSource {
36
37
  readonly name: string;
@@ -45,23 +46,19 @@ export interface SkillGrantSource {
45
46
  * the person's invoked names are read through it ({@link seedSkillGrants}).
46
47
  */
47
48
  export declare function skillNamedBy<T extends Pick<SkillGrantSource, "name">>(args: unknown, skills: readonly T[]): T | undefined;
48
- /**
49
- * The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases: the skill's name
50
- * as the table spells it ({@link skillNamedBy} resolved it, so it equals the call's `skill` exactly) and its two lists in the
51
- * rule grammar's canonical spelling (`Tool()` / `Tool(*)` read as the bare name), de-duplicated and sorted, hashed as the
52
- * approval binding is hashed (`boundInputHashOf`). `undefined` — no skill named — digests as a nameless skill declaring
53
- * nothing (activating it grants nothing). Any change to the name or to either list changes it; the body does not. The read
54
- * point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation).
55
- */
49
+ /** The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases ({@link skillGrantBinding}). The read point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation). */
56
50
  export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" | "allowedTools" | "disallowedTools"> | undefined): string;
57
51
  /**
58
- * The digest a parked call to the engine's `Skill` tool binds — written by the park beside the row's input binding — or
59
- * undefined for any other call, and when no skill is loaded (no engine `Skill` tool is mounted, so a call by that name is
60
- * not an activation).
52
+ * The card members of a call to the engine's `Skill` tool — the ONE factory every ask spreads them from: the live
53
+ * `AskRequest` an approver is handed, an ancestor's, the parked row and (through the row) the inbox projection.
54
+ * `boundGrants` is the skill's name and canonical lists, `boundGrantDigest` its hash — the same value, so what a person was
55
+ * shown and what an approving resume is judged by cannot differ. The binding is a property of the CALL, whoever asked and
56
+ * however the question was worded. Empty for any other call, and when no skill is loaded (no engine `Skill` tool is
57
+ * mounted, so a call by that name is not an activation).
61
58
  */
62
- export declare function skillCallGrantDigest(call: {
59
+ export declare function skillGrantBindingMembers(call: {
63
60
  readonly toolName: string;
64
- }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): string | undefined;
61
+ }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): SkillGrantBindingMembers;
65
62
  /**
66
63
  * The approving resume of a parked call, judged against the card it was asked under. A row is a `Skill` approval when it
67
64
  * carries a digest (the park said so), or when the resume would run the engine's `Skill` tool on it (skills loaded, the
@@ -87,10 +84,10 @@ export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowe
87
84
  /**
88
85
  * Compile one skill's two lists into rows (`origin` stamped on each) and the entries that read as nothing. An allow list
89
86
  * holding `*` voids that list whole (every other entry on it is reported too: the list grants nothing — the safe direction).
90
- * A `*` on the deny list reads as nothing by itself and the rest of that list still refuses: a deny list never loses an
91
- * entry to a wildcard beside it.
87
+ * A `*` on the deny list reads as nothing by itself, like any deny entry that does — and a skill whose deny list holds one
88
+ * is not written at all ({@link SkillGrantTable.activate}).
92
89
  */
93
- export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin): {
90
+ export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin, roster?: ReadonlySet<string>): {
94
91
  rows: SkillGrantRow[];
95
92
  inert: SkillInertEntry[];
96
93
  };
@@ -98,8 +95,10 @@ export declare function compileSkillGrant(skill: SkillGrantSource, origin: Skill
98
95
  export interface SkillGrantHit {
99
96
  readonly skill: string;
100
97
  readonly entries: readonly string[];
98
+ /** On a refusal only: the call activates `skill`, whose `disallowed-tools` entries `entries` read as nothing (why, each). */
99
+ readonly readsAsNothing?: readonly AllowEntryInert[];
101
100
  }
102
- /** The refusal a deny row writes. */
101
+ /** The refusal a deny hit writes: a deny row's, or an activation's whose deny entries read as nothing. */
103
102
  export declare function skillDenyDecision(hit: SkillGrantHit): PermissionResult;
104
103
  /** The run's deny rows over `req` through `lane` — the refusal they write, or undefined (no lane, no row). The one
105
104
  * reading the gate's regular decision, its approved-edit round and the resumed execution boundary share. */
@@ -111,12 +110,13 @@ export declare function skillAllowDecision(hit: SkillGrantHit, updatedInput: unk
111
110
  * releases anything — the entries it releases for the rest of the run in this mode (an entry that grants nothing, or
112
111
  * stands down in this mode, is not named). Deployment text, sanitized and capped.
113
112
  */
114
- export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined): string;
113
+ export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined, roster?: ReadonlySet<string>): string;
115
114
  /** The face the gate and the resumed execution boundary read the table through, bound to the leg's call facts. */
116
115
  export interface SkillGrantLane {
117
116
  /** The allow layer's skill member: what grants this call in this mode, or undefined. */
118
117
  allowing(req: ToolCallRequest, mode: PermissionMode | undefined): SkillGrantHit | undefined;
119
- /** The run's deny rows: what refuses this call, or undefined. */
118
+ /** What refuses this call, or undefined: the run's deny rows, else — for a call to the `Skill` tool — a named skill whose
119
+ * `disallowed-tools` holds an entry that reads as nothing (`readsAsNothing`). */
120
120
  denying(req: ToolCallRequest): SkillGrantHit | undefined;
121
121
  }
122
122
  /** The gate inputs the run's skill rows ride (a member group of `ToolGateInput`). */
@@ -141,7 +141,7 @@ export interface SkillGateSeats {
141
141
  /** The `Skill` tool's per-call seat over the loaded skills: a skill that declares a list states no permission of its own. */
142
142
  export type SkillSeatReader = (args: unknown, mode: PermissionMode | undefined) => PermissionSeatReading | undefined;
143
143
  /** The reader over these skills (`undefined` when none is loaded): `Execute skill: <name>` for a skill declaring a list. */
144
- export declare function skillSeatReader(skills: readonly SkillGrantSource[]): SkillSeatReader | undefined;
144
+ export declare function skillSeatReader(skills: readonly SkillGrantSource[], mounted?: MountedToolNames): SkillSeatReader | undefined;
145
145
  /**
146
146
  * The gate's seat inputs for one call, composed in ONE place: the mounted instance's static seat (`calledName`), the `Skill`
147
147
  * tool's per-call reader in the leg's live mode (only when the call's wire name is the engine's `Skill` — `skillToolName`),
@@ -162,7 +162,8 @@ export declare function skillGateWiring(opts: {
162
162
  * parent's capture, held until this child ends, and a resumed leg's frozen rows, which keep the word `model` where the
163
163
  * model wrote them) and the skills the person invoked by name (`user` — the invocation is the statement, so no card). Every invoked
164
164
  * name must be one of the loaded skills; anything else (or a value that is not a list of names) refuses the run loudly
165
- * (`config.user_invoked_skill_unknown`), never dropped. Returns the entries the grammar reads as nothing, across every
165
+ * (`config.user_invoked_skill_unknown`), never dropped; an invoked skill, or a carried deny record, whose deny entry reads as
166
+ * nothing refuses it too ({@link SKILL_DENY_UNREADABLE_CODE}). Returns the entries the grammar reads as nothing, across every
166
167
  * loaded skill — the start-of-run notice's content.
167
168
  */
168
169
  export declare function seedSkillGrants(table: SkillGrantTable, opts: {
@@ -170,23 +171,50 @@ export declare function seedSkillGrants(table: SkillGrantTable, opts: {
170
171
  userInvokedSkills: unknown;
171
172
  inherited: readonly SkillGrantRecord[] | undefined;
172
173
  }): readonly SkillInertEntry[];
174
+ /**
175
+ * The user-invoked half of {@link seedSkillGrants}, read where no roster exists yet — a resume's pre-claim screen, over the
176
+ * re-supplied config: the invoked names seeded exactly as prepare seeds them, on a table told no mounted names, so only what
177
+ * every roster refuses alike refuses here (a name the config's skills do not carry; a deny entry the grammar alone reads as
178
+ * nothing). A name the leg's roster reads as stale or a near miss is prepare's to refuse; any other throw (a value prepare
179
+ * reports its own way) is left to prepare.
180
+ */
181
+ export declare function screenUserInvokedSkills(config: {
182
+ readonly skills?: readonly SkillGrantSource[];
183
+ readonly userInvokedSkills?: unknown;
184
+ }): void;
173
185
  /** The per-task table. Identity is the contract: the `Skill` tool, prepare's seeds, the tool ctx's snapshot and the
174
186
  * lane all hold this one object. */
175
187
  export declare class SkillGrantTable {
176
188
  private rowsHeld;
189
+ private readonly rowKeys;
190
+ private mounted;
191
+ private activations;
192
+ /** State where the run's mounted names are read (prepare, once the roster is assembled): a skill's entries are read against
193
+ * them when seeded and whenever the skill is activated. A table never told reads the grammar alone. */
194
+ bindMountedNames(mounted: MountedToolNames): void;
195
+ /** State the skills the run's `Skill` tool activates (the list it serves) and its wire name, so a call naming one whose deny
196
+ * entries read as nothing is refused where the deny rows are read. A table never told refuses such a skill at activation only. */
197
+ bindSkills(skills: readonly SkillGrantSource[], skillToolName: string): void;
198
+ /** The names the run mounts as they stand, or undefined when none were stated. */
199
+ mountedNames(): ReadonlySet<string> | undefined;
177
200
  /** The table never holds the same row twice (skill, origin, list, entry) — one rule for both writers: a re-activation, a
178
201
  * capture re-supplied beside the frozen rows that already carry it. Keyed on the ROW, not the skill: a definition that
179
202
  * changed across a park writes its new rows when it is activated again. */
180
203
  private hold;
181
- /** Write a skill's rows (rows already held are not held twice). Returns the entries that read as nothing. */
204
+ /** Write a skill's rows (rows already held are not held twice) — or none, refused with {@link SKILL_DENY_UNREADABLE_CODE},
205
+ * when its `disallowed-tools` holds an entry that reads as nothing. Returns the allow entries that read as nothing. */
182
206
  activate(skill: SkillGrantSource, origin: SkillGrantOrigin): readonly SkillInertEntry[];
183
207
  /** Seed portable records (a delegated child's capture, a resumed leg's frozen rows): a record the model wrote keeps that
184
- * word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. */
208
+ * word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. Each
209
+ * record is read as its writer read it, against the leg's roster: an allow record that reads as nothing grants nothing
210
+ * (not held); a deny record that does refuses the seed ({@link SKILL_DENY_UNREADABLE_CODE}) and nothing is held. */
185
211
  seed(records: readonly SkillGrantRecord[], origin: SkillGrantOrigin): void;
186
212
  /** The rows as portable records (a copy). */
187
213
  records(): readonly SkillGrantRecord[];
188
214
  /** The lane over this table, bound to the leg's call facts. */
189
215
  lane(ctx: AllowEntryJudgeContext): SkillGrantLane;
216
+ /** A call to the `Skill` tool naming a skill whose deny entries read as nothing, read as the activation would read it. */
217
+ private activationRefused;
190
218
  }
191
219
  /** Read a delegated child's capture ({@link readCarriedRecords}): `undefined` refuses the delegation. */
192
220
  export declare function readSkillGrantCapture(snapshot: readonly unknown[] | undefined): readonly SkillGrantRecord[] | undefined;
@@ -198,3 +226,6 @@ export declare function frozenSkillGrants(table: Pick<SkillGrantTable, "records"
198
226
  export declare function readFrozenSkillGrants(section: unknown): readonly SkillGrantRecord[] | undefined;
199
227
  /** The refusal for a `TaskSpec.userInvokedSkills` name the run does not carry (or a value that is not a list of names). */
200
228
  export declare const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
229
+ /** The refusal for a skill whose `disallowed-tools` holds an entry that reads as nothing: the activation, or the leg that
230
+ * would carry the skill's deny record, does not happen. */
231
+ export declare const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
@@ -3,25 +3,32 @@ import { parsePermissionRule } from "./permission-rules.js";
3
3
  import { mountedToolPermissionSeat } from "./mode-policy.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { boundInputHashOf } from "./canonical-json.js";
6
- import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, judgeAllowEntries } from "./allow-entry.js";
6
+ import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries } from "./allow-entry.js";
7
7
  export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
8
8
  const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
9
9
  export function skillNamedBy(args, skills) {
10
10
  const named = args?.skill;
11
11
  return typeof named === "string" ? skills.find((s) => s.name === named) : undefined;
12
12
  }
13
- export function skillGrantDigest(skill) {
13
+ function skillGrantBinding(skill) {
14
14
  const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
15
15
  const p = parsePermissionRule(e);
16
16
  return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
17
17
  }))].sort();
18
- return boundInputHashOf({ skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) });
18
+ const boundGrants = { skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) };
19
+ return { boundGrantDigest: boundInputHashOf(boundGrants), boundGrants };
20
+ }
21
+ export function skillGrantDigest(skill) {
22
+ return skillGrantBinding(skill).boundGrantDigest;
19
23
  }
20
- export function skillCallGrantDigest(call, args, skills, skillToolName) {
21
- return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName ? skillGrantDigest(skillNamedBy(args, skills)) : undefined;
24
+ export function skillGrantBindingMembers(call, args, skills, skillToolName) {
25
+ return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : {};
26
+ }
27
+ function isSkillActivationCall(call, skills, skillToolName) {
28
+ return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName;
22
29
  }
23
30
  export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
24
- if (row.boundGrantDigest === undefined && skillCallGrantDigest(row, args, skills, skillToolName) === undefined)
31
+ if (row.boundGrantDigest === undefined && skillGrantBindingMembers(row, args, skills, skillToolName).boundGrantDigest === undefined)
25
32
  return undefined;
26
33
  if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
27
34
  return undefined;
@@ -43,18 +50,18 @@ export function detachSkillLists(skills) {
43
50
  export function declaresSkillLists(skill) {
44
51
  return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
45
52
  }
46
- export function compileSkillGrant(skill, origin) {
53
+ export function compileSkillGrant(skill, origin, roster) {
47
54
  const rows = [];
48
55
  const inert = [];
49
56
  for (const [list, behavior, entries] of [
50
57
  ["allowed-tools", "allow", skill.allowedTools ?? []],
51
58
  ["disallowed-tools", "deny", skill.disallowedTools ?? []],
52
59
  ]) {
53
- const compiled = compileAllowList(entries, behavior);
60
+ const compiled = compileAllowList(entries, behavior, roster);
54
61
  for (const r of compiled.rows)
55
62
  rows.push({ skill: skill.name, origin, behavior, entry: r.entry, compiled: r.compiled });
56
63
  for (const i of compiled.inert)
57
- inert.push({ skill: skill.name, list, entry: i.entry, reason: i.reason });
64
+ inert.push({ skill: skill.name, list, ...i });
58
65
  }
59
66
  return { rows, inert };
60
67
  }
@@ -65,10 +72,15 @@ function judge(rows, req, behavior, ctx) {
65
72
  export function skillDenyDecision(hit) {
66
73
  return {
67
74
  action: "deny",
68
- message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
75
+ message: hit.readsAsNothing !== undefined
76
+ ? skillDenyUnreadable(hit.skill, hit.readsAsNothing, "activation").message
77
+ : `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
69
78
  decisionReason: "skill",
70
79
  };
71
80
  }
81
+ function unreadDenyOf(inert) {
82
+ return inert.filter((i) => i.list === "disallowed-tools");
83
+ }
72
84
  export function skillRowRefusal(lane, req) {
73
85
  const hit = lane?.denying(req);
74
86
  return hit === undefined ? undefined : skillDenyDecision(hit);
@@ -83,21 +95,21 @@ export function skillAllowDecision(hit, updatedInput) {
83
95
  }
84
96
  const CARD_ENTRIES_MAX = 10;
85
97
  const CARD_ENTRY_MAX_CHARS = 120;
86
- export function skillActivationQuestion(skill, mode) {
98
+ export function skillActivationQuestion(skill, mode, roster) {
87
99
  const head = `Execute skill: ${inlineUntrusted(skill.name, 200)}`;
88
- const granting = compileSkillGrant(skill, "model").rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
100
+ const granting = compileSkillGrant(skill, "model", roster).rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
89
101
  if (granting.length === 0)
90
102
  return head;
91
103
  const shown = granting.slice(0, CARD_ENTRIES_MAX).map((r) => inlineUntrusted(r.entry, CARD_ENTRY_MAX_CHARS));
92
104
  const rest = granting.length - shown.length;
93
105
  return `${head}\nApproving also lets these tools be used without asking for the rest of this run: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
94
106
  }
95
- export function skillSeatReader(skills) {
107
+ export function skillSeatReader(skills, mounted) {
96
108
  if (skills.length === 0)
97
109
  return undefined;
98
110
  return (args, mode) => {
99
111
  const skill = skillNamedBy(args, skills);
100
- return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode) } : undefined;
112
+ return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode, mounted?.()) } : undefined;
101
113
  };
102
114
  }
103
115
  export function skillGateWiring(opts) {
@@ -123,27 +135,68 @@ export function seedSkillGrants(table, opts) {
123
135
  for (const name of invoked)
124
136
  table.activate(named(name), "user");
125
137
  }
126
- return opts.skills.flatMap((s) => compileSkillGrant(s, "model").inert);
138
+ const roster = table.mountedNames();
139
+ return opts.skills.flatMap((s) => compileSkillGrant(s, "model", roster).inert);
140
+ }
141
+ export function screenUserInvokedSkills(config) {
142
+ if (config.userInvokedSkills === undefined)
143
+ return;
144
+ try {
145
+ seedSkillGrants(new SkillGrantTable(), { skills: config.skills ?? [], userInvokedSkills: config.userInvokedSkills, inherited: [] });
146
+ }
147
+ catch (e) {
148
+ const code = e?.code;
149
+ if (code === USER_INVOKED_SKILL_UNKNOWN_CODE || code === SKILL_DENY_UNREADABLE_CODE)
150
+ throw e;
151
+ }
127
152
  }
128
153
  export class SkillGrantTable {
129
154
  rowsHeld = [];
155
+ rowKeys = new Set();
156
+ mounted;
157
+ activations;
158
+ bindMountedNames(mounted) {
159
+ this.mounted = mounted;
160
+ }
161
+ bindSkills(skills, skillToolName) {
162
+ this.activations = { skills, skillToolName };
163
+ }
164
+ mountedNames() {
165
+ return this.mounted?.();
166
+ }
130
167
  hold(row) {
131
- if (!this.rowsHeld.some((r) => r.skill === row.skill && r.origin === row.origin && r.behavior === row.behavior && r.entry === row.entry))
132
- this.rowsHeld.push(row);
168
+ const key = JSON.stringify([row.skill, row.origin, row.behavior, row.entry]);
169
+ if (this.rowKeys.has(key))
170
+ return;
171
+ this.rowKeys.add(key);
172
+ this.rowsHeld.push(row);
133
173
  }
134
174
  activate(skill, origin) {
135
- const { rows, inert } = compileSkillGrant(skill, origin);
175
+ const { rows, inert } = compileSkillGrant(skill, origin, this.mountedNames());
176
+ const unreadDeny = unreadDenyOf(inert);
177
+ if (unreadDeny.length > 0)
178
+ throw skillDenyUnreadable(skill.name, unreadDeny, "activation");
136
179
  for (const row of rows)
137
180
  this.hold(row);
138
181
  return inert;
139
182
  }
140
183
  seed(records, origin) {
184
+ const roster = this.mountedNames();
185
+ const rows = [];
186
+ const unreadDeny = [];
141
187
  for (const r of records) {
142
- const compiled = compileAllowEntry(r.entry, r.behavior);
143
- if (typeof compiled === "string")
144
- continue;
145
- this.hold({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled });
188
+ const read = compileAllowList([r.entry], r.behavior, roster);
189
+ const row = read.rows[0];
190
+ if (row !== undefined)
191
+ rows.push({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled: row.compiled });
192
+ else if (r.behavior === "deny")
193
+ unreadDeny.push(...read.inert.map((i) => ({ skill: r.skill, ...i })));
146
194
  }
195
+ const first = unreadDeny[0];
196
+ if (first !== undefined)
197
+ throw skillDenyUnreadable(first.skill, unreadDeny.filter((i) => i.skill === first.skill), "carried");
198
+ for (const row of rows)
199
+ this.hold(row);
147
200
  }
148
201
  records() {
149
202
  return this.rowsHeld.map((r) => ({ skill: r.skill, behavior: r.behavior, entry: r.entry, origin: r.origin }));
@@ -151,9 +204,17 @@ export class SkillGrantTable {
151
204
  lane(ctx) {
152
205
  return {
153
206
  allowing: (req, mode) => judge(allowingRows(this.rowsHeld, req, mode), req, "allow", ctx),
154
- denying: (req) => judge(this.rowsHeld, req, "deny", ctx),
207
+ denying: (req) => judge(this.rowsHeld, req, "deny", ctx) ?? this.activationRefused(req),
155
208
  };
156
209
  }
210
+ activationRefused(req) {
211
+ const at = this.activations;
212
+ if (at === undefined || !isSkillActivationCall(req, at.skills, at.skillToolName))
213
+ return undefined;
214
+ const skill = skillNamedBy(req.args, at.skills);
215
+ const unread = skill === undefined ? [] : unreadDenyOf(compileSkillGrant(skill, "model", this.mountedNames()).inert);
216
+ return skill === undefined || unread.length === 0 ? undefined : { skill: skill.name, entries: unread.map((i) => i.entry), readsAsNothing: unread };
217
+ }
157
218
  }
158
219
  function readCarriedRecords(raw, carrier) {
159
220
  if (raw === undefined)
@@ -190,3 +251,15 @@ export function readFrozenSkillGrants(section) {
190
251
  return readCarriedRecords(section, "frozen");
191
252
  }
192
253
  export const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
254
+ export const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
255
+ function skillDenyUnreadable(skill, entries, site) {
256
+ const listed = entries.map(inertEntryPhrase).join(", ");
257
+ const one = entries.length === 1;
258
+ const name = inlineUntrusted(skill, 120);
259
+ const message = site === "activation"
260
+ ? `Skill "${name}" was not activated: its disallowed-tools entr${one ? `y ${listed} reads` : `ies ${listed} read`} as nothing — a refusal this engine cannot read is not dropped, so the activation does not happen (its instructions are not loaded and nothing it lists is granted). Spell each entry as the tool is mounted, or remove it, in its definition.`
261
+ : `the grant rows carried for Skill "${name}" (a park's frozen rows, or a parent's capture) hold the disallowed-tools entr${one ? `y ${listed}, which reads` : `ies ${listed}, which read`} as nothing for this run — a refusal this engine cannot read is not dropped, so this leg does not start without it. A parked task an earlier release wrote can be finished on a worker of that release; otherwise correct the entry in its disallowed-tools and submit the task again.`;
262
+ const e = new Error(message);
263
+ e.code = SKILL_DENY_UNREADABLE_CODE;
264
+ return e;
265
+ }