@sema-agent/core 5.38.0 → 5.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +182 -10
  2. package/dist/agents/send-message-tool.d.ts +8 -0
  3. package/dist/agents/send-message-tool.js +8 -0
  4. package/dist/agents/teacher.js +9 -3
  5. package/dist/agents/verify.js +9 -3
  6. package/dist/core/checkpoint-store.d.ts +12 -0
  7. package/dist/core/governance-codes.js +2 -0
  8. package/dist/core/hooks.d.ts +23 -0
  9. package/dist/core/hooks.js +53 -4
  10. package/dist/core/mailbox-store.d.ts +39 -0
  11. package/dist/core/mailbox-store.js +9 -0
  12. package/dist/core/memory-engine/engine.d.ts +27 -0
  13. package/dist/core/memory-engine/engine.js +103 -1
  14. package/dist/core/memory-engine/export-bundle.d.ts +192 -0
  15. package/dist/core/memory-engine/export-bundle.js +306 -0
  16. package/dist/core/memory-engine/file-backend.d.ts +178 -1
  17. package/dist/core/memory-engine/file-backend.js +637 -6
  18. package/dist/core/memory-engine/index.d.ts +2 -1
  19. package/dist/core/memory-engine/index.js +1 -0
  20. package/dist/core/memory-engine/layout.d.ts +89 -1
  21. package/dist/core/memory-engine/layout.js +131 -1
  22. package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
  23. package/dist/core/memory-engine/memory-backend-contract.js +52 -0
  24. package/dist/core/memory-engine/tools.js +8 -1
  25. package/dist/core/permission-rule-consent.d.ts +27 -4
  26. package/dist/core/permission-rule-consent.js +41 -4
  27. package/dist/core/permission-rule-model.d.ts +7 -1
  28. package/dist/core/runner/prepare-task.js +24 -3
  29. package/dist/core/runner/runtask.js +5 -0
  30. package/dist/core/runner/synthetic-tools.js +3 -1
  31. package/dist/core/runner/tool-disclosure.js +2 -1
  32. package/dist/core/sensitive-path-policy.js +3 -3
  33. package/dist/core/store-contracts/mailbox-store-contract.d.ts +29 -1
  34. package/dist/core/store-contracts/mailbox-store-contract.js +78 -0
  35. package/dist/core/types.d.ts +21 -0
  36. package/dist/core/write-protect.d.ts +73 -0
  37. package/dist/core/write-protect.js +195 -0
  38. package/dist/index.d.ts +4 -3
  39. package/dist/index.js +4 -3
  40. package/dist/orchestration/governance-baseline-validity.d.ts +44 -0
  41. package/dist/orchestration/governance-baseline-validity.js +55 -0
  42. package/dist/orchestration/run-workflow-tool.js +33 -8
  43. package/dist/orchestration/workflow-script-runner.js +9 -4
  44. package/dist/tools/fs/read-deny.d.ts +15 -5
  45. package/dist/tools/fs/read-deny.js +33 -12
  46. package/dist/tools/fs/safety.d.ts +5 -2
  47. package/dist/tools/fs/safety.js +5 -3
  48. package/dist/tools/fs/search.d.ts +33 -0
  49. package/dist/tools/fs/search.js +72 -0
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +21 -1
package/dist/index.js CHANGED
@@ -71,6 +71,7 @@ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, }
71
71
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
72
72
  export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, } from "./tools/fs/index.js";
73
73
  export { deploymentReadFaceClampNotice, resolveReadFace } from "./tools/fs/index.js";
74
+ export { WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, compileWriteProtection, } from "./core/write-protect.js";
74
75
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, } from "./core/tool-result-store.js";
75
76
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
76
77
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
@@ -93,7 +94,7 @@ export {} from "./core/checkpoint-store.js";
93
94
  export {} from "./core/checkpoint-store.js";
94
95
  export { projectHumanInput, buildHumanInputEvent, } from "./core/human-input-projection.js";
95
96
  export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
96
- export { InMemoryMailboxStore } from "./core/mailbox-store.js";
97
+ export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, } from "./core/mailbox-store.js";
97
98
  export { FileMailboxStore } from "./stores/file/mailbox-store.js";
98
99
  export { createFileTaskListStore } from "./stores/file/task-list-store.js";
99
100
  export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
