@sema-agent/server 7.119.0 → 7.121.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 (127) hide show
  1. package/MIGRATION.md +129 -0
  2. package/USAGE.md +72 -23
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  4. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  6. package/dist/boot/config-center.d.ts +5 -7
  7. package/dist/boot/config-center.js +37 -13
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/execution-env.js +3 -2
  10. package/dist/boot/fatal-exit.d.ts +7 -1
  11. package/dist/boot/fatal-exit.js +2 -1
  12. package/dist/boot/resolve-spec.js +1 -0
  13. package/dist/boot/runner-deps.d.ts +4 -3
  14. package/dist/boot/runner-deps.js +7 -3
  15. package/dist/boot/shutdown.d.ts +9 -10
  16. package/dist/boot/shutdown.js +54 -16
  17. package/dist/boot/stage-01-config.js +2 -0
  18. package/dist/boot/stage-09-leader.d.ts +2 -9
  19. package/dist/boot/stage-10-http-server.d.ts +1 -9
  20. package/dist/boot/stage-10-http-server.js +31 -8
  21. package/dist/capabilities/builtin-tools.d.ts +31 -8
  22. package/dist/capabilities/builtin-tools.js +22 -1
  23. package/dist/capabilities/composite-members.d.ts +1 -1
  24. package/dist/capabilities/composite-members.js +1 -1
  25. package/dist/capabilities/scenarios.d.ts +8 -6
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  28. package/dist/capabilities/send-user-file-tool.js +1 -1
  29. package/dist/config-catalog.js +13 -7
  30. package/dist/config-center/center-request.d.ts +16 -1
  31. package/dist/config-center/center-request.js +36 -5
  32. package/dist/config-lkg.d.ts +33 -5
  33. package/dist/config-lkg.js +55 -11
  34. package/dist/config-types.d.ts +46 -3
  35. package/dist/config-types.js +6 -0
  36. package/dist/config.d.ts +33 -4
  37. package/dist/config.js +107 -15
  38. package/dist/deployment-governance.d.ts +3 -1
  39. package/dist/deployment-governance.js +3 -1
  40. package/dist/device-store.d.ts +7 -0
  41. package/dist/device-store.js +4 -0
  42. package/dist/device-ws-hub.js +17 -9
  43. package/dist/device-ws-protocol.d.ts +15 -0
  44. package/dist/fleet/drain-entry.d.ts +87 -0
  45. package/dist/fleet/drain-entry.js +35 -0
  46. package/dist/fleet/engine-signal.d.ts +144 -0
  47. package/dist/fleet/engine-signal.js +60 -0
  48. package/dist/fleet/serving-lease.d.ts +47 -0
  49. package/dist/fleet/serving-lease.js +235 -0
  50. package/dist/fleet-client.d.ts +35 -19
  51. package/dist/fleet-client.js +49 -25
  52. package/dist/http/approval-sweeps.js +3 -5
  53. package/dist/http/dispatch.js +18 -13
  54. package/dist/http/health-self-report.d.ts +60 -0
  55. package/dist/http/health-self-report.js +53 -0
  56. package/dist/http/principal-gate.d.ts +8 -1
  57. package/dist/http/principal-gate.js +1 -0
  58. package/dist/http/readiness.d.ts +25 -5
  59. package/dist/http/readiness.js +31 -6
  60. package/dist/http/resume-legs.js +5 -1
  61. package/dist/http/route-ctx.d.ts +6 -4
  62. package/dist/http/route-table.d.ts +3 -0
  63. package/dist/http/route-table.js +4 -1
  64. package/dist/http/routes/a2a-serve.d.ts +1 -1
  65. package/dist/http/routes/a2a-serve.js +5 -5
  66. package/dist/http/routes/admin-drain.d.ts +12 -31
  67. package/dist/http/routes/admin-drain.js +15 -9
  68. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  69. package/dist/http/routes/approvals-assistant.js +3 -3
  70. package/dist/http/routes/capabilities.js +3 -1
  71. package/dist/http/routes/devices.js +6 -5
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +1 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +14 -6
  79. package/dist/http/routes/runs.d.ts +2 -2
  80. package/dist/http/routes/runs.js +2 -2
  81. package/dist/http/routes/side-query.d.ts +1 -1
  82. package/dist/http/routes/side-query.js +1 -1
  83. package/dist/http/routes/tasks.d.ts +2 -2
  84. package/dist/http/routes/tasks.js +3 -3
  85. package/dist/http/server.d.ts +7 -21
  86. package/dist/http/server.js +20 -1
  87. package/dist/http/wire-types.d.ts +7 -1
  88. package/dist/list-entry-display.d.ts +14 -0
  89. package/dist/list-entry-display.js +14 -0
  90. package/dist/model-reach-scope.d.ts +15 -3
  91. package/dist/model-reach-scope.js +3 -2
  92. package/dist/model-unreachable-notice.d.ts +2 -1
  93. package/dist/model-unreachable-notice.js +1 -0
  94. package/dist/namespaced-rule-name.js +2 -6
  95. package/dist/observability/fail-open.d.ts +20 -0
  96. package/dist/observability/fail-open.js +20 -0
  97. package/dist/operator-ask.d.ts +1 -1
  98. package/dist/plugins/one-time-migrations.d.ts +8 -0
  99. package/dist/plugins/one-time-migrations.js +72 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +65 -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 -6
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/request-key-closure.js +1 -1
  108. package/dist/rule-entry-list.d.ts +0 -1
  109. package/dist/rule-entry-list.js +4 -9
  110. package/dist/rules-consent.d.ts +16 -3
  111. package/dist/rules-consent.js +8 -4
  112. package/dist/run-local.js +2 -0
  113. package/dist/runs.js +1 -1
  114. package/dist/sema-config-dir.d.ts +19 -0
  115. package/dist/sema-config-dir.js +33 -0
  116. package/dist/session-policy-wire.js +5 -4
  117. package/dist/spec-fields.d.ts +5 -4
  118. package/dist/task-settings.d.ts +3 -3
  119. package/dist/tool-approval.d.ts +7 -0
  120. package/dist/tool-approval.js +4 -1
  121. package/dist/trace/core-keyset-guard.d.ts +2 -2
  122. package/dist/trace/engine-notice-wire.d.ts +6 -4
  123. package/dist/trace/engine-notice-wire.js +16 -9
  124. package/dist/trace/self-minted-wire.d.ts +78 -0
  125. package/dist/trace/self-minted-wire.js +32 -0
  126. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  127. package/package.json +3 -3
