@sema-agent/core 5.60.0 → 5.60.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.60.1 — 2026-08-25
4
+
5
+ ### Fixed
6
+ - **Resume-leg usage-governance holes (#449, P0/P1)**: a resume no longer burns a full model turn
7
+ when the usage window is exhausted — an owed row (approval/review decision, wake, parked steer,
8
+ bg snapshot) refuses PRE-CAS with the new `resume.usage_window_exhausted` CheckpointError
9
+ (`detail.retryAfterMs`; the row stays pending, zero consumption, the same token redeems after the
10
+ window); a bare resource_limit continue re-suspends at turn 0 through the same platform-limit leg
11
+ the turn boundary uses (fresh checkpoint, new `resumeAfterMs`, zero model calls). The resume
12
+ ledger key now falls back to the checkpoint-pinned principal (`resource_limit` mints stamp
13
+ `cp.principal`) — a caller that omits principal no longer silently charges the GLOBAL bucket and
14
+ collects a fresh quota; supplying a DIFFERENT principal refuses loudly
15
+ (`resume.principal_mismatch`, pre-CAS, row pending). NARROWING, named: mismatched-principal
16
+ resumes previously proceeded under the wrong identity.
17
+ - **covering-ask audit arm (#450, core half)**: the prepare-time policy-name audit gains ask-slot
18
+ coverage pins (the exemption for `mcp__<server>__*` spellings now proves it speaks the truth
19
+ against a live in-engine ask lane); `isNamespacedCoveringRuleName`/`namespacedRuleNameCovers`
20
+ root-exported (additive) so out-of-engine name-set publishers can self-check the covering grammar
21
+ (the fifth-publisher fail-open shape stays pinned as-is until the server-side consumer lands its
22
+ fix). Export surface 1758 → 1760.
23
+
3
24
  ## 5.60.0 — 2026-08-25
4
25
 
5
26
  ### Added
@@ -1263,7 +1263,7 @@ export declare const FACE_CHECKPOINT_VERSION = 9;
1263
1263
  * (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
1264
1264
  * wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
1265
1265
  * state), v7 (F-012 constraint-chain / delegation-provenance enforcement state), v8 (a
1266
- * non-budgetable `realApproval` gate bit with its org-origin resume semantics) and v9 (the
1266
+ * non-budgetable `realApproval` gate bit with its org-origin resume semantics), v9 (the
1267
1267
  * design/199 read-face section: resolved face + deny-set additions, folded stricter-wins on
1268
1268
  * resume). */
1269
1269
  export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
@@ -1551,11 +1551,21 @@ export interface Checkpoint {
1551
1551
  sourceTaskId?: string;
1552
1552
  /**
1553
1553
  * design S1d (source-tag persistence): the issuing task's authenticated end-user {@link
1554
- * import("./types.js").TaskSpec.principal} (design/62), stamped at the APPROVAL suspend mint so an aggregating
1555
- * inbox can attribute a paused checkpoint per-userthe durable analog of {@link
1556
- * import("./tool-policy.js").AskRequest}.principal. Caller-set (never a tool/worker arg), and like
1557
- * {@link sourceTaskId} it is **ECHO-ONLY triage metadata, NEVER a gate input** (no gate/CAS/winner/resume path
1558
- * reads it). Absent no principal (a non-ask gate, an unauthenticated task, or a legacy checkpoint).
1554
+ * import("./types.js").TaskSpec.principal} (design/62), stamped at the APPROVAL suspend mint and, since
1555
+ * #449 G2, at the `resource_limit` mint tooso an aggregating inbox can attribute a paused checkpoint
1556
+ * per-user — the durable analog of {@link import("./tool-policy.js").AskRequest}.principal. Caller-set
1557
+ * (never a tool/worker arg), and like {@link sourceTaskId} it is **NEVER a gate input**: no gate / CAS /
1558
+ * winner path reads it. The RESUME ENTRY however does read it as the row's identity record (#449 G2):
1559
+ * a resume config that omits `principal` inherits this value for the resumed leg (usage-ledger bucket,
1560
+ * later mints' scope derivation, ask attribution — a dropped field must not silently move the leg onto
1561
+ * the SHARED usage bucket), and a resume config carrying a DIFFERENT non-empty principal is refused
1562
+ * pre-CAS (`resume.principal_mismatch`). Absent ⇒ no principal (a legacy checkpoint, an unauthenticated
1563
+ * task, or a gate kind that predates the stamp). Rolling-deployment note (#449 G2 residual): the field
1564
+ * is NOT version-gated — an older worker accepts a principal-bearing row and keeps its historic
1565
+ * behavior (no fallback, no mismatch refusal); the identity-continuity guarantee holds once the fleet
1566
+ * is on enforcing workers. Version-gating it is a recorded design candidate: the lattice's newest rung
1567
+ * (design/199) encodes "v>=9 ⟺ face section present", so ANY new version above it must rewrite that
1568
+ * guard's window form and add its own presence invariant — a lattice-wide change, not a stamp.
1559
1569
  */
1560
1570
  principal?: string;
1561
1571
  /**
@@ -1812,6 +1822,23 @@ export declare class CheckpointError extends Error {
1812
1822
  * thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the checkpoint
1813
1823
  * stays `pending`); re-resume with the full original chain. */
1814
1824
  | "resume.parent_constraint_mismatch"
1825
+ /** #449 G1 (codex r2, pre-CAS entrance): a deployment usage-governance window for this resume's
1826
+ * ledger key (the config's principal, else the row's recorded one, else the shared bucket) is
1827
+ * EXHAUSTED, and the row still OWES a delivery a re-mint cannot carry (an approval/review
1828
+ * decision, a wake, parked steers, a background-task obituary snapshot). Refused BEFORE the CAS,
1829
+ * so nothing is consumed and nothing is unpinned — the SAME token with the SAME decision is
1830
+ * redeemable once the window frees (`detail.retryAfterMs` carries the wait). A bare
1831
+ * `resource_limit` continue with nothing parked does NOT take this refusal: it proceeds and
1832
+ * re-suspends at turn 0 into a FRESH checkpoint carrying the new wait hint. */
1833
+ | "resume.usage_window_exhausted"
1834
+ /** #449 G2 (identity continuity): the resume config carries a NON-EMPTY `principal` that differs from
1835
+ * the {@link Checkpoint.principal} recorded at the suspend mint. `ResumeTaskConfig`'s contract is
1836
+ * "the same TaskSpec the run suspended with", and the principal keys real lanes on the resumed leg
1837
+ * (the usage-governance ledger bucket, later mints' scope derivation, session-rule reads, ask
1838
+ * attribution) — silently running the leg under a different identity is the bad-value direction this
1839
+ * refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
1840
+ * original principal, or omit the field to inherit the recorded one. */
1841
+ | "resume.principal_mismatch"
1815
1842
  /** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
1816
1843
  * constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
1817
1844
  * edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
@@ -1839,6 +1866,9 @@ export declare class CheckpointError extends Error {
1839
1866
  * discriminant. */
1840
1867
  readonly detail?: {
1841
1868
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
1869
+ /** #449 G1 — on `resume.usage_window_exhausted` ONLY: how long until the binding window frees
1870
+ * (the same wait a suspended row carries as `gate.resumeAfterMs`). Absent on every other code. */
1871
+ retryAfterMs?: number;
1842
1872
  /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1843
1873
  * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1844
1874
  * damaged/caller-declared and no worker ever will" — blanket-retriable and gate-shape heuristics were
@@ -1941,6 +1971,23 @@ export declare class CheckpointError extends Error {
1941
1971
  * thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the checkpoint
1942
1972
  * stays `pending`); re-resume with the full original chain. */
1943
1973
  | "resume.parent_constraint_mismatch"
1974
+ /** #449 G1 (codex r2, pre-CAS entrance): a deployment usage-governance window for this resume's
1975
+ * ledger key (the config's principal, else the row's recorded one, else the shared bucket) is
1976
+ * EXHAUSTED, and the row still OWES a delivery a re-mint cannot carry (an approval/review
1977
+ * decision, a wake, parked steers, a background-task obituary snapshot). Refused BEFORE the CAS,
1978
+ * so nothing is consumed and nothing is unpinned — the SAME token with the SAME decision is
1979
+ * redeemable once the window frees (`detail.retryAfterMs` carries the wait). A bare
1980
+ * `resource_limit` continue with nothing parked does NOT take this refusal: it proceeds and
1981
+ * re-suspends at turn 0 into a FRESH checkpoint carrying the new wait hint. */
1982
+ | "resume.usage_window_exhausted"
1983
+ /** #449 G2 (identity continuity): the resume config carries a NON-EMPTY `principal` that differs from
1984
+ * the {@link Checkpoint.principal} recorded at the suspend mint. `ResumeTaskConfig`'s contract is
1985
+ * "the same TaskSpec the run suspended with", and the principal keys real lanes on the resumed leg
1986
+ * (the usage-governance ledger bucket, later mints' scope derivation, session-rule reads, ask
1987
+ * attribution) — silently running the leg under a different identity is the bad-value direction this
1988
+ * refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
1989
+ * original principal, or omit the field to inherit the recorded one. */
1990
+ | "resume.principal_mismatch"
1944
1991
  /** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
1945
1992
  * constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
1946
1993
  * edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
@@ -1968,6 +2015,9 @@ export declare class CheckpointError extends Error {
1968
2015
  * discriminant. */
1969
2016
  detail?: {
1970
2017
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
2018
+ /** #449 G1 — on `resume.usage_window_exhausted` ONLY: how long until the binding window frees
2019
+ * (the same wait a suspended row carries as `gate.resumeAfterMs`). Absent on every other code. */
2020
+ retryAfterMs?: number;
1971
2021
  /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1972
2022
  * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1973
2023
  * damaged/caller-declared and no worker ever will" — blanket-retriable and gate-shape heuristics were
@@ -177,11 +177,10 @@ export interface MaterializedMcp {
177
177
  * fails the WHOLE model request (provider 400), bricking the task for every healthy tool
178
178
  * (CC 2.1.216 ships the same intake prune, `tengu_mcp_drop_invalid_tool_schemas`).
179
179
  *
180
- * HONEST REACH (pinned in mcp-dropped-tools.test.ts): with the bundled TS MCP SDK, a string-root
181
- * schema already fails the SDK client's OWN zod at `listTools` the whole server is skipped
182
- * (warnings face) and this gate never sees the listing, losing healthy sibling tools with it
183
- * (SDK-upstream failure mode, recorded). This per-tool gate is DEFENSE IN DEPTH: it binds when a
184
- * transport/SDK revision delivers schemas the client does not fully validate.
180
+ * HONEST REACH (pinned in mcp-dropped-tools.test.ts): listings arrive through the lenient reader
181
+ * (`listToolsLenient` raw request, no SDK-client zod pass), so a string-root or otherwise odd
182
+ * schema DOES reach this gate and is judged per tool; healthy siblings survive. This per-tool gate
183
+ * is therefore the PRIMARY adjudicator for schema shape, not a fallback behind SDK validation.
185
184
  *
186
185
  * The drop is NEVER silent, two faces (CC parity — its `mcp_dropped_tools_delta` system reminder):
187
186
  * - operator: prepare-task forwards each entry to `onError(phase:"mcp")` alongside `warnings`;
@@ -406,12 +406,11 @@ export declare function memorySessionPollutedNotice(input: {
406
406
  * arithmetic would report a clean containment while a known failure sat in the report. Every
407
407
  * not-moved file does have an escalation row, so `escalated` is the superset and the honest clause.
408
408
  *
409
- * KNOWN GAP (adversarial-review round 1, P2 registered, not closed here): the derived index is
410
- * contained by a different path (`restorePollutedIndex` captures and restores `MEMORY.md`) that mints
411
- * no rejection row, so a polluted session whose ONLY memory change was an index line produces no
412
- * `count` and no notice its containment is disclosed by the harvest report's warnings alone. Closing
413
- * it needs a structured containment signal on `HarvestReport`, a public type this fix does not touch.
414
- * The wording therefore says "entry file(s)" rather than implying the whole containment.
409
+ * The derived index is contained by a different path (`restorePollutedIndex` captures and restores
410
+ * `MEMORY.md`) that mints no rejection row; that gap was CLOSED by the structured containment signal
411
+ * on `HarvestReport.containment` (indexRolledBack see the containment block below), so an
412
+ * index-only polluted session now signals and notices instead of hiding in warnings prose. The
413
+ * wording says "entry file(s)" because index containment reports through its own signal.
415
414
  *
416
415
  * `reason` is optional because the mark record is read separately from the report: a record that cannot
417
416
  * be read must not turn a true "N files were withheld" into a lie about why.
@@ -3965,6 +3965,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3965
3965
  : undefined,
3966
3966
  resourceLedger: resourceLedgerOut,
3967
3967
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3968
+ ...(spec.principal ? { principal: spec.principal } : {}),
3968
3969
  };
3969
3970
  if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3970
3971
  return false;
@@ -1,6 +1,7 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
4
+ import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
4
5
  import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
5
6
  import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
6
7
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
@@ -344,9 +345,11 @@ export async function raceUntilDeadline(p, deadline) {
344
345
  function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
345
346
  const message = moment === "entry"
346
347
  ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
347
- : reason === "env_lifetime"
348
- ? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
349
- : `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
348
+ : moment === "resume"
349
+ ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the resumed leg was stopped BEFORE its first model call nothing was delivered or executed on this leg. The window frees in ${String(retryAfterMs ?? 0)}ms.`
350
+ : reason === "env_lifetime"
351
+ ? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused — see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
352
+ : `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
350
353
  const e = new Error(message);
351
354
  e.code = reason === "env_lifetime" ? "env.lifetime_expired" : "usage.window_exhausted";
352
355
  if (retryAfterMs !== undefined)
@@ -3186,7 +3189,29 @@ export class Runner {
3186
3189
  }
3187
3190
  if (resume) {
3188
3191
  const walltimeExhaustedResume = effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0;
3189
- if (!walltimeExhaustedResume && resume.outcome.gate !== "wake") {
3192
+ const resumeWindowRetryAfterMs = !walltimeExhaustedResume && prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
3193
+ if (resumeWindowRetryAfterMs !== undefined) {
3194
+ const owesNothing = resume.outcome.gate === "resource_limit" &&
3195
+ readPendingSteerQueue(resume.cp.state).length === 0 &&
3196
+ (resume.cp.state.runningBackgroundTasks?.length ?? 0) === 0;
3197
+ const committed = owesNothing &&
3198
+ prepared.suspendForPlatformLimit !== undefined &&
3199
+ (await prepared.suspendForPlatformLimit("usage_window", { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) }, { resumeAfterMs: resumeWindowRetryAfterMs }));
3200
+ if (committed) {
3201
+ rs.limits.turnsExceeded = false;
3202
+ rs.limits.budgetHit = undefined;
3203
+ }
3204
+ else {
3205
+ const terminal = platformLimitTerminal("usage_window", resumeWindowRetryAfterMs, "resume");
3206
+ rs.limits.platformTerminal = terminal;
3207
+ try {
3208
+ this.deps.onError?.(terminal, { phase: "config", sessionId: prepared.sessionId });
3209
+ }
3210
+ catch {
3211
+ }
3212
+ }
3213
+ }
3214
+ if (resumeWindowRetryAfterMs === undefined && !walltimeExhaustedResume && resume.outcome.gate !== "wake") {
3190
3215
  await this.applyResumeDecision(prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
3191
3216
  if (rs.attach.attachState === undefined)
3192
3217
  return;
@@ -3205,7 +3230,9 @@ export class Runner {
3205
3230
  resume.decisionDelivered = true;
3206
3231
  announceWorkspaceMove();
3207
3232
  }
3208
- if (walltimeExhaustedResume) {
3233
+ if (resumeWindowRetryAfterMs !== undefined) {
3234
+ }
3235
+ else if (walltimeExhaustedResume) {
3209
3236
  timeout.fired = true;
3210
3237
  timeout.clear();
3211
3238
  void prepared.harness.abort();
@@ -4455,6 +4482,29 @@ export class Runner {
4455
4482
  throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
4456
4483
  "resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
4457
4484
  }
4485
+ {
4486
+ const rowPrincipal = cp.principal || undefined;
4487
+ const suppliedPrincipal = taskConfig.principal || undefined;
4488
+ if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
4489
+ throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
4490
+ "running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
4491
+ "refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
4492
+ }
4493
+ const owesDelivery = outcomeGate !== "resource_limit" ||
4494
+ readPendingSteerQueue(cp.state).length > 0 ||
4495
+ (cp.state.runningBackgroundTasks?.length ?? 0) > 0;
4496
+ if (owesDelivery && this.deps.usageWindowStore !== undefined) {
4497
+ const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
4498
+ if (preCasWindows !== undefined && preCasWindows.length > 0) {
4499
+ const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
4500
+ const wait = usageRetryAfterMs(await this.deps.usageWindowStore.read(ledgerKey, preCasWindows, Date.now()));
4501
+ if (wait !== undefined) {
4502
+ throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
4503
+ `refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
4504
+ }
4505
+ }
4506
+ }
4507
+ }
4458
4508
  if (plainPolicyOutcome !== undefined &&
4459
4509
  plainPolicyOutcome.decision === "allow" &&
4460
4510
  plainPolicyOutcome.updatedInput !== undefined &&
@@ -4571,6 +4621,7 @@ export class Runner {
4571
4621
  : undefined;
4572
4622
  const spec = {
4573
4623
  ...taskConfig,
4624
+ ...((taskConfig.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
4574
4625
  objective: "",
4575
4626
  sessionId: cp.sessionId,
4576
4627
  requireExistingSession: true,
@@ -413,8 +413,14 @@ export interface ToolPolicyProjection {
413
413
  *
414
414
  * Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
415
415
  * `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
416
- * list these): all four publishers of {@link ToolPolicyNameSets} must resolve a covering entry the same
417
- * way, because the prepare-time audit's exemption for these spellings speaks for all of them at once.
416
+ * list these two): every publisher of {@link ToolPolicyNameSets} must resolve a covering entry the same
417
+ * way, because the prepare-time audit's exemption for these spellings speaks for ALL published name sets
418
+ * at once — the four in-engine lanes do so by sharing this module's matcher. #450: that premise is NOT
419
+ * structural for an OUT-OF-ENGINE publisher (a host compiling its own settings into an exact-match ask
420
+ * closure is fail-open on these spellings while the audit stays silent) — which is why the underlying
421
+ * judgment predicates (`isNamespacedCoveringRuleName` / `namespacedRuleNameCovers`) ARE exported from
422
+ * the package index: an external lane either compiles through `createPermissionRulePolicy` or
423
+ * self-checks with those.
418
424
  */
419
425
  export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
420
426
  /** Does any covering entry reach `toolName`? See {@link namespacedCoveringEntries}. */
package/dist/index.d.ts CHANGED
@@ -140,7 +140,7 @@ export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, create
140
140
  export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassifyFn, type AutoModeClassifyInput, } from "./core/auto-mode.js";
141
141
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, type AutoModeRules, type BuildAutoModePromptOptions, type AutoModeWindowOptions, } from "./core/auto-mode-prompt.js";
142
142
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
143
- export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, type PermissionRule, type ParsedPermissionRule, type PermissionRuleIssue, type PermissionRuleCaps, type PermissionRulePolicyOptions, } from "./core/permission-rules.js";
143
+ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, isNamespacedCoveringRuleName, namespacedRuleNameCovers, type PermissionRule, type ParsedPermissionRule, type PermissionRuleIssue, type PermissionRuleCaps, type PermissionRulePolicyOptions, } from "./core/permission-rules.js";
144
144
  /**
145
145
  * design/179 — persisted ALLOW rules: the standing form of approvals a person already gave.
146
146
  *
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, create
117
117
  export { parseAutoModeResponse, createAutoModeDecider, } from "./core/auto-mode.js";
118
118
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, } from "./core/auto-mode-prompt.js";
119
119
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
120
- export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, } from "./core/permission-rules.js";
120
+ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, isNamespacedCoveringRuleName, namespacedRuleNameCovers, } from "./core/permission-rules.js";
121
121
  export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
122
122
  export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, } from "./core/permission-rule-store.js";
123
123
  export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, } from "./core/permission-rule-sync.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.60.0",
3
+ "version": "5.60.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
3
  "_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
4
- "count": 1758,
4
+ "count": 1760,
5
5
  "exports": {
6
6
  "A2ATaskState": "type",
7
7
  "A2ATaskStateReversal": "type",
@@ -1463,6 +1463,7 @@
1463
1463
  "isInstructionEntry": "function",
1464
1464
  "isIsolated": "function",
1465
1465
  "isModelGatedForClass": "function",
1466
+ "isNamespacedCoveringRuleName": "function",
1466
1467
  "isPersonalScope": "function",
1467
1468
  "isQuestionUnavailable": "function",
1468
1469
  "isRemoteExecutionEnv": "function",
@@ -1525,6 +1526,7 @@
1525
1526
  "missingRestoreSurface": "function",
1526
1527
  "modelCostToPricing": "function",
1527
1528
  "mysqlQuery": "function",
1529
+ "namespacedRuleNameCovers": "function",
1528
1530
  "nextSyncBaseline": "function",
1529
1531
  "normalizeAgentName": "function",
1530
1532
  "normalizeBaseUrl": "function",
@@ -3223,6 +3225,7 @@
3223
3225
  "isInstructionEntry": "advanced",
3224
3226
  "isIsolated": "advanced",
3225
3227
  "isModelGatedForClass": "advanced",
3228
+ "isNamespacedCoveringRuleName": "advanced",
3226
3229
  "isPersonalScope": "advanced",
3227
3230
  "isQuestionUnavailable": "advanced",
3228
3231
  "isRemoteExecutionEnv": "advanced",
@@ -3285,6 +3288,7 @@
3285
3288
  "missingRestoreSurface": "advanced",
3286
3289
  "modelCostToPricing": "advanced",
3287
3290
  "mysqlQuery": "advanced",
3291
+ "namespacedRuleNameCovers": "advanced",
3288
3292
  "nextSyncBaseline": "advanced",
3289
3293
  "normalizeAgentName": "advanced",
3290
3294
  "normalizeBaseUrl": "advanced",