@@ -127,7 +128,7 @@ export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootA
127
128
  export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, } from "./stores/file/adoption/adopt.js";
128
129
  export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, normalizePersistedRuleHit, } from "./core/hooks.js";
129
130
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
130
- export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, readV2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, erasureSelectHash, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
131
+ export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, readV2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, erasureSelectHash, computeMemoryBundleHash, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
131
132
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, } from "./core/shared-memory/types.js";
132
133
  export { sharedMemoryStoreContract, } from "./core/shared-memory/contract.js";
133
134
  export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
@@ -178,7 +179,7 @@ export { sessionRepoContract } from "./core/store-contracts/session-repo-contrac
178
179
  export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
179
180
  export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
180
181
  export { permissionRuleSyncContract } from "./core/store-contracts/permission-rule-sync-contract.js";
181
- export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
182
+ export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, mailboxTombstonedRecipientContract, } from "./core/store-contracts/mailbox-store-contract.js";
182
183
  export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
183
184
  export { canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
184
185
  export { serveDurableAgentRowLane, buildAgentPollDetails, } from "./core/task-registry-agent.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * design/98 §6.3 — the ONE reading of "is this governance baseline usable", shared by the deployment
3
+ * CAPABILITY predicate (`workflowsCapability`, which drives the surfaced `/workflows` affordance AND
4
+ * the orchestration-prompt injection) and the MOUNT-time validator (`createRunWorkflowTool`, which
5
+ * refuses loudly with a code).
6
+ *
7
+ * Why one function rather than two agreeing predicates: this seam split three times inside one
8
+ * release window, each time one level deeper. First the capability tested `!== undefined` while the
9
+ * mount tested truthiness (a JSON `null` announced workflows the tool never mounted). Then both
10
+ * tested the container while the mount refused an unusable `base` SLOT (`{ base: null }` announced
11
+ * workflows that hard-failed every opted-in task). Then the slot readings agreed but the mount
12
+ * additionally refused a malformed `worktreeBase` and malformed face lists — the same split again,
13
+ * one field further in. Two predicates that must "stay in agreement" are a standing invitation to
14
+ * drift; the only shape that cannot drift is a single function both call.
15
+ *
16
+ * The contract: {@link governanceBaselineProblem} returns `null` when the mount would SUCCEED, and
17
+ * a describable problem when the mount would REFUSE. Every mount-time refusal must be reachable
18
+ * from here, so a deployment is never told it has workflows it cannot actually run.
19
+ */
20
+ export interface GovernanceBaselineProblem {
21
+ /** Which slot/field is wrong, in the spelling a deployment configures (`base.excludeTools`). */
22
+ readonly where: string;
23
+ /** What was found there, for the operator-facing message (`null`, `a string`, `an array`). */
24
+ readonly found: string;
25
+ /** The coded refusal the mount raises for this problem. */
26
+ readonly code: "config.invalid_governance_baseline" | "config.invalid_tool_name_set";
27
+ }
28
+ /** Name a bad value WITHOUT throwing on it (a bigint / cyclic object / poisoned `toJSON` must not
29
+ * crash the describer and cost the refusal its name). */
30
+ export declare function describeBaselineValue(v: unknown): string;
31
+ /**
32
+ * The single validity reading. `null` ⇒ the mount would succeed ⇒ the capability may be announced.
33
+ *
34
+ * `base` is the control plane every script-spawned child inherits, so a non-object there refuses:
35
+ * ungoverned is spelled by not configuring `workflowGovernanceBaseline` at all, never by an
36
+ * unusable anchor. `worktreeBase` is the optional overlay whose documented absence means "base
37
+ * governs every child" — `null`/absent is that absence, any other non-object is garbage and refuses
38
+ * rather than impersonating either absence or an overlay.
39
+ */
40
+ export declare function governanceBaselineProblem(baseline: unknown): GovernanceBaselineProblem | null;
41
+ /** The mount's refusal, minted from the shared reading so the message and the code have one home. */
42
+ export declare function governanceBaselineError(p: GovernanceBaselineProblem): Error & {
43
+ code: string;
44
+ };
@@ -0,0 +1,55 @@
1
+ const FACE_LIST_FIELDS = ["excludeTools", "deferTools", "alwaysLoadTools"];
2
+ export function describeBaselineValue(v) {
3
+ return v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`;
4
+ }
5
+ function isSpecObject(v) {
6
+ return v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v);
7
+ }
8
+ function faceListProblem(slot, where) {
9
+ for (const field of FACE_LIST_FIELDS) {
10
+ const v = slot[field];
11
+ if (v === undefined || v === null)
12
+ continue;
13
+ if (!Array.isArray(v)) {
14
+ return { where: `${where}.${field}`, found: describeBaselineValue(v), code: "config.invalid_tool_name_set" };
15
+ }
16
+ for (const [i, entry] of v.entries()) {
17
+ if (typeof entry !== "string") {
18
+ return {
19
+ where: `${where}.${field}`,
20
+ found: `entry ${i} is ${describeBaselineValue(entry)}`,
21
+ code: "config.invalid_tool_name_set",
22
+ };
23
+ }
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ export function governanceBaselineProblem(baseline) {
29
+ if (!isSpecObject(baseline)) {
30
+ return { where: "workflowGovernanceBaseline", found: describeBaselineValue(baseline), code: "config.invalid_governance_baseline" };
31
+ }
32
+ const base = baseline.base;
33
+ if (!isSpecObject(base)) {
34
+ return { where: "base", found: describeBaselineValue(base), code: "config.invalid_governance_baseline" };
35
+ }
36
+ const baseFaces = faceListProblem(base, "base");
37
+ if (baseFaces !== null)
38
+ return baseFaces;
39
+ const wt = baseline.worktreeBase;
40
+ if (wt === null || wt === undefined)
41
+ return null;
42
+ if (!isSpecObject(wt)) {
43
+ return { where: "worktreeBase", found: describeBaselineValue(wt), code: "config.invalid_governance_baseline" };
44
+ }
45
+ return faceListProblem(wt, "worktreeBase");
46
+ }
47
+ export function governanceBaselineError(p) {
48
+ const detail = p.code === "config.invalid_tool_name_set"
49
+ ? `must be an array of tool names (got ${p.found}) — refusing to mount rather than govern children with a roster nobody can read.`
50
+ : `is ${p.found}, not a spec object — refusing to mount rather than launch children under no governance ` +
51
+ `(to run ungoverned, leave workflowGovernanceBaseline unconfigured).`;
52
+ const e = new Error(`createRunWorkflowTool: the workflow governance baseline's ${p.where} ${detail}`);
53
+ e.code = p.code;
54
+ return e;
55
+ }
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../core/tools.js";
3
+ import { governanceBaselineError, governanceBaselineProblem } from "./governance-baseline-validity.js";
3
4
  import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/untrusted-egress.js";
4
5
  import { startWorkflow } from "./workflow.js";
5
6
  import { buildWorkflowPrimitives } from "./workflow-primitives.js";
@@ -167,16 +168,40 @@ export async function createRunWorkflowTool(d) {
167
168
  childMaxTokens: lim.childMaxTokens,
168
169
  childMaxTurns: lim.childMaxTurns,
169
170
  };
171
+ const problem = governanceBaselineProblem(d.governanceBaseline);
172
+ if (problem !== null)
173
+ throw governanceBaselineError(problem);
174
+ const FACE_LIST_FIELDS = ["excludeTools", "deferTools", "alwaysLoadTools"];
175
+ const dropNullFaces = (slot) => {
176
+ let out = slot;
177
+ for (const field of FACE_LIST_FIELDS) {
178
+ if (slot[field] !== null)
179
+ continue;
180
+ out = { ...out };
181
+ delete out[field];
182
+ }
183
+ return out;
184
+ };
185
+ const sanitizedBaseline = (() => {
186
+ const wt = d.governanceBaseline.worktreeBase;
187
+ const { worktreeBase: _absentOverlay, ...rest } = d.governanceBaseline;
188
+ return {
189
+ ...rest,
190
+ base: dropNullFaces(d.governanceBaseline.base),
191
+ ...(wt === null || wt === undefined ? {} : { worktreeBase: dropNullFaces(wt) }),
192
+ };
193
+ })();
194
+ const unionFaceList = (own, parent) => own === undefined ? [...new Set(parent)] : [...new Set([...own, ...parent])];
170
195
  const withParentFace = (base, inherit) => ({
171
196
  ...base,
172
197
  ...(d.parentExcludeTools?.length
173
- ? { excludeTools: [...new Set([...(base.excludeTools ?? inherit?.excludeTools ?? []), ...d.parentExcludeTools])] }
198
+ ? { excludeTools: unionFaceList(base.excludeTools ?? inherit?.excludeTools, d.parentExcludeTools) }
174
199
  : {}),
175
200
  ...(d.parentDeferTools?.length
176
- ? { deferTools: [...new Set([...(base.deferTools ?? inherit?.deferTools ?? []), ...d.parentDeferTools])] }
201
+ ? { deferTools: unionFaceList(base.deferTools ?? inherit?.deferTools, d.parentDeferTools) }
177
202
  : {}),
178
203
  ...(d.parentAlwaysLoadTools?.length
179
- ? { alwaysLoadTools: [...new Set([...(base.alwaysLoadTools ?? inherit?.alwaysLoadTools ?? []), ...d.parentAlwaysLoadTools])] }
204
+ ? { alwaysLoadTools: unionFaceList(base.alwaysLoadTools ?? inherit?.alwaysLoadTools, d.parentAlwaysLoadTools) }
180
205
  : {}),
181
206
  ...(() => {
182
207
  const parent = d.parentRestoreGatedTools;
@@ -197,13 +222,13 @@ export async function createRunWorkflowTool(d) {
197
222
  const withParentProfile = (base) => d.parentPromptProfile !== undefined && base.promptProfile === undefined ? { ...base, promptProfile: d.parentPromptProfile } : base;
198
223
  const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || (d.parentAlwaysLoadTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined || d.parentRestoreGatedTools !== undefined
199
224
  ? {
200
- ...d.governanceBaseline,
201
- base: withParentProfile(withParentFace(d.governanceBaseline.base)),
202
- ...(d.governanceBaseline.worktreeBase !== undefined
203
- ? { worktreeBase: withParentFace(d.governanceBaseline.worktreeBase, d.governanceBaseline.base) }
225
+ ...sanitizedBaseline,
226
+ base: withParentProfile(withParentFace(sanitizedBaseline.base)),
227
+ ...(sanitizedBaseline.worktreeBase !== undefined
228
+ ? { worktreeBase: withParentFace(sanitizedBaseline.worktreeBase, sanitizedBaseline.base) }
204
229
  : {}),
205
230
  }
206
- : d.governanceBaseline;
231
+ : sanitizedBaseline;
207
232
  const governance = { baseline: baselineWithParentFace, models: d.models, caps: childCaps, onNotice: d.onNotice };
208
233
  const builtinsEnabled = d.builtinWorkflows !== false;
209
234
  const namedWorkflowSection = renderNamedWorkflowListing(await collectNamedWorkflowListings(d.scriptStore, builtinsEnabled));
@@ -1,9 +1,12 @@
1
+ import { governanceBaselineProblem } from "./governance-baseline-validity.js";
1
2
  export function isSelfOrchestrationActive(spec, deps) {
2
3
  return spec.selfOrchestration === true && workflowsCapability(deps);
3
4
  }
4
5
  export function workflowsCapability(deps) {
5
- return (deps.workflowScriptRunner?.safeForUntrustedScripts === true &&
6
- deps.workflowGovernanceBaseline !== undefined);
6
+ return deps.workflowScriptRunner?.safeForUntrustedScripts === true && hasUsableGovernanceBaseline(deps);
7
+ }
8
+ function hasUsableGovernanceBaseline(deps) {
9
+ return governanceBaselineProblem(deps.workflowGovernanceBaseline) === null;
7
10
  }
8
11
  export function selfOrchestrationFailClosedReason(spec, deps) {
9
12
  if (spec.selfOrchestration !== true)
@@ -14,8 +17,10 @@ export function selfOrchestrationFailClosedReason(spec, deps) {
14
17
  if (deps.workflowScriptRunner?.safeForUntrustedScripts !== true) {
15
18
  missing.push("a hard WorkflowScriptRunner (RunnerDeps.workflowScriptRunner.safeForUntrustedScripts === true)");
16
19
  }
17
- if (deps.workflowGovernanceBaseline === undefined) {
18
- missing.push("a governance baseline (RunnerDeps.workflowGovernanceBaseline)");
20
+ const problem = governanceBaselineProblem(deps.workflowGovernanceBaseline);
21
+ if (problem !== null) {
22
+ missing.push(`a usable governance baseline (RunnerDeps.workflowGovernanceBaseline.${problem.where === "workflowGovernanceBaseline" ? "" : `${problem.where} `}` +
23
+ `is ${problem.found})`);
19
24
  }
20
25
  return (`self-orchestration (TaskSpec.selfOrchestration) requires ${missing.join(" and ")}; it is DISABLED for ` +
21
26
  `this task (fail-closed — the run_workflow tool is not mounted and the orchestration prompt is not injected).`);
@@ -40,10 +40,18 @@ export interface ReadDenyMatcher {
40
40
  /** Normalized entries (built-ins first, then additions; exact duplicates folded). The disclosure /
41
41
  * persistence face. */
42
42
  readonly entries: readonly NormalizedReadDenyEntry[];
43
- /** Judge ONE path view (segment-window semantics). Returns the first matching entry, or null. */
44
- matchPath(path: string): ReadDenyHit | null;
45
- /** §3.3 target judgment: canonical view + lexical view; EITHER hit decides. */
46
- matchTarget(canonicalKey: string, lexicalView?: string): ReadDenyHit | null;
43
+ /** Judge ONE path view (segment-window semantics). Returns the first matching entry, or null.
44
+ * `aliasResolved` = the key's MINTER reports the real filesystem already collapsed any win32
45
+ * component alias in it (canonicalizeTarget's realpath arm); unset both readings are judged,
46
+ * which is the fail-closed default. */
47
+ matchPath(path: string, opts?: {
48
+ readonly aliasResolved?: boolean;
49
+ }): ReadDenyHit | null;
50
+ /** §3.3 target judgment: canonical view + lexical view; EITHER hit decides. `aliasResolved`
51
+ * applies to the CANONICAL arm only — the lexical view is an unresolved spelling. */
52
+ matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
53
+ readonly aliasResolved?: boolean;
54
+ }): ReadDenyHit | null;
47
55
  /** Traversal exclusions for the rg legs: `!`-polarity glob pairs — `!**\/<p>` plus `!**\/<p>/…`
48
56
  * (the entry itself and its descendants). */
49
57
  readonly rgExclusionGlobs: readonly ReadDenyRgGlob[];
@@ -145,7 +153,9 @@ export declare function compileSegmentPattern(raw: string, fold: "none" | "ascii
145
153
  * BOTH separator families — win32 canonical keys are backslash-form. Returns the matched raw pattern
146
154
  * or null.
147
155
  */
148
- export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[]): string | null;
156
+ export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[], opts?: {
157
+ readonly aliasResolved?: boolean;
158
+ }): string | null;
149
159
  /**
150
160
  * codex r3 — the PURE validation/canonicality predicate over a PERSISTED deny entry (checkpoint v9
151
161
  * face section), for the resume pre-CAS ladder: it must refuse BEFORE the approval CAS everything
@@ -91,15 +91,36 @@ export function compileSegmentPattern(raw, fold) {
91
91
  });
92
92
  return { raw, segments, foldAscii: fold === "ascii" };
93
93
  }
94
- export function matchSegmentPatterns(path, compiled) {
95
- const rawSegs = path.split(/[\\/]/).filter(Boolean);
96
- let loweredSegs;
94
+ export function matchSegmentPatterns(path, compiled, opts) {
95
+ const literalSegs = path.split(/[\\/]/).filter(Boolean);
96
+ const win32Segs = [];
97
+ for (const raw of literalSegs) {
98
+ const dot = /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, "");
99
+ if (dot === ".")
100
+ continue;
101
+ if (dot === "..") {
102
+ const last = win32Segs[win32Segs.length - 1];
103
+ if (win32Segs.length > 0 && last !== "..")
104
+ win32Segs.pop();
105
+ else
106
+ win32Segs.push("..");
107
+ continue;
108
+ }
109
+ const stripped = raw.replace(/[. ]+$/, "");
110
+ win32Segs.push(stripped.length > 0 ? stripped : raw);
111
+ }
112
+ const sameView = opts?.aliasResolved === true ||
113
+ (win32Segs.length === literalSegs.length && win32Segs.every((s, i) => s === literalSegs[i]));
114
+ const views = sameView ? [literalSegs] : [literalSegs, win32Segs];
115
+ const lowered = new Map();
97
116
  for (const pat of compiled) {
98
- const segs = pat.foldAscii ? (loweredSegs ??= rawSegs.map(asciiLower)) : rawSegs;
99
- const n = pat.segments.length;
100
- for (let i = 0; i + n <= segs.length; i++) {
101
- if (pat.segments.every((re, j) => re.test(segs[i + j] ?? "")))
102
- return pat.raw;
117
+ for (const view of views) {
118
+ const segs = pat.foldAscii ? (lowered.get(view) ?? lowered.set(view, view.map(asciiLower)).get(view)) : view;
119
+ const n = pat.segments.length;
120
+ for (let i = 0; i + n <= segs.length; i++) {
121
+ if (pat.segments.every((re, j) => re.test(segs[i + j] ?? "")))
122
+ return pat.raw;
123
+ }
103
124
  }
104
125
  }
105
126
  return null;
@@ -171,15 +192,15 @@ export function compileReadDeny(additions = [], layer = "additions", builtin) {
171
192
  rgExclusionGlobs.push({ flag, glob: `!**/${g}` }, { flag, glob: `!**/${g}/**` });
172
193
  rgProbeGlobs.push({ flag, glob: `**/${g}` }, { flag, glob: `**/${g}/**` });
173
194
  }
174
- const matchPath = (path) => {
175
- const hit = matchSegmentPatterns(path, compiled);
195
+ const matchPath = (path, opts) => {
196
+ const hit = matchSegmentPatterns(path, compiled, opts);
176
197
  return hit === null ? null : { pattern: hit };
177
198
  };
178
199
  return {
179
200
  entries: normalized,
180
201
  matchPath,
181
- matchTarget(canonicalKey, lexicalView) {
182
- const canonical = matchPath(canonicalKey);
202
+ matchTarget(canonicalKey, lexicalView, opts) {
203
+ const canonical = matchPath(canonicalKey, opts);
183
204
  if (canonical !== null)
184
205
  return { ...canonical, matchedView: canonicalKey };
185
206
  if (lexicalView !== undefined) {
@@ -268,7 +268,9 @@ export declare function lexicalViewOf(spelled: string, base: string): string;
268
268
  * `executionEnv` — see design/44 §5.)
269
269
  */
270
270
  export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRootsCanonical?: readonly string[], exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
271
- matchTarget(canonicalKey: string, lexicalView?: string): {
271
+ matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
272
+ readonly aliasResolved?: boolean;
273
+ }): {
272
274
  pattern: string;
273
275
  matchedView?: string;
274
276
  } | null;
@@ -291,6 +293,7 @@ export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, pat
291
293
  export declare function canonicalizeTarget(env: ExecutionEnv, path: string, signal?: AbortSignal, baseCwd?: string): Promise<{
292
294
  ok: true;
293
295
  key: string;
296
+ aliasResolved?: true;
294
297
  } | {
295
298
  ok: false;
296
299
  message: string;
@@ -341,7 +344,7 @@ export declare function withinAnyRoot(rootsCanonical: readonly string[], p: stri
341
344
  * just let the model discover it by trial), but it is a statement, not a recommendation, and it
342
345
  * comes last. Every shell call still passes the deployment's approval policy.
343
346
  */
344
- export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories \u2014 or additionalReadDirectories for read-only access. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
347
+ export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories \u2014 or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
345
348
  /** inv 1 (read-before-edit): a file must have been read this task before it can be edited/overwritten.
346
349
  * Message is CC 2.1.198 live-verbatim (all-tools-live-probe 2026-07-08 §2.1/§3.1/§5.1 — one message for
347
350
  * Edit/Write/NotebookEdit: "before writing to it", not the old sema "before editing").
@@ -339,7 +339,9 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
339
339
  return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
340
340
  }
341
341
  if (readDeny !== undefined) {
342
- const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical));
342
+ const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), {
343
+ aliasResolved: canon.aliasResolved === true,
344
+ });
343
345
  if (hit !== null) {
344
346
  const lexicalHit = hit.matchedView !== undefined && hit.matchedView !== key;
345
347
  const judged = lexicalHit
@@ -419,7 +421,7 @@ export async function canonicalizeTarget(env, path, signal, baseCwd) {
419
421
  }
420
422
  const canon = await env.canonicalPath(abs, signal);
421
423
  if (canon.ok)
422
- return { ok: true, key: canon.value };
424
+ return { ok: true, key: canon.value, aliasResolved: true };
423
425
  const info = await env.fileInfo(abs, signal);
424
426
  if (!info.ok) {
425
427
  return { ok: false, message: `cannot determine whether "${path}" is a symlink: ${info.error.message}`, unresolvedSymlink: true };
@@ -491,7 +493,7 @@ export function violationDetails(v) {
491
493
  export function withinAnyRoot(rootsCanonical, p) {
492
494
  return rootsCanonical.some((r) => within(r, p));
493
495
  }
494
- export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
496
+ export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
495
497
  export function requireRead(state, key) {
496
498
  const entry = state.get(key);
497
499
  if (entry === undefined || entry.isPartialView) {
@@ -208,6 +208,39 @@ export interface GrepRunResult {
208
208
  * withheld / no deny judge in play. */
209
209
  withheld?: ReadDenyWithheld;
210
210
  }
211
+ /**
212
+ * backlog #303 (rescan-hardened form) — the rg leg's deny TRIPWIRE. rg's exclusion globs are spelled
213
+ * from the pattern text and cannot express the win32 component-alias family (`.aws.` / `.aws ` —
214
+ * trailing dots/spaces the win32 namespace strips), so a directory planted under an alias spelling
215
+ * slips past the globs while the shared `matchPath` (two-view, read-deny.ts) correctly names it —
216
+ * the JS leg pruned it, the rg leg returned its contents, and the same tree answered differently
217
+ * depending on whether ripgrep was installed.
218
+ *
219
+ * The first fix FILTERED rg's text output line-by-line. The rescan killed that shape twice over:
220
+ * ripgrep prints filenames verbatim, so a path component containing a NEWLINE splits one record
221
+ * across physical lines whose fragments carry no `:line:` boundary — the filter kept them and the
222
+ * deny-listed content passed (a protection hole, not a precision residual); and pruning every line
223
+ * of a partial run fabricated a `No matches.` row inside the structured card. Text-splitting rg
224
+ * records is unsalvageable without structured output (`--json`, backlog #306), so this function no
225
+ * longer edits anything: it JUDGES. Any deny hit — or any line the format cannot account for —
226
+ * trips, and the caller abandons the rg run for the JS scanner, whose walk prunes with the
227
+ * authoritative judge and needs no path parsing at all. rg stays the fast path for the common case
228
+ * (no guarded entries in the result); the moment a guarded spelling is involved, the engine that
229
+ * cannot mis-parse it owns the answer.
230
+ *
231
+ * Trip conditions by mode — `files_with_matches`: every line IS a path, judged whole; `count`:
232
+ * `path:N` (a line without the numeric tail is a split record — ambiguous, trip); `content`:
233
+ * judged at EVERY `:digits:` boundary and every `-digits-` boundary (a candidate cut inside match
234
+ * text can over-trip — safe: the fallback re-derives the exact answer), and a non-separator line
235
+ * with NO boundary at all is a split record — trip. `--` separators and empty lines pass.
236
+ */
237
+ export declare function rgOutputDenyTripwire(stdout: string, mode: "content" | "files_with_matches" | "count", judge: Pick<ReadDenyJudge, "matchPath">): {
238
+ trip: false;
239
+ } | {
240
+ trip: true;
241
+ reason: "deny-hit" | "ambiguous-record";
242
+ pattern?: string;
243
+ };
211
244
  /** ripgrep grep: build flags from params, run, normalize to the same output as {@link jsGrep}.
212
245
  * Result-fidelity contract: output ripgrep DID produce is never silently replaced by a fallback
213
246
  * rescan — partial results ship with a caveat; only a zero-output failure degrades to
@@ -1043,6 +1043,63 @@ function formatRgStdout(stdout, p, caveat = "") {
1043
1043
  (off > 0 ? `\n[offset ${off}]` : "") +
1044
1044
  caveat);
1045
1045
  }
1046
+ export function rgOutputDenyTripwire(stdout, mode, judge) {
1047
+ if (stdout.length === 0)
1048
+ return { trip: false };
1049
+ const judgeBoundaries = (line, boundary) => {
1050
+ let sawBoundary = false;
1051
+ for (let m = boundary.exec(line); m !== null; m = boundary.exec(line)) {
1052
+ sawBoundary = true;
1053
+ const prefix = line.slice(0, m.index);
1054
+ if (prefix.length === 0)
1055
+ continue;
1056
+ const hit = judge.matchPath(prefix);
1057
+ if (hit !== null)
1058
+ return hit.pattern;
1059
+ }
1060
+ return sawBoundary ? null : "";
1061
+ };
1062
+ for (const line of stdout.split("\n")) {
1063
+ if (line.length === 0 || line === "--")
1064
+ continue;
1065
+ if (mode === "files_with_matches") {
1066
+ const h = judge.matchPath(line);
1067
+ if (h !== null)
1068
+ return { trip: true, reason: "deny-hit", pattern: h.pattern };
1069
+ }
1070
+ else if (mode === "count") {
1071
+ const m = /^(.*):\d+$/.exec(line);
1072
+ if (m === null)
1073
+ return { trip: true, reason: "ambiguous-record" };
1074
+ const h = judge.matchPath(m[1]);
1075
+ if (h !== null)
1076
+ return { trip: true, reason: "deny-hit", pattern: h.pattern };
1077
+ }
1078
+ else {
1079
+ const colon = judgeBoundaries(line, /:\d+:/g);
1080
+ if (colon !== null && colon !== "")
1081
+ return { trip: true, reason: "deny-hit", pattern: colon };
1082
+ const dash = judgeBoundaries(line, /-\d+-/g);
1083
+ if (dash !== null && dash !== "")
1084
+ return { trip: true, reason: "deny-hit", pattern: dash };
1085
+ if (colon === "" && dash === "")
1086
+ return { trip: true, reason: "ambiguous-record" };
1087
+ }
1088
+ }
1089
+ return { trip: false };
1090
+ }
1091
+ async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
1092
+ const denyOut = {};
1093
+ const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1094
+ const why = trip.reason === "deny-hit" ? "its output involved sensitive-path deny-listed entries the exclusion globs cannot express" : "its output contained a record the line format cannot account for";
1095
+ return {
1096
+ text: text.startsWith("Error (grep)")
1097
+ ? text
1098
+ : `${text}\n[note: the ripgrep pass was abandoned (${why}); results are from the fallback scanner, which prunes with the authoritative deny judge]`,
1099
+ degraded: { fallback: "js-scan", reason: `ripgrep output tripped the deny tripwire (${trip.reason})` },
1100
+ ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
1101
+ };
1102
+ }
1046
1103
  async function jsGrepFallback(env, root, p, signal, reason, deny) {
1047
1104
  const denyOut = {};
1048
1105
  const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
@@ -1097,6 +1154,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1097
1154
  if (err.code === "timeout") {
1098
1155
  const partial = err.partialStdout ?? "";
1099
1156
  if (partial.trim().length > 0) {
1157
+ if (deny !== undefined) {
1158
+ const trip = rgOutputDenyTripwire(partial, mode, deny);
1159
+ if (trip.trip)
1160
+ return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1161
+ }
1100
1162
  return {
1101
1163
  text: `${delimitUntrusted("ripgrep partial output", formatRgStdout(partial, p))}\n…[ripgrep timed out after producing partial output — results may be incomplete]`,
1102
1164
  degraded: { partial: true, reason: "ripgrep timed out" },
@@ -1124,6 +1186,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1124
1186
  }
1125
1187
  if (exitCode >= 2) {
1126
1188
  if (stdout.trim().length > 0) {
1189
+ if (deny !== undefined) {
1190
+ const trip = rgOutputDenyTripwire(stdout, mode, deny);
1191
+ if (trip.trip)
1192
+ return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1193
+ }
1127
1194
  return {
1128
1195
  text: `${delimitUntrusted("ripgrep partial output", formatRgStdout(stdout, p))}\n…[ripgrep exited with an error after producing partial output — results may be incomplete]`,
1129
1196
  degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
@@ -1131,6 +1198,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1131
1198
  }
1132
1199
  return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
1133
1200
  }
1201
+ if (deny !== undefined) {
1202
+ const trip = rgOutputDenyTripwire(stdout, mode, deny);
1203
+ if (trip.trip)
1204
+ return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1205
+ }
1134
1206
  const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
1135
1207
  const d = await denyDisclosure();
1136
1208
  return { text: formatRgStdout(orderedStdout, p) + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.38.0",
3
+ "version": "5.40.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",