@sema-agent/server 7.10.0 → 7.11.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 (59) hide show
  1. package/dist/adoption/plan.d.ts +152 -0
  2. package/dist/adoption/plan.js +513 -0
  3. package/dist/adoption/runner.d.ts +54 -0
  4. package/dist/adoption/runner.js +505 -0
  5. package/dist/adoption/sql.d.ts +76 -0
  6. package/dist/adoption/sql.js +106 -0
  7. package/dist/adoption/wire.d.ts +250 -0
  8. package/dist/adoption/wire.js +153 -0
  9. package/dist/approval-card.d.ts +24 -0
  10. package/dist/approval-card.js +32 -0
  11. package/dist/boot/adoption.d.ts +30 -0
  12. package/dist/boot/adoption.js +57 -0
  13. package/dist/boot/coordinators.d.ts +4 -0
  14. package/dist/boot/coordinators.js +3 -1
  15. package/dist/boot/parked-revive-gate.d.ts +38 -5
  16. package/dist/boot/parked-revive-gate.js +53 -6
  17. package/dist/boot/runner-deps.d.ts +10 -2
  18. package/dist/boot/runner-deps.js +12 -1
  19. package/dist/config-types.d.ts +16 -1
  20. package/dist/config.js +5 -1
  21. package/dist/http/routes/adoption.d.ts +26 -0
  22. package/dist/http/routes/adoption.js +120 -0
  23. package/dist/http/routes/capabilities.js +12 -0
  24. package/dist/http/routes/rules.d.ts +23 -0
  25. package/dist/http/routes/rules.js +117 -0
  26. package/dist/http/routes/shared-memory.d.ts +31 -0
  27. package/dist/http/routes/shared-memory.js +181 -0
  28. package/dist/http/routes/trace-usage.js +139 -3
  29. package/dist/http/server.d.ts +19 -1
  30. package/dist/http/server.js +42 -0
  31. package/dist/main.js +52 -3
  32. package/dist/observability/fail-open.d.ts +8 -0
  33. package/dist/observability/fail-open.js +8 -0
  34. package/dist/plugins/adoption-log-sql.d.ts +191 -0
  35. package/dist/plugins/adoption-log-sql.js +273 -0
  36. package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
  37. package/dist/plugins/checkpoint-store-sql.js +11 -0
  38. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  39. package/dist/plugins/local-checkpoint-store.js +8 -0
  40. package/dist/plugins/permission-rule-store-sql.d.ts +242 -0
  41. package/dist/plugins/permission-rule-store-sql.js +817 -0
  42. package/dist/plugins/pg-pool.js +37 -0
  43. package/dist/plugins/session-policy-store-sql.d.ts +6 -0
  44. package/dist/plugins/session-policy-store-sql.js +7 -1
  45. package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
  46. package/dist/plugins/shared-memory-store-sql.js +516 -0
  47. package/dist/plugins/store-backend.d.ts +30 -0
  48. package/dist/plugins/store-backend.js +14 -0
  49. package/dist/plugins/tidb-pool.js +47 -0
  50. package/dist/rules-consent.d.ts +126 -0
  51. package/dist/rules-consent.js +198 -0
  52. package/dist/shared-memory-scope-authorizer.d.ts +29 -0
  53. package/dist/shared-memory-scope-authorizer.js +17 -0
  54. package/dist/tool-approval.d.ts +55 -0
  55. package/dist/tool-approval.js +124 -5
  56. package/dist/trace/core-keyset-guard.d.ts +2 -2
  57. package/dist/trace/project.d.ts +1 -0
  58. package/dist/trace/project.js +1 -0
  59. package/package.json +3 -3
@@ -0,0 +1,57 @@
1
+ /**
2
+ * design/183 I6(server 同族)—— **每副本 boot 必查收编日志**。
3
+ *
4
+ * 🔴 为什么这道门必须存在:phase 真源住 SQL 正是为了「发起 host 在两条腿之间被摧毁 ⇒ 接棒副本按日志
5
+ * 续跑」。可是**没有人会替它按下重试** —— 运维那次 POST 的响应早就丢了。所以每个副本起来时都扫一遍
6
+ * 在飞行:能推就推到终态,推不动就**响亮**留痕(禁静默跳过 —— 静默跳过等于把半迁移状态带进服务期,
7
+ * 那正是 I6 存在的理由)。
8
+ *
9
+ * 🔴 为什么是「续跑或响亮拒」而不是「拒启」:半迁移的收编**是可恢复的**(每条腿幂等,phase 单调),
10
+ * 拒启只会让一个能自愈的状态变成一次停机。拒启留给不可恢复的装配谎言(boot/org-memory.ts 那类)。
11
+ * 与之对称:续跑失败**不吞异常** —— 它落一条 error 级留痕并把 outcome 记进返回值,调用方(main)
12
+ * 把读数打进启动日志,运维在启动行里就能看见「这台副本上还有 N 条收编没走完」。
13
+ *
14
+ * 阻塞性:扫描在**监听之前**跑完(main 的调用点)。一条弧的推进是几条 UPDATE,毫秒级;库里正常状态下
15
+ * 在飞行数为 0,一次 `WHERE state='in_flight'` 的索引点查而已。
16
+ */
17
+ import { createAdoptionRunner } from "../adoption/runner.js";
18
+ /**
19
+ * boot 期的在飞收编扫描。无 SQL 后端 ⇒ 零动作(收编面在那种部署上本来就 501)。
20
+ * 绝不抛:一次库抖动不该拒掉整台 worker 的启动,但**必须**留下响亮读数(`error` 位 + error 级日志)。
21
+ */
22
+ export async function runAdoptionBootScan(ctx) {
23
+ const store = ctx.backend?.adoptionLog?.();
24
+ if (!store)
25
+ return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [] };
26
+ const runner = createAdoptionRunner({
27
+ store,
28
+ dialect: ctx.backend?.kind === "pg" ? "pg" : "tidb",
29
+ now: ctx.now ?? Date.now,
30
+ logger: ctx.logger,
31
+ });
32
+ try {
33
+ const inFlight = await store.listInFlight();
34
+ if (inFlight.length === 0)
35
+ return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [] };
36
+ ctx.logger.warn?.("adoption_in_flight_on_boot", {
37
+ count: inFlight.length,
38
+ ids: inFlight.map((r) => r.adoptionId),
39
+ note: "design/183 I6: resuming in-flight identity adoptions before this replica starts serving",
40
+ });
41
+ const out = await runner.resumeInFlight();
42
+ const reading = { scanned: inFlight.length, ...out };
43
+ if (reading.stalled.length > 0 || reading.rejected.length > 0 || reading.failed.length > 0) {
44
+ ctx.logger.error?.("adoption_boot_scan_incomplete", reading);
45
+ }
46
+ else {
47
+ ctx.logger.info?.("adoption_boot_scan_resumed", reading);
48
+ }
49
+ return reading;
50
+ }
51
+ catch (err) {
52
+ const error = err instanceof Error ? err.message : String(err);
53
+ ctx.logger.error?.("adoption_boot_scan_failed", { error });
54
+ return { scanned: 0, resumed: [], stalled: [], rejected: [], failed: [], error };
55
+ }
56
+ }
57
+ //# sourceMappingURL=adoption.js.map
@@ -15,11 +15,15 @@ import { ToolApprovalCoordinator } from "../tool-approval.js";
15
15
  import { SendUserFileEmitter } from "../capabilities/send-user-file-tool.js";
