@sema-agent/core 5.0.0 → 5.0.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.0.1 (2026-08-02)
4
+
5
+ _Post-release review patch: the 5.0.0 review wave (five domain reviewers + two independent codex legs) surfaced silent-widening gaps in the new audit surfaces plus wiring/anchoring stragglers. All fixes are loud-direction; no new API._
6
+
7
+ **Fixed**
8
+
9
+ - **Persisted session/ancestor rules join the retired-name audit (review G-2).** Session-rule policies were composed after the only policy-name audit pass and exposed no name metadata — a stored `{toolDeny: ["bash"]}` compiled clean and silently widened. The audit is now a reusable pass run over every narrowing layer, and `createSessionRulePolicy` returns a `NamedToolPolicy`.
10
+ - **Pre-prefix MCP names reach the hard arm (review G-3).** The `config.legacy_tool_name` check only consulted the static `RETIRED_TOOL_NAMES` map, which cannot enumerate dynamic MCP names — a v4-era policy entry like `github__delete_repository` (auto-prefixed by the retired canonicalizer) degraded to an advisory and the destructive call ran. An unmatched `__`-shaped name without the `mcp__` prefix now hard-fails prepare with prefix guidance; an `mcp__*` miss stays advisory (the server may not be mounted this run).
11
+ - **Repair-loop accounts stay live across an in-flight attempt (review G-4, time half).** A checkpoint minted mid-attempt clones the threaded bundle at that instant, and only a pre-dispatch stamp existed — an attempt that ran 900ms and parked persisted ~zero elapsed time, so the resume re-granted the washed budget. An unref'd 1s interval keeps the stamp current. (The money half — mid-flight partial spend — is tracked separately: it has a double-count design question against the resumed attempt's own stats.)
12
+ - **Permission-rule policies join the retired-name audit (review F-6).** `createPermissionRulePolicy` now returns a `NamedToolPolicy` exposing its compiled tool names as `nameSets`, so the prepare-time policy-name audit covers rule policies: a rule written with a RETIRED name (e.g. `{rule: "bash", behavior: "deny"}`) that matches nothing in the run's roster is now the same HARD `config.legacy_tool_name` prepare failure as a list policy — previously it compiled with zero issues and the deny silently guarded nothing. A deployment mounting its own tool under a retired name is unaffected (roster hit). Behavior note: configs carrying such stale rules now fail loudly at prepare instead of running silently widened.
13
+ - **Name-addressed revives anchor correctly (review F-1).** The SendMessage by-name resolution hand-copy dropped `createdAt`, so a `{to: NAME}` revive's spawn frame fell back to the wake instant for `startedAt` — the exact-id rung was fixed in 5.0.0, the name rung is now too.
14
+ - **Two more revive legs chain `root ?? parent` (review F-8).** The revived child's own spec internals and the re-minted registry row now derive the same root anchor as the roster/observer legs — a pre-floor row no longer yields an internally inconsistent agent (fleet view anchored, `canAccess` root arm blind).
15
+ - **The disclosure channel reaches both remaining composition roots (review F-2/F-3).** The runner's SendMessage mount wires `onNotifyError` to the `observer.notify_failed` trace lane, and the delegation tool's steer-lane resume threads `opts.onObserverError` — a throwing deployment sink on either revive path is now disclosed instead of counted on an unread notifier.
16
+ - **Checkpoint mints read the clock once.** All three checkpoint mint points stamped `createdAt` and `deadline` from separate `Date.now()` reads; a mint straddling a ms tick violated `deadline = createdAt + ttl` by 1ms.
17
+ - Comment-truth sweep: 14 sites still narrating the deleted OLD_TO_NEW/tombstone machinery rewritten to post-5.0.0 behavior; `markStopSourceByShellId`'s orphaned docstring relocated and corrected (env is REQUIRED for effect — an env-less call is a no-op).
18
+
3
19
  ## 5.0.0 (2026-08-02)
4
20
 
5
21
  _The baggage-unload major: the compatibility faces RB-476 cleared for tool NAMES are now cleared for id arguments, adapters and indexes too (RB-479), plus the revive-frame anchoring fix a downstream consumer was blocked on (RB-478). Everything that used to resolve silently under an old spelling now refuses loudly with the current spelling named._
@@ -194,7 +194,15 @@ export async function runRepairLoop(runner, implSpec, config) {
194
194
  }
195
195
  : implSpec;
196
196
  restartedOnce = false;
