@sema-agent/server 7.118.0 → 7.120.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 (139) hide show
  1. package/MIGRATION.md +139 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +26 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +6 -6
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/execution-env.js +3 -2
  35. package/dist/boot/fatal-exit.d.ts +7 -1
  36. package/dist/boot/fatal-exit.js +2 -1
  37. package/dist/boot/leader.d.ts +4 -4
  38. package/dist/boot/leader.js +1 -2
  39. package/dist/boot/parked-revive-gate.d.ts +5 -1
  40. package/dist/boot/parked-revive-gate.js +8 -2
  41. package/dist/boot/resolve-spec.js +7 -4
  42. package/dist/boot/runner-deps.d.ts +4 -3
  43. package/dist/boot/runner-deps.js +7 -3
  44. package/dist/boot/stage-01-config.js +2 -0
  45. package/dist/boot/stage-07-capability-layer.js +5 -1
  46. package/dist/boot/stage-10-http-server.js +3 -2
  47. package/dist/boot/stores.js +12 -7
  48. package/dist/capabilities/builtin-tools.d.ts +31 -8
  49. package/dist/capabilities/builtin-tools.js +22 -1
  50. package/dist/capabilities/center-plugins.d.ts +2 -0
  51. package/dist/capabilities/center-plugins.js +24 -6
  52. package/dist/capabilities/code-review-council.js +8 -11
  53. package/dist/capabilities/scenarios.d.ts +42 -13
  54. package/dist/capabilities/scenarios.js +36 -9
  55. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  56. package/dist/capabilities/send-user-file-tool.js +1 -1
  57. package/dist/config-catalog.js +5 -5
  58. package/dist/config-lkg.d.ts +33 -5
  59. package/dist/config-lkg.js +55 -11
  60. package/dist/config-types.d.ts +8 -1
  61. package/dist/config.js +11 -11
  62. package/dist/deployment-governance.d.ts +8 -3
  63. package/dist/deployment-governance.js +7 -4
  64. package/dist/device-store.d.ts +7 -0
  65. package/dist/device-store.js +4 -0
  66. package/dist/device-ws-hub.js +17 -9
  67. package/dist/device-ws-protocol.d.ts +15 -0
  68. package/dist/host-decision.d.ts +52 -27
  69. package/dist/host-decision.js +14 -7
  70. package/dist/http/admission.js +6 -6
  71. package/dist/http/resume-legs.js +6 -1
  72. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  73. package/dist/http/routes/approvals-assistant.js +9 -5
  74. package/dist/http/routes/capabilities.js +5 -3
  75. package/dist/http/routes/devices.js +3 -3
  76. package/dist/http/routes/memory-origin.d.ts +1 -1
  77. package/dist/http/routes/rules.js +13 -6
  78. package/dist/http/routes/runs.js +11 -5
  79. package/dist/http/routes/sessions.js +20 -16
  80. package/dist/http/wire-types.d.ts +12 -13
  81. package/dist/images/manifest.d.ts +7 -1
  82. package/dist/images/manifest.js +18 -16
  83. package/dist/list-entry-display.d.ts +14 -0
  84. package/dist/list-entry-display.js +14 -0
  85. package/dist/memory-store-refusal.d.ts +7 -12
  86. package/dist/memory-store-refusal.js +4 -5
  87. package/dist/namespaced-rule-name.d.ts +4 -3
  88. package/dist/namespaced-rule-name.js +2 -6
  89. package/dist/observability/fail-open.d.ts +9 -1
  90. package/dist/observability/fail-open.js +9 -1
  91. package/dist/operator-ask.d.ts +7 -6
  92. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  93. package/dist/plugins/approval-exemption-store.js +1 -0
  94. package/dist/plugins/blob-backend.d.ts +13 -0
  95. package/dist/plugins/blob-backend.js +26 -0
  96. package/dist/plugins/local-session-store.d.ts +78 -47
  97. package/dist/plugins/local-session-store.js +152 -122
  98. package/dist/plugins/one-time-migrations.d.ts +14 -0
  99. package/dist/plugins/one-time-migrations.js +90 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -5
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  108. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  109. package/dist/plugins/store-backend.d.ts +17 -6
  110. package/dist/plugins/store-backend.js +46 -16
  111. package/dist/plugins/store-open-wait.d.ts +33 -0
  112. package/dist/plugins/store-open-wait.js +73 -0
  113. package/dist/rule-entry-list.d.ts +0 -1
  114. package/dist/rule-entry-list.js +5 -10
  115. package/dist/rules-consent.d.ts +12 -3
  116. package/dist/rules-consent.js +5 -4
  117. package/dist/run-local.js +6 -3
  118. package/dist/security.d.ts +5 -3
  119. package/dist/security.js +4 -0
  120. package/dist/sema-config-dir.d.ts +19 -0
  121. package/dist/sema-config-dir.js +33 -0
  122. package/dist/session-policy-wire.js +10 -14
  123. package/dist/spec-fields.d.ts +7 -2
  124. package/dist/store-posture.d.ts +14 -6
  125. package/dist/store-posture.js +7 -4
  126. package/dist/task-settings.d.ts +30 -58
  127. package/dist/task-settings.js +6 -18
  128. package/dist/tool-approval.d.ts +97 -43
  129. package/dist/tool-approval.js +120 -31
  130. package/dist/trace/engine-notice-wire.d.ts +6 -4
  131. package/dist/trace/engine-notice-wire.js +16 -9
  132. package/dist/trace/redact.d.ts +6 -0
  133. package/dist/trace/redact.js +9 -9
  134. package/dist/trace/self-minted-wire.d.ts +15 -1
  135. package/dist/trace/self-minted-wire.js +2 -0
  136. package/dist/user-input-ledger.d.ts +19 -15
  137. package/dist/user-input-ledger.js +12 -20
  138. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  139. package/package.json +2 -2
