@sema-agent/core 7.51.0 → 7.51.2

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 (39) hide show
  1. package/CHANGELOG.md +97 -1
  2. package/dist/agents/subagent.d.ts +5 -0
  3. package/dist/agents/subagent.js +1 -0
  4. package/dist/core/allow-entry.d.ts +4 -2
  5. package/dist/core/allow-entry.js +1 -1
  6. package/dist/core/engine-capabilities.d.ts +15 -0
  7. package/dist/core/engine-capabilities.js +1 -0
  8. package/dist/core/engine-notice.d.ts +8 -5
  9. package/dist/core/locked-config.d.ts +26 -5
  10. package/dist/core/locked-config.js +31 -6
  11. package/dist/core/mcp-server-spec.d.ts +37 -7
  12. package/dist/core/mcp-server-spec.js +84 -1
  13. package/dist/core/mcp.js +16 -4
  14. package/dist/core/runner/prepare-policy-chain.d.ts +1 -1
  15. package/dist/core/runner/prepare-policy-chain.js +4 -2
  16. package/dist/core/runner/session-rule-policy.d.ts +3 -1
  17. package/dist/core/runner/session-rule-policy.js +11 -9
  18. package/dist/core/runner/tool-output-projection.d.ts +5 -0
  19. package/dist/core/runner/tool-output-projection.js +3 -3
  20. package/dist/core/secret-env.d.ts +46 -15
  21. package/dist/core/secret-env.js +84 -18
  22. package/dist/core/session-policy-store.d.ts +54 -14
  23. package/dist/core/session-policy-store.js +83 -20
  24. package/dist/core/spec-contract.d.ts +5 -3
  25. package/dist/core/types.d.ts +2 -1
  26. package/dist/core/types.js +1 -0
  27. package/dist/engine/execution-env/kill-tree.js +4 -4
  28. package/dist/engine/execution-env/node-execution-env.d.ts +11 -4
  29. package/dist/engine/execution-env/node-execution-env.js +9 -4
  30. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -1
  31. package/dist/engine/lsp/node-lsp-manager.js +2 -2
  32. package/dist/index.d.ts +10 -2
  33. package/dist/index.js +7 -2
  34. package/dist/stores/file/fs-atomic.js +2 -2
  35. package/dist/stores/file/session-policy-store.d.ts +3 -3
  36. package/package.json +1 -1
  37. package/test/export-surface.snapshot.json +33 -1
  38. package/wire-changes/core@7.51.1.json +259 -0
  39. package/wire-changes/core@7.51.2.json +293 -0
@@ -1,12 +1,13 @@
1
1
  import { posix } from "node:path";
2
2
  import { isWinFormPath } from "../tools/fs/safety.js";
3
3
  import { NAMESPACED_NAME_SHAPES, carriesNamespaceSeparator } from "./protocol-table.js";
4
- import { compileAllowList, inertEntryPhrase, readRuleEntry, renderListEntry } from "./allow-entry.js";
4
+ import { compileAllowList, compiledEntryName, inertEntryPhrase, readRuleEntry, renderListEntry } from "./allow-entry.js";
5
5
  import { valueTypeLabel } from "./tool-name-list.js";
6
6
  const SESSION_RULE_BUCKETS = { toolAllow: "allow", toolDeny: "deny", allowDirs: "dirs" };
7
7
  const BUCKETS = Object.keys(SESSION_RULE_BUCKETS);
8
8
  const ENTRY_FIELDS = BUCKETS.filter((f) => SESSION_RULE_BUCKETS[f] !== "dirs");