16
16
  import { TaskEnvRegistry } from "../capabilities/sandbox-file-send.js";
17
17
  import type { StoreBackend } from "../plugins/store-backend.js";
18
+ import type { RuleConsentLane } from "../rules-consent.js";
18
19
  export interface LiveCoordinatorsCtx {
19
20
  config: ServiceConfig;
20
21
  logger: Logger;
21
22
  backend: StoreBackend | undefined;
22
23
  sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
24
+ /** #154 车二:持久化权限规则的同意车道(main.ts 与 `RunnerDeps.permissionRuleStore` **同源**于
25
+ * `backend.permissionRule()` 的同一个返回值)。在场 ⇒ ask 帧投 `ruleSuggestions` + 回决可兑付。 */
26
+ ruleConsent: RuleConsentLane | undefined;
23
27
  }
24
28
  export declare function createLiveCoordinators(ctx: LiveCoordinatorsCtx): {
25
29
  elicitation: ElicitationCoordinator | undefined;
@@ -8,7 +8,7 @@ import { withLedgerRecording } from "../plugins/send-file-ledger.js";
8
8
  import { basename, resolve } from "node:path";
9
9
  import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
10
10
  export function createLiveCoordinators(ctx) {
11
- const { config, logger, backend, sendUserFileTaskEnvs } = ctx;
11
+ const { config, logger, backend, sendUserFileTaskEnvs, ruleConsent } = ctx;
12
12
  // E23 (shell-host contract): inbound MCP elicitation coordinator (live-only HITL). Present ONLY when MCP_ELICITATION_ENABLED
13
13
  // — absent ⇒ onElicit is not wired ⇒ core advertises no elicitation capability to any server (fail-closed). Shared
14
14
  // by the runner (the onElicit seam) and the HTTP layer (the respond route + the per-run ALS context wraps).
@@ -71,6 +71,8 @@ export function createLiveCoordinators(ctx) {
71
71
  // X-2 写侧准入门两帽(协议未上场时门本身不参与,见 ToolApprovalCoordinator.admit)。
72
72
  admitMaxPerTask: config.streamApproval.admitMaxPerTask,
73
73
  admitMaxPerOwner: config.streamApproval.admitMaxPerOwner,
74
+ // #154 车二:同意车道(缺席 = 规则店没装配 ⇒ 帧上零候选、回决的 persistRule 如实拒)。
75
+ ...(ruleConsent !== undefined ? { ruleConsent } : {}),
74
76
  })
75
77
  : undefined;
76
78
  // SendUserFile(真 CC 契约,clay 2026-07-14)两种 lane 形态,其余缺席=诚实(工具不进 roster):
@@ -1,5 +1,6 @@
1
1
  import { type BackgroundAgentRecord, type ToolExecuteContext } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "../config.js";
3
+ import type { RunnerDepsOnAsk } from "./runner-deps.js";
3
4
  import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView, type LiveQuestionFace } from "../deployment-governance.js";
