@sema-agent/core 7.41.0 → 7.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/dist/agents/subagent.js +2 -0
- package/dist/core/agent-definition.d.ts +1 -1
- package/dist/core/allow-entry.d.ts +241 -0
- package/dist/core/allow-entry.js +340 -0
- package/dist/core/allow-rule-shadow.d.ts +18 -0
- package/dist/core/allow-rule-shadow.js +1 -0
- package/dist/core/ask-origin.d.ts +5 -5
- package/dist/core/ask-origin.js +1 -1
- package/dist/core/auto-mode.d.ts +3 -2
- package/dist/core/checkpoint-store.d.ts +11 -19
- package/dist/core/checkpoint-store.js +1 -1
- package/dist/core/decision-receipt.d.ts +4 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/engine-notice.d.ts +8 -5
- package/dist/core/gate-fold.js +1 -1
- package/dist/core/gate-lanes.d.ts +1 -49
- package/dist/core/gate-lanes.js +85 -223
- package/dist/core/governance-codes.d.ts +3 -3
- package/dist/core/governance-codes.js +3 -3
- package/dist/core/hooks.d.ts +13 -10
- package/dist/core/hooks.js +5 -3
- package/dist/core/mode-policy.d.ts +3 -3
- package/dist/core/mode-policy.js +2 -2
- package/dist/core/permission-rules.d.ts +24 -23
- package/dist/core/permission-rules.js +37 -33
- package/dist/core/reminder-disclosure.d.ts +53 -2
- package/dist/core/reminder-disclosure.js +49 -36
- package/dist/core/rule-lane-reads.d.ts +84 -0
- package/dist/core/rule-lane-reads.js +164 -0
- package/dist/core/runner/contracts.d.ts +4 -2
- package/dist/core/runner/gate-exit.d.ts +3 -3
- package/dist/core/runner/image.js +4 -3
- package/dist/core/runner/inline-image.d.ts +14 -0
- package/dist/core/runner/inline-image.js +26 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
- package/dist/core/runner/permission-rule-lanes.js +1 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +0 -19
- package/dist/core/runner/prepare-ask-lane.js +2 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-park-ask.js +3 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +12 -0
- package/dist/core/runner/prepare-policy-chain.js +10 -9
- package/dist/core/runner/prepare-project-context.d.ts +13 -3
- package/dist/core/runner/prepare-project-context.js +5 -4
- package/dist/core/runner/prepare-task.js +8 -5
- package/dist/core/runner/prepare-turn-wiring.d.ts +16 -3
- package/dist/core/runner/prepare-turn-wiring.js +19 -18
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner-deps.d.ts +11 -0
- package/dist/core/skill-grant.d.ts +8 -47
- package/dist/core/skill-grant.js +15 -136
- package/dist/core/synthetic-read-verdict.d.ts +108 -0
- package/dist/core/synthetic-read-verdict.js +111 -0
- package/dist/core/task-spec.d.ts +18 -0
- package/dist/core/tool-name-readings.d.ts +1 -1
- package/dist/core/tool-name-readings.js +1 -1
- package/dist/core/tool-policy.d.ts +25 -24
- package/dist/core/tool-policy.js +26 -17
- package/dist/core/tool-spec.d.ts +7 -0
- package/dist/core/trace.d.ts +16 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/tools/fs/dir-entries.d.ts +69 -0
- package/dist/tools/fs/dir-entries.js +60 -0
- package/dist/tools/fs/fs-pdf.d.ts +7 -11
- package/dist/tools/fs/fs-pdf.js +5 -14
- package/dist/tools/fs/fs-read.d.ts +173 -2
- package/dist/tools/fs/fs-read.js +399 -286
- package/dist/tools/fs/image-admission.d.ts +51 -0
- package/dist/tools/fs/image-admission.js +36 -0
- package/dist/tools/fs/search.js +3 -7
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +29 -1
- package/wire-changes/core@7.41.1.json +159 -0
- package/wire-changes/core@7.42.0.json +1107 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
+
## 7.42.0 — 2026-10-05
|
|
2
|
+
|
|
3
|
+
**Hard-breaking changes:** one — #1327 (both halves). **#1327 — What breaks:** a caller policy no longer takes an allow rule — `createPermissionRulePolicy` refuses one (`config.invalid_permission_rules`, issue `unsupported.allow`), `PermissionRule.behavior` is `"deny" | "ask"`, the policy's name projection (`nameSets`) has no `allow` group, and the three issue codes that existed only to refuse an allow in one shape or another (`unsupported.bash_prefix`, `unsupported.file_glob`, `invalid.allow_param_rule`) are retired; the allow layer's disclosure `persistedRuleShadowed` (a string, stored rows only) is gone from `PermissionResult`'s ask arm, `AskRequest` and the park closure's sixth seat, and `RiskDescriptor.shadowedRule` from the park row — replaced by `allowRuleShadowed: { member, entries, omitted? }[]` on all four; `SKILL_ENTRY_INERT_REASONS` / `SkillEntryInertReason` are renamed `ALLOW_ENTRY_INERT_REASONS` / `AllowEntryInertReason`; the notice `skills.allowed_tool_not_granted` is renamed `permission.allowed_tool_not_granted` (audience `operator` → `user`, each `detail.entries[]` gains `source`). **#1327 — Why:** "this does not need asking" had several readings — a stored allow row cleared an ordinary question, a skill's row cleared it without disclosing a refusal, a hook's own `allow` only answered the unstated question, and the account of a stored row that could not clear was appended to the message the auto classifier reads; a caller policy's allow rule was a further reading (a statement the fold read at the unstated station only; a content-, path- or param-form allow refused at construction under three different codes); now every allow source is one member of one layer, read under one predicate, with one disclosure that only the card renders, and an allow rule has that one home — a request's entries in `TaskSpec.allowedTools`, the deployment's in `RunnerDeps.allowedTools` — while a policy says deny / ask. **#1327 — What to do:** read `allowRuleShadowed` (one record per member: `hook` / `deployment` / `persisted` / `skill` / `request`) where you read `persistedRuleShadowed` / `riskDescriptor.shadowedRule`; re-import the inert-reason set under its new name; route the notice by its new code (and expect it at the user's session); move the allow entries you handed `createPermissionRulePolicy` to `TaskSpec.allowedTools` (content forms too; write-path and domain forms read as nothing and are announced) or, for the deployment's standing grants, to `RunnerDeps.allowedTools`, and keep deny / ask rules in the policy. **Behavior narrowing (#1311 car A1):** three — `combinePolicies` returns a rule's deny over a deny from an approval seat that was never asked; an inline `TaskSpec.images` entry the model API would not accept is refused before the first model call (`config.image_invalid`); the changed-file notice also honors the caller policy's, the rule store's, the org lane's and an inherited parent policy's deny. **Closed sets:** none on the package root; four engine-internal module vocabularies (`READ_OUTCOMES`, `SYNTHETIC_READ_LAYERS`, `SYNTHETIC_READ_UNANSWERABLE_CAUSES`, `ADMITTED_IMAGE_MIME_TYPES`) and one thrown code (`config.image_invalid`). **Package exports:** none. **#1363:** `PermissionRuleIssue.code` +1 (`invalid.behavior`).
|
|
4
|
+
|
|
5
|
+
**Closed sets:** `DecisionReason` +2 (`deployment_rule`, `request_rule`); `EngineNoticeCode` −1 +1 (`skills.allowed_tool_not_granted` → `permission.allowed_tool_not_granted`); new `ALLOW_RULE_MEMBERS` (5 words); `PermissionRuleIssue.code` −3 +2 (retired `unsupported.bash_prefix`, `unsupported.file_glob`, `invalid.allow_param_rule`; added `invalid.entry_type`, `unsupported.allow`); `PermissionRule.behavior` −1 (`allow`); trace kind +1 (`permission.allowed_tools_allowed`). **Package exports:** +11 / −2 — added `ALLOW_RULE_MEMBERS`, `ALLOW_ENTRY_INERT_REASONS`, `compileAllowEntry`, `compileAllowList` (#1356), types `AllowRuleMember`, `AllowRuleShadow`, `AllowEntryInertReason`, `CompiledAllowEntry`, `AllowEntryGateSeats`, `AllowEntryLane`, `AllowEntryHit`; removed `SKILL_ENTRY_INERT_REASONS`, `SkillEntryInertReason` (renamed).
|
|
6
|
+
|
|
7
|
+
### "This does not need asking" has one home: the allow layer (#1327; behavior + hard-breaking surface) (@server @cli @client-core @test)
|
|
8
|
+
|
|
9
|
+
- **Before (v7.41.x).** Five readings of one fact. A person's stored allow row cleared an `ordinary` / `unstated` question (and, matched on a question it could not clear, appended its account to the ask's `message` — the text the auto classifier reads). A skill's `allowed-tools` row cleared the same questions but disclosed nothing when it could not. A PreToolUse hook's explicit `allow` only stopped the `unstated` question from being minted (a Write / Bash question in `default` still reached the person). There was no request-level or deployment-level allow list. In `auto` a skill's whole `Bash` and its interpreter-headed prefixes (sema's own interpreter table) stood down to the classifier, while a stored `Bash(npm run:*)` cleared without asking it. A caller policy's allow rule (`createPermissionRulePolicy([{ rule, behavior: "allow" }])`) was the fifth: a bare or covering name returned an allow that stated the tool — read at the unstated station only, so it never cleared a Write's or a shell command's question — while a content-, path- or param-form allow was refused at construction under three different codes (`unsupported.bash_prefix`, `unsupported.file_glob`, `invalid.allow_param_rule`).
|
|
10
|
+
- **Now.** One layer, five members, one loop, one predicate, behind every tighten and ahead of the classifier — in this order: **a hook's explicit `allow`** (silent when a policy rewrote the arguments after the hook saw them), **the deployment's `RunnerDeps.allowedTools`**, **a person's stored allow rows**, **an active skill's `allowed-tools`**, **the request's `TaskSpec.allowedTools`**. A member that matches clears the ask only when an allow rule may clear it (the classes `unstated` and `ordinary`; never a deny, an ask rule, an org rule, governance, a hook's ask, a marked call, the plan mode's question, a mandated question). A member that matches and cannot clear is **disclosed**: `allowRuleShadowed` on the surviving ask (engine-stamped — a policy's own value is overwritten), rendered into the approval card's `message` at the card (`… (this run's allowedTools (Write) matches this call but does not clear the ask — <the ask class's reason>)`), carried on the park row's `riskDescriptor.allowRuleShadowed` — and never on the decision the classifier reads.
|
|
11
|
+
- **`TaskSpec.allowedTools`** — the reference implementation's `allowedTools`: the grammar a skill's list reads (tool names, `mcp__<server>` / `__*` / globs, `Bash(...)` command rules, `Read(//dir/**)`; `*` voids the list, `mcp__*` / write paths / domains grant nothing and are announced). Lives for the run (steers included); a delegated child holds the entries until it ends; a resumed leg holds them only when its spec supplies them again (not checkpointed). A value that is not a list of strings refuses the run before it starts (`config.invalid_permission_rules`, `issues[].code: "invalid.entry_type"`). A call it clears: `decisionReason: "request_rule"`, trace `permission.allowed_tools_allowed` `{ source: "request", entries }`.
|
|
12
|
+
- **`RunnerDeps.allowedTools`** — the same grammar for the deployment's standing yeses (`decisionReason: "deployment_rule"`, trace `source: "deployment"`), screened once at construction (a bad value refuses construction), its inert entries announced once there (no session id).
|
|
13
|
+
- **A caller policy says deny / ask only.** `createPermissionRulePolicy` refuses an allow rule of any spelling at construction: `config.invalid_permission_rules`, one `unsupported.allow` issue per rule, its sentence naming the two homes (`TaskSpec.allowedTools`, `RunnerDeps.allowedTools`); under `onInvalidRule: "skip"` it is reported through `onIssue` and dropped, and the rules after it still compile. `PermissionRule.behavior` is `"deny" | "ask"`; the policy's `check()` has no allow tier and its `nameSets` no `allow` group. A call no rule reaches still gets `defaultAction` (`"allow"` is no objection — it states nothing and clears nothing). Deny / ask rules and their order are unchanged.
|
|
14
|
+
- **A hook's `allow` clears the question** the default mode raises for a Write or a shell command, and in `auto` the classifier's question (the reference's default); under `dontAsk` the call therefore runs instead of being refused by the mode (the mode refuses only an ask that survives the allow layer, as the reference's dontAsk does); it never clears the plan mode's question (stricter than the reference — divergence `hook-explicit-allow-does-not-clear-plan-question`), and it is not stripped in `auto`.
|
|
15
|
+
- **`auto` strips one entry shape, whichever list holds it** (the reference's table: the whole shell tool; a shell rule whose content is empty or `*`; `X`, `X:*`, `X *`, `X*`, `X -…*` for `python* node deno tsx ruby perl php lua npx bunx npm/yarn/pnpm/bun run bash sh ssh zsh fish eval exec env xargs sudo`, python's `-m dotted.module` excepted). A stored `Bash(npm run:*)` now hands its calls to the classifier in `auto`; a skill's `Bash(sed:*)` (sed is not in the reference's table) now grants in `auto`. Stored rows are stripped before the coverage is chosen, so `Bash(npm run build)` behind `Bash(npm run:*)` still admits `npm run build`. A whole-tool entry naming the shell under an alias, and a compound prefix by its final segment, are stripped too (sema-only shapes).
|
|
16
|
+
- **One predicate, read whole.** A member clears only when both readings agree: the mandate reading (bits, doors, an ask rule) and the class table's own `ruleMayClear` column over every class the ask carries. So an ask a policy declares as a guard (`guard`), an unreadable rule (`ruleUnreadable`) or the classifier's fallback (`denialLimitFallback`) WITHOUT the real-approval bit is no longer cleared by any member — a stored row or a skill's row cleared it in 7.41.x.
|
|
17
|
+
- **K-5:** a rule-shaped clear (a list entry, a stored row) and the read-only reader end the auto classifier's denial streak (the reference resets there); a hook's allow does not.
|
|
18
|
+
- **An entry is read exactly as written**, in every list (a skill's two lists too): one with surrounding whitespace (`"Write "`), or whose tool-name part is empty or carries whitespace or an unbalanced parenthesis (`"Bash("`, `"Bash)"`, `""`), names no tool — it grants (or refuses) nothing and is announced with `reason: "grammar"`. 7.41.x read `"Write "` as `Write` (the rule parser trimmed it) and compiled `"Bash("` as a tool name nothing carries, silently. The reference reads a rule string without trimming it (2.1.280). The directory loader already trims the separators of a frontmatter list, so a skill file is unaffected; a host-built list with padded entries is.
|
|
19
|
+
- **The approved-edit round** no longer takes a hook's `allow` as a statement: bytes a hook rewrote after the person approved go to that round's question (a narrowing; rare).
|
|
20
|
+
- **Who is hurt.** A host that hands `createPermissionRulePolicy` an allow rule — the serving layer that encodes `settings.permissions.allow` as policy allow rules, a test helper that grants a server that way: construction now throws (under `"skip"` the rule is dropped and reported); a probe that pinned `unsupported.bash_prefix` / `unsupported.file_glob` / `invalid.allow_param_rule`, a rule policy's `nameSets[].allow`, or the `decisionReason: "rule"` a rule policy's allow stamped; a host that reads `persistedRuleShadowed` / `riskDescriptor.shadowedRule`, imports `SKILL_ENTRY_INERT_REASONS`, or routes `skills.allowed_tool_not_granted` (renamed — see the header); a deployment whose PreToolUse hook answers `allow` to calls it did not mean to approve (a Write / shell question in `default` and the classifier's question in `auto` are now cleared — it never was a no-op for MCP tools); a deployment that relied on a stored `Bash(npm run:*)` / `Bash(npx:*)` row clearing without the classifier in `auto`; a probe that pinned the stored row's account inside the ask's `message`; a policy that put its own `persistedRuleShadowed` on an ask (overwritten); a policy that declared a guard / an unreadable rule / a fallback without `requiresRealApproval` and relied on a stored or skill row to clear it; a host that builds an allow or deny list (`TaskSpec.allowedTools`, `RunnerDeps.allowedTools`, `SkillSpec.allowedTools` / `disallowedTools`) with padded entries — they read as nothing now (a padded deny entry no longer refuses; announced); a 7.41.x park row read by this release keeps its old `shadowedRule` key unread; A host that reads `persistedRuleShadowed` / `riskDescriptor.shadowedRule`, imports `SKILL_ENTRY_INERT_REASONS`, or routes `skills.allowed_tool_not_granted` (renamed — see the header); a probe or test that wrote a hook's `allow` on the model's own bytes as a mere statement and expected a policy's plain ask to still reach the person afterwards (an edit round, a park) — that ask is now cleared in the first round and the hook-judged bytes run (give the hook no `action` on those bytes, or mark the ask `authoredBy: "deployment"`)
|
|
21
|
+
- **Compensation.** The grant a policy's allow carried moves as it was spelled: `allowedTools: ["mcp__<server>__*"]` on the request (it also clears the default mode's ordinary questions, which a policy's allow never did); a deployment's standing grant goes in `RunnerDeps.allowedTools`. A question that must reach a person whatever a hook or a list says: `authoredBy: "deployment"` or `requiresRealApproval` on the ask (both refuse every allow member). A hook that only observes: answer no `action`. The card's account: read `allowRuleShadowed` or the card's `message`. `auto`: name the narrow command (`Bash(npm run build)`) instead of the launcher prefix.
|
|
22
|
+
- **Consumers.** server — send `settings.permissions.allow` whole into `TaskSpec.allowedTools` (content forms included — the 422 on a content-form allow and its premise go; deny / ask keep compiling into `createPermissionRulePolicy`) and the `APPROVAL_GRANT` knob into `RunnerDeps.allowedTools`; render `allowRuleShadowed`; route the renamed notice; never import sema's own settings file into the stored-rule store (it would be a second home); read an entry the engine's way at the submission door with `compileAllowEntry(entry, "allow" | "deny")` / `compileAllowList(entries, behavior)` (#1356: a reading, or an `ALLOW_ENTRY_INERT_REASONS` word) — refuse a deny-side entry that reads as nothing (400), keep the notice for an allow-side one (a deny list's `*` reads as nothing by itself and the rest of the list still refuses, the #1303 deny-side rule). cli — may hand the whole allow list over (the engine covers built-in names and content forms; write-path forms still read as nothing until KL-1303-b closes). client-core — `DecisionReason` +2, `PermissionRule.behavior` −1 (`allow`), `PermissionRuleIssue.code` −3 +2, the notice code rename, `ALLOW_ENTRY_INERT_REASONS` / `ALLOW_RULE_MEMBERS` mirrors from the package root. test — G4 family (see the 7.42.0 criteria).
|
|
23
|
+
- **Not folded into this layer (ticketed).** The name-list policies' own allow lists — `createAllowDenyPolicy({ allow })`, `createApprovalPolicy`'s `autoAllow`, `createCoarseCommandNamePolicy`'s allow list — still state the calls they name (a hit on a server's tool clears the `unstated` question), and the write gate's session exemption (`isExempt`) still answers for a previously approved path: two more homes until #1359 and #1360 move them. A rule policy is the one factory that no longer states.
|
|
24
|
+
- **Known limits.** KL-1327-a (an entry names a tool through its aliases — the reference's list does not expand aliases) · b (no host flag to put every hook-allowed call through the approver anyway) · c (no switch that strips every shell entry in `auto`) · d (a tool's own `"maybe"` tier that the probe proved reversible keeps its `unstated` question through a hook's allow).
|
|
25
|
+
- Pins: `test/backlog1327b-policy-allow-retired.test.ts` (⑧–⑧e: an allow rule refused with one code naming both homes, the grant on its new home, deny / ask unchanged, no `allow` group in the projection, the three retired codes gone; real Runner + compile-time cells), `test/backlog1327-allow-one-home.test.ts` (real Runner, ①–⑳ + D1/D2 + H1/H2), `test/backlog1327-allow-entry-unit.test.ts` (the strip table, the screen, the parity table, the renderer); re-pinned: the stored-row disclosure suites (144 / 213 / 239 / 353 / 502 / 619 / 804 / 817 / 850 / 997 / design252 / 276 / 375 / permission-rule-gate / -wiring / risk-descriptor*), 1303 K2a (the interpreter arm is `node`; `sed` grants in auto), rb442 N7; the tests that handed a policy an allow rule now state it on `allowedTools` (permission-rules, 1065, 1111, 1303d, mcp-capability-and-rule-coverage, the MCP fixture's `grantMcpServers`) or as the deny / ask they meant (hardening-permission-core, 625 / 654 / 657).
|
|
26
|
+
|
|
27
|
+
### The read layer under the attachment seat: one read, structured notes, deferred effects (#1311 car A1; refactor, Read bytes unchanged)
|
|
28
|
+
|
|
29
|
+
- **Before (v7.41.x).** The Read tool's whole read lived in one `execute` closure that wrote read state and advanced the reminder-disclosure throttle while it read, and spoke its notes (empty file, offset past the end, already in context, unchanged, the range header, the partial marker, the disclosure trailer) as finished strings — the already-in-context reminder spliced the canonical path into a marked `<system-reminder>`. Nothing else could read a file "the way Read does" without also committing Read's side effects and inheriting its wording.
|
|
30
|
+
- **Now.** `readForModel` (src/tools/fs/fs-read.ts) answers a value — the external data blocks in order, the engine's notes as structured members, the read-state observation, the disclosure effects, an outcome word (`attached` / `partial` / `unchanged` / `not_attached`) — and changes nothing; `commit` applies the read state and the effects when a carrier delivers; the Read tool is read → commit → its own renderer, and its result is byte for byte what it was (the Read, notebook, image, PDF, dedup and disclosure pins are unchanged). The disclosure pipeline itself is plan / commit / render (`discloseReminderShaped` keeps its one-call form for MCP and web). The image lane's byte checks are one function (`admitImageBytes`), the walk's per-entry read-deny judge is one function (`createEntryDenyJudge`), and a directory can be listed one level for the model (`enumerateDirectory`: files, directories and unfollowed links, denied entries withheld and counted exactly, 1000 entries then "… and N more entries", an abort-cut listing marked incomplete).
|
|
31
|
+
- **Who is hurt.** Nobody: no package export moves and no tool result changes. Internal: `pdfResultToToolReturn` retires (the reader's refusal carries the error envelope).
|
|
32
|
+
- Pins: `test/backlog1311a-read-layer.test.ts` ①②③⑤ and M6; every existing Read / Glob / Grep / image pin.
|
|
33
|
+
|
|
34
|
+
### An inline `TaskSpec.images` entry passes the same admission as an image the Read tool serves (#1311 car A1; behavior narrowing) (@server @client-core)
|
|
35
|
+
|
|
36
|
+
- **Before (v7.41.x).** An inline image (`{ data, mimeType }`) went to the provider as given — no check that `mimeType` is one the API accepts, that `data` decodes to that type, or that it fits the 5 MB base64 limit; a mis-declared or over-size image failed the whole first request at the provider.
|
|
37
|
+
- **Now.** At the step where a refused image URL already fails (after the UserPromptSubmit hook, before the first model call and before the objective's `human_input` is published) the inline arm decodes `data` and runs the Read image lane's admission without downsampling: the type is one of `image/png`, `image/jpeg`, `image/gif`, `image/webp`; the decoded bytes are within 5 MiB and carry that type's signature; the base64 is within 5 242 880 chars. A failure ends the run `failed` with `TaskResult.terminal.code` `config.image_invalid` and a message starting `TaskSpec.images: `; nothing is sent. A valid image is sent unchanged (re-encoded from the bytes that were checked, which for canonical base64 is the same string).
|
|
38
|
+
- **Who is hurt.** A caller that sent `image/jpg`, an upper-case type, a type the API does not take, base64 with stray characters that happened to decode, or an image whose base64 is over 5 MB — each used to fail at the provider (or, on a lenient provider, reach the model) and now fails at the door.
|
|
39
|
+
- **Compensation.** Send the API's own type word and the file's exact bytes; shrink an image over 5 MB before sending. The URL arm is unchanged (it already identifies the type by signature).
|
|
40
|
+
- **Not checked, stated.** Like the Read image lane, the door checks the type's SIGNATURE, not that the image decodes: a file cut short after its first bytes still passes and can still fail at the provider (KL-1311a-c).
|
|
41
|
+
- **Consumers.** server — a request whose inline image fails now gets the typed failure instead of a provider error; client-core — `config.image_invalid` joins the codes it maps.
|
|
42
|
+
- Pins: `test/backlog1311a-read-layer.test.ts` ⑥.
|
|
43
|
+
|
|
44
|
+
### One synthetic-Read deny judge; the changed-file notice reads it (#1311 car A1; behavior narrowing of an advisory notice) (@server @test)
|
|
45
|
+
|
|
46
|
+
- **Before (v7.41.x).** The changed-file notice asked two layers whether the model could re-read a file (the run's skill deny rows and the session / ancestor / frozen narrowing fold) and ignored the deployment's caller policy, the person's rule store, the org lane and a live inherited parent policy — a file the deployment denied to `Read` still got "modified externally, re-read before editing".
|
|
47
|
+
- **Now.** `createSyntheticReadJudge` is the run's one answer to "would a `Read` of this file be refused outright?" over every layer that can refuse it, in three states — `deny`, `no_objection`, `unanswerable` with a cause word (`threw`, `timed_out`, `approval_seat`, `call_unreadable`, `org_unavailable`) — and the consumer picks where `unanswerable` goes. The rule store and org lane are read through the live gate's own readers (a store that cannot be read takes no part, as in the gate; an org lane that cannot be read is `unanswerable`). A deployment policy is asked with an already-aborted signal and no approver: a deny an approval seat attested reads as `approval_seat`, never as the policy's refusal. The changed-file notice reads `deny` as "do not report" and everything else as "report" (unchanged direction); prepare hands the same judge out (`syntheticReadVerdict`) for the attachment frame. Every probe carries the call id `<face>-visibility-probe` (`changed-files-visibility-probe` here).
|
|
48
|
+
- **The abort contract, now kept by every engine policy factory.** `createPreToolUseConstraintPolicy` (the inherited PreToolUse screening) handed an already-aborted signal no longer invokes the hook: it answers a deny carrying the engine settlement `task_aborted`, as `createApprovalPolicy` already did. INTEGRATION-CORE §4.6j states the contract for deployment policies.
|
|
49
|
+
- **Who is hurt.** A deployment that relied on the notice for a path its own policy denies to `Read` (the notice was not actionable — the model could not re-read it); a deployment policy that logs every `check` sees probe calls with the synthetic id; a deployment wrapper that answers an aborted signal with its own deny makes the notice silent for that file.
|
|
50
|
+
- **Compensation.** None needed for the notice; a policy that wants to tell probes apart reads the `-visibility-probe` id.
|
|
51
|
+
- **The wait is bounded once.** One judgment waits at most one dependency timeout (15 s) in all — each layer gets what is left, a layer reached after it is not asked (`timed_out`); the changed-file notice judges its candidates together under one shared deadline, so a hung deployment policy costs a turn boundary one 15 s bound, not one per file per layer.
|
|
52
|
+
- **Known limits.** KL-1311a-a (a deployment closure that reaches an approver before reading its signal can still be asked by a probe) · KL-1311a-b (a wrapper's own deny on an aborted signal reads as a refusal).
|
|
53
|
+
- Pins: `test/backlog1311a-read-layer.test.ts` ④ and ④b (one row per engine policy factory: no seat reached under an aborted signal).
|
|
54
|
+
|
|
55
|
+
### `combinePolicies`: a deny from a seat that was never asked yields to a rule's deny (#1311 car A1; behavior, narrow) (@server @client-core)
|
|
56
|
+
|
|
57
|
+
- **Before (v7.41.x).** The fold returned the FIRST deny it met. Under an interrupted run (or a visibility probe), an approval policy composed ahead of a rule deny answered its own deny (`approval aborted for "<tool>" (run or turn interrupted)`, settlement `task_aborted`) and the fold stopped there: the rule behind it was never asked, so the answer to "does any layer refuse this?" depended on the composition order.
|
|
58
|
+
- **Now.** A deny attested `task_aborted` is held and the scan goes on; a later deny that does not carry that attestation is returned instead (the rewrite carry and the attestation transfer are unchanged); when no later layer refuses on its own, the held deny is returned — the same object, message and record as before. One rule, in the fold only; it keys on the attestation, so an interrupted live call and a probe take the same path.
|
|
59
|
+
- **Who is hurt.** A consumer of the gate's record for a call cut by an interruption whose composed policy ALSO denies the call by rule: `tool_end.gate` now reads `{ disposition: { kind: "denied", deniedBy: "policy" } }` with the rule's message, where it read `deniedBy: "ask_resolution"` with the `task_aborted` settlement. Layers after the held deny are now consulted during that interruption (with the aborted signal — an engine factory reaches no seat; a deployment closure is called).
|
|
60
|
+
- **Compensation.** None needed: the call is refused either way; read `deniedBy` / `settlement` per call as before.
|
|
61
|
+
- Pins: `test/backlog1311a-read-layer.test.ts` ④ (composition order, both orders deny) and ④d (live gate during cancellation: the rule's deny; positive control — no rule deny, the seat's deny unchanged).
|
|
62
|
+
|
|
63
|
+
### A permission rule's `behavior` is a closed set at the compiler (#1363; behavior narrowing: a misspelled verb is refused instead of compiling to a dead rule) (@server @client-core)
|
|
64
|
+
|
|
65
|
+
- **Before (v7.41.x).** `createPermissionRulePolicy([{ rule: "Write", behavior: "permit" }])` — or `"ALLOW"`, `" allow"`, an undefined `behavior`, a boxed `String("allow")` — compiled with no issue and matched nothing: a misspelled `deny` dropped that deny silently (fail-open). Only the literal `"allow"` was refused (`unsupported.allow`, car B of #1327).
|
|
66
|
+
- **Now.** The compiler judges `behavior` against the closed set `deny | ask` first: the literal `"allow"` keeps `unsupported.allow` (the allow layer's sentence); every other value raises `config.invalid_permission_rules` with the new issue code **`invalid.behavior`** (the sentence names the two words and the value it got). Under `onInvalidRule: "skip"` the rule is dropped and reported; the rules after it still compile.
|
|
67
|
+
- **Who is hurt.** A JS caller whose rule list carried a misspelled or missing verb: construction now throws where it used to build an inert rule. **Closed set:** `PermissionRuleIssue.code` +1 (`invalid.behavior`) — with car B's −3 +1 the release's net on that set is −3 +2.
|
|
68
|
+
- **Compensation.** Spell the verb `deny` or `ask`; an allow entry goes to `TaskSpec.allowedTools` / `RunnerDeps.allowedTools`.
|
|
69
|
+
- Pins: `test/backlog1363-rule-behavior-closed.test.ts` (five bad shapes × throw / skip; the literal `allow` and valid `deny` / `ask` as controls; the sentence names both words).
|
|
70
|
+
|
|
71
|
+
## 7.41.1 — 2026-10-05
|
|
72
|
+
|
|
73
|
+
**Hard-breaking changes:** none. **Closed sets:** none added (two existing sets become reachable from the package root). **Package exports:** +5 / −0 — `SKILL_GRANT_ORIGINS`, `SKILL_ENTRY_INERT_REASONS`, types `SkillGrantOrigin`, `SkillEntryInertReason` (all defined in 7.41.0, re-exported now), and `mintNamespacePrefix` (#1355, defined since the namespace minting landed, re-exported now).
|
|
74
|
+
|
|
75
|
+
### The two skill-grant vocabularies are on the package root (#1347; surface only; +4 exports) (@client-core @server)
|
|
76
|
+
|
|
77
|
+
- **Before (v7.41.0).** `SKILL_GRANT_ORIGINS` (`model` / `user` / `inherited`) and `SKILL_ENTRY_INERT_REASONS` (`wildcard_all` / `namespace_wildcard` / `path_pattern` / `domain` / `content_unsupported` / `grammar`) and their two types were declared in `dist/core/skill-grant.d.ts` and not re-exported from `index.d.ts`; the 7.41.0 ship post called them consumable closed sets because the wire manifest scans every `dist/` file, not the package root.
|
|
78
|
+
- **Now.** Both constants and both types are re-exported from the package root beside `RULE_TOOL_NAME_VERDICTS`; the consumer smoke probe asserts their presence.
|
|
79
|
+
- **Who is hurt.** Nobody: additive.
|
|
80
|
+
- **Consumers.** client-core — generate the two mirrors from the package root (CC-338). server — none.
|
|
81
|
+
- Pins: `test/export-surface.snapshot.json` (+4 names); `scripts/consumer-smoke-probe.mjs` (two new object entries, run by `gate:blackbox`).
|
|
82
|
+
|
|
83
|
+
### The peer-segment minter is on the package root (#1355 part one; surface only; +1 export) (@server)
|
|
84
|
+
|
|
85
|
+
- **Before (v7.41.0).** A covering-form rule such as `mcp__github.com` (two segments, accepted since #1156) or `mcp__github.com__*` was compiled and stored as written, while the engine mints the peer segment normalized (character set, length clamp with a digest suffix, separator fold: `mcp__github_com__`). The rule's segment and the minted segment are compared byte for byte, so such a rule never matches — a deny that reads protective and is inert. Nothing on the package root let a rule writer ask "is this segment one the engine mints?".
|
|
86
|
+
- **Now.** `mintNamespacePrefix(ns, peer)` (src/core/protocol-naming.ts) is re-exported from the package root beside `PROTOCOL_TABLE` / `MCP_NAMESPACE` / `A2A_NAMESPACE`: a rule writer mints the prefix for the rule's peer and compares it with the rule's own spelling. Part two (7.42.0): `judgeRuleToolName` gains the verdict `unmintable_segment` with the minted spelling as `currentName`, and both rule compilers refuse through that one judge.
|
|
87
|
+
- **Who is hurt.** Nobody: additive.
|
|
88
|
+
- **Consumers.** server — S-1113: judge `permissions.deny` / `APPROVAL_DENY` covering-form rules at the door until the 7.42.0 verdict lands.
|
|
89
|
+
- Pins: `test/export-surface.snapshot.json` (+1 name); `scripts/consumer-smoke-probe.mjs` (one new function entry, run by `gate:blackbox`).
|
|
90
|
+
|
|
1
91
|
## 7.41.0 — 2026-10-04
|
|
2
92
|
|
|
3
93
|
**Hard-breaking changes:** one — #1303. **#1303 — Before:** a skill that declared `allowed-tools` (or a host's `SkillSpec.manifest.allowTools`) narrowed the whole task once activated: every unlisted tool was refused (`allowPaths` fenced writes to directories), the listed ones still asked where they always asked, and activating it asked nobody. **#1303 — Who is hurt:** a deployment that relied on a skill's list to fence the model — an unlisted tool is now judged as usual (asked under an asking mode, run under `bypassPermissions` or a deployment allow) and a listed one runs without asking for the rest of the run; a host that hand-built `SkillSpec.manifest` (now refused by name: `config.spec_retired_key`) or wrote `allowPaths` (the write fence is gone), imported the deleted `SkillManifest` type or `skillScopeWriteToolNames`, passed the retired `deployedTools` option to `createSkillsFromDirectory` (refused: `config.deps_retired_key`) or matched on one of its three removed warning codes, or built an auto-promote record with `manifest` / `gate3Scoped` / `lineageId` or passed a manifest to `deriveTripwire`; an embedder with no approver under an asking mode (activating a skill that declares a list is now a card nobody answers — refused); a deployment policy that returns an unmarked `ask` (class `ordinary`) for a listed tool (the list clears it, as a stored allow rule would); a delegation tool that read `ctx.activeSkillScope()` (renamed `ctx.activeSkillGrants()`). **#1303 — Compensation:** fence tools with deny rules (a deployment deny list, a person's deny row) or a delegated agent definition's `allowTools`; fence write paths with session `allowDirs` / the write face (`createSessionRulePolicy`, the fs-write gate); mark a policy's question `mandated` / `requiresRealApproval` (or write an ask rule) when it must reach a person; with no approver, name `Skill` in an allow list or run `bypassPermissions`; move `manifest.allowTools` to `SkillSpec.allowedTools`; drop `deployedTools` (the loader copies every entry and the run's grant table reads it); give `deriveTripwire` the artifact's `{ allowedTools }` and drop `manifest` / `gate3Scoped` / `lineageId` from the promoted record. Everything else in this version adds, tightens or re-words: #1118 adds optional members and text lines; #1294 changes which hook seats fire for a call approved on a parked checkpoint (a PreToolUse seat now sees that call again on the resumed leg); #1307 / #1308 re-word request-time refusals of the file stores (no on-disk location in the sentence; the location rides the store's disclosure seat); #1309 turns a session's enumeration over its own damaged row from a short list into a refusal; #1310 and #1314 change model-facing text only; #1156 refuses a retired or prefix-less `__` tool name at the session-rules write (it used to fail every later run instead); #1328 refuses a resumed `Skill` call whose final arguments select another skill than the card named. **Closed sets:** `DecisionReason` +1 `skill`; `ASK_CLASSES` / `AskClass` +1 `requires_user_interaction` (`MODE_ARBITRATION` +1 row; `RELEASABLE_ASK_CLASSES` and `PERSISTED_RULE_MANDATES` unchanged); `ENGINE_NOTICE_CODES` +1 `skills.allowed_tool_not_granted` (audience `operator`, minter `engine`); trace kinds +1 `permission.skill_allowed`; config refusal codes +1 `config.user_invoked_skill_unknown`; `CheckpointErrorReason` +1 `skill_grants_damaged` and `CheckpointError.detail.field` +1 `boundGrantDigest` (both on the existing `checkpoint.invalid_outcome`); `SessionPolicyError.code` +1 `legacy_tool_name` (with `detail: { field, name, verdict, currentName? }`); new closed set `RULE_TOOL_NAME_VERDICTS` (`current` / `retired` / `unprefixed_namespace`). **Package exports:** +22 / −2 (the entry's count 2558 → 2578). Added — #1118: `readAgentState`, `agentStateSourceOfRecord`, `agentStateSummary`, types `AgentState`, `AgentApproval`, `AgentApprovalState`, `AgentStateRow`, `AgentStateSource`, `TranscriptStep`; #1303: types `SkillGrantRecord`, `SkillGrantHit`, `SkillGrantLane`, `SkillGateSeats`, `PermissionSeatInputs`, `PermissionSeatReading`; #1308: type `FileUsageWindowStoreOptions`; #1156: `judgeRuleToolName`, `RULE_TOOL_NAME_VERDICTS`, `assertSessionRuleNamesCurrent`, types `RuleToolNameVerdict`, `SessionPolicyStoreOptions`, `SessionRuleNameRefusal`. Removed — #1303: `SkillManifest`, `skillScopeWriteToolNames`. Members — new and optional: `TaskSpec.userInvokedSkills`, `SkillSpec.allowedTools` / `disallowedTools`, `CheckpointState.skillGrants`, the `tool_approval` pending action's `boundGrantDigest`, `ToolGateInput.requiresUserInteraction`, `UnifiedTaskOutput.agentState`, `AgentPollDetailsInput.agentState`, `ListAgentsRow.agentState`, `serveDurableAgentRowLane(row, agentState?)`, `checkpointSeat` on `AgentTranscriptToolOptions` / `ListAgentsToolOptions` / `TaskToolOptions` / `TaskPollOptions`, `TaskRegistry.agentStateSource` (new method), `FileUsageWindowStore`'s constructor options and `createFileTaskListStore`'s `onCorruptRead`, `mountedToolNames` on both session-rules stores (`FileStorageBackendOptions.sessionPolicy` forwards it), `loosenReasons`' third parameter; `AgentTranscriptToolOptions.registry` widens to `Pick<TaskRegistry, "getAccessibleTask"> & Partial<Pick<TaskRegistry, "agentStateSource">>` (a full `TaskRegistry` still fits). Removed (the hard-breaking change above): `SkillSpec.manifest` (a host still passing it is refused by name), `SkillsDirectoryOptions.deployedTools` (refused by name), three `SkillsDirectoryWarningCode` words (`allowed_tool_pattern_unsupported`, `allowed_tool_not_mounted`, `disallowed_tools_unenforced`), `PromotedRecord.manifest` / `gate3Scoped` / `lineageId`; `deriveTripwire`'s second parameter is `Pick<SkillSpec, "allowedTools">` (was `SkillManifest`).
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1704,6 +1704,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1704
1704
|
: seatModel?.id;
|
|
1705
1705
|
const rowModelKey = perCallModel !== undefined ? requestedModel ?? perCallModel.id : typeof seatModel === "string" ? seatModel : seatModel?.id;
|
|
1706
1706
|
const inheritedSkillGrants = readSkillGrantCapture(ctx.activeSkillGrants?.());
|
|
1707
|
+
const inheritedAllowedTools = ctx.activeRequestAllowedTools?.();
|
|
1707
1708
|
if (inheritedSkillGrants === undefined) {
|
|
1708
1709
|
return {
|
|
1709
1710
|
isError: true,
|
|
@@ -2087,6 +2088,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2087
2088
|
const childInternals = {
|
|
2088
2089
|
...(childPick.modelRef !== undefined ? { inheritedModelRef: childPick.modelRef } : {}),
|
|
2089
2090
|
...(inheritedSkillGrants.length > 0 ? { inheritedSkillGrants } : {}),
|
|
2091
|
+
...(inheritedAllowedTools !== undefined && inheritedAllowedTools.length > 0 ? { inheritedAllowedTools: [...inheritedAllowedTools] } : {}),
|
|
2090
2092
|
...(childProvenanceRef !== undefined && provenanceRequest !== undefined ? { delegationProvenance: { ref: childProvenanceRef, contentSafety: provenanceRequest } } : {}),
|
|
2091
2093
|
...(treeScope !== undefined ? { registryScope: treeScope } : {}),
|
|
2092
2094
|
...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
|
|
@@ -116,7 +116,7 @@ export interface SkillSpec {
|
|
|
116
116
|
* either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
|
|
117
117
|
* name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
|
|
118
118
|
* run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
|
|
119
|
-
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`
|
|
119
|
+
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`).
|
|
120
120
|
*/
|
|
121
121
|
allowedTools?: string[];
|
|
122
122
|
/** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { type ToolCallRequest } from "./tool-policy.js";
|
|
2
|
+
import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
|
|
3
|
+
import { type PersistedRule } from "./permission-rule-model.js";
|
|
4
|
+
import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
5
|
+
import { type EngineNotice } from "./engine-notice.js";
|
|
6
|
+
/** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. */
|
|
7
|
+
export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"];
|
|
8
|
+
export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
|
|
9
|
+
/** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell
|
|
10
|
+
* command rule; the Read directory form; a deny-side path pattern (a skill's `disallowed-tools`). */
|
|
11
|
+
export type CompiledAllowEntry = {
|
|
12
|
+
readonly kind: "tool";
|
|
13
|
+
readonly name: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "namespace";
|
|
16
|
+
readonly pattern: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "shell";
|
|
19
|
+
readonly name: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "command";
|
|
22
|
+
readonly rule: PersistedRule;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "read_dir";
|
|
25
|
+
readonly rule: PersistedRule;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "path";
|
|
28
|
+
readonly rule: PersistedRule;
|
|
29
|
+
};
|
|
30
|
+
/** Read one entry under one direction. */
|
|
31
|
+
export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny"): CompiledAllowEntry | AllowEntryInertReason;
|
|
32
|
+
/**
|
|
33
|
+
* Compile one list WHOLE: the entries it reads and the ones it reads as nothing. An allow list holding `*` voids the list
|
|
34
|
+
* (every other entry on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads
|
|
35
|
+
* as nothing by itself and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny"): {
|
|
38
|
+
rows: Array<{
|
|
39
|
+
entry: string;
|
|
40
|
+
compiled: CompiledAllowEntry;
|
|
41
|
+
}>;
|
|
42
|
+
inert: Array<{
|
|
43
|
+
entry: string;
|
|
44
|
+
reason: AllowEntryInertReason;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
/** The programs whose open allow rule the reference implementation drops in auto mode — its interpreter / launcher table
|
|
48
|
+
* plus the shells and the run-anything builtins (2.1.280). A program that only reaches the network is not here. */
|
|
49
|
+
export declare const AUTO_STRIPPED_PROGRAMS: readonly ["python", "python3", "python2", "node", "deno", "tsx", "ruby", "perl", "php", "lua", "npx", "bunx", "npm run", "yarn run", "pnpm run", "bun run", "bash", "sh", "ssh", "zsh", "fish", "eval", "exec", "env", "xargs", "sudo"];
|
|
50
|
+
/**
|
|
51
|
+
* Does auto mode strip this allow entry for this call (see the module doc)? Read for every rule-shaped member — a list
|
|
52
|
+
* entry, and a person's stored row through {@link autoStrippedRule}; a hook's allow is not an entry and is never stripped.
|
|
53
|
+
* A stripped entry takes no part in the match (the reference removes it before matching), so it neither clears nor is
|
|
54
|
+
* disclosed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function autoStripped(compiled: CompiledAllowEntry, call: ToolCallRequest | undefined): boolean;
|
|
57
|
+
/** {@link autoStripped} over a person's stored ALLOW row (a deny / ask row is never stripped; a path row never is). */
|
|
58
|
+
export declare function autoStrippedRule(rule: PersistedRule, call: ToolCallRequest): boolean;
|
|
59
|
+
/** One row a list matcher reads. */
|
|
60
|
+
export interface AllowEntryRow {
|
|
61
|
+
readonly behavior: "allow" | "deny";
|
|
62
|
+
readonly entry: string;
|
|
63
|
+
readonly compiled: CompiledAllowEntry;
|
|
64
|
+
}
|
|
65
|
+
/** The call facts a content row is judged against — the persisted lane's own bases. */
|
|
66
|
+
export interface AllowEntryJudgeContext {
|
|
67
|
+
readonly root: string | undefined;
|
|
68
|
+
readonly sessionId: string | undefined;
|
|
69
|
+
readonly liveCwd: () => string | undefined;
|
|
70
|
+
readonly home: string | undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The ONE list matcher, both directions — the rows that speak for the call, or undefined. ALLOW: the first name row that
|
|
74
|
+
* reaches the call, else the command rows' coverage (the persisted allow matcher — the whole command, or each segment
|
|
75
|
+
* covered by a row or read-only by itself), else the Read directory rows. DENY: a name row, a command row whose program run
|
|
76
|
+
* the call performs (the wide tightening reading; a call that cannot be read against the row is refused too), or a path row
|
|
77
|
+
* the call's target reaches.
|
|
78
|
+
*/
|
|
79
|
+
export declare function judgeAllowEntries<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, behavior: "allow" | "deny", ctx: AllowEntryJudgeContext): readonly R[] | undefined;
|
|
80
|
+
/** The allow rows of a list, as the allow layer reads them in `mode`: the strip applied BEFORE the match (in `auto`). */
|
|
81
|
+
export declare function allowingRows<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, mode: PermissionMode | undefined): readonly R[];
|
|
82
|
+
/** The two sources {@link AllowEntryTable} holds (a skill's list is the skill table's). */
|
|
83
|
+
export type AllowEntrySource = "request" | "deployment";
|
|
84
|
+
/** What spoke for a call: the entries it stands on, as the list spells them. */
|
|
85
|
+
export interface AllowEntryHit {
|
|
86
|
+
readonly entries: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
/** The face the gate reads one entry list through. */
|
|
89
|
+
export interface AllowEntryLane {
|
|
90
|
+
/** The allow layer's member: what grants this call in this mode, or undefined. */
|
|
91
|
+
allowing(req: ToolCallRequest, mode: PermissionMode | undefined): AllowEntryHit | undefined;
|
|
92
|
+
}
|
|
93
|
+
/** The gate inputs the two entry lists ride (a member group of `ToolGateInput`). Each is the allow layer's member for its
|
|
94
|
+
* source — read under the one consumption predicate beside a person's stored rows and a skill's list, clearing only what an
|
|
95
|
+
* allow rule may clear — with its attribution sink (`onAllowed` observes a call it cleared; never affects the outcome).
|
|
96
|
+
* Absent ⇒ that source has no rows. */
|
|
97
|
+
export interface AllowEntryGateSeats {
|
|
98
|
+
deploymentAllows?: AllowEntryLane & {
|
|
99
|
+
onAllowed?: (info: {
|
|
100
|
+
toolName: string;
|
|
101
|
+
toolCallId: string;
|
|
102
|
+
entries: readonly string[];
|
|
103
|
+
}) => void;
|
|
104
|
+
};
|
|
105
|
+
requestAllows?: AllowEntryLane & {
|
|
106
|
+
onAllowed?: (info: {
|
|
107
|
+
toolName: string;
|
|
108
|
+
toolCallId: string;
|
|
109
|
+
entries: readonly string[];
|
|
110
|
+
}) => void;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* One source's allow rows, compiled once: the request's (prepare seeds it from the spec's snapshot and, in a delegated
|
|
115
|
+
* child, from the parent's capture) or the deployment's (compiled from the Runner's snapshot). Allow rows only — neither
|
|
116
|
+
* list has a deny side. Identity is the contract: prepare seeds it, the delegation capture and the gate's lane read it.
|
|
117
|
+
*/
|
|
118
|
+
export declare class AllowEntryTable {
|
|
119
|
+
private held;
|
|
120
|
+
/** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. */
|
|
121
|
+
seed(entries: readonly string[]): ReadonlyArray<{
|
|
122
|
+
entry: string;
|
|
123
|
+
reason: AllowEntryInertReason;
|
|
124
|
+
}>;
|
|
125
|
+
/** The entries held, as written (a copy) — what a delegated child is handed. */
|
|
126
|
+
entries(): readonly string[];
|
|
127
|
+
/** The lane over this table, bound to the leg's call facts. */
|
|
128
|
+
lane(ctx: AllowEntryJudgeContext): AllowEntryLane;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Seed one leg's request and deployment tables — a delegated child's captured request entries first (an entry that reads as
|
|
132
|
+
* nothing was announced where it was written), then the leg's own list; the deployment's (announced once, at construction) —
|
|
133
|
+
* and return the dead entries of the leg's OWN lists, the skills' beside the request's, for its one start-of-run notice.
|
|
134
|
+
*/
|
|
135
|
+
export declare function seedAllowEntryTables(tables: {
|
|
136
|
+
readonly request: AllowEntryTable;
|
|
137
|
+
readonly deployment: AllowEntryTable;
|
|
138
|
+
}, lists: {
|
|
139
|
+
skillInert: ReadonlyArray<{
|
|
140
|
+
skill: string;
|
|
141
|
+
list: "allowed-tools" | "disallowed-tools";
|
|
142
|
+
entry: string;
|
|
143
|
+
reason: AllowEntryInertReason;
|
|
144
|
+
}>;
|
|
145
|
+
inherited: readonly string[] | undefined;
|
|
146
|
+
request: readonly string[] | undefined;
|
|
147
|
+
deployment: readonly string[] | undefined;
|
|
148
|
+
}): AllowEntryInertNote[];
|
|
149
|
+
/** The deployment's allow rules, read ONCE at construction: screened (a value that is not a list of strings refuses), frozen,
|
|
150
|
+
* and its dead entries announced once (no session). Returns the deps with the snapshot in place of the caller's list. */
|
|
151
|
+
export declare function withDeploymentAllowedTools<D extends {
|
|
152
|
+
readonly allowedTools?: readonly string[];
|
|
153
|
+
readonly onNotice?: ((n: EngineNotice) => void) | undefined;
|
|
154
|
+
}>(deps: D): D;
|
|
155
|
+
/** The code a list value that is not a list of strings refuses with — the policy compiler's family, with its issues. */
|
|
156
|
+
export declare const INVALID_PERMISSION_RULES_CODE = "config.invalid_permission_rules";
|
|
157
|
+
/**
|
|
158
|
+
* Screen and snapshot one `allowedTools` value (`seat` names the key in the refusal): absent ⇒ undefined; a list of strings
|
|
159
|
+
* ⇒ a frozen copy (the one read every later reader takes); anything else refuses loudly with
|
|
160
|
+
* {@link INVALID_PERMISSION_RULES_CODE} and an issue per defect — never dropped, never read as "no entries".
|
|
161
|
+
*/
|
|
162
|
+
export declare function screenAllowedToolsValue(value: unknown, seat: string): readonly string[] | undefined;
|
|
163
|
+
/** The disclosure record for one member, minted from the entries it matched: each sanitized (deployment or model-authored
|
|
164
|
+
* text on a human trust boundary) and capped, the rest counted. */
|
|
165
|
+
export declare function allowRuleShadowOf(member: AllowRuleMember, entries: readonly string[]): AllowRuleShadow;
|
|
166
|
+
/** Read a disclosure value off a decision WITHOUT trusting it (a policy's own value, an ancestor's): every record a known
|
|
167
|
+
* member with a list of strings, or nothing — a display member must never throw where a card is minted. */
|
|
168
|
+
export declare function readAllowRuleShadows(value: unknown): readonly AllowRuleShadow[] | undefined;
|
|
169
|
+
/** Why a matched entry did not clear the ask, by the ask's class — one row per class, so a class added later is a
|
|
170
|
+
* compile error here rather than somebody else's reason on the card. Two facts the ask carries are read first, because
|
|
171
|
+
* they name the reason more exactly than a class shared by several: a table word (`mandate` — the exact mark) and the
|
|
172
|
+
* deployment's own authorship (`authoredBy`). */
|
|
173
|
+
declare const SHADOW_REASON: {
|
|
174
|
+
readonly content_question: "the call is the engine's own question to a person";
|
|
175
|
+
readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person";
|
|
176
|
+
readonly ancestor: "the task that delegated this one owns this question";
|
|
177
|
+
readonly unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule";
|
|
178
|
+
readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
|
|
179
|
+
readonly org_rule: "an organization policy rule requires approval for this call";
|
|
180
|
+
readonly governance_unavailable: "this deployment is org-governed and cannot currently read its organization policy";
|
|
181
|
+
readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown";
|
|
182
|
+
readonly ask_rule: string;
|
|
183
|
+
readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides";
|
|
184
|
+
readonly requires_user_interaction: "this tool's server declares that a call must be put to a person";
|
|
185
|
+
readonly operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
|
|
186
|
+
readonly shell_floor: string;
|
|
187
|
+
readonly write_protection_unresolved: string;
|
|
188
|
+
readonly probe: string;
|
|
189
|
+
readonly write_protection: string;
|
|
190
|
+
readonly tool_marks_mcp: string;
|
|
191
|
+
readonly tool_marks_a2a: string;
|
|
192
|
+
readonly policy_rra: "the deployment's policy requires a real approval for this call";
|
|
193
|
+
readonly artifact_shared_read: "this read targets another task's shared artifact — it is cleared by confirming this call";
|
|
194
|
+
readonly mode: "the permission mode puts this question to a person";
|
|
195
|
+
readonly unstated: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
|
|
196
|
+
readonly ordinary: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
|
|
197
|
+
};
|
|
198
|
+
/** The fence over the reason table: `never` while every class has a row. */
|
|
199
|
+
export type ShadowReasonTableCoversEveryClass = AssertAllKeysHandled<Exclude<AskClass, keyof typeof SHADOW_REASON>>;
|
|
200
|
+
/**
|
|
201
|
+
* The disclosure in the sentence a PERSON reads — rendered where a card or a park row is minted, never onto the decision
|
|
202
|
+
* the classifier reads: "<members> match(es) this call but do(es) not clear the ask — <why>".
|
|
203
|
+
*/
|
|
204
|
+
export declare function allowRuleShadowSentence(shadows: readonly AllowRuleShadow[], ask: {
|
|
205
|
+
askClass?: AskClass;
|
|
206
|
+
mandate?: PersistedRuleMandate;
|
|
207
|
+
authoredBy?: "deployment";
|
|
208
|
+
}): string;
|
|
209
|
+
/** The card's two disclosure members off a surviving ask, minted at every station that builds an approval request: the
|
|
210
|
+
* sanitized records, and the message with the sentence rendered onto it. */
|
|
211
|
+
export declare function cardDisclosureOf(decision: {
|
|
212
|
+
message?: string;
|
|
213
|
+
allowRuleShadowed?: unknown;
|
|
214
|
+
askClass?: AskClass;
|
|
215
|
+
mandate?: PersistedRuleMandate;
|
|
216
|
+
authoredBy?: "deployment";
|
|
217
|
+
}, fallbackMessage: string): {
|
|
218
|
+
message: string;
|
|
219
|
+
allowRuleShadowed?: readonly AllowRuleShadow[];
|
|
220
|
+
};
|
|
221
|
+
/** The notice for list entries that grant (or refuse) nothing — one code for every list. */
|
|
222
|
+
export declare const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
|
|
223
|
+
/** One entry that reads as nothing, and the list it came from (a skill's names the skill and which of its two lists). */
|
|
224
|
+
export type AllowEntryInertNote = {
|
|
225
|
+
readonly source: "skill";
|
|
226
|
+
readonly skill: string;
|
|
227
|
+
readonly list: "allowed-tools" | "disallowed-tools";
|
|
228
|
+
readonly entry: string;
|
|
229
|
+
readonly reason: AllowEntryInertReason;
|
|
230
|
+
} | {
|
|
231
|
+
readonly source: AllowEntrySource;
|
|
232
|
+
readonly entry: string;
|
|
233
|
+
readonly reason: AllowEntryInertReason;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* The notice for entries this grammar reads as nothing — delivered only when one exists: once per prepared task for the
|
|
237
|
+
* skill and request lists (`sessionId` given), once per Runner for the deployment's list. The person whose list it is
|
|
238
|
+
* reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more).
|
|
239
|
+
*/
|
|
240
|
+
export declare function allowedToolsNotGrantedNotice(inert: readonly AllowEntryInertNote[], sessionId: string | undefined): EngineNotice;
|
|
241
|
+
export {};
|