@sema-agent/server 7.87.3 → 7.88.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.
Files changed (57) hide show
  1. package/dist/auth-bridge.js +5 -3
  2. package/dist/boot/device-lane.d.ts +12 -19
  3. package/dist/boot/device-lane.js +3 -13
  4. package/dist/boot/stage-01-config.js +2 -1
  5. package/dist/boot/stage-05-execution-env.js +1 -2
  6. package/dist/boot/stage-07-capability-layer.js +2 -1
  7. package/dist/boot/stage-09-leader.js +2 -1
  8. package/dist/boot/stage-10-http-server.js +4 -2
  9. package/dist/boot/stores.js +2 -1
  10. package/dist/brain.js +3 -2
  11. package/dist/config-catalog.js +4 -3
  12. package/dist/config-center/apply-effective.js +3 -2
  13. package/dist/config-center/http-client.d.ts +11 -2
  14. package/dist/config-center/http-client.js +1 -1
  15. package/dist/config-center/read-warnings.d.ts +1 -1
  16. package/dist/config-center/read-warnings.js +5 -3
  17. package/dist/config-center/types.d.ts +3 -1
  18. package/dist/config-invariants.d.ts +2 -2
  19. package/dist/config-invariants.js +12 -0
  20. package/dist/config-provider.d.ts +9 -1
  21. package/dist/config-provider.js +3 -1
  22. package/dist/config-types.d.ts +7 -0
  23. package/dist/config.js +28 -3
  24. package/dist/device-enrollment.d.ts +25 -4
  25. package/dist/device-enrollment.js +20 -2
  26. package/dist/device-protocol.d.ts +15 -0
  27. package/dist/device-protocol.js +2 -0
  28. package/dist/device-ws-hub.d.ts +11 -0
  29. package/dist/device-ws-hub.js +6 -0
  30. package/dist/fleet-client.js +2 -1
  31. package/dist/hooks/hook-llm.js +3 -2
  32. package/dist/http/admission.js +10 -1
  33. package/dist/http/device-placement.d.ts +177 -0
  34. package/dist/http/device-placement.js +148 -0
  35. package/dist/http/durable-run-leg.js +4 -0
  36. package/dist/http/resume-legs.js +4 -0
  37. package/dist/http/route-ctx.d.ts +8 -0
  38. package/dist/http/routes/capabilities.js +1 -1
  39. package/dist/http/routes/devices.d.ts +18 -8
  40. package/dist/http/routes/devices.js +175 -12
  41. package/dist/http/routes/tasks.js +10 -0
  42. package/dist/http/server.d.ts +13 -0
  43. package/dist/http/wire-types.d.ts +15 -0
  44. package/dist/model-route-url.d.ts +15 -0
  45. package/dist/model-route-url.js +19 -0
  46. package/dist/observability/fail-open.d.ts +11 -1
  47. package/dist/observability/fail-open.js +18 -2
  48. package/dist/request-key-closure.js +1 -1
  49. package/dist/run-local.js +2 -1
  50. package/dist/sealed-key.js +2 -1
  51. package/dist/security.d.ts +6 -0
  52. package/dist/security.js +3 -0
  53. package/dist/session-titler.js +4 -2
  54. package/dist/trace/redact.d.ts +20 -0
  55. package/dist/trace/redact.js +117 -26
  56. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +979 -0
  57. package/package.json +8 -3
@@ -136,4 +136,24 @@ export declare function redactDeep(value: unknown): unknown;
136
136
  * owner-scoped, so this preview is only ever shown to the ask's own principal (or an operator inspecting it).
137
137
  */
138
138
  export declare function redactedPreview(value: unknown, cap?: number): string | null;
