@sema-agent/server 7.114.0 → 7.116.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 (159) hide show
  1. package/MIGRATION.md +237 -2
  2. package/USAGE.md +39 -3
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-ask-audit-store.d.ts +25 -6
  10. package/dist/approval-ask-audit-store.js +6 -1
  11. package/dist/approval-card.d.ts +25 -24
  12. package/dist/approval-card.js +14 -0
  13. package/dist/approval-crash-convergence.d.ts +60 -0
  14. package/dist/approval-crash-convergence.js +75 -0
  15. package/dist/approval-deny-reasons.d.ts +13 -0
  16. package/dist/approval-deny-reasons.js +2 -0
  17. package/dist/approval.js +7 -3
  18. package/dist/boot/coordinators.js +0 -12
  19. package/dist/boot/execution-env.js +2 -1
  20. package/dist/boot/exit-boundary.d.ts +3 -2
  21. package/dist/boot/exit-boundary.js +3 -0
  22. package/dist/boot/resolve-spec.js +5 -1
  23. package/dist/boot/shutdown.d.ts +19 -0
  24. package/dist/boot/shutdown.js +12 -1
  25. package/dist/boot/stage-07-capability-layer.js +21 -15
  26. package/dist/boot/stores.js +4 -4
  27. package/dist/capabilities/composite-members.d.ts +1 -0
  28. package/dist/capabilities/composite-members.js +1 -0
  29. package/dist/capabilities/hands-lane.d.ts +0 -23
  30. package/dist/capabilities/hands-lane.js +3 -2
  31. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  32. package/dist/capabilities/send-user-file-tool.js +1 -8
  33. package/dist/capability-facts.d.ts +296 -0
  34. package/dist/capability-facts.js +354 -0
  35. package/dist/config-catalog.js +2 -1
  36. package/dist/config-center/apply-effective.js +11 -11
  37. package/dist/config-provider.d.ts +16 -0
  38. package/dist/config-provider.js +11 -2
  39. package/dist/config-types.d.ts +20 -1
  40. package/dist/config.d.ts +55 -29
  41. package/dist/config.js +48 -25
  42. package/dist/deployment-governance.d.ts +15 -4
  43. package/dist/deployment-governance.js +20 -2
  44. package/dist/fleet/fleet-bus.d.ts +1 -22
  45. package/dist/hooks/hook-runner.d.ts +29 -3
  46. package/dist/hooks/hook-runner.js +30 -17
  47. package/dist/http/admission.d.ts +6 -0
  48. package/dist/http/admission.js +29 -7
  49. package/dist/http/approval-sweeps.js +20 -0
  50. package/dist/http/checkpoint-ctx.d.ts +68 -0
  51. package/dist/http/checkpoint-ctx.js +36 -0
  52. package/dist/http/durable-run-leg.js +2 -1
  53. package/dist/http/principal-gate.d.ts +3 -0
  54. package/dist/http/principal-gate.js +3 -0
  55. package/dist/http/resume-legs.js +19 -1
  56. package/dist/http/retired-skill-manifest.d.ts +23 -0
  57. package/dist/http/retired-skill-manifest.js +22 -0
  58. package/dist/http/route-ctx.d.ts +7 -0
  59. package/dist/http/route-table.d.ts +3 -2
  60. package/dist/http/route-table.js +5 -5
  61. package/dist/http/routes/a2a-serve.d.ts +1 -3
  62. package/dist/http/routes/a2a-serve.js +14 -23
  63. package/dist/http/routes/admin-config-refresh.js +7 -2
  64. package/dist/http/routes/admin-drain.js +7 -2
  65. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  66. package/dist/http/routes/admin-memory-optout.js +20 -7
  67. package/dist/http/routes/adoption.js +19 -5
  68. package/dist/http/routes/agents-roster.js +4 -2
  69. package/dist/http/routes/approvals-assistant.js +41 -18
  70. package/dist/http/routes/background.d.ts +0 -2
  71. package/dist/http/routes/background.js +4 -5
  72. package/dist/http/routes/capabilities.d.ts +2 -1
  73. package/dist/http/routes/capabilities.js +63 -65
  74. package/dist/http/routes/config-catalog.d.ts +4 -5
  75. package/dist/http/routes/config-catalog.js +16 -2
  76. package/dist/http/routes/devices.js +17 -11
  77. package/dist/http/routes/diagnostics.d.ts +5 -0
  78. package/dist/http/routes/diagnostics.js +8 -1
  79. package/dist/http/routes/fleet.js +4 -2
  80. package/dist/http/routes/images.js +30 -3
  81. package/dist/http/routes/leader.js +7 -1
  82. package/dist/http/routes/mcp-reconnect.js +17 -10
  83. package/dist/http/routes/memory-bundle.js +20 -6
  84. package/dist/http/routes/memory-compliance.js +19 -5
  85. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  86. package/dist/http/routes/memory-consolidation.js +17 -3
  87. package/dist/http/routes/memory-origin.js +19 -5
  88. package/dist/http/routes/notify-wake.js +5 -3
  89. package/dist/http/routes/observability.js +19 -5
  90. package/dist/http/routes/retention-ops.js +6 -1
  91. package/dist/http/routes/rules.js +49 -21
  92. package/dist/http/routes/run-memory-capture.js +5 -2
  93. package/dist/http/routes/runs.js +90 -60
  94. package/dist/http/routes/session-sync.js +64 -53
  95. package/dist/http/routes/sessions-list.js +8 -5
  96. package/dist/http/routes/sessions.js +40 -25
  97. package/dist/http/routes/shared-memory.js +5 -3
  98. package/dist/http/routes/tasks.js +5 -3
  99. package/dist/http/routes/trace-usage.js +18 -11
  100. package/dist/http/routes/workflows.js +36 -13
  101. package/dist/http/server.js +4 -1
  102. package/dist/http/wire-types.d.ts +6 -0
  103. package/dist/index.d.ts +1 -1
  104. package/dist/leader/wire.js +2 -1
  105. package/dist/local-data-root.d.ts +1 -1
  106. package/dist/mcp-content-origin.d.ts +0 -8
  107. package/dist/model-entry-refusal.d.ts +10 -2
  108. package/dist/model-entry-refusal.js +19 -3
  109. package/dist/namespaced-rule-name.d.ts +54 -0
  110. package/dist/namespaced-rule-name.js +33 -0
  111. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  112. package/dist/observability/corrupt-read-seat.js +4 -3
  113. package/dist/observability/fail-open.d.ts +10 -9
  114. package/dist/observability/fail-open.js +5 -1
  115. package/dist/observability/permission-rule-events.d.ts +3 -0
  116. package/dist/observability/permission-rule-events.js +4 -0
  117. package/dist/permission-rule-list.d.ts +81 -0
  118. package/dist/permission-rule-list.js +17 -0
  119. package/dist/plugins/file-run-store.d.ts +7 -1
  120. package/dist/plugins/file-run-store.js +20 -7
  121. package/dist/plugins/memory-run-store.js +2 -1
  122. package/dist/plugins/one-time-migrations.d.ts +4 -2
  123. package/dist/plugins/one-time-migrations.js +26 -3
  124. package/dist/plugins/pg-session-storage.js +1 -1
  125. package/dist/plugins/run-store-sql.js +4 -4
  126. package/dist/plugins/session-policy-store-sql.js +4 -2
  127. package/dist/plugins/store-backend.d.ts +6 -1
  128. package/dist/plugins/store-backend.js +2 -1
  129. package/dist/plugins/store-contracts.d.ts +27 -8
  130. package/dist/plugins/store-contracts.js +12 -3
  131. package/dist/plugins/tidb-session-store.js +1 -1
  132. package/dist/request-key-closure.js +1 -1
  133. package/dist/run-local.d.ts +37 -3
  134. package/dist/run-local.js +52 -6
  135. package/dist/runs.d.ts +0 -58
  136. package/dist/runs.js +15 -20
  137. package/dist/runtime-governance.d.ts +21 -1
  138. package/dist/runtime-governance.js +6 -1
  139. package/dist/security.js +2 -2
  140. package/dist/session-policy-wire.d.ts +40 -59
  141. package/dist/session-policy-wire.js +60 -32
  142. package/dist/spec-fields.d.ts +32 -0
  143. package/dist/spec-fields.js +31 -16
  144. package/dist/standing-answer-grant.d.ts +8 -0
  145. package/dist/standing-answer-grant.js +5 -0
  146. package/dist/task-settings.d.ts +17 -6
  147. package/dist/task-settings.js +16 -30
  148. package/dist/tool-approval.d.ts +32 -4
  149. package/dist/tool-approval.js +31 -8
  150. package/dist/trace/core-keyset-guard.d.ts +13 -8
  151. package/dist/trace/project.js +9 -0
  152. package/dist/trace/self-minted-wire.d.ts +42 -0
  153. package/dist/trace/self-minted-wire.js +13 -0
  154. package/dist/user-input-ledger.d.ts +8 -5
  155. package/package.json +6 -4
  156. package/scripts/drain-check-local.mjs +528 -0
  157. package/scripts/drain-check.mysql.sql +67 -0
  158. package/scripts/drain-check.pg.sql +71 -0
  159. package/scripts/retired-mode-seed-drain-local.mjs +0 -317
