@sema-agent/server 7.115.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 (116) hide show
  1. package/MIGRATION.md +167 -4
  2. package/USAGE.md +6 -3
  3. package/dist/approval-ask-audit-store.d.ts +25 -6
  4. package/dist/approval-ask-audit-store.js +6 -1
  5. package/dist/approval-card.d.ts +8 -4
  6. package/dist/approval-card.js +2 -0
  7. package/dist/approval-crash-convergence.d.ts +60 -0
  8. package/dist/approval-crash-convergence.js +75 -0
  9. package/dist/approval-deny-reasons.d.ts +13 -0
  10. package/dist/approval-deny-reasons.js +2 -0
  11. package/dist/approval.js +2 -1
  12. package/dist/boot/coordinators.js +0 -12
  13. package/dist/boot/execution-env.js +2 -1
  14. package/dist/boot/resolve-spec.js +1 -0
  15. package/dist/boot/stage-07-capability-layer.js +21 -15
  16. package/dist/boot/stores.js +4 -4
  17. package/dist/capabilities/hands-lane.d.ts +0 -23
  18. package/dist/capabilities/hands-lane.js +3 -2
  19. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  20. package/dist/capabilities/send-user-file-tool.js +1 -8
  21. package/dist/capability-facts.d.ts +296 -0
  22. package/dist/capability-facts.js +354 -0
  23. package/dist/config-center/apply-effective.js +1 -1
  24. package/dist/config.d.ts +16 -13
  25. package/dist/config.js +12 -12
  26. package/dist/http/admission.d.ts +6 -0
  27. package/dist/http/admission.js +22 -5
  28. package/dist/http/principal-gate.d.ts +3 -0
  29. package/dist/http/principal-gate.js +3 -0
  30. package/dist/http/resume-legs.js +10 -1
  31. package/dist/http/retired-skill-manifest.d.ts +23 -0
  32. package/dist/http/retired-skill-manifest.js +22 -0
  33. package/dist/http/route-table.d.ts +3 -2
  34. package/dist/http/route-table.js +5 -5
  35. package/dist/http/routes/a2a-serve.js +14 -21
  36. package/dist/http/routes/admin-config-refresh.js +7 -2
  37. package/dist/http/routes/admin-drain.js +7 -2
  38. package/dist/http/routes/admin-memory-optout.js +20 -6
  39. package/dist/http/routes/adoption.js +19 -5
  40. package/dist/http/routes/agents-roster.js +4 -2
  41. package/dist/http/routes/approvals-assistant.js +41 -18
  42. package/dist/http/routes/background.d.ts +0 -2
  43. package/dist/http/routes/background.js +4 -5
  44. package/dist/http/routes/capabilities.d.ts +2 -1
  45. package/dist/http/routes/capabilities.js +62 -65
  46. package/dist/http/routes/config-catalog.d.ts +4 -5
  47. package/dist/http/routes/config-catalog.js +16 -2
  48. package/dist/http/routes/devices.js +17 -11
  49. package/dist/http/routes/diagnostics.js +6 -1
  50. package/dist/http/routes/fleet.js +4 -2
  51. package/dist/http/routes/images.js +30 -3
  52. package/dist/http/routes/leader.js +7 -1
  53. package/dist/http/routes/mcp-reconnect.js +17 -10
  54. package/dist/http/routes/memory-bundle.js +20 -6
  55. package/dist/http/routes/memory-compliance.js +19 -5
  56. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  57. package/dist/http/routes/memory-consolidation.js +17 -3
  58. package/dist/http/routes/memory-origin.js +19 -5
  59. package/dist/http/routes/notify-wake.js +5 -3
  60. package/dist/http/routes/observability.js +19 -5
  61. package/dist/http/routes/retention-ops.js +6 -1
  62. package/dist/http/routes/rules.js +49 -21
  63. package/dist/http/routes/run-memory-capture.js +5 -2
  64. package/dist/http/routes/runs.js +90 -60
  65. package/dist/http/routes/session-sync.js +64 -53
  66. package/dist/http/routes/sessions-list.js +8 -5
  67. package/dist/http/routes/sessions.js +39 -24
  68. package/dist/http/routes/shared-memory.js +5 -3
  69. package/dist/http/routes/tasks.js +2 -1
  70. package/dist/http/routes/trace-usage.js +18 -11
  71. package/dist/http/routes/workflows.js +36 -13
  72. package/dist/http/server.js +4 -1
  73. package/dist/http/wire-types.d.ts +6 -0
  74. package/dist/namespaced-rule-name.d.ts +54 -0
  75. package/dist/namespaced-rule-name.js +33 -0
  76. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  77. package/dist/observability/corrupt-read-seat.js +4 -3
  78. package/dist/observability/fail-open.d.ts +5 -1
  79. package/dist/observability/fail-open.js +5 -1
  80. package/dist/observability/permission-rule-events.d.ts +3 -0
  81. package/dist/observability/permission-rule-events.js +4 -0
  82. package/dist/plugins/file-run-store.d.ts +7 -1
  83. package/dist/plugins/file-run-store.js +20 -7
  84. package/dist/plugins/memory-run-store.js +2 -1
  85. package/dist/plugins/one-time-migrations.d.ts +4 -2
  86. package/dist/plugins/one-time-migrations.js +26 -3
  87. package/dist/plugins/pg-session-storage.js +1 -1
  88. package/dist/plugins/run-store-sql.js +4 -4
  89. package/dist/plugins/session-policy-store-sql.js +4 -2
  90. package/dist/plugins/store-backend.d.ts +6 -1
  91. package/dist/plugins/store-backend.js +2 -1
  92. package/dist/plugins/store-contracts.d.ts +27 -8
  93. package/dist/plugins/store-contracts.js +12 -3
  94. package/dist/plugins/tidb-session-store.js +1 -1
  95. package/dist/request-key-closure.js +1 -1
  96. package/dist/run-local.d.ts +37 -3
  97. package/dist/run-local.js +48 -5
  98. package/dist/runs.d.ts +0 -58
  99. package/dist/runs.js +15 -20
  100. package/dist/session-policy-wire.d.ts +40 -60
  101. package/dist/session-policy-wire.js +60 -32
  102. package/dist/spec-fields.d.ts +32 -0
  103. package/dist/spec-fields.js +31 -16
  104. package/dist/standing-answer-grant.d.ts +8 -0
  105. package/dist/standing-answer-grant.js +5 -0
  106. package/dist/task-settings.d.ts +13 -3
  107. package/dist/tool-approval.d.ts +16 -0
  108. package/dist/tool-approval.js +29 -7
  109. package/dist/trace/core-keyset-guard.d.ts +4 -4
  110. package/dist/trace/self-minted-wire.d.ts +42 -0
  111. package/dist/trace/self-minted-wire.js +13 -0
  112. package/dist/user-input-ledger.d.ts +8 -5
  113. package/package.json +4 -2
  114. package/scripts/drain-check-local.mjs +115 -25
  115. package/scripts/drain-check.mysql.sql +67 -0
  116. package/scripts/drain-check.pg.sql +71 -0
