@sema-agent/server 7.79.3 → 7.80.1

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 (89) hide show
  1. package/dist/boot/stage-07-capability-layer.js +2 -1
  2. package/dist/capabilities/skills.d.ts +10 -0
  3. package/dist/capabilities/skills.js +3 -1
  4. package/dist/http/admission.js +3 -1
  5. package/dist/http/dispatch.js +28 -8
  6. package/dist/http/resume-legs.js +4 -3
  7. package/dist/http/route-ctx.d.ts +101 -4
  8. package/dist/http/route-ctx.js +4 -0
  9. package/dist/http/route-table.d.ts +71 -43
  10. package/dist/http/route-table.js +111 -187
  11. package/dist/http/routes/a2a-serve.d.ts +18 -3
  12. package/dist/http/routes/a2a-serve.js +25 -23
  13. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  14. package/dist/http/routes/admin-config-refresh.js +32 -31
  15. package/dist/http/routes/admin-drain.d.ts +9 -2
  16. package/dist/http/routes/admin-drain.js +32 -31
  17. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  18. package/dist/http/routes/admin-memory-optout.js +27 -40
  19. package/dist/http/routes/adoption.d.ts +15 -2
  20. package/dist/http/routes/adoption.js +52 -52
  21. package/dist/http/routes/agents-roster.d.ts +9 -2
  22. package/dist/http/routes/agents-roster.js +82 -80
  23. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  24. package/dist/http/routes/approvals-assistant.js +38 -39
  25. package/dist/http/routes/attachments.d.ts +22 -2
  26. package/dist/http/routes/attachments.js +81 -72
  27. package/dist/http/routes/capabilities.d.ts +21 -2
  28. package/dist/http/routes/capabilities.js +174 -170
  29. package/dist/http/routes/config-catalog.d.ts +9 -2
  30. package/dist/http/routes/config-catalog.js +28 -31
  31. package/dist/http/routes/devices.d.ts +24 -2
  32. package/dist/http/routes/devices.js +19 -21
  33. package/dist/http/routes/diagnostics.d.ts +9 -2
  34. package/dist/http/routes/diagnostics.js +41 -43
  35. package/dist/http/routes/fleet.d.ts +11 -2
  36. package/dist/http/routes/fleet.js +23 -22
  37. package/dist/http/routes/images.d.ts +63 -9
  38. package/dist/http/routes/images.js +53 -46
  39. package/dist/http/routes/leader.d.ts +20 -2
  40. package/dist/http/routes/leader.js +43 -40
  41. package/dist/http/routes/memory-bundle.d.ts +15 -2
  42. package/dist/http/routes/memory-bundle.js +38 -38
  43. package/dist/http/routes/memory-compliance.d.ts +16 -4
  44. package/dist/http/routes/memory-compliance.js +44 -45
  45. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  46. package/dist/http/routes/memory-consolidation.js +66 -66
  47. package/dist/http/routes/memory-origin.d.ts +23 -4
  48. package/dist/http/routes/memory-origin.js +65 -65
  49. package/dist/http/routes/memory-policy.d.ts +21 -2
  50. package/dist/http/routes/memory-policy.js +119 -115
  51. package/dist/http/routes/notify-wake.d.ts +21 -11
  52. package/dist/http/routes/notify-wake.js +118 -116
  53. package/dist/http/routes/observability.d.ts +16 -2
  54. package/dist/http/routes/observability.js +54 -51
  55. package/dist/http/routes/retention-ops.d.ts +19 -2
  56. package/dist/http/routes/retention-ops.js +20 -22
  57. package/dist/http/routes/rules.d.ts +36 -2
  58. package/dist/http/routes/rules.js +22 -24
  59. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  60. package/dist/http/routes/run-memory-capture.js +84 -84
  61. package/dist/http/routes/runs.d.ts +102 -22
  62. package/dist/http/routes/runs.js +79 -96
  63. package/dist/http/routes/session-sync.d.ts +41 -2
  64. package/dist/http/routes/session-sync.js +26 -24
  65. package/dist/http/routes/sessions-list.d.ts +16 -2
  66. package/dist/http/routes/sessions-list.js +78 -75
  67. package/dist/http/routes/sessions.d.ts +77 -2
  68. package/dist/http/routes/sessions.js +50 -47
  69. package/dist/http/routes/shared-memory.d.ts +19 -2
  70. package/dist/http/routes/shared-memory.js +65 -63
  71. package/dist/http/routes/side-query.d.ts +11 -2
  72. package/dist/http/routes/side-query.js +81 -79
  73. package/dist/http/routes/tasks.d.ts +17 -2
  74. package/dist/http/routes/tasks.js +18 -11
  75. package/dist/http/routes/trace-usage.d.ts +40 -2
  76. package/dist/http/routes/trace-usage.js +46 -35
  77. package/dist/http/routes/workflows.d.ts +35 -3
  78. package/dist/http/routes/workflows.js +29 -29
  79. package/dist/http/wire-types.d.ts +15 -9
  80. package/dist/parked-decide.d.ts +9 -0
  81. package/dist/parked-decide.js +3 -3
  82. package/dist/run-local.js +2 -1
  83. package/dist/spec-fields.js +33 -30
  84. package/dist/task-cwd.d.ts +21 -0
  85. package/dist/task-cwd.js +3 -0
  86. package/dist/trace/core-keyset-guard.d.ts +6 -1
  87. package/dist/trace/project.d.ts +20 -0
  88. package/dist/trace/project.js +15 -1
  89. package/package.json +2 -2
