@sema-agent/server 7.9.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 (82) hide show
  1. package/USAGE.md +6 -0
  2. package/dist/adoption/plan.d.ts +152 -0
  3. package/dist/adoption/plan.js +513 -0
  4. package/dist/adoption/runner.d.ts +54 -0
  5. package/dist/adoption/runner.js +505 -0
  6. package/dist/adoption/sql.d.ts +76 -0
  7. package/dist/adoption/sql.js +106 -0
  8. package/dist/adoption/wire.d.ts +250 -0
  9. package/dist/adoption/wire.js +153 -0
  10. package/dist/approval-card.d.ts +24 -0
  11. package/dist/approval-card.js +32 -0
  12. package/dist/approval-reconciler.d.ts +1 -1
  13. package/dist/approval-reconciler.js +1 -1
  14. package/dist/boot/adoption.d.ts +30 -0
  15. package/dist/boot/adoption.js +57 -0
  16. package/dist/boot/coordinators.d.ts +4 -0
  17. package/dist/boot/coordinators.js +3 -1
  18. package/dist/boot/parked-revive-gate.d.ts +38 -5
  19. package/dist/boot/parked-revive-gate.js +53 -6
  20. package/dist/boot/reapers.js +3 -3
  21. package/dist/boot/runner-deps.d.ts +10 -2
  22. package/dist/boot/runner-deps.js +12 -1
  23. package/dist/capabilities/repo-tools.d.ts +36 -2
  24. package/dist/capabilities/repo-tools.js +125 -11
  25. package/dist/capabilities/scenarios.d.ts +2 -2
  26. package/dist/capabilities/scenarios.js +1 -1
  27. package/dist/config-types.d.ts +22 -2
  28. package/dist/config.js +44 -2
  29. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  30. package/dist/fleet/fleet-terminal-window.js +1 -1
  31. package/dist/git-api-kind.d.ts +7 -0
  32. package/dist/git-api-kind.js +8 -0
  33. package/dist/http/routes/adoption.d.ts +26 -0
  34. package/dist/http/routes/adoption.js +120 -0
  35. package/dist/http/routes/approvals-assistant.js +2 -2
  36. package/dist/http/routes/capabilities.js +12 -0
  37. package/dist/http/routes/rules.d.ts +23 -0
  38. package/dist/http/routes/rules.js +117 -0
  39. package/dist/http/routes/shared-memory.d.ts +31 -0
  40. package/dist/http/routes/shared-memory.js +181 -0
  41. package/dist/http/routes/trace-usage.js +139 -3
  42. package/dist/http/server.d.ts +19 -1
  43. package/dist/http/server.js +47 -5
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/main.js +54 -5
  47. package/dist/observability/fail-open.d.ts +8 -0
  48. package/dist/observability/fail-open.js +8 -0
  49. package/dist/plugins/adoption-log-sql.d.ts +191 -0
  50. package/dist/plugins/adoption-log-sql.js +273 -0
  51. package/dist/plugins/checkpoint-store-sql.d.ts +22 -9
  52. package/dist/plugins/checkpoint-store-sql.js +40 -29
  53. package/dist/plugins/local-checkpoint-store.d.ts +11 -1
  54. package/dist/plugins/local-checkpoint-store.js +10 -2
  55. package/dist/plugins/memory-engine-pg.js +3 -3
  56. package/dist/plugins/permission-rule-store-sql.d.ts +242 -0
  57. package/dist/plugins/permission-rule-store-sql.js +817 -0
  58. package/dist/plugins/pg-pool.js +44 -7
  59. package/dist/plugins/run-store-sql.d.ts +3 -3
  60. package/dist/plugins/run-store-sql.js +3 -3
  61. package/dist/plugins/session-policy-store-sql.d.ts +6 -0
  62. package/dist/plugins/session-policy-store-sql.js +7 -1
  63. package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
  64. package/dist/plugins/shared-memory-store-sql.js +516 -0
  65. package/dist/plugins/store-backend.d.ts +30 -0
  66. package/dist/plugins/store-backend.js +14 -0
  67. package/dist/plugins/tidb-pool.js +55 -8
  68. package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
  69. package/dist/plugins/workflow-journal-store-sql.js +6 -6
  70. package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
  71. package/dist/plugins/workflow-run-store-sql.js +12 -12
  72. package/dist/rules-consent.d.ts +126 -0
  73. package/dist/rules-consent.js +198 -0
  74. package/dist/run-local.js +2 -2
  75. package/dist/shared-memory-scope-authorizer.d.ts +29 -0
  76. package/dist/shared-memory-scope-authorizer.js +17 -0
  77. package/dist/tool-approval.d.ts +55 -0
  78. package/dist/tool-approval.js +124 -5
  79. package/dist/trace/core-keyset-guard.d.ts +2 -2
  80. package/dist/trace/project.d.ts +1 -0
  81. package/dist/trace/project.js +1 -0
  82. package/package.json +3 -3
