@sema-agent/server 7.127.2 → 7.129.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 (112) hide show
  1. package/MIGRATION.md +130 -0
  2. package/USAGE.md +137 -20
  3. package/dist/approval-card.d.ts +53 -26
  4. package/dist/approval-card.js +40 -11
  5. package/dist/approval-crash-convergence.js +4 -4
  6. package/dist/audit.js +4 -1
  7. package/dist/bake-runner/main.js +2 -1
  8. package/dist/boot/execution-lane.d.ts +11 -0
  9. package/dist/boot/execution-lane.js +6 -0
  10. package/dist/boot/memory-consolidation.d.ts +46 -16
  11. package/dist/boot/memory-consolidation.js +15 -8
  12. package/dist/boot/resolve-spec.js +3 -1
  13. package/dist/boot/side-query-lane.d.ts +7 -20
  14. package/dist/boot/side-query-lane.js +0 -24
  15. package/dist/boot/stage-01-config.js +3 -0
  16. package/dist/boot/stage-06-runners.js +1 -1
  17. package/dist/boot/stores.js +2 -2
  18. package/dist/boot/web-admin.js +31 -20
  19. package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
  20. package/dist/boot/webfetch-summarize-lane.js +2 -3
  21. package/dist/brain.js +6 -9
  22. package/dist/capabilities/builtin-tools.d.ts +2 -2
  23. package/dist/capabilities/engine-capabilities.d.ts +14 -0
  24. package/dist/capabilities/engine-capabilities.js +12 -0
  25. package/dist/capabilities/mcp-probe.js +11 -5
  26. package/dist/capability-facts.d.ts +3 -1
  27. package/dist/capability-facts.js +1 -0
  28. package/dist/config-catalog.js +1 -1
  29. package/dist/config-provider.d.ts +17 -4
  30. package/dist/config-provider.js +8 -6
  31. package/dist/config-types.d.ts +6 -5
  32. package/dist/egress-redirect.d.ts +12 -0
  33. package/dist/egress-redirect.js +7 -0
  34. package/dist/entry-mode-clamp-notice.d.ts +30 -0
  35. package/dist/entry-mode-clamp-notice.js +13 -0
  36. package/dist/fleet/fleet-bus.d.ts +7 -0
  37. package/dist/fleet/fleet-bus.js +1 -0
  38. package/dist/hooks/hook-llm.js +6 -2
  39. package/dist/hooks/hook-runner.d.ts +1 -0
  40. package/dist/hooks/hook-runner.js +10 -4
  41. package/dist/host-lsp-manager.d.ts +3 -3
  42. package/dist/http/admission.js +5 -2
  43. package/dist/http/resume-legs.js +46 -27
  44. package/dist/http/route-ctx.d.ts +1 -1
  45. package/dist/http/routes/approvals-assistant.js +6 -4
  46. package/dist/http/routes/capabilities.js +1 -0
  47. package/dist/http/routes/session-sync.js +3 -4
  48. package/dist/http/routes/sessions.js +4 -17
  49. package/dist/http/send.d.ts +1 -1
  50. package/dist/http/send.js +4 -3
  51. package/dist/http/wire-types.d.ts +14 -5
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/main.js +2 -0
  55. package/dist/mcp-assertion-header.d.ts +89 -0
  56. package/dist/mcp-assertion-header.js +49 -0
  57. package/dist/observability/fail-open.d.ts +7 -3
  58. package/dist/observability/fail-open.js +7 -3
  59. package/dist/observability/secret-env-scrub.d.ts +37 -3
  60. package/dist/observability/secret-env-scrub.js +2 -2
  61. package/dist/parked-decide.js +5 -2
  62. package/dist/plugins/host-platform.d.ts +43 -13
  63. package/dist/plugins/host-platform.js +67 -18
  64. package/dist/plugins/memory-embedder.js +5 -0
  65. package/dist/plugins/one-time-migrations.d.ts +180 -9
  66. package/dist/plugins/one-time-migrations.js +315 -3
  67. package/dist/plugins/pg-pool.js +3 -1
  68. package/dist/plugins/remote-env-host.d.ts +17 -20
  69. package/dist/plugins/remote-env-host.js +16 -28
  70. package/dist/plugins/session-policy-store-sql.d.ts +16 -4
  71. package/dist/plugins/session-policy-store-sql.js +17 -20
  72. package/dist/plugins/session-rules-column.d.ts +16 -0
  73. package/dist/plugins/session-rules-column.js +6 -0
  74. package/dist/plugins/tidb-pool.js +4 -1
  75. package/dist/read-face-posture.d.ts +2 -1
  76. package/dist/request-value-closure.js +1 -1
  77. package/dist/retired-card-words.d.ts +17 -0
  78. package/dist/retired-card-words.js +49 -0
  79. package/dist/rule-entry-list.js +2 -0
  80. package/dist/run-local.js +15 -6
  81. package/dist/server-secret-env.d.ts +77 -85
  82. package/dist/server-secret-env.js +40 -26
  83. package/dist/session-policy-wire.d.ts +61 -124
  84. package/dist/session-policy-wire.js +59 -121
  85. package/dist/session-sync.d.ts +8 -6
  86. package/dist/session-sync.js +6 -5
  87. package/dist/spec-fields.d.ts +3 -2
  88. package/dist/standing-answer-grant.d.ts +17 -9
  89. package/dist/standing-answer-grant.js +11 -8
  90. package/dist/task-mcp.d.ts +31 -3
  91. package/dist/task-mcp.js +42 -4
  92. package/dist/task-settings.d.ts +6 -1
  93. package/dist/task-settings.js +1 -1
  94. package/dist/tool-approval.d.ts +14 -7
  95. package/dist/tool-approval.js +23 -18
  96. package/dist/trace/core-keyset-guard.d.ts +3 -3
  97. package/dist/trace/engine-notice-wire.d.ts +1 -1
  98. package/dist/trace/engine-notice-wire.js +1 -0
  99. package/dist/trace/project.d.ts +7 -4
  100. package/dist/trace/project.js +7 -76
  101. package/dist/trace/tool-output-media.d.ts +10 -4
  102. package/dist/trace/tool-output-media.js +7 -29
  103. package/dist/web-admin-client/apply-effective.js +28 -11
  104. package/dist/web-admin-client/facade.d.ts +2 -2
  105. package/dist/web-admin-client/facade.js +2 -2
  106. package/dist/web-admin-client/http-client.d.ts +2 -2
  107. package/dist/web-admin-client/read-face.d.ts +73 -20
  108. package/dist/web-admin-client/read-face.js +76 -36
  109. package/dist/web-admin-client/skills-mcp.d.ts +20 -17
  110. package/dist/web-admin-client/skills-mcp.js +27 -8
  111. package/dist/web-admin-client/types.d.ts +18 -0
  112. package/package.json +3 -3