@@ -8,6 +8,7 @@ import { webSearchConfigFromEnv, createWebSearchBackend, shouldProbeWebSearchOnB
8
8
  import { createAuthorizer } from "../security.js";
9
9
  import { assertGateIntentServiceable, buildForceDurableGateInertNotice, hasOperatorGateIntent } from "../approval.js";
10
10
  import { loadSkills } from "../capabilities/skills.js";
11
+ import { toolsRunOnThisHost } from "../task-cwd.js";
11
12
  import { createRepoClient } from "../capabilities/repo-tools.js";
12
13
  import { buildScenarios, builtinScenarioDetails } from "../capabilities/scenarios.js";
13
14
  import { RateLimiter } from "../observability/rate-limit.js";
@@ -108,7 +109,7 @@ export async function runStage07(ctx) {
108
109
  ? backend.rateLimiter(config.rateLimitPerMin, counterDegradeHook).startRefresh()
109
110
  : new RateLimiter(config.rateLimitPerMin);
110
111
  bindRateLimiter(rateLimiter);
111
- let skills = loadSkills(config.skillsDir);
112
+ let skills = loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) });
112
113
  skills = await configCenter.applyCenterCapabilities(skills);
113
114
  const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
114
115
  const webSearchCfg = webSearchConfigFromEnv();