@@ -0,0 +1,181 @@
1
+ import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError } from "@sema-agent/core";
2
+ import { sendJson, sendError } from "../send.js";
3
+ import { gatedPrincipal } from "../principal-gate.js";
4
+ const PREFIX = "/v1/shared-memory/";
5
+ /** `unavailable` 时给客户端的重试间隔。目录退避窗与库导入都在秒级,10s 是"别打穿"与"别显得死了"之间。 */
6
+ const RETRY_AFTER_SEC = 10;
7
+ /** 一次 documents 列举的窗口。core 的模型面用 50;HTTP 面给同一个数量级,靠 cursor 续窗。 */
8
+ const HTTP_LIST_LIMIT = 100;
9
+ /** core 的文档路径规则,HTTP 参数面复述:绝对、无控制字符/反斜杠、段非空且不以 `.` 开头、文件名带后缀。 */
10
+ const FORBIDDEN_PATH_CHARS = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Default_Ignorable_Code_Point}\u2028\u2029\\]/u;
11
+ const DOCUMENT_SUFFIXES = [".md", ".txt", ".json", ".jsonl"];
12
+ function isDocumentPath(path) {
13
+ if (!path.startsWith("/") || FORBIDDEN_PATH_CHARS.test(path))
14
+ return false;
15
+ const segments = path.slice(1).split("/");
16
+ const last = segments.at(-1);
17
+ if (last === undefined || !segments.every((s) => s !== "" && !s.startsWith(".")))
18
+ return false;
19
+ return DOCUMENT_SUFFIXES.some((s) => last.endsWith(s));
20
+ }
21
+ /** 零存在性 oracle 的那**一句**拒绝。三种成因(库不存在 / 不是成员 / 是别的 org 的库)共用它,因此
22
+ * 调用方无法用响应差异枚举别人的库。改这里的字面量时三条腿一起改——它们同形是**被测断言**。 */
23
+ function refuseStore(res) {
24
+ sendError(res, 404, "not_found.shared_memory_store", "not found");
25
+ }
26
+ /** 供给面自述不可用 ⇒ 503 + Retry-After。**绝不**塌成「200 空库」:后者对调用方是终局的
27
+ * 「你们团队没有共享库」,而真相是「暂时读不到,一会儿再来」——两句话是相反的指令。 */
28
+ function refuseUnavailable(res, message) {
29
+ res.setHeader("retry-after", String(RETRY_AFTER_SEC));
30
+ sendError(res, 503, "memory.shared_unavailable", "shared memory stores are unavailable", {
31
+ retryAfterSec: RETRY_AFTER_SEC,
32
+ ...(message !== undefined ? { detail: message } : {}),
33
+ });
34
+ }
35
+ /** 取本次请求的绑定快照。返回 `undefined` = 已经应答(401/503),调用方直接 return。 */
36
+ async function bindSnapshot(req, res, ctx) {
37
+ const { deps } = ctx;
38
+ // 身份先于可用性(memory-policy 同款次序):后端姿态不该对一个本来什么都读不到的调用方 oracle。
39
+ const principal = gatedPrincipal(req, deps.config); // direct-door safe: verified identity, never the spoofable header
40
+ if (!principal && deps.config.requirePrincipal === true) {
41
+ sendError(res, 401, "auth.unauthorized", "unauthorized");
42
+ return undefined;
43
+ }
44
+ const provider = deps.sharedMemoryStore;
45
+ if (!provider) {
46
+ // 到不了:域头的合取式已经保证它在场。写出来是为了让类型收窄不靠断言。
47
+ refuseStore(res);
48
+ return undefined;
49
+ }
50
+ const snapshot = await provider.snapshot({ sessionId: `http:${principal ?? "anonymous"}`, taskId: "http-read", ...(principal ? { principal } : {}) }, {});
51
+ if (snapshot.state === "unavailable") {
52
+ refuseUnavailable(res, snapshot.message);
53
+ return undefined;
54
+ }
55
+ return snapshot;
56
+ }
57
+ /**
58
+ * reader 抛出的 **typed** 失败 → HTTP 的逐格映射。返回 `true` = 已应答。
59
+ *
60
+ * 🔴 codex 复审轮2 F2(真缺陷):不折叠的话这些全落进外层兜底的 500 `internal.error`。而它们**不是**
61
+ * 服务器缺陷:`store_not_found` 是治理面在绑定之后回收了库(正常竞态),`refused/document_too_large`
62
+ * 是一行旧数据(可恢复状况)。更要命的是——真 SQL 供给面对超帽行是**抛**而不是返回内容,所以本文件
63
+ * 那条 413 分支对它根本不可达,除非在这里折。
64
+ *
65
+ * `kind` 是 core 的闭集,用穷举 `switch` 消费:core 加员时这里当场编译红,而不是悄悄落进某个 default。
66
+ */
67
+ function foldReaderError(res, err) {
68
+ if (!(err instanceof SharedMemoryStoreError))
69
+ return false;
70
+ switch (err.kind) {
71
+ case "store_not_found":
72
+ refuseStore(res); // 与"你不是成员"同形——绑定之后库没了,对调用方就是"它不存在"
73
+ return true;
74
+ case "refused":
75
+ if (err.reason === "document_too_large") {
76
+ // 与模型面同一条规则:拒绝,不截断(一份被截断的共享文档读起来和完整的一模一样)。
77
+ sendError(res, 413, "request.payload_too_large", `document exceeds the ${SHARED_MEMORY_READ_CAP_BYTES}-byte shared-store read cap`, { capBytes: SHARED_MEMORY_READ_CAP_BYTES });
78
+ return true;
79
+ }
80
+ // host 自述的其它拒绝(配额等):按"现在读不到,带着机器原因重试"回,原因原样进 detail。
81
+ refuseUnavailable(res, err.reason);
82
+ return true;
83
+ case "transient":
84
+ refuseUnavailable(res, err.reason);
85
+ return true;
86
+ }
87
+ }
88
+ /** 快照里解析一个 store,越界返回 `undefined` 并已应答 404(同形拒)。 */
89
+ function resolveStore(res, snapshot, storeId) {
90
+ if (snapshot.state !== "connected") {
91
+ refuseStore(res); // connecting 状态下没有绑定,任何具名库对这次请求都还不存在
92
+ return undefined;
93
+ }
94
+ const found = snapshot.stores.find((s) => s.info.id === storeId);
95
+ if (!found) {
96
+ refuseStore(res);
97
+ return undefined;
98
+ }
99
+ return found.reader;
100
+ }
101
+ export async function handleSharedMemory(req, res, url, ctx) {
102
+ const miss = { fell: false };
103
+ await handleSharedMemoryBody(req, res, url, ctx, miss);
104
+ return !miss.fell;
105
+ }
106
+ async function handleSharedMemoryBody(req, res, url, ctx, miss) {
107
+ // GET /v1/shared-memory/stores —— 本会话可见的库清单(= 模型面 memory_list 无参形的同一份)。
108
+ if (url === "/v1/shared-memory/stores" && req.method === "GET") {
109
+ const snapshot = await bindSnapshot(req, res, ctx);
110
+ if (!snapshot)
111
+ return;
112
+ if (snapshot.state !== "connected") {
113
+ // 诚实的「还没好」:既不是错误(没坏)也不是空(不是终局)。与 core 模型面的 CONNECTING 同义。
114
+ // (`unavailable` 已在 bindSnapshot 里 503 掉,走不到这里;判 `!== "connected"` 是为了让类型
115
+ // 收窄不靠断言 —— 联合加员时这里当场编译红,而不是悄悄落进某个 else。)
116
+ sendJson(res, 200, { state: "connecting", stores: [] });
117
+ return;
118
+ }
119
+ sendJson(res, 200, { state: "connected", stores: snapshot.stores.map((s) => ({ id: s.info.id, description: s.info.description, writable: s.info.writable })) });
120
+ return;
121
+ }
122
+ const rest = url.startsWith(PREFIX) ? url.slice(PREFIX.length) : undefined;
123
+ const segments = rest === undefined ? [] : rest.split("/");
124
+ // stores/:store/documents | stores/:store/document
125
+ if (segments.length === 3 && segments[0] === "stores" && req.method === "GET" && (segments[2] === "documents" || segments[2] === "document")) {
126
+ const storeId = decodeURIComponent(segments[1]);
127
+ const query = new URL(req.url ?? "", "http://x").searchParams; // route-level `url` is query-stripped
128
+ const snapshot = await bindSnapshot(req, res, ctx);
129
+ if (!snapshot)
130
+ return;
131
+ const reader = resolveStore(res, snapshot, storeId);
132
+ if (!reader)
133
+ return;
134
+ if (segments[2] === "documents") {
135
+ const prefix = query.get("path_prefix") ?? undefined;
136
+ const cursor = query.get("cursor") ?? undefined;
137
+ const page = await reader.list(prefix, { ...(cursor !== undefined ? { cursor } : {}), limit: HTTP_LIST_LIMIT }).catch((err) => {
138
+ if (foldReaderError(res, err))
139
+ return undefined;
140
+ throw err; // 非 typed 的失败仍归外层兜底(那才真是服务器缺陷)
141
+ });
142
+ if (page === undefined)
143
+ return;
144
+ const entries = Array.isArray(page) ? page : page.entries;
145
+ const nextCursor = Array.isArray(page) ? undefined : page.nextCursor;
146
+ sendJson(res, 200, { store: storeId, entries, ...(nextCursor !== undefined ? { nextCursor } : {}) });
147
+ return;
148
+ }
149
+ const path = query.get("path");
150
+ if (path === null || path === "" || !isDocumentPath(path)) {
151
+ sendError(res, 400, "request.query_invalid", "missing or malformed ?path= (absolute, .md/.txt/.json/.jsonl)");
152
+ return;
153
+ }
154
+ let folded = false;
155
+ const doc = await reader.read(path, {}).catch((err) => {
156
+ if (foldReaderError(res, err)) {
157
+ folded = true;
158
+ return null;
159
+ }
160
+ throw err;
161
+ });
162
+ if (folded)
163
+ return;
164
+ if (doc === null) {
165
+ // 成员本人有权区分「打错了路径」与「无权」——见文件头注。
166
+ sendError(res, 404, "not_found.shared_memory_document", "not found");
167
+ return;
168
+ }
169
+ const bytes = Buffer.byteLength(doc.content, "utf8");
170
+ if (bytes > SHARED_MEMORY_READ_CAP_BYTES) {
171
+ // core 的模型面对超帽文档是**拒绝并报出真实大小**,从不截断(一份被截断的共享文档读起来和完整的
172
+ // 一模一样,而那正是这个帽存在的理由)。HTTP 面用同一条规则,两个面因此对「什么读得到」永不分歧。
173
+ sendError(res, 413, "request.payload_too_large", `document is ${bytes} bytes, over the ${SHARED_MEMORY_READ_CAP_BYTES}-byte shared-store read cap`, { bytes, capBytes: SHARED_MEMORY_READ_CAP_BYTES });
174
+ return;
175
+ }
176
+ sendJson(res, 200, { store: storeId, path, content: doc.content, bytes, ...(doc.updatedAt !== undefined ? { updatedAt: doc.updatedAt } : {}) });
177
+ return;
178
+ }
179
+ miss.fell = true;
180
+ }
181
+ //# sourceMappingURL=shared-memory.js.map
@@ -1,10 +1,26 @@
1
+ import { buildToolResultRef } from "@sema-agent/core";
1
2
  import { projectEvents, taskSummary, mapTraceEvent } from "../../trace/project.js";