197
- const result = await runner.runTask(spec, { repairBundle: bundle });
197
+ const liveStamp = setInterval(stampAccount, 1_000);
198
+ liveStamp.unref?.();
199
+ let result;
200
+ try {
201
+ result = await runner.runTask(spec, { repairBundle: bundle });
202
+ }
203
+ finally {
204
+ clearInterval(liveStamp);
205
+ }
198
206
  spend += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0);
199
207
  stampAccount();
200
208
  accumulate(result.stats);
@@ -369,7 +369,7 @@ export function createSendMessageTool(opts) {
369
369
  }
370
370
  if (byName.status === "found") {
371
371
  const h = byName.handle;
372
- target = { task_id: h.id, type: h.type, status: h.status, ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}), ...(h.sessionScoped ? { sessionScoped: true } : {}), ...(h.description !== undefined ? { description: h.description } : {}), ...(h.name !== undefined ? { name: h.name } : {}), ...(h.agentType !== undefined ? { agentType: h.agentType } : {}), ...(h.owner !== undefined ? { owner: h.owner } : {}), ...(h.scope !== undefined ? { scope: h.scope } : {}), ...(h.parentTaskId !== undefined ? { parentTaskId: h.parentTaskId } : {}), ...(h.parentSessionId !== undefined ? { parentSessionId: h.parentSessionId } : {}), ...(h.rootSessionId !== undefined ? { rootSessionId: h.rootSessionId } : {}) };
372
+ target = { task_id: h.id, type: h.type, status: h.status, createdAt: h.createdAt, ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}), ...(h.sessionScoped ? { sessionScoped: true } : {}), ...(h.description !== undefined ? { description: h.description } : {}), ...(h.name !== undefined ? { name: h.name } : {}), ...(h.agentType !== undefined ? { agentType: h.agentType } : {}), ...(h.owner !== undefined ? { owner: h.owner } : {}), ...(h.scope !== undefined ? { scope: h.scope } : {}), ...(h.parentTaskId !== undefined ? { parentTaskId: h.parentTaskId } : {}), ...(h.parentSessionId !== undefined ? { parentSessionId: h.parentSessionId } : {}), ...(h.rootSessionId !== undefined ? { rootSessionId: h.rootSessionId } : {}) };
373
373
  }
374
374
  else {
375
375
  let rosterHit;
@@ -1381,7 +1381,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1381
1381
  ? {
1382
1382
  ...(reviveClaim.row.parentTaskId !== undefined ? { parentTaskId: reviveClaim.row.parentTaskId } : {}),
1383
1383
  ...(reviveClaim.row.parentSessionId !== undefined ? { parentSessionId: reviveClaim.row.parentSessionId } : {}),
1384
- ...(reviveClaim.row.rootSessionId !== undefined ? { rootSessionId: reviveClaim.row.rootSessionId } : {}),
1384
+ ...((reviveClaim.row.rootSessionId ?? reviveClaim.row.parentSessionId) !== undefined
1385
+ ? { rootSessionId: reviveClaim.row.rootSessionId ?? reviveClaim.row.parentSessionId }
1386
+ : {}),
1385
1387
  }
1386
1388
  : {
1387
1389
  ...(ctx.taskId !== undefined ? { parentTaskId: ctx.taskId } : {}),
@@ -2045,7 +2047,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2045
2047
  ...(reviveRow.name !== undefined ? { name: reviveRow.name } : {}),
2046
2048
  ...(reviveRow.parentTaskId !== undefined ? { parentTaskId: reviveRow.parentTaskId } : {}),
2047
2049
  ...(reviveRow.parentSessionId !== undefined ? { parentSessionId: reviveRow.parentSessionId } : {}),
2048
- ...(reviveRow.rootSessionId !== undefined ? { rootSessionId: reviveRow.rootSessionId } : {}),
2050
+ ...((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined
2051
+ ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId }
2052
+ : {}),
2049
2053
  ...(reviveRow.model !== undefined ? { model: reviveRow.model } : {}),
2050
2054
  ...(reviveRow.teamName !== undefined ? { teamName: reviveRow.teamName } : {}),
2051
2055
  deliveryChannel: "attaching",
@@ -2673,6 +2677,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2673
2677
  ...(opts.background?.notify ? { notify: opts.background.notify } : {}),
2674
2678
  sink: ctx.onSubagentSpawn,
2675
2679
  ...(opts.background ? { registry: opts.background.registry } : {}),
2680
+ ...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
2676
2681
  });
2677
2682
  notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
2678
2683
  resume,