9
- const STORE_STAMPS = new Set(["rev", "gen"]);
9
+ const STORE_STAMPS = new Set(["rev", "gen", "grammar"]);
10
+ export const SESSION_RULES_GRAMMAR = 1;
10
11
  export class SessionPolicyError extends Error {
11
12
  code;
12
13
  detail;
@@ -49,18 +50,37 @@ function lexWithin(root, p) {
49
50
  return true;
50
51
  return np === nr || np.startsWith(nr + "/");
51
52
  }
53
+ function grammarOf(value) {
54
+ const stamp = value.grammar;
55
+ if (stamp === undefined)
56
+ return { grammar: value.rev !== undefined ? 0 : SESSION_RULES_GRAMMAR };
57
+ if (typeof stamp !== "number" || !Number.isInteger(stamp) || stamp < 0) {
58
+ const got = typeof stamp === "number" ? "a number that is not a whole number of 0 or more" : valueTypeLabel(stamp);
59
+ return { got, sentence: `session rules carry a grammar stamp that is ${got} — the stamp is the whole number of the rule grammar the rules were written in (this engine reads grammar ${SESSION_RULES_GRAMMAR})` };
60
+ }
61
+ if (stamp > SESSION_RULES_GRAMMAR) {
62
+ return {
63
+ got: `${stamp}, a grammar newer than this engine's (${SESSION_RULES_GRAMMAR})`,
64
+ sentence: `session rules were written in rule grammar ${stamp}, newer than this engine reads (grammar ${SESSION_RULES_GRAMMAR}) — refusing rather than read their entries in an older grammar; an engine that reads grammar ${stamp} reads them`,
65
+ };
66
+ }
67
+ return { grammar: stamp };
68
+ }
52
69
  export function readSessionRulesShape(value) {
53
70
  const fault = (field, got, sentence) => ({ fault: { field, got }, sentence });
54
71
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
55
72
  const got = Array.isArray(value) ? "an array" : valueTypeLabel(value);
56
73
  return fault("", got, `session rules must be an object of rule buckets (${BUCKETS.join(", ")}), got ${got}`);
57
74
  }
58
- const rules = {};
75
+ const read = grammarOf(value);
76
+ if ("got" in read)
77
+ return fault("grammar", read.got, read.sentence);
78
+ const rules = { grammar: read.grammar };
59
79
  for (const [key, v] of Object.entries(value)) {
60
80
  if (STORE_STAMPS.has(key) || v === undefined)
61
81
  continue;
62
82
  if (!Object.hasOwn(SESSION_RULE_BUCKETS, key)) {
63
- return fault(key, "not a rule bucket", `session rules hold ${JSON.stringify(key)}, which is not a rule bucket — the buckets are ${BUCKETS.join(", ")}, and a shell command rule is a toolDeny / toolAllow entry written Bash(<command>) (Bash(rm:*) refuses every run of rm)`);
83
+ return fault(key, "not a rule bucket", `session rules hold ${JSON.stringify(key)}, which is not a rule bucket — the buckets are ${BUCKETS.join(", ")}, and a shell command rule is a toolDeny / toolAllow entry written Bash(<command>) (Bash(rm:*) matches a command whose first word is rm)`);
64
84
  }
65
85
  const field = key;
66
86
  const listed = Array.isArray(v) ? Array.from(v) : undefined;
@@ -78,34 +98,76 @@ export function readSessionRulesShape(value) {
78
98
  return { rules };
79
99
  }
80
100
  export function normalizeRules(rules) {
101
+ return canonicalRules(shapeOrRefuse(rules));
102
+ }
103
+ function shapeOrRefuse(rules) {
81
104
  const read = readSessionRulesShape(rules);
82
105
  if ("fault" in read)
83
106
  throw new SessionPolicyError("invalid_shape", read.sentence, read.fault);
107
+ return read.rules;
108
+ }
109
+ function canonicalRules(read) {
84
110
  const out = {};
85
111
  for (const field of BUCKETS) {
86
- const list = read.rules[field];
112
+ const list = read[field];
87
113
  if (list === undefined)
88
114
  continue;
89
115
  out[field] = SESSION_RULE_BUCKETS[field] === "dirs" ? list.map((d) => posix.normalize(d).replace(/\/+$/, "") || "/") : [...list];
90
116
  }
91
117
  return out;
92
118
  }
93
- function effectiveEntries(field, list, mountedToolNames) {
94
- return compileAllowList(list ?? [], SESSION_RULE_BUCKETS[field], mountedToolNames).rows.map((r) => r.entry);
119
+ function readsAsNothingIn(grammar, field, entry, compiled) {
120
+ return grammar === 0 && SESSION_RULE_BUCKETS[field] === "allow" && compiledEntryName(compiled) !== entry;
121
+ }
122
+ export function compileSessionRuleBucket(rules, field, roster) {
123
+ const list = compileAllowList(rules[field] ?? [], SESSION_RULE_BUCKETS[field], roster);
124
+ const rows = list.rows.filter((r) => !readsAsNothingIn(rules.grammar, field, r.entry, r.compiled));
125
+ if (rows.length === list.rows.length)
126
+ return list;
127
+ const earlier = list.rows.filter((r) => readsAsNothingIn(rules.grammar, field, r.entry, r.compiled)).map((r) => ({ entry: r.entry, reason: "earlier_grammar" }));
128
+ return { rows, inert: [...list.inert, ...earlier] };
129
+ }
130
+ const ADVISORY_ENTRIES_SHOWN = 5;
131
+ export function earlierGrammarAdvisory(sessionId, entries) {
132
+ const one = entries.length === 1;
133
+ const shown = entries.slice(0, ADVISORY_ENTRIES_SHOWN).map(renderListEntry).join(", ");
134
+ const rest = entries.length > ADVISORY_ENTRIES_SHOWN ? ` and ${entries.length - ADVISORY_ENTRIES_SHOWN} more` : "";
135
+ return (`session permission rules of session ${JSON.stringify(sessionId)}: toolAllow ${shown}${rest} ${one ? "was" : "were"} stored before the rule grammar stamp, when session rules compared an entry as a tool name as written, so a parenthesized entry named no tool — ` +
136
+ `${one ? "it admits" : "they admit"} nothing, as when written. To make ${one ? "it an allow rule" : "them allow rules"}, write the session's rules afresh through this engine (an operator write of a value without the stored row's rev; the write stamps grammar ${SESSION_RULES_GRAMMAR}); left as stored, ${one ? "it keeps" : "they keep"} admitting nothing.`);
137
+ }
138
+ export function earlierGrammarAnnouncer(sink) {
139
+ const announced = new Set();
140
+ return (entries) => {
141
+ const key = JSON.stringify(entries);
142
+ if (sink === undefined || entries.length === 0 || announced.has(key))
143
+ return;
144
+ announced.add(key);
145
+ try {
146
+ sink(entries);
147
+ }
148
+ catch {
149
+ }
150
+ };
151
+ }
152
+ function effectiveEntries(rules, field, mountedToolNames) {
153
+ return compileSessionRuleBucket(rules, field, mountedToolNames).rows.map((r) => r.entry);
95
154
  }
96
155
  function assertEntriesRead(rules, mountedToolNames) {
97
156
  for (const field of ENTRY_FIELDS) {
98
157
  for (const entry of rules[field] ?? []) {
99
- const read = readRuleEntry(entry, SESSION_RULE_BUCKETS[field], mountedToolNames);
158
+ const reading = readRuleEntry(entry, SESSION_RULE_BUCKETS[field], mountedToolNames);
159
+ const read = reading.kind === "row" && readsAsNothingIn(rules.grammar, field, entry, reading.compiled) ? { kind: "inert", reason: "earlier_grammar" } : reading;
100
160
  if (read.kind === "row")
101
161
  continue;
102
162
  const { kind: _kind, ...why } = read;
103
163
  void _kind;
104
- const way = why.verdict === "retired"
105
- ? ` If this deployment mounts its own tool under ${renderListEntry(entry)}, declare it in the store's mountedToolNames.`
106
- : why.currentName === undefined
107
- ? ` An entry is a tool name (Read; a protocol tool is named ${NAMESPACED_NAME_SHAPES}), a protocol namespace (mcp__<server>), or a rule in its tool's own grammar (Bash(<command>), Edit(<path>), Skill(<name>), WebFetch(domain:<host>)).`
108
- : "";
164
+ const way = why.reason === "earlier_grammar"
165
+ ? ` These rules are written in grammar ${rules.grammar} (a stored row from before the grammar stamp), in which this entry names no tool. To make it an allow rule, write the rules afresh — a value without the stored row's rev; the write stamps grammar ${SESSION_RULES_GRAMMAR} — or leave the entry out.`
166
+ : why.verdict === "retired"
167
+ ? ` If this deployment mounts its own tool under ${renderListEntry(entry)}, declare it in the store's mountedToolNames.`
168
+ : why.currentName === undefined
169
+ ? ` An entry is a tool name (Read; a protocol tool is named ${NAMESPACED_NAME_SHAPES}), a protocol namespace (mcp__<server>), or a rule in its tool's own grammar (Bash(<command>), Edit(<path>), Skill(<name>), WebFetch(domain:<host>)).`
170
+ : "";
109
171
  const sentence = why.guidance !== undefined ? ` — ${why.guidance}` : "";
110
172
  throw new SessionPolicyError("inert_entry", `session rules ${field} holds ${inertEntryPhrase({ entry, ...why })}, which ${SESSION_RULE_BUCKETS[field] === "deny" ? "refuses" : "admits"} nothing as written${sentence}; nothing was written.${way}`, { field, entry, ...why });
111
173
  }
@@ -115,8 +177,9 @@ export function judgeSessionRulesWrite(prior, rules, opts, mountedToolNames = NO
115
177
  if (opts?.expectedRev !== undefined && (prior?.rev ?? 0) !== opts.expectedRev) {
116
178
  throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
117
179
  }
118
- const clean = normalizeRules(rules);
119
- assertEntriesRead(clean, mountedToolNames);
180
+ const read = shapeOrRefuse(rules);
181
+ const clean = canonicalRules(read);
182
+ assertEntriesRead(read, mountedToolNames);
120
183
  if (prior !== null && opts?.operator !== true) {
121
184
  const stored = readSessionRulesShape(prior);
122
185
  if ("fault" in stored) {
@@ -127,7 +190,7 @@ export function judgeSessionRulesWrite(prior, rules, opts, mountedToolNames = NO
127
190
  throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
128
191
  }
129
192
  }
130
- return clean;
193
+ return { ...clean, grammar: SESSION_RULES_GRAMMAR };
131
194
  }
132
195
  export function loosenReasons(priorValue, nextValue, mountedToolNames = NO_DECLARED_TOOLS) {
133
196
  const shapeOf = (side, value) => {
@@ -139,16 +202,16 @@ export function loosenReasons(priorValue, nextValue, mountedToolNames = NO_DECLA
139
202
  const prior = shapeOf("prior", priorValue);
140
203
  const next = shapeOf("next", nextValue);
141
204
  const reasons = [];
142
- const keptDeny = new Set(effectiveEntries("toolDeny", next.toolDeny, mountedToolNames));
143
- for (const x of effectiveEntries("toolDeny", prior.toolDeny, mountedToolNames))
205
+ const keptDeny = new Set(effectiveEntries(next, "toolDeny", mountedToolNames));
206
+ for (const x of effectiveEntries(prior, "toolDeny", mountedToolNames))
144
207
  if (!keptDeny.has(x))
145
208
  reasons.push(`removed toolDeny ${renderListEntry(x)}`);
146
209
  if (prior.toolAllow !== undefined) {
147
210
  if (next.toolAllow === undefined)
148
211
  reasons.push("removed toolAllow allowlist (all allowed)");
149
212
  else {
150
- const had = new Set(effectiveEntries("toolAllow", prior.toolAllow, mountedToolNames));
151
- for (const x of effectiveEntries("toolAllow", next.toolAllow, mountedToolNames))
213
+ const had = new Set(effectiveEntries(prior, "toolAllow", mountedToolNames));
214
+ for (const x of effectiveEntries(next, "toolAllow", mountedToolNames))
152
215
  if (!had.has(x))
153
216
  reasons.push(`widened toolAllow with ${renderListEntry(x)}`);
154
217
  }
@@ -7,7 +7,8 @@
7
7
  * 任意命令)。它只能来自部署作者/可信代码路径;LLM 生成的 contract 直接喂 runSpec = 不受信输入获得
8
8
  * 命令执行权(越红线),禁止。contract 里的 `spec` **文本**才是 DATA-tier(runSpec 恒围栏置入 objective)。
9
9
  * 命令可信 ≠ 它跑的代码可信:门跑的测试会加载模型改过的代码,故门子进程的环境缺省经 `scrubSecretEnv`
10
- * (`OracleGateSpec.inheritEnv`,与 tool shell 同一三态),凭据名只在部署显式列名 / `"all"` 时交给它。
10
+ * (`OracleGateSpec.inheritEnv`,与 tool shell 同一三态),凭据名只在部署显式列名 / `"all"` 时交给它;
11
+ * 部署声明的自有凭据名(`declareWithheldEnv`)在任一形下都不交。
11
12
  *
12
13
  * 🔴 **oracle 三值(fable-M6)**:每门结果 ∈ {green, red, error} —— 判据不满足 = red;基础设施故障
13
14
  * (ENOENT/timeout/maxBuffer/parse 解析不出)= **error 非 red**。假红防线:error 绝不冒充"曾经红过"
@@ -56,8 +57,9 @@ export interface OracleGateSpec {
56
57
  * way. Absent / `"scrub"`: the environment without its credential-shaped variables (`scrubSecretEnv`); the command
57
58
  * runs code the model wrote, so the credentials are not handed to it by default. `"all"`: the whole environment.
58
59
  * A list: exactly the named variables that are set (a strict allow list — `PATH` too, when the command is looked
59
- * up on it). Names only, never values: the contract is serializable data. A value of any other shape makes the
60
- * gate `error`.
60
+ * up on it). Under every form, the deployment's declared own credentials (`declareWithheldEnv`) are withheld, and a
61
+ * list naming one makes the gate `error`. Names only, never values: the contract is serializable data. A value of
62
+ * any other shape makes the gate `error`.
61
63
  */
62
64
  inheritEnv?: "all" | "scrub" | string[];
63
65
  }
@@ -18,7 +18,8 @@ export type { PermissionMode } from "./ask-origin.js";
18
18
  export type { ToolEffect, ToolContentOrigin, ReversibilityVerdict, ReadRootGrantCandidate, ToolSpec, ToolReturn, NestedUsage, NestedUsageAccum, ToolExecuteContext } from "./tool-spec.js";
19
19
  export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook, FileEditedNotice, FileEditedHook, HandsBandOptions } from "./hands-band.js";
20
20
  export type { AgentDefinition, SkillSpec } from "./agent-definition.js";
21
- export type { McpServerSpec, A2aServerSpec, McpElicitRequest, McpElicitResponse, OnElicit, McpRevocationLedger, McpMountIdentity } from "./mcp-server-spec.js";
21
+ export type { McpServerSpec, A2aServerSpec, McpElicitRequest, McpElicitResponse, OnElicit, McpRevocationLedger, McpMountIdentity, McpToolAxesRead } from "./mcp-server-spec.js";
22
+ export { readMcpToolAxes } from "./mcp-server-spec.js";
22
23
  export type { TaskLimits } from "./task-limits.js";
23
24
  export type { TaskStatus, RemoteEnvFailureNote, EffectiveMemoryScopes, TaskResult, HumanReviewGateRow } from "./task-result.js";
24
25
  export type { TerminalCause, PausedCause } from "./terminal-cause.js";
@@ -1,4 +1,5 @@
1
1
  export { MODEL_ROLES } from "./model-seat.js";
2
+ export { readMcpToolAxes } from "./mcp-server-spec.js";
2
3
  export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
3
4
  export { __resetMalformedDelegationSeatAnnouncement, backgroundChildTerminal, deliverDelegationLifecycle, emitsChildRowFrames, taskStartedFrame } from "./delegation-frames.js";
4
5
  export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, parentLegIds, settleOwedUserInput, undrainedUserInputNotices, USER_INPUT_UNDRAINED_CAUSES, USER_INPUT_UNDRAINED_FAMILIES } from "./engine-notice.js";
@@ -1,5 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
- import { scrubSecretEnv } from "../../core/secret-env.js";
2
+ import { inheritedHostEnv } from "../../core/secret-env.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  const DEFAULT_GRACE_MS = 3000;
@@ -145,7 +145,7 @@ function enumerateDescendantsSync(rootPid) {
145
145
  let out;
146
146
  try {
147
147
  const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], {
148
- env: scrubSecretEnv(process.env),
148
+ env: inheritedHostEnv("scrub"),
149
149
  timeout: PS_ENUMERATION_TIMEOUT_MS,
150
150
  encoding: "utf8",
151
151
  windowsHide: true,
@@ -211,7 +211,7 @@ function parseEtimeSeconds(etime) {
211
211
  function runTaskkill(args) {
212
212
  try {
213
213
  const child = spawn("taskkill", args, {
214
- env: scrubSecretEnv(process.env),
214
+ env: inheritedHostEnv("scrub"),
215
215
  stdio: "ignore",
216
216
  detached: true,
217
217
  windowsHide: true,
@@ -271,7 +271,7 @@ function tryMsysGroupKill(bashPath, pid) {
271
271
  `[ -n "$g" ] && [ "$g" != "0" ] && kill -9 -"$g" 2>/dev/null; ` +
272
272
  `done; exit 0`;
273
273
  try {
274
- const child = spawn(bashPath, ["-c", script], { env: scrubSecretEnv(process.env), stdio: "ignore", detached: true, windowsHide: true });
274
+ const child = spawn(bashPath, ["-c", script], { env: inheritedHostEnv("scrub"), stdio: "ignore", detached: true, windowsHide: true });
275
275
  child.on("error", () => { });
276
276
  return true;
277
277
  }
@@ -88,6 +88,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
88
88
  /** `ExecutionEnv.cdPath` = the `CDPATH` the shell child receives, read LIVE off the same merge each exec gets (never the
89
89
  * engine process's own environment): its entries, `[]` when the child gets none. Residual: a per-exec `env.CDPATH`. */
90
90
  get cdPath(): readonly string[];
91
+ /** The environment the next shell child would receive, for a READ (the scrub seat is a no-op: a read reports nothing).
92
+ * `undefined` when no child would be spawned at all — the policy is refused against the own credentials declared
93
+ * since construction, and every spawn throws that reason — so a reader sees "the child gets none", never a throw. */
94
+ private childEnvForRead;
91
95
  private shellPath?;
92
96
  private shellEnv?;
93
97
  private inheritEnv;
@@ -108,7 +112,9 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
108
112
  * fail-closed (drop secret-pattern keys, keep PATH/build/locale). `"all"` = full inherit (trusted env);
109
113
  * `string[]` = strict allowlist. `shellEnv`/per-exec `env` always pass through. Any other value — an unknown
110
114
  * word, a non-string, a list entry that is not a variable name — throws here (`inheritEnvPolicyProblem`'s
111
- * reason), the same judgment the oracle gate's `inheritEnv` meets. */
115
+ * reason), the same judgment the oracle gate's `inheritEnv` meets. Whatever the policy, the deployment's declared
116
+ * own credentials (`declareWithheldEnv`) are withheld from the inherited part; a list naming one throws here when
117
+ * the declaration came first, and every spawn refuses it (`spawn_error` / `spawn_failed`) whenever it came. */
112
118
  inheritEnv?: "all" | "scrub" | string[];
113
119
  /** design/105: inject a resident scheduler daemon backend (the shell daemon). Absent ⇒ scheduling INERT. */
114
120
  scheduler?: SchedulerCapability;
@@ -125,9 +131,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
125
131
  * deployments (their pre-suspend dispose becomes a no-op too — do not combine).
126
132
  */
127
133
  retainBackgroundProcesses?: boolean;
128
- /** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set the default
129
- * "scrub" leg withholds from a shell child. Absent ⇒ a once-per-process console summary (loud
130
- * enough to notice, quiet enough not to spam). Never called on the "all"/allowlist legs. */
134
+ /** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set withheld from a
135
+ * shell child — the default "scrub" leg's, and on any leg the declared own credentials the inherited part
136
+ * held (kind `own-credential`). Absent ⇒ a once-per-process console summary (loud enough to notice,
137
+ * quiet enough not to spam). An allow list withholds nothing else, so it reports nothing. */
131
138
  onSecretEnvScrub?: (findings: readonly SecretEnvFinding[]) => void;
132
139
  /** #1081 — the structured-notice seat for the SAME withheld-keys disclosure (`EngineNotice`,
133
140
  * `code: "config.secret_env_scrubbed"`, `detail.names`); independent of `onSecretEnvScrub` (a raw
@@ -7,7 +7,7 @@ import { isAbsolute, join, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
9
9
  import { killProcessTree, shutdownDebug } from "./kill-tree.js";
10
- import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv, scrubSecretEnv } from "../../core/secret-env.js";
10
+ import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv } from "../../core/secret-env.js";
11
11
  import { physicalPathOfAsync } from "../../core/physical-path.js";
12
12
  import { posixModeBitsMeaningful } from "../../core/posix-mode-face.js";
13
13
  import { walkSymlinkChain } from "./symlink-chain.js";
@@ -157,7 +157,7 @@ async function runCommand(command, args, timeoutMs) {
157
157
  child = spawn(command, args, {
158
158
  stdio: ["ignore", "pipe", "ignore"],
159
159
  windowsHide: true,
160
- env: scrubSecretEnv(process.env),
160
+ env: inheritedHostEnv("scrub"),
161
161
  });
162
162
  }
163
163
  catch {
@@ -315,12 +315,17 @@ export function openSpoolPair(base) {
315
315
  export class NodeExecutionEnv {
316
316
  cwd;
317
317
  get homeDir() {
318
- return getShellEnv(this.inheritEnv, this.shellEnv, undefined, () => { }).HOME;
318
+ return this.childEnvForRead()?.HOME;
319
319
  }
320
320
  get cdPath() {
321
- const value = getShellEnv(this.inheritEnv, this.shellEnv, undefined, () => { }).CDPATH;
321
+ const value = this.childEnvForRead()?.CDPATH;
322
322
  return value === undefined || value === "" ? [] : value.split(":");
323
323
  }
324
+ childEnvForRead() {
325
+ if (inheritEnvPolicyProblem(this.inheritEnv) !== undefined)
326
+ return undefined;
327
+ return getShellEnv(this.inheritEnv, this.shellEnv, undefined, () => { });
328
+ }
324
329
  shellPath;
325
330
  shellEnv;
326
331
  inheritEnv;
@@ -21,7 +21,8 @@ export interface NodeLspManagerOptions {
21
21
  /** Injectable spawn (tests supply a fake server; default = {@link defaultLspSpawn} over `child_process.spawn`). */
22
22
  spawn?: LspSpawn;
23
23
  /** The language server's environment for the default spawn, verbatim. Omitted ⇒ the host environment scrubbed of
24
- * credential-shaped names (`scrubSecretEnv`, a tool child's scrub). Unread when {@link spawn} is supplied. */
24
+ * credential-shaped names and of the declared own credentials (`inheritedHostEnv("scrub")`, a tool child's
25
+ * reading; `declareWithheldEnv`). Unread when {@link spawn} is supplied. */
25
26
  env?: Record<string, string>;
26
27
  /** #1081 — the withheld-keys notice sink, SAME shared once-per-process latch/code as the shell-exec
27
28
  * site's `NodeExecutionEnv.onNotice`. Absent ⇒ console only. Unread when {@link spawn} is supplied. */
@@ -9,7 +9,7 @@ import { TransportLspSession } from "../../core/lsp-session.js";
9
9
  import { StdioLspTransport } from "./stdio-lsp-transport.js";
10
10
  import { MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "../../core/lsp.js";
11
11
  import { asThrownError, thrownText } from "../../core/thrown-value.js";
12
- import { announceSecretEnvScrub, scrubSecretEnv } from "../../core/secret-env.js";
12
+ import { announceSecretEnvScrub, inheritedHostEnv } from "../../core/secret-env.js";
13
13
  export const DEFAULT_LSP_SERVERS = {
14
14
  typescript: "typescript-language-server --stdio",
15
15
  javascript: "typescript-language-server --stdio",
@@ -336,7 +336,7 @@ const lspSpawnWithEnv = (env, onNotice) => (command, args, cwd, signal) => new P
336
336
  let scrubbedEnv = env;
337
337
  if (scrubbedEnv == null) {
338
338
  const findings = [];
339
- scrubbedEnv = scrubSecretEnv(process.env, findings);
339
+ scrubbedEnv = inheritedHostEnv("scrub", findings);
340
340
  announceSecretEnvScrub(onNotice, findings);
341
341
  }
342
342
  child = spawn(command, args, { ...lspSpawnOptions(cwd), env: scrubbedEnv });
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export type { RunInternals, ResolvedWorkspace } from "./core/runner/prepare-task
11
11
  export type { RunnerSelfSeat } from "./core/runner/contracts.js";
12
12
  export type { BudgetAxis } from "./core/runner/assemble-result.js";
13
13
  export { terminalProjection } from "./core/runner/terminal-projection.js";
14
+ export { toolOutputBlockChars } from "./core/runner/tool-output-projection.js";
14
15
  export { SESSION_LOG_DIGEST_SCHEME, sessionEntryDigest, sessionLogDigest, sessionLogDigestsComparable, } from "./engine/session/log-digest.js";
15
16
  export type { ResumeTaskConfig } from "./core/runner/runtask.js";
16
17
  export { defineTool } from "./core/tools.js";
@@ -75,7 +76,7 @@ export { WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS } from "./orchestration/workflow-siz
75
76
  export { ENGINE_AUTHORITY_ENVELOPE_TAGS } from "./core/untrusted-text.js";
76
77
  export { materializeA2aTools, A2aRpcError, type MaterializedA2a, type A2aPeerStatus, type A2aRefreshResult, type A2aToolAxis } from "./core/a2a.js";
77
78
  export { PROTOCOL_TABLE, MCP_NAMESPACE, A2A_NAMESPACE, protocolOf, type ProtocolNamespace, type ProtocolId } from "./core/protocol-table.js";
78
- export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, judgeSessionRulesWrite, readSessionRulesShape, type SessionPolicyStoreOptions, type SessionRuleShapeRefusal, type SessionRuleEntryRefusal, type SessionRuleEntryField, type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions, } from "./core/session-policy-store.js";
79
+ export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, judgeSessionRulesWrite, readSessionRulesShape, SESSION_RULES_GRAMMAR, compileSessionRuleBucket, type SessionRulesInGrammar, type SessionPolicyStoreOptions, type SessionRuleShapeRefusal, type SessionRuleEntryRefusal, type SessionRuleEntryField, type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions, } from "./core/session-policy-store.js";
79
80
  export { SAFETY_MERGE_CONFORMANCE_CORPUS, type SafetyMergeVector } from "./core/safety-merge-corpus.js";
80
81
  export { SAFETY_AXIS_VOCABULARY } from "./core/safety-axis-vocab.js";
81
82
  export { createSessionRulePolicy } from "./core/runner/session-rule-policy.js";
@@ -98,6 +99,8 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
98
99
  export { walkSymlinkChain, SYMLINK_CHAIN_MAX_HOPS, type SymlinkHopReader } from "./engine/execution-env/symlink-chain.js";
99
100
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
100
101
  export type { SecretEnvFinding, SecretEnvFindingKind } from "./core/secret-env.js";
102
+ export { declareWithheldEnv, inheritedHostEnv, inheritEnvPolicyProblem, type InheritEnvPolicy } from "./core/secret-env.js";
103
+ export { ENGINE_CAPABILITIES, type EngineCapability } from "./core/engine-capabilities.js";
101
104
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
102
105
  export { physicalPathOf, physicalPathOfExisting } from "./core/physical-path.js";
103
106
  export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode, WriteExpectation, WriteReceipt, SymlinkChain, BoundFileObject } from "./internal/harness.js";
@@ -338,12 +341,15 @@ export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedacte
338
341
  export type { Egress, EgressTier } from "./core/untrusted-egress.js";
339
342
  export type { RedactionFinding, RedactionReport, RedactionConfidence, RedactionSource, SummarizableFinding, } from "./core/untrusted-egress.js";
340
343
  export { summarizeRedactions } from "./core/untrusted-egress.js";
344
+ export { headerOwnershipCollision } from "./core/protocol-http-auth.js";
345
+ export type { ProtocolHttpAuth } from "./core/protocol-http-auth.js";
341
346
  export { MemoryRosterStore, FileRosterStore, type RosterStore, type RosterEntry, type RosterAccess, type RosterGcOptions } from "./agents/roster-store.js";
342
347
  export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, type ConfigKnob, type ConfigOverrideDeclaration, type ConfigProvenance, type EffectiveConfigField, } from "./config/catalog.js";
343
348
  export { normalizeAgentName } from "./core/task-registry.js";
344
349
  export { DELEGATION_MAX_CONCURRENT_DEFAULT, DELEGATION_MAX_PER_SESSION_DEFAULT, SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT, } from "./config/defaults.js";
345
350
  export { COORDINATOR_ROLE_PROMPT, TEAMMATE_COMMUNICATION_ADDENDUM } from "./prompts/coordinator.js";
346
- export { createSubagentTool, FORK_SUBAGENT_TYPE, GENERAL_PURPOSE_SUBAGENT_TYPE, agentWhenToUseText, FORK_DIRECTIVE_FRAME, SUBAGENT_SYSTEM_NOTE, type SubagentToolOptions, resolveDelegationEntryCaps, type ResolvedDelegationEntryCaps, type SubagentSpawnContext, type SubagentSteerHandle, type SubagentStep, type SubagentEditedFile, } from "./agents/subagent.js";
351
+ export type { BuiltinSubagentType } from "./agents/subagent.js";
352
+ export { createSubagentTool, FORK_SUBAGENT_TYPE, BUILTIN_SUBAGENT_TYPES, GENERAL_PURPOSE_SUBAGENT_TYPE, agentWhenToUseText, FORK_DIRECTIVE_FRAME, SUBAGENT_SYSTEM_NOTE, type SubagentToolOptions, resolveDelegationEntryCaps, type ResolvedDelegationEntryCaps, type SubagentSpawnContext, type SubagentSteerHandle, type SubagentStep, type SubagentEditedFile, } from "./agents/subagent.js";
347
353
  export { getSessionRetainLedger, releaseSessionRetainLedger, } from "./agents/retain-ledger.js";
348
354
  export { createSendMessageTool, createAgentContinuationVerb, SEND_MESSAGE_TOOL_NAME, type SendMessageToolOptions, type AgentContinuationReceipt } from "./agents/send-message-tool.js";
349
355
  export { createPeerAdmission, peerAdmissionFor, judgePeerAdmission, resolvePeerLaneConfig, PEER_LANE_DEFAULTS, PEER_HOP_CHAIN_WINDOW, PEER_MESSAGE_NOTICE, createPeerSelfRef, createPeerInboundChainRef, peerAxisToken, appendHopToken, type PeerAdmission, type PeerLaneConfig, type PeerAdmissionOptions, type PeerAdmissionRequest, type PeerAdmissionVerdict, type PeerAdmissionRefusal, type PeerRefusalCode, type PeerAxisTag, type PeerIdentity, type PeerSelfRef, type PeerInboundChainRef, } from "./agents/peer-admission.js";
@@ -395,3 +401,5 @@ export { RULE_OFFERS_ABSENCES, type RuleOffersAbsence } from "./core/ask-origin.
395
401
  export { BOUND_GRANT_ABSENCES, type BoundGrantAbsence } from "./core/skill-grant-binding.js";
396
402
  export { DISPOSABLE_PROMPT_SECTIONS, type DisposablePromptSection, type PromptSectionEntry, type PromptSectionDisposition, type PromptSectionAuthor, type McpInstructionsEntry } from "./core/prompt-section-seat.js";
397
403
  export type { McpRevocationLedger, McpMountIdentity } from "./core/mcp-server-spec.js";
404
+ export { readMcpToolAxes } from "./core/mcp-server-spec.js";
405
+ export type { McpToolAxesRead } from "./core/mcp-server-spec.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { Runner, runTask } from "./core/runner/runtask.js";
2
2
  export { resolveTaskLimits } from "./core/runner/prepare-task.js";
3
3
  export { terminalProjection } from "./core/runner/terminal-projection.js";
4
+ export { toolOutputBlockChars } from "./core/runner/tool-output-projection.js";
4
5
  export { SESSION_LOG_DIGEST_SCHEME, sessionEntryDigest, sessionLogDigest, sessionLogDigestsComparable, } from "./engine/session/log-digest.js";
5
6
  export { defineTool } from "./core/tools.js";
6
7
  export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
@@ -61,7 +62,7 @@ export { WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS } from "./orchestration/workflow-siz
61
62
  export { ENGINE_AUTHORITY_ENVELOPE_TAGS } from "./core/untrusted-text.js";
62
63
  export { materializeA2aTools, A2aRpcError } from "./core/a2a.js";
63
64
  export { PROTOCOL_TABLE, MCP_NAMESPACE, A2A_NAMESPACE, protocolOf } from "./core/protocol-table.js";
64
- export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, judgeSessionRulesWrite, readSessionRulesShape, } from "./core/session-policy-store.js";
65
+ export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, judgeSessionRulesWrite, readSessionRulesShape, SESSION_RULES_GRAMMAR, compileSessionRuleBucket, } from "./core/session-policy-store.js";
65
66
  export { SAFETY_MERGE_CONFORMANCE_CORPUS } from "./core/safety-merge-corpus.js";
66
67
  export { SAFETY_AXIS_VOCABULARY } from "./core/safety-axis-vocab.js";
67
68
  export { createSessionRulePolicy } from "./core/runner/session-rule-policy.js";
@@ -82,6 +83,8 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
82
83
  export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
83
84
  export { walkSymlinkChain, SYMLINK_CHAIN_MAX_HOPS } from "./engine/execution-env/symlink-chain.js";
84
85
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
86
+ export { declareWithheldEnv, inheritedHostEnv, inheritEnvPolicyProblem } from "./core/secret-env.js";
87
+ export { ENGINE_CAPABILITIES } from "./core/engine-capabilities.js";
85
88
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
86
89
  export { physicalPathOf, physicalPathOfExisting } from "./core/physical-path.js";
87
90
  export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, canonicalPathIsAuthoritative, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
@@ -276,12 +279,13 @@ export { A2A_TASK_STATES, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-t
276
279
  export { InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, WorkflowJournalOversizeError, assertJournalEntryFits, } from "./core/workflow-journal-store.js";
277
280
  export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedactedSummary } from "./core/untrusted-egress.js";
278
281
  export { summarizeRedactions } from "./core/untrusted-egress.js";
282
+ export { headerOwnershipCollision } from "./core/protocol-http-auth.js";
279
283
  export { MemoryRosterStore, FileRosterStore } from "./agents/roster-store.js";
280
284
  export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, } from "./config/catalog.js";
281
285
  export { normalizeAgentName } from "./core/task-registry.js";
282
286
  export { DELEGATION_MAX_CONCURRENT_DEFAULT, DELEGATION_MAX_PER_SESSION_DEFAULT, SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT, } from "./config/defaults.js";
283
287
  export { COORDINATOR_ROLE_PROMPT, TEAMMATE_COMMUNICATION_ADDENDUM } from "./prompts/coordinator.js";
284
- export { createSubagentTool, FORK_SUBAGENT_TYPE, GENERAL_PURPOSE_SUBAGENT_TYPE, agentWhenToUseText, FORK_DIRECTIVE_FRAME, SUBAGENT_SYSTEM_NOTE, resolveDelegationEntryCaps, } from "./agents/subagent.js";
288
+ export { createSubagentTool, FORK_SUBAGENT_TYPE, BUILTIN_SUBAGENT_TYPES, GENERAL_PURPOSE_SUBAGENT_TYPE, agentWhenToUseText, FORK_DIRECTIVE_FRAME, SUBAGENT_SYSTEM_NOTE, resolveDelegationEntryCaps, } from "./agents/subagent.js";
285
289
  export { getSessionRetainLedger, releaseSessionRetainLedger, } from "./agents/retain-ledger.js";
286
290
  export { createSendMessageTool, createAgentContinuationVerb, SEND_MESSAGE_TOOL_NAME } from "./agents/send-message-tool.js";
287
291
  export { createPeerAdmission, peerAdmissionFor, judgePeerAdmission, resolvePeerLaneConfig, PEER_LANE_DEFAULTS, PEER_HOP_CHAIN_WINDOW, PEER_MESSAGE_NOTICE, createPeerSelfRef, createPeerInboundChainRef, peerAxisToken, appendHopToken, } from "./agents/peer-admission.js";
@@ -328,3 +332,4 @@ export { SHADOW_REASON, allowRuleShadowSentence, cardDisclosureOf } from "./core
328
332
  export { RULE_OFFERS_ABSENCES } from "./core/ask-origin.js";
329
333
  export { BOUND_GRANT_ABSENCES } from "./core/skill-grant-binding.js";
330
334
  export { DISPOSABLE_PROMPT_SECTIONS } from "./core/prompt-section-seat.js";
335
+ export { readMcpToolAxes } from "./core/mcp-server-spec.js";
@@ -5,7 +5,7 @@ import { hrtime } from "node:process";
5
5
  import { basename, dirname, join, resolve as resolvePath } from "node:path";
6
6
  import { createHash, randomBytes } from "node:crypto";
7
7
  import { execFileSync } from "node:child_process";
8
- import { scrubSecretEnv } from "../../core/secret-env.js";
8
+ import { inheritedHostEnv } from "../../core/secret-env.js";
9
9
  import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
10
10
  import { thrownText } from "../../core/thrown-value.js";
11
11
  const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
@@ -713,7 +713,7 @@ function probeStartTimesOnce(pid) {
713
713
  encoding: "utf8",
714
714
  timeout: START_TIME_PROBE_TIMEOUT_MS,
715
715
  stdio: ["ignore", "pipe", "ignore"],
716
- env: { ...scrubSecretEnv(process.env), LC_ALL: "C", LANG: "C" },
716
+ env: { ...inheritedHostEnv("scrub"), LC_ALL: "C", LANG: "C" },
717
717
  });
718
718
  }
719
719
  catch (err) {
@@ -102,9 +102,9 @@ export declare function judgeSessionPolicyRowObject(raw: string): {
102
102
  *
103
103
  * Required: `rev` >= 1 (a rev-0 row would read as the live reader's SYNTHETIC empty seed); `gen`, when present, a
104
104
  * whole number (a STRING `rev` poisons the CAS arithmetic); the rules themselves read by the ONE shape reading
105
- * (`readSessionRulesShape` — every key a bucket, every bucket a list of strings), so a row this engine cannot read as
106
- * rules (a key that is no bucket) is refused like any malformed row, never read as fewer rules. The `__sid`/`__principal`
107
- * metadata is STRIPPED so the value is byte-identical to the in-memory twin's.
105
+ * (`readSessionRulesShape` — every key a bucket, every bucket a list of strings, a `grammar` this engine reads), so a row it
106
+ * cannot read as rules (a key that is no bucket, a later grammar) is refused like any malformed row, never read as fewer rules.
107
+ * The `__sid`/`__principal` metadata is STRIPPED so the value is byte-identical to the in-memory twin's.
108
108
  */
109
109
  export declare function judgeSessionPolicyRowShape(row: Record<string, unknown>): {
110
110
  rules: StoredSessionRules;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.51.0",
3
+ "version": "7.51.2",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",