2
3
  import { projectArtifacts } from "../../trace/artifacts.js";
3
4
  import { usageSummary, usageSeries, usageBreakdown } from "../../usage-analytics.js";
4
5
  import { sleep } from "../sse-log.js";
5
6
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
6
7
  import { gatedPrincipal } from "../principal-gate.js";
7
- const TRACE_RE = /^\/v1\/tasks(?:\/([^/]+)(\/turns|\/stream|\/artifacts))?$/;
8
+ // [3321] tool-results 读面**共用这一段守卫**([3318] 的字面承诺):新 sub 进的是同一个 `sub` 捕获组,
9
+ // 于是下方 fleetWide / gateOwner / per-id owner-404 三道门原样罩住它,不存在第二套门可以漂。
10
+ // ⚠️ 「both-or-neither」结构不可破:`:id` 与 `sub` 同在一个可选组里,所以 TRACE_RE 从不匹配裸
11
+ // `/v1/tasks/<seg>` —— 那正是 `POST /v1/tasks/stream` 不被本读面遮蔽的原因(见下方 source-summary 注)。
12
+ // 新臂写成 `sub` 的一个 alternation 分支(而不是新开捕获组),组号与既有两个捕获保持逐字不变。
13
+ const TRACE_RE = /^\/v1\/tasks(?:\/([^/]+)(\/turns|\/stream|\/artifacts|\/tool-results\/[^/]+))?$/;
14
+ /** tool-results 读面的 sub 前缀(路由判别 + 段切分单源)。 */
15
+ const TOOL_RESULTS_SUB = "/tool-results/";
16
+ /** `limit` 的 server 权威上限(chars)。缺席 = 不传给 store,用 store 自己的默认;超上限 = 400(不静默夹取:
17
+ * 夹取会让「我要 1M」与「我要 256K」返回同一段内容,调用方无从知道自己被截了)。 */
18
+ const TOOL_RESULT_LIMIT_CAP = 262_144;
19
+ /** `offset` 的上限 = **最窄后端**能表达的值(codex 复审 R1 medium,已核真)。PG 那半边发的是
20
+ * `SUBSTRING(content FROM $1::int …)` 且绑的是 `offset + 1`(`plugins/tool-result-store-sql.ts`),
21
+ * 于是 `offset = 2147483647` 过得了「非负整数」这一关,却在库里变成 `2147483648::int` ⇒ 范围错 ⇒ 500。
22
+ * 「坏输入回 400」是本面的承诺,承诺不能挑后端兑现,所以上限按 int32 - 1 收(留出 +1 的余量)。 */
23
+ const TOOL_RESULT_OFFSET_CAP = 2_147_483_646;
8
24
  export async function handleTraceUsage(req, res, url, ctx) {
9
25
  const miss = { fell: false };
10
26
  await handleTraceUsageBody(req, res, url, ctx, miss);
@@ -14,7 +30,7 @@ export async function handleTraceUsage(req, res, url, ctx) {
14
30
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
15
31
  async function handleTraceUsageBody(req, res, url, ctx, miss) {
16
32
  const { deps } = ctx;
17
- const { isFleetWide } = ctx.helpers;
33
+ const { isFleetWide, safeDecode } = ctx.helpers;
18
34
  // Task Trace API (S1, read-only): GET /v1/tasks[/:id/turns|/stream]. Authorized by EITHER the full authToken
19
35
  // OR a read-only traceToken (sema-registry pulls traces fleet-wide with one read-only token). Handled BEFORE
20
36
  // the global gate (like /metrics) so a traceToken-only caller isn't 401'd.
@@ -61,14 +77,19 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
61
77
  }
62
78
  const query = new URL(req.url ?? "", "http://x").searchParams;
63
79
  const taskId = traceMatch[1];
64
- const sub = traceMatch[2]; // undefined (list) | "/turns" | "/stream" | "/artifacts"
80
+ const sub = traceMatch[2]; // undefined (list) | "/turns" | "/stream" | "/artifacts" | "/tool-results/<ref>"
65
81
  // per-id owner gate for the principal path (404 for a non-owner — no existence oracle, parity with runOwnerOk).
82
+ // 🔴 该行**只在 principal 路径**被取(fleet 路径按设计不做 per-id 归属判定)——tool-results 面还要用
83
+ // `run.sessionId` 算 ref 前缀,所以这里把取到的行留住(`ownedRun`),fleet 路径由该面自己补一次取行:
84
+ // 少了那次取行,fleet caller 就能拿任意 taskId 配任意 ref 读走**别的 session** 的产物。
85
+ let ownedRun;
66
86
  if (taskId && gateOwner !== null) {
67
87
  const run = await deps.runStore.getRun(taskId);
68
88
  if (!run || (run.owner !== null && run.owner !== gateOwner)) {
69
89
  sendError(res, 404, "not_found.run", "task not found");
70
90
  return;
71
91
  }
92
+ ownedRun = run;
72
93
  }
73
94
  if (!taskId)
74
95
  await handleTaskList(res, deps.runStore, query, gateOwner); // gateOwner (when set) forces the owner filter
@@ -78,6 +99,9 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
78
99
  await streamTaskTrace(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000);
79
100
  else if (sub === "/artifacts")
80
101
  await handleTaskArtifacts(res, deps.runStore, taskId);
102
+ else if (sub?.startsWith(TOOL_RESULTS_SUB)) {
103
+ await handleToolResult(res, deps.toolResultStore, deps.runStore, taskId, ownedRun, safeDecode(sub.slice(TOOL_RESULTS_SUB.length)), query);
104
+ }
81
105
  else
82
106
  sendError(res, 404, "not_found.route", "not found");
83
107
  return;
@@ -202,6 +226,118 @@ async function handleTaskArtifacts(res, runStore, taskId) {
202
226
  const events = await runStore.getEvents(taskId, 0);
203
227
  sendJson(res, 200, { artifacts: projectArtifacts(events, { taskId, jobId: run.jobId }) });
204
228
  }
229
+ /**
230
+ * GET /v1/tasks/:id/tool-results/:ref?offset&limit —— core `ToolResultStore.get` 的 wire 镜像([3321])。
231
+ *
232
+ * 三件事,顺序本身就是契约:
233
+ *
234
+ * ① **query 先判**。坏 offset/limit ⇒ 400,且这一判**先于**任何 ref/归属/store 在场性判定 —— 否则
235
+ * 「坏 query + 已知 ref = 400」而「坏 query + 未知 ref = 404」,状态码之差立刻成了存在性 oracle。
236
+ * query 的合法性只取决于 query 自身,与调用方看不看得见这个 ref 无关,所以先判是安全的。
237
+ *
238
+ * ② **归属绑定 = 对本 task 的耐久日志做精确成员判定**,不解析 ref、不复制段转义规则。
239
+ *
240
+ * 先说为什么**不是**前缀判定([3321]② 的字面形、[3322] 的指定形):core 的 ref 形是
241
+ * `tr_<seg(session)>_<seg(toolCallId)>`,而 `_` 在两段里都合法 ⇒ session `team` 的前缀 `tr_team_`
242
+ * 同样命中 session `team_blue` 的 ref(`tr_team_blue_toolu_x`)。本仓的 sessionId 是**调用方自选的
243
+ * 任意 ≤64 字符串**(`http/server.ts` 的 submit 面只查长度,刻意宽松),所以租户 A 可以把自己的
244
+ * sessionId 取成租户 B 的前缀,拿自己的 taskId 配 B 的 ref 读走 B 的产物(codex 复审 R1 high,已核真)。
245
+ * 这在字符串层面**无解**:把余段当 toolCallId 重铸一遍会逐字相等(拼接本身有歧义),而按「更长的
246
+ * session 是否存在」去探,既堵不住折成 base+sha 的 sessionId(候选串 ≠ 真 id),又反手造出一个新的
247
+ * 跨租户**拒绝**面(别人注册一个 `<你的 session>_toolu` 就能让你自己的 ref 永远 404)——R2 复审两条
248
+ * 中标,已撤回该启发式。
249
+ *
250
+ * 改判据:ref **必须由本 task 自己的耐久事件日志作证**。日志里每条 tool_start/tool_end 都带
251
+ * `toolCallId`(`trace/project.ts` 的 toolStartEventData / toolEndEventData 是唯一铸造点),用 core
252
+ * 的**同一个** `buildToolResultRef` 把它们铸成 ref 集合,再要求 `ref` 是其中一员 —— 精确相等,
253
+ * 零解析、零转义规则复制,歧义面结构上不存在(A 的 taskId 只作证 A 自己的 toolCallId)。
254
+ * 辖域(诚实写明,也正是本路由的语义):本面服务的是**这条 task 的 trace 里看得见的那些产物** ——
255
+ * trace 的 tool-result 块与本判据读的是同一份日志,所以「trace 上看得见 ⇒ 这里读得到」成立;
256
+ * 同 session 里**别的 task** 的 ref(或日志已过保留窗被逐出的)按 404 同形拒,请到那条 task 上读。
257
+ *
258
+ * ⚠️ 两处**已知残余**,都要 core 侧动刀才能消,写在明处而不是让下一个人自己撞(codex 复审 R3):
259
+ *
260
+ * (a) **覆盖面**:core 除了「按工具调用 id 卸载」这条主路,还从**合成 id** 铸 ref ——
261
+ * `task-registry-monitor` 的 `<handle>_<stream>_seg<n>`、`task-registry-agent` 的 `<handle>_c<n>`、
262
+ * `runner/compaction-call-options` 的 `<toolCallId>_s<sha>`、budget 的 `idx<k>`。这些 id **不进**
263
+ * 耐久事件日志(日志记的是外层调用 id),所以那几类 ref 在本面一律 404 同形拒。
264
+ * 方向明确:**不**在本仓照抄那几条命名规则去反推(复制上游规则 = 两处各自漂,宪法禁),
265
+ * 真解是 core 在写入时带上 (session/task) 出处、或给出 core 属主的精确成员判定。
266
+ *
267
+ * (b) **注入性**:`buildToolResultRef` 对 `(session, toolCallId)` **不是单射**(`_` 两段都合法):
268
+ * `("team","blue_toolu_1")` 与 `("team_blue","toolu_1")` 铸出同一枚 ref。所以「本 task 日志作证过
269
+ * 一个恰好长成 `<受害 session 尾巴>_<受害 toolCallId>` 的 toolCallId」时,判据仍会放行。
270
+ * 与被撤掉的前缀形的差别是**门槛**:前缀形只要知道 ref 就能读,本形还要求攻击者能让**模型网关**
271
+ * 铸出那一枚精确的 toolCallId(provider 铸,本服务不控也不产)。同样只有 core 换成单射编码才真消。
272
+ * 这里**不留特征化测试**去钉这条通路 —— 把缺陷钉成契约是另一种病。
273
+ *
274
+ * ③ **同形 404**(unknown ref / 未获本 task 日志作证 / 本部署没有 durable store)。任何可区分的响应都是
275
+ * 缺陷:前两者之差会告诉调用方「这枚 ref 存在,只是不是你的」;第三者若回 501 则会把部署形告诉一个
276
+ * 连自己的 ref 都读不到的 caller。文案恒定且**不回显 ref**(回显=把输入原样反射进错误面)。
277
+ * 残余面(诚实写明):响应字节同形,**时序**并不同形 —— 三条臂到达的后端调用数不同。要抹平时序得让
278
+ * 每条臂都跑满全部后端调用,那是另一档代价/收益裁定,不在本批;本批的判据是响应本身不给 oracle。
279
+ */
280
+ async function handleToolResult(res, store, runStore, taskId, ownedRun,
281
+ /** 已过 `safeDecode` 的 ref;`null` = %-序列坏(客户端错,但同形 404 —— 它同样不该成为一条可区分的信道)。
282
+ * 解码用调用点传下来的 helper 而不是本地 try/catch:那条规则(坏序列 ⇒ null,绝不 500)在本仓只有
283
+ * `safeDecode` 一个属主,复制一份等于把它变成两个。 */
284
+ ref, query) {
285
+ // ① query 校验(先于一切存在性判定 —— 见头注)。非负十进制整数,`Number()` 的宽松形(空串/前后空白/
286
+ // "1e3"/"0x10"/"1.5")一律不认:宽松解析会让两个不同的请求串静默映射到同一段内容。
287
+ const intParam = (name, cap) => {
288
+ const raw = query.get(name);
289
+ if (raw === null)
290
+ return undefined; // 缺席 = 用 store 默认
291
+ if (!/^\d+$/.test(raw))
292
+ return "bad";
293
+ const n = Number(raw);
294
+ if (!Number.isSafeInteger(n))
295
+ return "bad";
296
+ if (cap !== undefined && n > cap)
297
+ return "bad";
298
+ return n;
299
+ };
300
+ const offset = intParam("offset", TOOL_RESULT_OFFSET_CAP);
301
+ if (offset === "bad") {
302
+ sendError(res, 400, "request.query_invalid", "offset must be a non-negative integer of at most 2147483646");
303
+ return;
304
+ }
305
+ const limit = intParam("limit", TOOL_RESULT_LIMIT_CAP);
306
+ if (limit === "bad") {
307
+ sendError(res, 400, "request.query_invalid", "limit must be a non-negative integer of at most 262144");
308
+ return;
309
+ }
310
+ // ③ 同形 404 的**唯一**下发点:每条臂都必须走这里,不许任何一条自己写一句(文案分家=oracle 复活)。
311
+ const notFound = () => sendError(res, 404, "not_found.tool_result", "tool result not found");
312
+ if (ref === null)
313
+ return notFound(); // 坏 %-序列(见参数注)
314
+ // fleet 路径没做 per-id 取行(见调用点注),这里补齐 —— 归属绑定要 run.sessionId,没有行就没有绑定面。
315
+ const run = ownedRun ?? (await runStore.getRun(taskId));
316
+ if (!run)
317
+ return notFound();
318
+ // ② 精确成员判定(见头注):本 task 耐久日志作证过的 toolCallId,经 core 同一个铸造点变成 ref 集合。
319
+ // 成本口径:整份日志一读一扫,与同域的 `/turns`、`/artifacts` 同款(不是本面引入的新量级);分页读会
320
+ // 逐页重扫 —— 真消要一张 (task, ref) 出处索引,与上面 (a) 是同一件 core/store 侧的活。
321
+ const events = await runStore.getEvents(taskId, 0);
322
+ let attested = false;
323
+ for (const ev of events) {
324
+ // 取键姿势与同域的 `trace/artifacts.ts` 逐字同款(同一份日志、同一个字段,读法不该有第二种)。
325
+ const { toolCallId: id } = (ev.data ?? {});
326
+ // 逐条比对而不是先建全集:命中即停,长日志上不必把整份集合物化。
327
+ if (typeof id === "string" && id.length > 0 && buildToolResultRef(run.sessionId, id) === ref) {
328
+ attested = true;
329
+ break;
330
+ }
331
+ }
332
+ if (!attested)
333
+ return notFound();
334
+ if (!store)
335
+ return notFound(); // durable store 缺席:404 同形,不 501(不泄露部署形)
336
+ const slice = await store.get(ref, { ...(offset !== undefined ? { offset } : {}), ...(limit !== undefined ? { limit } : {}) });
337
+ if (!slice)
338
+ return notFound();
339
+ sendJson(res, 200, { content: slice.content, offset: slice.offset, totalChars: slice.totalChars });
340
+ }
205
341
  async function handleTaskTurns(res, runStore, taskId, query) {
206
342
  const run = await runStore.getRun(taskId);
207
343
  if (!run) {
@@ -5,7 +5,8 @@ import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
6
  import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
7
7
  import { type OwnerAwareSessionStore } from "../security.js";
8
- import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, StoreBackend } from "../plugins/store-backend.js";
8
+ import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, ToolResultStoreFull, StoreBackend } from "../plugins/store-backend.js";
9
+ import type { RuleConsentLane } from "../rules-consent.js";
9
10
  import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js";
10
11
  import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js";
11
12
  import type { LeaderEndpoint } from "../leader/endpoint.js";
@@ -101,6 +102,13 @@ export interface ServiceStoreDeps {
101
102
  /** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). Enables PUT/GET /v1/sessions/:id/policy
102
103
  * when present; core reads the rules at prepare-time (subtract-only) via RunnerDeps.sessionPolicyStore (wired separately). */
103
104
  sessionPolicyStore?: ServiceSessionPolicyStore;
105
+ /** [3321] tool-results 读面([3315] 请求,core [3322] `buildToolResultRef` 到货):`GET /v1/tasks/:id/
106
+ * tool-results/:ref` 的数据源 —— 与 RunnerDeps 侧的 offload 店**同实例**(main.ts 的 `toolResultStore`,
107
+ * 即「durable 后端在场」那一只;无 backend 部署里 Runner 用的内存兜底 `runnerOffloadStore` **不接**这里)。
108
+ * 🔴 缺席 ⇒ 路由回 404 而不是 501:501 会向一个无权 caller 泄露「这台机器配没配 durable 店」的部署形,
109
+ * 与 unknown-ref / 归属不符两臂**同形** 404 才是防 oracle 的收口([3321] 的「另定」项按防 oracle 收紧)。
110
+ * 这是本组「absent ⇒ 501 诚实缺席」通例的**唯一具名例外**,例外理由即上一句。 */
111
+ toolResultStore?: ToolResultStoreFull;
104
112
  /** E19 rewind-files snapshot store. Present ⇒ the `rewindFiles` capability is advertised; core uses it via
105
113
  * RunnerDeps.fileSnapshotStore (snapshot per turn + restore on resumeAt, ANY env since gate-split 1.134.0). Also the 2c artifact store. */
106
114
  fileSnapshotStore?: ServiceFileSnapshotStore;
@@ -156,6 +164,11 @@ export interface ServiceStoreDeps {
156
164
  * `backend.session()`/`fileSnapshot()`/`sessionPolicy()`/`resumeAnchor()` must come from ONE backend). Present
157
165
  * whenever a durable backend (tidb/pg/local) is; absent on the env-only/no-backend worker (the routes 501). */
158
166
  backend?: StoreBackend;
167
+ /** #154 车二:**持久化权限规则的同意车道**(core design/179 三步协议的宿主半场)。
168
+ * 在场 ⇔ 规则店真装配(`backend.permissionRule()` 有值)⇒ `POST /v1/rules/cc-import/{prepare,redeem}`
169
+ * 可用、ask 帧才投 `ruleSuggestions`;缺席 ⇒ 两口 501、帧上零候选(发一格无处可兑的「不再询问」
170
+ * = wire 谎言,比缺席更坏)。 */
171
+ ruleConsent?: RuleConsentLane;
159
172
  }
160
173
  /** **进程内活体协调面**(本副本 only,重启即空):HITL 协调器、可 steer 句柄注册表、事件总线、
161
174
  * 收件箱。跨副本一律不可见——每个消费点都按「不在本副本 ⇒ 409/降级」写,不假装全局。 */
@@ -281,6 +294,11 @@ export interface ServiceSeamDeps {
281
294
  * 读面要该 scope 在授权表里、写面另要条目 `write === true`。缺席=本部署没有目录源 ⇒ `org:` 键
282
295
  * 逐字保持 operator-only(收编前行为),绝不因目录不接线而放宽。 */
283
296
  orgMemoryDirectory?: import("../org-memory-admission.js").OrgMemoryDirectory;
297
+ /** design/177 —— org 共享记忆库的供给面(与 core `RunnerDeps.sharedMemoryStores` **同一个实例**:
298
+ * HTTP 只读面看到的库集合按定义等于模型面看到的那一份,两面不可能分歧)。
299
+ * 🔴 缺席 ⇒ `/v1/shared-memory/*` 整域不挂载(诚实缺席);挂载还另要 {@link orgMemoryDirectory}
300
+ * 在场 —— 没有成员性判据的读面只能全放或全拒,两个都比「这个面不存在」差。 */
301
+ sharedMemoryStore?: import("@sema-agent/core").SharedMemoryStoreProvider;
284
302
  /** WEB-SETUP-RECON 附B — org sessionMirror 治理态的 SERVICE 半场 = **观测面(audit),非执法面**。
285
303
  * 裁定 (c):「该镜像而未镜像」发生在数据产生端(壳)——本 server 是镜像**目标**,只看得见到达的 push;
286
304
  * 没到达的会话对 server 不存在(无客户端会话清单、无心跳契约),required 锁的真执法只能在壳(CC
@@ -30,7 +30,10 @@ import { clampVerifyRounds, verifyRoundsFromBody } from "./verify-rounds.js"; //
30
30
  import { handleCapabilities } from "./routes/capabilities.js";
31
31
  import { handleObservability } from "./routes/observability.js";
32
32
  import { handleDiagnostics } from "./routes/diagnostics.js";
33
+ import { handleAdoption } from "./routes/adoption.js";
33
34
  import { handleMemoryPolicy } from "./routes/memory-policy.js";
35
+ import { handleSharedMemory } from "./routes/shared-memory.js";
36
+ import { handleRules } from "./routes/rules.js";
34
37
  import { handleSessionsList } from "./routes/sessions-list.js";
35
38
  import { handleSessions, createSessionsLocal } from "./routes/sessions.js";
36
39
  import { handleSessionSync, createSessionSyncLocal } from "./routes/session-sync.js";
@@ -147,7 +150,10 @@ const ROUTE_DOMAINS = [
147
150
  handleImages,
148
151
  handleApprovalsAssistant,
149
152
  handleObservability,
153
+ handleAdoption,
150
154
  handleMemoryPolicy,
155
+ handleSharedMemory,
156
+ handleRules,
151
157
  handleSessionsList,
152
158
  handleSessions,
153
159
  handleSessionSync,
@@ -598,9 +604,20 @@ export function createHttpServer(rawDeps) {
598
604
  // #154 件三:装配自证只读面(routes/diagnostics.ts):GET /v1/diagnostics/wiring(operator-only)。
599
605
  if (await handleDiagnostics(req, res, url, ctx))
600
606
  return;
607
+ // design/183 §4.3:收编面(routes/adoption.ts,operator lane;POST /v1/adoption + GET /v1/adoption/:id)。
608
+ if (await handleAdoption(req, res, url, ctx))
609
+ return;
601
610
  // design/158 A9:memory 导出/同步 + /v1/policy 只读面(routes/memory-policy.ts)。
602
611
  if (await handleMemoryPolicy(req, res, url, ctx))
603
612
  return;
613
+ // #154 车二:CC settings 导入两口(lane=principal,billable=false)。
614
+ if (await handleRules(req, res, url, ctx))
615
+ return;
616
+ // design/177 共享记忆库只读面(routes/shared-memory.ts)。🔴 域头的合取式**就是**挂载条件:供给面
617
+ // 与 org 折叠面双在场才有这个域,缺一个则请求落到本函数尾的全局 404(诚实缺席,不是 501)。
618
+ // `capabilities.sharedMemory` 读同一个合取式 ——「说 yes ⟺ 面真能用」因此是结构性的,不靠人盯。
619
+ if (deps.sharedMemoryStore && deps.orgMemoryDirectory && (await handleSharedMemory(req, res, url, ctx)))
620
+ return;
604
621
  // design/158 A9:会话/发件链接列表面(routes/sessions-list.ts)。
605
622
  if (await handleSessionsList(req, res, url, ctx))
606
623
  return;
@@ -1406,8 +1423,8 @@ export function createHttpServer(rawDeps) {
1406
1423
  // escaping HttpError here was (a) attributed to the OPERATOR's /decide request as a bare 400/403 — undiagnosable
1407
1424
  // from that contract — and (b) swallowed by the D-D SLA deny-sweep's catch, silently failing EVERY tick. Fold it
1408
1425
  // into a typed 409 result instead: the operator sees "this parked task is blocked by a policy change" (retry
1409
- // after the policy is restored, or let terminal_at abort it); the sweep skips the row this tick (retried next
1410
- // tick, terminal_at backstop — its documented per-call-failure semantics).
1426
+ // after the policy is restored, or let terminal_at_ms abort it); the sweep skips the row this tick (retried next
1427
+ // tick, terminal_at_ms backstop — its documented per-call-failure semantics).
1411
1428
  let spec;
1412
1429
  try {
1413
1430
  spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
@@ -2110,6 +2127,13 @@ export function createHttpServer(rawDeps) {
2110
2127
  e.code === "checkpoint.reopened_concurrently" ||
2111
2128
  e.code === "resume.parent_constraint_missing" || // 历史复审轴A #4(1.254):core 新增两码同为
2112
2129
  e.code === "resume.parent_constraint_mismatch" || // PRE-CAS(checkpoint 保持 pending)——按本块 doctrine 归 retriable;当前 /decide 面只 resume 顶层 checkpoint 应不可达,前瞻补齐防嵌套 resume 面开放时踩中
2130
+ // core 5.22.0(BREAKING「编辑复裁前移 pre-CAS」)闭集加员:审批人**改写**入参时,继承祖先链
2131
+ // 的冻结投影先静态复裁 —— 判 deny ⇒ constraint_rejected;链上有 opaque 层/声明 live remainder
2132
+ // ⇒ constraint_unprojectable。两者与上面两码同族:PRE-CAS、checkpoint 留 pending 且**同 token
2133
+ // 仍可再决**(core 明写 fresh-redecision 语义:照原样批 / deny / 改别的都还点得动)。归 terminal
2134
+ // 会释放 task_active 并孤儿化那条仍 pending 的卡 —— 一张还能点的卡当场变死件。
2135
+ e.code === "resume.constraint_rejected" ||
2136
+ e.code === "resume.constraint_unprojectable" ||
2113
2137
  e.code.startsWith("wake."); // design/144:wake 拒绝(gate_pending/nothing_to_deliver)全是 PRE-CAS,checkpoint 保持 pending——驱 terminal 会孤儿化仍在等的 park
2114
2138
  if (claimedRow && taskId && deps.runStore) {
2115
2139
  // Re-park on the SAME gate family the outcome targeted: a plan_review retry must re-park `needs_review`
@@ -2651,7 +2675,7 @@ export function createHttpServer(rawDeps) {
2651
2675
  * (graceful; vs the abort the reaper's expire() gives resource_limit/needs_review). Reuses resumeCheckpoint,
2652
2676
  * so the markResuming CAS makes it idempotent across replicas (one replica wins each resume) and the parked
2653
2677
  * run row is driven correctly. A per-call failure (e.g. a redeployed scenario) is swallowed → the row stays
2654
- * pending and is retried next tick, with the terminal_at backstop as the eventual abort if the deny never
2678
+ * pending and is retried next tick, with the terminal_at_ms backstop as the eventual abort if the deny never
2655
2679
  * succeeds. Bounded per tick by the store query's LIMIT. Wired into main.ts's reaper.
2656
2680
  */
2657
2681
  async function denyExpiredApprovals(now) {
@@ -2659,7 +2683,7 @@ export function createHttpServer(rawDeps) {
2659
2683
  if (!cs)
2660
2684
  return;
2661
2685
  const expired = await cs.listExpiredApprovalGates(now).catch(() => []);
2662
- // [1591] 候裁③ 静默臂:parked 后台子代的过期 cp 不走 sweep 的重活赎回(deadline→terminal_at 窗内
2686
+ // [1591] 候裁③ 静默臂:parked 后台子代的过期 cp 不走 sweep 的重活赎回(deadline→terminal_at_ms 窗内
2663
2687
  // 每行每 tick 一条 warn 的噪声源)——其收割属 reaper 的 expire+reconcileParkedAgents 车道。判别在
2664
2688
  // resume 尝试之前做(省掉注定 409 的整条 ctx/resolveSpec 重建尝试;代价=每过期行两次店读,过期集
2665
2689
  // 本就被店查询 LIMIT 界住);命中聚合为单条 info 留痕。判别自身故障 ⇒ 按非 parked 处理(保留 warn,
@@ -2684,7 +2708,7 @@ export function createHttpServer(rawDeps) {
2684
2708
  for (const { sessionId } of expired) {
2685
2709
  // 对抗评审 2026-07-11: log the swallow — a per-tick failure retried forever (e.g. a policy change now folded
2686
2710
  // into a 409 by resumeCheckpoint, or a redeployed scenario) was fully silent; the row sat pending to its
2687
- // terminal_at with zero operator-visible signal. Behavior unchanged (skip + retry next tick), now diagnosable.
2711
+ // terminal_at_ms with zero operator-visible signal. Behavior unchanged (skip + retry next tick), now diagnosable.
2688
2712
  if (await isParkedOwned(sessionId)) {
2689
2713
  parkedSkipped += 1;
2690
2714
  continue;
@@ -2770,12 +2794,19 @@ const ROUTE_LABEL_PATTERNS = [
2770
2794
  // #151 车4:durable 回决口。必须排在 `/v1/tasks/:id/$sub` **之前**?—— 不必(那条只认三个字面动词),
2771
2795
  // 但仍按「具体先于泛化」的表序纪律放在它前面,免得日后 $sub 放宽成 `[^/]+` 时静默吞掉本条。
2772
2796
  [/^\/v1\/tasks\/[^/]+\/asks\/[^/]+\/decision$/, "/v1/tasks/:id/asks/:askId/decision"],
2797
+ // [3321] tool-results 读面:同样排在 `/v1/tasks/:id/$sub` 之前(具体先于泛化的表序纪律;$sub 那条
2798
+ // 只认三个字面动词,今日不冲突,但它日放宽成 `[^/]+` 时本条不会被静默吞掉)。
2799
+ [/^\/v1\/tasks\/[^/]+\/tool-results\/[^/]+$/, "/v1/tasks/:id/tool-results/:ref"],
2773
2800
  [/^\/v1\/tasks\/[^/]+\/(turns|stream|artifacts)$/, "/v1/tasks/:id/$sub"],
2774
2801
  [/^\/v1\/leader\/[^/]+$/, "/v1/leader/:id"],
2775
2802
  [/^\/v1\/attachments\/[^/]+$/, "/v1/attachments/:id"],
2776
2803
  [/^\/v1\/capabilities\/scenarios\/[^/]+$/, "/v1/capabilities/scenarios/:name"],
2804
+ [/^\/v1\/adoption\/[^/]+$/, "/v1/adoption/:id"],
2777
2805
  [/^\/v1\/memory\/sync\/[^/]+$/, "/v1/memory/sync/:scope"],
2778
2806
  [/^\/v1\/usage\/(summary|series|breakdown)$/, "/v1/usage/$sub"],
2807
+ // design/177 共享记忆库只读面。两条各自成桶(列举是窗口读、正文是单文档读,延迟分布不同族)。
2808
+ [/^\/v1\/shared-memory\/stores\/[^/]+\/documents$/, "/v1/shared-memory/stores/:store/documents"],
2809
+ [/^\/v1\/shared-memory\/stores\/[^/]+\/document$/, "/v1/shared-memory/stores/:store/document"],
2779
2810
  ];
2780
2811
  const ROUTE_LABEL_LITERALS = new Set([
2781
2812
  "/health", "/metrics", "/metrics/summary", "/metrics/plan-cache",
@@ -2789,8 +2820,19 @@ const ROUTE_LABEL_LITERALS = new Set([
2789
2820
  // 名册门的枚举器当时只认 `url === …`,整条路由对门隐形(枚举器已一并补上)。
2790
2821
  "/v1/fleet/stream",
2791
2822
  "/v1/memory/export", "/v1/sendfile-links",
2823
+ // design/177:共享记忆库清单面(具名两条走上面的模式表)。
2824
+ "/v1/shared-memory/stores",
2792
2825
  // #154:装配自证读面(operator-only)。低频但**每次都在排障现场被打**,落进 `other` 桶等于排障时看不见。
2793
2826
  "/v1/diagnostics/wiring",
2827
+ // design/183:收编发起口(operator-only,一次性部署级动作)。低频、但它的 duration 与失败率是
2828
+ // 迁移现场唯一的机器信号,落进 `other` 桶就看不见了。
2829
+ "/v1/adoption",
2830
+ // #154 车二:CC settings 导入两口。字面量(无 id 段)⇒ 进 LITERALS 表而不是模式表。
2831
+ // 🔴 写**字面量**而不是引 `RULES_CC_IMPORT_*_PATH` 常量:名册门与 billable 申明门都是**扫源码文本**
2832
+ // 的(它们正则抓本表里的双引号词),引常量会让两条新路由对两道门隐形 —— 那不是「过了门」,是绕过门。
2833
+ // ⚠️ 本段注释里也不许出现方括号后紧跟右圆括号的字符对:billable 门的表捕获正则是**非贪婪**的,
2834
+ // 那两个字符会让它在这里提前收尾,后面的字面量整段落在门的视野外(写这条注时真的踩到过)。
2835
+ "/v1/rules/cc-import/prepare", "/v1/rules/cc-import/redeem",
2794
2836
  ]);
2795
2837
  /** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
2796
2838
  * [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { buildSessionAudit, type SessionAudit } from "./audit.js";
12
12
  export { createLogger, type Logger, type LogLevel } from "./observability/logger.js";
13
13
  export { Metrics, createMetrics, type Labels } from "./observability/metrics.js";
14
14
  export { RateLimiter, type RateDecision } from "./observability/rate-limit.js";
15
- export { GiteaClient, repoToolsFor, parseRepo, type RepoCoords } from "./capabilities/repo-tools.js";
15
+ export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo, type GitApiKind, type RepoCoords, type RepoReadClient } from "./capabilities/repo-tools.js";
16
16
  export { nowTool } from "./capabilities/builtin-tools.js";
17
17
  export { createCouncilTool } from "./capabilities/code-review-council.js";
18
18
  export { loadSkills, skillsForScenario, type LoadedSkill } from "./capabilities/skills.js";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ export { buildSessionAudit } from "./audit.js";
17
17
  export { createLogger } from "./observability/logger.js";
18
18
  export { Metrics, createMetrics } from "./observability/metrics.js";
19
19
  export { RateLimiter } from "./observability/rate-limit.js";
20
- export { GiteaClient, repoToolsFor, parseRepo } from "./capabilities/repo-tools.js";
20
+ export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo } from "./capabilities/repo-tools.js";
21
21
  export { nowTool } from "./capabilities/builtin-tools.js";
22
22
  export { createCouncilTool } from "./capabilities/code-review-council.js";
23
23
  export { loadSkills, skillsForScenario } from "./capabilities/skills.js";