@@ -1,12 +1,13 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
3
+ import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, isValidDurableScope, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
4
4
  import { dialectProtocolJsonEncoder, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
5
5
  import { isMissingColumnError } from "./sql-errors.js";
6
6
  import { canonicalJson } from "../canonical-json.js";
7
7
  import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, isPersistedRuleTool, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
8
8
  import { ensureIndexes } from "./ensure-index.js";
9
- import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn } from "./sql-json-column.js";
9
+ import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, SqlJsonColumnError } from "./sql-json-column.js";
10
+ import { redactSecrets } from "../trace/redact.js";
10
11
  export function writerOfSqlRuleStore(store) {
11
12
  const w = Reflect.get(store, PERMISSION_RULE_WRITER);
12
13
  if (w === null || typeof w !== "object")
@@ -20,7 +21,7 @@ export function writerOfSqlRuleStore(store) {
20
21
  }
21
22
  export const DurableRuleScopeSchema = z.union([
22
23
  z.object({ kind: z.literal("global") }).strict(),
23
- z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
24
+ z.object({ kind: z.literal("project"), root: z.string() }).strict(),
24
25
  ]);
25
26
  const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
26
27
  const RuleAddSchema = z
@@ -57,11 +58,17 @@ const QuarantinedRuleAddRowSchema = z
57
58
  function readBucketRowBehavior(raw, what, ownerKey) {
58
59
  const behavior = readRowBehavior(raw);
59
60
  if (behavior === undefined) {
60
- throw new Error(`permission_rule bucket ${ownerKey} carries a ${what} whose behavior word is outside the closed set (${JSON.stringify(raw)}) — ` +
61
- `refusing the whole bucket rather than reading a standing deny as "no rule"`);
61
+ throw new BucketRefusedError(`permission_rule bucket ${ownerKey} carries a ${what} whose behavior word is outside the closed set`, `(${JSON.stringify(raw)}) — refusing the whole bucket rather than reading a standing deny as "no rule"`);
62
62
  }
63
63
  return behavior;
64
64
  }
65
+ class BucketRefusedError extends Error {
66
+ verdict;
67
+ constructor(verdict, evidence) {
68
+ super(`${verdict} ${evidence}`);
69
+ this.verdict = verdict;
70
+ }
71
+ }
65
72
  const WritablePersistedRuleSchema = PersistedRuleRowSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
66
73
  const WritableRuleTombstoneSchema = RuleTombstoneSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
67
74
  const FrontierSchema = z.record(z.string(), z.number().int().nonnegative().safe());
@@ -349,6 +356,64 @@ async function refusingUnreadableColumns(what, fn) {
349
356
  throw foldJsonColumn(e, (m) => `${m} — the ${what} does not carry a readable shape; refusing the whole of it rather than report a partial one`);
350
357
  }
351
358
  }
