@sema-agent/core 7.44.0 → 7.46.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 (159) hide show
  1. package/CHANGELOG.md +418 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +65 -38
  6. package/dist/agents/tool-filter.d.ts +82 -5
  7. package/dist/agents/tool-filter.js +134 -4
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +30 -4
  12. package/dist/core/allow-entry.js +32 -5
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +11 -5
  15. package/dist/core/checkpoint-store.d.ts +41 -4
  16. package/dist/core/decision-receipt.js +1 -1
  17. package/dist/core/effective-path-target.d.ts +143 -10
  18. package/dist/core/effective-path-target.js +113 -9
  19. package/dist/core/engine-notice.d.ts +25 -3
  20. package/dist/core/engine-notice.js +12 -6
  21. package/dist/core/gate-fold.js +3 -0
  22. package/dist/core/gate-lanes.js +11 -3
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +3 -1
  26. package/dist/core/governance-codes.js +3 -0
  27. package/dist/core/hooks.d.ts +5 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/layout.d.ts +5 -1
  31. package/dist/core/memory-engine/layout.js +1 -0
  32. package/dist/core/path-rule-binding.d.ts +65 -0
  33. package/dist/core/path-rule-binding.js +99 -0
  34. package/dist/core/permission-rule-model.d.ts +47 -18
  35. package/dist/core/permission-rule-model.js +38 -42
  36. package/dist/core/permission-rule-provider.d.ts +7 -0
  37. package/dist/core/permission-rule-provider.js +10 -3
  38. package/dist/core/permission-rule-store.d.ts +8 -0
  39. package/dist/core/permission-rule-store.js +6 -2
  40. package/dist/core/permission-rule-sync.js +2 -2
  41. package/dist/core/permission-rules.d.ts +22 -36
  42. package/dist/core/permission-rules.js +50 -40
  43. package/dist/core/real-approval.d.ts +9 -6
  44. package/dist/core/real-approval.js +5 -5
  45. package/dist/core/runner/child-tree.d.ts +110 -0
  46. package/dist/core/runner/child-tree.js +107 -0
  47. package/dist/core/runner/construction-doors.d.ts +5 -0
  48. package/dist/core/runner/construction-doors.js +6 -0
  49. package/dist/core/runner/contracts.d.ts +54 -2
  50. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  51. package/dist/core/runner/permission-rule-lanes.js +18 -12
  52. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  53. package/dist/core/runner/persisted-path-rows.js +31 -12
  54. package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
  55. package/dist/core/runner/prepare-ask-lane.js +4 -2
  56. package/dist/core/runner/prepare-config-doors.js +3 -1
  57. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  58. package/dist/core/runner/prepare-execution-env.js +11 -3
  59. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  60. package/dist/core/runner/prepare-gate-stations.js +3 -1
  61. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  62. package/dist/core/runner/prepare-hands-readface.js +6 -6
  63. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  64. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  65. package/dist/core/runner/prepare-listings.d.ts +4 -4
  66. package/dist/core/runner/prepare-listings.js +2 -22
  67. package/dist/core/runner/prepare-park-ask.d.ts +5 -0
  68. package/dist/core/runner/prepare-park-ask.js +6 -4
  69. package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
  70. package/dist/core/runner/prepare-policy-chain.js +14 -5
  71. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  72. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  73. package/dist/core/runner/prepare-task.js +41 -18
  74. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  75. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  76. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  77. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  78. package/dist/core/runner/resume-apply.js +17 -3
  79. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  80. package/dist/core/runner/resume-policy-outcome.js +2 -1
  81. package/dist/core/runner/run-leg.js +5 -1
  82. package/dist/core/runner/run-notification-lane.js +2 -2
  83. package/dist/core/runner/runtask.d.ts +2 -0
  84. package/dist/core/runner/runtask.js +4 -2
  85. package/dist/core/runner/stream-steer-verb.js +2 -2
  86. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  87. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  88. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  89. package/dist/core/runner/synthetic-tools.js +3 -2
  90. package/dist/core/runner/write-protection-station.d.ts +3 -2
  91. package/dist/core/runner/write-protection-station.js +1 -0
  92. package/dist/core/runner-deps.d.ts +14 -0
  93. package/dist/core/sensitive-path-policy.d.ts +8 -0
  94. package/dist/core/sensitive-path-policy.js +18 -2
  95. package/dist/core/session-policy-store.d.ts +6 -2
  96. package/dist/core/session-policy-store.js +2 -1
  97. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  98. package/dist/core/shell-wrapper-projection.js +56 -0
  99. package/dist/core/shell-wrapper-table.d.ts +144 -34
  100. package/dist/core/shell-wrapper-table.js +40 -20
  101. package/dist/core/skill-grant.d.ts +10 -2
  102. package/dist/core/skill-grant.js +20 -4
  103. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  104. package/dist/core/synthetic-read-verdict.js +3 -1
  105. package/dist/core/task-notification.d.ts +12 -1
  106. package/dist/core/task-notification.js +13 -1
  107. package/dist/core/task-spec.d.ts +5 -3
  108. package/dist/core/tool-catalog-entries.d.ts +21 -0
  109. package/dist/core/tool-catalog-entries.js +9 -0
  110. package/dist/core/tool-policy.d.ts +45 -12
  111. package/dist/core/tool-policy.js +18 -20
  112. package/dist/core/tool-spec.d.ts +23 -4
  113. package/dist/core/types.d.ts +1 -1
  114. package/dist/core/types.js +1 -1
  115. package/dist/core/write-protect.d.ts +4 -0
  116. package/dist/core/write-protect.js +30 -9
  117. package/dist/index.d.ts +10 -6
  118. package/dist/index.js +9 -5
  119. package/dist/orchestration/run-spec.js +8 -7
  120. package/dist/orchestration/workflow.js +3 -2
  121. package/dist/shell/lex.d.ts +7 -0
  122. package/dist/shell/lex.js +17 -0
  123. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  124. package/dist/tools/fs/bash-program-position.js +33 -69
  125. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  126. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  127. package/dist/tools/fs/index.d.ts +3 -2
  128. package/dist/tools/fs/program-read-model.d.ts +694 -0
  129. package/dist/tools/fs/program-read-model.js +1252 -0
  130. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  131. package/dist/tools/fs/program-read-model.rows.js +613 -0
  132. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  133. package/dist/tools/fs/read-deny-home.js +62 -24
  134. package/dist/tools/fs/read-deny.d.ts +54 -14
  135. package/dist/tools/fs/read-deny.js +104 -50
  136. package/dist/tools/fs/removal-targets.d.ts +83 -0
  137. package/dist/tools/fs/removal-targets.js +236 -0
  138. package/dist/tools/fs/safety.d.ts +3 -0
  139. package/dist/tools/fs/safety.js +10 -3
  140. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  141. package/dist/tools/fs/shell-read-argv.js +386 -0
  142. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  143. package/dist/tools/fs/shell-read-bindings.js +746 -0
  144. package/dist/tools/fs/shell-read-check.js +23 -15
  145. package/dist/tools/fs/shell-read-establish.js +79 -5
  146. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  147. package/dist/tools/fs/shell-read-judge.js +9 -1
  148. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  149. package/dist/tools/fs/shell-read-plan.js +299 -393
  150. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  151. package/dist/tools/fs/shell-read-word.js +30 -38
  152. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
  153. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  154. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  155. package/dist/tools/fs/shell-reentry-text.js +90 -53
  156. package/package.json +1 -1
  157. package/test/export-surface.snapshot.json +49 -3
  158. package/wire-changes/core@7.45.0.json +1084 -0
  159. package/wire-changes/core@7.46.0.json +1522 -0