139
+ /**
140
+ * 🔴 **配置型 URL 上日志 / 诊断面的唯一 display-safe 铸点**(S-475 件B;`build*` = 返回纯数据的一只纯函数)。
141
+ *
142
+ * 病(7.87.2 亲核):启动日志 `listening` 行把配置里的 URL 写**原值** —— `brain: brainSummary(config)` 里的
143
+ * `gateway: config.gatewayBaseUrl` 与同行的 `otel: config.otel.endpoint`,logger 不脱敏。部署若把凭据放在
144
+ * 网关 URL 的 userinfo / query 里,密码明文落 `engine-*.log`(日志的读者面比 run 错误文本更宽:它落盘、
145
+ * 被采集、进工单)。同类出口不止这两处,类级机器门在 `test/config-url-display-safe.test.ts`。
146
+ *
147
+ * **内部就调 {@link redactSecrets} —— 同一条 URL 律、同一张名表、同一批记号,零第二份实现**:
148
+ * 本函数自己只加两件事,两件都是**配置型 URL 特有**的:
149
+ * ① **有效性闸**:配置里的那一位应当是一个**绝对 URL**。`new URL(raw)` 解析不动 ⇒ 这一位根本不是 URL
150
+ * (裸 `host:port`、相对路径、手滑的散文),自由文本的 URL 律对它没有定界依据 ⇒ 返回**占位**
151
+ * {@link DISPLAY_SAFE_URL_TOKEN},**不回落原值**([ref]:安全轴上的兜底必须 fail-closed 或响亮)。
152
+ * 成文代价:`MODEL_GATEWAY_BASEURL=127.0.0.1:8000` 这种缺 scheme 的配置在日志上变成 `«redacted:url»`
153
+ * —— 而它在 `fetch` 那一侧本来就是必然失败的形(WHATWG 要求绝对 URL),日志上响亮一点方向一致。
154
+ * ② **空串 = 未设**:里面没有凭据,原样回空串(不铸占位 —— 占位会让「没配」读成「配了但看不见」)。
155
+ * 解析成功后**不重建 URL**(`new URL(...).toString()` 会归一化路径/端口/大小写):保形是这条律的一半,
156
+ * 运维要靠 host / port / path / 非凭据参数逐字排障,所以回的是**原串经脱敏器**的结果。
157
+ */
158
+ export declare function buildDisplaySafeUrl(raw: string): string;
139
159
  //# sourceMappingURL=redact.d.ts.map
@@ -591,11 +591,28 @@ const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => {
591
591
  `|(?<!${WORD_SEGMENT_CLASS})${keywordSrc}(?:${twoStrongAhead(valueClass)}|${noConnectorAhead(valueClass)})${atLeast(valueClass, min)}${tailSrc})`, "g");
592
592
  return re;
593
593
  };