359
+ function bucketRowColumns(dialect) {
360
+ return `actor, counter, rev, ${jsonColumnText(dialect, "rules_json")}, ${jsonColumnText(dialect, "tombstones_json")}, ${jsonColumnText(dialect, "quarantined_json")}, ${jsonColumnText(dialect, "observed_vector_json")}, checksum`;
361
+ }
362
+ function decodeBucketRow(ownerKey, row) {
363
+ const at = `${PERMISSION_RULE_TABLE} bucket ${ownerKey}`;
364
+ const rulesRaw = decodeJsonColumn(row.rules_json, readRulesJson, `${at}.rules_json`);
365
+ const tombstonesRaw = decodeJsonColumn(row.tombstones_json, readTombstonesJson, `${at}.tombstones_json`);
366
+ const quarantinedRaw = decodeJsonColumn(row.quarantined_json, readQuarantinedJson, `${at}.quarantined_json`);
367
+ const observedVector = row.observed_vector_json === null || row.observed_vector_json === undefined ? undefined : decodeJsonColumn(row.observed_vector_json, readObservedVectorJson, `${at}.observed_vector_json`);
368
+ const expected = buildRuleBucketChecksum({ rules: rulesRaw, tombstones: tombstonesRaw, quarantined: quarantinedRaw, ...(observedVector !== undefined ? { observedVector } : {}) });
369
+ if (String(row.checksum ?? "") !== expected) {
370
+ throw new BucketRefusedError(`permission_rule bucket ${ownerKey} failed its integrity check`, `(stored ${String(row.checksum)}, computed ${expected}) — refusing the whole bucket rather than reporting a partial rule set`);
371
+ }
372
+ const rules = rulesRaw.map((r) => ({ ...r, behavior: readBucketRowBehavior(r.behavior, "rule row", ownerKey) }));
373
+ const tombstones = tombstonesRaw.map((t) => ({ ...t, behavior: readBucketRowBehavior(t.behavior, "tombstone", ownerKey) }));
374
+ const quarantined = quarantinedRaw.map((q) => ({ ...q, behavior: readBucketRowBehavior(q.behavior, "quarantined row", ownerKey) }));
375
+ const badTombstone = tombstones.find((t) => {
376
+ const parsed = parseRuleText(t.rule, t.behavior);
377
+ return "reject" in parsed || parsed.rule.rule !== t.rule;
378
+ });
379
+ if (badTombstone !== undefined) {
380
+ throw new BucketRefusedError(`permission_rule bucket ${ownerKey} carries a tombstone whose rule text the single validator refuses or does not spell canonically`, `(${badTombstone.rule}) — refusing the whole bucket rather than keeping a rule alive under a tombstone that can never match it`);
381
+ }
382
+ const screened = screenRuleSyncState({ rules, tombstones });
383
+ if (screened.rejected.length > 0) {
384
+ const names = screened.rejected.map((r) => `${r.rule}(${r.reason})`).join(", ");
385
+ throw new BucketRefusedError(`permission_rule bucket ${ownerKey} carries rows the single validator refuses`, `[${names}] — refusing the whole bucket rather than authorizing a rule whose text and matcher metadata disagree`);
386
+ }
387
+ return {
388
+ actor: String(row.actor ?? ""),
389
+ counter: Number(row.counter ?? 0),
390
+ rev: Number(row.rev ?? 0),
391
+ rules,
392
+ tombstones,
393
+ quarantined,
394
+ ...(observedVector !== undefined ? { observedVector } : {}),
395
+ };
396
+ }
397
+ async function writeBucket(exec, dialect, ownerKey, cur, next, nowMs) {
398
+ const writable = z.object({ rules: z.array(WritablePersistedRuleSchema), tombstones: z.array(WritableRuleTombstoneSchema) }).safeParse(next);
399
+ if (!writable.success) {
400
+ throw new Error(`refusing to write a permission_rule bucket this backend could not read back: ${writable.error.message}`);
401
+ }
402
+ const body = { rules: next.rules, tombstones: next.tombstones, quarantined: cur.quarantined, ...(cur.observedVector !== undefined ? { observedVector: cur.observedVector } : {}) };
403
+ const enc = dialectProtocolJsonEncoder(dialect);
404
+ const affected = await exec(dialect === "tidb"
405
+ ? `UPDATE ${PERMISSION_RULE_TABLE} SET rules_json = ?, tombstones_json = ?, quarantined_json = ?, checksum = ?, rev = rev + 1, updated_at_ms = ? WHERE owner_key = ? AND rev = ?`
406
+ : `UPDATE ${PERMISSION_RULE_TABLE} SET rules_json = $1, tombstones_json = $2, quarantined_json = $3, checksum = $4, rev = rev + 1, updated_at_ms = $5 WHERE owner_key = $6 AND rev = $7`, [
407
+ enc(next.rules, "permission_rule.rules_json"),
408
+ enc(next.tombstones, "permission_rule.tombstones_json"),
409
+ enc(cur.quarantined, "permission_rule.quarantined_json"),
410
+ buildRuleBucketChecksum(body),
411
+ nowMs,
412
+ ownerKey,
413
+ cur.rev,
414
+ ]);
415
+ return affected === 1;
416
+ }
352
417
  class SqlDurableRulePartition {
353
418
  db;
354
419
  owner;
@@ -372,47 +437,11 @@ class SqlDurableRulePartition {
372
437
  return refusingUnreadableColumns(`bucket ${this.ownerKey}`, () => this.readBucketRow());
373
438
  }
374
439
  async readBucketRow() {
375
- const { rows } = await this.db.query(this.q(`SELECT actor, counter, rev, ${jsonColumnText("tidb", "rules_json")}, ${jsonColumnText("tidb", "tombstones_json")}, ${jsonColumnText("tidb", "quarantined_json")}, ${jsonColumnText("tidb", "observed_vector_json")}, checksum FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?`, `SELECT actor, counter, rev, ${jsonColumnText("pg", "rules_json")}, ${jsonColumnText("pg", "tombstones_json")}, ${jsonColumnText("pg", "quarantined_json")}, ${jsonColumnText("pg", "observed_vector_json")}, checksum FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`), [this.ownerKey]);
440
+ const { rows } = await this.db.query(this.q(`SELECT ${bucketRowColumns("tidb")} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?`, `SELECT ${bucketRowColumns("pg")} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`), [this.ownerKey]);
376
441
  const row = rows[0];
377
442
  if (row === undefined)
378
443
  return { ...EMPTY_BUCKET, rules: [], tombstones: [], quarantined: [] };
379
- const at = `${PERMISSION_RULE_TABLE} bucket ${this.ownerKey}`;
380
- const rulesRaw = decodeJsonColumn(row.rules_json, readRulesJson, `${at}.rules_json`);
381
- const tombstonesRaw = decodeJsonColumn(row.tombstones_json, readTombstonesJson, `${at}.tombstones_json`);
382
- const quarantinedRaw = decodeJsonColumn(row.quarantined_json, readQuarantinedJson, `${at}.quarantined_json`);
383
- const observedVector = row.observed_vector_json === null || row.observed_vector_json === undefined ? undefined : decodeJsonColumn(row.observed_vector_json, readObservedVectorJson, `${at}.observed_vector_json`);
384
- const expected = buildRuleBucketChecksum({ rules: rulesRaw, tombstones: tombstonesRaw, quarantined: quarantinedRaw, ...(observedVector !== undefined ? { observedVector } : {}) });
385
- if (String(row.checksum ?? "") !== expected) {
386
- throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
387
- `refusing the whole bucket rather than reporting a partial rule set`);
388
- }
389
- const rules = rulesRaw.map((r) => ({ ...r, behavior: readBucketRowBehavior(r.behavior, "rule row", this.ownerKey) }));
390
- const tombstones = tombstonesRaw.map((t) => ({ ...t, behavior: readBucketRowBehavior(t.behavior, "tombstone", this.ownerKey) }));
391
- const quarantined = quarantinedRaw.map((q) => ({ ...q, behavior: readBucketRowBehavior(q.behavior, "quarantined row", this.ownerKey) }));
392
- const state = {
393
- actor: String(row.actor ?? ""),
394
- counter: Number(row.counter ?? 0),
395
- rev: Number(row.rev ?? 0),
396
- rules,
397
- tombstones,
398
- quarantined,
399
- ...(observedVector !== undefined ? { observedVector } : {}),
400
- };
401
- const badTombstone = tombstones.find((t) => {
402
- const parsed = parseRuleText(t.rule, t.behavior);
403
- return "reject" in parsed || parsed.rule.rule !== t.rule;
404
- });
405
- if (badTombstone !== undefined) {
406
- throw new Error(`permission_rule bucket ${this.ownerKey} carries a tombstone whose rule text the single validator refuses or does not spell canonically (${badTombstone.rule}) — ` +
407
- `refusing the whole bucket rather than keeping a rule alive under a tombstone that can never match it`);
408
- }
409
- const screened = screenRuleSyncState({ rules, tombstones });
410
- if (screened.rejected.length > 0) {
411
- const names = screened.rejected.map((r) => `${r.rule}(${r.reason})`).join(", ");
412
- throw new Error(`permission_rule bucket ${this.ownerKey} carries rows the single validator refuses [${names}] — ` +
413
- `refusing the whole bucket rather than authorizing a rule whose text and matcher metadata disagree`);
414
- }
415
- return state;
444
+ return decodeBucketRow(this.ownerKey, row);
416
445
  }
