@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +10 -2
  3. package/dist/agents/agent-transcript-tool.js +52 -26
  4. package/dist/agents/list-agents-tool.d.ts +17 -2
  5. package/dist/agents/list-agents-tool.js +28 -16
  6. package/dist/agents/peer-session-drain.d.ts +3 -1
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/subagent-steps.d.ts +12 -2
  9. package/dist/agents/subagent-steps.js +8 -4
  10. package/dist/agents/subagent.d.ts +1 -17
  11. package/dist/agents/subagent.js +12 -24
  12. package/dist/core/agent-definition.d.ts +19 -40
  13. package/dist/core/agent-state.d.ts +91 -0
  14. package/dist/core/agent-state.js +79 -0
  15. package/dist/core/ask-origin.d.ts +28 -6
  16. package/dist/core/ask-origin.js +9 -5
  17. package/dist/core/ask-unresolvable-notice.d.ts +27 -4
  18. package/dist/core/ask-unresolvable-notice.js +8 -0
  19. package/dist/core/auto-promote.d.ts +14 -27
  20. package/dist/core/auto-promote.js +5 -5
  21. package/dist/core/checkpoint-store.d.ts +11 -12
  22. package/dist/core/decision-receipt.d.ts +5 -2
  23. package/dist/core/decision-receipt.js +1 -1
  24. package/dist/core/engine-notice.d.ts +7 -1
  25. package/dist/core/gate-crash-trace.d.ts +1 -1
  26. package/dist/core/gate-fold.js +7 -3
  27. package/dist/core/gate-lanes.js +22 -3
  28. package/dist/core/governance-codes.d.ts +3 -1
  29. package/dist/core/governance-codes.js +3 -0
  30. package/dist/core/hooks.d.ts +20 -6
  31. package/dist/core/hooks.js +28 -28
  32. package/dist/core/mcp-tool-meta.d.ts +24 -0
  33. package/dist/core/mcp-tool-meta.js +17 -0
  34. package/dist/core/mcp.d.ts +1 -6
  35. package/dist/core/mcp.js +6 -7
  36. package/dist/core/mode-policy.d.ts +25 -0
  37. package/dist/core/mode-policy.js +8 -0
  38. package/dist/core/protocol-table.d.ts +3 -0
  39. package/dist/core/protocol-table.js +3 -0
  40. package/dist/core/reminder-mint.d.ts +21 -1
  41. package/dist/core/reminder-mint.js +20 -0
  42. package/dist/core/retired-keys.d.ts +2 -1
  43. package/dist/core/retired-keys.js +13 -2
  44. package/dist/core/rule-denial-sentence.d.ts +2 -2
  45. package/dist/core/runner/call-hook-stations.d.ts +106 -0
  46. package/dist/core/runner/call-hook-stations.js +118 -0
  47. package/dist/core/runner/contracts.d.ts +35 -25
  48. package/dist/core/runner/gate-exit.d.ts +5 -1
  49. package/dist/core/runner/gate-exit.js +10 -6
  50. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
  51. package/dist/core/runner/prepare-delegation-surface.js +4 -4
  52. package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
  53. package/dist/core/runner/prepare-gate-stations.js +13 -70
  54. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
  55. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  56. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  57. package/dist/core/runner/prepare-park-ask.js +4 -1
  58. package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
  59. package/dist/core/runner/prepare-policy-chain.js +14 -25
  60. package/dist/core/runner/prepare-project-context.d.ts +15 -16
  61. package/dist/core/runner/prepare-project-context.js +15 -11
  62. package/dist/core/runner/prepare-run-refs.d.ts +8 -8
  63. package/dist/core/runner/prepare-run-refs.js +3 -3
  64. package/dist/core/runner/prepare-safety-scan.js +3 -3
  65. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
  66. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  67. package/dist/core/runner/prepare-task.js +14 -12
  68. package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
  69. package/dist/core/runner/prepare-turn-wiring.js +16 -13
  70. package/dist/core/runner/resume-admission.js +2 -1
  71. package/dist/core/runner/resume-apply.js +12 -23
  72. package/dist/core/runner/resume-checkpoint-screen.js +3 -0
  73. package/dist/core/runner/run-leg.js +61 -47
  74. package/dist/core/runner/runtask.d.ts +1 -1
  75. package/dist/core/runner/session-rule-policy.d.ts +7 -19
  76. package/dist/core/runner/session-rule-policy.js +1 -2
  77. package/dist/core/runner/skill-base-dir.d.ts +10 -0
  78. package/dist/core/runner/skill-base-dir.js +4 -0
  79. package/dist/core/runner/synthetic-tools.d.ts +11 -17
  80. package/dist/core/runner/synthetic-tools.js +7 -8
  81. package/dist/core/safety-axis-vocab.d.ts +2 -2
  82. package/dist/core/safety-axis-vocab.js +2 -0
  83. package/dist/core/session-policy-store.d.ts +63 -11
  84. package/dist/core/session-policy-store.js +52 -6
  85. package/dist/core/skill-grant.d.ts +239 -0
  86. package/dist/core/skill-grant.js +313 -0
  87. package/dist/core/skills-directory.d.ts +5 -34
  88. package/dist/core/skills-directory.js +9 -58
  89. package/dist/core/task-registry-agent.d.ts +14 -2
  90. package/dist/core/task-registry-agent.js +23 -8
  91. package/dist/core/task-registry-shared.d.ts +6 -0
  92. package/dist/core/task-registry.d.ts +16 -0
  93. package/dist/core/task-registry.js +19 -6
  94. package/dist/core/task-spec.d.ts +10 -0
  95. package/dist/core/tool-face.d.ts +3 -2
  96. package/dist/core/tool-name-aliases.d.ts +38 -12
  97. package/dist/core/tool-name-aliases.js +16 -0
  98. package/dist/core/tool-name-readings.d.ts +6 -2
  99. package/dist/core/tool-name-readings.js +6 -4
  100. package/dist/core/tool-policy.d.ts +2 -2
  101. package/dist/core/tool-registry.d.ts +1 -4
  102. package/dist/core/tool-registry.js +0 -3
  103. package/dist/core/tool-spec.d.ts +11 -13
  104. package/dist/core/tools.d.ts +1 -1
  105. package/dist/core/trace.d.ts +15 -0
  106. package/dist/core/types.d.ts +1 -1
  107. package/dist/engine/harness/agent-harness.d.ts +12 -1
  108. package/dist/engine/harness/agent-harness.js +17 -13
  109. package/dist/engine/loop/agent-loop.d.ts +16 -1
  110. package/dist/engine/loop/agent-loop.js +9 -15
  111. package/dist/engine/loop/types.d.ts +2 -0
  112. package/dist/index.d.ts +9 -4
  113. package/dist/index.js +4 -2
  114. package/dist/stores/file/adoption/adopt.js +23 -16
  115. package/dist/stores/file/index.d.ts +8 -2
  116. package/dist/stores/file/index.js +2 -2
  117. package/dist/stores/file/mailbox-store.js +2 -2
  118. package/dist/stores/file/permission-rule-adopt.js +10 -1
  119. package/dist/stores/file/permission-rule-store.d.ts +6 -4
  120. package/dist/stores/file/permission-rule-store.js +127 -59
  121. package/dist/stores/file/session-policy-store.d.ts +86 -14
  122. package/dist/stores/file/session-policy-store.js +135 -118
  123. package/dist/stores/file/task-list-store.d.ts +5 -0
  124. package/dist/stores/file/task-list-store.js +18 -4
  125. package/dist/stores/file/usage-window-store.d.ts +9 -1
  126. package/dist/stores/file/usage-window-store.js +47 -33
  127. package/dist/tools/fs/safety.d.ts +1 -1
  128. package/package.json +1 -1
  129. package/test/export-surface.snapshot.json +45 -5
  130. package/wire-changes/core@7.41.0.json +1865 -0
  131. package/dist/core/runner/active-skill-scope.d.ts +0 -58
  132. package/dist/core/runner/active-skill-scope.js +0 -133
  133. package/dist/core/skill-tool-specifier.d.ts +0 -80
  134. package/dist/core/skill-tool-specifier.js +0 -57