@@ -41,6 +41,16 @@ export interface LoadedSkill {
41
41
  export interface LoadSkillsOptions {
42
42
  /** 不可信 lane 的约束根(通常=clone root)。缺席=受控 lane,不做逐条目约束。 */
43
43
  confineTo?: string;
44
+ /**
45
+ * S-377 —— 「**工具跑的那台机器**的盘就是本进程这台盘吗」。真 ⇒ 目录形态技能带上自己的目录
46
+ * (`SkillSpec.baseDir`);假/缺席 ⇒ **不带**。判据的单一属主 = `task-cwd.ts` 的
47
+ * {@link import("../task-cwd.js").toolsRunOnThisHost}(调用方按它算,别在这里第二次判 lane)。
48
+ *
49
+ * 🔴 **缺席 = 不带**,是**故意的 fail-closed 方向**:带了一条工具那边不存在的路径,伤是「模型按它去
50
+ * 读、扑空,顺带看到本机目录布局」;不带的伤只是少一条线索(附件本来就随 invoke 全文内联投递,
51
+ * core 的律也明写缺席 ⇒ 结果逐字节与 baseDir 到货之前相同)。两个方向不对称 ⇒ 默认取轻的那边。
52
+ */
53
+ toolsShareThisFilesystem?: boolean;
44
54
  }
45
55
  /** `p` 的 realpath 必须落在 `rootReal` 之内(含自身),否则抛。`sep` 边界判定防 `/a/root-evil` 撞
46
56
  * `/a/root` 前缀。目标不存在 ⇒ realpath 抛 ENOENT,同样是拒绝(悬空链接不该被读)。
@@ -1,5 +1,5 @@
1
1
  import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
- import { join, sep } from "node:path";
2
+ import { join, resolve, sep } from "node:path";
3
3
  export function assertConfined(p, rootReal, label = "skill entry") {
4
4
  let real;
5
5
  try {
@@ -59,6 +59,8 @@ export function loadSkills(dir, opts) {
59
59
  const files = collectAttachments(skillDir, "", confineRoot);
60
60
  if (files.length > 0)
61
61
  skill.spec.files = files;
62
+ if (opts?.toolsShareThisFilesystem === true)
63
+ skill.spec.baseDir = resolve(skillDir);
62
64
  push(skill, join(skillDir, "SKILL.md"));
63
65
  }
64
66
  }
@@ -7,7 +7,7 @@ import { selfOrchestrationDenial } from "../task-workflow.js";
7
7
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
8
8
  import { parseHooksConfig } from "../hooks/hook-runner.js";
9
9
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
10
- import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
10
+ import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
11
11
  import { verifyRoundsFromBody } from "./verify-rounds.js";
12
12
  import { sendError, httpErrorCode } from "./send.js";
13
13
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
@@ -27,6 +27,8 @@ export function validateUserSkills(skills) {
27
27
  return "skill.description must be a string";
28
28
  if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
29
29
  return `skill.content must be a non-empty string of at most ${MAX_SKILL_CONTENT_CHARS} characters (the engine rejects skills over this size outright at load time)`;
30
+ if (s["baseDir"] !== undefined && !isValidCwd(s["baseDir"]))
31
+ return `skill.baseDir must be an absolute path string of at most ${MAX_CWD_CHARS} characters with no ".." segment and no NUL byte (it is not resolved or read here — the engine only substitutes it into the skill's own text)`;
30
32
  }
31
33
  return null;
32
34
  }
@@ -2,9 +2,10 @@ import { createHash } from "node:crypto";
2
2
  import { setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isDestructiveSessionWrite, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
3
3
  import { publicStoreProbeError } from "../store-live-probe.js";
4
4
  import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
5
- import { PRE_CREDENTIAL_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath } from "./route-table.js";
5
+ import { ROUTE_METHOD_ORDER } from "./route-ctx.js";
6
+ import { PRE_CREDENTIAL_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute } from "./route-table.js";
6
7
  import { sendJson, sendError, isSseResponse } from "./send.js";
7
- import { authorized, systemFor } from "./principal-gate.js";
8
+ import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal } from "./principal-gate.js";
8
9
  const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
9
10
  export function hasServiceAuth(config) {
10
11
  return Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
@@ -42,19 +43,29 @@ export function createDispatch(ctx) {
42
43
  };
43
44
  deps.drainState.lastAttachEndedAt = () => lastAttachEndedAt;
44
45
  }
45
- async function dispatchSegment(rows, req, res, url, ctx) {
46
+ async function dispatchSegment(rows, req, res, url, search, method, allow, ctx) {
46
47
  for (const row of rows) {
47
48
  if (row.enabled !== undefined && !row.enabled(deps))
48
49
  continue;
49
- if (await row.handler(req, res, url, ctx))
50
- return true;
50
+ const found = lookupRoute(row.decls, url, method);
51
+ for (const m of found.allow)
52
+ if (!allow.includes(m))
53
+ allow.push(m);
54
+ if (found.decl === null)
55
+ continue;
56
+ await row.handler(req, res, { decl: found.decl, params: found.params, search }, ctx);
57
+ return true;
51
58
  }
52
59
  return false;
53
60
  }
54
61
  async function handle(req, res) {
55
62
  const startedAt = Date.now();
56
63
  const method = req.method ?? "GET";
57
- const url = (req.url ?? "/").split("?")[0];
64
+ const rawUrl = req.url ?? "/";
65
+ const qMark = rawUrl.indexOf("?");
66
+ const url = qMark === -1 ? rawUrl : rawUrl.slice(0, qMark);
67
+ const search = qMark === -1 ? "" : rawUrl.slice(qMark);
68
+ const allow = { pre: [], post: [] };
58
69
  noteActivity(url);
59
70
  const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
60
71
  if (admitted)
@@ -180,7 +191,7 @@ export function createDispatch(ctx) {
180
191
  }
181
192
  }
182
193
  }
183
- if (await dispatchSegment(PRE_CREDENTIAL_ROUTES, req, res, url, ctx))
194
+ if (await dispatchSegment(PRE_CREDENTIAL_ROUTES, req, res, url, search, method, allow.pre, ctx))
184
195
  return;
185
196
  let source = null;
186
197
  const anyServiceAuth = hasServiceAuth(deps.config);
@@ -221,8 +232,17 @@ export function createDispatch(ctx) {
221
232
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting destructive session writes (policy / sync-import / fork / delete)");
222
233
  return;
223
234
  }
224
- if (await dispatchSegment(POST_CREDENTIAL_ROUTES, req, res, url, ctx))
235
+ if (await dispatchSegment(POST_CREDENTIAL_ROUTES, req, res, url, search, method, allow.post, ctx))
225
236
  return;
237
+ if (allow.pre.length > 0 || allow.post.length > 0) {
238
+ if (allow.post.length > 0 && deps.config.requirePrincipal && gatedPrincipal(req, deps.config) === undefined) {
239
+ sendPrincipalRefusal(res, req, deps.config);
240
+ return;
241
+ }
242
+ res.setHeader("allow", ROUTE_METHOD_ORDER.filter((m) => allow.pre.includes(m) || allow.post.includes(m)).join(", "));
243
+ sendError(res, 405, "method_not_allowed", "this path exists but not for this HTTP method — the `Allow` response header lists the methods it does answer");
244
+ return;
245
+ }
226
246
  sendError(res, 404, "not_found.route", "not found");
227
247
  }
228
248
  return { handle };
@@ -1031,16 +1031,17 @@ export function createResumeLegs(ctx) {
1031
1031
  async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
1032
1032
  const cs = deps.checkpointStore;
1033
1033
  if (!deps.runStore) {
1034
- return { status: 501, body: { error: "delivering a workflow decision on the host's next turn requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
1034
+ return { status: 501, body: { error: "delivering this decision requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
1035
1035
  }
1036
1036
  const ctx = await cs.getCtx(sessionId).catch(() => null);
1037
1037
  if (!ctx) {
1038
1038
  return {
1039
1039
  status: 409,
1040
1040
  body: {
1041
- error: "the workflow's originating session no longer has the rebuild inputs for a new turn: a session's resume context is garbage-collected once it is stale AND that session itself has no pending checkpoint and no active run (REAP_RUN_STALE_SEC, default 120s) — a workflow CHILD's park does not hold the HOST session's row open. Raise REAP_RUN_STALE_SEC to cover how long approvals may sit before someone decides them, or resume the workflow run yourself with the runId below",
1041
+ error: "this approval waited too long to be delivered: the session this run was started from no longer keeps what a new turn would need, because that state is dropped once the session has been idle past this deployment's retention window (REAP_RUN_STALE_SEC — its current value is staleAfterSec below). Nothing was decided and the approval is still pending: resume the run yourself with the runId below, or ask whoever operates this deployment to raise REAP_RUN_STALE_SEC so approvals can sit longer before someone decides them",
1042
1042
  errorCode: "conflict.resume_context_unavailable",
1043
1043
  runId,
1044
+ staleAfterSec: deps.config.runStaleSec,
1044
1045
  },
1045
1046
  };
1046
1047
  }
@@ -1108,7 +1109,7 @@ export function createResumeLegs(ctx) {
1108
1109
  }
1109
1110
  const gateKind = cp.gate?.kind;
1110
1111
  if (gateKind !== "task_done") {
1111
- return { status: 409, body: { error: `cannot wake: this checkpoint awaits a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
1112
+ return { status: 409, body: { error: `cannot wake: this session is waiting on a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
1112
1113
  }
1113
1114
  const trusted = operator;
1114
1115
  if (message !== undefined) {
@@ -426,11 +426,107 @@ export interface RouteCtxBase {
426
426
  export interface RouteCtx extends RouteCtxBase {
427
427
  req: RouteRequestState;
428
428
  }
429
- export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx) => Promise<boolean>;
430
- /** 域实现的收尾闩:见文件头「已处理」语义。 */
431
- export interface RouteMiss {
432
- fell: boolean;
429
+ /**
430
+ * 域入口的签名(S-146 第三刀 车②)。
431
+ *
432
+ * 🔴 **`url` 与「已处理」布尔一并删掉**,零别名([ref] 硬 breaking):分派器已经**匹配过一次**并把
433
+ * 结果整个交进来(`match`),所以一只 handler 既不需要再判路,也不可能「没匹配上」——「返回 false
434
+ * 放过给下一域」那一形在第三刀之后结构上不存在(路径归属由声明表一次定死),留着一个恒为 true 的
435
+ * 返回值就是给它留门。域内的 `RouteMiss` 闩随之删除。
436
+ *
437
+ * 阀门(`RouteRow.enabled`)是唯一的「本域今天不在」通道,它在**匹配之前**判,请求落到分派器尾的
438
+ * 全局 404(诚实缺席,不是 501)。
439
+ */
440
+ export type RouteHandler<Id extends string = string> = (req: IncomingMessage, res: ServerResponse, match: RouteMatch<Id>, ctx: RouteCtx) => Promise<void>;
441
+ /**
442
+ * 分派结果 —— handler 能看到的**全部**路由事实。
443
+ *
444
+ * · `decl` 命中的那一行(`id` 是域内闭集词:handler `switch (match.decl.id)` + `assertNever`,漏一口 = 编译红);
445
+ * · `params` 匹配式**具名捕获组** → 段值。没参与匹配的可选组**不入表**(值缺席),与旧 `m[i]` 的
446
+ * `undefined` 同形 —— 所以必填段照旧写 `!`,可选段照旧判 `undefined`;
447
+ * · `search` 原始查询串(`"?a=b"` 或 `""`)。域内取查询参数一律 `new URLSearchParams(match.search)`:
448
+ * 与旧的 `new URL(req.url ?? "", "http://x").searchParams` **逐字同义**(前导 `?` 与百分号
449
+ * 解码的处置相同),但读的是分派器已经剖好的那一份,而不是让每只域再从 `req.url` 剖一遍。
450
+ * 🔴 `req.url` / `req.method` 自此不进任何域的判路 —— `test/route-shape-roster.test.ts`
451
+ * 的反向格(域内判路必须为零)是它的机器钉。
452
+ */
453
+ export interface RouteMatch<Id extends string = string> {
454
+ readonly decl: RouteDispatchDecl & {
455
+ readonly id: Id;
456
+ };
457
+ readonly params: Readonly<Record<string, string | undefined>>;
458
+ readonly search: string;
433
459
  }
460
+ /** 一张 `X_ROUTES` 里**可分派**行的 `id` 闭集(纯门行没有 `id`,不进这只并集)。 */
461
+ export type RouteIdsOf<T extends readonly RouteDecl[]> = Extract<T[number], {
462
+ readonly id: string;
463
+ }>["id"];
464
+ /**
465
+ * 闭集 `switch` 的收尾臂([ref] 形):所有 `id` 都被 `case` 吃掉之后,这里的实参类型是 `never` ——
466
+ * 漏一口就是**编译红**。运行期到得了这一行只可能是有人绕过类型把野值塞进了声明表,所以它**响**
467
+ * (抛),不静默 200 也不静默 404。
468
+ */
469
+ export declare function assertNever(x: never): never;
470
+ /** 改写门是**方法感知**的(它的撤销动词是 DELETE)。闭集 —— 加第五个动词是编译红。 */
471
+ export type RouteMethod = "GET" | "POST" | "PUT" | "DELETE";
472
+ /** 闭集的**规范序**(405 的 `Allow` 头按它排,于是同一条路径的头字节恒定,不随声明序抖)。
473
+ * `satisfies` 让它与 {@link RouteMethod} 双向闭合:漏一个动词 / 多一个都是编译红。 */
474
+ export declare const ROUTE_METHOD_ORDER: readonly ["GET", "POST", "PUT", "DELETE"];
475
+ export interface RouteDeclFacts {
476
+ /** 「这条 url **烧模型**」—— drain / model-roster-pending / 无 service-token 三道 503 的共同判别。
477
+ * 没有这一列 = 不烧模型(零默认值:要它为真必须显式写出来,漏写的方向是**少收一笔账单**而不是
478
+ * 少一道门 —— 反了才是灾难,所以三道门读的是 true 这一侧)。 */
479
+ readonly billable?: true;
480
+ /** 「本条 url 在**没有任何 service credential** 的部署形下,这几个方法必须 fail-closed」。
481
+ * 判据(两个合取项)= 「授权的唯一输入是 principal 头」∧「持久改写,或爆炸半径跨租户的治理读」;
482
+ * 逐口论证在各 `routes/*.ts` 的头注里,声明行只记结论。 */
483
+ readonly credentialGated?: readonly RouteMethod[];
484
+ /** 「这条 url **是**计费提交面,但它到底提不提交由**请求体里的方法**决定」—— 单 URL 多方法的
485
+ * JSON-RPC 端点。两道**可用性** 503(drain / roster-pending)按 url 一刀切会把同端点上的纯读一起
486
+ * 关掉,所以分派器对本行**跳过**那两道,改由域模块在写分支里施加同样的两道(文案逐字同源)。
487
+ * 🔴 只豁免那两道,**不豁免安全轴**:`billable` 本身仍为 true,无 service token 那道 fail-closed
488
+ * 503 照旧罩着(它判的是「这台机能不能收写」,与方法无关,且必须在解析请求体之前)。 */
489
+ readonly methodDispatched?: true;
490
+ }
491
+ /** **可分派行**多出来的两列(S-146 第三刀):身份 + 它真认的方法。纯门行两列都没有(见下)。 */
492
+ interface RouteDeclIdentity {
493
+ /** **全表唯一**的行名(kebab 闭集词)。这是声明行的身份:域 handler 用
494
+ * `switch (match.decl.id)` 闭集分派(漏一口 = 编译红),所以它必须稳定、唯一、且与路径解耦
495
+ * (路径会改形,身份不该跟着改)。全表唯一性由 `test/route-shape-roster.test.ts` 层A 的良构格钉住。 */
496
+ readonly id: string;
497
+ /** 本行真被响应的 HTTP 方法 —— 车② 起它是**分派器**的方法判据:路径命中而方法不在列 ⇒ 分派器
498
+ * 统一 405 + `Allow`(并集按**路径**取,跨行跨域),handler 一个字都不再判方法。
499
+ *
500
+ * 🔴 因此本列必须是**逐口真值**,不能是并集。车② 为此做了两件事(改前的两条结构性口径,原文在
501
+ * 车① 的本注里):① 5 条「一行覆盖多条子路径、各子路径方法集不同」的并集行按动词拆成逐口行
502
+ * (`session-verb` / `run-subagent-verb` / `run-task-verb` / `session-sync-verb` / `session-sync-blobs`);
503
+ * ② 2 条「一条路径住着两只域」的跨域行按方法拆成两行各归各域(`/v1/tasks` = trace-usage 的 GET +
504
+ * tasks 的 POST;`/v1/memory/export` = memory-policy 的 GET + memory-bundle 的 POST)。 */
505
+ readonly methods: readonly RouteMethod[];
506
+ }
507
+ /**
508
+ * **可分派行**:一条真路由。匹配式二选一 —— `path`(精确;标签**就是** path 自身,所以没有 label 列,
509
+ * 少一处手抄)或 `pattern`(形状;捕获组一律**具名**,段值经 `RouteMatch.params` 交给 handler)。
510
+ */
511
+ export type RouteDispatchDecl = (RouteDeclFacts & RouteDeclIdentity & {
512
+ readonly path: string;
513
+ }) | (RouteDeclFacts & RouteDeclIdentity & {
514
+ readonly pattern: RegExp;
515
+ readonly label?: string;
516
+ });
517
+ /**
518
+ * **纯门行**(facts-only):一段前缀上的门性质,**不是路由**。
519
+ *
520
+ * 🔴 它刻意比任何标签行都宽(`/v1/devices/` 下将来长出的任何写动词天然在改写门内),正因为宽,它
521
+ * **不可分派**:型上就没有 `id` 也没有 `methods` —— 于是「分派器会不会拿一条门行去应答请求」这个
522
+ * 问题在类型上就不存在,不靠谁记得在 `if` 里排除它。它只喂三只门谓词与 `routeLabel`。
523
+ */
524
+ export type RouteGateDecl = RouteDeclFacts & {
525
+ readonly prefix: string;
526
+ readonly label?: string;
527
+ };
528
+ /** 一张声明表的行 = 可分派行 ∪ 纯门行(闭集)。 */
529
+ export type RouteDecl = RouteDispatchDecl | RouteGateDecl;
434
530
  /**
435
531
  * [ref]([ref] 存在性 oracle 封口)——`not_found.run` 的**唯一**文案源,unknown 与 not-yours 两臂共用。
436
532
  * 修前:unknown 臂发这条长指路文案、not-yours 臂发短文「run not found」,同 code 下按**文案长度**即可
@@ -456,4 +552,5 @@ export declare function acceptedResumingReceipt(taskId: string, sessionId: strin
456
552
  export declare const RUN_NOT_FOUND_MESSAGE = "run not found \u2014 the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
457
553
  /** [ref] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
458
554
  export declare function runNotFoundMessage(id: string): string;
555
+ export {};
459
556
  //# sourceMappingURL=route-ctx.d.ts.map
@@ -17,6 +17,10 @@ export function sendResumeOutcome(res, out, logger, extraBody) {
17
17
  res.setHeader("retry-after", String(Math.ceil(wait)));
18
18
  sendJson(res, out.status, extraBody === undefined ? out.body : { ...out.body, ...extraBody });
19
19
  }
20
+ export function assertNever(x) {
21
+ throw new Error(`unreachable route id: ${String(x)}`);
22
+ }
23
+ export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "DELETE"];
20
24
  export function acceptedResumingReceipt(taskId, sessionId) {
21
25
  return { status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } };
22
26
  }
@@ -18,9 +18,18 @@
18
18
  * · `ROUTES` —— **行序 = 分派序**,`position` 说它在全局服务凭据门的哪一侧。门不再「按行号插在链中间」,
19
19
  * 而是在 `pre` 段与 `post` 段之间**恒定一处**执行 ⇒ ① 与 ② 合一(名册就是分派序本身,两者再不可能漂),
20
20
  * 那道「从 handle() 源码抽序对拍」的门随之删掉 —— 不是放松,是它守的事变成了结构上不可违反。
21
- * ⚠️ 刻意**不引入 matcher**:handler 自己判 url 这一点一个字不动(改它等于重写 34 个域模块的判路)。
22
- * · `ROUTE_DECLARATIONS` —— ③④⑤ 合成一张;一行声明它的匹配式与它**真有**的性质
23
- * (`billable` / `credentialGated` / `methodDispatched`),没有该性质就不写那一列(零默认值)。
21
+ * 🔴 S-146 第三刀 车②:**matcher 落地**。一行多了 `decls` 一列(本域真应答的口集合),而
22
+ * `ROUTE_DECLARATIONS` 就是这一列按行序摊平 ⇒「分派表」与「声明表」从此是同一份数据的两个投影,
23
+ * 不再是两份要对拍的名单;域 handler 不再看 url / method(见 route-ctx.ts `RouteHandler` 头注)。
24
+ * · `ROUTE_DECLARATIONS` —— ③④⑤ 合成一张;一行声明它的 `id`(全表唯一身份)、匹配式、它**真被响应**
25
+ * 的 `methods`、以及它**真有**的性质(`billable` / `credentialGated` / `methodDispatched`),
26
+ * 没有该性质就不写那一列(零默认值)。
27
+ * 🔴 S-146 第三刀 车①:这 115 行的**行本体搬回各域**(`routes/x.ts` 的 `X_ROUTES`)—— 路径的
28
+ * 单一属主与 `*_PATH` 常量今天的属主对齐,本文件只做拼接与查找结构。`RouteDecl` / `RouteMethod`
29
+ * 两个类型住在 `route-ctx.js`(域模块本来就 import 的那只接缝模块),理由见那里的行注。
30
+ * 车②:行分两族 —— **可分派行**(`path` / `pattern`,带 `id` + `methods`)与**纯门行**
31
+ * (`prefix`,facts-only,型上就没有 `id`/`methods`),于是「门行会不会被当成路由应答」这个问题
32
+ * 在类型上就不存在。
24
33
  *
25
34
  * 判据两条,逐字:
26
35
  * · **标签取首命中**(优先序):精确行先于形状行([[ref]] 的结论,这里是结构性的而不是靠行序),
@@ -29,24 +38,36 @@
29
38
  * `||` 的逐字同义,也是「前缀行比标签行宽」这件事的正解(`/v1/devices/` 下将来长出的任何写动词
30
39
  * 天然在改写门内,不必等谁想起来补一行标签)。
31
40
  *
32
- * 新增一条路由 = 新建 `routes/x.ts` + 本表加一行(+ `test/route-order-freeze.test.ts` 的冻结表加一行,
33
- * 那是 position 的外部对拍源)。`position` 与匹配式都是必填 —— 忘了就是**编译红**,不是静默 404。
41
+ * 新增一条路由 = 新建 `routes/x.ts` + **该域自己的 `X_ROUTES` 加一行** + 本表的拼接列表加一段
42
+ * (+ `test/route-order-freeze.test.ts` 的冻结表加一行,那是 position 的外部对拍源)。
43
+ * `position` / `id` / `methods` / 匹配式都是必填 —— 忘了就是**编译红**,不是静默 404。
34
44
  *
35
45
  * 🔴 **旧禁令已解除**:标签表此前必须「写字面量不引常量」、注释里不许出现某两个字符 —— 那是两道
36
46
  * 扫源码文本的门反向塑形源码的产物(它们的捕获正则抓不到常量、非贪婪捕获会被那对字符提前截断)。
37
47
  * 两道门都改成读本表之后,这条禁令没有了对象,随它的 11 处行注一并删;要引常量就引(`routes/*.ts`
38
48
  * 的 `*_PATH` 常量本来就是那些路径的单一属主)。
39
49
  *
40
- * 🔴 本文件是**叶模块**:只 import `routes/*.ts` 的 handler 与几只单源正则、以及类型。组合根与流水线
50
+ * 🔴 本文件是**叶模块**:只 import `routes/*.ts` 的 handler 与 `X_ROUTES`、以及类型。组合根与流水线
41
51
  * (`server.ts` / `dispatch.ts`)一律反过来 import 它,值边单向,`test/module-cycle-gate.test.ts` 看守。
52
+ * 反向的**类型**边也没有:`RouteDecl` 住在 `route-ctx.js`,域模块 import 的是它,不是本文件。
42
53
  */
43
54
  import type { FlatServiceDeps } from "./server.js";
44
- import type { RouteHandler } from "./route-ctx.js";
55
+ import type { IncomingMessage, ServerResponse } from "node:http";
56
+ import type { RouteDecl, RouteDispatchDecl, RouteMatch, RouteCtx, RouteMethod } from "./route-ctx.js";
45
57
  /** 一只域 handler 相对**全局服务凭据门**的位置。门前 = 未鉴权可达(每一只都要单独论证),门后 = 先过凭据。 */
46
58
  export type RoutePosition = "pre-credential" | "post-credential";
47
59
  export interface RouteRow {
48
- /** 域入口(`routes/*.ts` 的 `handleX`)。签名一致性是编译期钉:入口漂了形 = 编译红,不是运行时静默 404。 */
49
- readonly handler: RouteHandler;
60
+ /** 域入口(`routes/*.ts` 的 `handleX`)。签名一致性是编译期钉:入口漂了形 = 编译红,不是运行时静默 404。
61
+ *
62
+ * 🔴 **方法式**声明(不是 `handler: RouteHandler`):方法式的参数位是**双变**的,于是一只只认自己
63
+ * 那几个 `id` 的域入口(`RouteHandler<"a" | "b">`)能落进这张宽表,而域内那只闭集 `switch` 仍按它
64
+ * 自己的窄 union 判穷尽。窄↔宽的**绑定**由 {@link row} 工厂在编译期钉死:`decls` 与 `handler` 配错
65
+ * (把 B 域的表配给 A 域的入口)就是红,不是运行期的静默全不命中。 */
66
+ handler(req: IncomingMessage, res: ServerResponse, match: RouteMatch, ctx: RouteCtx): Promise<void>;
67
+ /** 本域**真应答的那些口**(S-146 第三刀 车②)—— 一行 ROUTES = 一只域入口 = 它的口集合。
68
+ * `ROUTE_DECLARATIONS` 就是这一列按行序摊平,所以「声明表」与「分派表」再不可能是两份名单:
69
+ * 一只域忘了挂自己的声明 = 它一个口都分派不到,当场全红,而不是静默漂一行。 */
70
+ readonly decls: readonly RouteDecl[];
50
71
  /** 必填 —— 新增路由必须**显式**决定它在凭据门的哪一侧。 */
51
72
  readonly position: RoutePosition;
52
73
  /** 域名(kebab;= `routes/` 下的模块名)。评审把本表当「域全集」读时它是那份清单的名字列,
@@ -62,42 +83,33 @@ export declare const ROUTES: readonly RouteRow[];
62
83
  * 「门前行不许出现在门后行之后」由 `test/route-order-freeze.test.ts` 钉住。 */
63
84
  export declare const PRE_CREDENTIAL_ROUTES: readonly RouteRow[];
64
85
  export declare const POST_CREDENTIAL_ROUTES: readonly RouteRow[];
65
- /** 改写门是**方法感知**的(它的撤销动词是 DELETE)。闭集 —— 加第五个动词是编译红。 */
66
- export type RouteMethod = "GET" | "POST" | "PUT" | "DELETE";
67
- interface RouteDeclFacts {
68
- /** 「这条 url **烧模型**」—— drain / model-roster-pending / 无 service-token 三道 503 的共同判别。
69
- * 没有这一列 = 不烧模型(零默认值:要它为真必须显式写出来,漏写的方向是**少收一笔账单**而不是
70
- * 少一道门 —— 反了才是灾难,所以三道门读的是 true 这一侧)。 */
71
- readonly billable?: true;
72
- /** 「本条 url 在**没有任何 service credential** 的部署形下,这几个方法必须 fail-closed」。
73
- * 判据(两个合取项)= 「授权的唯一输入是 principal 头」∧「持久改写,或爆炸半径跨租户的治理读」;
74
- * 逐口论证在各 `routes/*.ts` 的头注里,本表只记结论。 */
75
- readonly credentialGated?: readonly RouteMethod[];
76
- /** 「这条 url **是**计费提交面,但它到底提不提交由**请求体里的方法**决定」—— 单 URL 多方法的
77
- * JSON-RPC 端点。两道**可用性** 503(drain / roster-pending)按 url 一刀切会把同端点上的纯读一起
78
- * 关掉,所以分派器对本行**跳过**那两道,改由域模块在写分支里施加同样的两道(文案逐字同源)。
79
- * 🔴 只豁免那两道,**不豁免安全轴**:`billable` 本身仍为 true,无 service token 那道 fail-closed
80
- * 503 照旧罩着(它判的是「这台机能不能收写」,与方法无关,且必须在解析请求体之前)。 */
81
- readonly methodDispatched?: true;
82
- }
83
86
  /**
84
- * 一行的匹配式三选一(闭集;`path` 行的标签**就是** path 自身,所以它没有 label 列 —— 少一处手抄)。
85
- * `prefix` 与 `pattern` 行的 label 可缺席:那是**纯门行**(比标签行宽,只供门的并集判据),
86
- * 缺席 ⇒ 本行不铸任何标签。
87
+ * 全表 = 流水线四行 + **每一行 `ROUTES` 自己的 `decls`**,按行序摊平。
88
+ *
89
+ * 🔴 车② 之前这里是一张手写的 `...X_ROUTES` 拼接列表,而它与 `ROUTES` 是**两份**按同一条口径排的
90
+ * 名单(「域首次出现的位置」),漏一段 / 排错序都只在标签面才看得见。现在拼接就是 `ROUTES` 的投影:
91
+ * `a2a-serve.ts` / `workflows.ts` / `runs.ts` 三只文件各占两行域入口,车② 把它们的 `X_ROUTES`
92
+ * **按行拆成两只数组**各归各行(`A2A_SERVE_ROUTES`/`A2A_RPC_ROUTES`、`WORKFLOWS_ROUTES`/
93
+ * `WORKFLOW_AGENT_STEER_ROUTES`、`RUNS_ROUTES`/`RUN_VERBS_ROUTES`)—— 于是「一行域入口 = 一组口」
94
+ * 这句话没有例外,拼接列表这件手抄活儿随之消失。
95
+ *
96
+ * 🔴 顺序的判据只有一条:**形状行取首命中**(精确行由 `LABEL_BY_PATH` 先查,前缀行不铸标签 ⇒ 两者
97
+ * 与序无关)。按域重排后形状行的相对序确实变了(例如 `SESSION_WAKE_RE` 从 `$verb` 行之前挪到了
98
+ * 之后),但每一对「重排后跨了身位」的形状行都**互不重叠**或**同标签**,所以 `routeLabel` 的
99
+ * 像逐字未变 —— 这不是靠读出来的,是 `test/route-match-parity.test.ts` 的 `labels` 金样逐格钉住的。
100
+ *
101
+ * 🔴 由 `test/route-shape-roster.test.ts` 层A 看守:本表每一行的样本经 `routeLabel` 必须落非 "other",
102
+ * 且标签静态段与样本逐段吻合、族段不是占位符。新路由不补本表 ⇒ 那门当场红并点名。
87
103
  */
88
- export type RouteDecl = (RouteDeclFacts & {
89
- readonly path: string;
90
- }) | (RouteDeclFacts & {
91
- readonly prefix: string;
92
- readonly label?: string;
93
- }) | (RouteDeclFacts & {
94
- readonly pattern: RegExp;
95
- readonly label?: string;
96
- });
97
- /** 🔴 由 `test/route-shape-roster.test.ts` 棘轮看守:门机器枚举 `src/http/**` 的全部路由模式并断言每条
98
- * 都映射到非 "other"——新路由不补本表该门当场红(census 抓获本表曾漂掉 56 条模式:session-sync/workspace/
99
- * workflows/subagents/attachments/images/leader 等整族全折 "other",metrics 不可分桶)。 */
100
104
  export declare const ROUTE_DECLARATIONS: readonly RouteDecl[];
105
+ /**
106
+ * **匹配一次**(S-146 第三刀 车②)—— 一条可分派行对一条 url:命中 ⇒ 具名捕获组表,不命中 ⇒ `null`。
107
+ *
108
+ * 🔴 `null` 与 `{}` 是**两件事**(`{}` = 命中且无参),所以返回值不是 `Record | undefined`。
109
+ * 没参与匹配的可选组不入表(`m.groups` 里它们是 `undefined`,原样留着 —— 域内判「这一段在不在」
110
+ * 读的就是这个 `undefined`,与改前 `m[i] === undefined` 逐字同形)。
111
+ */
112
+ export declare function matchDecl(d: RouteDispatchDecl, url: string): Readonly<Record<string, string | undefined>> | null;
101
113
  /** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`). */
102
114
  export declare function routeLabel(_method: string, url: string): string;
103
115
  /** POST endpoints that trigger BILLABLE work — the fail-closed auth guard must cover ALL of them. */
@@ -109,5 +121,21 @@ export declare function isBillableSubmitPath(url: string): boolean;
109
121
  export declare function isCredentialGatedRewrite(method: string, url: string): boolean;
110
122
  /** 见 `RouteDeclFacts.methodDispatched` 的头注(单 URL 多方法端点)。 */
111
123
  export declare function isMethodDispatchedSubmitPath(url: string): boolean;
112
- export {};
124
+ /** 一次分派查找的结果:命中的行与它的参数,外加**这条路径上所有行**的方法并集(405 的 `Allow`)。 */
125
+ export interface RouteLookup {
126
+ /** 方法也对上了 ⇒ 这一行;路径对上了但方法都不在 ⇒ `null`(调用方据 {@link allow} 发 405)。 */
127
+ readonly decl: RouteDispatchDecl | null;
128
+ readonly params: Readonly<Record<string, string | undefined>>;
129
+ /** 本段里**路径**命中的每一行的 `methods` 并集(按声明序去重;空 = 这条路径在本段一行都不占)。 */
130
+ readonly allow: readonly RouteMethod[];
131
+ }
132
+ /**
133
+ * 在一组可分派行里按「**精确 → 形状**,同类按声明序」找路径命中,再按方法过滤。
134
+ *
135
+ * 🔴 **路径命中不等于就是它**:方法不在这一行的 `methods` 里就继续往后找(`GET /v1/images/bakes`
136
+ * 落 `image-by-profile` 而不是只认 `POST` 的 `image-bake-submit`,与改前域内 if 链的落点逐字同)。
137
+ * 全部路径命中行的方法并集就是 `Allow` —— 并集按**路径**取而不是按行,所以同一条路径住着两只域时
138
+ * (`/v1/tasks` 的 GET 在 trace-usage、POST 在 tasks)405 的 `Allow` 是两行之和。
139
+ */
140
+ export declare function lookupRoute(decls: readonly RouteDecl[], url: string, method: string): RouteLookup;
113
141
  //# sourceMappingURL=route-table.d.ts.map