@@ -3,7 +3,7 @@ import { snapshotActorAssertion } from "../../internal/llm.js";
3
3
  import { MAX_STEER_INPUT_ID_CHARS } from "../checkpoint-store.js";
4
4
  import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
5
5
  import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
6
- import { isSystemInjectionPriority, steerPriorityOf, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
6
+ import { injectionLaneOf, isSystemInjectionPriority, steerPriorityOf, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
7
7
  import { deliverEngineNotice } from "../types.js";
8
8
  import { inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
9
9
  import { announceAcceptance, armAcceptance, nextHumanInputSeq } from "./steer-admission.js";
@@ -133,7 +133,7 @@ export function streamSteerVerb(input) {
133
133
  const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
134
134
  if (mintsAFrame)
135
135
  armAcceptance(parkRecord, () => publishQueued(h));
136
- if (priority === "later") {
136
+ if (injectionLaneOf(priority) === "followUp") {
137
137
  await h.harness.followUp(payload, noteOptions);
138
138
  noteAccepted();
139
139
  return;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The sub-agent tool lists of one prepared task: the allow advisory for `TaskSpec.agents` and THE judgment of every denied-tools
3
+ * list a deployment writes — the task's own definitions (`TaskSpec.agents`), the definitions a mounted delegation tool carries
4
+ * (the `authored` faces — the engine's built-ins name tools of other vocabularies and are not judged) and its tool-level list.
5
+ * Universe = the task's assembled roster plus every mounted delegation tool's pool, names and aliases; a pool the engine cannot
6
+ * list (an `extraTools` factory with no declared roster) turns the refusals that need a roster into host notices. A tool's own lists are judged where
7
+ * the tool is mounted: only a tool a deployment mounted (depth 0) publishes them (`authored`, `agentToolLevelAllowTools`, `agentToolLevelDenyTools`), the
8
+ * engine's nested copy for a delegated child carries neither.
9
+ *
10
+ * Throws `config.agent.unknown_tool` (`refused`, the one site) — synchronous, like the phase that calls it.
11
+ */
12
+ import type { AgentTool } from "../../internal/harness.js";
13
+ import type { TaskSpec } from "../types.js";
14
+ import type { AnnounceOnceSink } from "./contracts.js";
15
+ /**
16
+ * The names a definition's denied-tools entries stand for, for a lane that has neither a roster nor a notice sink at the point
17
+ * it builds its own policy (a workflow leg's agentType): each entry is read by the one reading, so `Tool(content)` denies the
18
+ * whole tool there too, and an entry that names no tool is refused by the same refusal. The stale-name and told dispositions
19
+ * need the leg's roster and a sink; the leg's own policy-name audit is what reads them.
20
+ */
21
+ export declare function deniedToolNamesOf(where: string, value: unknown): string[];
22
+ export interface SubAgentToolListsInput {
23
+ /** borrowed-readonly — the REBOUND spec: `agents` (the task's definitions) and `tools` (the mounted delegation tools). */
24
+ spec: Pick<TaskSpec, "agents" | "tools">;
25
+ /** borrowed-readonly — the FULLY ASSEMBLED mount array (names + aliases = the roster half of the universe). */
26
+ tools: ReadonlyArray<AgentTool>;
27
+ /** borrowed-readonly — the once-per-session ledger whose operator sink carries the notices. */
28
+ onceLedger: Pick<AnnounceOnceSink, "onError">;
29
+ /** borrowed-readonly — the acquired session's id (operator-line context). */
30
+ sessionId: string;
31
+ }
32
+ export declare function auditSubAgentToolLists(input: SubAgentToolListsInput): void;
@@ -0,0 +1,121 @@
1
+ import { notAListSentence, notAStringSentence, readDeniedEntry, readToolNameList, valueTypeLabel } from "../../agents/tool-filter.js";
2
+ import { compileAllowList, inertEntryPhrase, renderListEntry } from "../allow-entry.js";
3
+ import { inlineUntrusted } from "../untrusted-text.js";
4
+ const DENY_LIST_NAMES_ONLY = "To block by path or command, write a deny rule in the caller's permission policy: a denied-tools list only takes tool names (it narrows which tools the agent is handed).";
5
+ function refused(message) {
6
+ const e = new Error(message);
7
+ e.code = "config.agent.unknown_tool";
8
+ return e;
9
+ }
10
+ function refusal(where, flaw) {
11
+ return refused(`${where} declares tool ${inertEntryPhrase(flaw)} in its denied tools, but no tool of that spelling exists in this deployment — fix the entry or mount the tool.${flaw.reason === "grammar" ? ` ${DENY_LIST_NAMES_ONLY}` : ""}`);
12
+ }
13
+ function refusalNotAString(where, side, index, type) {
14
+ return refused(notAStringSentence(where, side, index, type));
15
+ }
16
+ function refusalNotAList(where, side, list) {
17
+ return refused(notAListSentence(where, side, list));
18
+ }
19
+ export function deniedToolNamesOf(where, value) {
20
+ const list = readToolNameList(value);
21
+ if (list.kind !== "entries")
22
+ throw refusalNotAList(where, "denied", list);
23
+ return list.entries.map((entry, index) => {
24
+ if (entry === "*")
25
+ return entry;
26
+ const read = readDeniedEntry(entry);
27
+ if (read.kind === "not_a_string")
28
+ throw refusalNotAString(where, "denied", index, read.type);
29
+ const flaw = compileAllowList([read.name], "deny").inert[0];
30
+ if (flaw !== undefined && flaw.reason === "grammar")
31
+ throw refusal(where, { ...flaw, entry: read.name });
32
+ return read.name;
33
+ });
34
+ }
35
+ function judgeDeniedToolNames(where, value, universe, advise) {
36
+ if (value === undefined || value === null)
37
+ return;
38
+ const list = readToolNameList(value);
39
+ if (list.kind !== "entries")
40
+ throw refusalNotAList(where, "denied", list);
41
+ let index = -1;
42
+ for (const entry of list.entries) {
43
+ index += 1;
44
+ if (entry === "*")
45
+ continue;
46
+ const read = readDeniedEntry(entry);
47
+ if (read.kind === "not_a_string")
48
+ throw refusalNotAString(where, "denied", index, read.type);
49
+ const { name, content } = read;
50
+ const raw = entry;
51
+ const { rows, inert } = compileAllowList([name], "deny", universe.known);
52
+ const flaw = inert[0] === undefined ? undefined : { ...inert[0], entry: raw };
53
+ const stale = flaw !== undefined && (flaw.reason === "tool_name" || flaw.reason === "near_miss");
54
+ if (flaw !== undefined && (flaw.reason === "grammar" || (stale && universe.complete)))
55
+ throw refusal(where, flaw);
56
+ if (flaw === undefined && universe.known.has(name)) {
57
+ if (content)
58
+ advise(new Error(`${where} declares ${renderListEntry(raw)} in its denied tools: this list reads tool names, not content, so the whole tool "${inlineUntrusted(name, 80)}" is denied to the agent. ${DENY_LIST_NAMES_ONLY}`));
59
+ continue;
60
+ }
61
+ advise(new Error(`${where} declares tool ${flaw !== undefined ? inertEntryPhrase(flaw) : renderListEntry(raw)} in its denied tools, a name this task's roster and delegation pools do not carry — the entry hides nothing here. Advisory only: a list shared across deployments is not wrong because one lacks a tool, but a misspelling guards nothing.` +
62
+ (rows[0]?.compiled.kind === "namespace" ? " This list matches tool names exactly: a server-level entry hides none of that server's tools; name them, or write a deny rule in the caller's permission policy." : "") +
63
+ (universe.complete ? "" : " A delegation tool this task mounts injects tools the engine cannot enumerate (extraTools), so the name cannot be checked here.")));
64
+ }
65
+ }
66
+ function judgeAllowedToolList(where, value, known, advise) {
67
+ if (value === undefined || value === null)
68
+ return;
69
+ const list = readToolNameList(value);
70
+ if (list.kind !== "entries")
71
+ throw refusalNotAList(where, "allowed", list);
72
+ const unknownNames = [];
73
+ let index = -1;
74
+ for (const entry of list.entries) {
75
+ index += 1;
76
+ if (typeof entry !== "string")
77
+ throw refusalNotAString(where, "allowed", index, valueTypeLabel(entry));
78
+ if (known !== undefined && entry !== "*" && !known.has(entry))
79
+ unknownNames.push(entry);
80
+ }
81
+ if (unknownNames.length === 0)
82
+ return;
83
+ advise(new Error(`${where} allows tool(s) ${unknownNames.map(renderListEntry).join(", ")} not present in this task's assembled roster — likely a typo (the entry would be item-filtered at spawn; the agent stays usable). Advisory only: the delegation pool can differ from this roster, so a tool mounted only on the delegation tool or produced by per-spawn extraTools makes this spurious.`));
84
+ }
85
+ export function auditSubAgentToolLists(input) {
86
+ const { spec, tools, onceLedger, sessionId } = input;
87
+ const advise = (err) => {
88
+ try {
89
+ onceLedger.onError(err, { phase: "config", sessionId });
90
+ }
91
+ catch {
92
+ }
93
+ };
94
+ const delegationTools = (spec.tools ?? []).filter((t) => t.agentToolFaces !== undefined);
95
+ const taskAgents = spec.agents ?? [];
96
+ if (taskAgents.length === 0 && delegationTools.length === 0)
97
+ return;
98
+ const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
99
+ const denyKnown = new Set(known);
100
+ for (const t of delegationTools)
101
+ for (const p of t.agentToolPool ?? [])
102
+ for (const n of [p.name, ...(p.aliases ?? [])])
103
+ denyKnown.add(n);
104
+ const universe = { known: denyKnown, complete: delegationTools.every((t) => t.agentToolPool !== undefined) };
105
+ for (const def of taskAgents) {
106
+ judgeAllowedToolList(`TaskSpec.agents: agent "${def.name}"`, def.allowTools, known, advise);
107
+ judgeDeniedToolNames(`TaskSpec.agents: agent "${def.name}"`, def.denyTools, universe, advise);
108
+ }
109
+ const replaced = new Set(taskAgents.map((d) => d.name));
110
+ for (const t of delegationTools) {
111
+ for (const face of t.agentToolFaces ?? []) {
112
+ if (face.authored === true && !replaced.has(face.name)) {
113
+ const faceWhere = `delegation tool "${t.name}": agent definition "${face.name}"`;
114
+ judgeAllowedToolList(faceWhere, face.allowTools, undefined, advise);
115
+ judgeDeniedToolNames(faceWhere, face.denyTools, universe, advise);
116
+ }
117
+ }
118
+ judgeAllowedToolList(`delegation tool "${t.name}": the tool-level list`, t.agentToolLevelAllowTools, undefined, advise);
119
+ judgeDeniedToolNames(`delegation tool "${t.name}": the tool-level list`, t.agentToolLevelDenyTools, universe, advise);
120
+ }
121
+ }
@@ -110,7 +110,7 @@ export interface SkillListingEntry {
110
110
  * and the tool, so what the prompt promises is exactly what the tool serves. Each field is READ ONCE, BY NAME, and
111
111
  * both consumers see the copy: the gate screens this list while the invoke substitutes from it, so a `get baseDir()`
112
112
  * answering those two reads differently passed a screen that measured a value the model never received — and a rest
113
- * copy would silently drop an INHERITED or non-enumerable list, i.e. lose a skill's grant or its refusals. */
113
+ * copy would silently drop an INHERITED or non-enumerable list. The two tables are read by {@link readSkillTable}. */
114
114
  export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[];
115
115
  /** [c209-C] The skills listing's lead line — single-sourced so the run loop's durable-resume
116
116
  * transcript probe (MED-3② skills arm, runtask) matches EXACTLY what the initial frame carries
@@ -2,7 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool } from "../tools.js";
3
3
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
4
  import { SKILL_LISTING_DEFAULT_BUDGET, SKILL_LISTING_DESC_MAX_CHARS_DEFAULT } from "../skill-listing-budget.js";
5
- import { skillNamedBy } from "../skill-grant.js";
5
+ import { readSkillTable, skillNamedBy } from "../skill-grant.js";
6
6
  import { toolFace } from "../tool-catalog-entries.js";
7
7
  import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
8
8
  import { foldListingWithin } from "./listing-fold.js";
@@ -116,7 +116,8 @@ export function normalizeSkills(skills) {
116
116
  seen.add(s.name);
117
117
  const { name, description, content, allowedTools, disallowedTools, files, baseDir, ...rest } = s;
118
118
  void rest;
119
- out.push({ name, description, content, ...(allowedTools !== undefined ? { allowedTools: [...allowedTools] } : {}), ...(disallowedTools !== undefined ? { disallowedTools: [...disallowedTools] } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
119
+ const [allowed, denied] = [readSkillTable(allowedTools), readSkillTable(disallowedTools)];
120
+ out.push({ name, description, content, ...(allowed !== undefined ? { allowedTools: allowed } : {}), ...(denied !== undefined ? { disallowedTools: denied } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
120
121
  }
121
122
  }
122
123
  return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -33,8 +33,9 @@ export interface LegWriteProtection extends WriteProtectionMount {
33
33
  export declare function mountWriteProtection(input: {
34
34
  /** borrowed-readonly — the deployment seats: the table (`writeProtectedPaths`; absent ⇒ the default
35
35
  * table, `[]` ⇒ opted out), the data root the memory library is materialized under
36
- * (`memoryEngineDir`; absent ⇒ the resolver's env/default chain) and the operator sink. */
37
- deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "onNotice">;
36
+ * (`memoryEngineDir`; absent ⇒ the resolver's env/default chain), the configuration root the host
37
+ * declared (`agentConfigDir`) and the operator sink. */
38
+ deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "agentConfigDir" | "onNotice">;
38
39
  /** The HANDS' environment, or `undefined` with no hands — the driver's own predicate, not a second
39
40
  * reading of the env seats, so the judge sees exactly the environment the write tools do. */
40
41
  env: ExecutionEnv | undefined;
@@ -6,6 +6,7 @@ export function mountWriteProtection(input) {
6
6
  const announced = new Set();
7
7
  const matcher = compileWriteProtection(input.deps.writeProtectedPaths, {
8
8
  ...(input.deps.memoryEngineDir !== undefined ? { dataRoot: input.deps.memoryEngineDir } : {}),
9
+ ...(input.deps.agentConfigDir !== undefined ? { agentConfigDir: input.deps.agentConfigDir } : {}),
9
10
  ...(input.env !== undefined ? { env: input.env } : {}),
10
11
  rootPath: input.rootPath,
11
12
  onViewAbsent: (absence) => {
@@ -227,6 +227,20 @@ export interface RunnerDeps {
227
227
  * ignored). Same deployment-only seat, clamp argument and checkpoint posture as the tiers key.
228
228
  */
229
229
  readDenyBuiltinExclude?: readonly string[];
230
+ /**
231
+ * Where the host keeps the agent's configuration directory when it is not `<home>/.sema` — an ABSOLUTE path of an
232
+ * existing directory, resolved by the host (the engine reads no environment variable for it). The rows of the
233
+ * `agent-config` tier that name that directory (`~/.sema/settings.json`, `~/.sema/settings.local.json`,
234
+ * `~/.sema/engine-data/.env`, `~/.sema/config-lkg-*.json`) are then ALSO placed at the same files below this root, so the model can neither read
235
+ * nor (through the write-protection table and a wired `createSensitivePathPolicy({ agentConfigDir })`) rewrite the
236
+ * relocated settings; the default `~/.sema` location stays refused. Rows of the home's dotted family (`~/.sema.*`)
237
+ * and the external tools' files are not part of the directory and stay anchored at the home. A symlink the root is
238
+ * reached through is followed (the target is guarded too). ABSENT = today's behaviour, the literal `~/.sema` under
239
+ * the declared and account homes. A value that is not a string, is empty or relative, or names no existing directory
240
+ * refuses `new Runner` with `config.agent_config_dir_invalid` — never read as absent. DEPLOYMENT seat only (no
241
+ * TaskSpec twin) and fixed at construction: `swapDeps` does not carry it.
242
+ */
243
+ agentConfigDir?: string;
230
244
  /**
231
245
  * backlog #286 (#279 — CC 2.1.233 `DANGEROUS_FILES`/`DANGEROUS_DIRECTORIES`/
232
246
  * `DANGEROUS_DIRECTORY_PATHS` parity): the WRITE-protection table. DEFAULT-ON: a path-confinable
@@ -155,6 +155,14 @@ export declare function createSensitivePathPolicy(opts: {
155
155
  * gate and the transcript gate keep speaking about the same directory.
156
156
  */
157
157
  dataRoot?: string;
158
+ /**
159
+ * The host's configuration root when it keeps the agent's configuration directory somewhere other than the home's
160
+ * `.sema` — the same absolute directory it hands the Runner (`RunnerDeps.agentConfigDir`). Every pattern that opens with
161
+ * the `.sema` segment (`.sema/settings.json`, `.sema/hooks`, …) then also guards the same tail below this root; the
162
+ * patterns themselves keep guarding every `.sema` anywhere. Absent ⇒ the list is judged as it stands. A value that is not
163
+ * the absolute path of an existing directory throws (`config.agent_config_dir_invalid`).
164
+ */
165
+ agentConfigDir?: string;
158
166
  /** Override the guarded tool set (default: `Write`, `Edit`, `MultiEdit`-legacy → Edit, `NotebookEdit`). */
159
167
  tools?: string[];
160
168
  }): ToolPolicy;
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { canonicalizeTarget, expandHomeTilde, isAbsoluteForFamily, isWinFormPath, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
4
4
  import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TABLE, readDenyPatternRun } from "../tools/fs/read-deny.js";
5
+ import { agentConfigDirSegments, agentConfigDirTail, checkedAgentConfigDir } from "./agent-config-dir.js";
5
6
  import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
6
7
  import { pathTargetBaseOf } from "./effective-path-target.js";
7
8
  import { symlinkChainHops } from "./path-identity.js";
@@ -32,6 +33,7 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
32
33
  ".mcp.json",
33
34
  ".ai-agent/.env",
34
35
  ".sema/engine-data/.env",
36
+ ".sema/config-lkg-*.json",
35
37
  ".codex/config.toml",
36
38
  ".cursor/mcp.json",
37
39
  ".continue/config.json",
@@ -115,6 +117,19 @@ function matchSensitive(canonicalKey, compiled, aliasResolved) {
115
117
  }
116
118
  export function createSensitivePathPolicy(opts) {
117
119
  const compiled = compilePatterns(opts.patterns);
120
+ const configRoot = checkedAgentConfigDir(opts.agentConfigDir, "createSensitivePathPolicy agentConfigDir");
121
+ const configRows = configRoot === undefined ? [] : opts.patterns.flatMap((raw) => {
122
+ const tail = agentConfigDirTail(raw, !CASE_INSENSITIVE_FS);
123
+ return tail === undefined ? [] : [{ raw, tail }];
124
+ });
125
+ const configTwins = async (signal) => {
126
+ if (configRoot === undefined || configRows.length === 0)
127
+ return [];
128
+ const fold = CASE_INSENSITIVE_FS ? "ascii" : "none";
129
+ const lower = (segs) => (CASE_INSENSITIVE_FS ? segs.map((seg) => seg.replace(/[A-Z]/g, (c) => c.toLowerCase())) : segs);
130
+ const anchors = [configRoot, ...(await canonicalDataRootKeys(opts.env, [configRoot], signal))].map((root) => lower(agentConfigDirSegments(root)));
131
+ return configRows.map(({ raw, tail }) => ({ ...(compileSegmentPattern(tail, fold) ?? { raw, segments: [], foldAscii: CASE_INSENSITIVE_FS }), raw, anchors, rootedOnly: true }));
132
+ };
118
133
  const dataRootPaths = dataRootSpellings(opts.dataRoot);
119
134
  const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
120
135
  const family = (cwd) => pathFamilyOf({ root: opts.rootPath, cwd: cwd ?? opts.env.cwd });
@@ -130,12 +145,13 @@ export function createSensitivePathPolicy(opts) {
130
145
  }
131
146
  if (relative === "")
132
147
  return { kind: "clear" };
133
- const hit = matchSensitive(relative ?? canon.key, compiled, canon.aliasResolved === true);
148
+ const twins = await configTwins(signal);
149
+ const hit = matchSensitive(relative ?? canon.key, [...compiled, ...twins], canon.aliasResolved === true);
134
150
  if (hit)
135
151
  return { kind: "hit", pattern: hit, via: "target" };
136
152
  if (relative === null) {
137
153
  for (const hop of await symlinkChainHops(opts.env, path, signal, fam, base)) {
138
- const viaHop = matchSensitive(hop, compiled, false);
154
+ const viaHop = matchSensitive(hop, [...compiled, ...twins], false);
139
155
  if (viaHop)
140
156
  return { kind: "hit", pattern: viaHop, via: "chain" };
141
157
  }
@@ -52,12 +52,16 @@ export interface PutRulesOptions {
52
52
  * this from a verified, role-checked principal (mirrors `steer.trusted`). */
53
53
  operator?: boolean;
54
54
  }
55
- /** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, and the
56
- * spelling to write instead when the judgment has one (a retired name's 1:1 successor, an unmintable segment's minted name). */
55
+ /** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, the judgment's
56
+ * one-line sentence when it has one, and the spelling to write instead when the judgment has one (a retired name's 1:1
57
+ * successor, an unmintable segment's minted name). */
57
58
  export interface SessionRuleNameRefusal {
58
59
  field: SessionRuleToolNameField;
59
60
  name: string;
60
61
  verdict: Exclude<RuleToolNameVerdict["kind"], "current">;
62
+ /** The `guidance` of the judgment ({@link judgeRuleToolName}), byte for byte — the same string the refusal's sentence quotes. Present for
63
+ * `retired` and `unmintable_segment`, the two verdicts that carry one; absent for `unprefixed_namespace`, whose judgment has no sentence of its own. */
64
+ guidance?: string;
61
65
  currentName?: string;
62
66
  }
63
67
  /** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
@@ -41,6 +41,7 @@ export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLA
41
41
  continue;
42
42
  const quoted = JSON.stringify(name);
43
43
  const successor = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
44
+ const guidance = verdict.kind === "unprefixed_namespace" ? undefined : verdict.guidance;
44
45
  let message;
45
46
  switch (verdict.kind) {
46
47
  case "retired":
@@ -57,7 +58,7 @@ export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLA
57
58
  return exhaustive;
58
59
  }
59
60
  }
60
- throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(successor !== undefined ? { currentName: successor } : {}) });
61
+ throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(guidance !== undefined ? { guidance } : {}), ...(successor !== undefined ? { currentName: successor } : {}) });
61
62
  }
62
63
  }
63
64
  }
@@ -0,0 +1,11 @@
1
+ import type { ShellWrapperRow } from "./shell-wrapper-table.js";
2
+ /**
3
+ * The PROJECTION {@link import("./shell-wrapper-table.js").SHELL_WRAPPER_TABLE} is: the second lexer's view of the program read model's rows (`program-read-model.ts`
4
+ * — one table of program facts, read through the binary gate). A launcher row's options by arity (a value word, a
5
+ * switch or an attached-only value, a command string), its required operand (a duration, or the spelling it must have),
6
+ * its bare-dash switch and its `NAME=value` operands; a row that runs its operands, or a code body, as shell input
7
+ * (`viaShell`); an option that runs a command (`executes`). The table is this projection written out (its
8
+ * literal type is the export's), and `test/shell-wrapper-table-projection.test.ts` pins it equal — a launcher fact is
9
+ * edited in its row, never here.
10
+ */
11
+ export declare function shellWrapperProjection(): Record<string, ShellWrapperRow>;
@@ -0,0 +1,56 @@
1
+ import { programRows } from "../tools/fs/program-read-model.js";
2
+ import { DURATION_SPELLING } from "../tools/fs/program-read-model.rows.js";
3
+ export function shellWrapperProjection() {
4
+ const out = {};
5
+ for (const row of programRows()) {
6
+ const projected = projectRow(row);
7
+ if (projected !== undefined)
8
+ out[row.key] = projected;
9
+ }
10
+ return out;
11
+ }
12
+ function spellings(row) {
13
+ const out = new Map();
14
+ for (const g of row.grammars) {
15
+ const add = (spelling, spec) => {
16
+ const e = out.get(spelling) ?? { spec, arities: new Set() };
17
+ e.arities.add(spec.arity);
18
+ out.set(spelling, e);
19
+ };
20
+ for (const [l, spec] of Object.entries(g.short))
21
+ add(`-${l}`, spec);
22
+ for (const [n, spec] of Object.entries(g.long))
23
+ add(`--${n}`, spec);
24
+ for (const [n, spec] of Object.entries(g.dashWords ?? {}))
25
+ add(`-${n}`, spec);
26
+ }
27
+ return [...out].map(([spelling, e]) => ({ spelling, spec: e.spec, arity: e.arities.size === 1 ? [...e.arities][0] : "disputed" }));
28
+ }
29
+ const runsCommand = (spec) => spec.effect === "executes" || spec.value?.kind === "shell-text" || spec.value?.kind === "command";
30
+ function projectRow(row) {
31
+ const sorted = (xs) => [...new Set(xs)].sort();
32
+ const all = spellings(row);
33
+ const viaShell = row.reentry?.kind === "shell" || row.launcher?.shellText === true || row.body === "first-operand" || row.body === "assignment-values" || (row.operands?.rest?.kind === "program-text" && row.operands.rest.language === "arithmetic");
34
+ if (row.launcher === undefined) {
35
+ if (viaShell)
36
+ return { valueFlags: [], flags: [], commandString: [], viaShell: true };
37
+ const executes = sorted(all.filter((o) => runsCommand(o.spec)).map((o) => o.spelling));
38
+ return executes.length > 0 ? { valueFlags: [], flags: [], commandString: [], executes } : undefined;
39
+ }
40
+ const commandString = sorted(all.filter((o) => o.spec.value?.kind === "shell-text").map((o) => o.spelling));
41
+ const values = all.filter((o) => o.spec.value?.kind !== "shell-text");
42
+ const attached = sorted(values.filter((o) => o.arity === "attached" && !o.spelling.startsWith("--")).map((o) => o.spelling));
43
+ const required = row.launcher.requiredOperands ?? 0;
44
+ const duration = required > 0 && row.launcher.requiredOperandSpelling === DURATION_SPELLING;
45
+ return {
46
+ valueFlags: sorted(values.filter((o) => o.arity === 1).map((o) => o.spelling)),
47
+ flags: sorted(values.filter((o) => o.arity === 0 || (o.arity === "attached" && o.spelling.startsWith("--"))).map((o) => o.spelling)),
48
+ ...(attached.length > 0 ? { attached } : {}),
49
+ commandString,
50
+ ...(required > 0 && !duration ? { positional: row.launcher.requiredOperandSpelling ?? /^/ } : {}),
51
+ ...(duration ? { duration: true } : {}),
52
+ ...(row.launcher.bareDash === true ? { bareDash: true } : {}),
53
+ ...(row.launcher.assignmentOperands === true ? { assignments: true } : {}),
54
+ ...(viaShell ? { viaShell: true } : {}),
55
+ };
56
+ }