@@ -6,17 +6,16 @@
6
6
  * 人肉汇编必然漂移。目录本体(逐键登记 + 实算)在 `src/config-catalog.ts`(D1 双向对账门钉住完备性),
7
7
  * 本文件只做门与投影。
8
8
  *
9
- * 门序 = `POST /v1/admin/config/refresh` 逐字同族(operator lane):身份 → 授权 → 应答;billable=false,
9
+ * 门序 = operator 面口径:身份 → 能力(事实 `configCatalog`,501)→ 授权 → 应答;billable=false,
10
10
  * 刻意不吃 drain / roster-pending 两道 503(排障读面在停机窗里恰恰要打得通)。
11
11
  *
12
12
  * 🔴 鉴权 = `explicitOperatorOk`(空名单=谁都不是)——设计稿 §0.5 点名**禁** `isOperator` 那个
13
13
  * 「空名单=人人都是」的 fail-open 谓词:本面是部署事实外泄口(env 在场性/后端形态/限额值),
14
14
  * 单用户形也不放宽。
15
15
  *
16
- * 🔴 旋钮 `CONFIG_CATALOG_ENABLED=false` ⇒ **本域不认领**(装配器尾部 404 `not_found.route`)——
17
- * 与 memory-consolidation 阀门同判:旋钮没开是一句运维表态(合规部署不想让部署事实上 wire),
18
- * 不是部署能力缺失,404 的动作是「去把旋钮打开」,501 会指去一条修不好的路。能力位 `configCatalog`
19
- * 与本判据同源(capabilities.ts,两项合取)。
16
+ * 🔴 旋钮 `CONFIG_CATALOG_ENABLED=false` / operator 名单空 ⇒ 501(事实 `configCatalog`,与能力位同一只函数):
17
+ * 旋钮关 = `feature.config_catalog_disabled`(合规部署不想让部署事实上 wire 的表态;「去开旋钮」由码与句承担),
18
+ * 名单空 = `capability.operator_principals_required`。HTTP 上「这台部署上不可用」只有 501 一种答。
20
19
  *
