@sema-agent/server 7.115.0 → 7.116.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +167 -4
- package/USAGE.md +6 -3
- package/dist/approval-ask-audit-store.d.ts +25 -6
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/approval-card.d.ts +8 -4
- package/dist/approval-card.js +2 -0
- package/dist/approval-crash-convergence.d.ts +60 -0
- package/dist/approval-crash-convergence.js +75 -0
- package/dist/approval-deny-reasons.d.ts +13 -0
- package/dist/approval-deny-reasons.js +2 -0
- package/dist/approval.js +2 -1
- package/dist/boot/coordinators.js +0 -12
- package/dist/boot/execution-env.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/stage-07-capability-layer.js +21 -15
- package/dist/boot/stores.js +4 -4
- package/dist/capabilities/hands-lane.d.ts +0 -23
- package/dist/capabilities/hands-lane.js +3 -2
- package/dist/capabilities/send-user-file-tool.d.ts +10 -22
- package/dist/capabilities/send-user-file-tool.js +1 -8
- package/dist/capability-facts.d.ts +296 -0
- package/dist/capability-facts.js +354 -0
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +16 -13
- package/dist/config.js +12 -12
- package/dist/http/admission.d.ts +6 -0
- package/dist/http/admission.js +22 -5
- package/dist/http/principal-gate.d.ts +3 -0
- package/dist/http/principal-gate.js +3 -0
- package/dist/http/resume-legs.js +10 -1
- package/dist/http/retired-skill-manifest.d.ts +23 -0
- package/dist/http/retired-skill-manifest.js +22 -0
- package/dist/http/route-table.d.ts +3 -2
- package/dist/http/route-table.js +5 -5
- package/dist/http/routes/a2a-serve.js +14 -21
- package/dist/http/routes/admin-config-refresh.js +7 -2
- package/dist/http/routes/admin-drain.js +7 -2
- package/dist/http/routes/admin-memory-optout.js +20 -6
- package/dist/http/routes/adoption.js +19 -5
- package/dist/http/routes/agents-roster.js +4 -2
- package/dist/http/routes/approvals-assistant.js +41 -18
- package/dist/http/routes/background.d.ts +0 -2
- package/dist/http/routes/background.js +4 -5
- package/dist/http/routes/capabilities.d.ts +2 -1
- package/dist/http/routes/capabilities.js +62 -65
- package/dist/http/routes/config-catalog.d.ts +4 -5
- package/dist/http/routes/config-catalog.js +16 -2
- package/dist/http/routes/devices.js +17 -11
- package/dist/http/routes/diagnostics.js +6 -1
- package/dist/http/routes/fleet.js +4 -2
- package/dist/http/routes/images.js +30 -3
- package/dist/http/routes/leader.js +7 -1
- package/dist/http/routes/mcp-reconnect.js +17 -10
- package/dist/http/routes/memory-bundle.js +20 -6
- package/dist/http/routes/memory-compliance.js +19 -5
- package/dist/http/routes/memory-consolidation.d.ts +9 -12
- package/dist/http/routes/memory-consolidation.js +17 -3
- package/dist/http/routes/memory-origin.js +19 -5
- package/dist/http/routes/notify-wake.js +5 -3
- package/dist/http/routes/observability.js +19 -5
- package/dist/http/routes/retention-ops.js +6 -1
- package/dist/http/routes/rules.js +49 -21
- package/dist/http/routes/run-memory-capture.js +5 -2
- package/dist/http/routes/runs.js +90 -60
- package/dist/http/routes/session-sync.js +64 -53
- package/dist/http/routes/sessions-list.js +8 -5
- package/dist/http/routes/sessions.js +39 -24
- package/dist/http/routes/shared-memory.js +5 -3
- package/dist/http/routes/tasks.js +2 -1
- package/dist/http/routes/trace-usage.js +18 -11
- package/dist/http/routes/workflows.js +36 -13
- package/dist/http/server.js +4 -1
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/namespaced-rule-name.d.ts +54 -0
- package/dist/namespaced-rule-name.js +33 -0
- package/dist/observability/corrupt-read-seat.d.ts +4 -3
- package/dist/observability/corrupt-read-seat.js +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/plugins/file-run-store.d.ts +7 -1
- package/dist/plugins/file-run-store.js +20 -7
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/one-time-migrations.d.ts +4 -2
- package/dist/plugins/one-time-migrations.js +26 -3
- package/dist/plugins/pg-session-storage.js +1 -1
- package/dist/plugins/run-store-sql.js +4 -4
- package/dist/plugins/session-policy-store-sql.js +4 -2
- package/dist/plugins/store-backend.d.ts +6 -1
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/store-contracts.d.ts +27 -8
- package/dist/plugins/store-contracts.js +12 -3
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +37 -3
- package/dist/run-local.js +48 -5
- package/dist/runs.d.ts +0 -58
- package/dist/runs.js +15 -20
- package/dist/session-policy-wire.d.ts +40 -60
- package/dist/session-policy-wire.js +60 -32
- package/dist/spec-fields.d.ts +32 -0
- package/dist/spec-fields.js +31 -16
- package/dist/standing-answer-grant.d.ts +8 -0
- package/dist/standing-answer-grant.js +5 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/tool-approval.d.ts +16 -0
- package/dist/tool-approval.js +29 -7
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/self-minted-wire.d.ts +42 -0
- package/dist/trace/self-minted-wire.js +13 -0
- package/dist/user-input-ledger.d.ts +8 -5
- package/package.json +5 -3
- package/scripts/drain-check-local.mjs +115 -25
- package/scripts/drain-check.mysql.sql +67 -0
- 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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
67
|
-
if (!
|
|
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
|
|
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
|
|
299
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
61
|
-
const
|
|
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
|
|
70
|
+
const rulesLaneForCaller = facts.ruleLaneForCaller(deps, gatedPrincipal(req, deps.config)).available;
|
|
73
71
|
sendJson(res, 200, gateCompositeMembers({
|
|
74
72
|
...deps.capabilities,
|
|
75
|
-
asyncRuns:
|
|
76
|
-
artifacts:
|
|
77
|
-
approvals:
|
|
78
|
-
sessions:
|
|
79
|
-
deviceExecutor: deps.
|
|
80
|
-
? { enabled: true, protocolVersion: DEVICE_PROTOCOL_VERSION, maxInflightPerDevice: deps.config.remoteExec?.provider === "device" ? (deps.config.remoteExec.maxInflightPerDevice ?? 4) : 4, wsPath: DEVICE_WS_PATH, management:
|
|
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:
|
|
83
|
-
fleet: deps.
|
|
84
|
-
? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "
|
|
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:
|
|
87
|
-
leader:
|
|
88
|
-
images:
|
|
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:
|
|
96
|
-
runMemoryCaptureOptOut:
|
|
97
|
-
mcpReconnect:
|
|
98
|
-
background: { listFace:
|
|
99
|
-
sharedMemory:
|
|
100
|
-
adoption:
|
|
101
|
-
memoryBundle:
|
|
102
|
-
memoryCompliance:
|
|
103
|
-
memoryOrigin:
|
|
104
|
-
memoryConsolidationDriver:
|
|
105
|
-
memoryOptOutGrant:
|
|
106
|
-
outcomeLedger:
|
|
107
|
-
permissionRules:
|
|
108
|
-
permissionRulesWrite:
|
|
109
|
-
permissionRulesRevoke:
|
|
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:
|
|
123
|
-
subagentResume:
|
|
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:
|
|
137
|
-
subagentStream:
|
|
138
|
-
taskHandles:
|
|
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:
|
|
147
|
+
mcpInjection: facts.mcpInjection(deps).available,
|
|
148
148
|
a2a: true,
|
|
149
149
|
a2aInjection: a2aInjectionHonored(deps.config),
|
|
150
|
-
a2aServe: deps.
|
|
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:
|
|
156
|
-
askUserQuestion:
|
|
157
|
-
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:
|
|
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:
|
|
162
|
+
agentRoster: facts.agentRoster(deps).available,
|
|
168
163
|
retention: retentionAdvertisement(deps.config),
|
|
169
164
|
peerLane: Boolean(deps.peerDirectory),
|
|
170
|
-
sessionList:
|
|
171
|
-
sessionSearch:
|
|
172
|
-
sessionFork:
|
|
173
|
-
sessionDelete:
|
|
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:
|
|
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:
|
|
182
|
-
sessionSync:
|
|
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:
|
|
186
|
-
workflows:
|
|
187
|
-
workflowsList:
|
|
188
|
-
|
|
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:
|
|
202
|
-
configCatalog:
|
|
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 (!
|
|
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
|
-
* 门序 =
|
|
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` ⇒
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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;
|