@sema-agent/server 7.66.0 → 7.67.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 (38) hide show
  1. package/USAGE.md +6 -3
  2. package/dist/approval-card.d.ts +8 -0
  3. package/dist/approval.d.ts +3 -0
  4. package/dist/approval.js +2 -1
  5. package/dist/boot/resolve-spec.js +12 -5
  6. package/dist/config-catalog.js +2 -2
  7. package/dist/config-center/apply-effective.js +1 -1
  8. package/dist/config-types.d.ts +15 -2
  9. package/dist/config.d.ts +25 -3
  10. package/dist/config.js +58 -30
  11. package/dist/deployment-governance.d.ts +8 -0
  12. package/dist/http/routes/diagnostics.d.ts +27 -2
  13. package/dist/http/routes/diagnostics.js +21 -1
  14. package/dist/http/routes/rules.js +15 -10
  15. package/dist/http/routes/tasks.js +3 -8
  16. package/dist/http/server.js +3 -0
  17. package/dist/observability/fail-open.d.ts +4 -0
  18. package/dist/observability/fail-open.js +4 -0
  19. package/dist/observability/metrics.js +1 -1
  20. package/dist/permission-rule-vocab.d.ts +13 -21
  21. package/dist/permission-rule-vocab.js +6 -5
  22. package/dist/plugins/permission-rule-store-file.d.ts +3 -5
  23. package/dist/plugins/permission-rule-store-file.js +2 -1
  24. package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
  25. package/dist/plugins/permission-rule-store-sql.js +35 -20
  26. package/dist/posture-source.d.ts +57 -0
  27. package/dist/posture-source.js +27 -0
  28. package/dist/read-face-posture.d.ts +9 -4
  29. package/dist/read-face-posture.js +7 -11
  30. package/dist/rules-consent.d.ts +15 -10
  31. package/dist/rules-consent.js +41 -21
  32. package/dist/session-titler.js +3 -12
  33. package/dist/task-settings.d.ts +30 -0
  34. package/dist/task-settings.js +39 -18
  35. package/dist/tool-approval.d.ts +5 -4
  36. package/dist/tool-approval.js +2 -2
  37. package/dist/trace/project.d.ts +4 -3
  38. package/package.json +3 -3
@@ -5,8 +5,8 @@ import { createLogger } from "../../observability/logger.js";
5
5
  import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
6
6
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
7
7
  import { HttpError } from "../../security.js";
8
- import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
9
- import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
8
+ import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
9
+ import { PERSISTED_RULE_MATCHES, isPersistedRuleTool } from "../../permission-rule-vocab.js";
10
10
  const logger = createLogger();