@@ -1,12 +1,39 @@
1
+ import { SESSION_RULES_GRAMMAR, compileSessionRuleBucket, readSessionRulesShape } from "@sema-agent/core";
1
2
  import { SCHEMA_STATEMENTS, acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
3
  import { PG_SCHEMA_STATEMENTS, acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
3
4
  import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
4
5
  import { ensureIndexes } from "./ensure-index.js";
5
- import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn } from "./sql-json-column.js";
6
+ import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn } from "./sql-json-column.js";
6
7
  import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
7
8
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError } from "./store-contracts.js";
8
9
  import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
9
10
  import { SCHEMA_MISMATCH_ERROR_CODE } from "./schema-consistency.js";
11
+ import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
12
+ import { readJsonObjectColumn } from "./sql-json-column.js";
13
+ import { retireStoredCardWords } from "../retired-card-words.js";
14
+ import { redactSecrets } from "../trace/redact.js";
15
+ const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
16
+ export function buildSessionRuleReceipts() {
17
+ return { rows: [], renamed: false };
18
+ }
19
+ export class OneTimeMigrationInterruptedError extends Error {
20
+ receipts;
21
+ constructor(cause, receipts) {
22
+ super(`${cause instanceof Error ? cause.message : String(cause)} — one-time migration M17 had already committed changes whose receipts the next boot cannot re-derive ` +
23
+ "(the rows below are rewritten, so a retry finds nothing left to remove from them) — record them before retrying: " +
24
+ receipts.sessionRuleRowsWithOldGrammarEntries.map((r) => `${r.policyKey} (session ${r.sessionId}): toolAllowDropped=${JSON.stringify(r.toolAllowDropped)}`).join("; "), { cause });
25
+ this.name = "OneTimeMigrationInterruptedError";
26
+ this.receipts = receipts;
27
+ }
28
+ }
29
+ function interruptedWith(cause, receipts) {
30
+ if (receipts.rows.length === 0)
31
+ return cause;
32
+ return new OneTimeMigrationInterruptedError(cause, {
33
+ ...(receipts.renamed ? { sessionRuleColumn: "renamed" } : {}),
34
+ sessionRuleRowsWithOldGrammarEntries: [...receipts.rows],
35
+ });
36
+ }
10
37
  export const ONE_TIME_MIGRATIONS = [
11
38
  {
12
39
  id: "M4",
@@ -178,6 +205,42 @@ export const ONE_TIME_MIGRATIONS = [
178
205
  "models 域降级、rosters 整份读不出 —— 拒句点名迁移 id;出路是运维手跑契约命令行(契约删族之后只剩手改)",
179
206
  reportKeys: [],
180
207
  },
208
+ {
209
+ id: "M17",
210
+ introducedIn: "7.129.0",
211
+ releasedOn: "2026-10-15",
212
+ removableAfter: "2026-11-14",
213
+ removal: "删 migrateOn 的 M17 步(`migrateSessionRuleGrammar`:`session_policy` 在场探针、旧名 → 进行中的名 → 判读 → 终名三段、退役命令桶扫描与点名)与 " +
214
+ "`retiredSessionRuleBucketsOf` / `RetiredSessionRuleBucket` / `RETIRED_SESSION_RULE_BUCKETS` / `RETIRED_SESSION_RULES_COLUMN` / `OLD_GRAMMAR_SESSION_RULES_COLUMN` / `SESSION_RULES_SCAN_PAGE` / `SessionRuleRowRetiredBuckets` / " +
215
+ "`oldGrammarEntriesOf` / `OldGrammarEntries` / `judgeOldGrammarRows` / `renameSessionRulesColumnSql` / `scanSessionRuleRows` / `SessionRuleScanRow` / " +
216
+ "`RETIRED_BUCKET_PATTERNS` / `SessionRuleRowOldGrammarEntries` / 收据账 `SessionRuleReceipts` / `buildSessionRuleReceipts` / `OneTimeMigrationReceipts` / " +
217
+ "`OneTimeMigrationInterruptedError` / `interruptedWith`(`migrateOn` 外层的那一道 try / catch)/ `logOneTimeMigrationsInterrupted` / `runAndLogOneTimeMigrations` 里的失败日志臂 / `logSessionRuleOldGrammarRows`;" +
218
+ "报告、日志行(含逐行的 `session_rule_row_retired_bucket` 与 `session_rule_row_old_grammar_entries`)与 `NO_SQL_MIGRATIONS` 的 `sessionRuleColumn` / " +
219
+ "`sessionRuleRowsWithRetiredBuckets` / `sessionRuleRowsWithOldGrammarEntries`;" +
220
+ "**列名本身留在中央 DDL 里**(`session-rules-column.ts`,它不是迁移的一部分)",
221
+ afterRemoval: "① 列还叫 `rules`、或停在进行中的名 `rule_entries_old_grammar` 的存量库(跳过了带本条的版本直接升 / 判读没走完就跳版):起服列册探针 `schema-consistency.ts`(S-547)拒启 " +
222
+ "`config.schema_mismatch`(缺 `rule_entries`、多那一列)—— " +
223
+ "出路是先在一只带本条的版本上起服一次;② 行里仍带退役命令桶:SQL 店读回照旧答 `corrupt`(引擎的形读法点名那只桶,与本条在场时同一个结局),只是起服日志不再逐行点名 —— " +
224
+ "运维在库里把那一行改写成引擎文法或删掉它(删会话即删规则);③ 跳过本条直接升的库同样没有改名那一拍的条目判读 —— 由 ① 的拒启接住(出路同 ①)",
225
+ reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries"],
226
+ },
227
+ {
228
+ id: "M18",
229
+ introducedIn: "7.129.0",
230
+ releasedOn: "2026-10-15",
231
+ removableAfter: "2026-11-14",
232
+ removal: "删 migrateOn 的 M18 步(`migrateRetiredCardWords` 的存卡扫描、`migrateRetiredCardWordsInCardJson`、" +
233
+ "`RETIRED_CARD_WORD_PATTERNS` / `RETIRED_CARD_WORD_BATCH`);报告、日志行与 `NO_SQL_MIGRATIONS` 的 `retiredCardWordAskRowsRewritten` / " +
234
+ "`retiredCardWordRowsUnreadable`;**连同读侧半场一起删**:`src/retired-card-words.ts` 整只文件、" +
235
+ "`approval-card.ts` 里 `readStoredCardEnvelope` 的改写那一步与 `currentParkArm`(两读者 `parkRowFacts` / `tool-approval.ts` `parkRowStandingAnswerFacts` " +
236
+ "回到直读传入的那一只)与 `noteRetiredCardWordRead`、fail-open tag `server.approval-card.retired-card-word-read`(`observability/fail-open.ts` 与 " +
237
+ "`docs/FAIL-OPEN-CENSUS.md` 那一行)、`test/retired-card-words-m18.test.ts`(唯一入口那一格改钉到 `approval-card.test.ts`)",
238
+ afterRemoval: "既有坏行臂接住(响亮,不再改写):仍带退役词的存卡(跳过了带本条的版本直接升的库、窗关之后还在写的 7.128.x 副本)整卡读不出 —— 断连重放跳过 + 一次 warn、" +
239
+ "幂等重入走 park、回决口按要人;仍带它们的待决停驻行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署、local 车道引擎 File 店里窗关仍未决的 —— 停驻行从来不由本服务改写," +
240
+ "窗内靠读侧改写读,窗关后读侧改写删)由本服务的读面按坏行处置:运维队列行缺那一位 + `server.approval-card.closed-word-out-of-set` 计数、耐久 `/decide` 的 remember 按强制(只放行这一次)。" +
241
+ "引擎续跑不受影响:续跑入口读行摘要、不按今日闭集重判停驻行上的这两座,行字节引擎怎么写的还是怎么在",
242
+ reportKeys: ["retiredCardWordAskRowsRewritten", "retiredCardWordRowsUnreadable"],
243
+ },
181
244
  ];
182
245
  export function migrationWindowOpen(window, today) {
183
246
  return window.removableAfter === null || today <= window.removableAfter;
@@ -193,6 +256,11 @@ export const NO_SQL_MIGRATIONS = {
193
256
  undurableScopeRuleRows: [],
194
257
  undurableScopeRuleBucketsDeferred: 0,
195
258
  undurableScopeRuleBucketsUnreadable: [],
259
+ sessionRuleColumn: "file-store",
260
+ sessionRuleRowsWithRetiredBuckets: [],
261
+ sessionRuleRowsWithOldGrammarEntries: [],
262
+ retiredCardWordAskRowsRewritten: 0,
263
+ retiredCardWordRowsUnreadable: 0,
196
264
  };
197
265
  const AUDIT_TABLE = "device_audit";
198
266
  const AUDIT_DETAIL_COLUMN = "detail";
@@ -318,6 +386,15 @@ async function withPgLock(pool, run) {
318
386
  }
319
387
  }
320
388
  async function migrateOn(db, dialect) {
389
+ const receipts = buildSessionRuleReceipts();
390
+ try {
391
+ return await migrateStepsOn(db, dialect, receipts);
392
+ }
393
+ catch (e) {
394
+ throw interruptedWith(e, receipts);
395
+ }
396
+ }
397
+ async function migrateStepsOn(db, dialect, receipts) {
321
398
  const auditTablePresent = await tableExists(db, dialect, AUDIT_TABLE);
322
399
  const auditDetailJsonNullRows = !auditTablePresent
323
400
  ? 0
@@ -331,7 +408,9 @@ async function migrateOn(db, dialect) {
331
408
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
332
409
  : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
333
410
  const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
411
+ const { askRows: retiredCardWordAskRowsRewritten, unreadable: retiredCardWordRowsUnreadable } = await migrateRetiredCardWords(db, dialect);
334
412
  const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
413
+ const { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries } = await migrateSessionRuleGrammar(db, dialect, receipts);
335
414
  let undurableScopeRuleRows = [];
336
415
  let undurableScopeRuleBucketsDeferred = 0;
337
416
  let undurableScopeRuleBucketsUnreadable = [];
@@ -342,8 +421,113 @@ async function migrateOn(db, dialect) {
342
421
  auditDetailJsonNullRows,
343
422
  sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
344
423
  workflowParkIndexKey, workflowParkIndexRowsCarried, undurableScopeRuleRows, undurableScopeRuleBucketsDeferred, undurableScopeRuleBucketsUnreadable,
424
+ sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries,
425
+ retiredCardWordAskRowsRewritten, retiredCardWordRowsUnreadable,
345
426
  };
346
427
  }
428
+ const SESSION_POLICY_TABLE = "session_policy";
429
+ const RETIRED_SESSION_RULES_COLUMN = "rules";
430
+ const OLD_GRAMMAR_SESSION_RULES_COLUMN = "rule_entries_old_grammar";
431
+ const SESSION_RULES_SCAN_PAGE = 500;
432
+ export function retiredSessionRuleBucketsOf(body) {
433
+ if (body === null || typeof body !== "object" || Array.isArray(body))
434
+ return [];
435
+ return RETIRED_SESSION_RULE_BUCKETS.filter((b) => Object.hasOwn(body, b));
436
+ }
437
+ export function oldGrammarEntriesOf(body, rev) {
438
+ if (body === null || typeof body !== "object" || Array.isArray(body))
439
+ return undefined;
440
+ const retired = retiredSessionRuleBucketsOf(body);
441
+ const buckets = Object.fromEntries(Object.entries(body).filter(([k]) => !retired.includes(k)));
442
+ const shaped = readSessionRulesShape({ ...buckets, rev });
443
+ if ("fault" in shaped)
444
+ return undefined;
445
+ const compiled = compileSessionRuleBucket(shaped.rules, "toolAllow");
446
+ if (body.grammar === undefined && compiled.inert.some((i) => i.reason === "wildcard_all"))
447
+ return { toolAllowDropped: [], wildcardMasked: true };
448
+ const earlier = new Set(compiled.inert.filter((i) => i.reason === "earlier_grammar").map((i) => i.entry));
449
+ const toolAllow = shaped.rules.toolAllow?.filter((e) => !earlier.has(e));
450
+ const toolAllowDropped = (shaped.rules.toolAllow ?? []).filter((e) => earlier.has(e));
451
+ if (toolAllowDropped.length === 0 && body.grammar !== undefined)
452
+ return { toolAllowDropped };
453
+ return { toolAllowDropped, rewritten: { ...body, ...(toolAllow !== undefined ? { toolAllow } : {}), grammar: SESSION_RULES_GRAMMAR } };
454
+ }
455
+ const RETIRED_BUCKET_PATTERNS = ['%"commandDeny"%', '%"commandAllow"%'];
456
+ async function scanSessionRuleRows(db, dialect, column, patterns, visit) {
457
+ const text = jsonColumnTextExpr(dialect, column);
458
+ const prefilter = patterns.length === 0 ? "" : ` AND (${patterns.map((_, i) => `${text} LIKE ${dialect === "tidb" ? "?" : `$${i + 2}`}`).join(" OR ")})`;
459
+ const scan = dialect === "tidb"
460
+ ? `SELECT policy_key, session_id, rev, ${jsonColumnText("tidb", column)} FROM ${SESSION_POLICY_TABLE} WHERE policy_key > ?${prefilter} ORDER BY policy_key LIMIT ${SESSION_RULES_SCAN_PAGE}`
461
+ : `SELECT policy_key, session_id, rev, ${jsonColumnText("pg", column)} FROM ${SESSION_POLICY_TABLE} WHERE policy_key > $1${prefilter} ORDER BY policy_key LIMIT ${SESSION_RULES_SCAN_PAGE}`;
462
+ let cursor = "";
463
+ for (;;) {
464
+ const page = await db.rows(scan, [cursor, ...patterns]);
465
+ for (const row of page) {
466
+ let body;
467
+ try {
468
+ body = decodeJsonColumn(row[column], readOpaqueJsonValueColumn, "session_policy.rule_entries for the M17 scans (read under its in-progress name while the old-grammar judge runs)");
469
+ }
470
+ catch (e) {
471
+ if (!isJsonColumnBodyProblem(e))
472
+ throw e;
473
+ continue;
474
+ }
475
+ await visit({ policyKey: String(row.policy_key), sessionId: String(row.session_id), rev: Number(row.rev), text: String(row[column]), body });
476
+ }
477
+ if (page.length < SESSION_RULES_SCAN_PAGE)
478
+ return;
479
+ cursor = String(page[page.length - 1].policy_key);
480
+ }
481
+ }
482
+ async function judgeOldGrammarRows(db, dialect, receipts) {
483
+ const column = OLD_GRAMMAR_SESSION_RULES_COLUMN;
484
+ const text = jsonColumnTextExpr(dialect, column);
485
+ const cas = dialect === "tidb"
486
+ ? `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = ? WHERE policy_key = ? AND CAST(${text} AS BINARY) = CAST(? AS BINARY)`
487
+ : `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
488
+ await scanSessionRuleRows(db, dialect, column, [], async (row) => {
489
+ const read = oldGrammarEntriesOf(row.body, row.rev);
490
+ if (read?.wildcardMasked) {
491
+ receipts.rows.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: [], wildcardMasked: true });
492
+ return;
493
+ }
494
+ if (read?.rewritten === undefined)
495
+ return;
496
+ if ((await db.exec(cas, [JSON.stringify(read.rewritten), row.policyKey, row.text])) !== 1) {
497
+ throw new Error(`one-time migration M17: session_policy row ${row.policyKey} changed while its rule entries were being rewritten under the in-progress column ${column} — no writer can reach the ` +
498
+ "table at that moment (builds up to 7.128.x lost the column, this build's replicas wait on the boot lock), so the step refuses rather than guess");
499
+ }
500
+ if (read.toolAllowDropped.length > 0)
501
+ receipts.rows.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: read.toolAllowDropped.map((e) => redactSecrets(e)) });
502
+ });
503
+ }
504
+ export async function migrateSessionRuleGrammar(db, dialect, receipts) {
505
+ if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE)))
506
+ return { sessionRuleColumn: "no-table", sessionRuleRowsWithRetiredBuckets: [], sessionRuleRowsWithOldGrammarEntries: [] };
507
+ const col = SESSION_RULES_COLUMN;
508
+ const has = (column) => columnExists(db, dialect, SESSION_POLICY_TABLE, column);
509
+ if (await has(RETIRED_SESSION_RULES_COLUMN)) {
510
+ await execUntil(db, renameSessionRulesColumnSql(dialect, RETIRED_SESSION_RULES_COLUMN, OLD_GRAMMAR_SESSION_RULES_COLUMN), async () => !(await has(RETIRED_SESSION_RULES_COLUMN)));
511
+ }
512
+ let sessionRuleColumn = "current";
513
+ if (await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) {
514
+ await judgeOldGrammarRows(db, dialect, receipts);
515
+ await execUntil(db, renameSessionRulesColumnSql(dialect, OLD_GRAMMAR_SESSION_RULES_COLUMN, col.name), async () => !(await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) && (await has(col.name)));
516
+ receipts.renamed = true;
517
+ sessionRuleColumn = "renamed";
518
+ }
519
+ const named = [];
520
+ await scanSessionRuleRows(db, dialect, col.name, RETIRED_BUCKET_PATTERNS, async (row) => {
521
+ const buckets = retiredSessionRuleBucketsOf(row.body);
522
+ if (buckets.length > 0)
523
+ named.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
524
+ });
525
+ return { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets: named, sessionRuleRowsWithOldGrammarEntries: receipts.rows };
526
+ }
527
+ function renameSessionRulesColumnSql(dialect, from, to) {
528
+ const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
529
+ return dialect === "tidb" ? `ALTER TABLE ${SESSION_POLICY_TABLE} CHANGE COLUMN ${from} ${to}${definition}` : `ALTER TABLE ${SESSION_POLICY_TABLE} RENAME COLUMN ${from} TO ${to}`;
530
+ }
347
531
  const WORKFLOW_PARK_INDEX_TABLE = "workflow_agent_session";
348
532
  async function primaryKeyWidth(db, dialect, table) {
349
533
  const sql = dialect === "tidb"
@@ -472,6 +656,57 @@ async function migrateShadowedRuleKey(db, dialect) {
472
656
  return { migrated, unreadable };
473
657
  }
474
658
  }
659
+ const RETIRED_CARD_WORD_PATTERNS = ['%"shadowed"%', '%"governance_unavailable"%'];
660
+ const RETIRED_CARD_WORD_BATCH = 500;
661
+ function decodeForM18(text, where) {
662
+ try {
663
+ return { ok: true, value: decodeJsonColumn(text, readJsonObjectColumn, where) };
664
+ }
665
+ catch (error) {
666
+ if (isJsonColumnBodyProblem(error))
667
+ return { ok: false, error };
668
+ throw error;
669
+ }
670
+ }
671
+ export function migrateRetiredCardWordsInCardJson(text) {
672
+ const decoded = decodeForM18(text, "approval_ask.card_json (M18 rewrite)");
673
+ if (!decoded.ok)
674
+ return { kind: "unreadable", error: decoded.error };
675
+ const next = retireStoredCardWords(decoded.value);
676
+ return next.seats.length === 0 ? { kind: "untouched" } : { kind: "rewrite", text: JSON.stringify(next.value), value: next.value };
677
+ }
678
+ async function migrateRetiredCardWords(db, dialect) {
679
+ let askRows = 0;
680
+ let unreadable = 0;
681
+ if (!(await tableExists(db, dialect, "approval_ask")))
682
+ return { askRows, unreadable };
683
+ const cardText = jsonColumnTextExpr(dialect, "card_json");
684
+ let after = "";
685
+ for (;;) {
686
+ const rows = await db.rows(dialect === "tidb"
687
+ ? `SELECT ask_id, ${jsonColumnText(dialect, "card_json")} FROM approval_ask WHERE (${cardText} LIKE ? OR ${cardText} LIKE ?) AND ask_id > ? ORDER BY ask_id LIMIT ${RETIRED_CARD_WORD_BATCH}`
688
+ : `SELECT ask_id, ${jsonColumnText(dialect, "card_json")} FROM approval_ask WHERE (${cardText} LIKE $1 OR ${cardText} LIKE $2) AND ask_id > $3 ORDER BY ask_id LIMIT ${RETIRED_CARD_WORD_BATCH}`, [...RETIRED_CARD_WORD_PATTERNS, after]);
689
+ for (const r of rows) {
690
+ const askId = String(r.ask_id);
691
+ after = askId;
692
+ const text = r.card_json;
693
+ if (typeof text !== "string")
694
+ continue;
695
+ const next = migrateRetiredCardWordsInCardJson(text);
696
+ if (next.kind === "unreadable") {
697
+ unreadable += 1;
698
+ continue;
699
+ }
700
+ if (next.kind === "untouched")
701
+ continue;
702
+ askRows += await db.exec(dialect === "tidb"
703
+ ? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
704
+ : `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
705
+ }
706
+ if (rows.length < RETIRED_CARD_WORD_BATCH)
707
+ return { askRows, unreadable };
708
+ }
709
+ }
475
710
  const INCARNATION_BACKFILL_BATCH = 500;