@@ -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,8 +117,8 @@ 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) },
125
123
  settings: { permissions: { allowGrant: true } },
126
124
  permissionModeAuto: {
@@ -133,59 +131,58 @@ export async function handleCapabilities(req, res, match, ctx) {
133
131
  writeProtection: projectWriteProtectionCapability(deps.writeProtection),
134
132
  appendSystemPrompt: true,
135
133
  compactionModel: true,
136
- subagentOutput: Boolean(deps.subagentTaskOutput) && Boolean(deps.runStore),
137
- subagentStream: Boolean(deps.subagentTaskOutput) && Boolean(deps.runStore),
138
- 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,
139
137
  taskAgents: deps.config.requirePrincipal !== true,
140
138
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
141
139
  interactiveTools: true,
142
140
  disableSkills: true,
141
+ userInvokedSkills: true,
142
+ skillToolLists: true,
143
143
  projectContext: cwdHonored(deps.config),
144
144
  callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
145
145
  taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyOverrides(deps.config.autonomy).writeFace !== "roots",
146
146
  executionLane: projectExecutionLaneCapability(deps.config),
147
- mcpInjection: mcpInjectionHonored(deps.config),
147
+ mcpInjection: facts.mcpInjection(deps).available,
148
148
  a2a: true,
149
149
  a2aInjection: a2aInjectionHonored(deps.config),
150
- a2aServe: deps.config.a2aServe !== undefined,
150
+ a2aServe: facts.a2aServe(deps).available,
151
151
  modelUsage: Boolean(deps.runStore && deps.modelUsage),
152
152
  sessionInit: true,
153
153
  mcp: true,
154
154
  mcpProbe: true,
155
- mcpElicitation: Boolean(deps.elicitation),
156
- askUserQuestion: Boolean(deps.question),
157
- toolApproval: Boolean(deps.toolApproval),
155
+ mcpElicitation: facts.elicitation(deps).available,
156
+ askUserQuestion: facts.askUserQuestion(deps).available,
157
+ toolApproval: facts.toolApproval(deps).available,
158
158
  approvalsStreamLive: projectApprovalsStreamLiveCapability(deps),
159
- streamApproval: resolveStreamApprovalGate({
160
- toolApprovalEnabled: Boolean(deps.toolApproval),
161
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
162
- backend: deps.backend,
163
- parkFacility: deps.checkpointStore !== undefined,
164
- }).active,
159
+ streamApproval: facts.streamApproval(deps).available,
165
160
  approvalDecisionNote: true,
166
161
  sideQuery: true,
167
- agentRoster: Boolean(deps.backgroundAgentStore),
162
+ agentRoster: facts.agentRoster(deps).available,
168
163
  retention: retentionAdvertisement(deps.config),
169
164
  peerLane: Boolean(deps.peerDirectory),
170
- sessionList: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
171
- sessionSearch: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
172
- sessionFork: Boolean(deps.sessionStorage?.fork),
173
- 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,
174
169
  resumeAt: Boolean(deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
175
170
  promptSuggestions: true,
176
- sessionPolicy: Boolean(deps.sessionPolicyStore),
171
+ sessionPolicy: facts.sessionPolicy(deps).available,
177
172
  rewindFiles: Boolean(deps.fileHistoryStore),
178
173
  restoreFiles: Boolean(deps.fileHistoryStore),
179
174
  rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
180
175
  fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore, deps.config.fileHistoryCapture),
181
- scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
182
- 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,
183
179
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
184
180
  s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
185
- sendUserFileLedger: Boolean(deps.sendFileLedger),
186
- workflows: workflowsEngineCan && workflowsDenial === null,
187
- workflowsList: Boolean(deps.workflowRunStore),
188
- 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 },
189
186
  workflowModels: (() => {
190
187
  const allow = deps.workflowModelAllowlist
191
188
  ?? workflowModelAllowlistFor({
@@ -198,8 +195,8 @@ export async function handleCapabilities(req, res, match, ctx) {
198
195
  const live = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
199
196
  return allow.filter((n) => Object.hasOwn(live, n));
200
197
  })(),
201
- manualCompact: Boolean(deps.runStore),
202
- configCatalog: deps.config.configCatalogEnabled === true && deps.config.operatorPrincipals.length > 0,
198
+ manualCompact: facts.durableRuns(deps).available,
199
+ configCatalog: facts.configCatalog(deps).available,
203
200
  sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
204
201
  }));
205
202
  return;
@@ -219,7 +216,7 @@ export async function handleCapabilities(req, res, match, ctx) {
219
216
  sendPrincipalRefusal(res, req, deps.config);
220
217
  return;
221
218
  }
222
- if (!mcpInjectionHonored(deps.config)) {
219
+ if (!facts.mcpInjection(deps).available) {
223
220
  sendMcpProbeRefusal(res, ctx, buildMcpInjectionRefusal(), "gate_closed");
224
221
  return;
225
222
  }
@@ -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;