4
5
  import { type SandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
5
6
  /** 本腿读的配置切面(`ServiceConfig` 结构满足)。两个构造口各自的切面 + lane 判别式读的 `remoteExec`。 */
@@ -15,8 +16,35 @@ export interface ApprovalExemptionProbe {
15
16
  export interface ParkedReviveGateDeps {
16
17
  /** ⚠️ **活引用**:热改字段(autonomy/commandPolicy/守卫集/审批四旋钮)每次赎回现读,见下方「禁 memoize」。 */
17
18
  readonly config: ParkedReviveGateConfigView;
18
- /** #152:活体 AskUserQuestion 面。赎回腿今天无 ALS ctx ⇒ 判 ask,与原形同判。 */
19
+ /** #152:活体 AskUserQuestion 面。赎回腿今天无 ALS ctx ⇒ 判 ask,与原形同判。
20
+ * ⚠️ 它同时是**决议链元数据** `contentMandate` 的唯一判据(见 {@link mandatePostureOf})。 */
19
21
  readonly question: LiveQuestionFace | undefined;
22
+ /**
23
+ * 部署级活体审批席 —— `RunnerDeps.onAsk` 的**同源铸法**(main.ts 递 `createRunnerDepsOnAsk(toolApproval)`,
24
+ * 单一属主同 boot/runner-deps.ts)。
25
+ *
26
+ * ⚠️ **本仓确有 per-task `spec.onAsk` 装配点**(codex 对抗复审 R1-中1 验真纠正 —— 别照抄
27
+ * `createRunnerDepsOnAsk` 头注那句「bg/resume 两条腿没有 per-task 装配点」,那句话的射程只是那两条腿):
28
+ * `http/routes/tasks.ts` 的宿主 sync/stream 腿在装配点写 `spec.onAsk = toolApproval.boundAsk({owner,
29
+ * taskId, sessionId, legKey…})`(windowZero 形则写一个恒返 `"unavailable"` 的闭包)。core 冻的是
30
+ * `spec.onAsk ?? deps.onAsk`,所以那条腿冻下去的是**带身份的** bound 席,不是这一格。
31
+ *
32
+ * 这对本腿的两个用途分别意味着什么:
33
+ * · **`durableMandate` 判据(承重,见 {@link mandatePostureOf})—— 不受影响**:两个装配点是**同一个
34
+ * 条件**的两种表达(`spec.onAsk` 的两支都在 `deps.toolApproval` 在场时才写,windowZero 支写的也是
35
+ * 函数),所以「席位在不在场」这个**布尔**在 spec 位与 deps 位上恒等。core 的
36
+ * `isLiveApproverSeat` 只判 `typeof === "function"` ⇒ 本腿算出的位与 park 时冻的位逐字相同,摘要对得上。
37
+ * · **重建条目的 `onAsk` 席位本身 —— 是一个诚实的降级,不是等价替换**:bound 席携的身份
38
+ * (owner/taskId/sessionId/legKey/腿时限)是**宿主那条腿**的,park 时未持久化 ⇒ 跨副本重建不出来。
39
+ * 交这一格的效果:赎回腿没有 ALS 上下文,`ToolApprovalCoordinator.ask` 第一行 `if (!ctx) return
40
+ * "unavailable"`(tool-approval.ts)⇒ 继承 ask 恒走 core 的 `approverUnavailable` 回路 = **再 park
41
+ * 一次给人**。它**到不了**宿主原来那张卡(那是残余,与「客户端折叠层重建不出」同族);它也**不会**
42
+ * 错投到别处(无 ctx 就直接返回,`askBroadcast` 根本不执行 —— codex 提出的「按赎回腿归因/投递」
43
+ * 这条机制经亲读证伪)。
44
+ * 为什么仍然要交:席位缺席 + `durableMandate` 为 false(装了活体席的部署就是这一形)⇒ 继承 ask 只剩
45
+ * headless auto-deny,比「再问人一次」严且哑。交了之后最坏也是 park,方向 fail-safe。
46
+ */
47
+ readonly approverSeat: RunnerDepsOnAsk | undefined;
20
48
  readonly approvalExemptionStore: ApprovalExemptionProbe | undefined;
21
49
  readonly logger: {
22
50
  info(event: string, fields?: Record<string, unknown>): void;
@@ -36,6 +64,9 @@ export interface ParkedReviveGateDeps {
36
64
  * 事件(载体键集钉见 test/parked-revive-gate-wiring.test.ts)。
37
65
  */
38
66
  export type RebuiltInheritedGate = NonNullable<NonNullable<NonNullable<ToolExecuteContext["reviveClaim"]>["parkedResume"]>["inheritedGate"]>;
67
+ /** 重建链的**单层条目**形 —— 同样从载体推导(理由同 {@link RebuiltInheritedGate}:core 又加一个
68
+ * 能收紧的槽时,本仓的编译面要看得见)。 */
69
+ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentConstraints"]>[number];
39
70
  /**
40
71
  * 赎回腿的父约束链重建(design/181 件二)。
41
72
  *
@@ -66,10 +97,12 @@ export type RebuiltInheritedGate = NonNullable<NonNullable<NonNullable<ToolExecu
66
97
  * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
67
98
  * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
68
99
  * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
69
- * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
70
- * `ask` 时,core 只要还看得见 checkpoint 店 + durable 审批配置就把它**重新浮回 durable 门**
71
- * ( park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
72
- * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
100
+ * · `onAsk` = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
101
+ * 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
102
+ * `durableMandate`( {@link mandatePostureOf}),继承层判 `ask` core 只要还看得见 checkpoint 店
103
+ * + durable 审批配置就把它**重新浮回 durable 门**( park 一次,人再批一次);deny 只是「durable
104
+ * 设施不在场」的兜底臂。⛔ 但**不是无条件**(codex R4-中1):core 对挂起链计 `suspendCount`,到
105
+ * `maxSuspends`(默认 5)即终止任务而不再 park。
73
106
  * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
74
107
  * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
75
108
  * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
@@ -57,17 +57,20 @@ function hostGateCwd(config, localRoot) {
57
57
  * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
58
58
  * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
59
59
  * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
60
- * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
61
- * `ask` 时,core 只要还看得见 checkpoint 店 + durable 审批配置就把它**重新浮回 durable 门**
62
- * ( park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
63
- * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
60
+ * · `onAsk` = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
61
+ * 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
62
+ * `durableMandate`( {@link mandatePostureOf}),继承层判 `ask` core 只要还看得见 checkpoint 店
63
+ * + durable 审批配置就把它**重新浮回 durable 门**( park 一次,人再批一次);deny 只是「durable
64
+ * 设施不在场」的兜底臂。⛔ 但**不是无条件**(codex R4-中1):core 对挂起链计 `suspendCount`,到
65
+ * `maxSuspends`(默认 5)即终止任务而不再 park。
64
66
  * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
65
67
  * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
66
68
  * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
67
69
  */
68
70
  export function createParkedReviveInheritedGate(deps) {
69
- const { config, question, approvalExemptionStore, logger, localRoot } = deps;
71
+ const { config, question, approvalExemptionStore, logger, localRoot, approverSeat } = deps;
70
72
  const slots = deps.slots ?? sandboxPathEnvSlots;
73
+ const mandate = mandatePostureOf(deps);
71
74
  return (row) => {
72
75
  // lane 分形与 resolve-spec 同一判别式(单一属主,取值处只此一个)。
73
76
  // 🔑 沙箱腿的 slot 键 = **row.sessionId**(子代自己的会话,design/181 §2.2 裁定):revive 后 core 以
@@ -101,11 +104,55 @@ export function createParkedReviveInheritedGate(deps) {
101
104
  // 治理段无可施加时 applyRuntimeGovernance 原样返回 base ⇒ 基线本身即为链(恒非 undefined,无需断言)。
102
105
  // shellGate 只在治理段真产出档位时出现在载体上(缺席 = 本次没有 live 值可交,core 用 seed 兜底)。
103
106
  return {
104
- parentConstraints: [{ policy: governed.toolPolicy ?? baseline, durableMandate: true }],
107
+ parentConstraints: [
108
+ {
109
+ policy: governed.toolPolicy ?? baseline,
110
+ ...(approverSeat !== undefined ? { onAsk: approverSeat } : {}),
111
+ ...mandate,
112
+ },
113
+ ],
105
114
  ...(governed.shellGate !== undefined ? { shellGate: governed.shellGate } : {}),
106
115
  };
107
116
  };
108
117
  }
118
+ /**
119
+ * 重建条目的**决议链元数据**(core 5.22.0 F-012)—— 两位 mandate 的逐字重算。
120
+ *
121
+ * 为什么它承重:5.22.0 把 re-supply 契约从 count 换成 `cpv1:` 内容摘要
122
+ * (`constraintChainDigest`),而摘要 **把这两位卷了进去**。park 时那一层记的是什么,赎回腿就必须供
123
+ * 什么——供多了、供少了都是 `resume.parent_constraint_mismatch` 响亮拒(pre-CAS,checkpoint 留
124
+ * pending)。**不许**为了让摘要对上而回读 checkpoint 里记的那两位再原样回声:那等于把「重算出同一条链」
125
+ * 换成「抄一份自证」,摘要的防篡改语义当场归零。
126
+ *
127
+ * core 的原式(`prepare-task.js` 的 `inheritedGateForChildren`):
128
+ * ```
129
+ * durableMandate = runtimeCaps.forceDurableGate || (spec.durableApproval !== undefined && !isLiveApproverSeat(spec.onAsk ?? deps.onAsk))
130
+ * contentMandate = runtimeCaps.forceDurableGate || (spec.durableApproval !== undefined && !isLiveQuestionFace(spec.onQuestion ?? deps.onQuestion))
131
+ * ```
132
+ * 本仓三项对位:
133
+ * · `spec.durableApproval !== undefined` —— 本工厂**只在** `config.durableApproval` 为真时构造
134
+ * (main.ts 的构造条件逐字),故这一项在本函数射程内恒成立;
135
+ * · 审批席 = `spec.onAsk ?? deps.onAsk` 的**在场性**。本仓两个装配点(deps 级
136
+ * `createRunnerDepsOnAsk`、per-task 级 `routes/tasks.ts` 的 `boundAsk`)受**同一个**条件闸
137
+ * ——`toolApproval` 协调器在不在场——所以这个布尔在两位上恒等,`approverSeat` 在不在场即判据。
138
+ * ⚠️ 判据是**在场性**不是同一只闭包:身份不同不影响摘要(全文见 {@link ParkedReviveGateDeps.approverSeat});
139
+ * · 问答席 = resolve-spec 的 `liveQuestionFace`:在场 ⇒ spec 不 stamp sentinel、`deps.onQuestion` 的
140
+ * 协调器生效(`isLiveQuestionFace` 判 true);缺席 ⇒ spec stamp `QUESTION_AWAITS_RESUME`
141
+ * (该 sentinel 被 `isLiveQuestionFace` 判 false)。两支都归结为 `question` 在不在场。
142
+ *
143
+ * ⛔ **已披露残余(fail-closed 且响亮,不是静默放行)**:`runtimeCaps.forceDurableGate`(center 逐
144
+ * principal 的授权位)在本装配点解不出来 —— core 的 `runtimeCapsResolver` 是 async 且按 principal 现解,
145
+ * 而重供席位的形是同步 `(row) => gate`。该位为真、且部署又装了活体席的组合上,park 记 true 而本腿供
146
+ * false ⇒ core pre-CAS 响亮拒、checkpoint 留 pending(带 `parked_resume.startup_failed` 码可观测),
147
+ * 绝不会让赎回腿在比挂起时更松的链下跑起来。收口件二选一:core 把该位一起持久进链条目,或重供席位改
148
+ * async 让 resolver 进得来。
149
+ */
150
+ function mandatePostureOf(deps) {
151
+ return {
152
+ ...(deps.approverSeat === undefined ? { durableMandate: true } : {}),
153
+ ...(deps.question === undefined ? { contentMandate: true } : {}),
154
+ };
155
+ }
109
156
  /** host 腿的裁决 env(真 fs;构造是纯字段赋值,不 spawn)。**每次赎回现铸**——与「禁 memoize」同因:
110
157
  * cwd 占位读的是活 config 的 `localDataRoot`。 */
111
158
  function createHostAdjudicationEnv(config, localRoot) {
@@ -75,10 +75,18 @@ export interface RunnerDepsCtx {
75
75
  * 基座**——主/sub 两 Runner 必须同源:委托子代的 prepare 同样跑准入(委托冻结的重判腿),漏挂
76
76
  * subRunner=子代平面绕过目录判决。 */
77
77
  orgMemoryAdmission: import("./org-memory.js").OrgMemoryAdmissionWiring;
78
+ /** design/177 —— org 共享记忆库的供给面(core `RunnerDeps.sharedMemoryStores`)。在场即挂载
79
+ * `memory_list`/`memory_read` 两只工具(core 没有 TaskSpec 伴生旋钮:dep 的在场**就是**部署意图);
80
+ * 缺席 ⇒ 两只都不挂,连占位都没有。装配点按「SQL 供给面 ∧ org 折叠面」双在场才铸它,与 HTTP 面
81
+ * 同一个合取式(main.ts)。 */
82
+ sharedMemoryStores: RunnerDeps["sharedMemoryStores"];
78
83
  toolResultStore: ToolResultStoreFull | undefined;
79
84
  sessionPolicyStore: ReturnType<StoreBackend["sessionPolicy"]> | undefined;
80
85
  runtimeCapsResolver: RunnerDeps["runtimeCapsResolver"];
81
86
  fileSnapshotStore: ReturnType<StoreBackend["fileSnapshot"]> | undefined;
87
+ /** #154 车二:持久化权限规则店 provider(core `RunnerDeps.permissionRuleStore`)。缺席 ⇒ 引擎的
88
+ * `permissionRules.storeWired` 如实报 false、`AskRequest.ruleSuggestions` 不铸(诚实缺席)。 */
89
+ permissionRuleStore: RunnerDeps["permissionRuleStore"];
82
90
  executionEnvFactory: RunnerDeps["executionEnvFactory"];
83
91
  lspManager: RunnerDeps["lspManager"];
84
92
  fleetBus: FleetEventBus;
@@ -94,7 +102,7 @@ export interface RunnerDepsCtx {
94
102
  }
95
103
  /** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
96
104
  * 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
97
- export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes">;
105
+ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores">;
98
106
  /**
99
107
  * design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
100
108
  * `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
@@ -123,7 +131,7 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "
123
131
  * 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
124
132
  */
125
133
  /** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
126
- export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore" | "orgMemoryAdmission">;
134
+ export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores">;
127
135
  export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
128
136
  export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
129
137
  //# sourceMappingURL=runner-deps.d.ts.map
@@ -66,11 +66,16 @@ export function createSharedRunnerDeps(ctx) {
66
66
  // 构造(boot/stores.ts),两键同真同假。
67
67
  usageWindows: ctx.config.usageWindows ? ctx.config.usageWindows : undefined,
68
68
  usageWindowStore: ctx.usageWindowStore ? ctx.usageWindowStore : undefined,
69
+ // design/177 —— 必须进**共享基座**(codex 复审轮3 真缺口):它原先只挂在主 runnerDeps 上,于是
70
+ // 委托子代静默没有 memory_list/memory_read,而能力面与 HTTP 面照样宣布共享库可用 —— 正是 [1543]§三族A
71
+ // 「主 runner 挂了、subRunner 漏挂」那个病族。授权按 per-request principal 折叠(子代同宿主身份),
72
+ // 所以共享同一个 provider 实例既正确又是唯一能让两只 Runner 看到同一份库集合的做法。
73
+ sharedMemoryStores: ctx.sharedMemoryStores ? ctx.sharedMemoryStores : undefined,
69
74
  };
70
75
  return shared;
71
76
  }
72
77
  export function createRunnerDeps(ctx) {
73
- const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, memorySyncRunner, runtimeCapsResolver, fileSnapshotStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, } = ctx;
78
+ const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, memorySyncRunner, runtimeCapsResolver, fileSnapshotStore, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, } = ctx;
74
79
  // design/158 A10 留档发现②:共享基座——见 createSharedRunnerDeps 头注(main.ts 的第二处展开是
75
80
  // 并行车道待办,未在本次改动内接线)。
76
81
  const sharedRunnerDeps = createSharedRunnerDeps(ctx);
@@ -242,6 +247,9 @@ export function createRunnerDeps(ctx) {
242
247
  // memory dir). Absent (multi-tenant / MEMORY_ENGINE=off) ⇒ no deps.memoryBackend ⇒ memory dark.
243
248
  memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
244
249
  memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
250
+ // (sharedMemoryStores 由共享基座展开携带 —— 主/sub 两只 Runner 同源,见 createSharedRunnerDeps。
251
+ // 它与 `memoryBackend` **正交且完全不桥接**:那条是本地记忆链(注入索引 + 文件投影 + harvest,
252
+ // 有写路径),这条是纯文档只读面,共享内容永不进注入块/索引、永不落模型可写盘、永不被 harvest。)
245
253
  // S3-TOB 复审 F-9(operator 可观测底座):harvest 报告 → metrics(拒收/incident/patch 计数从此可见;
246
254
  // core swallow-guard 保证 throwing consumer 不伤边界)。
247
255
  onMemoryHarvestReport: memoryEngine
@@ -264,6 +272,9 @@ export function createRunnerDeps(ctx) {
264
272
  // E19: working-tree snapshot/restore for rewind (+ the 2c artifact store). core snapshots each completed turn +
265
273
  // restores on resumeAt when spec.rewindFiles is set, for ANY env when this store is wired (gate-split 1.134.0).
266
274
  fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
275
+ // #154 车二(core 5.18.0 design/179):持久化权限规则店 —— 引擎据它铸 `AskRequest.ruleSuggestions`
276
+ // 并把 `permissionRules.storeWired` 报成真。缺席=诚实缺席(零候选、storeWired:false),绝不占位。
277
+ permissionRuleStore: permissionRuleStore ? permissionRuleStore : undefined,
267
278
  // (executionEnvFactory/lspManager already carried by the shared-base spread above.)
268
279
  // design/129-B (core 1.240.0): the PROCESS-LEVEL background-child observer — spawn/tick/terminal
269
280
  // for every bg delegation child, never dying with a leg. Feeds the fleet rows (launch 即有行, session-scoped
@@ -867,6 +867,21 @@ export interface ServiceConfigFlat {
867
867
  * cap → posture-gated like askQuestion (single-user turnkey → ON; multi-tenant opt-in);
868
868
  * `TOOL_APPROVAL_ENABLED=true/false` overrides. Absent ⇒ core's headless auto-deny stands ([819]⑤ fail-closed). */
869
869
  toolApprovalEnabled: boolean;
870
+ /**
871
+ * #154 车二(core design/179 持久化权限规则「不再询问」车道)**总开关**。`PERMISSION_RULES_ENABLED`,
872
+ * **默认 OFF**。
873
+ *
874
+ * 🔴 为什么是显式 opt-in 而不是「有 SQL 后端就自动上」(codex 交叉复审 round7 [high] 一,验真后收):
875
+ * 这条车道**只会加放行、不会减**,而 v1 **还没有撤销面**(列出 / 删除自己已存规则的口子没建;core 的
876
+ * `removePersistedRule` 在包里,但本仓零调用点)。一次误导入的全局规则因此会一直生效,租户自己拿它
877
+ * 没办法。在撤销面落地之前,**默认不开**是这条轴唯一诚实的姿势;开它的部署等于明说「我接受这条状态,
878
+ * 并且知道现在只能靠运维改库来收回」。
879
+ *
880
+ * 关 ⇒ 规则店根本不装配:`RunnerDeps.permissionRuleStore` 缺席(引擎的 `permissionRules.storeWired`
881
+ * 如实报 false)、ask 帧不带 `ruleSuggestions`、回决的 `persistRule` 拒、`POST /v1/rules/cc-import/*`
882
+ * 两口 501。三张表仍随中央 `ensureSchema` 建(纯 CREATE、零行),这不是行为面。
883
+ */
884
+ permissionRulesEnabled: boolean;
870
885
  /** #151 车2(design/172 §3.3 D3,窗长三元的安全余量):`ToolApprovalCoordinator` 的可选 `windowMarginMs`
871
886
  * 构造项——有效窗 = `min(ttlMs, legRemainingMs − 本值)`,余量不足 ⇒ 不开窗直接走窗到期同路(park)。
872
887
  * 仅在装配点把 `ToolApprovalRunContext.legDeadlineMonotonic` 传给协调器(车3 的活)时才实际生效——本
@@ -1009,7 +1024,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
1009
1024
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
1010
1025
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
1011
1026
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
1012
- export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
1027
+ export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
1013
1028
  /** 组:memory(记忆面 + TOC 同步腿)。 */
1014
1029
  export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1015
1030
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
package/dist/config.js CHANGED
@@ -1053,6 +1053,10 @@ function parseApprovalDomain(ctx) {
1053
1053
  ttlMs: numEnvBounded("QUESTION_TTL_MS", String(DEFAULT_QUESTION_THROTTLE.ttlMs), 1_000, 3_600_000),
1054
1054
  },
1055
1055
  toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"), // [816]/[820]② live tool-approval HITL; posture-gated (single-user → ON), mirrors askQuestion
1056
+ // #154 车二:持久化权限规则车道的总开关。**默认 OFF**,理由(撤销面未落地之前只加放行不减)逐字见
1057
+ // `ServiceApprovalConfig.permissionRulesEnabled`。**不**做 posture 派生:这条轴与「单用户交钥匙」
1058
+ // 无关,它取决于运维接不接受「暂时只能改库收回」,那必须是一次显式表态。
1059
+ permissionRulesEnabled: boolEnv("PERMISSION_RULES_ENABLED", false),
1056
1060
  // #151 车2(design/172 §3.3 D3):ToolApprovalCoordinator 窗长三元的安全余量。numEnv fail-loud 形,
1057
1061
  // 照邻居旋钮(approvalTimeoutSec 等)抄——坏形(非数字)在启动期炸,不静默折成 NaN。
1058
1062
  streamAskWindowMarginMs: numEnv("STREAM_ASK_WINDOW_MARGIN_MS", "10000"),
@@ -1578,7 +1582,7 @@ const MODEL_PLANE_GROUP_KEYS = [
1578
1582
  const APPROVAL_GROUP_KEYS = [
1579
1583
  "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
1580
1584
  "approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1581
- "resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
1585
+ "resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
1582
1586
  "sensitiveWritePatterns", "manualModeShellGate",
1583
1587
  ];
1584
1588
  const MEMORY_GROUP_KEYS = [
@@ -0,0 +1,26 @@
1
+ /**
2
+ * design/183 §4.3 —— **收编面**(operator lane):`POST /v1/adoption` 发起、`GET /v1/adoption/:id` 读状态。
3
+ *
4
+ * 形态:一次性的**部署级动作**,不是常驻业务面 —— 所以它 billable=false(零模型工作)、operator-only、
5
+ * 且在没有 SQL 后端的部署上诚实 501(收编重绑的是多租身份轴,local 后端根本没有那个轴)。
6
+ *
7
+ * ── 三条拒的分家(183 §7.3 拒绝可判别)──────────────────────────────────────────────────────────
8
+ * · `adoption.source_already_bound` (409):同一个源已绑到**别的**目的地 = 二次收编/多租转让,
9
+ * 183 D7 明确拒,非本协议射程。与「已收编回执」不同形 —— 后者是 200。
10
+ * · `adoption.destination_conflict` (409):目的地侧已有与源侧重叠的逻辑键。**响应列出冲突表族**,
11
+ * 运维据此知道该去清理哪张表;源/目的地两侧字节零变更。
12
+ * · `adoption.destination_unrepresentable` (409):目的地身份装不进这次收编会**派生**出来的某个键
13
+ * (典型:memory 的 `proj:` 键 = 新 tenant 段 + 原有项目段后缀,合起来越过列宽)。与上一条分家的理由:
14
+ * 运维的动作完全不同 —— 那条要去清理另一个 principal 的行,这条要换一个更短的目的地身份。
15
+ * · `not_found.adoption` (404):operator 拿着一个不存在的 id 来读。
16
+ *
17
+ * 幂等:同参数重跑秒回**同一份** `AdoptionReceipt`(`immutableReport` 逐字节恒同 —— 它在 phase 6
18
+ * 一次落库,此后原样回放)。并发同参 POST 由 `adoption_log.from_principal` 的 UNIQUE 仲裁:恰一行落库,
19
+ * 两条连接拿到同一个 adoptionId、同一份回执。
20
+ *
21
+ * 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
22
+ */
23
+ import type { IncomingMessage, ServerResponse } from "node:http";
24
+ import type { RouteCtx } from "../route-ctx.js";
25
+ export declare function handleAdoption(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
26
+ //# sourceMappingURL=adoption.d.ts.map
@@ -0,0 +1,120 @@
1
+ import { createAdoptionRunner } from "../../adoption/runner.js";
2
+ import { AdoptionRequestSchema } from "../../adoption/wire.js";
3
+ import { checkAdoptionWidths } from "../../adoption/plan.js";
4
+ import { sendJson, sendError } from "../send.js";
5
+ import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
6
+ const ADOPTION_ID_RE = /^\/v1\/adoption\/([^/]+)$/;
7
+ /** 结局 → wire。**拒从不走 200 回执包络**(见顶注三条拒的分家)。
8
+ * 成功码写死 200(不收 `okStatus` 参数):一个**表达式**状态码会让 errorCode 归一门读不出这是成功面,
9
+ * 而且本面本来就只有一个成功码 —— 同参重跑必须与首次同形,重跑创建不了任何东西。 */
10
+ function sendOutcome(res, out) {
11
+ switch (out.kind) {
12
+ case "receipt":
13
+ sendJson(res, 200, { adoptionId: out.adoptionId, ...out.receipt });
14
+ return;
15
+ case "source_already_bound":
16
+ sendError(res, 409, "adoption.source_already_bound", "this source principal is already bound to a different adoption target", {
17
+ adoptionId: out.adoptionId,
18
+ fromPrincipal: out.fromPrincipal,
19
+ boundTo: out.boundTo,
20
+ });
21
+ return;
22
+ // 闭集穷举:`AdoptionRejectCode` 加一个成员而这里没加臂 ⇒ **编译红**(拒绝码是 wire 契约,
23
+ // 不许有一个走到默认臂的静默码)。
24
+ case "rejected":
25
+ switch (out.code) {
26
+ case "adoption.destination_conflict":
27
+ sendError(res, 409, "adoption.destination_conflict", "the destination principal already holds rows sharing a logical key with the source", {
28
+ adoptionId: out.adoptionId,
29
+ detail: out.detail,
30
+ conflicts: out.conflicts,
31
+ });
32
+ return;
33
+ case "adoption.destination_unrepresentable":
34
+ sendError(res, 409, "adoption.destination_unrepresentable", "the destination principal does not fit every key this adoption would derive from it", {
35
+ adoptionId: out.adoptionId,
36
+ detail: out.detail,
37
+ });
38
+ return;
39
+ }
40
+ return;
41
+ }
42
+ }
43
+ export async function handleAdoption(req, res, url, ctx) {
44
+ const miss = { fell: false };
45
+ await handleAdoptionBody(req, res, url, ctx, miss);
46
+ return !miss.fell;
47
+ }
48
+ async function handleAdoptionBody(req, res, url, ctx, miss) {
49
+ const { deps } = ctx;
50
+ const { readJson } = ctx.helpers;
51
+ const isPost = req.method === "POST" && url === "/v1/adoption";
52
+ const idMatch = req.method === "GET" ? ADOPTION_ID_RE.exec(url) : null;
53
+ if (!isPost && !idMatch) {
54
+ miss.fell = true;
55
+ return;
56
+ }
57
+ // 门序 = operator 面的既有口径:身份(401)→ 授权(403)→ 能力(501)→ 验型(400)。
58
+ // 授权在能力之前:一个够不着任何东西的调用方不该从「有没有 SQL 后端」上读出部署形态。
59
+ const principal = gatedPrincipal(req, deps.config); // direct-door safe: verified identity, never the spoofable header
60
+ if (deps.config.requirePrincipal && !principal) {
61
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
62
+ return;
63
+ }
64
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
65
+ sendError(res, 403, "auth.operator_only", "adoption is an operator-only deployment action");
66
+ return;
67
+ }
68
+ const store = deps.backend?.adoptionLog?.();
69
+ if (!store) {
70
+ sendError(res, 501, "capability.adoption_store_required", "adoption requires a SQL store backend (DB_BACKEND=mysql|pg)");
71
+ return;
72
+ }
73
+ const runner = createAdoptionRunner({ store, dialect: deps.backend?.kind === "pg" ? "pg" : "tidb", now: Date.now, logger: deps.logger });
74
+ if (idMatch) {
75
+ const id = ctx.helpers.safeDecode(idMatch[1]);
76
+ if (id === null) {
77
+ // 复用既有的 `request.id_invalid`(sessions/attachments 同族),不为一条新路由铸第二个同义码
78
+ // ——附录 A 的判据②:只有消费端真会分支时才铸新码。
79
+ sendError(res, 400, "request.id_invalid", "invalid adoption id");
80
+ return;
81
+ }
82
+ const out = await runner.get(id);
83
+ if (!out) {
84
+ sendError(res, 404, "not_found.adoption", "adoption not found");
85
+ return;
86
+ }
87
+ sendOutcome(res, out);
88
+ return;
89
+ }
90
+ let raw;
91
+ try {
92
+ raw = await readJson(req);
93
+ }
94
+ catch {
95
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
96
+ return;
97
+ }
98
+ const parsed = AdoptionRequestSchema.safeParse(raw);
99
+ if (!parsed.success) {
100
+ sendError(res, 400, "request.body_shape", "invalid adoption body — expected {fromPrincipal, toPrincipal} as non-empty strings");
101
+ return;
102
+ }
103
+ const { fromPrincipal, toPrincipal } = parsed.data;
104
+ // 🔴 列宽校验在**铸行之前**(codex R1-F3):合法长度的 principal 也可能装不进最窄的那根被写列,
105
+ // 而那种失败若发生在腿的中途,phase 会永远停在 INTENT —— 每次重发和每次 boot 扫描都再撞一次同样的墙。
106
+ const tooWide = checkAdoptionWidths(fromPrincipal, toPrincipal);
107
+ if (tooWide !== undefined) {
108
+ sendError(res, 400, "request.field_invalid", "the adoption principals do not fit every identity column and derived key this adoption must write", { detail: tooWide });
109
+ return;
110
+ }
111
+ if (fromPrincipal === toPrincipal) {
112
+ // 183 §8 的「nothing to adopt」分支:同名不是一次无操作的成功,它是一个提错了的请求
113
+ // ——铸一行 adoption_log 会永久占掉那个 from 的 UNIQUE 名额(D7 之后再也收编不了),所以拒在入库之前。
114
+ sendError(res, 400, "request.field_invalid", "fromPrincipal and toPrincipal are the same — nothing to adopt");
115
+ return;
116
+ }
117
+ const out = await runner.start(fromPrincipal, toPrincipal);
118
+ sendOutcome(res, out);
119
+ }
120
+ //# sourceMappingURL=adoption.js.map
@@ -81,6 +81,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
81
81
  // the old caps honestly hides the affordances (the adaptation was pre-announced).
82
82
  memory: false,
83
83
  memoryWrite: false,
84
+ // design/177 —— org 共享记忆库只读面。判据**逐字**等于 server.ts 里那个域的挂载合取式(供给面 ∧
85
+ // org 折叠面),所以"说 yes ⟺ 面真能用"是结构性的:少一个,域不挂载、这里也翻假,消费方看到的
86
+ // 与它真能打通的永远一致。折叠面缺席时刻意不报 true —— 一个没有成员性判据的共享读面比没有更糟。
87
+ sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
88
+ // design/183 form b:收编面(operator lane)。谓词与 handleAdoption 的 501 同源:SQL backend 的
89
+ // adoptionLog 工厂在场(local backend 刻意不实现——单用户数据根没有多租身份轴可重绑)。
90
+ // SDK 半场车逮的能力位缺口:两条新车道此前只能 trial-by-501,违本仓「says yes ⟺ route works」姿势。
91
+ adoption: Boolean(deps.backend?.adoptionLog),
92
+ // #154 车二:规则车道(cc-import 两口 + respond persistRule 兑付口)。谓词与 /v1/rules/cc-import
93
+ // 的 501 同源:ruleConsent lane 在场 ⟺ PERMISSION_RULES_ENABLED=true ∧ 规则店 wired(默认 OFF ⇒
94
+ // 恒 false=诚实;撤销面落地前多租户部署不该看到 true)。
95
+ permissionRules: Boolean(deps.ruleConsent),
84
96
  // CLI surfaces: /v1/policy is always served (config surface); /v1/usage returns real
85
97
  // cumulative spend only when a per-principal quota is configured (else enabled:false).
86
98
  policy: true,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * #154 车二 —— **CC settings 导入两口**(design/179 §7 的 wire 半场)。
3
+ *
4
+ * POST /v1/rules/cc-import/prepare → 预览 + 一张**一次性、principal 绑定、有期、载荷绑定**的票
5
+ * POST /v1/rules/cc-import/redeem → 原子消费票 → confirm → 批量兑付进店
6
+ *
7
+ * **lane = principal**(不是 operator):用户导的是**他自己**的规则,一条规则的语义就是「这个人自己
8
+ * 说过 yes」。operator 在这条路上没有位置——他既不该替租户扩权,也不该被要求代人点确认。
9
+ * 因此本域**只认已验明的 principal**,连 `explicitOperatorOk` 的旁路都不给:一个 operator 想给自己
10
+ * 导规则,用他自己的 principal 走同一条路即可(那时他就是租户)。
11
+ *
12
+ * **billable = false**:两口都不烧模型(读 settings 文本、写规则行),不进 `isBillableSubmitPath`。
13
+ *
14
+ * 🔴 **四类拒绝在 wire 上同形 404**(信任边界 ①):unknown ticket / 别人的 ticket / 过期 / 已用过 ——
15
+ * 四个成因返回不同的码就是一台存在性 oracle(拿别人的票撞库能探到「它在别处存在」)。分类只进服务端
16
+ * 日志,不上 wire。同族先例 = run/trace 属主门与 `POST /v1/tasks/:id/asks/:askId/decision` 的 404 姿势。
17
+ */
18
+ import type { IncomingMessage, ServerResponse } from "node:http";
19
+ import type { RouteCtx } from "../route-ctx.js";
20
+ export declare const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
21
+ export declare const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
22
+ export declare function handleRules(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
23
+ //# sourceMappingURL=rules.d.ts.map