@@ -0,0 +1,239 @@
1
+ import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
2
+ import { type PersistedRule } from "./permission-rule-model.js";
3
+ import type { PermissionMode } from "./ask-origin.js";
4
+ import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
5
+ import type { AgentTool } from "../internal/harness-types.js";
6
+ import type { EngineNotice } from "./engine-notice.js";
7
+ /** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
8
+ * delegated child's seed from its parent. */
9
+ export declare const SKILL_GRANT_ORIGINS: readonly ["model", "user", "inherited"];
10
+ export type SkillGrantOrigin = (typeof SKILL_GRANT_ORIGINS)[number];
11
+ /** The PORTABLE form of one row — what a delegated child captures and what a park checkpoint section freezes: the
12
+ * skill, the list it came from, and the entry as the skill spells it. The compiled reading is re-derived wherever a
13
+ * record is seeded (never trusted off the record). */
14
+ export interface SkillGrantRecord {
15
+ readonly skill: string;
16
+ readonly behavior: "allow" | "deny";
17
+ readonly entry: string;
18
+ readonly origin?: SkillGrantOrigin;
19
+ }
20
+ /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names. */
21
+ export declare const SKILL_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"];
22
+ export type SkillEntryInertReason = (typeof SKILL_ENTRY_INERT_REASONS)[number];
23
+ /** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell
24
+ * command rule (`interpreter` = an interpreter-headed open prefix); the Read directory form; a deny-side path pattern. */
25
+ type CompiledEntry = {
26
+ readonly kind: "tool";
27
+ readonly name: string;
28
+ } | {
29
+ readonly kind: "namespace";
30
+ readonly pattern: string;
31
+ } | {
32
+ readonly kind: "shell";
33
+ readonly name: string;
34
+ } | {
35
+ readonly kind: "command";
36
+ readonly rule: PersistedRule;
37
+ readonly interpreter: boolean;
38
+ } | {
39
+ readonly kind: "read_dir";
40
+ readonly rule: PersistedRule;
41
+ } | {
42
+ readonly kind: "path";
43
+ readonly rule: PersistedRule;
44
+ };
45
+ /** One row of the table. */
46
+ export interface SkillGrantRow {
47
+ readonly skill: string;
48
+ readonly origin: SkillGrantOrigin;
49
+ readonly behavior: "allow" | "deny";
50
+ readonly entry: string;
51
+ readonly compiled: CompiledEntry;
52
+ }
53
+ /** An entry the grammar reads as granting or refusing nothing. */
54
+ export interface SkillInertEntry {
55
+ readonly skill: string;
56
+ readonly list: "allowed-tools" | "disallowed-tools";
57
+ readonly entry: string;
58
+ readonly reason: SkillEntryInertReason;
59
+ }
60
+ /** The two lists a skill declares (the shape of a `SkillSpec`, read by name). */
61
+ export interface SkillGrantSource {
62
+ readonly name: string;
63
+ readonly allowedTools?: readonly string[];
64
+ readonly disallowedTools?: readonly string[];
65
+ }
66
+ /**
67
+ * The skill a `Skill` call's arguments name among `skills`: the definition whose `name` equals `args.skill` exactly —
68
+ * case-sensitive, no alias, no trimming, first of a name (a prepared table is already deduplicated) — or undefined (no such
69
+ * name, or a `skill` member that is not a string). The ONE resolution: the `Skill` tool activates through it, the card is
70
+ * asked through it ({@link skillSeatReader}), a parked approval is bound and judged through it ({@link skillGrantDigest}) and
71
+ * the person's invoked names are read through it ({@link seedSkillGrants}).
72
+ */
73
+ export declare function skillNamedBy<T extends Pick<SkillGrantSource, "name">>(args: unknown, skills: readonly T[]): T | undefined;
74
+ /**
75
+ * The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases: the skill's name
76
+ * as the table spells it ({@link skillNamedBy} resolved it, so it equals the call's `skill` exactly) and its two lists in the
77
+ * rule grammar's canonical spelling (`Tool()` / `Tool(*)` read as the bare name), de-duplicated and sorted, hashed as the
78
+ * approval binding is hashed (`boundInputHashOf`). `undefined` — no skill named — digests as a nameless skill declaring
79
+ * nothing (activating it grants nothing). Any change to the name or to either list changes it; the body does not. The read
80
+ * point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation).
81
+ */
82
+ export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" | "allowedTools" | "disallowedTools"> | undefined): string;
83
+ /**
84
+ * The digest a parked call to the engine's `Skill` tool binds — written by the park beside the row's input binding — or
85
+ * undefined for any other call, and when no skill is loaded (no engine `Skill` tool is mounted, so a call by that name is
86
+ * not an activation).
87
+ */
88
+ export declare function skillCallGrantDigest(call: {
89
+ readonly toolName: string;
90
+ }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): string | undefined;
91
+ /**
92
+ * The approving resume of a parked call, judged against the card it was asked under. A row is a `Skill` approval when it
93
+ * carries a digest (the park said so), or when the resume would run the engine's `Skill` tool on it (skills loaded, the
94
+ * row's wire name). For such a row: the refusal sentence when the skill the arguments name — as the resume's skills define
95
+ * it, none loaded included — does not digest as the row's `boundGrantDigest`, or the row carries none (parked before the
96
+ * binding); undefined otherwise. ONE judgement (the digest), judged at two sites: the claim (`site: "claim"`, the decided
97
+ * bytes — the row stays pending: deny it, then activate again) and the resumed execution boundary (`site: "execution"`, the
98
+ * bytes that will run, after every station that can rewrite them — the approval is spent: the call does not run). The
99
+ * sentence says which side of the digest differs, read off the row's own arguments: the arguments name another skill than
100
+ * the card's (`row.args`), or the card's skill no longer declares the lists the card showed (or is not served). Names the
101
+ * skills, never a path; says the way out.
102
+ */
103
+ export declare function parkedSkillApprovalRefusal(row: {
104
+ readonly toolName: string;
105
+ readonly args: unknown;
106
+ readonly boundGrantDigest?: string;
107
+ }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string, site?: "claim" | "execution"): string | undefined;
108
+ /** A resume's skills read ONCE: each definition copied with its two lists, so the parked Skill approval's binding and the
109
+ * resumed leg's prepare read one value — a later edit to the caller's own objects or arrays reaches neither. */
110
+ export declare function detachSkillLists<T extends SkillGrantSource>(skills: readonly T[]): T[];
111
+ /** Does this skill declare a list (either, non-empty)? The one test of "the Skill card is asked for this skill". */
112
+ export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowedTools" | "disallowedTools">): boolean;
113
+ /**
114
+ * Compile one skill's two lists into rows (`origin` stamped on each) and the entries that read as nothing. An allow list
115
+ * holding `*` voids that list whole (every other entry on it is reported too: the list grants nothing — the safe direction).
116
+ * A `*` on the deny list reads as nothing by itself and the rest of that list still refuses: a deny list never loses an
117
+ * entry to a wildcard beside it.
118
+ */
119
+ export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin): {
120
+ rows: SkillGrantRow[];
121
+ inert: SkillInertEntry[];
122
+ };
123
+ /** The call facts a content row is judged against — the persisted lane's own bases. */
124
+ export interface SkillGrantJudgeContext {
125
+ readonly root: string | undefined;
126
+ readonly sessionId: string | undefined;
127
+ readonly liveCwd: () => string | undefined;
128
+ readonly home: string | undefined;
129
+ }
130
+ /** What spoke for a call: the skill and the entries it stands on. */
131
+ export interface SkillGrantHit {
132
+ readonly skill: string;
133
+ readonly entries: readonly string[];
134
+ }
135
+ /** The refusal a deny row writes. */
136
+ export declare function skillDenyDecision(hit: SkillGrantHit): PermissionResult;
137
+ /** The run's deny rows over `req` through `lane` — the refusal they write, or undefined (no lane, no row). The one
138
+ * reading the gate's regular decision, its approved-edit round and the resumed execution boundary share. */
139
+ export declare function skillRowRefusal(lane: Pick<SkillGrantLane, "denying"> | undefined, req: ToolCallRequest): PermissionResult | undefined;
140
+ /** The allow a grant writes (the allow layer's skill member). */
141
+ export declare function skillAllowDecision(hit: SkillGrantHit, updatedInput: unknown): PermissionResult;
142
+ /**
143
+ * The Skill card's question: the reference implementation's sentence (`Execute skill: <name>`), and — when approving
144
+ * releases anything — the entries it releases for the rest of the run in this mode (an entry that grants nothing, or
145
+ * stands down in this mode, is not named). Deployment text, sanitized and capped.
146
+ */
147
+ export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined): string;
148
+ /** The face the gate and the resumed execution boundary read the table through, bound to the leg's call facts. */
149
+ export interface SkillGrantLane {
150
+ /** The allow layer's skill member: what grants this call in this mode, or undefined. */
151
+ allowing(req: ToolCallRequest, mode: PermissionMode | undefined): SkillGrantHit | undefined;
152
+ /** The run's deny rows: what refuses this call, or undefined. */
153
+ denying(req: ToolCallRequest): SkillGrantHit | undefined;
154
+ }
155
+ /** The gate inputs the run's skill rows ride (a member group of `ToolGateInput`). */
156
+ export interface SkillGateSeats {
157
+ /**
158
+ * The active skills' rows for this run. DENY rows (a skill's `disallowed-tools`) are read where a person's deny rows are
159
+ * read — on every decision right after them, and on an approved edit's bytes — and refuse with `decisionReason: "skill"`
160
+ * (`deniedBy: "policy"`). ALLOW rows are the allow layer's member beside a person's stored allow rows, under the same
161
+ * consumption predicate: they clear only an ask an allow rule may clear (never a mandated one, a hook's, an ask rule's,
162
+ * governance's or a marked call's) and are not re-read on an approved edit. `onAllowed` observes a call they cleared (the
163
+ * allow-side attribution; never affects the outcome). Absent ⇒ no skill rows.
164
+ */
165
+ skillGrants?: SkillGrantLane & {
166
+ onAllowed?: (info: {
167
+ toolName: string;
168
+ toolCallId: string;
169
+ skill: string;
170
+ entries: readonly string[];
171
+ }) => void;
172
+ };
173
+ }
174
+ /** The `Skill` tool's per-call seat over the loaded skills: a skill that declares a list states no permission of its own. */
175
+ export type SkillSeatReader = (args: unknown, mode: PermissionMode | undefined) => PermissionSeatReading | undefined;
176
+ /** The reader over these skills (`undefined` when none is loaded): `Execute skill: <name>` for a skill declaring a list. */
177
+ export declare function skillSeatReader(skills: readonly SkillGrantSource[]): SkillSeatReader | undefined;
178
+ /**
179
+ * The gate's seat inputs for one call, composed in ONE place: the mounted instance's static seat (`calledName`), the `Skill`
180
+ * tool's per-call reader in the leg's live mode (only when the call's wire name is the engine's `Skill` — `skillToolName`),
181
+ * and the run's skill rows with their attribution sink.
182
+ */
183
+ export declare function skillGateWiring(opts: {
184
+ tools: readonly AgentTool[];
185
+ skillToolName: string;
186
+ seatOf: SkillSeatReader | undefined;
187
+ mode: () => PermissionMode | undefined;
188
+ lane: SkillGrantLane;
189
+ onAllowed: NonNullable<SkillGateSeats["skillGrants"]>["onAllowed"];
190
+ }): (calledName: string, wireName: string) => PermissionSeatInputs & SkillGateSeats;
191
+ /**
192
+ * Seed a task's table at prepare, before any call is gated: the rows from outside this leg's own writers (`inherited` — a
193
+ * parent's capture, held until this child ends, and a resumed leg's frozen rows, which keep the word `model` where the
194
+ * model wrote them) and the skills the person invoked by name (`user` — the invocation is the statement, so no card). Every invoked
195
+ * name must be one of the loaded skills; anything else (or a value that is not a list of names) refuses the run loudly
196
+ * (`config.user_invoked_skill_unknown`), never dropped. Returns the entries the grammar reads as nothing, across every
197
+ * loaded skill — the start-of-run notice's content.
198
+ */
199
+ export declare function seedSkillGrants(table: SkillGrantTable, opts: {
200
+ skills: readonly SkillGrantSource[];
201
+ userInvokedSkills: unknown;
202
+ inherited: readonly SkillGrantRecord[] | undefined;
203
+ }): readonly SkillInertEntry[];
204
+ /** The per-task table. Identity is the contract: the `Skill` tool, prepare's seeds, the tool ctx's snapshot and the
205
+ * lane all hold this one object. */
206
+ export declare class SkillGrantTable {
207
+ private rowsHeld;
208
+ /** The table never holds the same row twice (skill, origin, list, entry) — one rule for both writers: a re-activation, a
209
+ * capture re-supplied beside the frozen rows that already carry it. Keyed on the ROW, not the skill: a definition that
210
+ * changed across a park writes its new rows when it is activated again. */
211
+ private hold;
212
+ /** Write a skill's rows (rows already held are not held twice). Returns the entries that read as nothing. */
213
+ activate(skill: SkillGrantSource, origin: SkillGrantOrigin): readonly SkillInertEntry[];
214
+ /** Seed portable records (a delegated child's capture, a resumed leg's frozen rows): a record the model wrote keeps that
215
+ * word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. */
216
+ seed(records: readonly SkillGrantRecord[], origin: SkillGrantOrigin): void;
217
+ /** The rows as portable records (a copy). */
218
+ records(): readonly SkillGrantRecord[];
219
+ /** The lane over this table, bound to the leg's call facts. */
220
+ lane(ctx: SkillGrantJudgeContext): SkillGrantLane;
221
+ }
222
+ /** Read a delegated child's capture ({@link readCarriedRecords}): `undefined` refuses the delegation. */
223
+ export declare function readSkillGrantCapture(snapshot: readonly unknown[] | undefined): readonly SkillGrantRecord[] | undefined;
224
+ /** The section a park freezes (`CheckpointState.skillGrants`): the table's rows under the frozen origins, or undefined
225
+ * when there are none (an absent section IS zero rows on every reader). */
226
+ export declare function frozenSkillGrants(table: Pick<SkillGrantTable, "records">): SkillGrantRecord[] | undefined;
227
+ /** Read a park's frozen section ({@link readCarriedRecords}): absent ⇒ none; `undefined` ⇒ damaged — the resume refuses
228
+ * it before the claim and prepare never seeds from it. */
229
+ export declare function readFrozenSkillGrants(section: unknown): readonly SkillGrantRecord[] | undefined;
230
+ /** The start-of-run notice for entries that grant (or refuse) nothing. */
231
+ export declare const SKILL_ENTRY_NOT_GRANTED_CODE = "skills.allowed_tool_not_granted";
232
+ /** The refusal for a `TaskSpec.userInvokedSkills` name the run does not carry (or a value that is not a list of names). */
233
+ export declare const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
234
+ /**
235
+ * The notice for entries this grammar reads as nothing — one per prepared task, delivered only when one exists. The
236
+ * deployment's operator owns the skill files; the run goes on (an entry that grants nothing makes it ask more).
237
+ */
238
+ export declare function skillEntriesNotGrantedNotice(inert: readonly SkillInertEntry[], sessionId: string): EngineNotice;
239
+ export {};
@@ -0,0 +1,313 @@
1
+ import { heldBy, wireOf } from "./tool-name-readings.js";
2
+ import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
3
+ import { protocolOf } from "./protocol-table.js";
4
+ import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
5
+ import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
6
+ import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
7
+ import { mountedToolPermissionSeat } from "./mode-policy.js";
8
+ import { inlineUntrusted } from "./untrusted-text.js";
9
+ import { boundInputHashOf } from "./canonical-json.js";
10
+ export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
11
+ const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
12
+ export const SKILL_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]);
13
+ export function skillNamedBy(args, skills) {
14
+ const named = args?.skill;
15
+ return typeof named === "string" ? skills.find((s) => s.name === named) : undefined;
16
+ }
17
+ export function skillGrantDigest(skill) {
18
+ const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
19
+ const p = parsePermissionRule(e);
20
+ return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
21
+ }))].sort();
22
+ return boundInputHashOf({ skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) });
23
+ }
24
+ export function skillCallGrantDigest(call, args, skills, skillToolName) {
25
+ return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName ? skillGrantDigest(skillNamedBy(args, skills)) : undefined;
26
+ }
27
+ export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
28
+ if (row.boundGrantDigest === undefined && skillCallGrantDigest(row, args, skills, skillToolName) === undefined)
29
+ return undefined;
30
+ if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
31
+ return undefined;
32
+ const nameIn = (a) => a?.skill;
33
+ const shown = (n) => inlineUntrusted(typeof n === "string" ? n : String(n), 120);
34
+ const chosen = shown(nameIn(args));
35
+ const why = row.boundGrantDigest === undefined
36
+ ? `this parked Skill activation of "${chosen}" carries no digest of the allowed-tools / disallowed-tools it was asked under (it was parked before Skill approvals bound them), so it cannot cover what the activation grants or refuses`
37
+ : nameIn(args) !== nameIn(row.args)
38
+ ? `this Skill approval was given on the card for "${shown(nameIn(row.args))}", and ${site === "claim" ? "this decision selects" : "the call would now activate"} "${chosen}" — not the Skill that card asked about, so the approval does not cover activating it`
39
+ : `"${chosen}" no longer declares the allowed-tools / disallowed-tools this Skill approval was asked under — as this resume serves it (one it does not load counts as absent), it changed after the approval was given, so the approval does not cover what activating it would now grant or refuse`;
40
+ return site === "claim"
41
+ ? `${why} — refusing pre-CAS (nothing ran; the checkpoint stays pending). Decide this checkpoint as deny and re-activate it: the new card shows the current lists.`
42
+ : `The approved Skill call was not executed: ${why}. This approval is spent — to use "${chosen}", have the model activate it again: the new card shows its lists.`;
43
+ }
44
+ export function detachSkillLists(skills) {
45
+ return skills.map((s) => (typeof s === "object" && s !== null ? { ...s, ...(Array.isArray(s.allowedTools) ? { allowedTools: [...s.allowedTools] } : {}), ...(Array.isArray(s.disallowedTools) ? { disallowedTools: [...s.disallowedTools] } : {}) } : s));
46
+ }
47
+ export function declaresSkillLists(skill) {
48
+ return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
49
+ }
50
+ function ruleRowOf(parsed) {
51
+ return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "skill", counter: 1 }, origin: "user", createdAt: "" }] };
52
+ }
53
+ function compileEntry(raw, behavior) {
54
+ const parsed = parsePermissionRule(raw);
55
+ const name = parsed.toolName;
56
+ if (name === "*")
57
+ return "wildcard_all";
58
+ if (parsed.ruleContent === undefined) {
59
+ const ns = protocolOf(name);
60
+ if (ns !== undefined) {
61
+ if (name.slice(ns.prefix.length).split("__")[0]?.includes("*") === true)
62
+ return "namespace_wildcard";
63
+ if (isNamespacedCoveringRuleName(name))
64
+ return { kind: "namespace", pattern: name };
65
+ return { kind: "tool", name };
66
+ }
67
+ return COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name };
68
+ }
69
+ if (name === COMMAND_RULE_TOOL) {
70
+ const allowRead = parseRuleText(raw, behavior);
71
+ if ("rule" in allowRead)
72
+ return { kind: "command", rule: ruleRowOf(allowRead.rule), interpreter: false };
73
+ if (behavior === "allow" && allowRead.reject.code === "invalid.bare_interpreter_prefix") {
74
+ const wide = parseRuleText(raw, "deny");
75
+ if ("rule" in wide)
76
+ return { kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }), interpreter: true };
77
+ }
78
+ return "grammar";
79
+ }
80
+ if (ruleToolGrammarOf(name) === "path") {
81
+ const read = parseRuleText(raw, behavior);
82
+ if ("rule" in read) {
83
+ if (read.rule.match === "subpath" && read.rule.tool === READ_RULE_TOOL && behavior === "allow")
84
+ return { kind: "read_dir", rule: ruleRowOf(read.rule) };
85
+ if (behavior === "deny")
86
+ return { kind: "path", rule: ruleRowOf(read.rule) };
87
+ }
88
+ return "path_pattern";
89
+ }
90
+ if (/^domain:/.test(parsed.ruleContent))
91
+ return "domain";
92
+ return "content_unsupported";
93
+ }
94
+ export function compileSkillGrant(skill, origin) {
95
+ const rows = [];
96
+ const inert = [];
97
+ for (const [list, behavior, entries] of [
98
+ ["allowed-tools", "allow", skill.allowedTools ?? []],
99
+ ["disallowed-tools", "deny", skill.disallowedTools ?? []],
100
+ ]) {
101
+ const voided = behavior === "allow" && entries.some((e) => parsePermissionRule(e).toolName === "*");
102
+ for (const entry of entries) {
103
+ const compiled = voided ? "wildcard_all" : compileEntry(entry, behavior);
104
+ if (typeof compiled === "string")
105
+ inert.push({ skill: skill.name, list, entry, reason: compiled });
106
+ else
107
+ rows.push({ skill: skill.name, origin, behavior, entry, compiled });
108
+ }
109
+ }
110
+ return { rows, inert };
111
+ }
112
+ function standsDownIn(row, mode, req) {
113
+ if (mode !== "auto")
114
+ return false;
115
+ if (row.compiled.kind === "shell" || (row.compiled.kind === "command" && row.compiled.interpreter))
116
+ return true;
117
+ return row.compiled.kind === "tool" && req !== undefined && answersTo(req, (n) => COARSE_SHELL_TOOLS.includes(n));
118
+ }
119
+ function answersTo(req, has) {
120
+ return heldBy("skillGrant.names", { has }, req);
121
+ }
122
+ function nameRowReaches(compiled, req) {
123
+ const covers = compiled.kind === "namespace" ? (n) => namespacedRuleNameCovers(compiled.pattern, n) : compiled.kind === "tool" || compiled.kind === "shell" ? (n) => n === compiled.name : undefined;
124
+ return covers !== undefined && answersTo(req, covers);
125
+ }
126
+ function commandOf(req) {
127
+ const tool = laneToolNamesOf(req).find((n) => ruleToolGrammarOf(n) === "command");
128
+ const command = req.args?.command;
129
+ return tool !== undefined && typeof command === "string" ? { tool, command } : undefined;
130
+ }
131
+ function judge(rows, req, behavior, ctx) {
132
+ const live = rows.filter((r) => r.behavior === behavior);
133
+ if (live.length === 0)
134
+ return undefined;
135
+ for (const row of live)
136
+ if (nameRowReaches(row.compiled, req))
137
+ return { skill: row.skill, entries: [row.entry] };
138
+ const call = commandOf(req);
139
+ const liveCwd = ctx.liveCwd();
140
+ if (call !== undefined) {
141
+ const byRule = new Map();
142
+ for (const r of live)
143
+ if (r.compiled.kind === "command")
144
+ byRule.set(r.compiled.rule, r);
145
+ if (behavior === "allow") {
146
+ const covering = findAdmittingRule([...byRule.keys()], { tool: call.tool, command: call.command, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}), ...(liveCwd !== undefined ? { execCwd: liveCwd } : {}) });
147
+ const hits = (covering ?? []).flatMap((rule) => byRule.get(rule) ?? []);
148
+ const first = hits[0];
149
+ if (first !== undefined)
150
+ return { skill: first.skill, entries: hits.map((r) => r.entry) };
151
+ }
152
+ else {
153
+ for (const [rule, row] of byRule) {
154
+ if (programRunReachOf(rule, call.command).reach !== "not_reached")
155
+ return { skill: row.skill, entries: [row.entry] };
156
+ }
157
+ }
158
+ }
159
+ const pathKind = behavior === "allow" ? "read_dir" : "path";
160
+ const pathRows = live.filter((r) => r.compiled.kind === pathKind);
161
+ if (pathRows.length > 0) {
162
+ const table = pathRows.map((r) => r.compiled.rule);
163
+ for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === "path")) {
164
+ const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, liveCwd, home: ctx.home });
165
+ const spoke = answer !== undefined && ("unreadable" in answer ? behavior === "deny" : "behavior" in answer && answer.behavior === behavior);
166
+ if (spoke) {
167
+ const rule = answer !== undefined && "rules" in answer ? answer.rules[0]?.rule : undefined;
168
+ const row = pathRows.find((r) => r.compiled.rule.rule === rule) ?? pathRows[0];
169
+ return { skill: row.skill, entries: [row.entry] };
170
+ }
171
+ }
172
+ }
173
+ return undefined;
174
+ }
175
+ export function skillDenyDecision(hit) {
176
+ return {
177
+ action: "deny",
178
+ 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`,
179
+ decisionReason: "skill",
180
+ };
181
+ }
182
+ export function skillRowRefusal(lane, req) {
183
+ const hit = lane?.denying(req);
184
+ return hit === undefined ? undefined : skillDenyDecision(hit);
185
+ }
186
+ export function skillAllowDecision(hit, updatedInput) {
187
+ return {
188
+ action: "allow",
189
+ message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in allowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")})`,
190
+ decisionReason: "skill",
191
+ ...(updatedInput !== undefined ? { updatedInput } : {}),
192
+ };
193
+ }
194
+ const CARD_ENTRIES_MAX = 10;
195
+ const CARD_ENTRY_MAX_CHARS = 120;
196
+ export function skillActivationQuestion(skill, mode) {
197
+ const head = `Execute skill: ${inlineUntrusted(skill.name, 200)}`;
198
+ const granting = compileSkillGrant(skill, "model").rows.filter((r) => r.behavior === "allow" && !standsDownIn(r, mode));
199
+ if (granting.length === 0)
200
+ return head;
201
+ const shown = granting.slice(0, CARD_ENTRIES_MAX).map((r) => inlineUntrusted(r.entry, CARD_ENTRY_MAX_CHARS));
202
+ const rest = granting.length - shown.length;
203
+ return `${head}\nApproving also lets these tools be used without asking for the rest of this run: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
204
+ }
205
+ export function skillSeatReader(skills) {
206
+ if (skills.length === 0)
207
+ return undefined;
208
+ return (args, mode) => {
209
+ const skill = skillNamedBy(args, skills);
210
+ return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode) } : undefined;
211
+ };
212
+ }
213
+ export function skillGateWiring(opts) {
214
+ const skillGrants = { ...opts.lane, ...(opts.onAllowed !== undefined ? { onAllowed: opts.onAllowed } : {}) };
215
+ return (calledName, wireName) => ({
216
+ ...(mountedToolPermissionSeat(opts.tools, calledName) === "passthrough" ? { permissionSeat: "passthrough" } : {}),
217
+ ...(opts.seatOf !== undefined && wireName === opts.skillToolName ? { permissionSeatOf: (args) => opts.seatOf?.(args, opts.mode()) } : {}),
218
+ skillGrants,
219
+ });
220
+ }
221
+ export function seedSkillGrants(table, opts) {
222
+ table.seed(opts.inherited ?? [], "inherited");
223
+ const named = (n) => skillNamedBy({ skill: n }, opts.skills);
224
+ const invoked = opts.userInvokedSkills;
225
+ if (invoked !== undefined) {
226
+ const unknown = Array.isArray(invoked) ? invoked.filter((n) => named(n) === undefined) : [invoked];
227
+ if (unknown.length > 0) {
228
+ const e = new Error(`TaskSpec.userInvokedSkills names ${unknown.map((n) => JSON.stringify(n)).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a skill this run carries (the loaded skills: ${opts.skills.map((s) => s.name).join(", ") || "none"}) — refusing rather than running without the invocation`);
229
+ e.code = USER_INVOKED_SKILL_UNKNOWN_CODE;
230
+ throw e;
231
+ }
232
+ for (const name of invoked)
233
+ table.activate(named(name), "user");
234
+ }
235
+ return opts.skills.flatMap((s) => compileSkillGrant(s, "model").inert);
236
+ }
237
+ export class SkillGrantTable {
238
+ rowsHeld = [];
239
+ hold(row) {
240
+ if (!this.rowsHeld.some((r) => r.skill === row.skill && r.origin === row.origin && r.behavior === row.behavior && r.entry === row.entry))
241
+ this.rowsHeld.push(row);
242
+ }
243
+ activate(skill, origin) {
244
+ const { rows, inert } = compileSkillGrant(skill, origin);
245
+ for (const row of rows)
246
+ this.hold(row);
247
+ return inert;
248
+ }
249
+ seed(records, origin) {
250
+ for (const r of records) {
251
+ const compiled = compileEntry(r.entry, r.behavior);
252
+ if (typeof compiled === "string")
253
+ continue;
254
+ this.hold({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled });
255
+ }
256
+ }
257
+ records() {
258
+ return this.rowsHeld.map((r) => ({ skill: r.skill, behavior: r.behavior, entry: r.entry, origin: r.origin }));
259
+ }
260
+ lane(ctx) {
261
+ return {
262
+ allowing: (req, mode) => judge(this.rowsHeld.filter((r) => !standsDownIn(r, mode, req)), req, "allow", ctx),
263
+ denying: (req) => judge(this.rowsHeld, req, "deny", ctx),
264
+ };
265
+ }
266
+ }
267
+ function readCarriedRecords(raw, carrier) {
268
+ if (raw === undefined)
269
+ return [];
270
+ if (!Array.isArray(raw))
271
+ return undefined;
272
+ const out = [];
273
+ for (const r of raw) {
274
+ if (typeof r !== "object" || r === null)
275
+ return undefined;
276
+ const { skill, behavior, entry, origin } = r;
277
+ if (typeof skill !== "string" || typeof entry !== "string" || (behavior !== "allow" && behavior !== "deny"))
278
+ return undefined;
279
+ if (typeof compileEntry(entry, behavior) === "string")
280
+ return undefined;
281
+ if (carrier === "capture") {
282
+ out.push({ skill, behavior, entry });
283
+ continue;
284
+ }
285
+ if (!FROZEN_SKILL_GRANT_ORIGINS.has(origin))
286
+ return undefined;
287
+ out.push({ skill, behavior, entry, origin: origin });
288
+ }
289
+ return out;
290
+ }
291
+ export function readSkillGrantCapture(snapshot) {
292
+ return readCarriedRecords(snapshot, "capture");
293
+ }
294
+ export function frozenSkillGrants(table) {
295
+ const rows = table.records().filter((r) => FROZEN_SKILL_GRANT_ORIGINS.has(r.origin));
296
+ return rows.length > 0 ? rows : undefined;
297
+ }
298
+ export function readFrozenSkillGrants(section) {
299
+ return readCarriedRecords(section, "frozen");
300
+ }
301
+ export const SKILL_ENTRY_NOT_GRANTED_CODE = "skills.allowed_tool_not_granted";
302
+ export const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
303
+ const NOTICE_ENTRIES_SHOWN = 5;
304
+ export function skillEntriesNotGrantedNotice(inert, sessionId) {
305
+ const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${inlineUntrusted(e.skill, 80)}: ${e.list} "${inlineUntrusted(e.entry, 120)}" (${e.reason})`);
306
+ const rest = inert.length - shown.length;
307
+ return {
308
+ code: SKILL_ENTRY_NOT_GRANTED_CODE,
309
+ message: `${inert.length} skill list entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
310
+ "An allowed-tools list holding `*` voids the list, `mcp__*` names no server, and a write-path or domain entry has no allow reading; name the tools (or `mcp__<server>`) instead.",
311
+ detail: { sessionId, entries: inert.map((e) => ({ skill: e.skill, list: e.list, entry: e.entry, reason: e.reason })) },
312
+ };
313
+ }
@@ -18,7 +18,11 @@
18
18
  * directory third parties can write to is the same as pasting their text into the system prompt. The
19
19
  * loader validates SHAPE (spec conformance), never intent.
20
20
  *
21
- * **`allowed-tools` is mapped TIGHTEN-ONLY** — see {@link SkillsDirectoryOptions.deployedTools}.
21
+ * **`allowed-tools` / `disallowed-tools` are COPIED, never judged.** Each frontmatter list reaches
22
+ * {@link SkillSpec.allowedTools} / {@link SkillSpec.disallowedTools} entry by entry as the author wrote it — no
23
+ * intersection with a roster, no projection onto a tool name, no refusal, no de-duplication. What an entry means
24
+ * (a tool, a namespace, a command rule, a path, a wildcard that grants nothing) is read in ONE place, the run's
25
+ * grant table, which announces what it cannot read; a loader that also judged would be a second reader of the grammar.
22
26
  *
23
27
  * Frontmatter is parsed by a deliberately closed subset reader (no YAML dependency — the runtime
24
28
  * dependency budget is a standing constraint): top-level `key: value` scalars, optional surrounding
@@ -42,17 +46,8 @@ export type SkillsDirectoryWarningCode =
42
46
  | "name_mismatch"
43
47
  /** Required `description` absent. */
44
48
  | "missing_description"
45
- /** An `allowed-tools` entry carries a pattern specifier (`Bash(git:*)`) — the scope policy
46
- * matches names, not patterns, so the entry cannot match and the tool is DISABLED in this skill
47
- * frame. Disclosed loudly until pattern enforcement lands (the entry is deliberately NOT widened to
48
- * its base name: that would turn "only git" into "all of Bash"). */
49
- | "allowed_tool_pattern_unsupported"
50
49
  /** `description` exceeds the spec cap. */
51
50
  | "description_too_long"
52
- /** An `allowed-tools` entry the deployment does not mount — dropped (tighten-only). */
53
- | "allowed_tool_not_mounted"
54
- /** A `disallowed-tools` declaration this mapping cannot carry — see {@link SkillsDirectoryOptions.deployedTools}. */
55
- | "disallowed_tools_unenforced"
56
51
  /** An attachment was left out (non-text bytes, over budget, unreadable, or resolving outside the skill). */
57
52
  | "attachment_skipped"
58
53
  /** A file or directory entry existed but could not be read/resolved. */
@@ -66,30 +61,6 @@ export interface SkillsDirectoryWarning {
66
61
  detail: string;
67
62
  }
68
63
  export interface SkillsDirectoryOptions {
69
- /**
70
- * The tool names this deployment actually mounts. When given, a skill's `allowed-tools` declaration is
71
- * INTERSECTED with it and every name outside is dropped with an `allowed_tool_not_mounted` warning.
72
- *
73
- * 🔴 **Tighten-only, by ruling.** `allowed-tools` is a skill-authored capability claim and the format
74
- * marks it experimental; sema's permission discipline is tighten-only, so it is mapped exclusively onto
75
- * {@link SkillManifest} — a structure that can only SUBTRACT capability (the scope policy returns
76
- * `allow`/`deny` and composes ahead of the task policy under a deny-short-circuit fold, so it can never
77
- * re-grant what the task withheld). Consequences worth stating plainly:
78
- * - a name outside `deployedTools` is dropped, NOT honored — a skill cannot pull in a tool the
79
- * deployment never mounted;
80
- * - when the intersection comes out EMPTY the manifest is still emitted with an empty `allowTools`
81
- * (narrow to nothing while the skill's frame is active). Dropping the manifest instead would widen
82
- * the run relative to what the skill asked for, which is the one direction this mapping may not take;
83
- * - omitting `deployedTools` skips the filter. Still safe — an unmounted name simply never matches a
84
- * call — but the warning is then unavailable, so a deployment that wants the audit trail should pass it.
85
- *
86
- * Names are compared in CANONICAL form on both sides (the same normalization the enforcing policy
87
- * applies), so passing this option can never produce a stricter result than omitting it: a declared
88
- * legacy alias and the mounted current name are one tool here exactly as they are at call time.
89
- * A wildcard declaration (`allowed-tools: "*"`) claims the whole roster and therefore narrows by
90
- * nothing — no manifest is emitted, leaving the task policy untouched.
91
- */
92
- deployedTools?: readonly string[];
93
64
  /** Sink for non-fatal diagnostics. Absent ⇒ warnings are dropped and the load still proceeds. */
94
65
  onWarning?: (warning: SkillsDirectoryWarning) => void;
95
66
  /**