594
+ const CREDENTIAL_NAME_WORDS = [
595
+ "api_key", "apikey", "api-key", "key",
596
+ "token", "access_token", "refresh_token", "id_token",
597
+ "auth", "authorization",
598
+ "password", "passwd", "pwd",
599
+ "secret", "client_secret", "private_key",
600
+ "signature", "sig",
601
+ "x-amz-signature", "x-amz-security-token", "x-amz-credential",
602
+ "sas",
603
+ ];
604
+ const CREDENTIAL_NAME_ALT = (() => {
605
+ for (const w of CREDENTIAL_NAME_WORDS) {
606
+ if (!/^[a-z][a-z0-9_-]*$/.test(w))
607
+ throw new Error(`redact: credential name ${JSON.stringify(w)} must be lowercase [a-z0-9_-] (the alternation is built without escaping)`);
608
+ }
609
+ return [...CREDENTIAL_NAME_WORDS].sort((a, b) => b.length - a.length || (a < b ? -1 : 1)).join("|");
610
+ })();
594
611
  const STRUCTURAL_SECRET_PATTERNS = [
595
612
  [/-----BEGIN(?:[^-](?:[^-](?:[^-]|-(?!-))*)?)?PRIVATE KEY(?: BLOCK)?-----[\s\S]{0,8192}?-----END(?:[^-](?:[^-](?:[^-]|-(?!-))*)?)?PRIVATE KEY(?: BLOCK)?-----/g, "«redacted:private-key»"],
596
613
  [/---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----[\s\S]{0,8192}?---- END SSH2 ENCRYPTED PRIVATE KEY ----/g, "«redacted:private-key»"],
597
- [/:(?<=[A-Za-z][A-Za-z0-9+.-]*:)\/\//g, "$1:«redacted»@"],
598
- [/("(?:private_key|client_secret|password|api[_-]?key|secret|token)"\s*:\s*)"[^"\\]*(?:\\.[^"\\]*)*"/gi, '$1"«redacted»"'],
614
+ [/:(?<=[A-Za-z][A-Za-z0-9+.-]*:)\/\//g, "$1«redacted»"],
615
+ [new RegExp(String.raw `("(?:${CREDENTIAL_NAME_ALT})"\s*:\s*)"[^"\\]*(?:\\.[^"\\]*)*"`, "gi"), '$1"«redacted»"'],
599
616
  ];
600
617
  const PPK_CONTAINER_TOKEN = "«redacted:private-key»";
601
618
  const PEM_FRAGMENT_TOKEN = "«redacted:private-key:partial»";
@@ -1067,7 +1084,8 @@ const FRAGMENT_SECRET_PATTERNS = [
1067
1084
  [valueArm("(?:gl(?:pat|dt|rt|rtr|cbt|ptt|oas|ft|imt|agent|wt|soat|ffct)-|(?:sk|rk)_(?:live|test|org)_|pypi-)", "[A-Za-z0-9_-]", 8, "needs-left-boundary"), "«redacted:key:partial»"],
1068
1085
  [valueArm("xapp-\\d-", "[A-Za-z0-9-]", 4, "needs-left-boundary"), "«redacted:slack-token:partial»"],
1069
1086
  ];
1070
- const REDACTION_TOKENS = new Set([...FULL_ARM_PATTERNS.map(([, r]) => r), PEM_FRAGMENT_TOKEN, ...FRAGMENT_SECRET_PATTERNS.map(([, r]) => r)]
1087
+ const DISPLAY_SAFE_URL_TOKEN = "«redacted:url»";
1088
+ const REDACTION_TOKENS = new Set([...FULL_ARM_PATTERNS.map(([, r]) => r), PEM_FRAGMENT_TOKEN, DISPLAY_SAFE_URL_TOKEN, ...FRAGMENT_SECRET_PATTERNS.map(([, r]) => r)]
1071
1089
  .flatMap((r) => r.match(/«[^»]*»/g) ?? []));
1072
1090
  {
1073
1091
  const closed = new RegExp(`^${REDACTION_TOKEN_RE_SRC}$`);
@@ -1091,11 +1109,11 @@ const FRAGMENT_ARM_TOKENS = new Set(FRAGMENT_SECRET_PATTERNS.map(([, repl]) => r
1091
1109
  }
1092
1110
  const armOfToken = (repl) => (FRAGMENT_ARM_TOKENS.has(repl) ? "fragment" : "full");
1093
1111
  const NO_HITS = [];
1094
- const URL_USERINFO_PATTERN = (() => {
1095
- const row = FULL_ARM_PATTERNS.find(([, repl]) => repl.endsWith("»@"));
1096
- if (row === undefined)
1097
- throw new Error("redact: URL-userinfo rule missing from FULL_ARM_PATTERNS");
1098
- return row;
1112
+ const URL_CREDENTIAL_PATTERN = (() => {
1113
+ const rows = FULL_ARM_PATTERNS.filter(([, repl]) => repl === "$1«redacted»");
1114
+ if (rows.length !== 1)
1115
+ throw new Error(`redact: expected exactly one URL-credential rule (replacement "$1«redacted»") in FULL_ARM_PATTERNS, found ${rows.length}`);
1116
+ return rows[0];
1099
1117
  })();
1100
1118
  const SSH2_FENCE_PATTERN = (() => {
1101
1119
  const row = FULL_ARM_PATTERNS.find(([re]) => re.source.startsWith(SSH2_FENCE_OPEN));
@@ -1105,7 +1123,51 @@ const SSH2_FENCE_PATTERN = (() => {
1105
1123
  })();
1106
1124
  const isRegexSpace = (c) => c === 32 || (c >= 9 && c <= 13) || c === 0xa0 || c === 0x1680 || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x2029 || c === 0x202f || c === 0x205f || c === 0x3000 || c === 0xfeff;
1107
1125
  const isLineTerminator = (c) => c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
1126
+ const isControlCode = (c) => c < 0x20 || c === 0x7f;
1108
1127
  const TOKEN_UNIT_Y = new RegExp(REDACTION_TOKEN_RE_SRC, "y");
1128
+ const CREDENTIAL_PARAM_Y = new RegExp(`(?:${CREDENTIAL_NAME_ALT})=`, "iy");
1129
+ const CREDENTIAL_NAME_SET = new Set(CREDENTIAL_NAME_WORDS);
1130
+ const CREDENTIAL_NAME_SCAN_MAX = Math.max(...CREDENTIAL_NAME_WORDS.map((w) => w.length)) * 3 + 1;
1131
+ function decodeAsciiName(name) {
1132
+ let out = "";
1133
+ for (let i = 0; i < name.length; i++) {
1134
+ const c = name.charCodeAt(i);
1135
+ if (c !== 37) {
1136
+ out += name[i];
1137
+ continue;
1138
+ }
1139
+ const hex = name.slice(i + 1, i + 3);
1140
+ if (hex.length !== 2 || !/^[0-9A-Fa-f]{2}$/.test(hex))
1141
+ return undefined;
1142
+ const b = Number.parseInt(hex, 16);
1143
+ if (b >= 0x80)
1144
+ return undefined;
1145
+ out += String.fromCharCode(b);
1146
+ i += 2;
1147
+ }
1148
+ return out;
1149
+ }
1150
+ function credentialParamValueStart(text, p) {
1151
+ CREDENTIAL_PARAM_Y.lastIndex = p;
1152
+ if (CREDENTIAL_PARAM_Y.test(text))
1153
+ return CREDENTIAL_PARAM_Y.lastIndex;
1154
+ const limit = Math.min(text.length, p + CREDENTIAL_NAME_SCAN_MAX);
1155
+ let hasPct = false;
1156
+ for (let i = p; i < limit; i++) {
1157
+ const c = text.charCodeAt(i);
1158
+ if (c === 37) {
1159
+ hasPct = true;
1160
+ continue;
1161
+ }
1162
+ if (c !== CH_EQ)
1163
+ continue;
1164
+ if (!hasPct)
1165
+ return -1;
1166
+ const decoded = decodeAsciiName(text.slice(p, i));
1167
+ return decoded !== undefined && CREDENTIAL_NAME_SET.has(decoded.toLowerCase()) ? i + 1 : -1;
1168
+ }
1169
+ return -1;
1170
+ }
1109
1171
  function tokenUnitEnd(text, p) {
1110
1172
  TOKEN_UNIT_Y.lastIndex = p;
1111
1173
  return TOKEN_UNIT_Y.test(text) ? TOKEN_UNIT_Y.lastIndex : -1;
@@ -1195,8 +1257,10 @@ function opaqueUnitEnd(p) {
1195
1257
  scanEvidenceCursor = k;
1196
1258
  return k < st.length && st[k] === p ? scanEvidence.ends[k] : -1;
1197
1259
  }
1198
- function scanUserinfoRun(text, i, kind, json) {
1260
+ let lastAtInRun = -1;
1261
+ function scanUrlRun(text, i, kind, json) {
1199
1262
  const n = text.length;
1263
+ lastAtInRun = -1;
1200
1264
  while (i < n) {
1201
1265
  const unit = opaqueUnitEnd(i);
1202
1266
  if (unit >= 0) {
@@ -1211,11 +1275,11 @@ function scanUserinfoRun(text, i, kind, json) {
1211
1275
  }
1212
1276
  return i;
1213
1277
  }
1214
- if (c === 64 || isRegexSpace(c) || (json && c === 34))
1278
+ if (isRegexSpace(c) || (json && c === 34))
1215
1279
  return i;
1216
1280
  switch (kind) {
1217
1281
  case "plain-user":
1218
- if (c === 47 || c === 58)
1282
+ if (c === CH_AT || c === 47 || c === 58)
1219
1283
  return i;
1220
1284
  break;
1221
1285
  case "plain-pass":
@@ -1226,33 +1290,50 @@ function scanUserinfoRun(text, i, kind, json) {
1226
1290
  if (c === 58 && text.charCodeAt(i + 1) === 47 && text.charCodeAt(i + 2) === 47)
1227
1291
  return i;
1228
1292
  break;
1293
+ case "param-value":
1294
+ if (c === 38 || c === 35 || isControlCode(c))
1295
+ return i;
1296
+ break;
1229
1297
  }
1298
+ if (c === CH_AT)
1299
+ lastAtInRun = i;
1230
1300
  i += 1;
1231
1301
  }
1232
1302
  return i;
1233
1303
  }
1234
1304
  function matchUserinfoAt(text, p, json) {
1235
- const userEnd = scanUserinfoRun(text, p, "plain-user", json);
1236
- if (text.charCodeAt(userEnd) !== 58)
1237
- return null;
1238
- const keyId = userEnd > p && credentialSpansCover(scanEvidence, p, userEnd);
1239
- const passEnd = scanUserinfoRun(text, userEnd + 1, keyId ? "token-pass" : "plain-pass", json);
1240
- if (passEnd === userEnd + 1 || text.charCodeAt(passEnd) !== 64)
1305
+ const probeEnd = scanUrlRun(text, p, "plain-user", json);
1306
+ const keyId = probeEnd > p && text.charCodeAt(probeEnd) === 58 && credentialSpansCover(scanEvidence, p, probeEnd);
1307
+ scanUrlRun(text, p, keyId ? "token-pass" : "plain-pass", json);
1308
+ if (lastAtInRun <= p)
1241
1309
  return null;
1242
- return [userEnd, passEnd + 1];
1310
+ return lastAtInRun;
1243
1311
  }
1244
- function collectUrlUserinfoSpans(text, json, span) {
1245
- const head = URL_USERINFO_PATTERN[0];
1312
+ function collectUrlCredentialSpans(text, json, span) {
1313
+ const head = URL_CREDENTIAL_PATTERN[0];
1246
1314
  head.lastIndex = 0;
1247
1315
  for (let m = head.exec(text); m !== null; m = head.exec(text)) {
1248
1316
  const colon = m.index;
1249
- const hit = matchUserinfoAt(text, colon + 3, json);
1250
- if (hit === null) {
1317
+ const start = colon + 3;
1318
+ const at = matchUserinfoAt(text, start, json);
1319
+ if (at === null) {
1251
1320
  head.lastIndex = colon + 1;
1252
1321
  continue;
1253
1322
  }
1254
- span(hit[0], hit[1]);
1255
- head.lastIndex = hit[1];
1323
+ span(start, at);
1324
+ head.lastIndex = at + 1;
1325
+ }
1326
+ for (let i = 0; i < text.length; i++) {
1327
+ const c = text.charCodeAt(i);
1328
+ if (c !== 63 && c !== 38 && c !== 35)
1329
+ continue;
1330
+ const valueStart = credentialParamValueStart(text, i + 1);
1331
+ if (valueStart < 0)
1332
+ continue;
1333
+ const valueEnd = scanUrlRun(text, valueStart, "param-value", json);
1334
+ if (valueEnd > valueStart)
1335
+ span(valueStart, valueEnd);
1336
+ i = valueEnd - 1;
1256
1337
  }
1257
1338
  }
1258
1339
  let ppkScanBytes = 0;
@@ -1437,8 +1518,8 @@ const armPool = (() => {
1437
1518
  const members = [];
1438
1519
  for (const row of STRUCTURAL_SECRET_PATTERNS) {
1439
1520
  const order = members.length;
1440
- members.push(row === URL_USERINFO_PATTERN
1441
- ? scannerMember(row[1], order, 2, (text, json, _observe, span) => collectUrlUserinfoSpans(text, json, span))
1521
+ members.push(row === URL_CREDENTIAL_PATTERN
1522
+ ? scannerMember(row[1], order, 2, (text, json, _observe, span) => collectUrlCredentialSpans(text, json, span))
1442
1523
  : row === SSH2_FENCE_PATTERN
1443
1524
  ? scannerMember(row[1], order, 2, (text, _json, _observe, span) => collectSsh2FenceSpans(text, span))
1444
1525
  : regexMember(row, ARM_RANK.structural, order, true));
@@ -1631,4 +1712,14 @@ export function redactedPreview(value, cap = 2000) {
1631
1712
  s = redactSecrets(s, { json: true });
1632
1713
  return s.length > cap ? `${s.slice(0, cap)}…[truncated]` : s;
1633
1714
  }
1715
+ export function buildDisplaySafeUrl(raw) {
1716
+ if (raw === "")
1717
+ return "";
1718
+ if (!URL.canParse(raw))
1719
+ return DISPLAY_SAFE_URL_TOKEN;
1720
+ const u = new URL(raw);
1721
+ if (!u.href.startsWith(`${u.protocol}//`))
1722
+ return DISPLAY_SAFE_URL_TOKEN;
1723
+ return redactSecrets(u.href);
1724
+ }
1634
1725
  //# sourceMappingURL=redact.js.map