@@ -1,4 +1,4 @@
1
- import type { ToolPolicy } from "./tool-policy.js";
1
+ import type { NamedToolPolicy } from "./tool-policy.js";
2
2
  export declare const BASH_GENERIC_PARAMS: ReadonlySet<string>;
3
3
  export interface PermissionRule {
4
4
  rule: string;
@@ -32,4 +32,4 @@ export declare function parsePermissionRule(rule: string): ParsedPermissionRule;
32
32
  export declare function wildcardMatch(pattern: string, value: string): boolean;
33
33
  export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
34
34
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps">): PermissionRuleIssue[];
35
- export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): ToolPolicy;
35
+ export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;
@@ -259,7 +259,17 @@ export function createPermissionRulePolicy(rules, opts) {
259
259
  }
260
260
  }
261
261
  const defaultAction = opts?.defaultAction ?? "allow";
262
+ const nameSets = { deny: [], ask: [], allow: [] };
263
+ for (const [toolName, entry] of byTool) {
264
+ if (entry.bare.deny !== undefined || entry.param.deny.length > 0)
265
+ nameSets.deny.push(toolName);
266
+ if (entry.bare.ask !== undefined || entry.param.ask.length > 0)
267
+ nameSets.ask.push(toolName);
268
+ if (entry.bare.allow !== undefined)
269
+ nameSets.allow.push(toolName);
270
+ }
262
271
  return {
272
+ nameSets: [nameSets],
263
273
  check(req) {
264
274
  const entry = byTool.get(req.toolName);
265
275
  if (entry) {
@@ -1378,6 +1378,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1378
1378
  ...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
1379
1379
  ...(deps.mailboxStore !== undefined ? { mailbox: deps.mailboxStore } : {}),
1380
1380
  ...(reviveSpawn !== undefined ? { reviveSpawn } : {}),
1381
+ onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })),
1381
1382
  })));
1382
1383
  if (!(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
1383
1384
  tools.push(firstPartyOffload(createAgentTranscriptTool({
@@ -2190,8 +2191,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2190
2191
  }
2191
2192
  }
2192
2193
  const policy = spec.toolPolicy ?? deps.toolPolicy;
2193
- {
2194
- const nameGroups = toolPolicyNameSets(policy);
2194
+ const auditPolicyNames = (auditee) => {
2195
+ const nameGroups = toolPolicyNameSets(auditee);
2195
2196
  if (nameGroups.length > 0) {
2196
2197
  const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
2197
2198
  for (const t of harnessTools)
@@ -2208,6 +2209,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2208
2209
  err.code = "config.legacy_tool_name";
2209
2210
  throw err;
2210
2211
  }
2212
+ if (n.includes("__") && !n.startsWith("mcp__")) {
2213
+ const err = new Error(`tool policy ${kind}-list entry "${n}" is a pre-prefix MCP tool name and matches nothing in this run's roster — MCP tools are named "mcp__<server>__<tool>" and legacy-name normalization was removed (RB-476-A), so this entry would silently guard nothing. Prefix the entry with "mcp__".`);
2214
+ err.code = "config.legacy_tool_name";
2215
+ throw err;
2216
+ }
2211
2217
  unmatched.add(`"${n}" (${kind})`);
2212
2218
  }
2213
2219
  }
@@ -2231,7 +2237,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2231
2237
  }
2232
2238
  }
2233
2239
  }
2234
- }
2240
+ };
2241
+ auditPolicyNames(policy);
2235
2242
  let sessionRulePolicy;
2236
2243
  if (deps.sessionPolicyStore && spec.sessionId) {
2237
2244
  const rules = await deps.sessionPolicyStore.getRules(spec.sessionId, spec.principal);
@@ -2268,6 +2275,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2268
2275
  })
2269
2276
  : undefined;
2270
2277
  const narrowingLayers = [sessionRulePolicy, ...ancestorRulePolicies, skillScopePolicy].filter((p) => p !== undefined);
2278
+ for (const layer of narrowingLayers)
2279
+ auditPolicyNames(layer);
2271
2280
  const denyNarrowingPolicy = narrowingLayers.length === 0 ? undefined : narrowingLayers.length === 1 ? narrowingLayers[0] : combinePolicies(...narrowingLayers);
2272
2281
  const basePolicyForResumeEdit = spec.toolPolicy ?? deps.toolPolicy;
2273
2282
  const sameInstanceAncestorCount = policy === undefined ? 0 : (inheritedParentConstraints ?? []).reduce((n, pc) => (pc.policy === policy ? n + 1 : n), 0);