476
711
  async function migrateSessionIncarnations(db, dialect) {
477
712
  if (!(await tableExists(db, dialect, "session_meta")))
@@ -530,7 +765,9 @@ export function logOneTimeMigrations(log, backend, report) {
530
765
  const changed = report.auditDetailJsonNullRows > 0 ||
531
766
  report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
532
767
  report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
533
- report.undurableScopeRuleBucketsUnreadable.length > 0;
768
+ report.undurableScopeRuleBucketsUnreadable.length > 0 ||
769
+ report.sessionRuleColumn === "renamed" || report.sessionRuleRowsWithRetiredBuckets.length > 0 || report.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
770
+ report.retiredCardWordAskRowsRewritten > 0 || report.retiredCardWordRowsUnreadable > 0;
534
771
  (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
535
772
  backend,
536
773
  auditDetailJsonNullRows: report.auditDetailJsonNullRows,
@@ -544,6 +781,11 @@ export function logOneTimeMigrations(log, backend, report) {
544
781
  undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
545
782
  undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
546
783
  undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
784
+ sessionRuleColumn: report.sessionRuleColumn,
785
+ sessionRuleRowsWithRetiredBuckets: report.sessionRuleRowsWithRetiredBuckets.length,
786
+ sessionRuleRowsWithOldGrammarEntries: report.sessionRuleRowsWithOldGrammarEntries.length,
787
+ retiredCardWordAskRowsRewritten: report.retiredCardWordAskRowsRewritten,
788
+ retiredCardWordRowsUnreadable: report.retiredCardWordRowsUnreadable,
547
789
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
548
790
  note: "one-time persisted-data migrations (M4: audit details written as the JSON " +
549
791
  "text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
@@ -556,7 +798,16 @@ export function logOneTimeMigrations(log, backend, report) {
556
798
  "on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim; " +
557
799
  "M11: persisted permission rules whose project scope root is not a complete absolute path — accepted by earlier versions, dropped on every read " +
558
800
  "by the current engine and not removable through DELETE /v1/rules — removed, one permission_rule_row_removed line per row; a bucket the read path " +
559
- "refuses in whole is left untouched, one permission_rule_bucket_unreadable line per bucket). Each step " +
801
+ "refuses in whole is left untouched, one permission_rule_bucket_unreadable line per bucket; M17: the session-rules column renamed from `rules` to " +
802
+ "`rule_entries`, so a build that reads rules under the earlier name-only grammar finds no column to misread; at that rename every row's rule entries " +
803
+ "are read by the engine in the grammar the row carries (none = the grammar before the stamp) and rewritten as the same statement in the current grammar " +
804
+ "(the column carries the in-progress name rule_entries_old_grammar until every row is rewritten, so an interrupted boot resumes there) — toolAllow entries " +
805
+ "that the earlier grammar read as naming no tool are removed from the row, every row is stamped with the current grammar, one session_rule_row_old_grammar_entries " +
806
+ "line per row that lost entries; " +
807
+ "rows still holding a retired command bucket keep that bucket and are named, one session_rule_row_retired_bucket line per row on every boot; " +
808
+ "M18: stored approval cards written by an earlier engine with two words the current engine retired rewritten — ruleOffersAbsence shadowed removed, ask class " +
809
+ "governance_unavailable read as rule_unreadable; the engine's own parked-row documents are never rewritten (their row digest covers the ask class), they and " +
810
+ "the cards this step cannot reach are read the same way, counted under server.approval-card.retired-card-word-read). Each step " +
560
811
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
561
812
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
562
813
  "version that still carries a step at least once before upgrading past its deletion.",
@@ -582,6 +833,67 @@ export function logOneTimeMigrations(log, backend, report) {
582
833
  "keeps being refused, as before this release, until someone repairs or deletes that row by hand",
583
834
  });
584
835
  }
836
+ logSessionRuleOldGrammarRows(log, backend, report.sessionRuleRowsWithOldGrammarEntries);
837
+ for (const row of report.sessionRuleRowsWithRetiredBuckets) {
838
+ log.warn("session_rule_row_retired_bucket", {
839
+ backend,
840
+ policyKey: row.policyKey,
841
+ sessionId: row.sessionId,
842
+ buckets: row.buckets,
843
+ note: "a session's stored rules hold a retired command bucket (commandAllow / commandDeny), which the current rule grammar does not read: the bucket is left " +
844
+ "as written and every read of the row is refused as corrupt (the session's rules face answers 500 and its new tasks do not start). No rewrite is made for " +
845
+ "you: the retired bucket also refused every shell command line it could not read as one command name (a path such as /bin/rm, pipelines, &&), which no " +
846
+ "rule entry refuses. Rewrite the row in the database in the current grammar (toolDeny entries such as Bash(rm:*), or Bash to refuse the shell outright; " +
847
+ "keep rev and gen) or delete the session (deleting the session deletes its rules)",
848
+ });
849
+ }
850
+ }
851
+ function logSessionRuleOldGrammarRows(log, backend, rows) {
852
+ for (const row of rows) {
853
+ log.warn("session_rule_row_old_grammar_entries", {
854
+ backend,
855
+ policyKey: row.policyKey,
856
+ sessionId: row.sessionId,
857
+ toolAllowDropped: row.toolAllowDropped,
858
+ ...(row.wildcardMasked ? { wildcardMasked: true } : {}),
859
+ note: row.wildcardMasked
860
+ ? "a session's stored rules were written before the rule-grammar stamp and its toolAllow list holds `*`: the engine reads that list as void in every grammar (every call " +
861
+ "refused), which hides what the other entries meant in the earlier grammar, so this row was NOT rewritten or stamped — it stays in its earlier grammar. To restore the other " +
862
+ "entries, an operator rewrites the row without `*` (entries spelled as rules, `Bash(rm:*)`, then become allow rules — review them first)."
863
+ :
864
+ "a session's stored rules were written before the rule-grammar stamp, when an entry was compared as a tool name as written; the engine reads such a row " +
865
+ "in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
866
+ "The row was rewritten as the same statement in the current grammar: these entries were removed and the row stamped (an emptied list stays in place and allows " +
867
+ "nothing); toolDeny entries read the same in every grammar and were left as written. Every entry named on this line is named exactly once, by the boot that " +
868
+ "removed it — no later boot lists it again — so keep the line. Write the session's rules afresh through the session policy endpoint if an entry was meant as an allow rule",
869
+ });
870
+ }
871
+ }
872
+ function logOneTimeMigrationsInterrupted(log, backend, error) {
873
+ const { receipts } = error;
874
+ log.warn("one_time_migrations_interrupted", {
875
+ backend,
876
+ sessionRuleColumn: receipts.sessionRuleColumn ?? "in-progress",
877
+ sessionRuleRowsWithOldGrammarEntries: receipts.sessionRuleRowsWithOldGrammarEntries.length,
878
+ note: "the one-time migrations failed after committing changes whose receipts the next boot cannot re-derive: the session_rule_row_old_grammar_entries lines that follow name " +
879
+ "rows this boot already rewrote — a retried boot finds them rewritten and removes nothing from them. These lines are the only place the rewritten rows are ever " +
880
+ "reported — no later boot lists them — so save them. The failure itself follows as this boot's refusal. sessionRuleColumn \"renamed\": every row was rewritten and the final " +
881
+ "column name was published before the failure; \"in-progress\": the column still carries its in-progress name — the next boot rewrites the remaining rows (rows already " +
882
+ "rewritten are left alone) and publishes the final name",
883
+ });
884
+ logSessionRuleOldGrammarRows(log, backend, receipts.sessionRuleRowsWithOldGrammarEntries);
885
+ }
886
+ export async function runAndLogOneTimeMigrations(log, backend) {
887
+ let report;
888
+ try {
889
+ report = await backend.runOneTimeMigrations();
890
+ }
891
+ catch (e) {
892
+ if (e instanceof OneTimeMigrationInterruptedError)
893
+ logOneTimeMigrationsInterrupted(log, backend.kind, e);
894
+ throw e;
895
+ }
896
+ logOneTimeMigrations(log, backend.kind, report);
585
897
  }
586
898
  function removedRuleRowNote(kind) {
587
899
  switch (kind) {
@@ -17,6 +17,7 @@ import { ensurePgPermissionRuleSchema } from "./permission-rule-store-sql.js";
17
17
  import { ensurePgLeaderRunSchema } from "./leader-run-store-sql.js";
18
18
  import { ensurePgRetentionSchema } from "./retention-store-sql.js";
19
19
  import { ensureIndexes } from "./ensure-index.js";
20
+ import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
20
21
  import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
21
22
  export const PG_SCHEMA_STATEMENTS = [
22
23
  `CREATE TABLE IF NOT EXISTS task_run (
@@ -176,7 +177,8 @@ export const PG_SCHEMA_STATEMENTS = [
176
177
  -- sha256, so the principal is not recoverable from it. Enumeration-only — it is NOT part of the key and
177
178
  -- NOT part of the CAS / tighten-only path. NULL = a session-wide (principal-less) policy row.
178
179
  principal VARCHAR(190) COLLATE "C",
179
- rules JSONB NOT NULL,
180
+ -- 会话规则(TiDB 孪生同注:列定义唯一属主 = session-rules-column.ts 的 SESSION_RULES_COLUMN;7.128.x 及以前叫 rules,存量库由 M17 改名)。
181
+ ${SESSION_RULES_COLUMN.pg},
180
182
  rev BIGINT NOT NULL,
181
183
  -- gen (core 7.25.0 StoredSessionRules.gen, S-529 二期):TiDB 孪生同注 —— 这一行的血统,DEFAULT 0 = 第 0 代。
182
184
  gen BIGINT NOT NULL DEFAULT 0,
@@ -1,4 +1,4 @@
1
- import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
1
+ import { FileError, type InheritEnvPolicy, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
2
2
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
3
3
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
4
4
  /** SIGTERM → grace → SIGKILL (no host isolation). 🔴 宽限的**唯一属主**:本 driver 的 `kill` 梯子用它排升级定时器;会话级收敛
@@ -35,18 +35,17 @@ export interface HostEnvConfig {
35
35
  homeDir?: string;
36
36
  /** Per-command wall-clock bound (ms) when the caller gives none. Default {@link DEFAULT_COMMAND_TIMEOUT_MS}. */
37
37
  commandTimeoutMs?: number;
38
- /** Shell-child env policy (D1; mirrors core's `NodeExecutionEnv`). Default `"scrub"`: inherit the worker's
39
- * `process.env` but DROP secret-pattern keys (core's `scrubSecretEnv` — `*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD`…)
40
- * AND every env name the server's config catalog types as `secret` (`server-secret-env.ts`; 7.99.0 F7 —
41
- * `SERVICE_AUTH_TOKENS` / `APPROVAL_HMAC_KEYS` slip the shape rule)
42
- * so a model-driven shell can't `printenv DEEPSEEK_API_KEY` and exfiltrate orchestrator secrets — keeps PATH/HOME/
43
- * locale/build (low breakage). `"all"` = inherit everything (explicit opt-out, trusted local dev). A `string[]` =
44
- * strict (hermetic) allowlist via `allowlistedEnv` — the repo's one "named keys pass verbatim" predicate, shared with
45
- * the local language server's `LSP_ENV_ALLOW` (S-699; that leg puts named keys back ON TOP of the scrub instead).
46
- * `env`/per-command `options.env` ALWAYS pass through verbatim. */
47
- inheritEnv?: "scrub" | "all" | string[];
48
- /** @deprecated use {@link inheritEnv}. `true`→`"all"`, `false`→`[]` (hermetic). Ignored if `inheritEnv` is set. */
49
- inheritProcessEnv?: boolean;
38
+ /** Shell-child env policy — core's `InheritEnvPolicy`, read by core's `inheritedHostEnv` (the three words are judged
39
+ * in ONE place, the engine; this adapter never reads them). Default `"scrub"`: inherit the worker's `process.env`
40
+ * minus credential-shaped names (core's shape rules) minus this deployment's OWN credentials (every env name the
41
+ * server's config catalog types as `secret` + the name-list knobs + the effective model entries' `apiKeyEnv` — declared at boot and
42
+ * re-declared before every config generation lands, names only ever added, `declareServerWithheldEnv`),
43
+ * so a model-driven shell can't `printenv DEEPSEEK_API_KEY` — keeps PATH/HOME/locale/build (low breakage).
44
+ * `"all"` = the whole environment MINUS the declared own credentials (the user's own `GH_TOKEN` flows, the engine's
45
+ * `SERVICE_AUTH_TOKEN` never does — 7.129.0 / core 7.51.1). A `string[]` = strict (hermetic) allowlist; naming a declared
46
+ * own credential is REFUSED at construction (`inheritEnvPolicyProblem`) and again at every spawn. `env`/per-command
47
+ * `options.env` are the adapter's explicit grant and ALWAYS pass through verbatim. */
48
+ inheritEnv?: InheritEnvPolicy;
50
49
  /**
51
50
  * EXACT persistent working directory (TOC, CC-like): when set, the adapter uses THIS dir verbatim — NO
52
51
  * `sema-host-<id>-<rand>` per-task subdir — and **NEVER `rm -rf`s it on {@link destroy}**. It is the
@@ -419,21 +418,19 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
419
418
  * 同步 `existsSync` 而不是 await:这是拆除路径上的判据,不该再引入一次异步让出。 */
420
419
  private isLingeringMirrorTarget;
421
420
  private fsReady;
422
- /** Base env (process.env when inheriting) merged with the adapter's secret env, then per-command overrides win. */
423
- /** D1 secure default = `"scrub"`: inherit `process.env` minus secret-pattern keys (core's `scrubSecretEnv`, single
424
- * source of the shape rule) minus the config catalog's `secret`-typed names (`scrubServerSecrets`, 7.99.0 F7). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
425
- * maps `true`→`"all"` / `false`→`[]`. */
426
- private inheritMode;
427
421
  /** 每次 spawn 交给子进程的 env(组合 + 剥除披露 + win32 的 SHELL 钉)。`sh` = 调用点刚从 {@link hostShell} 读到的那只(spawn 路径
428
422
  * 都在 `shellReady()` 之后):没有已落定的 shell 就到不了这条腿 —— 类型即次序守卫。 */
429
423
  private mergeEnv;
430
424
  /**
431
425
  * 子进程 env 的**唯一组合**,分两层(一条律:**env 组合是纯函数,shell 只属于 spawn**):
432
- * · **组合层**(恒有)—— 继承姿态 + `cfg.env` + perCommand + win32 大小写折叠。**不碰 shell**,所以读面(如 {@link cdPath})任何时候
426
+ * · **组合层**(恒有)—— 继承面 + `cfg.env` + perCommand + win32 大小写折叠。继承面 = 本仓唯一组合(`server-secret-env.ts`)交引擎
427
+ * `inheritedHostEnv(policy)`:三个继承词(`"all"` / `"scrub"` / `[names]`)只在引擎一处判,本进程自有凭据名(起服声明)任何词都不交 ——
428
+ * 本类不再有三臂字面(7.129.0 / core 7.51.1;修前 `all` 是 `{ ...process.env }` 零剥除)。**不碰 shell**,所以读面(如 {@link cdPath})任何时候
433
429
  * 都能读,不依赖 shell 是否已落定。
434
- * · **spawn 层**(`spawnShell` 在场)—— 这一次是真要交给子进程:scrub 腿经 `hostSubprocessEnv` 记剥除(`spawnShell` 缺席 ⇒ 只读组合,
430
+ * · **spawn 层**(`spawnShell` 在场)—— 这一次是真要交给子进程:经 `hostSubprocessEnv` 记剥除(`spawnShell` 缺席 ⇒ 只读组合 `composeHostSubprocessEnv`,
435
431
  * 不计数:一次没发生的 spawn 不进剥除账),win32 上把 `SHELL` 钉到已解析的 Git Bash({@link pinHostShell},在大小写折叠**之前**)。
436
432
  * 披露与钉同一个开关(`spawnShell`)—— 「真 spawn」只有一种说法,不再有 `disclose` 与钉各自一个布尔。
433
+ * 引擎对点到自有凭据名的白名单在这里**抛**(spawn 时的拒;构造器已先问过同一只判)—— 子进程不起,拒句带名。
437
434
  */
438
435
  private childEnv;
439
436
  private fileError;
@@ -7,10 +7,10 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
9
9
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
- import { resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
10
+ import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
11
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
- import { FileError, NodeExecutionEnv, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
- import { allowlistedEnv, composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
12
+ import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
+ import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
14
14
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
15
15
  const PROVIDER = "host";
16
16
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
@@ -122,10 +122,18 @@ export class RemoteHostExecutionEnv {
122
122
  this.workspaceDir = path.resolve(base, `sema-host-${sanitizeId(id)}-${suffix}`);
123
123
  this.persistent = false;
124
124
  }
125
+ const inheritEnvProblem = inheritEnvPolicyProblem(config.inheritEnv);
126
+ if (inheritEnvProblem !== undefined)
127
+ throw new Error(`RemoteHostExecutionEnv: ${inheritEnvProblem}`);
128
+ if ("inheritProcessEnv" in config) {
129
+ throw new Error('RemoteHostExecutionEnv: inheritProcessEnv was retired in 7.129.0 and is refused rather than read — write inheritEnv: "all" (for true) or inheritEnv: [] (for false)');
130
+ }
125
131
  this.cfg = {
126
132
  ...config,
127
133
  commandTimeoutMs: config.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
128
134
  };
135
+ if (Array.isArray(config.inheritEnv))
136
+ this.cfg.inheritEnv = [...config.inheritEnv];
129
137
  this.cwd = this.workspaceDir;
130
138
  this.homeDir = config.homeDir;
131
139
  this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
@@ -395,7 +403,7 @@ export class RemoteHostExecutionEnv {
395
403
  const cwd = this.resolve(options?.cwd ?? this.cwd);
396
404
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
397
405
  const detachSignal = options?.detachSignal;
398
- if (process.platform !== "win32" && hostExecSpoolEnabled()) {
406
+ if (!IS_WIN32 && hostExecSpoolEnabled()) {
399
407
  const spool = this.openFgSpool();
400
408
  if (spool)
401
409
  return this.execViaSpool(command, options, cwd, timeoutMs, detachSignal, spool);
@@ -1114,7 +1122,7 @@ export class RemoteHostExecutionEnv {
1114
1122
  break;
1115
1123
  seen.add(next);
1116
1124
  hops.push(next);
1117
- if (process.platform === "win32" && UNC_SPELLING.test(next))
1125
+ if (IS_WIN32 && UNC_SPELLING.test(next))
1118
1126
  break;
1119
1127
  cur = next;
1120
1128
  }
@@ -1310,33 +1318,13 @@ export class RemoteHostExecutionEnv {
1310
1318
  return { ok: false, error: new FileError("unknown", `failed to create host workspace: ${errMsg(e)}`, this.resolve(p), asError(e)) };
1311
1319
  }
1312
1320
  }
1313
- inheritMode() {
1314
- if (this.cfg.inheritEnv !== undefined)
1315
- return this.cfg.inheritEnv;
1316
- if (this.cfg.inheritProcessEnv === false)
1317
- return [];
1318
- if (this.cfg.inheritProcessEnv === true)
1319
- return "all";
1320
- return "scrub";
1321
- }
1322
1321
  mergeEnv(perCommand, sh) {
1323
1322
  return this.childEnv(perCommand, sh);
1324
1323
  }
1325
1324
  childEnv(perCommand, spawnShell) {
1326
- const disclose = spawnShell !== undefined;
1327
- const mode = this.inheritMode();
1328
- let base;
1329
- if (mode === "all")
1330
- base = { ...process.env };
1331
- else if (Array.isArray(mode)) {
1332
- base = allowlistedEnv(mode, process.env);
1333
- }
1334
- else if (!disclose) {
1335
- base = composeHostSubprocessEnv([]);
1336
- }
1337
- else {
1338
- base = hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", allow: [] });
1339
- }
1325
+ const base = spawnShell !== undefined
1326
+ ? hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", policy: this.cfg.inheritEnv, allow: [] })
1327
+ : composeHostSubprocessEnv({ policy: this.cfg.inheritEnv, allow: [] });
1340
1328
  if (this.cfg.env)
1341
1329
  Object.assign(base, this.cfg.env);
1342
1330
  if (perCommand)
@@ -10,12 +10,24 @@ import type { Logger } from "../observability/logger.js";
10
10
  * 逐行重算这把键(身份在 hash 的**原像**里,等值 UPDATE 够不着)。两处各写一遍 sha256 表达式的下场是
11
11
  * 某天有人在这里改了 key 公式而收编腿还在按旧式算 —— 迁完的行从此谁也读不到,且**静默**。 */
12
12
  export declare const policyKeyFor: (sessionId: string, principal: string | undefined) => string;
13
+ /** 一行规则体交出的形:引擎形读法读出的桶 + 体里**原样**的文法戳(`StoredSessionRules.grammar`;无戳体 ⇒ 缺席)。 */
14
+ export type StoredSessionRulesBody = SessionPermissionRules & Pick<StoredSessionRules, "grammar">;
13
15
  /**
14
- * 一行的 **`rules` 体**(已按文本取出的那一串)→ 收窄好的 `SessionPermissionRules`(C1)。读不出形 ⇒
16
+ * 一行的 **规则体**(已按文本取出的那一串)→ 收窄好的 {@link StoredSessionRulesBody}(C1)。读不出形 ⇒
15
17
  * `SessionPolicyError("corrupt")`,与 rev / gen 两道拒同口径、同一个码。
16
18
  *
17
- * 形判据复用**现成的那一只**读者 `readSessionPermissionRules`(五桶 `string[]` 的单一属主;未知键照旧忽略,
18
- * 与 core 的 `normalizeRules` 同口径),不在这里铸第二份键集。
19
+ * 形判据 = 引擎的**那一只**形读法 `readSessionRulesShape`(core 7.51.0:写口判官、收紧比对的 prior、运行期规则层读的都是它;
20
+ * 不是桶的键 —— 含退役的 `commandAllow` / `commandDeny` —— 不丢、判坏;句子点名那个键与类型,不带值;core 7.51.2 起坏戳 / 比引擎新的
21
+ * `grammar` 戳同样由它拒)。本仓不另持一只读者:修前本仓自己那一只形读者与它对纯空白的 `allowDirs` 项给两个答案(本仓读得出、引擎判坏 ——
22
+ * 读出来交给运行期,运行期又把整份规则判成读不出、每个调用都拒),同一问两只读者。
23
+ *
24
+ * 🔴 **戳原样交出,不补、不改**(core 7.51.2 #1761,行自带规则文法):一行条目的意思由行自带的 `grammar` 戳说,文法只有引擎一处读 ——
25
+ * 戳在写口判官的返回值里盖(`judgeSessionRulesWrite`,本店整只存),引擎读行时按戳读(`readSessionRulesShape`:戳缺席 ⇒ 与 `rev` 并排的值是戳之前
26
+ * 写的行,文法 0;不与 `rev` 并排的值是一次新写,文法 1)。列体里没有 `rev`(计数是另两列),所以形读法对一只**无戳的列体**解出的文法是「新写」的 1 ——
27
+ * 不是这一行的。本函数因此只取形读法读出的**桶**,戳按体里有无原样带出:带戳 ⇒ 戳(与形读法读出的同一个数);无戳 ⇒ 缺席,引擎拿到回装点交出的行
28
+ * (桶 + 戳 + `rev` / `gen`)读成文法 0,`toolAllow` 里拼写不是它所指名字的条目什么都不放行并通告一次(引擎自己的通告,本店不自铸)。
29
+ * 本店零处读 / 比较戳的数值:判「有没有戳」是形读法自己判戳之前的同一问(`grammar === undefined`),不是第二种文法读法。
30
+ * 修前(7.51.2 刚装上):把形读法的整份读数交回 ⇒ 无戳的存量行被交成文法 1 —— 升级那一刻 `Bash(rm:*)` 由什么都不放行变成放行 rm。
19
31
  *
20
32
  * 🔴 **恰好解一次码,而这是由读腿的投影保证的**(codex 对抗复审两轮,两条读法各自实测被推翻后的终形):
21
33
  * · 「体是什么就直接判形」——**错**:MariaDB(本仓 README 声明支持的 MySQL 协议后端之一)的 `JSON` 是
@@ -32,7 +44,7 @@ export declare const policyKeyFor: (sessionId: string, principal: string | undef
32
44
  * 🔴 S-550:上面这一整段现在是**整仓**的口径,实现搬进唯一属主 `sql-json-column.ts`(投影 `jsonColumnText`
33
45
  * + 恰好解一次 `decodeJsonColumn` + 折码 `foldJsonColumn`);本店只留「形判据是哪一只」与「坏体折成哪个码」。
34
46
  */
35
- export declare function readSessionRulesBody(body: unknown): JsonColumnShape<SessionPermissionRules>;
47
+ export declare function readSessionRulesBody(body: unknown): JsonColumnShape<StoredSessionRulesBody>;
36
48
  /** 本店唯一的日志口:抹除封键那一臂(S-891)的点名。缺席 ⇒ 退回 `console.warn`(不静默)。 */
37
49
  export type SessionPolicyStoreLogger = Pick<Logger, "warn">;
38
50
  /** Dual-dialect durable SessionPolicyStore. See the file header for the dialect-delta ledger. */