@@ -398,8 +398,11 @@ export declare function createStoreBackend(config: ServiceConfig, onWarn?: BootL
398
398
  * 而 LocalBackend 的 `ensureSchema` 是**空实现**,真正会抛的 mkdir + 数据根 BootLock 在构造函数里、
399
399
  * 落在 try 之外。于是那条降级臂对 local 形从来没生效过,注释宣称的行为与代码真做的事撕裂。
400
400
  *
401
- * 降级条件不变(两形同一姿势):`sessionBackend=auto`(配置的 DB 不可达,S5 原形)或**默认推导**的
402
- * local(`dbBackend=local && !dbBackendExplicit`)。**显式** DB_BACKEND=local / tidb 仍 fail-loud——
401
+ * **降级臂只有一条:缺省推导的 local**(`dbBackend=local && !dbBackendExplicit`)。🔴 **SQL 族没有降级臂** —— 配了 DSN 就是要这只库:
402
+ * `SESSION_BACKEND=auto` 与显式 `DB_BACKEND=mysql|pg` 同一待遇,库当下打不开 ⇒ 在 `STORE_LOCK_WAIT_MS` 的窗内重试
403
+ * ({@link openWithinWait}),到窗仍打不开 ⇒ 拒启(拒句 {@link buildSqlStoreOpenRefusal} 点名后端与库的显示安全形)。修前 `auto` 在这里
404
+ * 静默落内存(只留一行 warn):会话 / 停驻 / run 账本进进程内存、重启即丢,而 `/health` 照样 200 —— [ref] P 类 fail-open。
405
+ * 零 DSN 的内存是配置说的(`config.ts` 的 `auto` 在无坐标时就不探测),不经过本函数。**显式** DB_BACKEND=local 仍 fail-loud——
403
406
  * operator 要了 durable,静默丢=丢数据。 */
404
407
  export declare function openStoreBackendWithFallback(config: ServiceConfig, logger: {
405
408
  warn: (msg: string, meta?: Record<string, unknown>) => void;
@@ -409,9 +412,17 @@ export declare function openStoreBackendWithFallback(config: ServiceConfig, logg
409
412
  degradedReason: StoreDegradedReason | undefined;
410
413
  }>;
411
414
  /**
412
- * S-866 ⑤c / G4 —— SQL 族「显式要了持久而店打不开」的拒启句(唯一铸点)。点名 `DB_BACKEND=<引擎>`,给三条出路:修坐标 / 连通性;
413
- * `SESSION_BACKEND=auto`(尽力而为:店不可达时以内存起服,什么都不持久);`DB_BACKEND=memory`(显式内存部署)。驱动原文只取 message
414
- * (与修前 `fatalExit` 打出的同一句),凭据不在其中(驱动不回显口令)。
415
+ * 已配置的 SQL 库坐标 → 一条只含 `scheme://host:port` 的 URL 串,**交给**配置型 URL 的唯一显示铸点 {@link buildDisplaySafeUrl}
416
+ * (出口处写 `buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config))`)。它**不是**一份显示投影:显示形只有铸点一只;这里只做「结构化坐标 → URL 串」的拼装
417
+ * (`config.tidb` / `config.pg` 本来就是结构化坐标,没有 DSN 字符串),user / password / database 一概不进产物。
418
+ * host 过不了闭字符集(拼进 URL 会把 `/` `@` `?` 之类带成别的成分)或无坐标 ⇒ 交出一个不是 URL 的串,铸点答它的占位 `«redacted:url»`。
419
+ * scheme 只是引擎名(`postgres` / `mysql`),不对应任何连接。
415
420
  */
416
- export declare function buildSqlStoreOpenRefusal(engine: "mysql" | "pg", err: unknown): string;
421
+ export declare function sqlStoreCoordinatesAsUrl(config: Pick<ServiceConfig, "dbBackend" | "tidb" | "pg">): string;
422
+ /**
423
+ * SQL 族「配了 DSN 而店打不开(窗内重试过了)」的拒启句(唯一铸点)。点名 `DB_BACKEND=<引擎>`、库的显示安全形(`postgres://host:port` / `mysql://host:port`,经 {@link sqlStoreCoordinatesAsUrl} 拼出、再过 {@link buildDisplaySafeUrl},
424
+ * 不带口令)、等了多久,出路两条:修坐标 / 连通性(窗是 `STORE_LOCK_WAIT_MS`);或**显式**内存部署(`DB_BACKEND=memory`)。🔴 没有第三条
425
+ * 「`SESSION_BACKEND=auto` 内存起服」—— 那一条就是被删掉的静默降级。驱动原文只取 message(与修前 `fatalExit` 打出的同一句),凭据不在其中(驱动不回显口令)。
426
+ */
427
+ export declare function buildSqlStoreOpenRefusal(engine: "mysql" | "pg", endpoint: string, waitedMs: number, err: unknown): string;
417
428
  //# sourceMappingURL=store-backend.d.ts.map
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { createCorruptReadFailOpenSeat, createDurableAbsenceSeat } from "../observability/corrupt-read-seat.js";
3
+ import { createLogger } from "../observability/logger.js";
3
4
  import { join } from "node:path";
4
- import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/core";
5
+ import { AdoptionError, FileStorageBackend } from "@sema-agent/core";
5
6
  import { FileWorkflowJournalStore } from "@sema-agent/core";
6
7
  import { fileSessionCaptureRecordStore } from "@sema-agent/core";
7
8
  import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
@@ -12,8 +13,7 @@ import { StoreBootRefusalError } from "./store-contracts.js";
12
13
  import { buildStoreOpenRefusalTail } from "./store-open-refusal.js";
13
14
  import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
14
15
  import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
15
- import { LocalSessionStore } from "./local-session-store.js";
16
- import { noteReplayInSessionOpenScope } from "../session-records-skipped-notice.js";
16
+ import { LocalSessionStore, createLocalSessionRepo } from "./local-session-store.js";
17
17
  import { LocalCheckpointStore } from "./local-checkpoint-store.js";
18
18
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
19
19
  import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
@@ -30,6 +30,7 @@ import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-s
30
30
  import { TiDBWorkflowJournalStore, PgWorkflowJournalStore } from "./workflow-journal-store-sql.js";
31
31
  import { TiDBFileHistoryStore, PgFileHistoryStore, FILE_HISTORY_BLOB_TABLE, FILE_HISTORY_BLOB_KEY_PREFIX } from "./file-history-store-sql.js";
32
32
  import { MinioBlobBackend, SqlBlobBackend } from "./blob-backend.js";
33
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
33
34
  import { fileMemoryControlDirFor } from "../memory-scope.js";
34
35
  import { createTidbPool, ensureSchema as ensureTidbSchema } from "./tidb-pool.js";
35
36
  import { createPgPool, ensurePgSchema, pgPoolOptions } from "./pg-pool.js";
@@ -55,6 +56,7 @@ import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
55
56
  import { createFilePermissionRuleStores } from "./permission-rule-store-file.js";
56
57
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
57
58
  import { storeDegradedReasonFor } from "../store-posture.js";
59
+ import { openWithinWait } from "./store-open-wait.js";
58
60
  function historyBlobBackend(config, dialect, pool) {
59
61
  const s = config.snapshotBlobStore;
60
62
  if (!s)
@@ -240,9 +242,7 @@ class LocalBackend {
240
242
  throw new Error(`DB_BACKEND=local cannot open data dir ${root}: ${e instanceof Error ? e.message : String(e)}` +
241
243
  (tail === undefined ? "" : ` ${tail}`));
242
244
  }
243
- this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root, {
244
- onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInSessionOpenScope),
245
- }), this.fileBackend.root);
245
+ this.sessionStore = new LocalSessionStore(createLocalSessionRepo(this.fileBackend.root), this.fileBackend.root, { log: createLogger(config.logLevel) });
246
246
  const corruptReadWarn = (msg, fields) => this.discloseWarn(msg, fields);
247
247
  this.runStore = new FileRunStore(this.fileBackend.root, {
248
248
  error: (msg, fields) => void (this.onError ?? this.onWarn)?.(msg, fields),
@@ -344,21 +344,41 @@ export function createStoreBackend(config, onWarn, onError) {
344
344
  return new TiDBBackend(createTidbPool(config), config);
345
345
  }
346
346
  export async function openStoreBackendWithFallback(config, logger) {
347
- const mayFallback = config.dbBackend === "local" ? !config.dbBackendExplicit : config.sessionBackend === "auto";
347
+ const sqlEngine = config.dbBackend === "mysql" || config.dbBackend === "pg" ? config.dbBackend : undefined;
348
+ const mayFallback = config.dbBackend === "local" && !config.dbBackendExplicit;
348
349
  let backend;
350
+ let inFlight;
349
351
  try {
350
- backend = createStoreBackend(config, (msg, meta) => logger.warn(msg, meta), logger.error === undefined ? undefined : (msg, meta) => logger.error(msg, meta));
351
- if (backend)
352
- await backend.ensureSchema();
352
+ const attempt = async () => {
353
+ let built;
354
+ try {
355
+ built = createStoreBackend(config, (msg, meta) => logger.warn(msg, meta), logger.error === undefined ? undefined : (msg, meta) => logger.error(msg, meta));
356
+ inFlight = built;
357
+ if (built)
358
+ await built.ensureSchema();
359
+ inFlight = undefined;
360
+ return built;
361
+ }
362
+ catch (err) {
363
+ inFlight = undefined;
364
+ await built?.close().catch(() => undefined);
365
+ throw err;
366
+ }
367
+ };
368
+ backend =
369
+ sqlEngine === undefined
370
+ ? await attempt()
371
+ : await openWithinWait(attempt, config.storeLockWaitMs, (w) => logger.warn("store_sql_open_wait", { engine: sqlEngine, endpoint: buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config)), waitMs: w.waitMs, error: w.error }));
353
372
  }
354
373
  catch (err) {
374
+ void inFlight?.close().catch(() => undefined);
355
375
  if (err instanceof AdoptionError)
356
376
  throw err;
357
377
  if (err instanceof StoreBootRefusalError)
358
378
  throw err;
359
379
  if (!mayFallback) {
360
- if (config.dbBackend === "mysql" || config.dbBackend === "pg")
361
- throw new Error(buildSqlStoreOpenRefusal(config.dbBackend, err), { cause: err });
380
+ if (sqlEngine !== undefined)
381
+ throw new Error(buildSqlStoreOpenRefusal(sqlEngine, buildDisplaySafeUrl(sqlStoreCoordinatesAsUrl(config)), config.storeLockWaitMs, err), { cause: err });
362
382
  throw err;
363
383
  }
364
384
  logger.warn("store_db_unreachable_fallback_memory", {
@@ -370,11 +390,21 @@ export async function openStoreBackendWithFallback(config, logger) {
370
390
  }
371
391
  return { backend, degradedReason: undefined };
372
392
  }
373
- export function buildSqlStoreOpenRefusal(engine, err) {
393
+ const SQL_HOST = /^(?:[A-Za-z0-9._-]{1,253}|\[[0-9A-Fa-f:.]{2,45}\])$/;
394
+ const SQL_COORDINATES_UNPRINTABLE = "<unprintable-coordinates>";
395
+ export function sqlStoreCoordinatesAsUrl(config) {
396
+ const coords = config.dbBackend === "pg" ? config.pg : config.tidb;
397
+ if (coords === undefined || !SQL_HOST.test(coords.host))
398
+ return SQL_COORDINATES_UNPRINTABLE;
399
+ return `${config.dbBackend === "pg" ? "postgres" : "mysql"}://${coords.host}:${coords.port}`;
400
+ }
401
+ export function buildSqlStoreOpenRefusal(engine, endpoint, waitedMs, err) {
374
402
  const coords = engine === "pg" ? "PG_*" : "MYSQL_*";
375
403
  const detail = err instanceof Error ? err.message : String(err);
376
- return (`DB_BACKEND=${engine}: cannot open the configured ${engine === "pg" ? "PostgreSQL" : "MySQL-protocol"} store (${detail}) — refusing to boot, because DB_BACKEND asks for a durable store and SESSION_BACKEND does not allow running without it. ` +
377
- `Next: fix the ${coords} coordinates or the store's reachability; or set SESSION_BACKEND=auto to boot in memory while the store is unreachable (best effort — nothing persists, and /health reports storeDegraded); ` +
378
- `or set DB_BACKEND=memory for an explicitly in-memory deployment.`);
404
+ const waited = waitedMs > 0 ? `after retrying for ${waitedMs}ms (STORE_LOCK_WAIT_MS)` : "on the first attempt (STORE_LOCK_WAIT_MS=0: no wait)";
405
+ return (`DB_BACKEND=${engine}: cannot open the configured ${engine === "pg" ? "PostgreSQL" : "MySQL-protocol"} store at ${endpoint} ${waited} (${detail}) — refusing to boot, because a configured SQL store is never replaced by an in-memory one ` +
406
+ `(SESSION_BACKEND=auto included: sessions, parked runs and every durable record would silently live in memory and vanish on restart). ` +
407
+ `Next: fix the ${coords} coordinates or the store's reachability (the boot wait is STORE_LOCK_WAIT_MS); ` +
408
+ `or run explicitly in memory with DB_BACKEND=memory (and remove the ${coords} coordinates).`);
379
409
  }
380
410
  //# sourceMappingURL=store-backend.js.map
@@ -0,0 +1,33 @@
1
+ /** 进入等待腿时的**一次性**披露(结构上只在第一次失败时调一次,不是靠一个 flag 挡刷屏)。 */
2
+ export type StoreOpenWaitSink = (fields: {
3
+ readonly waitMs: number;
4
+ readonly error: string;
5
+ }) => void;
6
+ /** 时钟三面(异步形:`sleep` 返回 Promise)。三面都是形参,判据不靠真时间。`deadline` = 一只可取消的到点信号(窗尾打断挂起的尝试用)。 */
7
+ export interface StoreOpenWaitClock {
8
+ now(): number;
9
+ sleep(ms: number): Promise<void>;
10
+ deadline(ms: number): {
11
+ readonly expired: Promise<void>;
12
+ cancel(): void;
13
+ };
14
+ }
15
+ /** {@link openWithinWait} 抛出的是不是「到窗仍未返回、且从没有过一次真失败」那一形(调用方据此区分「没有证据」与「有一次确定的失败」)。 */
16
+ export declare function isStoreOpenWindowExpired(err: unknown): boolean;
17
+ /**
18
+ * 跑 `open`;失败且窗为正 ⇒ 在窗内每 {@link STORE_LOCK_POLL_MS} 重试。
19
+ *
20
+ * 🔴 **窗尾只有一条规则**:到窗时手上有一次**完成了的**真失败 ⇒ **原样**抛最近那一次(同一个对象,不重铸 —— 库的拒句读驱动原文,桶的拒句读
21
+ * 「缺桶」那只错);**从头到尾一次真失败都没有**(每次尝试都挂着)⇒ 抛 {@link StoreOpenWindowExpired}(「到窗仍未返回」,{@link isStoreOpenWindowExpired} 判别)。
22
+ * · 窗从**第一次尝试开始**算,每次尝试与**剩余**窗赛跑 —— 挂起的尝试到窗尾也被打断(库接受了 TCP 却永不回握手、DDL 被长期锁住);被打断的那次尝试
23
+ * 不再有人等它:它迟到的结局被吞掉(没人接 = 未处理拒绝会杀进程),它手上的资源由调用方的 `open` 自己收(见 `openStoreBackendWithFallback` 的在飞登记)。
24
+ * · 剩余窗 ≤ 0 **不再起新一次尝试**:一次失败之后的等待吃完了窗,就抛那次失败 —— 不起一次注定跟 0 ms 的到点赛跑、又几乎总是输的尝试
25
+ * (那一形会把一次确定的失败换成「到窗仍未返回」:口令错读成库挂住、缺桶读成没有证据)。
26
+ * 窗为 0 ⇒ **只试一次、不设界**(与 7.118.x 同形:一次尝试挂多久由驱动自己的超时决定 —— 本腿不替它造一个)。
27
+ *
28
+ * @param open 一次完整尝试(造后端 + `ensureSchema()` / 一次桶探针);**失败时必须自己把这次造出的池还回去**,窗内会被再调用。
29
+ * @param waitMs `config.storeLockWaitMs`。`> 0` 才等;`0` / 非数 ⇒ 只试一次(`!(waitMs > 0)` 同时盖住「键缺席」的手搓 config)。
30
+ * @param onWait 进入等待腿的一次性披露口(第一次完成的失败时调一次)。
31
+ */
32
+ export declare function openWithinWait<T>(open: () => Promise<T>, waitMs: number, onWait: StoreOpenWaitSink, clock?: StoreOpenWaitClock): Promise<T>;
33
+ //# sourceMappingURL=store-open-wait.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { STORE_LOCK_POLL_MS } from "./store-lock-wait.js";
2
+ const REAL_CLOCK = {
3
+ now: () => performance.now(),
4
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
5
+ deadline: (ms) => {
6
+ let timer;
7
+ const expired = new Promise((resolve) => {
8
+ timer = setTimeout(resolve, ms);
9
+ });
10
+ return { expired, cancel: () => clearTimeout(timer) };
11
+ },
12
+ };
13
+ class StoreOpenWindowExpired extends Error {
14
+ constructor(waitMs) {
15
+ super(`the open attempt was still pending when the ${waitMs}ms window ended`);
16
+ }
17
+ }
18
+ export function isStoreOpenWindowExpired(err) {
19
+ return err instanceof StoreOpenWindowExpired;
20
+ }
21
+ export async function openWithinWait(open, waitMs, onWait, clock = REAL_CLOCK) {
22
+ if (!(waitMs > 0))
23
+ return await open();
24
+ const startedAt = clock.now();
25
+ let lastFailure;
26
+ for (;;) {
27
+ const remainingMs = waitMs - (clock.now() - startedAt);
28
+ if (lastFailure !== undefined && remainingMs <= 0)
29
+ throw lastFailure.error;
30
+ const deadline = clock.deadline(Math.max(remainingMs, 0));
31
+ let outcome;
32
+ try {
33
+ outcome = await Promise.race([
34
+ attemptOnce(open).then((value) => ({ kind: "opened", value }), (error) => ({ kind: "failed", error })),
35
+ deadline.expired.then(() => ({ kind: "window_ended" })),
36
+ ]);
37
+ }
38
+ finally {
39
+ deadline.cancel();
40
+ }
41
+ switch (outcome.kind) {
42
+ case "opened":
43
+ return outcome.value;
44
+ case "window_ended":
45
+ if (lastFailure !== undefined)
46
+ throw lastFailure.error;
47
+ throw new StoreOpenWindowExpired(waitMs);
48
+ case "failed": {
49
+ if (lastFailure === undefined)
50
+ onWait({ waitMs, error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) });
51
+ lastFailure = { error: outcome.error };
52
+ const left = waitMs - (clock.now() - startedAt);
53
+ if (left <= 0)
54
+ throw outcome.error;
55
+ await clock.sleep(Math.min(STORE_LOCK_POLL_MS, left));
56
+ break;
57
+ }
58
+ default: {
59
+ const unhandled = outcome;
60
+ throw new Error(`unhandled open attempt outcome: ${JSON.stringify(unhandled)}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ function attemptOnce(open) {
66
+ try {
67
+ return open();
68
+ }
69
+ catch (err) {
70
+ return Promise.reject(err);
71
+ }
72
+ }
73
+ //# sourceMappingURL=store-open-wait.js.map
@@ -21,7 +21,6 @@
21
21
  */
22
22
  import { compileAllowList } from "@sema-agent/core";
23
23
  import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
24
- export declare function presentRuleEntry(entry: string): string;
25
24
  /** ① 形:字符串数组(空串也是字符串 —— 引擎把它读成 `grammar` 并通告 / 拒,见 ③)。 */
26
25
  export declare const ruleEntryListShapeOk: (v: unknown) => v is readonly string[];
27
26
  /**
@@ -1,11 +1,6 @@
1
1
  import { compileAllowList } from "@sema-agent/core";
2
+ import { displayListEntry } from "./list-entry-display.js";
2
3
  import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES, ruleEntryToolName } from "./namespaced-rule-name.js";
3
- import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
- const PRESENTED_ENTRY_MAX = 160;
5
- export function presentRuleEntry(entry) {
6
- const clean = entry.replace(CONTROL_AND_BIDI_STRIP_RE, "");
7
- return clean.length <= PRESENTED_ENTRY_MAX ? clean : `${clean.slice(0, PRESENTED_ENTRY_MAX)}…(${entry.length} chars)`;
8
- }
9
4
  export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
10
5
  export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
11
6
  export function describeAllowLayerOverCap(field, count) {
@@ -26,14 +21,14 @@ export function emptyToolSegmentEntryOf(list) {
26
21
  return undefined;
27
22
  }
28
23
  export function describeEmptyToolSegmentEntry(field, u) {
29
- return `${field} names "${presentRuleEntry(u.entry)}", which can never match a tool (${describeEmptyToolSegment(u.ns)})`;
24
+ return `${field} names ${displayListEntry(u.entry)}, which can never match a tool (${describeEmptyToolSegment(u.ns)})`;
30
25
  }
31
26
  export function inertRuleEntriesOf(list, behavior) {
32
27
  return compileAllowList(list, behavior, NO_DECLARED_TOOL_NAMES).inert;
33
28
  }
34
29
  export function inertRuleEntryGuidance(i) {
35
30
  const reason = i.reason;
36
- const write = i.currentName !== undefined ? ` — write "${presentRuleEntry(i.currentName)}"` : "";
31
+ const write = i.currentName !== undefined ? ` — write ${displayListEntry(i.currentName)}` : "";
37
32
  switch (reason) {
38
33
  case "tool_name":
39
34
  return `the tool name can never match a mounted tool (${i.verdict ?? "stale"})${write}`;
@@ -50,7 +45,7 @@ export function inertRuleEntryGuidance(i) {
50
45
  case "content_unsupported":
51
46
  return "this tool takes no content form here — name the tool itself, or use Bash(<command>) / Bash(<prefix>:*) for a shell command";
52
47
  case "grammar":
53
- return "the entry is not read as written (surrounding whitespace, an empty or unbalanced tool name) — write the tool name exactly, with no padding";
48
+ return "the entry is not read as written (surrounding whitespace, an empty or unbalanced tool name, or an invisible or control character in the tool name) — write the tool name exactly, with no padding";
54
49
  default: {
55
50
  const unhandled = reason;
56
51
  throw new Error(`unhandled allow-entry inert reason: ${String(unhandled)}`);
@@ -58,7 +53,7 @@ export function inertRuleEntryGuidance(i) {
58
53
  }
59
54
  }
60
55
  export function describeInertRuleEntries(field, inert) {
61
- return inert.map((i) => `${field} entry "${presentRuleEntry(i.entry)}" reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
56
+ return inert.map((i) => `${field} entry ${displayListEntry(i.entry)} reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
62
57
  }
63
58
  const isPlainRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
64
59
  function skillDenyInertSentence(skill) {
@@ -163,7 +163,7 @@ export interface LocalImportRuleKey {
163
163
  * 它在端点上先判、有自己的整句文案,而且判完就把类型收窄掉(`permission-rule-vocab.ts` 的
164
164
  * `DIRECT_RULE_WRITE_ADMITS`)⇒ 放宽态根本到不了本表所服务的那段代码。 */
165
165
  export type LocalImportRefusalCode =
166
- /** scope 串读不出形(既不是 `global` 也不是 `project:<非空 root>`)。 */
166
+ /** scope 串读不出形,或读出来的 scope 引擎的 durable 谓词不收(既不是 `global` 也不是 `project:<绝对 root>`;判据 = {@link parseRuleScope})。 */
167
167
  "scope_unreadable"
168
168
  /** scope 的 project root 里有 bidi 控制符([ref]② Trojan Source 族):人看到的射程与机器判据用的
169
169
  * 射程不是同一串 —— 一条规则的**作用范围**因此可以被伪装。 */
@@ -480,7 +480,7 @@ export declare function serializeRuleScope(scope: RuleScope): string;
480
480
  * 各挑各的」这件事本身,不是把某个数字调大一点)。
481
481
  *
482
482
  * 病:铸侧 `cardRuleScopeRoot` 对 root 长度零把关(入口尺 = `isValidCwd` 的 {@link MAX_CWD_CHARS}),
483
- * core 与两个店都只校验 `root.min(1)` ⇒ 写面全程无阻;而 `DELETE /v1/rules` 的体 schema 此前独立写死
483
+ * core 与两个店都不设 root 长度上限 ⇒ 写面全程无阻;而 `DELETE /v1/rules` 的体 schema 此前独立写死
484
484
  * `scope: max(1088)`,且 safeParse 排在 `parseRuleScope`/权限判据**之前** ⇒ 超限 root 的规则
485
485
  * `GET /v1/rules` **列得出**、按同一串 DELETE **恒 400**。全仓只有一个 `lane.removeRule` 调用点(直删
486
486
  * 店行是无墓碑硬删、会被 sync 的 join 复活),所以那是**一条已列出的常驻放行规则在受支持接口上不可撤**;
@@ -502,8 +502,17 @@ export type DurableRuleScope = Exclude<RuleScope, {
502
502
  kind: "session";
503
503
  }>;
504
504
  /** {@link serializeRuleScope} 的逆。读不出形 ⇒ `undefined`(调用方 400,绝不猜一个 global 出来 ——
505
- * 猜 global 会把一次「删项目内规则」的请求变成一次删不掉任何东西的 no-op,或者更糟)。 */
505
+ * 猜 global 会把一次「删项目内规则」的请求变成一次删不掉任何东西的 no-op,或者更糟)。
506
+ *
507
+ * 🔴 **收不收只有一只判据:core 的 `isValidDurableScope`**(引擎 7.46.0 起 project root 必须是完整绝对路径:`/…`、`X:\…` /
508
+ * `X:/…`、`\\host\…`)。四个 wire 入口(`/v1/rules` 写 / 撤 / 列举过滤、local-import 的行)都经这里;cc-import 的层 root 调同一只
509
+ * 谓词(`http/routes/rules.ts`)。修前这里手抄了一份「root 非空」:`project:myproj` 本仓收、引擎写臂拒(写口答 503「再发一次」——
510
+ * 客户端会无限重试)、引擎读侧丢(撤销口删不动),一条规则在两侧是两种命运。存量的那种行由一次性迁移 M11 删
511
+ * (`plugins/one-time-migrations.ts` 登记表)。 */
506
512
  export declare function parseRuleScope(text: string): DurableRuleScope | undefined;
513
+ /** 引擎不收的 scope(或 cc-import 层 root)的**唯一一句**拒句:写 / 撤 / 列举过滤三个路由口、cc-import 预览的层 root、local-import 的成员拒因
514
+ * `scope_unreadable` 都出站这一只(各口的码照旧各自的),不在读点各抄一份。 */
515
+ export declare const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
507
516
  /** 一条规则在 wire 上的行 = 一个逻辑 (rule, scope) 对([ref] v2 §6 F5)。
508
517
  * `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
509
518
  * 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
@@ -5,7 +5,7 @@ import { createLogger } from "./observability/logger.js";
5
5
  import { MAX_CWD_CHARS } from "./task-cwd.js";
6
6
  import { isDirectWritableRuleBehavior } from "./permission-rule-vocab.js";
7
7
  import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
8
- import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, addPersistedRule, removePersistedRule, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
8
+ import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, addPersistedRule, removePersistedRule, isValidDurableScope, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
9
9
  import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
10
10
  export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
11
11
  export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
@@ -57,9 +57,10 @@ export function parseRuleScope(text) {
57
57
  return { kind: "global" };
58
58
  if (!text.startsWith("project:"))
59
59
  return undefined;
60
- const root = text.slice("project:".length);
61
- return root === "" ? undefined : { kind: "project", root };
60
+ const scope = { kind: "project", root: text.slice("project:".length) };
61
+ return isValidDurableScope(scope) ? scope : undefined;
62
62
  }
63
+ export const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
63
64
  export function sameWireRuleIdentity(row, identity) {
64
65
  return row.behavior === identity.behavior && row.rule === identity.rule && row.scope === identity.scope;
65
66
  }
@@ -163,7 +164,7 @@ function anchorBatchMembers(members, displayed) {
163
164
  function screenLocalImportRow(row) {
164
165
  const scope = parseRuleScope(row.scope);
165
166
  if (scope === undefined) {
166
- return { reason: "scope_unreadable", detail: "scope must be 'global' or 'project:<root>' with a non-empty root" };
167
+ return { reason: "scope_unreadable", detail: RULE_SCOPE_REFUSAL_SENTENCE };
167
168
  }
168
169
  if (scope.kind === "project" && hasBidiControls(scope.root)) {
169
170
  return {
package/dist/run-local.js CHANGED
@@ -33,7 +33,7 @@ import { createInstructionSourceSeat } from "./instruction-source-notice.js";
33
33
  import { loadDeploymentSkills } from "./capabilities/skills.js";
34
34
  import { createRepoClient } from "./capabilities/repo-tools.js";
35
35
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
36
- import { buildScenarios, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
36
+ import { buildScenarios, gateScenarioRequest, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
37
37
  import { createTaskListLane } from "./boot/task-list-lane.js";
38
38
  import { pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
39
39
  import { HttpError } from "./security.js";
@@ -56,6 +56,7 @@ import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.
56
56
  import { createHostLspManager } from "./host-lsp-manager.js";
57
57
  import { buildDisplaySafeUrl } from "./trace/redact.js";
58
58
  import { installOutboundDispatcher } from "./outbound-dispatcher.js";
59
+ import { provisionSemaConfigDir } from "./sema-config-dir.js";
59
60
  export function parseArgs(argv) {
60
61
  let objective;
61
62
  let root;
@@ -139,6 +140,7 @@ export const NO_TTY_ASK_CLASS_ROW = {
139
140
  content_question: { remedy: "terminal_only", autoBudgetReleases: false },
140
141
  hook: { remedy: "terminal_only", autoBudgetReleases: false },
141
142
  ancestor: { remedy: "terminal_only", autoBudgetReleases: false },
143
+ dangerous_delete: { remedy: "terminal_only", autoBudgetReleases: false },
142
144
  unverifiable_delete: { remedy: "terminal_only", autoBudgetReleases: false },
143
145
  transcript_write: { remedy: "terminal_only", autoBudgetReleases: false },
144
146
  org_rule: { remedy: "terminal_only", autoBudgetReleases: false },
@@ -324,6 +326,7 @@ export async function runLocal(argv, deps = {}) {
324
326
  const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
325
327
  installSecretEnvScrubRecorder({ logger, metrics });
326
328
  logConfigDiagnostics(logger);
329
+ provisionSemaConfigDir(config, logger);
327
330
  installOutboundDispatcher(logger);
328
331
  const providerInput = { provider: config.configProvider ?? "local", localDir: root };
329
332
  try {
@@ -592,11 +595,11 @@ export async function runLocal(argv, deps = {}) {
592
595
  const { overlay } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
593
596
  Object.assign(scenarios, overlay);
594
597
  }
595
- const requested = args.scenario ?? config.defaultScenario;
596
- const scenarioName = scenarios[requested] ? requested : "default";
598
+ let scenarioName = args.scenario ?? config.defaultScenario;
597
599
  const sessionId = args.session ?? uuidv7();
598
600
  let cap;
599
601
  try {
602
+ scenarioName = gateScenarioRequest(undefined, args.scenario, config.defaultScenario, Object.keys(scenarios), logger);
600
603
  cap = selectScenario(scenarios, scenarioName)({ scenario: scenarioName, objective: args.objective }, undefined, { sessionId });
601
604
  }
602
605
  catch (err) {
@@ -72,9 +72,11 @@ export declare const LEGACY_UNOWNED_AMNESTY_TENANT = "default";
72
72
  * · `runOwnerOk` 三读面(head / audit 单读 / events):`owner === null` 那一支**本来就放行**
73
73
  * (判据是 `owner !== null && principal !== owner`),不需要本臂。[ref] 帖文里「单读 404」一句
74
74
  * 经亲读实现行**不成立**,已加现状钉(session-head-probe.test.ts)锁住这个事实。
75
- * · `PUT /v1/sessions/:id/policy` 的 `owner === null ⇒ 409 conflict.session_ownerless`:那是**有意的
76
- * fail-closed** —— 无主会话没有稳定的 run-principal,存下去的规则读不到、静默失效。放宽它等于制造
77
- * 一条 fail-open,不在本臂辖域。
75
+ * · `GET|PUT /v1/sessions/:id/policy` 的 `owner === null ⇒ 409 conflict.session_ownerless`:那是**有意的
76
+ * fail-closed** —— 规则按「会话 × principal」存、按 run 的 principal 读,而规则路由的存储键就是会话的属主;无主会话没有
77
+ * 这把键,替它读写规则只能**猜**一个 principal(猜错 = 200 说已生效,其实没有哪条 run 读它)。已经存下的规则不受影响:
78
+ * 它们存在当初那个属主的键下,那个 principal 的 run 照读照生效(7.119.0 前 local 后端重启即把属主忘掉,于是那些规则
79
+ * 在跑、却读不到也删不掉 —— 修在属主落盘,不在放宽本门)。放宽它等于制造一条 fail-open,不在本臂辖域。
78
80
  * · `…/sync/*`(PULL/PUSH/import):PULL 虽是只读、形同 workspace,但这一族**带一条破坏性 import 腿**
79
81
  * (`isDestructiveSessionWrite` 明列),而 [ref]/[ref] 裁的是会话**读面与生命周期**四门。放宽一个
80
82
  * 写面族超出裁定射程 —— 要做另立一次表态,不在本臂里顺手带过。
package/dist/security.js CHANGED
@@ -75,6 +75,10 @@ export function createAuthorizer(config, sessionStore) {
75
75
  throw new HttpError(403, "cannot claim a caller-supplied sessionId on a multi-tenant deployment: this worker's session store cannot record session ownership, so the id would stay unowned and the next caller could attach to it. Configure a durable session backend (SESSION_BACKEND), or omit sessionId to start a new session.");
76
76
  }
77
77
  }
78
+ else if (owner === null && principal !== undefined && !config.requirePrincipal && register) {
79
+ await register(requested, principal);
80
+ owner = await ownerOf(requested);
81
+ }
78
82
  const legacyShellOwner = !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
79
83
  if (!legacyShellOwner && owner != null && owner !== principal) {
80
84
  throw principal
@@ -0,0 +1,19 @@
1
+ import type { Logger } from "./observability/logger.js";
2
+ /** 部署声明的配置根(绝对路径);没声明 ⇒ `undefined`;空串 ⇒ 抛(起服拒,句子点名变量与出路)。纯读 env,不碰文件。**只有 `loadConfig` 调它**
3
+ * (结果 = 配置席 `agentConfigDir`);别的读者读那一席。 */
4
+ export declare function declaredSemaConfigDir(env?: NodeJS.ProcessEnv): string | undefined;
5
+ /** 配置根:配置席 `agentConfigDir`(`loadConfig` 铸,声明了的根)在 ⇒ 它;缺席 ⇒ `~/.sema`(与引擎读字面 `~/.sema` 同一处)。不读 env。 */
6
+ export declare function configRootOf(config: {
7
+ readonly agentConfigDir?: string;
8
+ }): string;
9
+ /**
10
+ * 起服的**供给动作**(每个入口一次:HTTP 服务第 1 段、`run-local`,都在 logger 建起来之后、任何 Runner 之前):声明了配置根 ⇒ 确保目录在
11
+ * (不在就建,`0700` —— 本服务本来就往里写 LKG / 缓存 / 店)。没声明 ⇒ 什么都不做(`~/.sema` 由各写者首次写时自建,同 7.119.x)。
12
+ *
13
+ * **建不出来不在这里拒**:能不能用由引擎判(`new Runner` 对不是已存在目录的根拒 `config.agent_config_dir_invalid`,起服随之拒),本服务
14
+ * 只把建目录这件事的失败如实说一行(`sema_config_dir_unprovisioned`,点名变量与 errno)—— 运维在引擎的拒句之前就看到是哪根旋钮、为什么建不出。
15
+ */
16
+ export declare function provisionSemaConfigDir(config: {
17
+ readonly agentConfigDir?: string;
18
+ }, logger: Pick<Logger, "warn">): void;
19
+ //# sourceMappingURL=sema-config-dir.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ export function declaredSemaConfigDir(env = process.env) {
5
+ const raw = env.SEMA_CONFIG_DIR;
6
+ if (raw === undefined)
7
+ return undefined;
8
+ if (raw === "") {
9
+ throw new Error('env SEMA_CONFIG_DIR="" is empty — refusing to start rather than moving the configuration files: up to 7.119.x an empty value meant the process ' +
10
+ "working directory (the last-known-good config, the skill cache and the scheduler store were kept there), and reading it as unset would move them " +
11
+ "to ~/.sema without a word. Unset the variable to use ~/.sema, or set it to the directory you mean (to keep the old place, the working directory's path).");
12
+ }
13
+ return resolve(raw);
14
+ }
15
+ export function configRootOf(config) {
16
+ return config.agentConfigDir ?? join(homedir(), ".sema");
17
+ }
18
+ export function provisionSemaConfigDir(config, logger) {
19
+ const dir = config.agentConfigDir;
20
+ if (dir === undefined)
21
+ return;
22
+ try {
23
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ }
25
+ catch (e) {
26
+ logger.warn("sema_config_dir_unprovisioned", {
27
+ dir,
28
+ err: e instanceof Error ? e.message : String(e),
29
+ note: "SEMA_CONFIG_DIR names a directory this server could not create — the engine refuses a configuration root that is not an existing directory when the first Runner is built, so the boot stops there with the engine's refusal: create the directory, point the variable at one, or unset it (the default is ~/.sema)",
30
+ });
31
+ }
32
+ }
33
+ //# sourceMappingURL=sema-config-dir.js.map
@@ -1,5 +1,6 @@
1
- import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, judgeRuleToolName } from "@sema-agent/core";
2
- import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES } from "./namespaced-rule-name.js";
1
+ import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent } from "@sema-agent/core";
2
+ import { displayListEntry } from "./list-entry-display.js";
3
+ import { describeEmptyToolSegment, emptyToolSegmentOf } from "./namespaced-rule-name.js";
3
4
  const RULE_FIELDS = {
4
5
  toolAllow: true,
5
6
  toolDeny: true,
@@ -99,22 +100,17 @@ export function describeSessionPermissionRulesProblem(p) {
99
100
  }
100
101
  }
101
102
  function describeUnreachableSessionRuleName(u) {
102
- return `${u.field} names "${u.name}", which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
103
+ return `${u.field} names ${displayListEntry(u.name)}, which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
103
104
  }
104
105
  function describeLegacyToolName(r) {
106
+ const why = r.guidance !== undefined ? ` (${r.guidance})` : "";
105
107
  switch (r.verdict) {
106
- case "retired": {
107
- const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
108
- const guidance = v.kind === "retired" ? v.guidance : `renamed "${r.currentName ?? "?"}"`;
109
- return `${r.field} names "${r.name}", a retired tool name (${guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
110
- }
108
+ case "retired":
109
+ return `${r.field} names ${displayListEntry(r.name)}, a retired tool name${why}; with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` (${displayListEntry(r.currentName)})` : ""}`;
111
110
  case "unprefixed_namespace":
112
- return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
113
- case "unmintable_segment": {
114
- const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
115
- const guidance = v.kind === "unmintable_segment" ? v.guidance : "a protocol name whose peer segment the engine never mints";
116
- return `${r.field} names "${r.name}", which can never match a tool (${guidance}), so nothing was written`;
117
- }
111
+ return `${r.field} names ${displayListEntry(r.name)}, which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
112
+ case "unmintable_segment":
113
+ return `${r.field} names ${displayListEntry(r.name)}, which can never match a tool${why}, so nothing was written`;
118
114
  default: {
119
115
  const unhandled = r.verdict;
120
116
  throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
@@ -65,8 +65,13 @@ export declare const skillToolListOk: (v: unknown) => v is readonly string[];
65
65
  export declare function skillToolListRefusal(skill: Record<string, unknown>): string | undefined;
66
66
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
67
67
  * 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
68
- * 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。⚠️ 已知残账(core 半场):
69
- * core 只对 allowTools 未知项 fail-loud,denyTools 未知项(如 typo)静默失效 —— 已上板请 core 镜像。
68
+ * 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。`allowTools` / `denyTools` 本门只判形与条数(`toolList`),
69
+ * **名字归引擎一只判**(core 7.45.0 #1377 / #1385,开跑前,三来源同判):没有工具能是的拼法(带空白 / 括号形坏 / 不可见字符)、非字符串条目 ⇒ 整 run 拒
70
+ * `config.agent.unknown_tool`;无人挂的名 ⇒ 不拒、经 `onError`(phase `"config"`)告一次(本仓汇点 `runner_config_advisory`);大小写孪生 / 退役名(`bash` / `BASH`)
71
+ * ⇒ 开跑前拒 `config.agent.unknown_tool` —— 主车道的委派工具带 extraTools 工厂,本仓在同一处声明它的名册(core 7.46.0 `extraToolNames`;工厂与名册读同一批工具对象,
72
+ * `capabilities/builtin-tools.ts` `createSubagentExtraTools`),引擎因此列得出子代工具池。两张表管子代手里的**全部**工具:委派池与 hands 内建(`Read` / `Edit` / `Write` /
73
+ * `Bash` …,core #1430)—— `denyTools:["Write"]` ⇒ 子代没有 `Write`;`allowTools` 只点了调用方工具 ⇒ 子代一只 hands 工具都没有。hands 行之外的引擎工具
74
+ * (`Monitor` 等)仍不受这两张表管(引擎已知限制)。`denyTools` 的 `Tool(content)` ⇒ 整只工具对该子代藏起并告。本仓不在受理口另做名字对照(那会是同一问的第二个判官)。
70
75
  * ⚠️ 键判断必须 Object.hasOwn:`in` 走原型链,JSON 里的 "toString"/"constructor"/"__proto__" 键会被
71
76
  * 误判为白名单字段,随后 SHAPES[k].ok 不是函数 ⇒ TypeError(fresh 500 / resume 砖;cross-review 复现)。 */
72
77
  export declare function taskAgentIssue(item: unknown): string | null;