417
446
  async ensureBucket() {
418
447
  const nowMs = this.now();
@@ -500,22 +529,8 @@ class SqlDurableRulePartition {
500
529
  throw new Error(`the SQL permission-rule backend was handed a write delta it does not know: ${JSON.stringify(unknown)}`);
501
530
  }
502
531
  }
503
- const writable = z.object({ rules: z.array(WritablePersistedRuleSchema), tombstones: z.array(WritableRuleTombstoneSchema) }).safeParse({ rules: nextRules, tombstones: nextTombstones });
504
- if (!writable.success) {
505
- throw new Error(`refusing to write a permission_rule bucket this backend could not read back: ${writable.error.message}`);
506
- }
507
- const body = { rules: nextRules, tombstones: nextTombstones, quarantined: cur.quarantined, ...(cur.observedVector !== undefined ? { observedVector: cur.observedVector } : {}) };
508
- const nowMs = this.now();
509
- const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TABLE} SET rules_json = ?, tombstones_json = ?, quarantined_json = ?, checksum = ?, rev = rev + 1, updated_at_ms = ? WHERE owner_key = ? AND rev = ?`, `UPDATE ${PERMISSION_RULE_TABLE} SET rules_json = $1, tombstones_json = $2, quarantined_json = $3, checksum = $4, rev = rev + 1, updated_at_ms = $5 WHERE owner_key = $6 AND rev = $7`), [
510
- this.enc(nextRules, "permission_rule.rules_json"),
511
- this.enc(nextTombstones, "permission_rule.tombstones_json"),
512
- this.enc(cur.quarantined, "permission_rule.quarantined_json"),
513
- buildRuleBucketChecksum(body),
514
- nowMs,
515
- this.ownerKey,
516
- opts.expectedRev,
517
- ]);
518
- if (affected === 0) {
532
+ const landed = await writeBucket(async (sql, params) => (await this.db.query(sql, params)).affected, this.db.dialect, this.ownerKey, cur, { rules: nextRules, tombstones: nextTombstones }, this.now());
533
+ if (!landed) {
519
534
  const again = await this.read();
520
535
  return { conflict: true, rev: again.rev };
521
536
  }
@@ -766,6 +781,98 @@ export class SqlRuleImportTicketStore {
766
781
  }
767
782
  export const RULE_PENDING_APPROVAL_RETENTION_MS = 24 * 60 * 60_000;
768
783
  export const RULE_REAP_BATCH = 500;
784
+ const UNDURABLE_PURGE_PAGE = 200;
785
+ const UNDURABLE_PURGE_OCC_ATTEMPTS = 5;
786
+ export async function purgeUndurableScopeRows(db, dialect, now = Date.now) {
787
+ const acc = { removed: [], bucketsDeferred: 0, bucketsUnreadable: [] };
788
+ try {
789
+ await purgeAllBuckets(db, dialect, now, acc);
790
+ }
791
+ catch (e) {
792
+ if (acc.removed.length === 0)
793
+ throw e;
794
+ throw new Error(`the one-time removal of persisted permission rules whose scope the engine refuses failed AFTER removing ${acc.removed.length} row(s) — record them before retrying: ` +
795
+ acc.removed.map((r) => `${r.kind} in bucket ${r.ownerKey} (${r.behavior} ${r.tool}, scope root ${JSON.stringify(r.scopeRoot)})`).join("; ") +
796
+ `. Underlying failure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
797
+ }
798
+ return acc;
799
+ }
800
+ async function purgeAllBuckets(db, dialect, now, acc) {
801
+ const cols = `owner_key, ${bucketRowColumns(dialect)}`;
802
+ let after = "";
803
+ for (;;) {
804
+ const page = await db.rows(dialect === "tidb"
805
+ ? `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key > ? ORDER BY owner_key LIMIT ${UNDURABLE_PURGE_PAGE}`
806
+ : `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key > $1 ORDER BY owner_key LIMIT ${UNDURABLE_PURGE_PAGE}`, [after]);
807
+ for (const first of page) {
808
+ const ownerKey = String(first.owner_key);
809
+ after = ownerKey;
810
+ let row = first;
811
+ let settled = false;
812
+ for (let attempt = 0; attempt < UNDURABLE_PURGE_OCC_ATTEMPTS && row !== undefined && !settled; attempt++) {
813
+ const outcome = await purgeOneBucket(db, dialect, ownerKey, row, now);
814
+ switch (outcome.kind) {
815
+ case "purged":
816
+ acc.removed.push(...outcome.rows);
817
+ settled = true;
818
+ break;
819
+ case "unreadable":
820
+ acc.bucketsUnreadable.push({ ownerKey, why: outcome.why });
821
+ settled = true;
822
+ break;
823
+ case "clean":
824
+ settled = true;
825
+ break;
826
+ case "conflict":
827
+ row = (await db.rows(dialect === "tidb" ? `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?` : `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`, [ownerKey]))[0];
828
+ if (row === undefined)
829
+ settled = true;
830
+ break;
831
+ default: {
832
+ const unknown = outcome;
833
+ throw new Error(`M11: unhandled bucket outcome ${JSON.stringify(unknown)}`);
834
+ }
835
+ }
836
+ }
837
+ if (!settled)
838
+ acc.bucketsDeferred += 1;
839
+ }
840
+ if (page.length < UNDURABLE_PURGE_PAGE)
841
+ break;
842
+ }
843
+ }
844
+ async function purgeOneBucket(db, dialect, ownerKey, row, now) {
845
+ let cur;
846
+ try {
847
+ cur = decodeBucketRow(ownerKey, row);
848
+ }
849
+ catch (e) {
850
+ return { kind: "unreadable", why: contentFreeRefusal(ownerKey, e) };
851
+ }
852
+ const badRules = cur.rules.filter((r) => !isValidDurableScope(r.scope));
853
+ const badTombstones = cur.tombstones.filter((t) => !isValidDurableScope(t.scope));
854
+ if (badRules.length === 0 && badTombstones.length === 0)
855
+ return { kind: "clean" };
856
+ const rootOf = (scope) => redactSecrets(scope.kind === "project" ? scope.root : scope.kind);
857
+ const rows = [
858
+ ...badRules.map((r) => ({ ownerKey, kind: applyTombstones([r], cur.tombstones).length > 0 ? "rule" : "revoked-rule", behavior: r.behavior, tool: r.tool, scopeRoot: rootOf(r.scope) })),
859
+ ...badTombstones.map((t) => {
860
+ const parsed = parseRuleText(t.rule, t.behavior);
861
+ if ("reject" in parsed)
862
+ throw new Error(`permission_rule bucket ${ownerKey}: a tombstone the bucket judge admitted does not parse here — refusing to touch the bucket`);
863
+ return { ownerKey, kind: "tombstone", behavior: t.behavior, tool: parsed.rule.tool, scopeRoot: rootOf(t.scope) };
864
+ }),
865
+ ];
866
+ const landed = await writeBucket((sql, params) => db.exec(sql, params), dialect, ownerKey, cur, { rules: cur.rules.filter((r) => !badRules.includes(r)), tombstones: cur.tombstones.filter((t) => !badTombstones.includes(t)) }, now());
867
+ return landed ? { kind: "purged", rows } : { kind: "conflict" };
868
+ }
869
+ function contentFreeRefusal(ownerKey, e) {
870
+ if (e instanceof BucketRefusedError)
871
+ return e.verdict;
872
+ if (e instanceof SqlJsonColumnError)
873
+ return `${e.where}: ${e.problem}`;
874
+ return `permission_rule bucket ${ownerKey}: the bucket judge threw ${e instanceof Error ? e.name : typeof e} (its text is withheld here: it may quote the row)`;
875
+ }
769
876
  export function createSqlPermissionRuleStores(db, now) {
770
877
  const q = (tidb, pg) => (db.dialect === "tidb" ? tidb : pg);
771
878
  return {
@@ -152,6 +152,12 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
152
152
  private absentGuardFaces;
153
153
  private driftError;
154
154
  private resolve;
155
+ /**
156
+ * exec / execStream 指令帧的 `cwd` —— **恒填、恒绝对**的唯一铸点(协议文档 §3.1:executor 不再有「缺席时按
157
+ * 隐含工作目录」的第二种读法)。调用方没给 ⇒ env 的 cwd(铸造时已由 {@link effectiveDeviceCwd} 保证绝对);
158
+ * 给了 ⇒ 按同一只 {@link resolve} 解成绝对。其余指令 kind 的路径参数在下发前就已是绝对路径,不带 `cwd`。
159
+ */
160
+ private instructionCwd;
155
161
  absolutePath(p: string): Promise<Result<string, FileError>>;
156
162
  joinPath(parts: string[]): Promise<Result<string, FileError>>;
157
163
  workspaceHandle(): WorkspaceHandle;
@@ -321,8 +327,13 @@ export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): E
321
327
  * §4.3.3 的 cwd 优先序 —— 与 host 腿的 `effectiveHostWorkspace` **同源**:
322
328
  * ① 本 session 显式注册的 cwd(`deviceCwdHonored` 闸下由 resolve-spec 写);
323
329
  * ② `ctx.parentCwd`(委派子代:自己没注册过 cwd,继承父的工作目录 —— 少了它子代会丢工作目录);
324
- * ③ 设备 hello 声明并落库的 `workspaceRoot`(缺 cwd 时的解析基准,有明确 wire 契约,不是「executor
330
+ * ③ 设备入籍时登记并落库的 `workspaceRoot`(缺 cwd 时的解析基准,有明确 wire 契约,不是「executor
325
331
  * 默认根」这种空话)。
332
+ *
333
+ * 🔴 **这一只解析源同时守它的后置条件:结果必是 POSIX 绝对路径,否则响亮抛。** 协议文档对 executor 的承诺是
334
+ * 「exec / execStream 的指令帧恒带绝对 `cwd`」—— 不带或带相对的,都会把「用哪个目录」这件事留给 executor
335
+ * 自己猜(同一个事实两种读法)。判据是入籍口那**同一只**谓词 `isDeviceAbsolutePath`(`device-store.ts`):入籍口用它拒相对的
336
+ * `workspaceRoot`;这里的判留作存量行守卫(入籍口收紧之前落库的相对根)并守另两个来源 —— 委派链的父 cwd 来自引擎,不经入籍口。
326
337
  */
327
338
  export declare function effectiveDeviceCwd(sessionCwd: string | undefined, parentCwd: string | undefined, row: Pick<DeviceRow, "workspaceRoot">): string;
328
339
  export {};
@@ -1,11 +1,10 @@
1
1
  import { posix } from "node:path";
2
2
  import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
3
- import { DEVICE_ERROR_CODES, } from "../device-store.js";
3
+ import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
4
4
  export const DEVICE_PROVIDER = "device";
5
5
  const ok = (value) => ({ ok: true, value });
6
6
  const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
7
7
  const STREAM_PENDING_MAX_ENTRIES = 8192;
8
- const VERIFY_FIRST = "may have already executed on the target; verify its effect before re-running";
9
8
  export const DEVICE_OUTCOME_TO_FILE_ERROR = {
10
9
  aborted: "aborted",
11
10
  already_exists: "already_exists",
@@ -166,7 +165,10 @@ export class RemoteDeviceExecutionEnv {
166
165
  "performing a NON-atomic write here is exactly the silent downgrade guarded writes exist to prevent (#157). Refusing; the per-task factory mints a correctly-shaped env on the next task.", absPath);
167
166
  }
168
167
  resolve(p) {
169
- return posix.isAbsolute(p) ? posix.normalize(p) : posix.normalize(posix.join(this.cwd, p));
168
+ return isDeviceAbsolutePath(p) ? posix.normalize(p) : posix.normalize(posix.join(this.cwd, p));
169
+ }
170
+ instructionCwd(requested) {
171
+ return requested === undefined ? this.cwd : this.resolve(requested);
170
172
  }
171
173
  async absolutePath(p) {
172
174
  return ok(this.resolve(p));
@@ -354,7 +356,7 @@ export class RemoteDeviceExecutionEnv {
354
356
  const res = await this.dispatch("exec", { command }, {
355
357
  timeoutMs: this.timeoutMsFor(options?.timeout),
356
358
  ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }),
357
- cwd: options?.cwd === undefined ? this.cwd : this.resolve(options.cwd),
359
+ cwd: this.instructionCwd(options?.cwd),
358
360
  ...(mergedEnv(this.cfg.shellEnv, options?.env) === undefined ? {} : { shellEnv: mergedEnv(this.cfg.shellEnv, options?.env) }),
359
361
  onChunk: (c) => {
360
362
  if (c.kind === "stdout") {
@@ -430,7 +432,7 @@ export class RemoteDeviceExecutionEnv {
430
432
  const settled = this.dispatch("execStream", { command }, {
431
433
  timeoutMs: this.timeoutMsFor(options?.timeout),
432
434
  signal: inner.signal,
433
- cwd: options?.cwd === undefined ? this.cwd : this.resolve(options.cwd),
435
+ cwd: this.instructionCwd(options?.cwd),
434
436
  ...(mergedEnv(this.cfg.shellEnv, options?.env) === undefined ? {} : { shellEnv: mergedEnv(this.cfg.shellEnv, options?.env) }),
435
437
  onChunk: (c) => {
436
438
  if (!accepting) {
@@ -811,6 +813,11 @@ export function deviceExecutionEnvFactory(deps) {
811
813
  };
812
814
  }
813
815
  export function effectiveDeviceCwd(sessionCwd, parentCwd, row) {
814
- return sessionCwd ?? parentCwd ?? row.workspaceRoot;
816
+ const [source, cwd] = sessionCwd !== undefined ? ["session cwd", sessionCwd] : parentCwd !== undefined ? ["parent cwd", parentCwd] : ["device workspaceRoot", row.workspaceRoot];
817
+ if (!isDeviceAbsolutePath(cwd)) {
818
+ throw new Error(`device lane: the effective working directory (${source}) ${JSON.stringify(cwd)} is not an absolute path — every exec / execStream instruction carries an absolute \`cwd\`, ` +
819
+ "and the executor must never be left to guess a base directory. Fix the source named above (a device's workspaceRoot is fixed at enrollment: enroll again with an absolute path)");
820
+ }
821
+ return cwd;
815
822
  }
816
823
  //# sourceMappingURL=remote-env-device.js.map
@@ -26,8 +26,11 @@ type Caps = SchedulerCapability["schedulerCapabilities"];
26
26
  * cron via core `isValidCronExpr` (RCE-safe, no shell-exec); delay/at bounded by minDelaySec + horizon.
27
27
  */
28
28
  export declare function validateIntent(intent: ScheduledIntent, caps: Caps, nowMs: number): SchedulerError | null;
29
- /** The default shared store path: `SEMA_CONFIG_DIR` → `~/.sema`, file `scheduled_tasks.json` (server + shell agree). */
30
- export declare function defaultSchedulerStorePath(): string;
29
+ /** The default shared store path: the configuration root (the `agentConfigDir` seat `loadConfig` minted from `SEMA_CONFIG_DIR`, else `~/.sema`),
30
+ * file `scheduled_tasks.json` (server + shell agree). Reads the seat, never `process.env` at call time. */
31
+ export declare function defaultSchedulerStorePath(config: {
32
+ readonly agentConfigDir?: string;
33
+ }): string;
31
34
  /**
32
35
  * File-backed SchedulerCapability (durable across daemon restarts). Store = a single JSON file (atomic write via
33
36
  * the registry-core /node binding). The resident shell daemon reads the same store to fire intents. `now`
@@ -37,8 +40,9 @@ export declare class FileSchedulerBackend implements SchedulerCapability {
37
40
  readonly schedulerCapabilities: Caps;
38
41
  private readonly storePath;
39
42
  private readonly now;
40
- constructor(opts?: {
41
- storePath?: string;
43
+ /** `storePath` 必填:路径是装配层从配置定的事实(`SCHEDULER_STORE_PATH` ?? {@link defaultSchedulerStorePath}),本类不自己找缺省。 */
44
+ constructor(opts: {
45
+ storePath: string;
42
46
  caps?: Partial<Caps>;
43
47
  now?: () => number;
44
48
  });
@@ -1,8 +1,8 @@
1
1
  import { join } from "node:path";
2
- import { homedir } from "node:os";
3
2
  import { randomBytes, createHash } from "node:crypto";
4
3
  import { SchedulerError, isValidCronExpr } from "@sema-agent/core";
5
4
  import { loadSchedulerStore, saveSchedulerStore } from "@sema-agent/settings-schema/node";
5
+ import { configRootOf } from "../sema-config-dir.js";
6
6
  const ok = (value) => ({ ok: true, value });
7
7
  const err = (error) => ({ ok: false, error });
8
8
  export const DEFAULT_SCHEDULER_CAPS = {
@@ -59,16 +59,16 @@ function computeNextRunMs(when, nowMs) {
59
59
  return nowMs + when.delaySec * 1000;
60
60
  return undefined;
61
61
  }
62
- export function defaultSchedulerStorePath() {
63
- return join(process.env.SEMA_CONFIG_DIR ?? join(homedir(), ".sema"), "scheduled_tasks.json");
62
+ export function defaultSchedulerStorePath(config) {
63
+ return join(configRootOf(config), "scheduled_tasks.json");
64
64
  }
65
65
  export class FileSchedulerBackend {
66
66
  schedulerCapabilities;
67
67
  storePath;
68
68
  now;
69
- constructor(opts = {}) {
69
+ constructor(opts) {
70
70
  this.schedulerCapabilities = { ...DEFAULT_SCHEDULER_CAPS, ...(opts.caps ?? {}) };
71
- this.storePath = opts.storePath ?? defaultSchedulerStorePath();
71
+ this.storePath = opts.storePath;
72
72
  this.now = opts.now ?? (() => Date.now());
73
73
  }
74
74
  load() {
@@ -23,7 +23,7 @@ const TASK_BODY_DECLARED_KEYS = [
23
23
  "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "disableSkills", "enableFork",
24
24
  "excludeAllTools", "excludeTools", "fileHistory",
25
25
  "finalVerification", "forwardSubagentEvents", "images", "inheritEnv", "interactiveTools", "jobId", "limits", "maxCostUsd",
26
- "maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
26
+ "maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries", "outputStyle",
27
27
  "outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
28
28
  "resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
29
29
  "rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
@@ -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}`;
@@ -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,21 @@ 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 的**唯一一句**拒句:写 / 撤 / 列举过滤三个路由口、local-import 的成员拒因 `scope_unreadable` 都出站这一只
514
+ * (各口的码照旧各自的),不在读点各抄一份。cc-import 预览的层 root 判据是同一只(`isValidDurableScope`),但那份体里没有 scope 字段,
515
+ * 拒句另由 {@link buildCcImportLayerRootRefusal} 铸。 */
516
+ export declare const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
517
+ /** cc-import 预览里第 `layerIndex` 层的 root 不是引擎收的绝对路径时的拒句 —— 点名这份体里真有的字段与层号(体里没有 scope 字段,
518
+ * 不回 {@link RULE_SCOPE_REFUSAL_SENTENCE})。判据与上面三个入口是同一只谓词,所以「absolute」是同一个词;句子只此一处铸,路由不手拼。 */
519
+ export declare function buildCcImportLayerRootRefusal(layerIndex: number): string;
507
520
  /** 一条规则在 wire 上的行 = 一个逻辑 (rule, scope) 对([ref] v2 §6 F5)。
508
521
  * `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
509
522
  * 折成「一个 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,8 +57,12 @@ 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
+ }
63
+ export const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
64
+ export function buildCcImportLayerRootRefusal(layerIndex) {
65
+ return `layers[${layerIndex}].root must be an absolute path`;
62
66
  }
63
67
  export function sameWireRuleIdentity(row, identity) {
64
68
  return row.behavior === identity.behavior && row.rule === identity.rule && row.scope === identity.scope;
@@ -163,7 +167,7 @@ function anchorBatchMembers(members, displayed) {
163
167
  function screenLocalImportRow(row) {
164
168
  const scope = parseRuleScope(row.scope);
165
169
  if (scope === undefined) {
166
- return { reason: "scope_unreadable", detail: "scope must be 'global' or 'project:<root>' with a non-empty root" };
170
+ return { reason: "scope_unreadable", detail: RULE_SCOPE_REFUSAL_SENTENCE };
167
171
  }
168
172
  if (scope.kind === "project" && hasBidiControls(scope.root)) {
169
173
  return {
package/dist/run-local.js CHANGED
@@ -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;
@@ -325,6 +326,7 @@ export async function runLocal(argv, deps = {}) {
325
326
  const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
326
327
  installSecretEnvScrubRecorder({ logger, metrics });
327
328
  logConfigDiagnostics(logger);
329
+ provisionSemaConfigDir(config, logger);
328
330
  installOutboundDispatcher(logger);
329
331
  const providerInput = { provider: config.configProvider ?? "local", localDir: root };
330
332
  try {
package/dist/runs.js CHANGED
@@ -423,7 +423,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
423
423
  await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
424
424
  }
425
425
  };
426
- await instructionLeg.run(spec.sessionId, () => withModelReachScope(spec.sessionId, () => withPrincipal(principal, () => {
426
+ await instructionLeg.run(spec.sessionId, () => withModelReachScope({ sessionId: spec.sessionId, taskId }, () => withPrincipal(principal, () => {
427
427
  const withElicit = elicitation
428
428
  ? () => elicitation.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: elicitEmit, abortSignal: cancelCtrl.signal }, runBody)
429
429
  : runBody;
@@ -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