@sema-agent/server 7.75.4 → 7.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,10 +31,13 @@ const DEFAULT_PARK_TOMBSTONE_MAX = 2000;
31
31
  const DEFAULT_PARK_TOMBSTONE_TTL_MS = 60 * 60_000;
32
32
  const DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX = 128;
33
33
  const PARKING_RETRY_AFTER_MS = 250;
34
+ function ruleMaterialCarriesCredentialShape(command) {
35
+ return redactSecrets(command, { observe: false }) !== normalizeForRedaction(command);
36
+ }
34
37
  function durableRuleMaterial(material) {
35
38
  if (material === undefined)
36
39
  return { ruleCommand: null, ruleScopeRoot: null };
37
- if (redactSecrets(material.command, { observe: false }) !== normalizeForRedaction(material.command)) {
40
+ if (ruleMaterialCarriesCredentialShape(material.command)) {
38
41
  defaultLogger.warn("approval rule material withheld: the adjudicated command carries credential-shaped bytes", {
39
42
  commandChars: material.command.length,
40
43
  });
@@ -1799,6 +1802,10 @@ export class ToolApprovalCoordinator {
1799
1802
  return withFlag(false, "rule_store_error");
1800
1803
  if (inputWasEdited)
1801
1804
  return withFlag(false, "rule_input_edited");
1805
+ if (ruleMaterialCarriesCredentialShape(material.command)) {
1806
+ defaultLogger.warn("approval rule material withheld on the live leg: the adjudicated command carries credential-shaped bytes", { commandChars: material.command.length });
1807
+ return withFlag(false, "rule_material_absent");
1808
+ }
1802
1809
  if (persistRule.kind === "batch") {
1803
1810
  const offer = material.offers[persistRule.batchOfferIndex];
1804
1811
  if (offer === undefined || offer.kind !== "batch")
@@ -28,10 +28,13 @@
28
28
  * `redactModelText`(四座同调:结果两叶经 `redactTerminalResult`,账本两行经 `redactLedgerEventData`);
29
29
  * 它定的是**写回**判据(认出凭据形才改字节、抛则占位),不是第二张规则表。
30
30
  *
31
- * 两组臂,顺序即优先序:{@link FULL_SECRET_PATTERNS}(完整形)在前,{@link FRAGMENT_SECRET_PATTERNS}
32
- * ([ref] 前缀残片,上游先截后脱的产物 ⇒ 带 `:partial` 后缀的 token)在后。
33
- * ⚠️ **一个例外,自 7.75.2**:完整臂里的**裸形段**内部没有优先序 —— 该段全臂一次收候选跨度、解重叠、一次替换
34
- * ({@link shapeSectionRule}),段内臂序对输出无语义。段**之间**的先后照旧是契约。
31
+ * 臂分**三段 + 一个候选池**,段之间的先后是契约(上下文越多越靠前):结构段(PEM/URL/JSON + 三条体扫描器)
32
+ * → 词法关键字段(`Bearer`/`Basic`/`token`)→ **候选池** → URL 收尾归一化。
33
+ * ⚠️ **池内没有优先序,自 7.76.0**:{@link SHAPE_SECRET_PATTERNS}(裸形八条)与 {@link FRAGMENT_SECRET_PATTERNS}
34
+ * ([ref] 前缀残片八条,上游先截后脱的产物 ⇒ 带 `:partial` 后缀的 token)并进**同一个**候选池
35
+ * ({@link armPoolRule}):全臂看同一份未改写文本收候选跨度 → 不动点 → 并集律解重叠 → 一次替换,
36
+ * **池内臂序对输出无语义**(7.75.2 只做到裸形一段,残片段仍逐臂改写 ⇒ S-282 的两条漏形)。
37
+ * 左边界律因此有**三种**边界:非词内字符 ∨ 转义分隔单元尾 ∨ **一枚已收候选跨度的尾**(= 凭据尾)。
35
38
  */
36
39
  /** 本模块替换 token 的**闭形**正则源(`«redacted»` / `«redacted:aws-key»` / `«redacted:gh-token:partial»` …):
37
40
  * URL-userinfo 臂用它把「用户名位上已经是本模块 token」整个当一个用户名单元吃掉。模块加载时对
@@ -206,23 +206,28 @@ function forgetEscapedSeparatorTails() {
206
206
  escapedTailsValue = undefined;
207
207
  }
208
208
  const VALUE_ARM_STICKY = new Map();
209
+ const VALUE_ARMS = new Set();
209
210
  const VALUE_ARM_BODIES = [];
210
211
  const valueArm = (prefixSrc, valueClass, min, evidence) => {
211
212
  const body = `${prefixSrc}${twoStrongAhead(valueClass)}${atLeast(valueClass, min)}`;
212
213
  VALUE_ARM_BODIES.push(body);
213
- switch (evidence) {
214
- case "self-evident":
215
- return new RegExp(body, "g");
216
- case "needs-left-boundary": {
217
- const g = new RegExp(`(?<!${WORD_INTERIOR_CLASS})${body}`, "g");
218
- VALUE_ARM_STICKY.set(g, new RegExp(body, "y"));
219
- return g;
220
- }
221
- default: {
222
- const unhandled = evidence;
223
- throw new Error(`redact: unhandled value-arm evidence ${String(unhandled)}`);
214
+ const g = (() => {
215
+ switch (evidence) {
216
+ case "self-evident":
217
+ return new RegExp(body, "g");
218
+ case "needs-left-boundary": {
219
+ const re = new RegExp(`(?<!${WORD_INTERIOR_CLASS})${body}`, "g");
220
+ VALUE_ARM_STICKY.set(re, new RegExp(body, "y"));
221
+ return re;
222
+ }
223
+ default: {
224
+ const unhandled = evidence;
225
+ throw new Error(`redact: unhandled value-arm evidence ${String(unhandled)}`);
226
+ }
224
227
  }
225
- }
228
+ })();
229
+ VALUE_ARMS.add(g);
230
+ return g;
226
231
  };
227
232
  let anyValueArm;
228
233
  function anyValueArmRe() {
@@ -312,10 +317,19 @@ function collectValueArmSpans(g, y, repl, order, text, out) {
312
317
  pos = end;
313
318
  }
314
319
  }
315
- function runValueArm(g, y, repl, text) {
316
- const spans = [];
317
- collectValueArmSpans(g, y, repl, 0, text, spans);
318
- return applySpans(text, spans);
320
+ function collectStickySpans(y, repl, order, text, at, out) {
321
+ let pos = 0;
322
+ for (const p of at) {
323
+ if (p < pos)
324
+ continue;
325
+ y.lastIndex = p;
326
+ const m = y.exec(text);
327
+ if (m === null)
328
+ continue;
329
+ const end = p + m[0].length;
330
+ out.push({ start: p, end, repl, order });
331
+ pos = end;
332
+ }
319
333
  }
320
334
  const WORD_SEGMENT_CLASS = "[A-Za-z0-9._~+/-]";
321
335
  const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => new RegExp(`(?:${keywordSrc}\\s+${atLeast(valueClass, min)}${tailSrc}` +
@@ -747,9 +761,10 @@ function redactionTokenEndAt(s, p, limit) {
747
761
  return 0;
748
762
  }
749
763
  const labelOfToken = (token) => token.replace(/[«»]/g, "").replace(/^redacted:/, "");
764
+ const armOfToken = (repl) => (repl.match(/«[^»]*»/g) ?? [repl]).some((t) => t.endsWith(":partial»")) ? "fragment" : "full";
750
765
  const NO_HITS = [];
751
766
  const singleHit = (label, arm, r) => r.count > 0 ? { out: r.out, hits: [{ label, count: r.count, arm }] } : { out: r.out, hits: NO_HITS };
752
- const regexRule = ([re, repl], arm) => {
767
+ const regexRule = ([re, repl]) => {
753
768
  const groupCount = (new RegExp(`${re.source}|`).exec("")?.length ?? 1) - 1;
754
769
  const refs = [...repl.matchAll(/\$(.)/g)].map(([, d]) => d ?? "");
755
770
  for (const d of refs) {
@@ -757,13 +772,11 @@ const regexRule = ([re, repl], arm) => {
757
772
  throw new Error(`redact: replacement ${JSON.stringify(repl)} uses unsupported ref $${d}`);
758
773
  }
759
774
  const expand = refs.length === 0 ? undefined : (args) => repl.replace(/\$([1-9])/g, (_, d) => args[Number(d)] ?? "");
760
- const sticky = VALUE_ARM_STICKY.get(re);
761
- if (sticky !== undefined && refs.length > 0)
762
- throw new Error(`redact: value arm replacement ${JSON.stringify(repl)} must not use $N refs`);
775
+ const arm = armOfToken(repl);
776
+ if (VALUE_ARMS.has(re) || arm === "fragment")
777
+ throw new Error(`redact: rule ${JSON.stringify(repl)} belongs in the arm pool, not a per-arm section`);
763
778
  const label = labelOfToken(repl);
764
779
  const applyOnce = (text, wantCount) => {
765
- if (sticky !== undefined)
766
- return runValueArm(re, sticky, repl, text);
767
780
  if (!wantCount)
768
781
  return { out: text.replace(re, repl), count: 0 };
769
782
  let n = 0;
@@ -970,12 +983,13 @@ function redactPpkContainers(text, record) {
970
983
  }
971
984
  const URL_USERINFO_LABEL = labelOfToken(URL_USERINFO_PATTERN[1]);
972
985
  const urlUserinfoRule = { run: (text, _wantCount, json) => singleHit(URL_USERINFO_LABEL, "full", redactUrlUserinfo(text, json)) };
973
- const shapeSectionRule = (() => {
974
- const arms = SHAPE_SECRET_PATTERNS.map(([re, repl], order) => {
986
+ const armPoolRule = (() => {
987
+ const arms = [...SHAPE_SECRET_PATTERNS, ...FRAGMENT_SECRET_PATTERNS].map(([re, repl], order) => {
975
988
  if (/\$./.test(repl))
976
- throw new Error(`redact: shape-section replacement ${JSON.stringify(repl)} must not use $N refs`);
989
+ throw new Error(`redact: arm-pool replacement ${JSON.stringify(repl)} must not use $N refs`);
977
990
  return { g: re, y: VALUE_ARM_STICKY.get(re), repl, order };
978
991
  });
992
+ const boundaryArms = arms.flatMap((a) => (a.y === undefined ? [] : [{ y: a.y, repl: a.repl, order: a.order }]));
979
993
  return {
980
994
  run: (text, wantCount) => {
981
995
  const spans = [];
@@ -985,6 +999,24 @@ const shapeSectionRule = (() => {
985
999
  else
986
1000
  collectValueArmSpans(a.g, a.y, a.repl, a.order, text, spans);
987
1001
  }
1002
+ const tried = new Set();
1003
+ const freshEnds = (from) => {
1004
+ const at = [];
1005
+ for (let k = from; k < spans.length; k++) {
1006
+ const e = spans[k].end;
1007
+ if (tried.has(e))
1008
+ continue;
1009
+ tried.add(e);
1010
+ at.push(e);
1011
+ }
1012
+ return at.sort((x, y) => x - y);
1013
+ };
1014
+ for (let at = freshEnds(0); at.length > 0;) {
1015
+ const before = spans.length;
1016
+ for (const a of boundaryArms)
1017
+ collectStickySpans(a.y, a.repl, a.order, text, at, spans);
1018
+ at = freshEnds(before);
1019
+ }
988
1020
  const kept = resolveSpanOverlaps(spans);
989
1021
  const out = applySpans(text, kept).out;
990
1022
  if (!wantCount || kept.length === 0)
@@ -992,18 +1024,17 @@ const shapeSectionRule = (() => {
992
1024
  const byToken = new Map();
993
1025
  for (const sp of kept)
994
1026
  byToken.set(sp.repl, (byToken.get(sp.repl) ?? 0) + 1);
995
- return { out, hits: [...byToken].map(([token, count]) => ({ label: labelOfToken(token), count, arm: "full" })) };
1027
+ return { out, hits: [...byToken].map(([token, count]) => ({ label: labelOfToken(token), count, arm: armOfToken(token) })) };
996
1028
  },
997
1029
  };
998
1030
  })();
999
1031
  const SECRET_RULES = [
1000
- ...STRUCTURAL_SECRET_PATTERNS.map((p) => (p === URL_USERINFO_PATTERN ? urlUserinfoRule : regexRule(p, "full"))),
1001
- { run: (text, _wantCount, _json, observe) => singleHit(labelOfToken(PPK_CONTAINER_TOKEN), "full", redactPpkContainers(text, observe)) },
1002
- { run: (text, _wantCount, json) => singleHit(labelOfToken(PEM_FRAGMENT_TOKEN), "fragment", redactPemFragments(text, json)) },
1003
- { run: (text) => singleHit(labelOfToken(HEADLESS_PPK_TOKEN), "fragment", redactHeadlessPpkBodies(text)) },
1004
- ...KEYWORD_SECRET_PATTERNS.map((p) => regexRule(p, "full")),
1005
- shapeSectionRule,
1006
- ...FRAGMENT_SECRET_PATTERNS.map((p) => regexRule(p, "fragment")),
1032
+ ...STRUCTURAL_SECRET_PATTERNS.map((p) => (p === URL_USERINFO_PATTERN ? urlUserinfoRule : regexRule(p))),
1033
+ { run: (text, _wantCount, _json, observe) => singleHit(labelOfToken(PPK_CONTAINER_TOKEN), armOfToken(PPK_CONTAINER_TOKEN), redactPpkContainers(text, observe)) },
1034
+ { run: (text, _wantCount, json) => singleHit(labelOfToken(PEM_FRAGMENT_TOKEN), armOfToken(PEM_FRAGMENT_TOKEN), redactPemFragments(text, json)) },
1035
+ { run: (text) => singleHit(labelOfToken(HEADLESS_PPK_TOKEN), armOfToken(HEADLESS_PPK_TOKEN), redactHeadlessPpkBodies(text)) },
1036
+ ...KEYWORD_SECRET_PATTERNS.map((p) => regexRule(p)),
1037
+ armPoolRule,
1007
1038
  { run: (text, _wantCount, json) => ({ out: redactUrlUserinfo(text, json).out, hits: NO_HITS }) },
1008
1039
  ];
1009
1040
  let onRedaction;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.75.4",
3
+ "version": "7.76.1",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.17.6",
57
+ "@sema-agent/core": "7.17.7",
58
58
  "@sema-agent/settings-schema": "1.10.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",