11
11
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
12
12
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -46,7 +46,8 @@ const LocalImportRowSchema = z
46
46
  .object({
47
47
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
48
48
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
49
- tool: z.enum(PERSISTED_RULE_TOOLS),
49
+ behavior: z.enum(RULE_BEHAVIORS),
50
+ tool: z.string().min(1).max(190).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
50
51
  match: z.enum(PERSISTED_RULE_MATCHES),
51
52
  command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
52
53
  adds: z.array(LocalImportAddSchema).max(64),
@@ -58,16 +59,18 @@ const LocalImportPrepareBodySchema = z.object({ rows: z.array(LocalImportRowSche
58
59
  const LocalImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
59
60
  const RuleRevokeBodySchema = z
60
61
  .object({
62
+ behavior: z.enum(RULE_BEHAVIORS),
61
63
  rule: z.string().min(1).max(1024),
62
64
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
63
65
  principal: z.string().min(1).max(190).optional(),
64
66
  })
65
67
  .strict();
68
+ const RULES_CURSOR_FORMAT = 2;
66
69
  const RulesCursorSchema = z
67
- .object({ rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
70
+ .object({ v: z.literal(RULES_CURSOR_FORMAT), rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
68
71
  .strict();
69
72
  function encodeRulesCursor(c) {
70
- return Buffer.from(JSON.stringify(c)).toString("base64url");
73
+ return Buffer.from(JSON.stringify({ v: RULES_CURSOR_FORMAT, ...c })).toString("base64url");
71
74
  }
72
75
  function decodeRulesCursor(raw) {
73
76
  try {
@@ -78,7 +81,7 @@ function decodeRulesCursor(raw) {
78
81
  return { ok: false, why: err instanceof Error ? err.message : String(err) };
79
82
  }
80
83
  }
81
- const rowKey = (r) => `${r.scope}\u0000${r.rule}`;
84
+ const rowKey = (r) => `${r.scope}\u0000${r.rule}\u0000${r.behavior}`;
82
85
  export async function handleRules(req, res, url, ctx) {
83
86
  const miss = { fell: false };
84
87
  await handleRulesBody(req, res, url, ctx, miss);
@@ -132,7 +135,7 @@ async function handleRulesBody(req, res, url, ctx, miss) {
132
135
  if (isLocalPrepare) {
133
136
  const parsed = LocalImportPrepareBodySchema.safeParse(rawBody);
134
137
  if (!parsed.success) {
135
- sendError(res, 400, "request.body_shape", "body must be {rows:[{rule,scope,tool,match,command,adds}]}");
138
+ sendError(res, 400, "request.body_shape", "body must be {rows:[{behavior,rule,scope,tool,match,command,adds}]}");
136
139
  return;
137
140
  }
138
141
  const prepared = await lane.prepareLocalImport(principal, parsed.data.rows);
@@ -241,7 +244,9 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
241
244
  sendError(res, 400, "request.query_invalid", "the rules cursor was minted at a different revision — restart the listing", { rev: listing.rev });
242
245
  return;
243
246
  }
244
- const matching = listing.rows.filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after));
247
+ const matching = listing.rows
248
+ .filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after))
249
+ .sort((x, y) => (rowKey(x) < rowKey(y) ? -1 : rowKey(x) > rowKey(y) ? 1 : 0));
245
250
  const page = matching.slice(0, limit);
246
251
  const last = page[page.length - 1];
247
252
  const nextCursor = matching.length > limit && last !== undefined
@@ -252,7 +257,7 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
252
257
  async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals) {
253
258
  const parsed = RuleRevokeBodySchema.safeParse(rawBody);
254
259
  if (!parsed.success) {
255
- sendError(res, 400, "request.body_shape", "body must be {rule, scope} with an optional principal");
260
+ sendError(res, 400, "request.body_shape", "body must be {behavior, rule, scope} with an optional principal");
256
261
  return;
257
262
  }
258
263
  const scope = parseRuleScope(parsed.data.scope);
@@ -265,7 +270,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
265
270
  sendError(res, 403, "auth.operator_only", "revoking another principal's rules is an operator-only action");
266
271
  return;
267
272
  }
268
- const outcome = await lane.removeRule({ principal: target.principal, rule: parsed.data.rule, scope });
273
+ const outcome = await lane.removeRule({ principal: target.principal, behavior: parsed.data.behavior, rule: parsed.data.rule, scope });
269
274
  switch (outcome.status) {
270
275
  case "removed":
271
276
  sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
@@ -231,20 +231,15 @@ async function handleTasksBody(req, res, url, ctx, miss) {
231
231
  markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
232
232
  let ledgerSink;
233
233
  let ledgerTerminal = false;
234
- if (detachOnDisconnect && durableTaskId && deps.runStore) {
235
- const rs = deps.runStore;
236
- const tid = durableTaskId;
237
- ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
238
- usageKey = durableTaskId;
239
- if (usageKey)
240
- deps.modelUsage?.register(usageKey);
241
- }
242
234
  if (deps.checkpointStore && prepared.spec.sessionId) {
243
235
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
244
236
  }
245
237
  if (durableTaskId && deps.runStore) {
246
238
  const rs = deps.runStore;
247
239
  const tid = durableTaskId;
240
+ ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
241
+ usageKey = tid;
242
+ deps.modelUsage?.register(tid);
248
243
  const owner = principal ?? null;
249
244
  const abortFromVerb = () => {
250
245
  cancelledViaVerb.add(tid);
@@ -115,6 +115,7 @@ export function validateUserSkills(skills) {
115
115
  }
116
116
  return null;
117
117
  }
118
+ const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
118
119
  const MAX_BODY = 8 * 1024 * 1024;
119
120
  const MAX_IMAGES_PER_REQUEST = 20;
120
121
  const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
@@ -323,6 +324,7 @@ export function createHttpServer(rawDeps) {
323
324
  ...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
324
325
  configHash,
325
326
  pid: process.pid,
327
+ startedAt: PROCESS_STARTED_AT_MS,
326
328
  port: deps.config.port,
327
329
  ...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
328
330
  ...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
@@ -2434,6 +2436,7 @@ const WORKER_SWAP_REDEEMABLE = {
2434
2436
  env_factory_missing: true,
2435
2437
  governed_unwired: true,
2436
2438
  origin_missing: true,
2439
+ ledger_event_unknown: true,
2437
2440
  real_approval_damaged: false,
2438
2441
  real_approval_forged: false,
2439
2442
  read_face_damaged: false,
@@ -69,6 +69,10 @@ export declare const FAIL_OPEN_TAGS: {
69
69
  readonly cls: "F";
70
70
  readonly note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。";
71
71
  };
72
+ readonly "server.session-titler.gateway-unreachable": {
73
+ readonly cls: "F";
74
+ readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
75
+ };
72
76
  readonly "server.prompts.artifact-cache-write-failed": {
73
77
  readonly cls: "F";
74
78
  readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
@@ -60,6 +60,10 @@ export const FAIL_OPEN_TAGS = {
60
60
  cls: "F",
61
61
  note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。",
62
62
  },
63
+ "server.session-titler.gateway-unreachable": {
64
+ cls: "F",
65
+ note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
66
+ },
63
67
  "server.prompts.artifact-cache-write-failed": {
64
68
  cls: "F",
65
69
  note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
@@ -260,7 +260,7 @@ export function createMetrics() {
260
260
  m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
261
261
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
262
262
  m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
263
- m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
263
+ m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
264
264
  m.counter("permission_rule_events_total", "Persisted allow-rule seam events by event (persisted_rule_allowed = a rule authorized a call with nobody asked; rule_store_unreadable = the store failed to read so the call was adjudicated with ZERO rules)");
265
265
  m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
266
266
  m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
@@ -1,29 +1,21 @@
1
+ import type { RuleApprovalRecord } from "@sema-agent/core";
1
2
  /**
2
- * [ref] + [ref](core `e298c9e0`,随 7.2.x 提货生效;[ref])—— 规则车道**闭词表的单一属主**。
3
- *
4
- * 为什么要一个模块而不是各处各写一份 `z.enum([...])`:同一张词表此前在四处各抄一份字面量
5
- * (`approval-card.ts` 两腿的 offer schema、`http/routes/rules.ts` 的直通票口行形、
6
- * `plugins/permission-rule-store-sql.ts` 的 `rules_json` 读/写形),core 给 `PersistedRuleMatch` 加两员
7
- * (wildcard / subpath)、给 `PersistedRuleTool` 加 Read 时,四份抄件**没有一份**会编译红 —— zod 的 enum
8
- * 只是运行期的一个 Set,静默把一次上游语义扩张翻成一整类合法行的拒收([ref] 词表纪律说的正是这形:
9
- * 未知词必须是**编译**错,不是运行期一个安静的 false)。
3
+ * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
+ * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
5
+ * 「转录一份名单」换成「转 core 的**谓词**」——名单在 core 的目录里,抄一份的那天目录加一个带
6
+ * `pathTarget` 的工具,本仓会静默拒收一整类合法行(正是本文件顶注说的那个病)。
10
7
  *
11
- * 🔴 每张表都带一条**双向全等钉**(`Exact<>`):元组的成员并 ≡ core 的联合型。core 加员 ⇒ 这里编译红,
12
- * 由人裁定新词在每个消费面的处置(收 / 拒 / 剥),而不是让它在某一处静默落进「不认识」臂;本仓多写一个
13
- * core 没有的词 ⇒ 同样红(词表属主是 core,本仓只转录)。
14
- *
15
- * ⚠️ 转录**不是**判据:一个词在表里只说「这是 core 定义的一员」,不说「本仓每条腿都接受它」。逐腿的收窄
16
- * (例如卡编辑臂对 `subpath` 的拒)写在那条腿自己的穷举 `switch` 里,并各带理由。
8
+ * 谓词方向仍是 fail-CLOSED:`ruleToolGrammarOf` 认不出的名字**不是**一条可持久化规则的工具位
9
+ * (既非命令文法也非路径文法 ⇒ 这条行永远匹配不上任何调用),两只店与票口一律拒。
17
10
  */
18
- import type { RuleApprovalRecord } from "@sema-agent/core";
19
- /** `PersistedRuleTool`([ref] §2.2:`"Read"` 只说一种文法 —— `Read(//abs-dir/**)` 目录只读授权)。 */
20
- export declare const PERSISTED_RULE_TOOLS: readonly ["Bash", "Read"];
11
+ export declare function isPersistedRuleTool(name: string): boolean;
21
12
  /**
22
- * `PersistedRuleMatch` 四员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
13
+ * `PersistedRuleMatch` 五员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
23
14
  * 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
24
- * `subpath`(Read 目录形,永不容纳任何命令)。
15
+ * `subpath`(Read 目录形,永不容纳任何命令)/ `path`(core 7.9.0 [ref]:路径族 deny/ask/allow 的模式文法
16
+ * —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)。
25
17
  */
26
- export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath"];
18
+ export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path"];
27
19
  /** [ref] §2.3 B3:batch offer 成员的判别位(`command` = 历史逐段 Bash 规则;`directoryRead` = cd 段铸的目录只读授权)。 */
28
20
  export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
29
21
  /** [ref] §3.5:一段「为什么仍未被覆盖」的闭三词集(`redirection` 永久逐次 / `no_rule_form` 无可覆盖形 / `cap_overflow` 去重+帽挤出)。 */
@@ -31,7 +23,7 @@ export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rul
31
23
  /** [ref] §3.3-5:卡编辑面的 breadth 警告码(记录 `edited.warnings` 存的就是这两个字面)。 */
32
24
  export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
33
25
  /**
34
- * [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 2→3)。
26
+ * [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 3→4;[ref] 三态 behavior 进候选行)。
35
27
  * 类型绑 core 的字面型:core 再升号 ⇒ 这里编译红,两只 durable 店(SQL 双方言 + File)的 stale 判别与写侧钉
36
28
  * 同批随之;判别是**等式**(`!==`)不是下限 —— 旧号的行按 stale 信封交还(不迁移、不 widen、不丢行),
37
29
  * pending 卡短命,重触发即取新卡(B7/B8 既诺)。
@@ -1,7 +1,8 @@
1
- export const PERSISTED_RULE_TOOLS = ["Bash", "Read"];
2
- const TOOLS_PIN = true;
3
- void TOOLS_PIN;
4
- export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath"];
1
+ import { ruleToolGrammarOf } from "@sema-agent/core";
2
+ export function isPersistedRuleTool(name) {
3
+ return ruleToolGrammarOf(name) !== undefined;
4
+ }
5
+ export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path"];
5
6
  const MATCHES_PIN = true;
6
7
  void MATCHES_PIN;
7
8
  export const RULE_OFFER_BATCH_MEMBER_KINDS = ["command", "directoryRead"];
@@ -13,5 +14,5 @@ void REASONS_PIN;
13
14
  export const EDITED_RULE_BREADTH_WARNING_CODES = ["broad_prefix", "compound_prefix"];
14
15
  const WARNING_CODES_PIN = true;
15
16
  void WARNING_CODES_PIN;
16
- export const RULE_APPROVAL_RECORD_SCHEMA = 3;
17
+ export const RULE_APPROVAL_RECORD_SCHEMA = 4;
17
18
  //# sourceMappingURL=permission-rule-vocab.js.map
@@ -1,6 +1,7 @@
1
- import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleScope, type StaleRuleApprovalRecord } from "@sema-agent/core";
1
+ import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type StaleRuleApprovalRecord } from "@sema-agent/core";
2
2
  import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
3
3
  import type { PermissionRuleStoreBundle } from "../rules-consent.js";
4
+ import { type RuleCandidate } from "@sema-agent/core";
4
5
  /**
5
6
  * `RuleApprovalRecordStore` 的 File 形。
6
7
  *
@@ -62,10 +63,7 @@ export declare class FileRuleImportTicketStore {
62
63
  principal: string;
63
64
  approvalId: string;
64
65
  purpose: RuleTicketPurpose;
65
- candidates: ReadonlyArray<{
66
- rule: string;
67
- scope: RuleScope;
68
- }>;
66
+ candidates: ReadonlyArray<RuleCandidate>;
69
67
  ttlMs: number;
70
68
  }): Promise<RuleImportTicket>;
71
69
  /** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose →
@@ -6,12 +6,13 @@ import { FileDurableRulePartitionProvider, AppendLog, ensureDir, readJsonlRecord
6
6
  import { z } from "zod";
7
7
  import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
8
8
  import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
9
+ import { RULE_BEHAVIORS } from "@sema-agent/core";
9
10
  const RuleScopeSchema = z.union([
10
11
  z.object({ kind: z.literal("global") }).strict(),
11
12
  z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
12
13
  ]);
13
14
  const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
14
- const RuleCandidateSchema = z.object({ rule: z.string(), scope: RuleScopeSchema }).strict();
15
+ const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: RuleScopeSchema }).strict();
15
16
  const RuleOffer2Schema = z.union([
16
17
  z.object({ kind: z.literal("single"), candidate: z.number().int().nonnegative().safe() }).strict(),
17
18
  z
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
2
+ import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleCandidate, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
3
3
  import type { Pool as MySqlPool } from "mysql2/promise";
4
4
  import type { PgQueryFn } from "./pg-query.js";
5
5
  import { type SqlDriver } from "./sql-driver.js";
@@ -274,11 +274,14 @@ export declare const SQL_DB_NOW_MS: {
274
274
  readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
275
275
  readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
276
276
  };
277
- /** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。 */
278
- export declare function buildRulePayloadHash(candidates: ReadonlyArray<{
279
- rule: string;
280
- scope: RuleScope;
281
- }>): string;
277
+ /** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
278
+ *
279
+ * 🔴 入参型收到 core 的 `RuleCandidate`(core 7.9.0 [ref] 起含 `behavior`)—— 规则的身份是
280
+ * **(behavior, 文本, 作用域) 三元组**,而这道摘要正是「prepare 那一刻的候选集」与「redeem 那一刻的
281
+ * 候选集」之间的等式。此前的形参写成 `{rule, scope}` 二元组:序列化器吃的是整只对象所以运行期**恰好**
282
+ * 覆盖了 behavior,但那是巧合不是保证 —— 哪天有人从一个只有二元组的地方喂进来,一次把候选从 `deny`
283
+ * 改写成 `allow`(文本一个字节不动)在这道等式下就认不出来了。收到域型让机器替人守这一条。 */
284
+ export declare function buildRulePayloadHash(candidates: ReadonlyArray<RuleCandidate>): string;
282
285
  /**
283
286
  * 票面三态协议的 SQL 实现。
284
287
  *
@@ -315,10 +318,7 @@ export declare class SqlRuleImportTicketStore {
315
318
  principal: string;
316
319
  approvalId: string;
317
320
  purpose: RuleTicketPurpose;
318
- candidates: ReadonlyArray<{
319
- rule: string;
320
- scope: RuleScope;
321
- }>;
321
+ candidates: ReadonlyArray<RuleCandidate>;
322
322
  ttlMs: number;
323
323
  }): Promise<RuleImportTicket>;
324
324
  /**
@@ -1,10 +1,10 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, parseAllowRuleText, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
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";
4
4
  import { dialectProtocolJsonEncoder } from "./sql-driver.js";
5
5
  import { isMissingColumnError } from "./sql-errors.js";
6
6
  import { canonicalJson } from "../canonical-json.js";
7
- import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
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
  export function writerOfSqlRuleStore(store) {
9
9
  const w = Reflect.get(store, PERMISSION_RULE_WRITER);
10
10
  if (w === null || typeof w !== "object")
@@ -24,10 +24,11 @@ const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().i
24
24
  const RuleAddSchema = z
25
25
  .object({ dot: RuleDotSchema, origin: z.enum(["user", "imported-cc", "starter"]), createdAt: z.string() })
26
26
  .strict();
27
- const PersistedAllowRuleSchema = z
27
+ const PersistedRuleRowSchema = z
28
28
  .object({
29
+ behavior: z.string().optional(),
29
30
  rule: z.string(),
30
- tool: z.enum(PERSISTED_RULE_TOOLS),
31
+ tool: z.string().min(1).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
31
32
  match: z.enum(PERSISTED_RULE_MATCHES),
32
33
  command: z.string(),
33
34
  scope: DurableRuleScopeSchema,
@@ -35,14 +36,15 @@ const PersistedAllowRuleSchema = z
35
36
  })
36
37
  .strict();
37
38
  const RuleTombstoneSchema = z
38
- .object({ rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
39
+ .object({ behavior: z.string().optional(), rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
39
40
  .strict();
40
41
  const QUARANTINE_REASONS = new Set(Object.entries(RULE_SYNC_DROP_CODES)
41
42
  .filter(([, side]) => side.includes("local-quarantined"))
42
43
  .map(([code]) => code));
43
44
  const isQuarantineReason = (v) => typeof v === "string" && QUARANTINE_REASONS.has(v);
44
- const QuarantinedRuleAddSchema = z
45
+ const QuarantinedRuleAddRowSchema = z
45
46
  .object({
47
+ behavior: z.string().optional(),
46
48
  rule: z.string(),
47
49
  scope: DurableRuleScopeSchema,
48
50
  add: RuleAddSchema,
@@ -50,8 +52,18 @@ const QuarantinedRuleAddSchema = z
50
52
  atMs: z.number().int().nonnegative().safe(),
51
53
  })
52
54
  .strict();
55
+ function readBucketRowBehavior(raw, what, ownerKey) {
56
+ const behavior = readRowBehavior(raw);
57
+ if (behavior === undefined) {
58
+ throw new Error(`permission_rule bucket ${ownerKey} carries a ${what} whose behavior word is outside the closed set (${JSON.stringify(raw)}) — ` +
59
+ `refusing the whole bucket rather than reading a standing deny as "no rule"`);
60
+ }
61
+ return behavior;
62
+ }
63
+ const WritablePersistedRuleSchema = PersistedRuleRowSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
64
+ const WritableRuleTombstoneSchema = RuleTombstoneSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
53
65
  const FrontierSchema = z.record(z.string(), z.number().int().nonnegative().safe());
54
- const RuleCandidateSchema = z.object({ rule: z.string(), scope: DurableRuleScopeSchema }).strict();
66
+ const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: DurableRuleScopeSchema }).strict();
55
67
  const RedeemedDotsSchema = z.record(z.string(), RuleDotSchema);
56
68
  const RuleEditedSchema = z
57
69
  .object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string(), warnings: z.array(z.enum(EDITED_RULE_BREADTH_WARNING_CODES)).optional() })
@@ -108,9 +120,9 @@ export const TIDB_PERMISSION_RULE_STATEMENTS = [
108
120
  state VARCHAR(32) NOT NULL,
109
121
  rev BIGINT NOT NULL DEFAULT 0,
110
122
  candidates_json LONGTEXT NOT NULL,
111
- -- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 3 —— design/382 §3.5 由 2 升,
123
+ -- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 4 —— core 7.9.0 #625 由 3 升,
112
124
  -- 代码里的单点是 RULE_APPROVAL_RECORD_SCHEMA;OCC 守卫另在 rev,两轴禁混,schema-naming 门③ 白名单
113
- -- 登记)。读侧对非 3 的行(含上一形的 2)交还 design/375 §4.5 的
125
+ -- 登记)。读侧对非 4 的行(含上一形的 3)交还 design/375 §4.5 的
114
126
  -- StaleRuleApprovalRecord 信封(identity 键 only)—— 本仓 SCHEMA POLICY 是删表重建,重建后的表
115
127
  -- 不该有这种行,这一臂是「未来再换形」的防御位,绝不静默 widen 成一条本形担保不了的记录。
116
128
  version INT NOT NULL,
@@ -263,7 +275,7 @@ export async function ensurePgPermissionRuleSchema(q) {
263
275
  rev BIGINT NOT NULL DEFAULT 0,
264
276
  candidates_json TEXT COLLATE "C" NOT NULL,
265
277
  -- version / offers_json / selected_offer:design/375 换形三列(MySQL 孪生的行内注写了理由:
266
- -- version=数据格式版本恒 3(design/382 §3.5 由 2 升;单点 RULE_APPROVAL_RECORD_SCHEMA),非 3 的行(含 2)
278
+ -- version=数据格式版本恒 4(core 7.9.0 #625 由 3 升;单点 RULE_APPROVAL_RECORD_SCHEMA),非 4 的行(含 3)
267
279
  -- 读成 Stale 信封;offers_json=按 index 引用的 OPTION 结构,
268
280
  -- 禁存文本副本;selected_offer=确认的 OFFER index,与 ticket/redeemed_dots 的 CANDIDATE index
269
281
  -- 两空间分离)。
@@ -356,10 +368,18 @@ class SqlDurableRulePartition {
356
368
  const row = rows[0];
357
369
  if (row === undefined)
358
370
  return { ...EMPTY_BUCKET, rules: [], tombstones: [], quarantined: [] };
359
- const rules = parseColumn(z.array(PersistedAllowRuleSchema), row.rules_json, "rules_json", this.ownerKey);
360
- const tombstones = parseColumn(z.array(RuleTombstoneSchema), row.tombstones_json, "tombstones_json", this.ownerKey);
361
- const quarantined = parseColumn(z.array(QuarantinedRuleAddSchema), row.quarantined_json, "quarantined_json", this.ownerKey);
371
+ const rulesRaw = parseColumn(z.array(PersistedRuleRowSchema), row.rules_json, "rules_json", this.ownerKey);
372
+ const tombstonesRaw = parseColumn(z.array(RuleTombstoneSchema), row.tombstones_json, "tombstones_json", this.ownerKey);
373
+ const quarantinedRaw = parseColumn(z.array(QuarantinedRuleAddRowSchema), row.quarantined_json, "quarantined_json", this.ownerKey);
362
374
  const observedVector = row.observed_vector_json === null || row.observed_vector_json === undefined ? undefined : parseColumn(FrontierSchema, row.observed_vector_json, "observed_vector_json", this.ownerKey);
375
+ const expected = buildRuleBucketChecksum({ rules: rulesRaw, tombstones: tombstonesRaw, quarantined: quarantinedRaw, ...(observedVector !== undefined ? { observedVector } : {}) });
376
+ if (String(row.checksum ?? "") !== expected) {
377
+ throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
378
+ `refusing the whole bucket rather than reporting a partial rule set`);
379
+ }
380
+ const rules = rulesRaw.map((r) => ({ ...r, behavior: readBucketRowBehavior(r.behavior, "rule row", this.ownerKey) }));
381
+ const tombstones = tombstonesRaw.map((t) => ({ ...t, behavior: readBucketRowBehavior(t.behavior, "tombstone", this.ownerKey) }));
382
+ const quarantined = quarantinedRaw.map((q) => ({ ...q, behavior: readBucketRowBehavior(q.behavior, "quarantined row", this.ownerKey) }));
363
383
  const state = {
364
384
  actor: String(row.actor ?? ""),
365
385
  counter: Number(row.counter ?? 0),
@@ -369,13 +389,8 @@ class SqlDurableRulePartition {
369
389
  quarantined,
370
390
  ...(observedVector !== undefined ? { observedVector } : {}),
371
391
  };
372
- const expected = buildRuleBucketChecksum({ rules, tombstones, quarantined, ...(observedVector !== undefined ? { observedVector } : {}) });
373
- if (String(row.checksum ?? "") !== expected) {
374
- throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
375
- `refusing the whole bucket rather than reporting a partial rule set`);
376
- }
377
392
  const badTombstone = tombstones.find((t) => {
378
- const parsed = parseAllowRuleText(t.rule);
393
+ const parsed = parseRuleText(t.rule, t.behavior);
379
394
  return "reject" in parsed || parsed.rule.rule !== t.rule;
380
395
  });
381
396
  if (badTombstone !== undefined) {
@@ -467,7 +482,7 @@ class SqlDurableRulePartition {
467
482
  assertDeleteDeltaCarriesNoAdd(delta);
468
483
  nextTombstones = [...cur.tombstones, delta.tombstone];
469
484
  }
470
- const writable = z.object({ rules: z.array(PersistedAllowRuleSchema), tombstones: z.array(RuleTombstoneSchema) }).safeParse({ rules: nextRules, tombstones: nextTombstones });
485
+ const writable = z.object({ rules: z.array(WritablePersistedRuleSchema), tombstones: z.array(WritableRuleTombstoneSchema) }).safeParse({ rules: nextRules, tombstones: nextTombstones });
471
486
  if (!writable.success) {
472
487
  throw new Error(`refusing to write a permission_rule bucket this backend could not read back: ${writable.error.message}`);
473
488
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * S-167 → S-178 —— **一个部署级旋钮的值是谁定的**,四词闭集 + 一句运维指路。
3
+ *
4
+ * ## 为什么是一个共享件而不是每键一份
5
+ *
6
+ * S-167 给 `READ_FACE` 造了这套东西(值 + 来源 + 指路句),理由写在 `read-face-posture.ts` 的顶注里:
7
+ * 「缺席」这一态里塞着两件不同的事(单机 turnkey 没表态 vs 多租户就是要引擎默认),而把它们分开之后,
8
+ * **env 恒赢的判据**与**运维面「为什么这台机器是这个值」**才有真答案。S-178 把同一条律推到另外三个键
9
+ * (`STREAM_ASK_WINDOW_MS` / `DURABLE_APPROVAL` / `SESSION_AUTO_TITLE`)时,唯一诚实的做法是**抽这一份**
10
+ * ——四词表抄第二遍的那天,两份表会在加词时分家,而「这个值是谁定的」在运维面上是个是非题,不能有两个答案。
11
+ *
12
+ * ## 优先序(与 `config.ts` 的 `postureOn` 同形)
13
+ * `env 显式` > `center 发布` > `posture 派生` > `引擎/服务端默认`。
14
+ *
15
+ * ## 定界
16
+ * 本模块只说**来源**。值本身、以及「这台机器凭什么派生出这个值」那句话,由每个键自己给
17
+ * ({@link PostureKnobFacts.postureMeaning})——那句话是键专属的事实,共享它只会得到一句谁都不认的空话。
18
+ */
19
+ /** 旋钮取值来源的闭集词表(顺序 = 优先序)。 */
20
+ export declare const POSTURE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
21
+ /** 一个旋钮的取值来源(闭集四词)。加词 ⇒ {@link postureSourceNote} 的穷举 switch 编译红。 */
22
+ export type PostureSource = (typeof POSTURE_SOURCE_WORDS)[number];
23
+ /** 一个旋钮报出「我是谁定的」所需要的三件事。 */
24
+ export interface PostureKnobFacts {
25
+ /** 这台机器上占住这个键的 env 变量名(指路句要逐字点它,运维才知道去改哪个)。 */
26
+ readonly knob: string;
27
+ readonly source: PostureSource;
28
+ /**
29
+ * `posture` 那一臂**专属**的一句话:这台机器为什么派生出了这个值,以及怎么把它钉回去。
30
+ * 只在 `source === "posture"` 时被读到 —— 别的臂的句子是通用的(见 {@link postureSourceNote})。
31
+ */
32
+ readonly postureMeaning: string;
33
+ }
34
+ /** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。 */
35
+ export declare function postureSourceNote(facts: PostureKnobFacts): string;
36
+ /**
37
+ * 这个值是**运维显式表态**的吗?
38
+ *
39
+ * 🔴 S-178 施工时真踩(本批全量抓出):`hasOperatorGateIntent`(`src/approval.ts`)判「运维表达过门意图
40
+ * 没有」时读的是 `durableApproval` 这个**值** —— 那在本键进 posture 家族之前是对的(它只有显式设过才为
41
+ * 真),之后当场失真:单机 turnkey 上 posture 派生的 `true` 会被读成「运维要门」,于是 run-local 那条
42
+ * **结构上没有 durable 门**的腿在每次 boot 打一条补偿 warn,而它其实正处在「零门意图的既定姿势」里。
43
+ * 这与 S-167 头注①记的是**同一形**病(用一个会被 posture 铸出来的量当「有人表态过」的代理)。
44
+ *
45
+ * ⇒ 判据落到**来源**上,穷举 switch(四词闭集加员即编译红):`env`(这台机器的运维写下的)与 `center`
46
+ * (组织发布的)是表态;`posture`(姿态派生)与 `engine-default`(内建缺省)都**不是**「有人要求过」。
47
+ */
48
+ export declare function isExplicitPosture(source: PostureSource): boolean;
49
+ /** operator 诊断面的一行读数:值 + 来源 + 指路句。三个 S-178 旋钮与 S-167 的 readFace 同形。 */
50
+ export interface PostureKnobReading<V> {
51
+ value: V;
52
+ source: PostureSource;
53
+ note: string;
54
+ }
55
+ /** 一行读数的铸点(纯数据 ⇒ `build*`)。 */
56
+ export declare function buildPostureKnobReading<V>(value: V, facts: PostureKnobFacts): PostureKnobReading<V>;
57
+ //# sourceMappingURL=posture-source.d.ts.map
@@ -0,0 +1,27 @@
1
+ export const POSTURE_SOURCE_WORDS = ["env", "center", "posture", "engine-default"];
2
+ export function postureSourceNote(facts) {
3
+ switch (facts.source) {
4
+ case "env":
5
+ return `pinned by this machine's ${facts.knob} env var (deployment sovereignty — it wins over any published value for this key)`;
6
+ case "center":
7
+ return `applied from the config-center (restart-to-apply); set ${facts.knob} on this machine to take the key back`;
8
+ case "posture":
9
+ return facts.postureMeaning;
10
+ case "engine-default":
11
+ return `not pinned by this deployment — the built-in default for ${facts.knob} is in force`;
12
+ }
13
+ }
14
+ export function isExplicitPosture(source) {
15
+ switch (source) {
16
+ case "env":
17
+ case "center":
18
+ return true;
19
+ case "posture":
20
+ case "engine-default":
21
+ return false;
22
+ }
23
+ }
24
+ export function buildPostureKnobReading(value, facts) {
25
+ return { value, source: facts.source, note: postureSourceNote(facts) };
26
+ }
27
+ //# sourceMappingURL=posture-source.js.map
@@ -27,10 +27,14 @@
27
27
  * 两者在 core 里是并列机制(deny 表在 open 面上照样拦)。core 的 `config.read_face_deployment_clamped`
28
28
  * (只读挂载钳制)同样不受本件影响。
29
29
  */
30
- /** 档位来源的闭集词表(顺序 = 优先序:env > center > posture > 引擎默认)。 */
30
+ import { type PostureSource } from "./posture-source.js";
31
+ /**
32
+ * 档位来源的闭集词表 —— **S-178 起转 `posture-source.ts` 的共享件**(本模块只是给这一档取个本地名字,
33
+ * 不再持第二份词表:同一张四词表抄两遍的那天,两份会在加词时分家)。
34
+ */
31
35
  export declare const READ_FACE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
32
- /** 档位的来源(闭集四词)。加词 ⇒ {@link readFaceSourceNote} 的穷举 switch 编译红。 */
33
- export type ReadFaceSource = (typeof READ_FACE_SOURCE_WORDS)[number];
36
+ /** 档位的来源(闭集四词)。加词 ⇒ {@link postureSourceNote} 的穷举 switch 编译红。 */
37
+ export type ReadFaceSource = PostureSource;
34
38
  /** 一台 worker 的 READ 面档位事实(值 + 来源)。`ServiceConfigFlat` 结构上满足本形。 */
35
39
  export interface ReadFacePostureFacts {
36
40
  readonly readFace?: "open" | "roots" | undefined;
@@ -41,7 +45,8 @@ export interface ReadFacePostureFacts {
41
45
  * 单用户 turnkey ⇒ `open`;多租户 ⇒ 不铸(引擎默认 `roots`,server 不复制上游默认)。
42
46
  */
43
47
  export declare function readFaceForPosture(singleUserTurnkey: boolean): ReadFacePostureFacts;
44
- /** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。 */
48
+ /** 本档的运维指路句 —— 骨架由共享件 {@link postureSourceNote} 给(四词穷举在那里,加词即编译红);
49
+ * 本函数只提供 `posture` 那一臂的**键专属**事实句。 */
45
50
  export declare function readFaceSourceNote(source: ReadFaceSource): string;
46
51
  /** operator 诊断面的投影(`GET /v1/diagnostics/wiring`)。`face:null` = 本部署没钉档,引擎默认接管。 */
47
52
  export declare function buildReadFaceWiring(facts: ReadFacePostureFacts): {
@@ -1,18 +1,14 @@
1
- export const READ_FACE_SOURCE_WORDS = ["env", "center", "posture", "engine-default"];
1
+ import { POSTURE_SOURCE_WORDS, postureSourceNote } from "./posture-source.js";
2
+ export const READ_FACE_SOURCE_WORDS = POSTURE_SOURCE_WORDS;
2
3
  export function readFaceForPosture(singleUserTurnkey) {
3
4
  return singleUserTurnkey ? { readFace: "open", readFaceSource: "posture" } : { readFaceSource: "engine-default" };
4
5
  }
5
6
  export function readFaceSourceNote(source) {
6
- switch (source) {
7
- case "env":
8
- return "pinned by this machine's READ_FACE env var (deployment sovereignty — it wins over any published readFace domain)";
9
- case "center":
10
- return "applied from the config-center readFace domain (restart-to-apply); set READ_FACE on this machine to take the key back";
11
- case "posture":
12
- return "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back";
13
- case "engine-default":
14
- return "not pinned by this deployment — the engine's own default (roots) is in force";
15
- }
7
+ return postureSourceNote({
8
+ knob: "READ_FACE",
9
+ source,
10
+ postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back",
11
+ });
16
12
  }
17
13
  export function buildReadFaceWiring(facts) {
18
14
  return { face: facts.readFace ?? null, source: facts.readFaceSource, note: readFaceSourceNote(facts.readFaceSource) };