@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sendError, sendJson } from "../send.js";
|
|
2
2
|
import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
3
|
import { assertNever } from "../route-ctx.js";
|
|
4
|
+
import * as facts from "../../capability-facts.js";
|
|
4
5
|
import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, sameDeviceOwner, } from "../../device-store.js";
|
|
5
6
|
import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
|
|
6
7
|
import { deviceOwnerDomainOf } from "../device-placement.js";
|
|
@@ -133,13 +134,22 @@ export async function handleDevices(req, res, match, ctx) {
|
|
|
133
134
|
sendPrincipalRefusal(res, req, deps.config);
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
const lane = facts.deviceManagement(deps);
|
|
138
|
+
if (!lane.available) {
|
|
139
|
+
switch (lane.why) {
|
|
140
|
+
case "device_hub_absent":
|
|
141
|
+
case "device_store_absent":
|
|
142
|
+
case "device_enrollment_absent":
|
|
143
|
+
sendError(res, 501, "capability.device_lane_required", DEVICE_LANE_REQUIRED_MESSAGE);
|
|
144
|
+
return;
|
|
145
|
+
case "run_store_absent":
|
|
146
|
+
sendError(res, 501, "capability.run_store_required", "device rebind needs the run store to judge the no-active-run door (a durable, cross-replica criterion) — this deployment has no run store wired");
|
|
147
|
+
return;
|
|
148
|
+
default:
|
|
149
|
+
return assertNever(lane.why);
|
|
150
|
+
}
|
|
142
151
|
}
|
|
152
|
+
const { store, hub, enrollment, runStore } = lane;
|
|
143
153
|
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
144
154
|
const domain = deviceOwnerDomainOf(req, deps.config);
|
|
145
155
|
if (match.decl.id === "devices-list") {
|
|
@@ -293,11 +303,7 @@ export async function handleDevices(req, res, match, ctx) {
|
|
|
293
303
|
sendDeviceNotFound(res);
|
|
294
304
|
return;
|
|
295
305
|
}
|
|
296
|
-
|
|
297
|
-
sendError(res, 501, "capability.run_store_required", "device rebind needs the run store to judge the no-active-run door (a durable, cross-replica criterion) — this deployment has no run store wired");
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const activeTaskId = await deps.runStore.getActiveTaskId(rootSessionId);
|
|
306
|
+
const activeTaskId = await runStore.getActiveTaskId(rootSessionId);
|
|
301
307
|
if (activeTaskId) {
|
|
302
308
|
sendError(res, 409, "device.rebind_active_run", "this session has a non-terminal run — rebinding now would leave in-flight work addressed at another workspace; cancel the run (or let it finish), then retry", { activeTaskId });
|
|
303
309
|
return;
|
|
@@ -4,7 +4,8 @@ import { projectExecutionLaneCapability } from "../../capabilities/execution-lan
|
|
|
4
4
|
import { readOutboundWiring } from "../../outbound-dispatcher.js";
|
|
5
5
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
6
6
|
import { sendJson, sendError } from "../send.js";
|
|
7
|
-
import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
|
|
7
|
+
import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, sendOperatorPrincipalsRequired, verifiedIdentitySource } from "../principal-gate.js";
|
|
8
|
+
import * as facts from "../../capability-facts.js";
|
|
8
9
|
import { assertNever } from "../route-ctx.js";
|
|
9
10
|
export function buildServerWiringGates(input) {
|
|
10
11
|
const gate = resolveStreamApprovalGate({
|
|
@@ -87,6 +88,10 @@ export async function handleDiagnostics(req, res, match, ctx) {
|
|
|
87
88
|
switch (match.decl.id) {
|
|
88
89
|
case "diagnostics-wiring": {
|
|
89
90
|
if (deps.config.operatorPrincipals.length > 0 || deps.config.requirePrincipal) {
|
|
91
|
+
if (!facts.operatorLane(deps).available) {
|
|
92
|
+
sendOperatorPrincipalsRequired(res);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
90
95
|
const principal = gatedPrincipal(req, deps.config);
|
|
91
96
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
92
97
|
sendError(res, 403, "auth.operator_only", "wiring diagnostics are operator-only on a multi-tenant worker");
|
|
@@ -4,12 +4,14 @@ import { taskNotificationFoldKey } from "../../orchestration/workflow-completion
|
|
|
4
4
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
5
5
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
6
|
import { assertNever } from "../route-ctx.js";
|
|
7
|
+
import * as facts from "../../capability-facts.js";
|
|
7
8
|
export async function handleFleet(req, res, match, ctx) {
|
|
8
9
|
const { deps } = ctx;
|
|
9
10
|
const { sessionOwnerScope } = ctx.helpers;
|
|
10
11
|
switch (match.decl.id) {
|
|
11
12
|
case "fleet-stream": {
|
|
12
|
-
|
|
13
|
+
const fleet = facts.fleetStream(deps);
|
|
14
|
+
if (!fleet.available) {
|
|
13
15
|
sendError(res, 501, "capability.self_orchestration_required", "fleet stream requires self-orchestration / fleet wiring");
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
@@ -22,7 +24,7 @@ export async function handleFleet(req, res, match, ctx) {
|
|
|
22
24
|
const fleetParams = new URLSearchParams(match.search);
|
|
23
25
|
const callerSession = fleetParams.get("session");
|
|
24
26
|
const observeOnly = fleetParams.get("observe") === "1";
|
|
25
|
-
await streamFleet(req, res,
|
|
27
|
+
await streamFleet(req, res, fleet.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
28
30
|
default:
|
|
@@ -5,9 +5,10 @@ import { manifestToIndexEntry } from "../../images/manifest.js";
|
|
|
5
5
|
import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
|
|
6
6
|
import { streamSseLog } from "../sse-log.js";
|
|
7
7
|
import { sendJson, sendError } from "../send.js";
|
|
8
|
-
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
8
|
+
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired } from "../principal-gate.js";
|
|
9
9
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
10
10
|
import { assertNever } from "../route-ctx.js";
|
|
11
|
+
import * as facts from "../../capability-facts.js";
|
|
11
12
|
export function createImagesLocal(deps) {
|
|
12
13
|
return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
|
|
13
14
|
}
|
|
@@ -50,9 +51,14 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
50
51
|
const principal = gatedPrincipal(req, deps.config);
|
|
51
52
|
const operators = deps.config.operatorPrincipals;
|
|
52
53
|
const isExplicitOperator = explicitOperatorOk(principal, operators);
|
|
54
|
+
const operatorLane = facts.operatorLane(deps);
|
|
53
55
|
const cfg = deps.config.imageBakes;
|
|
54
56
|
const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
|
|
55
57
|
if (match.decl.id === "image-bake-submit") {
|
|
58
|
+
if (!operatorLane.available) {
|
|
59
|
+
sendOperatorPrincipalsRequired(res);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
56
62
|
if (!isExplicitOperator) {
|
|
57
63
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
58
64
|
return;
|
|
@@ -124,6 +130,10 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
124
130
|
return;
|
|
125
131
|
}
|
|
126
132
|
if (match.decl.id === "image-bake-events") {
|
|
133
|
+
if (!operatorLane.available) {
|
|
134
|
+
sendOperatorPrincipalsRequired(res);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
127
137
|
if (!isExplicitOperator) {
|
|
128
138
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
129
139
|
return;
|
|
@@ -142,6 +152,10 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
142
152
|
return;
|
|
143
153
|
}
|
|
144
154
|
if (match.decl.id === "image-bake-cancel") {
|
|
155
|
+
if (!operatorLane.available) {
|
|
156
|
+
sendOperatorPrincipalsRequired(res);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
145
159
|
if (!isExplicitOperator) {
|
|
146
160
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
147
161
|
return;
|
|
@@ -250,6 +264,10 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
250
264
|
return;
|
|
251
265
|
}
|
|
252
266
|
if (match.decl.id === "image-bake-detail") {
|
|
267
|
+
if (!operatorLane.available) {
|
|
268
|
+
sendOperatorPrincipalsRequired(res);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
253
271
|
if (!isExplicitOperator) {
|
|
254
272
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
255
273
|
return;
|
|
@@ -269,8 +287,9 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
269
287
|
}
|
|
270
288
|
return assertNever(match.decl.id);
|
|
271
289
|
}
|
|
272
|
-
|
|
273
|
-
|
|
290
|
+
const pool = facts.imagePool(deps);
|
|
291
|
+
if (pool.available && isIndexRouteId(match.decl.id)) {
|
|
292
|
+
const idx = pool.imageIndex;
|
|
274
293
|
const principal = gatedPrincipal(req, deps.config);
|
|
275
294
|
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
276
295
|
const viewer = { operator, tenantId: principal ?? null };
|
|
@@ -322,6 +341,10 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
322
341
|
return;
|
|
323
342
|
}
|
|
324
343
|
if (match.decl.id === "image-register") {
|
|
344
|
+
if (!facts.operatorLane(deps).available) {
|
|
345
|
+
sendOperatorPrincipalsRequired(res);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
325
348
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
326
349
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
327
350
|
return;
|
|
@@ -397,6 +420,10 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
397
420
|
return assertNever(match.decl.id);
|
|
398
421
|
}
|
|
399
422
|
{
|
|
423
|
+
if (!pool.available) {
|
|
424
|
+
sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
400
427
|
sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
|
|
401
428
|
return;
|
|
402
429
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
2
|
import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
3
|
import { assertNever } from "../route-ctx.js";
|
|
4
|
+
import * as facts from "../../capability-facts.js";
|
|
4
5
|
export const LEADER_SUBMIT_PATH = "/v1/leader";
|
|
5
6
|
export async function handleLeader(req, res, match, ctx) {
|
|
6
7
|
switch (match.decl.id) {
|
|
@@ -12,7 +13,12 @@ export async function handleLeader(req, res, match, ctx) {
|
|
|
12
13
|
}
|
|
13
14
|
const { deps } = ctx;
|
|
14
15
|
const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
15
|
-
const
|
|
16
|
+
const lane = facts.leaderLane(deps);
|
|
17
|
+
if (!lane.available) {
|
|
18
|
+
sendError(res, 501, "capability.leader_endpoint_required", "this orchestration endpoint is not available on this deployment: nothing is wired to serve it here");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const endpoint = lane.endpoint;
|
|
16
22
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
17
23
|
sendPrincipalRefusal(res, req, deps.config);
|
|
18
24
|
return;
|
|
@@ -5,6 +5,7 @@ import { sendJson, sendError } from "../send.js";
|
|
|
5
5
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
6
|
import {} from "../route-ctx.js";
|
|
7
7
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
8
|
+
import * as facts from "../../capability-facts.js";
|
|
8
9
|
const BodySchema = z.object({ server: z.string().min(1).max(190) }).strict();
|
|
9
10
|
const NEUTRALIZED_TEXT_MAX = 200;
|
|
10
11
|
const humanText = (s) => redactSecrets(s).slice(0, NEUTRALIZED_TEXT_MAX);
|
|
@@ -56,10 +57,20 @@ export async function handleMcpReconnect(req, res, match, ctx) {
|
|
|
56
57
|
case "mcp-reconnect": {
|
|
57
58
|
if (ctx.helpers.rateLimited(req, res))
|
|
58
59
|
return;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const fact = facts.mcpReconnect(deps);
|
|
61
|
+
if (!fact.available) {
|
|
62
|
+
switch (fact.why) {
|
|
63
|
+
case "run_store_absent":
|
|
64
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
65
|
+
return;
|
|
66
|
+
case "session_owner_face_absent":
|
|
67
|
+
sendError(res, 501, "capability.session_ownership_required", "the MCP re-dial verb requires the session-ownership face (sessionStorage.ownerOf)");
|
|
68
|
+
return;
|
|
69
|
+
default:
|
|
70
|
+
return assertNever(fact.why);
|
|
71
|
+
}
|
|
62
72
|
}
|
|
73
|
+
const { runStore, sessionStorage } = fact;
|
|
63
74
|
const principal = gatedPrincipal(req, deps.config);
|
|
64
75
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
65
76
|
sendPrincipalRefusal(res, req, deps.config);
|
|
@@ -76,11 +87,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
|
|
|
76
87
|
sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
|
|
77
88
|
return;
|
|
78
89
|
}
|
|
79
|
-
|
|
80
|
-
sendError(res, 501, "capability.session_ownership_required", "the MCP re-dial verb requires the session-ownership face (sessionStorage.ownerOf)");
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const owner = await deps.sessionStorage.ownerOf(sessionId);
|
|
90
|
+
const owner = await sessionStorage.ownerOf(sessionId);
|
|
84
91
|
if (owner === undefined) {
|
|
85
92
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
86
93
|
return;
|
|
@@ -88,7 +95,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
|
|
|
88
95
|
const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
89
96
|
if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
|
|
90
97
|
return;
|
|
91
|
-
const activeTaskId = await
|
|
98
|
+
const activeTaskId = await runStore.getActiveTaskId(sessionId);
|
|
92
99
|
const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
|
|
93
100
|
if (live) {
|
|
94
101
|
try {
|
|
@@ -113,7 +120,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
|
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
|
-
const run = activeTaskId === undefined ? undefined : await
|
|
123
|
+
const run = activeTaskId === undefined ? undefined : await runStore.getRun(activeTaskId);
|
|
117
124
|
if (run && isParkedRunStatus(run.status)) {
|
|
118
125
|
sendError(res, 409, "steering.not_running", "run is waiting on a pending decision — the MCP re-dial verb needs a live turn; a resumed continuation dials every declared server afresh (the re-dial applies to the current turn only and is not persisted), so resume this run first — a re-dial cannot be queued behind the decision");
|
|
119
126
|
return;
|
|
@@ -2,8 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
3
|
import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
|
|
4
4
|
import { buildOriginFencedBundle, readOriginAware } from "../../memory-origin-fence.js";
|
|
5
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
5
|
+
import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
6
|
import { assertNever } from "../route-ctx.js";
|
|
7
|
+
import * as facts from "../../capability-facts.js";
|
|
7
8
|
export const MEMORY_BUNDLE_EXPORT_PATH = "/v1/memory/export";
|
|
8
9
|
export const MEMORY_BUNDLE_IMPORT_PATH = "/v1/memory/import";
|
|
9
10
|
const ExportRequestSchema = z.object({ scopes: z.array(z.string().min(1)).min(1) });
|
|
@@ -67,16 +68,29 @@ export async function handleMemoryBundle(req, res, match, ctx) {
|
|
|
67
68
|
sendPrincipalRefusal(res, req, deps.config);
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
71
|
+
const fact = facts.memoryBundle(deps);
|
|
72
|
+
if (!fact.available && fact.why === "operator_principals_empty") {
|
|
73
|
+
sendOperatorPrincipalsRequired(res);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
70
76
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
71
77
|
sendError(res, 403, "auth.operator_only", "memory governance bundles are an operator-only deployment action");
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
if (!fact.available) {
|
|
81
|
+
switch (fact.why) {
|
|
82
|
+
case "memory_bundle_faces_absent":
|
|
83
|
+
sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/tidb memory backends do not");
|
|
84
|
+
return;
|
|
85
|
+
case "operator_principals_empty":
|
|
86
|
+
sendOperatorPrincipalsRequired(res);
|
|
87
|
+
return;
|
|
88
|
+
default:
|
|
89
|
+
return assertNever(fact.why);
|
|
90
|
+
}
|
|
79
91
|
}
|
|
92
|
+
const engineExport = fact.exportScopes;
|
|
93
|
+
const engineImport = fact.importBundle;
|
|
80
94
|
const raw = await ctx.helpers.readJson(req);
|
|
81
95
|
switch (match.decl.id) {
|
|
82
96
|
case "memory-bundle-export": {
|
|
@@ -2,8 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
|
|
3
3
|
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
4
4
|
import { sendJson, sendError } from "../send.js";
|
|
5
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
5
|
+
import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
6
|
import { assertNever } from "../route-ctx.js";
|
|
7
|
+
import * as facts from "../../capability-facts.js";
|
|
7
8
|
export const MEMORY_ERASE_PATH = "/v1/memory/erase";
|
|
8
9
|
const EraseRequestSchema = z.object({
|
|
9
10
|
requestId: z.string().min(1),
|
|
@@ -23,15 +24,28 @@ export async function handleMemoryCompliance(req, res, match, ctx) {
|
|
|
23
24
|
sendPrincipalRefusal(res, req, deps.config);
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
27
|
+
const fact = facts.memoryCompliance(deps);
|
|
28
|
+
if (!fact.available && fact.why === "operator_principals_empty") {
|
|
29
|
+
sendOperatorPrincipalsRequired(res);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
26
32
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
27
33
|
sendError(res, 403, "auth.operator_only", "memory provenance and erasure are operator-only deployment actions");
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
if (!fact.available) {
|
|
37
|
+
switch (fact.why) {
|
|
38
|
+
case "memory_compliance_faces_absent":
|
|
39
|
+
sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
|
|
40
|
+
return;
|
|
41
|
+
case "operator_principals_empty":
|
|
42
|
+
sendOperatorPrincipalsRequired(res);
|
|
43
|
+
return;
|
|
44
|
+
default:
|
|
45
|
+
return assertNever(fact.why);
|
|
46
|
+
}
|
|
34
47
|
}
|
|
48
|
+
const faces = fact.faces;
|
|
35
49
|
switch (match.decl.id) {
|
|
36
50
|
case "memory-entry-provenance": {
|
|
37
51
|
const entryId = ctx.helpers.safeDecode(match.params.entryId);
|
|
@@ -6,20 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ── 族属与门序 ────────────────────────────────────────────────────────────────────────────────────
|
|
8
8
|
* 与 `/v1/admin/drain` · `/v1/admin/config/refresh` 逐字同族(operator lane:同一条 `explicitOperatorOk`
|
|
9
|
-
* 门)。门序照 operator
|
|
9
|
+
* 门)。门序照 operator 面口径:身份(401)→ 能力(501,事实)→ 授权(403)→ 验型(400)。**授权在验型之前** —— 一个
|
|
10
10
|
* 够不着任何东西的调用方不该从「你的 body 形不对」上读出这台部署有没有这条面。
|
|
11
11
|
*
|
|
12
|
-
* ──
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* 域头合取式里,能力位 `memoryConsolidationDriver` 读同一个事实,「说 yes ⟺ 打得通」因此是结构性的。
|
|
21
|
-
* 🔴 刻意与 permissionRules 的 501 分家:那一族的 501 是「规则店没装」,消费端的动作是换部署;本族的
|
|
22
|
-
* 404 的动作是「去把旋钮打开」。同码会让运维照着一条修不好的路走。
|
|
12
|
+
* ── 缺席形(D4):**501**,与能力位同一只事实 ─────────────────────────────────────────────────────
|
|
13
|
+
* 两口恒挂载;域头调事实 `memoryConsolidation`(`src/capability-facts.ts`,`capabilities.memoryConsolidationDriver` 读的是同一只):
|
|
14
|
+
* · 阀门关(`MEMORY_CONSOLIDATION_DRIVER=off`,或 S-824 出厂缺省派生成关 —— 未设而整理座落主模型)⇒ 501
|
|
15
|
+
* `feature.memory_consolidation_disabled`。旋钮关是一句运维表态,不是部署能力缺失 —— 「去开旋钮」由 `feature.*` 这一族码
|
|
16
|
+
* 与拒句承担(同仓旋钮关的既有答法),不靠状态码:HTTP 上「这台部署上不可用」只有 501 一种答。
|
|
17
|
+
* · operator 名单空 ⇒ 501 `capability.operator_principals_required`(对谁都打不通 = 部署不可用,先于授权答)。
|
|
18
|
+
* 阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面归属 / provenance=off)在**启动期**就被拒了
|
|
19
|
+
* (`boot/memory-consolidation.ts` 的 `assertConsolidationDriverWirable`),那些机器根本起不来。
|
|
23
20
|
*
|
|
24
21
|
* ── scope **恒不猜**(D2)────────────────────────────────────────────────────────────────────────
|
|
25
22
|
* 一轮 consolidation 读**全库**(~1e5 prompt tokens/轮)。在两个库之间替 operator 挑一个去花这笔钱,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ControlPlaneCorruptError } from "@sema-agent/core";
|
|
3
3
|
import { sendJson, sendError } from "../send.js";
|
|
4
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
4
|
+
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sendOperatorPrincipalsRequired } from "../principal-gate.js";
|
|
5
5
|
import { assertNever } from "../route-ctx.js";
|
|
6
|
+
import * as facts from "../../capability-facts.js";
|
|
6
7
|
export const MEMORY_CONSOLIDATION_STATUS_PATH = "/v1/admin/memory/consolidation";
|
|
7
8
|
export const MEMORY_CONSOLIDATION_RUN_PATH = "/v1/admin/memory/consolidation/run";
|
|
8
9
|
const RunRequestSchema = z.object({ scope: z.string().optional() }).strict();
|
|
@@ -72,14 +73,27 @@ function codeOf(err) {
|
|
|
72
73
|
}
|
|
73
74
|
export async function handleMemoryConsolidation(req, res, match, ctx) {
|
|
74
75
|
const { deps } = ctx;
|
|
75
|
-
const faces = deps.memoryConsolidation;
|
|
76
76
|
const principal = gatedPrincipal(req, deps.config);
|
|
77
77
|
if (deps.config.requirePrincipal && !principal) {
|
|
78
78
|
sendPrincipalRefusal(res, req, deps.config);
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
+
const fact = facts.memoryConsolidation(deps);
|
|
82
|
+
if (!fact.available) {
|
|
83
|
+
switch (fact.why) {
|
|
84
|
+
case "consolidation_valve_closed":
|
|
85
|
+
sendError(res, 501, "feature.memory_consolidation_disabled", "memory consolidation is switched off on this deployment (MEMORY_CONSOLIDATION_DRIVER is off, or its default derived off because no cheap model is configured); turn it on to use these endpoints");
|
|
86
|
+
return;
|
|
87
|
+
case "operator_principals_empty":
|
|
88
|
+
sendOperatorPrincipalsRequired(res);
|
|
89
|
+
return;
|
|
90
|
+
default:
|
|
91
|
+
return assertNever(fact.why);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const faces = fact.valve;
|
|
81
95
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
82
|
-
sendError(res, 403, "auth.operator_only", "memory consolidation is operator-only: the caller principal is not in OPERATOR_PRINCIPALS
|
|
96
|
+
sendError(res, 403, "auth.operator_only", "memory consolidation is operator-only: the caller principal is not in OPERATOR_PRINCIPALS");
|
|
83
97
|
return;
|
|
84
98
|
}
|
|
85
99
|
switch (match.decl.id) {
|
|
@@ -3,8 +3,9 @@ import { ControlPlaneCorruptError } from "@sema-agent/core";
|
|
|
3
3
|
import { sendJson, sendError } from "../send.js";
|
|
4
4
|
import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
|
|
5
5
|
import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
|
|
6
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
|
+
import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
7
7
|
import { assertNever } from "../route-ctx.js";
|
|
8
|
+
import * as facts from "../../capability-facts.js";
|
|
8
9
|
export const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external";
|
|
9
10
|
export const MEMORY_ORIGIN_CLEARANCES_PATH = "/v1/memory/origin/clearances";
|
|
10
11
|
const ClearRequestSchema = z.object({
|
|
@@ -116,15 +117,28 @@ export async function handleMemoryOrigin(req, res, match, ctx) {
|
|
|
116
117
|
sendPrincipalRefusal(res, req, deps.config);
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
120
|
+
const fact = facts.memoryOrigin(deps);
|
|
121
|
+
if (!fact.available && fact.why === "operator_principals_empty") {
|
|
122
|
+
sendOperatorPrincipalsRequired(res);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
119
125
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
120
126
|
sendError(res, 403, "auth.operator_only", "memory origin marking and clearance are operator-only deployment actions");
|
|
121
127
|
return;
|
|
122
128
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
if (!fact.available) {
|
|
130
|
+
switch (fact.why) {
|
|
131
|
+
case "memory_origin_faces_absent":
|
|
132
|
+
sendError(res, 501, "capability.memory_engine_required", "memory origin marking and clearance are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the clearance ledger and its write-ahead custody rows live in that plane, and on a backend without it they would land on one replica's local disk");
|
|
133
|
+
return;
|
|
134
|
+
case "operator_principals_empty":
|
|
135
|
+
sendOperatorPrincipalsRequired(res);
|
|
136
|
+
return;
|
|
137
|
+
default:
|
|
138
|
+
return assertNever(fact.why);
|
|
139
|
+
}
|
|
127
140
|
}
|
|
141
|
+
const faces = fact.faces;
|
|
128
142
|
switch (match.decl.id) {
|
|
129
143
|
case "memory-origin-external": {
|
|
130
144
|
const scopes = parseScopes(new URLSearchParams(match.search).getAll("scopes"));
|
|
@@ -3,6 +3,7 @@ import { sendJson, sendError } from "../send.js";
|
|
|
3
3
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
4
4
|
import { sendResumeOutcome } from "../route-ctx.js";
|
|
5
5
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
6
|
+
import * as facts from "../../capability-facts.js";
|
|
6
7
|
export async function handleNotifyWake(req, res, match, ctx) {
|
|
7
8
|
const { deps } = ctx;
|
|
8
9
|
const { steerableRuns } = ctx.registry;
|
|
@@ -57,11 +58,12 @@ export async function handleNotifyWake(req, res, match, ctx) {
|
|
|
57
58
|
sendError(res, 400, "request.field_invalid", "source must be a string of at most 190 characters when present");
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
const owned = facts.sessionOwnership(deps);
|
|
62
|
+
if (!owned.available) {
|
|
61
63
|
sendError(res, 501, "capability.session_ownership_required", "external notify requires the session-ownership face (sessionStorage.ownerOf)");
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
|
-
const notifyOwner = await
|
|
66
|
+
const notifyOwner = await owned.sessionStorage.ownerOf(notifySession);
|
|
65
67
|
if (notifyOwner === undefined) {
|
|
66
68
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
67
69
|
return;
|
|
@@ -104,7 +106,7 @@ export async function handleNotifyWake(req, res, match, ctx) {
|
|
|
104
106
|
return;
|
|
105
107
|
}
|
|
106
108
|
case "session-wake": {
|
|
107
|
-
if (!deps.
|
|
109
|
+
if (!facts.checkpointPark(deps).available) {
|
|
108
110
|
sendError(res, 501, "capability.checkpoint_store_required", "wake requires the checkpoint store");
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
|
-
import { gatedPrincipal, isOperator, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
2
|
+
import { gatedPrincipal, isOperator, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
3
|
import { assertNever } from "../route-ctx.js";
|
|
4
|
+
import * as facts from "../../capability-facts.js";
|
|
4
5
|
export async function handleObservability(req, res, match, ctx) {
|
|
5
6
|
const { deps } = ctx;
|
|
6
7
|
switch (match.decl.id) {
|
|
7
8
|
case "outcomes": {
|
|
9
|
+
const fact = facts.outcomeLedger(deps);
|
|
10
|
+
if (!fact.available && fact.why === "operator_principals_empty") {
|
|
11
|
+
sendOperatorPrincipalsRequired(res);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
8
14
|
if (deps.config.requirePrincipal) {
|
|
9
15
|
const principal = gatedPrincipal(req, deps.config);
|
|
10
16
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
@@ -12,14 +18,22 @@ export async function handleObservability(req, res, match, ctx) {
|
|
|
12
18
|
return;
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
|
-
if (!
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
if (!fact.available) {
|
|
22
|
+
switch (fact.why) {
|
|
23
|
+
case "outcome_ledger_unqueryable":
|
|
24
|
+
sendError(res, 501, "capability.outcome_ledger_required", "no queryable outcome ledger on this worker (File sink — read the JSONL dataset directly)");
|
|
25
|
+
return;
|
|
26
|
+
case "operator_principals_empty":
|
|
27
|
+
sendOperatorPrincipalsRequired(res);
|
|
28
|
+
return;
|
|
29
|
+
default:
|
|
30
|
+
return assertNever(fact.why);
|
|
31
|
+
}
|
|
18
32
|
}
|
|
19
33
|
const q = new URLSearchParams(match.search);
|
|
20
34
|
const sig = q.get("signature") ?? undefined;
|
|
21
35
|
try {
|
|
22
|
-
const rows = await
|
|
36
|
+
const rows = await fact.sink.summary({ ...(sig ? { taskSignature: sig } : {}), includeAborted: q.get("includeAborted") === "true" });
|
|
23
37
|
sendJson(res, 200, { outcomes: rows });
|
|
24
38
|
}
|
|
25
39
|
catch (err) {
|
|
@@ -1,5 +1,6 @@
|
|
|
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
|
import { RETENTION_AUDIT_DEFAULT_LIMIT, RETENTION_AUDIT_MAX_LIMIT } from "../../plugins/retention-lane-store-sql.js";
|
|
5
6
|
const AUDIT_PATH = "/v1/ops/retention/audit";
|
|
@@ -31,6 +32,10 @@ export async function handleRetention(req, res, match, ctx) {
|
|
|
31
32
|
sendPrincipalRefusal(res, req, deps.config);
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
35
|
+
if (!facts.operatorLane(deps).available) {
|
|
36
|
+
sendOperatorPrincipalsRequired(res);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
34
39
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
35
40
|
sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
|
|
36
41
|
return;
|