@@ -2878,6 +2887,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2878
2887
  const token = mintCheckpointToken();
2879
2888
  const gate = { kind: "resource_limit", reason };
2880
2889
  const resourceLedgerOut = debitLedger(priorLedger, sliceSpend, resourceTotal);
2890
+ const mintedAt = Date.now();
2881
2891
  const cp = {
2882
2892
  token,
2883
2893
  version: resourceLedgerOut.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
@@ -2888,8 +2898,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2888
2898
  pendingAction: { kind: "resource_limit", reason },
2889
2899
  state: serializeCheckpointState(remoteHandle),
2890
2900
  status: "pending",
2891
- createdAt: Date.now(),
2892
- deadline: Date.now() + (sanitizedTtlMs(rs.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2901
+ createdAt: mintedAt,
2902
+ deadline: mintedAt + (sanitizedTtlMs(rs.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2893
2903
  suspendCount: priorSuspendCount,
2894
2904
  humanReview: humanReviewRef.count > 0
2895
2905
  ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
@@ -2958,6 +2968,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2958
2968
  const scope = checkpointScopeOf(spec);
2959
2969
  const gate = { kind: "plan_review", reason: reason ?? `plan review requested` };
2960
2970
  const reviewLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
2971
+ const mintedAt = Date.now();
2961
2972
  const cp = {
2962
2973
  token,
2963
2974
  version: reviewLedger.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
@@ -2968,9 +2979,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2968
2979
  pendingAction: { kind: "plan_review" },
2969
2980
  state: serializeCheckpointState(remoteHandle),
2970
2981
  status: "pending",
2971
- createdAt: Date.now(),
2982
+ createdAt: mintedAt,
2972
2983
  suspendedAt: now(),
2973
- deadline: Date.now() + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2984
+ deadline: mintedAt + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2974
2985
  suspendCount: priorSuspendCount + 1,
2975
2986
  humanReview: humanReviewRef.count > 0
2976
2987
  ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
@@ -3082,6 +3093,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3082
3093
  const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
3083
3094
  const checkpointState = serializeCheckpointState(remoteHandle);
3084
3095
  const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
3096
+ const mintedAt = Date.now();
3085
3097
  cp = {
3086
3098
  token,
3087
3099
  version: approvalLedger.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
@@ -3104,12 +3116,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3104
3116
  },
3105
3117
  state: checkpointState,
3106
3118
  deadline: ttlMs !== undefined
3107
- ? Date.now() + ttlMs
3119
+ ? mintedAt + ttlMs
3108
3120
  : durableApproval !== undefined
3109
3121
  ? undefined
3110
- : Date.now() + DEFAULT_UNATTENDED_APPROVAL_TTL_MS,
3122
+ : mintedAt + DEFAULT_UNATTENDED_APPROVAL_TTL_MS,
3111
3123
  status: "pending",
3112
- createdAt: Date.now(),
3124
+ createdAt: mintedAt,
3113
3125
  suspendedAt: now(),
3114
3126
  humanReview: humanReviewRef.count > 0
3115
3127
  ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
@@ -1,6 +1,6 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { ToolEffect } from "../types.js";
3
- import { type ToolPolicy } from "../tool-policy.js";
3
+ import { type NamedToolPolicy } from "../tool-policy.js";
4
4
  import type { SessionPermissionRules } from "../session-policy-store.js";
5
5
  export declare const PATH_WRITE_TOOLS: ReadonlySet<string>;
6
6
  export declare function isWithin(root: string, p: string): boolean;
@@ -8,4 +8,4 @@ export declare function createSessionRulePolicy(rules: SessionPermissionRules, o
8
8
  env: ExecutionEnv;
9
9
  rootPath?: string;
10
10
  toolEffects?: ReadonlyMap<string, ToolEffect>;
11
- }): ToolPolicy;
11
+ }): NamedToolPolicy;
@@ -32,6 +32,7 @@ export function createSessionRulePolicy(rules, opts) {
32
32
  : undefined;
33
33
  const allowDirs = rules.allowDirs && rules.allowDirs.length > 0 ? rules.allowDirs : undefined;
34
34
  return {
35
+ nameSets: [{ ...(rules.toolDeny?.length ? { deny: [...rules.toolDeny] } : {}), ...(rules.toolAllow?.length ? { allow: [...rules.toolAllow] } : {}) }],
35
36
  async check(req, signal) {
36
37
  const toolName = req.toolName;
37
38
  if (toolDeny.has(toolName))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",