21
20
  * 🔴 secret 纪律:值的裁剪在 catalog 构建层(secret=在场位、opaque=在场位+长度、url=剥 userinfo),
22
21
  * 本文件不再碰值 —— 一处裁剪,机器钉 = test/config-catalog.test.ts 的响应体 canary 扫描格。
@@ -1,11 +1,12 @@
1
1
  import { DOMAINS } from "@sema-agent/settings-schema";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sendOperatorPrincipalsRequired } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
+ import * as facts from "../../capability-facts.js";
5
6
  import { buildRetiredEnvWire, configKnobTable } from "../../config.js";
6
7
  import { buildConfigCatalog } from "../../config-catalog.js";
7
8
  export const CONFIG_CATALOG_PATH = "/v1/config/catalog";
8
- const OPERATOR_ONLY_MESSAGE = "the config catalog is operator-only: the caller principal is not in OPERATOR_PRINCIPALS (an empty list means NO one is an operator — configure it to enable this endpoint)";
9
+ const OPERATOR_ONLY_MESSAGE = "the config catalog is operator-only: the caller principal is not in OPERATOR_PRINCIPALS";
9
10
  export async function handleConfigCatalog(req, res, match, ctx) {
10
11
  const { deps } = ctx;
11
12
  switch (match.decl.id) {
@@ -15,6 +16,19 @@ export async function handleConfigCatalog(req, res, match, ctx) {
15
16
  sendPrincipalRefusal(res, req, deps.config);
16
17
  return;
17
18
  }
19
+ const fact = facts.configCatalog(deps);
20
+ if (!fact.available) {
21
+ switch (fact.why) {
22
+ case "config_catalog_disabled":
23
+ sendError(res, 501, "feature.config_catalog_disabled", "the config catalog is switched off on this deployment (CONFIG_CATALOG_ENABLED=false)");
24
+ return;
25
+ case "operator_principals_empty":
26
+ sendOperatorPrincipalsRequired(res);
27
+ return;
28
+ default:
29
+ return assertNever(fact.why);
30
+ }
31
+ }
18
32
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
19
33
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
20
34
  return;
@@ -1,6 +1,7 @@
1
1
  import { sendError, sendJson } from "../send.js";
2
2
  import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
+ import * as facts from "../../capability-facts.js";
4
5
  import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, sameDeviceOwner, } from "../../device-store.js";
5
6
  import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
6
7
  import { deviceOwnerDomainOf } from "../device-placement.js";
@@ -133,13 +134,22 @@ export async function handleDevices(req, res, match, ctx) {
133
134
  sendPrincipalRefusal(res, req, deps.config);
134
135
  return;
135
136
  }
136
- const store = deps.deviceStore;
137
- const hub = deps.deviceHub;
138
- const enrollment = deps.deviceEnrollment;
139
- if (!store || !hub || !enrollment) {
140
- sendError(res, 501, "capability.device_lane_required", DEVICE_LANE_REQUIRED_MESSAGE);
141
- return;
137
+ const lane = facts.deviceManagement(deps);
138
+ if (!lane.available) {
139
+ switch (lane.why) {
140
+ case "device_hub_absent":
141
+ case "device_store_absent":
142
+ case "device_enrollment_absent":
143
+ sendError(res, 501, "capability.device_lane_required", DEVICE_LANE_REQUIRED_MESSAGE);
144
+ return;
145
+ case "run_store_absent":
146
+ sendError(res, 501, "capability.run_store_required", "device rebind needs the run store to judge the no-active-run door (a durable, cross-replica criterion) — this deployment has no run store wired");
147
+ return;
148
+ default:
149
+ return assertNever(lane.why);
150
+ }
142
151
  }
152
+ const { store, hub, enrollment, runStore } = lane;
143
153
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
144
154
  const domain = deviceOwnerDomainOf(req, deps.config);
145
155
  if (match.decl.id === "devices-list") {
@@ -293,11 +303,7 @@ export async function handleDevices(req, res, match, ctx) {
293
303
  sendDeviceNotFound(res);
294
304
  return;
295
305
  }
296
- if (!deps.runStore) {
297
- sendError(res, 501, "capability.run_store_required", "device rebind needs the run store to judge the no-active-run door (a durable, cross-replica criterion) — this deployment has no run store wired");
298
- return;
299
- }
300
- const activeTaskId = await deps.runStore.getActiveTaskId(rootSessionId);
306
+ const activeTaskId = await runStore.getActiveTaskId(rootSessionId);
301
307
  if (activeTaskId) {
302
308
  sendError(res, 409, "device.rebind_active_run", "this session has a non-terminal run — rebinding now would leave in-flight work addressed at another workspace; cancel the run (or let it finish), then retry", { activeTaskId });
303
309
  return;
@@ -32,6 +32,9 @@ export interface ServerWiringGates {
32
32
  streamAskWindowMs: PostureKnobReading<number>;
33
33
  sessionAutoTitle: PostureKnobReading<boolean>;
34
34
  checkpointStore: boolean;
35
+ /** 7.115.0:运维的 allow 规则(`APPROVAL_GRANT`)原样 —— 空表 = 旋钮未设。规则名是工具名不是秘密,所以本面给全名单(条数 = 长度);
36
+ * 本位**只**在运维面(operator-only,门在路由上),不进 `/v1/capabilities` 的客户端能力位:它说的是部署替谁表了态,不是调用方能用什么。 */
37
+ approvalGrant: readonly string[];
35
38
  }
36
39
  /** `buildServerWiringGates` 的入参 = 三个谓词各自的**真输入**(不接受已算好的布尔:那样这里读到的就
37
40
  * 不再是装配现场的事实,而是别处算完可能已漂的复述)。 */
@@ -49,6 +52,8 @@ export interface ServerWiringGateInput {
49
52
  streamAskWindowMsSource: PostureSource;
50
53
  sessionAutoTitle: boolean;
51
54
  sessionAutoTitleSource: PostureSource;
55
+ /** 7.115.0:`config.approvalGrant` 本身(读面不重算)。 */
56
+ approvalGrant: readonly string[];
52
57
  }
53
58
  /** 纯数据产物(`build*`):三个门的当前读数。`streamApproval` 走**单一谓词**
54
59
  * `resolveStreamApprovalGate` —— 与 `/v1/capabilities` 的 `streamApproval` 位、与回决口的 501 门同一个
@@ -4,7 +4,8 @@ import { projectExecutionLaneCapability } from "../../capabilities/execution-lan
4
4
  import { readOutboundWiring } from "../../outbound-dispatcher.js";
5
5
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
6
6
  import { sendJson, sendError } from "../send.js";
7
- import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
7
+ import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, sendOperatorPrincipalsRequired, verifiedIdentitySource } from "../principal-gate.js";
8
+ import * as facts from "../../capability-facts.js";
8
9
  import { assertNever } from "../route-ctx.js";
9
10
  export function buildServerWiringGates(input) {
10
11
  const gate = resolveStreamApprovalGate({
@@ -35,6 +36,7 @@ export function buildServerWiringGates(input) {
35
36
  postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
36
37
  }),
37
38
  checkpointStore: input.checkpointStore !== undefined,
39
+ approvalGrant: [...input.approvalGrant],
38
40
  };
39
41
  }
40
42
  export function buildStaticWiringAudit(facts) {
@@ -86,6 +88,10 @@ export async function handleDiagnostics(req, res, match, ctx) {
86
88
  switch (match.decl.id) {
87
89
  case "diagnostics-wiring": {
88
90
  if (deps.config.operatorPrincipals.length > 0 || deps.config.requirePrincipal) {
91
+ if (!facts.operatorLane(deps).available) {
92
+ sendOperatorPrincipalsRequired(res);
93
+ return;
94
+ }
89
95
  const principal = gatedPrincipal(req, deps.config);
90
96
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
91
97
  sendError(res, 403, "auth.operator_only", "wiring diagnostics are operator-only on a multi-tenant worker");
@@ -121,6 +127,7 @@ export async function handleDiagnostics(req, res, match, ctx) {
121
127
  streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
122
128
  sessionAutoTitle: deps.config.sessionAutoTitle === true,
123
129
  sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
130
+ approvalGrant: deps.config.approvalGrant,
124
131
  }),
125
132
  });
126
133
  return;
@@ -4,12 +4,14 @@ import { taskNotificationFoldKey } from "../../orchestration/workflow-completion
4
4
  import { sendJson, sendError, sseHeaders } from "../send.js";
5
5
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
+ import * as facts from "../../capability-facts.js";
7
8
  export async function handleFleet(req, res, match, ctx) {
8
9
  const { deps } = ctx;
9
10
  const { sessionOwnerScope } = ctx.helpers;
10
11
  switch (match.decl.id) {
11
12
  case "fleet-stream": {
12
- if (!deps.fleetBus) {
13
+ const fleet = facts.fleetStream(deps);
14
+ if (!fleet.available) {
13
15
  sendError(res, 501, "capability.self_orchestration_required", "fleet stream requires self-orchestration / fleet wiring");
14
16
  return;
15
17
  }
@@ -22,7 +24,7 @@ export async function handleFleet(req, res, match, ctx) {
22
24
  const fleetParams = new URLSearchParams(match.search);
23
25
  const callerSession = fleetParams.get("session");
24
26
  const observeOnly = fleetParams.get("observe") === "1";
25
- await streamFleet(req, res, deps.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
27
+ await streamFleet(req, res, fleet.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
26
28
  return;
27
29
  }
28
30
  default:
@@ -5,9 +5,10 @@ import { manifestToIndexEntry } from "../../images/manifest.js";
5
5
  import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
6
6
  import { streamSseLog } from "../sse-log.js";
7
7
  import { sendJson, sendError } from "../send.js";
8
- import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
8
+ import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired } from "../principal-gate.js";
9
9
  import { scopedIdempotencyKey } from "../idempotency.js";
10
10
  import { assertNever } from "../route-ctx.js";
11
+ import * as facts from "../../capability-facts.js";
11
12
  export function createImagesLocal(deps) {
12
13
  return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
13
14
  }
@@ -50,9 +51,14 @@ export async function handleImages(req, res, match, ctx) {
50
51
  const principal = gatedPrincipal(req, deps.config);
51
52
  const operators = deps.config.operatorPrincipals;
52
53
  const isExplicitOperator = explicitOperatorOk(principal, operators);
54
+ const operatorLane = facts.operatorLane(deps);
53
55
  const cfg = deps.config.imageBakes;
54
56
  const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
55
57
  if (match.decl.id === "image-bake-submit") {
58
+ if (!operatorLane.available) {
59
+ sendOperatorPrincipalsRequired(res);
60
+ return;
61
+ }
56
62
  if (!isExplicitOperator) {
57
63
  sendError(res, 403, "auth.operator_only", "operator only");
58
64
  return;
@@ -124,6 +130,10 @@ export async function handleImages(req, res, match, ctx) {
124
130
  return;
125
131
  }
126
132
  if (match.decl.id === "image-bake-events") {
133
+ if (!operatorLane.available) {
134
+ sendOperatorPrincipalsRequired(res);
135
+ return;
136
+ }
127
137
  if (!isExplicitOperator) {
128
138
  sendError(res, 403, "auth.operator_only", "operator only");
129
139
  return;
@@ -142,6 +152,10 @@ export async function handleImages(req, res, match, ctx) {
142
152
  return;
143
153
  }
144
154
  if (match.decl.id === "image-bake-cancel") {
155
+ if (!operatorLane.available) {
156
+ sendOperatorPrincipalsRequired(res);
157
+ return;
158
+ }
145
159
  if (!isExplicitOperator) {
146
160
  sendError(res, 403, "auth.operator_only", "operator only");
147
161
  return;
@@ -250,6 +264,10 @@ export async function handleImages(req, res, match, ctx) {
250
264
  return;
251
265
  }
252
266
  if (match.decl.id === "image-bake-detail") {
267
+ if (!operatorLane.available) {
268
+ sendOperatorPrincipalsRequired(res);
269
+ return;
270
+ }
253
271
  if (!isExplicitOperator) {
254
272
  sendError(res, 403, "auth.operator_only", "operator only");
255
273
  return;
@@ -269,8 +287,9 @@ export async function handleImages(req, res, match, ctx) {
269
287
  }
270
288
  return assertNever(match.decl.id);
271
289
  }
272
- if (deps.imageIndex && isIndexRouteId(match.decl.id)) {
273
- const idx = deps.imageIndex;
290
+ const pool = facts.imagePool(deps);
291
+ if (pool.available && isIndexRouteId(match.decl.id)) {
292
+ const idx = pool.imageIndex;
274
293
  const principal = gatedPrincipal(req, deps.config);
275
294
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
276
295
  const viewer = { operator, tenantId: principal ?? null };
@@ -322,6 +341,10 @@ export async function handleImages(req, res, match, ctx) {
322
341
  return;
323
342
  }
324
343
  if (match.decl.id === "image-register") {
344
+ if (!facts.operatorLane(deps).available) {
345
+ sendOperatorPrincipalsRequired(res);
346
+ return;
347
+ }
325
348
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
326
349
  sendError(res, 403, "auth.operator_only", "operator only");
327
350
  return;
@@ -397,6 +420,10 @@ export async function handleImages(req, res, match, ctx) {
397
420
  return assertNever(match.decl.id);
398
421
  }
399
422
  {
423
+ if (!pool.available) {
424
+ sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
425
+ return;
426
+ }
400
427
  sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
401
428
  return;
402
429
  }
@@ -1,6 +1,7 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
+ import * as facts from "../../capability-facts.js";
4
5
  export const LEADER_SUBMIT_PATH = "/v1/leader";
5
6
  export async function handleLeader(req, res, match, ctx) {
6
7
  switch (match.decl.id) {
@@ -12,7 +13,12 @@ export async function handleLeader(req, res, match, ctx) {
12
13
  }
13
14
  const { deps } = ctx;
14
15
  const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
15
- const endpoint = deps.leaderEndpoint;
16
+ const lane = facts.leaderLane(deps);
17
+ if (!lane.available) {
18
+ sendError(res, 501, "capability.leader_endpoint_required", "this orchestration endpoint is not available on this deployment: nothing is wired to serve it here");
19
+ return;
20
+ }
21
+ const endpoint = lane.endpoint;
16
22
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
17
23
  sendPrincipalRefusal(res, req, deps.config);
18
24
  return;
@@ -5,6 +5,7 @@ import { sendJson, sendError } from "../send.js";
5
5
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import {} from "../route-ctx.js";
7
7
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
8
+ import * as facts from "../../capability-facts.js";
8
9
  const BodySchema = z.object({ server: z.string().min(1).max(190) }).strict();
9
10
  const NEUTRALIZED_TEXT_MAX = 200;
10
11
  const humanText = (s) => redactSecrets(s).slice(0, NEUTRALIZED_TEXT_MAX);
@@ -56,10 +57,20 @@ export async function handleMcpReconnect(req, res, match, ctx) {
56
57
  case "mcp-reconnect": {
57
58
  if (ctx.helpers.rateLimited(req, res))
58
59
  return;
59
- if (!deps.runStore) {
60
- sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
61
- return;
60
+ const fact = facts.mcpReconnect(deps);
61
+ if (!fact.available) {
62
+ switch (fact.why) {
63
+ case "run_store_absent":
64
+ sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
65
+ return;
66
+ case "session_owner_face_absent":
67
+ sendError(res, 501, "capability.session_ownership_required", "the MCP re-dial verb requires the session-ownership face (sessionStorage.ownerOf)");
68
+ return;
69
+ default:
70
+ return assertNever(fact.why);
71
+ }
62
72
  }
73
+ const { runStore, sessionStorage } = fact;
63
74
  const principal = gatedPrincipal(req, deps.config);
64
75
  if (deps.config.requirePrincipal && principal === undefined) {
65
76
  sendPrincipalRefusal(res, req, deps.config);
@@ -76,11 +87,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
76
87
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
77
88
  return;
78
89
  }
79
- if (!deps.sessionStorage?.ownerOf) {
80
- sendError(res, 501, "capability.session_ownership_required", "the MCP re-dial verb requires the session-ownership face (sessionStorage.ownerOf)");
81
- return;
82
- }
83
- const owner = await deps.sessionStorage.ownerOf(sessionId);
90
+ const owner = await sessionStorage.ownerOf(sessionId);
84
91
  if (owner === undefined) {
85
92
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
86
93
  return;
@@ -88,7 +95,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
88
95
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
89
96
  if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
90
97
  return;
91
- const activeTaskId = await deps.runStore.getActiveTaskId(sessionId);
98
+ const activeTaskId = await runStore.getActiveTaskId(sessionId);
92
99
  const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
93
100
  if (live) {
94
101
  try {
@@ -113,7 +120,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
113
120
  }
114
121
  }
115
122
  }
116
- const run = activeTaskId === undefined ? undefined : await deps.runStore.getRun(activeTaskId);
123
+ const run = activeTaskId === undefined ? undefined : await runStore.getRun(activeTaskId);
117
124
  if (run && isParkedRunStatus(run.status)) {
118
125
  sendError(res, 409, "steering.not_running", "run is waiting on a pending decision — the MCP re-dial verb needs a live turn; a resumed continuation dials every declared server afresh (the re-dial applies to the current turn only and is not persisted), so resume this run first — a re-dial cannot be queued behind the decision");
119
126
  return;
@@ -2,8 +2,9 @@ import { z } from "zod";
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
4
4
  import { buildOriginFencedBundle, readOriginAware } from "../../memory-origin-fence.js";
5
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
+ import * as facts from "../../capability-facts.js";
7
8
  export const MEMORY_BUNDLE_EXPORT_PATH = "/v1/memory/export";
8
9
  export const MEMORY_BUNDLE_IMPORT_PATH = "/v1/memory/import";
9
10
  const ExportRequestSchema = z.object({ scopes: z.array(z.string().min(1)).min(1) });
@@ -67,16 +68,29 @@ export async function handleMemoryBundle(req, res, match, ctx) {
67
68
  sendPrincipalRefusal(res, req, deps.config);
68
69
  return;
69
70
  }
71
+ const fact = facts.memoryBundle(deps);
72
+ if (!fact.available && fact.why === "operator_principals_empty") {
73
+ sendOperatorPrincipalsRequired(res);
74
+ return;
75
+ }
70
76
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
71
77
  sendError(res, 403, "auth.operator_only", "memory governance bundles are an operator-only deployment action");
72
78
  return;
73
79
  }
74
- const engineExport = deps.memoryBundleExport;
75
- const engineImport = deps.memoryBundleImport;
76
- if (engineExport === undefined || engineImport === undefined) {
77
- sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/tidb memory backends do not");
78
- return;
80
+ if (!fact.available) {
81
+ switch (fact.why) {
82
+ case "memory_bundle_faces_absent":
83
+ sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/tidb memory backends do not");
84
+ return;
85
+ case "operator_principals_empty":
86
+ sendOperatorPrincipalsRequired(res);
87
+ return;
88
+ default:
89
+ return assertNever(fact.why);
90
+ }
79
91
  }
92
+ const engineExport = fact.exportScopes;
93
+ const engineImport = fact.importBundle;
80
94
  const raw = await ctx.helpers.readJson(req);
81
95
  switch (match.decl.id) {
82
96
  case "memory-bundle-export": {
@@ -2,8 +2,9 @@ import { z } from "zod";
2
2
  import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
3
3
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
+ import * as facts from "../../capability-facts.js";
7
8
  export const MEMORY_ERASE_PATH = "/v1/memory/erase";
8
9
  const EraseRequestSchema = z.object({
9
10
  requestId: z.string().min(1),
@@ -23,15 +24,28 @@ export async function handleMemoryCompliance(req, res, match, ctx) {
23
24
  sendPrincipalRefusal(res, req, deps.config);
24
25
  return;
25
26
  }
27
+ const fact = facts.memoryCompliance(deps);
28
+ if (!fact.available && fact.why === "operator_principals_empty") {
29
+ sendOperatorPrincipalsRequired(res);
30
+ return;
31
+ }
26
32
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
27
33
  sendError(res, 403, "auth.operator_only", "memory provenance and erasure are operator-only deployment actions");
28
34
  return;
29
35
  }
30
- const faces = deps.memoryCompliance;
31
- if (faces === undefined) {
32
- sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
33
- return;
36
+ if (!fact.available) {
37
+ switch (fact.why) {
38
+ case "memory_compliance_faces_absent":
39
+ sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
40
+ return;
41
+ case "operator_principals_empty":
42
+ sendOperatorPrincipalsRequired(res);
43
+ return;
44
+ default:
45
+ return assertNever(fact.why);
46
+ }
34
47
  }
48
+ const faces = fact.faces;
35
49
  switch (match.decl.id) {
36
50
  case "memory-entry-provenance": {
37
51
  const entryId = ctx.helpers.safeDecode(match.params.entryId);
@@ -6,20 +6,17 @@
6
6
  *
7
7
  * ── 族属与门序 ────────────────────────────────────────────────────────────────────────────────────
8
8
  * 与 `/v1/admin/drain` · `/v1/admin/config/refresh` 逐字同族(operator lane:同一条 `explicitOperatorOk`
9
- * 门)。门序照 operator 面既有口径:身份(401)→ 授权(403)→ 验型(400)。**授权在验型之前** —— 一个
9
+ * 门)。门序照 operator 面口径:身份(401)→ 能力(501,事实)→ 授权(403)→ 验型(400)。**授权在验型之前** —— 一个
10
10
  * 够不着任何东西的调用方不该从「你的 body 形不对」上读出这台部署有没有这条面。
11
11
  *
12
- * ── 为什么缺席形是 **404 族**而不是 501(D4)───────────────────────────────────────────────────────
13
- * 本仓的 501 `capability.*` 说的是「**这家店**在本部署上不存在,换部署形态才行」(记忆引擎没接线、
14
- * 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`),或
15
- * 出厂缺省派生成关(S-824 / [ref]:未设而整理座落主模型 —— 起服时有一行提示点名 `MODEL_CHEAP_ID`),不是部署能力的
16
- * 缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
17
- * 归属 / provenance=off)在**启动期**就被拒了(`boot/memory-consolidation.ts` 的
18
- * `assertConsolidationDriverWirable`)—— 那些机器根本起不来,不会有一个"挂着的 501"去代表它们。
19
- * ⇒ 剩下的唯一缺席成因就是「阀门没开」,而它的诚实形是**这条路径不存在**:挂载条件写在 server.ts 的
20
- * 域头合取式里,能力位 `memoryConsolidationDriver` 读同一个事实,「说 yes ⟺ 打得通」因此是结构性的。
21
- * 🔴 刻意与 permissionRules 的 501 分家:那一族的 501 是「规则店没装」,消费端的动作是换部署;本族的
22
- * 404 的动作是「去把旋钮打开」。同码会让运维照着一条修不好的路走。
12
+ * ── 缺席形(D4):**501**,与能力位同一只事实 ─────────────────────────────────────────────────────
13
+ * 两口恒挂载;域头调事实 `memoryConsolidation`(`src/capability-facts.ts`,`capabilities.memoryConsolidationDriver` 读的是同一只):
14
+ * · 阀门关(`MEMORY_CONSOLIDATION_DRIVER=off`,或 S-824 出厂缺省派生成关 —— 未设而整理座落主模型)⇒ 501
15
+ * `feature.memory_consolidation_disabled`。旋钮关是一句运维表态,不是部署能力缺失 —— 「去开旋钮」由 `feature.*` 这一族码
16
+ * 与拒句承担(同仓旋钮关的既有答法),不靠状态码:HTTP 上「这台部署上不可用」只有 501 一种答。
17
+ * · operator 名单空 ⇒ 501 `capability.operator_principals_required`(对谁都打不通 = 部署不可用,先于授权答)。
18
+ * 阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面归属 / provenance=off)在**启动期**就被拒了
19
+ * (`boot/memory-consolidation.ts` 的 `assertConsolidationDriverWirable`),那些机器根本起不来。
23
20
  *
24
21
  * ── scope **恒不猜**(D2)────────────────────────────────────────────────────────────────────────
25
22
  * 一轮 consolidation 读**全库**(~1e5 prompt tokens/轮)。在两个库之间替 operator 挑一个去花这笔钱,
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sendOperatorPrincipalsRequired } from "../principal-gate.js";
5
5
  import { assertNever } from "../route-ctx.js";
6
+ import * as facts from "../../capability-facts.js";
6
7
  export const MEMORY_CONSOLIDATION_STATUS_PATH = "/v1/admin/memory/consolidation";
7
8
  export const MEMORY_CONSOLIDATION_RUN_PATH = "/v1/admin/memory/consolidation/run";
8
9
  const RunRequestSchema = z.object({ scope: z.string().optional() }).strict();
@@ -72,14 +73,27 @@ function codeOf(err) {
72
73
  }
73
74
  export async function handleMemoryConsolidation(req, res, match, ctx) {
74
75
  const { deps } = ctx;
75
- const faces = deps.memoryConsolidation;
76
76
  const principal = gatedPrincipal(req, deps.config);
77
77
  if (deps.config.requirePrincipal && !principal) {
78
78
  sendPrincipalRefusal(res, req, deps.config);
79
79
  return;
80
80
  }
81
+ const fact = facts.memoryConsolidation(deps);
82
+ if (!fact.available) {
83
+ switch (fact.why) {
84
+ case "consolidation_valve_closed":
85
+ sendError(res, 501, "feature.memory_consolidation_disabled", "memory consolidation is switched off on this deployment (MEMORY_CONSOLIDATION_DRIVER is off, or its default derived off because no cheap model is configured); turn it on to use these endpoints");
86
+ return;
87
+ case "operator_principals_empty":
88
+ sendOperatorPrincipalsRequired(res);
89
+ return;
90
+ default:
91
+ return assertNever(fact.why);
92
+ }
93
+ }
94
+ const faces = fact.valve;
81
95
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
82
- sendError(res, 403, "auth.operator_only", "memory consolidation is operator-only: the caller principal is not in OPERATOR_PRINCIPALS (an empty list means NO one is an operator — configure it to enable these endpoints)");
96
+ sendError(res, 403, "auth.operator_only", "memory consolidation is operator-only: the caller principal is not in OPERATOR_PRINCIPALS");
83
97
  return;
84
98
  }
85
99
  switch (match.decl.id) {
@@ -3,8 +3,9 @@ import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
5
5
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
6
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
+ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
7
7
  import { assertNever } from "../route-ctx.js";
8
+ import * as facts from "../../capability-facts.js";
8
9
  export const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external";
9
10
  export const MEMORY_ORIGIN_CLEARANCES_PATH = "/v1/memory/origin/clearances";
10
11
  const ClearRequestSchema = z.object({
@@ -116,15 +117,28 @@ export async function handleMemoryOrigin(req, res, match, ctx) {
116
117
  sendPrincipalRefusal(res, req, deps.config);
117
118
  return;
118
119
  }
120
+ const fact = facts.memoryOrigin(deps);
121
+ if (!fact.available && fact.why === "operator_principals_empty") {
122
+ sendOperatorPrincipalsRequired(res);
123
+ return;
124
+ }
119
125
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
120
126
  sendError(res, 403, "auth.operator_only", "memory origin marking and clearance are operator-only deployment actions");
121
127
  return;
122
128
  }
123
- const faces = deps.memoryOriginFace;
124
- if (faces === undefined) {
125
- sendError(res, 501, "capability.memory_engine_required", "memory origin marking and clearance are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the clearance ledger and its write-ahead custody rows live in that plane, and on a backend without it they would land on one replica's local disk");
126
- return;
129
+ if (!fact.available) {
130
+ switch (fact.why) {
131
+ case "memory_origin_faces_absent":
132
+ sendError(res, 501, "capability.memory_engine_required", "memory origin marking and clearance are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the clearance ledger and its write-ahead custody rows live in that plane, and on a backend without it they would land on one replica's local disk");
133
+ return;
134
+ case "operator_principals_empty":
135
+ sendOperatorPrincipalsRequired(res);
136
+ return;
137
+ default:
138
+ return assertNever(fact.why);
139
+ }
127
140
  }
141
+ const faces = fact.faces;
128
142
  switch (match.decl.id) {
129
143
  case "memory-origin-external": {
130
144
  const scopes = parseScopes(new URLSearchParams(match.search).getAll("scopes"));
@@ -3,6 +3,7 @@ import { sendJson, sendError } from "../send.js";
3
3
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { sendResumeOutcome } from "../route-ctx.js";
5
5
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
6
+ import * as facts from "../../capability-facts.js";
6
7
  export async function handleNotifyWake(req, res, match, ctx) {
7
8
  const { deps } = ctx;
8
9
  const { steerableRuns } = ctx.registry;
@@ -57,11 +58,12 @@ export async function handleNotifyWake(req, res, match, ctx) {
57
58
  sendError(res, 400, "request.field_invalid", "source must be a string of at most 190 characters when present");
58
59
  return;
59
60
  }
60
- if (!deps.sessionStorage?.ownerOf) {
61
+ const owned = facts.sessionOwnership(deps);
62
+ if (!owned.available) {
61
63
  sendError(res, 501, "capability.session_ownership_required", "external notify requires the session-ownership face (sessionStorage.ownerOf)");
62
64
  return;
63
65
  }
64
- const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
66
+ const notifyOwner = await owned.sessionStorage.ownerOf(notifySession);
65
67
  if (notifyOwner === undefined) {
66
68
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
67
69
  return;
@@ -104,7 +106,7 @@ export async function handleNotifyWake(req, res, match, ctx) {
104
106
  return;
105
107
  }
106
108
  case "session-wake": {
107
- if (!deps.checkpointStore) {
109
+ if (!facts.checkpointPark(deps).available) {
108
110
  sendError(res, 501, "capability.checkpoint_store_required", "wake requires the checkpoint store");
109
111
  return;
110
112
  }