@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
@@ -1,9 +1,10 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
2
+ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
3
+ import * as facts from "../../capability-facts.js";
3
4
  import { assertNever } from "../route-ctx.js";
4
5
  export const ADMIN_DRAIN_PATH = "/v1/admin/drain";
5
6
  export const MAX_DRAIN_REASON_CHARS = 256;
6
- const OPERATOR_ONLY_MESSAGE = "drain declaration 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)";
7
+ const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS";
7
8
  export async function handleAdminDrain(req, res, match, ctx) {
8
9
  const { deps } = ctx;
9
10
  switch (match.decl.id) {
@@ -13,6 +14,10 @@ export async function handleAdminDrain(req, res, match, ctx) {
13
14
  sendPrincipalRefusal(res, req, deps.config);
14
15
  return;
15
16
  }
17
+ if (!facts.operatorLane(deps).available) {
18
+ sendOperatorPrincipalsRequired(res);
19
+ return;
20
+ }
16
21
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
17
22
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
18
23
  return;
@@ -42,8 +42,6 @@
42
42
  import type { IncomingMessage, ServerResponse } from "node:http";
43
43
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
44
44
  export declare const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
45
- /** 带参口(`:principal`)⇒ 路由声明表(`http/route-table.ts`)里走 `pattern` 行,不是 `path` 行。 */
46
- export declare const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE: RegExp;
47
45
  export declare function handleMemoryOptOut(req: IncomingMessage, res: ServerResponse, match: RouteMatch<MemoryOptOutRouteId>, ctx: RouteCtx): Promise<void>;
48
46
  export declare const MEMORY_OPTOUT_ROUTES: readonly [{
49
47
  readonly id: "memory-optout-grants-list";
@@ -1,10 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
+ import * as facts from "../../capability-facts.js";
5
6
  import { memoryOptOutPrincipalShapeProblem } from "../../plugins/memory-optout-grant-store-sql.js";
6
7
  export const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
7
- export const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE = /^\/v1\/admin\/memory-optout\/([^/]+)$/;
8
8
  const PutBodySchema = z.object({ allow: z.boolean(), note: z.string().min(1).max(500).optional() }).strict();
9
9
  export async function handleMemoryOptOut(req, res, match, ctx) {
10
10
  const { deps } = ctx;
@@ -16,16 +16,29 @@ export async function handleMemoryOptOut(req, res, match, ctx) {
16
16
  sendPrincipalRefusal(res, req, deps.config);
17
17
  return;
18
18
  }
19
+ const fact = facts.memoryOptOutGrant(deps);
20
+ if (!fact.available && fact.why === "operator_principals_empty") {
21
+ sendOperatorPrincipalsRequired(res);
22
+ return;
23
+ }
19
24
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
20
- sendError(res, 403, "auth.operator_only", "memory opt-out grant administration 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)");
25
+ sendError(res, 403, "auth.operator_only", "memory opt-out grant administration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS");
21
26
  return;
22
27
  }
23
28
  const operator = principal;
24
- const store = deps.memoryOptOutGrant;
25
- if (store === undefined) {
26
- sendError(res, 501, "capability.memory_optout_grant_required", "this deployment has no memory opt-out grant table: the per-principal memory-capture opt-out authorization store lives on a SQL store backend (DB_BACKEND=mysql|pg) and the local backend deliberately keeps no companion store for forgetting — wire a SQL backend to administer opt-out grants");
27
- return;
29
+ if (!fact.available) {
30
+ switch (fact.why) {
31
+ case "memory_optout_grant_absent":
32
+ sendError(res, 501, "capability.memory_optout_grant_required", "this deployment has no memory opt-out grant table: the per-principal memory-capture opt-out authorization store lives on a SQL store backend (DB_BACKEND=mysql|pg) and the local backend deliberately keeps no companion store for forgetting — wire a SQL backend to administer opt-out grants");
33
+ return;
34
+ case "operator_principals_empty":
35
+ sendOperatorPrincipalsRequired(res);
36
+ return;
37
+ default:
38
+ return assertNever(fact.why);
39
+ }
28
40
  }
41
+ const store = fact.store;
29
42
  if (target !== undefined && (target === null || memoryOptOutPrincipalShapeProblem(target) !== undefined)) {
30
43
  sendError(res, 400, "request.path_malformed", "invalid memory opt-out grant principal — the path segment must percent-decode to a non-empty principal of at most 190 chars with no leading/trailing whitespace or control characters (the deployment default row is addressed by PUT on the collection root, not by a path segment)");
31
44
  return;
@@ -2,8 +2,9 @@ import { createAdoptionRunner } from "../../adoption/runner.js";
2
2
  import { AdoptionRequestSchema } from "../../adoption/wire.js";
3
3
  import { checkAdoptionPrincipalShape, checkAdoptionWidths } from "../../adoption/plan.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
  function sendOutcome(res, out) {
8
9
  switch (out.kind) {
9
10
  case "receipt":
@@ -46,15 +47,28 @@ export async function handleAdoption(req, res, match, ctx) {
46
47
  sendPrincipalRefusal(res, req, deps.config);
47
48
  return;
48
49
  }
50
+ const fact = facts.adoption(deps);
51
+ if (!fact.available && fact.why === "operator_principals_empty") {
52
+ sendOperatorPrincipalsRequired(res);
53
+ return;
54
+ }
49
55
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
50
56
  sendError(res, 403, "auth.operator_only", "adoption is an operator-only deployment action");
51
57
  return;
52
58
  }
53
- const store = deps.backend?.adoptionLog?.();
54
- if (!store) {
55
- sendError(res, 501, "capability.adoption_store_required", "adoption requires a SQL store backend (DB_BACKEND=mysql|pg)");
56
- return;
59
+ if (!fact.available) {
60
+ switch (fact.why) {
61
+ case "adoption_store_absent":
62
+ sendError(res, 501, "capability.adoption_store_required", "adoption requires a SQL store backend (DB_BACKEND=mysql|pg)");
63
+ return;
64
+ case "operator_principals_empty":
65
+ sendOperatorPrincipalsRequired(res);
66
+ return;
67
+ default:
68
+ return assertNever(fact.why);
69
+ }
57
70
  }
71
+ const store = fact.backend.adoptionLog();
58
72
  const runner = createAdoptionRunner({ store, dialect: deps.backend?.kind === "pg" ? "pg" : "tidb", now: Date.now, logger: deps.logger });
59
73
  switch (match.decl.id) {
60
74
  case "adoption-detail": {
@@ -1,6 +1,7 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { sendUnauthorized } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
+ import * as facts from "../../capability-facts.js";
4
5
  export const ROSTER_SCAN_LIMIT = 500;
5
6
  const ROSTER_STATUSES = ["running", "parked", "completed", "failed", "killed"];
6
7
  const ROSTER_KEY_DISPOSITION = {
@@ -63,11 +64,12 @@ export async function handleAgentRoster(req, res, match, ctx) {
63
64
  sendUnauthorized(res, "identity");
64
65
  return;
65
66
  }
66
- const store = deps.backgroundAgentStore;
67
- if (!store) {
67
+ const roster = facts.agentRoster(deps);
68
+ if (!roster.available) {
68
69
  sendError(res, 501, "capability.background_agent_store_required", "the agent roster requires a durable background-agent store (DB_BACKEND=mysql|pg, or the local file backend)");
69
70
  return;
70
71
  }
72
+ const store = roster.store;
71
73
  const q = new URLSearchParams(match.search);
72
74
  const limitRaw = q.get("limit");
73
75
  if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
@@ -2,7 +2,7 @@ import { screenedGate } from "../../trace/project.js";
2
2
  import { hostDecisionFacts } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
5
- import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, standingAnswerMayRetire } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -17,8 +17,10 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
+ import { answerGrantsBeyondCall } from "../../standing-answer-grant.js";
20
21
  import { sendResumeOutcome } from "../route-ctx.js";
21
22
  import { assertNever } from "../route-ctx.js";
23
+ import * as facts from "../../capability-facts.js";
22
24
  import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
23
25
  import { recordFailOpen } from "../../observability/fail-open.js";
24
26
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
@@ -258,7 +260,12 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
258
260
  const { readJson, rateLimited, quotaExceeded, safeDecode } = ctx.helpers;
259
261
  const { resumeCheckpoint, resumePreempted, preparePlanReview, drivePlanReview } = ctx.legs;
260
262
  {
261
- const cs = deps.checkpointStore;
263
+ const park = facts.checkpointPark(deps);
264
+ if (!park.available) {
265
+ sendError(res, 501, "capability.checkpoint_store_required", "the approvals and assistant faces need a durable approval store (DURABLE_APPROVAL=true); this deployment has none wired");
266
+ return;
267
+ }
268
+ const cs = park.checkpointStore;
262
269
  const principal = gatedPrincipal(req, deps.config);
263
270
  const operator = isOperator(principal, deps.config.operatorPrincipals);
264
271
  if (match.decl.id === "approvals-stream") {
@@ -295,11 +302,12 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
295
302
  const operators = deps.config.operatorPrincipals;
296
303
  const isExplicitOperator = explicitOperatorOk(principal, operators);
297
304
  if (!isExplicitOperator) {
298
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
299
- if (!ownerOf) {
305
+ const owned = facts.sessionOwnership(deps);
306
+ if (!owned.available) {
300
307
  sendError(res, 501, "capability.session_store_required", "approval exemptions require a session-store backend");
301
308
  return;
302
309
  }
310
+ const ownerOf = owned.sessionStorage.ownerOf.bind(owned.sessionStorage);
303
311
  const owner = await ownerOf(exSessionId);
304
312
  if (owner === undefined || (owner !== null && owner !== principal)) {
305
313
  sendError(res, 404, "not_found.session", "not found");
@@ -311,7 +319,8 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
311
319
  sendError(res, 404, "request.route_unsupported", "not found");
312
320
  return;
313
321
  }
314
- sendJson(res, 200, { exemptions: await deps.approvalExemptionStore.list(exSessionId) });
322
+ const rows = await deps.approvalExemptionStore.list(exSessionId);
323
+ sendJson(res, 200, { exemptions: rows.filter((r) => !answerGrantsBeyondCall({ toolName: r.toolName })) });
315
324
  return;
316
325
  }
317
326
  if (exToolName === undefined) {
@@ -357,13 +366,15 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
357
366
  if (match.decl.id === "assistant-tasks") {
358
367
  const ownerParam = new URLSearchParams(match.search).get("owner") || undefined;
359
368
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
360
- if (!deps.runStore) {
369
+ const ledger = facts.durableRuns(deps);
370
+ if (!ledger.available) {
361
371
  sendJson(res, 200, { tasks: [] });
362
372
  return;
363
373
  }
374
+ const runStore = ledger.runStore;
364
375
  const summaries = await cs.listByScope(scope);
365
376
  const byGate = new Map(summaries.map((s) => [s.sessionId, s]));
366
- const runs = await deps.runStore.listRuns({ owner: scope, limit: 100 });
377
+ const runs = await runStore.listRuns({ owner: scope, limit: 100 });
367
378
  const tasks = runs
368
379
  .filter((r) => r.status === "running" || isParkedRunStatus(r.status))
369
380
  .map((r) => {
@@ -385,20 +396,22 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
385
396
  if (match.decl.id === "assistant-task-preempt") {
386
397
  if (rateLimited(req, res) || quotaExceeded(req, res))
387
398
  return;
388
- if (!deps.runStore) {
399
+ const runs = facts.durableRuns(deps);
400
+ if (!runs.available) {
389
401
  sendError(res, 501, "capability.run_store_required", "preemption requires a durable run store (DB_BACKEND=mysql|pg|local)");
390
402
  return;
391
403
  }
404
+ const runStore = runs.runStore;
392
405
  if (deps.config.requirePrincipal && principal === undefined) {
393
406
  sendPrincipalRefusal(res, req, deps.config);
394
407
  return;
395
408
  }
396
- if (!deps.config.resourceSuspend || !deps.checkpointStore) {
409
+ if (!deps.config.resourceSuspend) {
397
410
  sendError(res, 501, "feature.preemption_disabled", "preemption not enabled on this worker (needs RESOURCE_SUSPEND=true + a durable checkpoint store)");
398
411
  return;
399
412
  }
400
413
  const taskId = match.params.taskId;
401
- const run = await deps.runStore.getRun(taskId);
414
+ const run = await runStore.getRun(taskId);
402
415
  if (!run) {
403
416
  sendError(res, 404, "not_found.run", "task not found");
404
417
  return;
@@ -415,16 +428,16 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
415
428
  sendJson(res, 202, { taskId, status: run.status, note: "task is not preempt-eligible (verify/cascade leg) — preempt is a no-op" });
416
429
  return;
417
430
  }
418
- let flagged = await deps.runStore.requestPreempt(taskId, run.owner);
431
+ let flagged = await runStore.requestPreempt(taskId, run.owner);
419
432
  const PREEMPT_RECLASSIFY_ATTEMPTS = 3;
420
433
  for (let attempt = 0; !flagged; attempt++) {
421
- const now = await deps.runStore.getRun(taskId);
434
+ const now = await runStore.getRun(taskId);
422
435
  if (now !== undefined && isParkedRunStatus(now.status)) {
423
436
  sendError(res, 409, "conflict.already_suspended", "task is already suspended", { taskId, status: now.status });
424
437
  return;
425
438
  }
426
439
  if (now?.status === "running" && attempt < PREEMPT_RECLASSIFY_ATTEMPTS) {
427
- flagged = await deps.runStore.requestPreempt(taskId, run.owner);
440
+ flagged = await runStore.requestPreempt(taskId, run.owner);
428
441
  continue;
429
442
  }
430
443
  if (now?.status === "running") {
@@ -448,16 +461,18 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
448
461
  if (match.decl.id === "assistant-task-resume") {
449
462
  if (rateLimited(req, res) || quotaExceeded(req, res))
450
463
  return;
451
- if (!deps.runStore) {
464
+ const runs = facts.durableRuns(deps);
465
+ if (!runs.available) {
452
466
  sendError(res, 501, "capability.run_store_required", "resume requires a durable run store (DB_BACKEND=mysql|pg|local)");
453
467
  return;
454
468
  }
469
+ const runStore = runs.runStore;
455
470
  if (deps.config.requirePrincipal && principal === undefined) {
456
471
  sendPrincipalRefusal(res, req, deps.config);
457
472
  return;
458
473
  }
459
474
  const taskId = match.params.taskId;
460
- const run = await deps.runStore.getRun(taskId);
475
+ const run = await runStore.getRun(taskId);
461
476
  if (!run) {
462
477
  sendError(res, 404, "not_found.run", "task not found");
463
478
  return;
@@ -475,10 +490,12 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
475
490
  if (match.decl.id === "assistant-task-plan-review") {
476
491
  if (rateLimited(req, res) || quotaExceeded(req, res))
477
492
  return;
478
- if (!deps.runStore) {
493
+ const runs = facts.durableRuns(deps);
494
+ if (!runs.available) {
479
495
  sendError(res, 501, "capability.run_store_required", "plan_review requires a durable run store (DB_BACKEND=mysql|pg|local)");
480
496
  return;
481
497
  }
498
+ const runStore = runs.runStore;
482
499
  if (deps.config.requirePrincipal && principal === undefined) {
483
500
  sendPrincipalRefusal(res, req, deps.config);
484
501
  return;
@@ -540,7 +557,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
540
557
  }
541
558
  }
542
559
  const planNonce = typeof body.boundCallId === "string" ? body.boundCallId : undefined;
543
- const run = await deps.runStore.getRun(taskId);
560
+ const run = await runStore.getRun(taskId);
544
561
  if (!run) {
545
562
  if (deps.config.directDoorActive) {
546
563
  sendError(res, 401, "principal_unverified", "principal proof required");
@@ -809,7 +826,13 @@ function redactPendingDisclosures(row) {
809
826
  function projectPendingForWire(row, governance) {
810
827
  const redacted = redactPendingDisclosures(row);
811
828
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
812
- return governanceForced ? { ...redacted, governanceForced } : redacted;
829
+ const rememberRefused = !standingAnswerMayRetire({
830
+ governanceForced: governanceForced === true,
831
+ requiresRealApproval: row.requiresRealApproval === true,
832
+ mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
833
+ grantsBeyondCall: answerGrantsBeyondCall(row),
834
+ });
835
+ return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
813
836
  }
814
837
  function createSseDeltaLane(res, spec) {
815
838
  let prev = new Map();
@@ -59,8 +59,6 @@ export interface SessionBackgroundReceipt {
59
59
  id: string;
60
60
  outcome: BackgroundShellHardKillReceipt["outcome"];
61
61
  }
62
- /** 能力位与两口 501 的**同一个**谓词:会话属主面在场(入站 owner 门 fail-closed,缺它两口都 501)。 */
63
- export declare function sessionBackgroundFacesAvailable(deps: Pick<RouteCtx["deps"], "sessionStorage">): boolean;
64
62
  export declare function handleBackground(req: IncomingMessage, res: ServerResponse, match: RouteMatch<BackgroundRouteId>, ctx: RouteCtx): Promise<void>;
65
63
  export declare const BACKGROUND_ROUTES: readonly [{
66
64
  readonly id: "session-background-list";
@@ -6,6 +6,7 @@ import { sendJson, sendError } from "../send.js";
6
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
7
7
  import {} from "../route-ctx.js";
8
8
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
9
+ import * as facts from "../../capability-facts.js";
9
10
  const LIST_LIMIT = 500;
10
11
  export const DESCRIPTION_MAX = 500;
11
12
  const ROW_KEY_DISPOSITION = {
@@ -32,9 +33,6 @@ function projectRow(r) {
32
33
  };
33
34
  }
34
35
  const StopBodySchema = z.object({ includeRetained: z.boolean() }).strict();
35
- export function sessionBackgroundFacesAvailable(deps) {
36
- return Boolean(deps.sessionStorage?.ownerOf);
37
- }
38
36
  export async function handleBackground(req, res, match, ctx) {
39
37
  const { deps } = ctx;
40
38
  const id = match.decl.id;
@@ -59,11 +57,12 @@ export async function handleBackground(req, res, match, ctx) {
59
57
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
60
58
  return;
61
59
  }
62
- if (!sessionBackgroundFacesAvailable(deps)) {
60
+ const owned = facts.sessionOwnership(deps);
61
+ if (!owned.available) {
63
62
  sendError(res, 501, "capability.session_ownership_required", "the session background faces require the session-ownership face (sessionStorage.ownerOf)");
64
63
  return;
65
64
  }
66
- const owner = await deps.sessionStorage.ownerOf(sessionId);
65
+ const owner = await owned.sessionStorage.ownerOf(sessionId);
67
66
  if (owner === undefined) {
68
67
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
69
68
  return;
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * 路由:`GET /v1/capabilities/scenarios/:name`、`GET /v1/capabilities`、`GET /v1/models`。
5
5
  * 三条都在全局 service-credential 门**之后**(config 面非密只读),装配表里的位置必须保住这一点。
6
- * 本域零可变状态;deps 面宽(能力布尔逐条对应各路由的 501 门,单源不许漂移)但全是存在性只读。
6
+ * 本域零可变状态。凡是某个端点也在判的位,值一律是 `src/capability-facts.ts` 里对应事实的投影 ——
7
+ * 端点的不可用门调的是**同一只**函数,所以「位说有而端点恒拒」在结构上不可能(不是靠两边各写一份再对拍)。
7
8
  */
8
9
  import type { IncomingMessage, ServerResponse } from "node:http";
9
10
  import { type McpProbeLocal } from "../../capabilities/mcp-probe.js";
@@ -7,13 +7,11 @@ import { taskWriteFaceRefusal } from "../../hosted-posture.js";
7
7
  import { autonomyOverrides } from "../../runtime-governance.js";
8
8
  import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
9
9
  import { a2aInjectionHonored } from "../../task-a2a.js";
10
- import { mcpInjectionHonored } from "../../task-mcp.js";
11
10
  import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey, buildMcpProbeInflightRefusal, buildMcpProbeRateKey, buildMcpProbeRateRefusal, createMcpProbeLocal, buildMcpProbeDeclarationRefusal, buildMcpProbeIncompleteRefusal, McpProbeIncomplete, MCP_PROBE_HTTP_STATUS, MCP_PROBE_MAX_INFLIGHT, MCP_PROBE_VERDICT_TIMEOUT_MS, createMcpProbeWalk, readMcpProbeBody, } from "../../capabilities/mcp-probe.js";
12
11
  import { mcpForScenario } from "../../config-center/facade.js";
13
12
  import { sendJson, sendError } from "../send.js";
14
13
  import { assertNever } from "../route-ctx.js";
15
14
  import { fileHistoryCaptureOf } from "../../file-history-capture.js";
16
- import { resolveStreamApprovalGate } from "../../tool-approval.js";
17
15
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
18
16
  import { projectWriteProtectionCapability } from "../../write-protection.js";
19
17
  import { projectStoreCapability } from "../../store-posture.js";
@@ -21,12 +19,12 @@ import { projectWebSearchCapability } from "../../plugins/web-search.js";
21
19
  import { projectExecutionLaneCapability } from "../../capabilities/execution-lane.js";
22
20
  import { gateCompositeMembers } from "../../capabilities/composite-members.js";
23
21
  import { projectApprovalsStreamLiveCapability } from "../../capabilities/approvals-push.js";
24
- import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
22
+ import { workflowModelAllowlistFor } from "../../task-workflow.js";
25
23
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
24
+ import * as facts from "../../capability-facts.js";
26
25
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
27
26
  import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
28
27
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
29
- import { sessionBackgroundFacesAvailable } from "./background.js";
30
28
  function routePairingOf(deps, name, m) {
31
29
  if (modelKeyPoisonOf(name, deps.config.modelApiKeyEnv ?? {}, process.env, deps.config.modelApiKeys ?? {}) !== undefined)
32
30
  return "unknown";
@@ -57,8 +55,8 @@ export async function handleCapabilities(req, res, match, ctx) {
57
55
  return;
58
56
  }
59
57
  case "capabilities": {
60
- const workflowsEngineCan = deps.workflowsCapable ?? Boolean(deps.workflowRunStore);
61
- const workflowsDenial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
58
+ const workflowsEngine = facts.selfOrchestrationEngine(deps);
59
+ const workflowsAdmission = facts.selfOrchestrationAdmission(deps);
62
60
  const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
63
61
  const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
64
62
  if (requestedModeRaw !== null && requestedMode === undefined) {
@@ -69,44 +67,44 @@ export async function handleCapabilities(req, res, match, ctx) {
69
67
  const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), localDeny: deps.config.permissionsDisableAutoMode });
70
68
  if (autoModeFault !== undefined)
71
69
  deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
72
- const rulesLaneReachable = Boolean(deps.ruleConsent) && gatedPrincipal(req, deps.config) !== undefined;
70
+ const rulesLaneForCaller = facts.ruleLaneForCaller(deps, gatedPrincipal(req, deps.config)).available;
73
71
  sendJson(res, 200, gateCompositeMembers({
74
72
  ...deps.capabilities,
75
- asyncRuns: Boolean(deps.runStore),
76
- artifacts: Boolean(deps.runStore),
77
- approvals: Boolean(deps.checkpointStore),
78
- sessions: Boolean(deps.sessionAudit),
79
- deviceExecutor: deps.deviceHub
80
- ? { enabled: true, protocolVersion: DEVICE_PROTOCOL_VERSION, maxInflightPerDevice: deps.config.remoteExec?.provider === "device" ? (deps.config.remoteExec.maxInflightPerDevice ?? 4) : 4, wsPath: DEVICE_WS_PATH, management: Boolean(deps.deviceStore && deps.deviceEnrollment) }
73
+ asyncRuns: facts.durableRuns(deps).available,
74
+ artifacts: facts.durableRuns(deps).available,
75
+ approvals: facts.checkpointPark(deps).available,
76
+ sessions: facts.sessionAuditView(deps).available,
77
+ deviceExecutor: facts.deviceLane(deps).available
78
+ ? { enabled: true, protocolVersion: DEVICE_PROTOCOL_VERSION, maxInflightPerDevice: deps.config.remoteExec?.provider === "device" ? (deps.config.remoteExec.maxInflightPerDevice ?? 4) : 4, wsPath: DEVICE_WS_PATH, management: facts.deviceManagement(deps).available }
81
79
  : false,
82
- sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
83
- fleet: deps.fleetBus
84
- ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory", hookFailureNotice: true }
80
+ sessionEvents: facts.sessionEventStream(deps).available,
81
+ fleet: facts.fleetStream(deps).available
82
+ ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "engine", hookFailureNotice: true }
85
83
  : false,
86
- workspace: false,
87
- leader: Boolean(deps.leaderEndpoint),
88
- images: Boolean(deps.imageIndex),
84
+ workspace: facts.workspaceBrowse().available,
85
+ leader: facts.leaderLane(deps).available,
86
+ images: facts.imagePool(deps).available,
89
87
  memory: false,
90
88
  memoryWrite: false,
91
89
  memoryEngine: projectMemoryEngineCapability(deps.memoryPosture),
92
90
  store: projectStoreCapability(deps.storePosture ?? { declared: deps.config.dbBackend, backend: "memory" }, deps.sqlEngineFacts?.()),
93
91
  centerWiring: deps.centerWiring?.(gatedPrincipal(req, deps.config)),
94
92
  webSearch: projectWebSearchCapability(deps.webSearchProvider),
95
- sessionBackgroundable: Boolean(deps.runStore),
96
- runMemoryCaptureOptOut: Boolean(deps.runStore),
97
- mcpReconnect: Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf),
98
- background: { listFace: sessionBackgroundFacesAvailable(deps), exitFaces: sessionBackgroundFacesAvailable(deps) },
99
- sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
100
- adoption: Boolean(deps.backend?.adoptionLog) && deps.config.operatorPrincipals.length > 0,
101
- memoryBundle: Boolean(deps.memoryBundleExport && deps.memoryBundleImport) && deps.config.operatorPrincipals.length > 0,
102
- memoryCompliance: Boolean(deps.memoryCompliance) && deps.config.operatorPrincipals.length > 0,
103
- memoryOrigin: Boolean(deps.memoryOriginFace) && deps.config.operatorPrincipals.length > 0,
104
- memoryConsolidationDriver: Boolean(deps.memoryConsolidation) && deps.config.operatorPrincipals.length > 0,
105
- memoryOptOutGrant: Boolean(deps.memoryOptOutGrant) && deps.config.operatorPrincipals.length > 0,
106
- outcomeLedger: Boolean(deps.outcomeSink?.summary) && (deps.config.requirePrincipal !== true || deps.config.operatorPrincipals.length > 0),
107
- permissionRules: rulesLaneReachable,
108
- permissionRulesWrite: rulesLaneReachable,
109
- permissionRulesRevoke: Boolean(deps.ruleConsent),
93
+ sessionBackgroundable: facts.durableRuns(deps).available,
94
+ runMemoryCaptureOptOut: facts.durableRuns(deps).available,
95
+ mcpReconnect: facts.mcpReconnect(deps).available,
96
+ background: { listFace: facts.sessionOwnership(deps).available, exitFaces: facts.sessionOwnership(deps).available },
97
+ sharedMemory: facts.sharedMemoryRead(deps).available,
98
+ adoption: facts.adoption(deps).available,
99
+ memoryBundle: facts.memoryBundle(deps).available,
100
+ memoryCompliance: facts.memoryCompliance(deps).available,
101
+ memoryOrigin: facts.memoryOrigin(deps).available,
102
+ memoryConsolidationDriver: facts.memoryConsolidation(deps).available,
103
+ memoryOptOutGrant: facts.memoryOptOutGrant(deps).available,
104
+ outcomeLedger: facts.outcomeLedger(deps).available,
105
+ permissionRules: rulesLaneForCaller,
106
+ permissionRulesWrite: rulesLaneForCaller,
107
+ permissionRulesRevoke: facts.ruleLane(deps).available,
110
108
  respondFreeFormRules: true,
111
109
  respondBatchRuleOffers: true,
112
110
  oneShot: true,
@@ -119,9 +117,10 @@ export async function handleCapabilities(req, res, match, ctx) {
119
117
  messageIdentity: true,
120
118
  forwardSubagentEvents: true,
121
119
  scratchpadDir: true,
122
- subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
123
- subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
120
+ subagentSteer: facts.subagentSteer(deps).available,
121
+ subagentResume: facts.subagentSteer(deps).available,
124
122
  taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config) },
123
+ settings: { permissions: { allowGrant: true } },
125
124
  permissionModeAuto: {
126
125
  accepted: true,
127
126
  classifierSeat: deps.autoModeFace?.seatMounted === true,
@@ -132,59 +131,58 @@ export async function handleCapabilities(req, res, match, ctx) {
132
131
  writeProtection: projectWriteProtectionCapability(deps.writeProtection),
133
132
  appendSystemPrompt: true,
134
133
  compactionModel: true,
135
- subagentOutput: Boolean(deps.subagentTaskOutput) && Boolean(deps.runStore),
136
- subagentStream: Boolean(deps.subagentTaskOutput) && Boolean(deps.runStore),
137
- taskHandles: Boolean(deps.taskHandleOutput) && Boolean(deps.taskHandleStop) && Boolean(deps.runStore),
134
+ subagentOutput: facts.subagentOutput(deps).available,
135
+ subagentStream: facts.subagentOutput(deps).available,
136
+ taskHandles: facts.taskHandles(deps).available,
138
137
  taskAgents: deps.config.requirePrincipal !== true,
139
138
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
140
139
  interactiveTools: true,
141
140
  disableSkills: true,
141
+ userInvokedSkills: true,
142
+ skillToolLists: true,
142
143
  projectContext: cwdHonored(deps.config),
143
144
  callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
144
145
  taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyOverrides(deps.config.autonomy).writeFace !== "roots",
145
146
  executionLane: projectExecutionLaneCapability(deps.config),
146
- mcpInjection: mcpInjectionHonored(deps.config),
147
+ mcpInjection: facts.mcpInjection(deps).available,
147
148
  a2a: true,
148
149
  a2aInjection: a2aInjectionHonored(deps.config),
149
- a2aServe: deps.config.a2aServe !== undefined,
150
+ a2aServe: facts.a2aServe(deps).available,
150
151
  modelUsage: Boolean(deps.runStore && deps.modelUsage),
151
152
  sessionInit: true,
152
153
  mcp: true,
153
154
  mcpProbe: true,
154
- mcpElicitation: Boolean(deps.elicitation),
155
- askUserQuestion: Boolean(deps.question),
156
- toolApproval: Boolean(deps.toolApproval),
155
+ mcpElicitation: facts.elicitation(deps).available,
156
+ askUserQuestion: facts.askUserQuestion(deps).available,
157
+ toolApproval: facts.toolApproval(deps).available,
157
158
  approvalsStreamLive: projectApprovalsStreamLiveCapability(deps),
158
- streamApproval: resolveStreamApprovalGate({
159
- toolApprovalEnabled: Boolean(deps.toolApproval),
160
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
161
- backend: deps.backend,
162
- parkFacility: deps.checkpointStore !== undefined,
163
- }).active,
159
+ streamApproval: facts.streamApproval(deps).available,
164
160
  approvalDecisionNote: true,
165
161
  sideQuery: true,
166
- agentRoster: Boolean(deps.backgroundAgentStore),
162
+ agentRoster: facts.agentRoster(deps).available,
167
163
  retention: retentionAdvertisement(deps.config),
168
164
  peerLane: Boolean(deps.peerDirectory),
169
- sessionList: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
170
- sessionSearch: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
171
- sessionFork: Boolean(deps.sessionStorage?.fork),
172
- sessionDelete: Boolean(deps.purgeSession),
165
+ sessionList: facts.sessionList(deps).available,
166
+ sessionSearch: facts.sessionList(deps).available,
167
+ sessionFork: facts.sessionFork(deps).available,
168
+ sessionDelete: facts.sessionDelete(deps).available,
173
169
  resumeAt: Boolean(deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
174
170
  promptSuggestions: true,
175
- sessionPolicy: Boolean(deps.sessionPolicyStore),
171
+ sessionPolicy: facts.sessionPolicy(deps).available,
176
172
  rewindFiles: Boolean(deps.fileHistoryStore),
177
173
  restoreFiles: Boolean(deps.fileHistoryStore),
178
174
  rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
179
175
  fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore, deps.config.fileHistoryCapture),
180
- scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
181
- sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
176
+ scheduler: facts.hostScheduler(deps.config).available,
177
+ sessionSync: facts.sessionSync(deps).available,
178
+ sessionSyncFileHistory: facts.sessionSyncFileHistory(deps).available,
182
179
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
183
180
  s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
184
- sendUserFileLedger: Boolean(deps.sendFileLedger),
185
- workflows: workflowsEngineCan && workflowsDenial === null,
186
- workflowsList: Boolean(deps.workflowRunStore),
187
- workflowsGate: { engineCan: workflowsEngineCan, denial: workflowsDenial },
181
+ sendUserFileLedger: facts.sendFileLedger(deps).available,
182
+ workflows: facts.selfOrchestration(deps).available,
183
+ workflowsList: facts.workflowRuns(deps).available,
184
+ workflowsJournal: facts.workflowsJournal(deps).available,
185
+ workflowsGate: { engineCan: workflowsEngine.available, denial: workflowsAdmission.available ? null : workflowsAdmission.why },
188
186
  workflowModels: (() => {
189
187
  const allow = deps.workflowModelAllowlist
190
188
  ?? workflowModelAllowlistFor({
@@ -197,8 +195,8 @@ export async function handleCapabilities(req, res, match, ctx) {
197
195
  const live = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
198
196
  return allow.filter((n) => Object.hasOwn(live, n));
199
197
  })(),
200
- manualCompact: Boolean(deps.runStore),
201
- configCatalog: deps.config.configCatalogEnabled === true && deps.config.operatorPrincipals.length > 0,
198
+ manualCompact: facts.durableRuns(deps).available,
199
+ configCatalog: facts.configCatalog(deps).available,
202
200
  sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
203
201
  }));
204
202
  return;
@@ -218,7 +216,7 @@ export async function handleCapabilities(req, res, match, ctx) {
218
216
  sendPrincipalRefusal(res, req, deps.config);
219
217
  return;
220
218
  }
221
- if (!mcpInjectionHonored(deps.config)) {
219
+ if (!facts.mcpInjection(deps).available) {
222
220
  sendMcpProbeRefusal(res, ctx, buildMcpInjectionRefusal(), "gate_closed");
223
221
  return;
224
222
  }