@sema-agent/server 7.103.2 → 7.105.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.
- package/MIGRATION.md +378 -2
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +320 -16
- package/deploy/sema-up/README.md +57 -7
- package/deploy/sema-up/cluster-up.sh +18 -4
- package/deploy/sema-up/kube-up.sh +16 -0
- package/deploy/sema-up/lib/dataroot.sh +142 -0
- package/deploy/sema-up/lib/flagshape.sh +69 -0
- package/deploy/sema-up/lib/lanes.py +14 -4
- package/deploy/sema-up/lib/shell-access.sh +41 -0
- package/deploy/sema-up/preflight.sh +14 -1
- package/deploy/sema-up/sema-up.sh +134 -37
- package/deploy/sema-up/smoke.sh +5 -0
- package/dist/auto-mode-face.js +3 -4
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.d.ts +6 -2
- package/dist/boot/config-center.js +5 -59
- package/dist/boot/execution-env.d.ts +11 -0
- package/dist/boot/execution-env.js +14 -0
- package/dist/boot/execution-lane.d.ts +25 -0
- package/dist/boot/execution-lane.js +95 -0
- package/dist/boot/memory-consolidation.d.ts +185 -9
- package/dist/boot/memory-consolidation.js +192 -32
- package/dist/boot/resolve-spec.d.ts +2 -0
- package/dist/boot/resolve-spec.js +28 -24
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +6 -1
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +18 -2
- package/dist/boot/session-incarnation-seat.d.ts +41 -0
- package/dist/boot/session-incarnation-seat.js +69 -0
- package/dist/boot/stage-01-config.js +2 -2
- package/dist/boot/stage-02-registry-pull.js +3 -1
- package/dist/boot/stage-04-stores.d.ts +2 -0
- package/dist/boot/stage-04-stores.js +3 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -0
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +4 -1
- package/dist/boot/stage-06-runners.js +7 -14
- package/dist/boot/stage-07-capability-layer.d.ts +4 -1
- package/dist/boot/stage-08-reapers.d.ts +4 -1
- package/dist/boot/stage-09-leader.d.ts +6 -1
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +6 -1
- package/dist/boot/stage-10-http-server.js +4 -1
- package/dist/brain.d.ts +1 -1
- package/dist/config-catalog.d.ts +16 -1
- package/dist/config-catalog.js +9 -9
- package/dist/config-center/restart-signal.d.ts +4 -4
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +37 -6
- package/dist/config-provider.d.ts +42 -10
- package/dist/config-provider.js +18 -16
- package/dist/config-types.d.ts +37 -16
- package/dist/config-types.js +1 -0
- package/dist/config.d.ts +28 -30
- package/dist/config.js +95 -77
- package/dist/env-facts.d.ts +13 -3
- package/dist/env-facts.js +15 -3
- package/dist/execution-lane-caps.d.ts +15 -0
- package/dist/execution-lane-caps.js +8 -0
- package/dist/file-history-capture.d.ts +42 -35
- package/dist/file-history-capture.js +15 -10
- package/dist/file-history-disclosure.d.ts +7 -31
- package/dist/file-history-disclosure.js +12 -0
- package/dist/fleet/fleet-bus.d.ts +5 -1
- package/dist/http/admission.d.ts +12 -5
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.d.ts +18 -0
- package/dist/http/dispatch.js +19 -6
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.d.ts +1 -1
- package/dist/http/resume-legs.js +9 -5
- package/dist/http/route-table.d.ts +15 -4
- package/dist/http/route-table.js +6 -5
- package/dist/http/routes/agents-roster.d.ts +7 -0
- package/dist/http/routes/agents-roster.js +6 -2
- package/dist/http/routes/approvals-assistant.d.ts +48 -19
- package/dist/http/routes/approvals-assistant.js +117 -50
- package/dist/http/routes/capabilities.js +3 -2
- package/dist/http/routes/config-catalog.js +1 -0
- package/dist/http/routes/fleet.d.ts +3 -2
- package/dist/http/routes/images.js +0 -1
- package/dist/http/routes/memory-consolidation.d.ts +3 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +29 -10
- package/dist/http/routes/session-sync.js +14 -15
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +49 -25
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/side-query.js +8 -1
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.d.ts +2 -0
- package/dist/http/routes/trace-usage.js +8 -13
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +30 -8
- package/dist/http/server.js +11 -5
- package/dist/http/sse-lifecycle.d.ts +1 -1
- package/dist/http/sse-log.d.ts +12 -9
- package/dist/http/sse-log.js +1 -8
- package/dist/http/sync-submit-body.d.ts +43 -0
- package/dist/http/sync-submit-body.js +4 -0
- package/dist/http/wire-types.d.ts +23 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/leader/merge.d.ts +8 -3
- package/dist/memory-incident-sink.js +2 -1
- package/dist/model-select.d.ts +9 -0
- package/dist/model-select.js +5 -1
- package/dist/observability/corrupt-read-seat.d.ts +151 -16
- package/dist/observability/corrupt-read-seat.js +158 -15
- package/dist/observability/fail-open.d.ts +17 -1
- package/dist/observability/fail-open.js +17 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.d.ts +60 -10
- package/dist/observability/run-terminal.js +119 -9
- package/dist/parked-decide.d.ts +8 -3
- package/dist/parked-decide.js +56 -20
- package/dist/parked-revive-surface.d.ts +14 -7
- package/dist/parked-revive-surface.js +5 -1
- package/dist/plugins/approval-ask-store-file.d.ts +4 -1
- package/dist/plugins/approval-ask-store-file.js +9 -12
- package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -1
- package/dist/plugins/approval-exemption-store.d.ts +21 -1
- package/dist/plugins/approval-exemption-store.js +36 -7
- package/dist/plugins/approval-nonce-store.d.ts +16 -2
- package/dist/plugins/approval-nonce-store.js +17 -7
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
- package/dist/plugins/file-resume-anchor-store.js +19 -5
- package/dist/plugins/file-run-store.d.ts +21 -8
- package/dist/plugins/file-run-store.js +60 -38
- package/dist/plugins/image-bake-store-sql.d.ts +0 -2
- package/dist/plugins/image-bake-store-sql.js +0 -5
- package/dist/plugins/local-session-store.d.ts +22 -0
- package/dist/plugins/local-session-store.js +74 -11
- package/dist/plugins/memory-run-store.d.ts +7 -5
- package/dist/plugins/memory-run-store.js +11 -4
- package/dist/plugins/one-time-migrations.d.ts +38 -1
- package/dist/plugins/one-time-migrations.js +143 -15
- package/dist/plugins/permission-rule-store-file.d.ts +9 -2
- package/dist/plugins/permission-rule-store-file.js +37 -51
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +19 -10
- package/dist/plugins/retention-store-sql.js +1 -0
- package/dist/plugins/run-store-sql.d.ts +15 -6
- package/dist/plugins/run-store-sql.js +15 -13
- package/dist/plugins/schema-indexes.js +1 -0
- package/dist/plugins/send-file-ledger.d.ts +5 -1
- package/dist/plugins/send-file-ledger.js +11 -6
- package/dist/plugins/session-event-payload.d.ts +14 -4
- package/dist/plugins/session-event-payload.js +5 -2
- package/dist/plugins/session-policy-store-sql.js +18 -5
- package/dist/plugins/store-backend.d.ts +13 -7
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +12 -5
- package/dist/plugins/tidb-session-store.d.ts +6 -1
- package/dist/plugins/tidb-session-store.js +17 -10
- package/dist/project-identity.d.ts +4 -6
- package/dist/read-face-posture.d.ts +21 -0
- package/dist/read-face-posture.js +9 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +72 -3
- package/dist/request-value-closure.js +12 -4
- package/dist/roster-bound-http-status.d.ts +6 -0
- package/dist/roster-bound-http-status.js +6 -0
- package/dist/rules-consent.js +0 -11
- package/dist/run-local.js +22 -27
- package/dist/runs.d.ts +22 -2
- package/dist/runs.js +29 -7
- package/dist/sealed-key.d.ts +31 -4
- package/dist/sealed-key.js +9 -0
- package/dist/security.d.ts +5 -1
- package/dist/session-children-retire.d.ts +92 -0
- package/dist/session-children-retire.js +85 -0
- package/dist/session-error-wire.d.ts +58 -0
- package/dist/session-error-wire.js +38 -0
- package/dist/session-incarnation.d.ts +61 -0
- package/dist/session-incarnation.js +43 -0
- package/dist/session-policy-wire.d.ts +98 -12
- package/dist/session-policy-wire.js +71 -8
- package/dist/session-records-skipped-notice.d.ts +20 -0
- package/dist/session-records-skipped-notice.js +27 -0
- package/dist/tool-approval.d.ts +65 -0
- package/dist/tool-approval.js +36 -9
- package/dist/trace/core-keyset-guard.d.ts +8 -7
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +11 -6
- package/dist/trace/project.js +12 -3
- package/dist/trace/redact.d.ts +7 -0
- package/dist/trace/redact.js +153 -61
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +2 -2
- package/dist/plugins/memory-session-policy-store.d.ts +0 -46
- package/dist/plugins/memory-session-policy-store.js +0 -52
package/dist/http/routes/runs.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { persistedPlaneOf } from "../../terminal.js";
|
|
2
|
-
import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead,
|
|
3
|
-
import { isModelTextLedgerEventType } from "../../trace/ledger-events.js";
|
|
2
|
+
import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
|
|
6
5
|
import { isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
|
|
@@ -16,7 +15,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
16
15
|
import { streamSseLog } from "../sse-log.js";
|
|
17
16
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
18
17
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
19
|
-
import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
18
|
+
import { ASK_DECISION_CONSUMER_ABSENT, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, outcomeOfDecidedRow, resolveStreamApprovalGate, respondSettledBy } from "../../tool-approval.js";
|
|
20
19
|
import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
|
|
21
20
|
import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
|
|
22
21
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
@@ -45,6 +44,7 @@ const AskDecisionBodySchema = z
|
|
|
45
44
|
.refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
|
|
46
45
|
.optional(),
|
|
47
46
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
47
|
+
settledBy: z.unknown().optional(),
|
|
48
48
|
})
|
|
49
49
|
.strict();
|
|
50
50
|
const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
|
|
@@ -125,14 +125,9 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
|
|
|
125
125
|
return run ? { status: run.status, updatedAt: run.updatedAt } : undefined;
|
|
126
126
|
},
|
|
127
127
|
getEvents: (id, after) => runStore.getEvents(id, after),
|
|
128
|
-
retainedFrom: (id) => runStore.retainedFrom(id),
|
|
129
128
|
formatEvent: (ev) => {
|
|
130
129
|
const type = normalizeRunEventType(ev.type);
|
|
131
|
-
const redacted = type
|
|
132
|
-
? { ...ev.data, result: memoRedactTerminalResult("ev.done.result", ev.data.result) }
|
|
133
|
-
: type === "failed" ? memoRedactedRead("ev.failed", ev.data, (d) => redactLedgerEventData(type, d))
|
|
134
|
-
: isModelTextLedgerEventType(type) ? memoRedactedRead("ev.model-text", ev.data, (d) => redactLedgerEventData(type, d))
|
|
135
|
-
: redactLedgerEventData(type, ev.data);
|
|
130
|
+
const redacted = redactLedgerEventForRead(type, ev.data);
|
|
136
131
|
const data = screenedStoredEventData(type, redacted);
|
|
137
132
|
return { id: ev.seq, event: type, data: { type, ...(data ?? {}) } };
|
|
138
133
|
},
|
|
@@ -1287,10 +1282,33 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1287
1282
|
}
|
|
1288
1283
|
const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
|
|
1289
1284
|
if (!parsedBody.success) {
|
|
1290
|
-
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label");
|
|
1285
|
+
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
|
|
1291
1286
|
return;
|
|
1292
1287
|
}
|
|
1293
1288
|
const body = parsedBody.data;
|
|
1289
|
+
const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
|
|
1290
|
+
switch (settledByVerdict.kind) {
|
|
1291
|
+
case "not_in_proof":
|
|
1292
|
+
sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
|
|
1293
|
+
return;
|
|
1294
|
+
case "deny_only":
|
|
1295
|
+
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
1296
|
+
return;
|
|
1297
|
+
case "not_string":
|
|
1298
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
1299
|
+
return;
|
|
1300
|
+
case "too_wide":
|
|
1301
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
|
|
1302
|
+
return;
|
|
1303
|
+
case "absent":
|
|
1304
|
+
case "accepted":
|
|
1305
|
+
break;
|
|
1306
|
+
default: {
|
|
1307
|
+
const unreachable = settledByVerdict;
|
|
1308
|
+
throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
|
|
1294
1312
|
const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
|
|
1295
1313
|
const taskId = match.params.taskId;
|
|
1296
1314
|
const askId = match.params.askId;
|
|
@@ -1424,6 +1442,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1424
1442
|
decisionActor: actor,
|
|
1425
1443
|
decisionNote: body.note ?? null,
|
|
1426
1444
|
idempotencyKey: body.idempotencyKey ?? null,
|
|
1445
|
+
...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
|
|
1427
1446
|
...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
|
|
1428
1447
|
});
|
|
1429
1448
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
2
|
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
-
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator,
|
|
3
|
+
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
5
|
import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, overwriteWipeIncapable, wipeSessionAttendants } from "../../session-sync.js";
|
|
6
6
|
import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
|
|
7
7
|
import { isUuidShape } from "../../security.js";
|
|
8
|
-
import { sendJson, sendError, sendSessionPolicyError, msg } from "../send.js";
|
|
8
|
+
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, msg } from "../send.js";
|
|
9
9
|
import { buildDisplaySafeUrl } from "../../trace/redact.js";
|
|
10
|
-
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
10
|
+
import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
|
|
11
11
|
const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
|
|
12
12
|
const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
|
|
13
13
|
const SYNC_IMPORT_BATCH = 500;
|
|
14
14
|
import { SHA256_HEX_RE } from "../../digest-form.js";
|
|
15
15
|
import { assertNever } from "../route-ctx.js";
|
|
16
|
-
import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
|
|
16
|
+
import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
|
|
17
17
|
export function createSessionSyncLocal() {
|
|
18
18
|
const pendingStagings = new Map();
|
|
19
19
|
const importLeases = new Map();
|
|
@@ -43,12 +43,12 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
43
43
|
}
|
|
44
44
|
const historyStore = deps.fileHistoryStore;
|
|
45
45
|
if (historyStore === undefined) {
|
|
46
|
-
sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: file-history
|
|
46
|
+
sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: no file-history store is wired, so there is no file history to export or import");
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const scope = sessionOwnerScopeForWrite(req);
|
|
50
50
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
51
|
-
|
|
51
|
+
sendUnauthorized(res, "identity");
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
const sessionId = safeDecode(match.params.sessionId);
|
|
@@ -235,6 +235,11 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
235
235
|
}
|
|
236
236
|
const policyRead = readSessionRulesRecords(body.policy);
|
|
237
237
|
if (policyRead.problem !== undefined) {
|
|
238
|
+
const legacy = legacyToolNameProblemOf(policyRead.problem);
|
|
239
|
+
if (legacy !== undefined) {
|
|
240
|
+
sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, { field: legacy.field, name: legacy.name });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
238
243
|
sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
|
|
239
244
|
return;
|
|
240
245
|
}
|
|
@@ -459,20 +464,14 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
459
464
|
}
|
|
460
465
|
}
|
|
461
466
|
catch (e) {
|
|
467
|
+
await pending.handle.abort().catch(() => undefined);
|
|
468
|
+
cleanupStaging(stagingId);
|
|
462
469
|
if (e instanceof PrecheckError) {
|
|
463
|
-
await pending.handle.abort().catch(() => undefined);
|
|
464
|
-
cleanupStaging(stagingId);
|
|
465
470
|
sendError(res, e.status, "invalid_entries", e.message);
|
|
466
471
|
return;
|
|
467
472
|
}
|
|
468
|
-
if (
|
|
469
|
-
await pending.handle.abort().catch(() => undefined);
|
|
470
|
-
cleanupStaging(stagingId);
|
|
471
|
-
sendError(res, 422, "invalid_entries", "staged session log failed validation");
|
|
473
|
+
if (sendSessionError(res, e, "import"))
|
|
472
474
|
return;
|
|
473
|
-
}
|
|
474
|
-
await pending.handle.abort().catch(() => undefined);
|
|
475
|
-
cleanupStaging(stagingId);
|
|
476
475
|
throw e;
|
|
477
476
|
}
|
|
478
477
|
const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isUuidV7, legacyUnownedAmnestyOpen } from "../../security.js";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
|
+
import { sendUnauthorized } from "../principal-gate.js";
|
|
3
4
|
import { filterFingerprint } from "../cursor-fingerprint.js";
|
|
4
5
|
import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
|
|
5
6
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -10,7 +11,7 @@ export async function handleSessionsList(req, res, match, ctx) {
|
|
|
10
11
|
case "sessions-list": {
|
|
11
12
|
const { fleetWide, gateOwner } = sessionOwnerScope(req);
|
|
12
13
|
if (!fleetWide && gateOwner === null) {
|
|
13
|
-
|
|
14
|
+
sendUnauthorized(res, "identity");
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
17
|
const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
|
|
@@ -25,7 +26,7 @@ export async function handleSessionsList(req, res, match, ctx) {
|
|
|
25
26
|
case "sendfile-links": {
|
|
26
27
|
const { fleetWide, gateOwner } = sessionOwnerScope(req);
|
|
27
28
|
if (!fleetWide && gateOwner === null) {
|
|
28
|
-
|
|
29
|
+
sendUnauthorized(res, "identity");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
if (!deps.sendFileLedger) {
|
|
@@ -6,10 +6,11 @@ import { redactSecrets } from "../../trace/redact.js";
|
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
7
7
|
import { targetOutsideStoreOf } from "../../memory-store-refusal.js";
|
|
8
8
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
9
|
-
import {
|
|
10
|
-
import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
|
-
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
|
|
9
|
+
import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
10
|
+
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
|
+
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
12
12
|
import { assertNever } from "../route-ctx.js";
|
|
13
|
+
import { rosterAgentId } from "./agents-roster.js";
|
|
13
14
|
const MCP_STATUS_TTL_MS = 5_000;
|
|
14
15
|
const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
|
|
15
16
|
const A2A_STATUS_TTL_MS = 5_000;
|
|
@@ -67,6 +68,34 @@ async function readLastLegMcp(deps, sessionId, principal) {
|
|
|
67
68
|
export async function handleSessions(req, res, match, ctx) {
|
|
68
69
|
const { deps } = ctx;
|
|
69
70
|
const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
|
|
71
|
+
const readOwnedSessionView = async (sessionAudit, sessionId, notFoundForm) => {
|
|
72
|
+
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
73
|
+
if (ownerOf !== undefined) {
|
|
74
|
+
const owner = await ownerOf(sessionId);
|
|
75
|
+
if (owner === undefined) {
|
|
76
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (!runOwnerOk(req, res, owner, notFoundForm))
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
let audit;
|
|
83
|
+
try {
|
|
84
|
+
audit = await sessionAudit(sessionId);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (ownerOf !== undefined)
|
|
88
|
+
throw e;
|
|
89
|
+
throw new Error(`session view read failed before ownership could be established: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
90
|
+
}
|
|
91
|
+
if (!audit) {
|
|
92
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (!runOwnerOk(req, res, audit.owner, notFoundForm))
|
|
96
|
+
return undefined;
|
|
97
|
+
return audit;
|
|
98
|
+
};
|
|
70
99
|
const sse = ctx.local.sessions;
|
|
71
100
|
const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
|
|
72
101
|
{
|
|
@@ -441,6 +470,8 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
441
470
|
}
|
|
442
471
|
catch (e) {
|
|
443
472
|
cleanup();
|
|
473
|
+
if (!res.headersSent && sendSessionError(res, e, "open"))
|
|
474
|
+
return;
|
|
444
475
|
if (!res.headersSent)
|
|
445
476
|
sendError(res, 500, "internal.error", `session events failed: ${e instanceof Error ? e.message : "unknown"}`);
|
|
446
477
|
else
|
|
@@ -467,12 +498,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
467
498
|
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)");
|
|
468
499
|
return;
|
|
469
500
|
}
|
|
470
|
-
|
|
471
|
-
if (!audit) {
|
|
472
|
-
sendError(res, 404, "not_found.session", "session not found");
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (!runOwnerOk(req, res, audit.owner, "session"))
|
|
501
|
+
if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
|
|
476
502
|
return;
|
|
477
503
|
const faces = deps.memoryCompliance;
|
|
478
504
|
if (faces === undefined) {
|
|
@@ -532,12 +558,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
532
558
|
sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires 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)");
|
|
533
559
|
return;
|
|
534
560
|
}
|
|
535
|
-
|
|
536
|
-
if (!audit) {
|
|
537
|
-
sendError(res, 404, "not_found.session", "session not found");
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
if (!runOwnerOk(req, res, audit.owner, "session"))
|
|
561
|
+
if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
|
|
541
562
|
return;
|
|
542
563
|
const face = deps.sessionMemoryStatus;
|
|
543
564
|
if (face === undefined) {
|
|
@@ -581,12 +602,8 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
581
602
|
sendError(res, 404, "not_found.route", "not found");
|
|
582
603
|
return;
|
|
583
604
|
}
|
|
584
|
-
const audit = await deps.sessionAudit
|
|
585
|
-
if (
|
|
586
|
-
sendError(res, 404, "not_found.session", "session not found");
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
if (!runOwnerOk(req, res, audit.owner))
|
|
605
|
+
const audit = await readOwnedSessionView(deps.sessionAudit, match.params.sessionId, "run");
|
|
606
|
+
if (audit === undefined)
|
|
590
607
|
return;
|
|
591
608
|
const q = new URLSearchParams(match.search);
|
|
592
609
|
const qi = (k) => {
|
|
@@ -636,7 +653,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
636
653
|
}
|
|
637
654
|
const scope = sessionOwnerScopeForWrite(req);
|
|
638
655
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
639
|
-
|
|
656
|
+
sendUnauthorized(res, "identity");
|
|
640
657
|
return;
|
|
641
658
|
}
|
|
642
659
|
const sourceId = safeDecode(match.params.sessionId);
|
|
@@ -673,7 +690,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
673
690
|
}
|
|
674
691
|
const scope = sessionOwnerScopeForWrite(req);
|
|
675
692
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
676
|
-
|
|
693
|
+
sendUnauthorized(res, "identity");
|
|
677
694
|
return;
|
|
678
695
|
}
|
|
679
696
|
const sessionId = safeDecode(match.params.sessionId);
|
|
@@ -700,6 +717,10 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
700
717
|
sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId: result.active });
|
|
701
718
|
return;
|
|
702
719
|
}
|
|
720
|
+
if ("childrenAlive" in result) {
|
|
721
|
+
sendError(res, 409, "conflict.session_children_alive", "this session still has background agents running where this replica cannot stop them (on another replica, for example), so the delete was refused before anything was deleted — `remaining` lists their ids (the same ids `GET /v1/agents/roster` shows); retry the delete once they finish, or stop them where they run", { remaining: result.childrenAlive.map((handle) => rosterAgentId({ handle })) });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
703
724
|
sendJson(res, 200, result);
|
|
704
725
|
return;
|
|
705
726
|
}
|
|
@@ -714,7 +735,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
714
735
|
}
|
|
715
736
|
const principal = gatedPrincipal(req, deps.config);
|
|
716
737
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
717
|
-
|
|
738
|
+
sendUnauthorized(res, "identity");
|
|
718
739
|
return;
|
|
719
740
|
}
|
|
720
741
|
const sessionId = safeDecode(match.params.sessionId);
|
|
@@ -756,7 +777,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
756
777
|
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
757
778
|
return;
|
|
758
779
|
}
|
|
759
|
-
const read =
|
|
780
|
+
const read = readWritableSessionPermissionRules(body.rules);
|
|
760
781
|
if (read.problem !== undefined) {
|
|
761
782
|
switch (read.problem.kind) {
|
|
762
783
|
case "not-an-object":
|
|
@@ -765,6 +786,9 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
765
786
|
case "field-not-string-array":
|
|
766
787
|
sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
|
|
767
788
|
return;
|
|
789
|
+
case "legacy-tool-name":
|
|
790
|
+
sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { field: read.problem.field, name: read.problem.name });
|
|
791
|
+
return;
|
|
768
792
|
default: {
|
|
769
793
|
const unhandled = read.problem;
|
|
770
794
|
throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError } from "@sema-agent/core";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
|
-
import { gatedPrincipal } from "../principal-gate.js";
|
|
3
|
+
import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
5
|
const RETRY_AFTER_SEC = 10;
|
|
6
6
|
const HTTP_LIST_LIMIT = 100;
|
|
@@ -29,7 +29,7 @@ async function bindSnapshot(req, res, ctx) {
|
|
|
29
29
|
const { deps } = ctx;
|
|
30
30
|
const principal = gatedPrincipal(req, deps.config);
|
|
31
31
|
if (!principal && deps.config.requirePrincipal === true) {
|
|
32
|
-
|
|
32
|
+
sendUnauthorized(res, "identity");
|
|
33
33
|
return undefined;
|
|
34
34
|
}
|
|
35
35
|
const provider = deps.sharedMemoryStore;
|
|
@@ -3,6 +3,7 @@ import { HttpError } from "../../security.js";
|
|
|
3
3
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
|
|
4
4
|
import { modelUnavailableMessage } from "../../model-entry-refusal.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
|
+
import { admitProviderEcho } from "../../observability/run-terminal.js";
|
|
6
7
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
7
8
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
8
9
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -28,6 +29,12 @@ function isModelRole(value) {
|
|
|
28
29
|
function passthroughModelRole(value) {
|
|
29
30
|
return value;
|
|
30
31
|
}
|
|
32
|
+
function shapeSideQueryEcho(result) {
|
|
33
|
+
if (result.responseModel === undefined || admitProviderEcho(result.responseModel, "side-query.responseModel"))
|
|
34
|
+
return result;
|
|
35
|
+
const { responseModel: _outOfShape, ...admitted } = result;
|
|
36
|
+
return admitted;
|
|
37
|
+
}
|
|
31
38
|
export async function handleSideQuery(req, res, match, ctx) {
|
|
32
39
|
const { deps } = ctx;
|
|
33
40
|
const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
@@ -100,7 +107,7 @@ export async function handleSideQuery(req, res, match, ctx) {
|
|
|
100
107
|
deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
|
|
101
108
|
deps.metrics?.inc?.("side_query_total", { result: "ok" });
|
|
102
109
|
if (!res.writableEnded)
|
|
103
|
-
sendJson(res, 200, result);
|
|
110
|
+
sendJson(res, 200, shapeSideQueryEcho(result));
|
|
104
111
|
}
|
|
105
112
|
catch (e) {
|
|
106
113
|
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
|
|
2
2
|
import { runWithVerification, runCascade, taskNotificationDedupKey, uuidv7 } from "@sema-agent/core";
|
|
3
|
-
import { graftParkToolCallId, markChildrenStoppedByUserOnAbort,
|
|
3
|
+
import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, syncRefusalHttpStatus, HEARTBEAT_MS } from "../../runs.js";
|
|
4
4
|
import { sealResultForExit, outwardPlane } from "../../terminal.js";
|
|
5
5
|
import { withPrincipal } from "../../observability/principal-context.js";
|
|
6
6
|
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
|
|
@@ -24,6 +24,7 @@ import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflic
|
|
|
24
24
|
import { commitPlacement, devicePlacementErrorBody } from "../device-placement.js";
|
|
25
25
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
26
26
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
27
|
+
import { buildSyncSubmitBody } from "../sync-submit-body.js";
|
|
27
28
|
import { assertNever } from "../route-ctx.js";
|
|
28
29
|
const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
29
30
|
function sseData(res, payload) {
|
|
@@ -31,6 +32,9 @@ function sseData(res, payload) {
|
|
|
31
32
|
const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
|
|
32
33
|
res.write(`${idLine}data: ${JSON.stringify(payload)}\n\n`);
|
|
33
34
|
}
|
|
35
|
+
async function mintSubmitExitResult(result, checkpoints) {
|
|
36
|
+
return redactTerminalResult(sealResultForExit(await graftParkToolCallId(result, checkpoints)));
|
|
37
|
+
}
|
|
34
38
|
function sendSubmitResult(res, resp) {
|
|
35
39
|
const raw = resp.body;
|
|
36
40
|
if (typeof raw === "object" && raw !== null && Object.hasOwn(raw, "retryAfterSec") && "retryAfterSec" in raw) {
|
|
@@ -421,7 +425,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
421
425
|
userMsgEntryId = ev.entryId;
|
|
422
426
|
const cancelVerbAtDone = ev.type === "done" && durableTaskId !== undefined && cancelledViaVerb.has(durableTaskId);
|
|
423
427
|
if (ev.type === "done") {
|
|
424
|
-
finalResult =
|
|
428
|
+
finalResult = await mintSubmitExitResult(ev.result, deps.checkpointStore);
|
|
425
429
|
if (durableTaskId && finalResult)
|
|
426
430
|
finalResult = { ...finalResult, taskId: durableTaskId };
|
|
427
431
|
if (cancelVerbAtDone && finalResult !== undefined) {
|
|
@@ -652,10 +656,10 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
652
656
|
: withApproval());
|
|
653
657
|
if (durableTaskId && deps.runStore) {
|
|
654
658
|
const finalPlane = finalResult ? outwardPlane(finalResult.terminal) : undefined;
|
|
655
|
-
if (finalPlane?.status === "suspended" && deps.checkpointStore)
|
|
656
|
-
await deps.runStore.setSuspended(durableTaskId);
|
|
657
|
-
else if (finalPlane?.status === "needs_review" && deps.checkpointStore)
|
|
658
|
-
await deps.runStore.setNeedsReview(durableTaskId);
|
|
659
|
+
if (finalResult && finalPlane?.status === "suspended" && deps.checkpointStore)
|
|
660
|
+
await deps.runStore.setSuspended(durableTaskId, finalResult);
|
|
661
|
+
else if (finalResult && finalPlane?.status === "needs_review" && deps.checkpointStore)
|
|
662
|
+
await deps.runStore.setNeedsReview(durableTaskId, finalResult);
|
|
659
663
|
else if (finalResult && finalPlane)
|
|
660
664
|
await deps.runStore.setTerminal(durableTaskId, finalPlane.status, finalResult, finalPlane.errorMessage ?? null);
|
|
661
665
|
else {
|
|
@@ -667,7 +671,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
667
671
|
await ledgerSink.flush().catch(() => undefined);
|
|
668
672
|
await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
|
|
669
673
|
}
|
|
670
|
-
await deps.runStore.setSuspended(durableTaskId);
|
|
674
|
+
await deps.runStore.setSuspended(durableTaskId, null);
|
|
671
675
|
settleFleet("suspended");
|
|
672
676
|
}
|
|
673
677
|
else {
|
|
@@ -710,7 +714,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
710
714
|
await ledgerSink.flush().catch(() => undefined);
|
|
711
715
|
await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
|
|
712
716
|
}
|
|
713
|
-
await deps.runStore.setSuspended(durableTaskId);
|
|
717
|
+
await deps.runStore.setSuspended(durableTaskId, null);
|
|
714
718
|
settleFleet("suspended");
|
|
715
719
|
}
|
|
716
720
|
else if (finalResult) {
|
|
@@ -859,7 +863,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
859
863
|
engineResult: result,
|
|
860
864
|
});
|
|
861
865
|
}
|
|
862
|
-
const resumeAtStatus =
|
|
866
|
+
const resumeAtStatus = syncRefusalHttpStatus(result);
|
|
863
867
|
if (resumeAtStatus) {
|
|
864
868
|
const resumeAtPlane = outwardPlane(result.terminal);
|
|
865
869
|
if (durableTaskId && deps.runStore)
|
|
@@ -869,22 +873,21 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
869
873
|
: undefined;
|
|
870
874
|
return { status: resumeAtStatus, body: { errorCode: resumeAtPlane.errorCode, error: redactErrorMessage(resumeAtPlane.errorMessage) ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
|
|
871
875
|
}
|
|
876
|
+
const exitResult = await mintSubmitExitResult(result, deps.checkpointStore);
|
|
872
877
|
const syncPlane = outwardPlane(result.terminal);
|
|
873
|
-
if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
|
|
878
|
+
if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && durableTaskId && prepared.spec.sessionId) {
|
|
879
|
+
await deps.runStore.setSuspended(durableTaskId, exitResult);
|
|
880
|
+
return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult, { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" })) };
|
|
877
881
|
}
|
|
878
|
-
if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
|
|
882
|
+
if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && durableTaskId && prepared.spec.sessionId) {
|
|
883
|
+
await deps.runStore.setNeedsReview(durableTaskId, exitResult);
|
|
884
|
+
return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult, { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" })) };
|
|
882
885
|
}
|
|
883
886
|
if (durableTaskId && deps.runStore)
|
|
884
|
-
await deps.runStore.setTerminal(durableTaskId, syncPlane.status,
|
|
887
|
+
await deps.runStore.setTerminal(durableTaskId, syncPlane.status, exitResult, syncPlane.errorMessage ?? null);
|
|
885
888
|
finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
|
|
886
889
|
noteRunOutcome(result);
|
|
887
|
-
return { status: 200, body: withSyncSubmitKeys(
|
|
890
|
+
return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult)) };
|
|
888
891
|
};
|
|
889
892
|
const resp = await idemCache.run(idemKey, async () => withRunReceipt(await runSyncSubmitLeg()), (r) => r.status < 400);
|
|
890
893
|
sendSubmitResult(res, resp);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ⚠️ 三块都跑在全局 service-credential 门**之前**(与 /metrics 同款待遇)——只读 traceToken 单凭据的调用方
|
|
8
8
|
* (registry 中继的姿势)在全局门会被 401,挂门后=门内 traceToken 支持成死代码(1.148 a2 实机抓获)。
|
|
9
|
+
* S-672(7.105.0):门前不等于不设防 —— 本域坐在读路谓词段,先过 `dispatch.ts` 的 `readCredentialOk`
|
|
10
|
+
* (部署配了 service 凭据时要「service token / traceToken / 已验 principal」之一),块内这几道门在它之后。
|
|
9
11
|
* 因此本域在 `handle()` 里的位置就是契约的一部分:必须留在鉴权门上方,顺序不可与其他域对调。本域零可变状态。
|
|
10
12
|
*/
|
|
11
13
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { projectEvents, taskSummary, mapTraceEvent } from "../../trace/project.js";
|
|
2
|
+
import { redactLedgerEventForRead } from "../../observability/run-terminal.js";
|
|
2
3
|
import { projectArtifacts } from "../../trace/artifacts.js";
|
|
3
4
|
import { usageSummary, usageSeries, usageBreakdown } from "../../usage-analytics.js";
|
|
4
5
|
import { sleep } from "../sse-log.js";
|
|
5
6
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
6
7
|
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
7
8
|
import { filterFingerprint } from "../cursor-fingerprint.js";
|
|
8
|
-
import { gatedPrincipal } from "../principal-gate.js";
|
|
9
|
+
import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
|
|
9
10
|
import { assertNever } from "../route-ctx.js";
|
|
10
11
|
const TOOL_RESULT_LIMIT_CAP = 262_144;
|
|
11
12
|
const TOOL_RESULT_OFFSET_CAP = 2_147_483_646;
|
|
@@ -14,7 +15,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
14
15
|
const { isFleetWide, safeDecode } = ctx.helpers;
|
|
15
16
|
if (match.decl.id === "tasks-source-summary") {
|
|
16
17
|
if (!isFleetWide(req)) {
|
|
17
|
-
|
|
18
|
+
sendUnauthorized(res, "operator");
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
if (!deps.runStore) {
|
|
@@ -34,7 +35,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
34
35
|
const fleetWide = isFleetWide(req);
|
|
35
36
|
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
36
37
|
if (!fleetWide && gateOwner === null) {
|
|
37
|
-
|
|
38
|
+
sendUnauthorized(res, "identity");
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
if (!deps.runStore) {
|
|
@@ -84,7 +85,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
84
85
|
const fleetWide = isFleetWide(req);
|
|
85
86
|
const jwtOwner = fleetWide ? undefined : gatedPrincipal(req, deps.config);
|
|
86
87
|
if (!fleetWide && !jwtOwner) {
|
|
87
|
-
|
|
88
|
+
sendUnauthorized(res, "identity");
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
const q = new URLSearchParams(match.search);
|
|
@@ -220,6 +221,7 @@ async function handleToolResult(res, store, runStore, taskId, ownedRun, ref, que
|
|
|
220
221
|
return notFound();
|
|
221
222
|
sendJson(res, 200, { content: slice.content, offset: slice.offset, totalChars: slice.totalChars });
|
|
222
223
|
}
|
|
224
|
+
const readFaceEvent = (ev) => ({ ...ev, data: redactLedgerEventForRead(ev.type, ev.data) });
|
|
223
225
|
async function handleTaskTurns(res, runStore, taskId, query) {
|
|
224
226
|
const run = await runStore.getRun(taskId);
|
|
225
227
|
if (!run) {
|
|
@@ -229,7 +231,7 @@ async function handleTaskTurns(res, runStore, taskId, query) {
|
|
|
229
231
|
const limit = Math.min(100, Math.max(1, Number(query.get("limit") ?? 20) || 20));
|
|
230
232
|
const cursorSeq = Number(query.get("cursor") ?? 0) || 0;
|
|
231
233
|
const [events, retainedFrom] = await Promise.all([runStore.getEvents(taskId, 0), runStore.retainedFrom(taskId)]);
|
|
232
|
-
const all = projectEvents(events);
|
|
234
|
+
const all = projectEvents(events.map(readFaceEvent));
|
|
233
235
|
const eligible = cursorSeq > 0 ? all.filter((t) => t.seq < cursorSeq) : all;
|
|
234
236
|
const turns = eligible.slice(Math.max(0, eligible.length - limit));
|
|
235
237
|
const oldest = turns[0];
|
|
@@ -244,13 +246,6 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
|
|
|
244
246
|
}
|
|
245
247
|
const lastId = Number(req.headers["last-event-id"] ?? new URLSearchParams(search).get("from") ?? 0);
|
|
246
248
|
let from = Number.isFinite(lastId) ? lastId : 0;
|
|
247
|
-
if (from > 0) {
|
|
248
|
-
const retainedFrom = await runStore.retainedFrom(taskId);
|
|
249
|
-
if (retainedFrom > from + 1) {
|
|
250
|
-
sendError(res, 416, "limit.retention_evicted", "resume point evicted past retention", { retainedFrom });
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
249
|
sseHeaders(res);
|
|
255
250
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "delta", resumeFrom: from })}\n\n`);
|
|
256
251
|
let closed = false;
|
|
@@ -261,7 +256,7 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
|
|
|
261
256
|
let lastBeat = Date.now();
|
|
262
257
|
const write = (events) => {
|
|
263
258
|
for (const ev of events) {
|
|
264
|
-
const out = mapTraceEvent(ev.type, ev.seq, (ev.data ?? {}));
|
|
259
|
+
const out = mapTraceEvent(ev.type, ev.seq, (readFaceEvent(ev).data ?? {}));
|
|
265
260
|
if (out)
|
|
266
261
|
res.write(`id: ${ev.seq}\nevent: ${out.event}\ndata: ${JSON.stringify(out.data)}\n\n`);
|
|
267
262
|
from = ev.seq;
|
package/dist/http/send.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* now imports them from here, so all ~500 existing `sendJson(...)` call sites are unchanged text.
|
|
9
9
|
*/
|
|
10
10
|
import type { ServerResponse } from "node:http";
|
|
11
|
+
import { type SessionErrorFace } from "../session-error-wire.js";
|
|
11
12
|
/**
|
|
12
13
|
* [ref](黑板 [ref]②)—— 一条响应「是 SSE 流」的**在场标记**。
|
|
13
14
|
*
|
|
@@ -71,5 +72,16 @@ export declare function httpErrorCode(status: number, code?: string): string;
|
|
|
71
72
|
* `500 corrupt` 并点名那一列)。一只腿之后,新站点只会忘记调它,不会再各自发明一份映射。
|
|
72
73
|
*/
|
|
73
74
|
export declare function sendSessionPolicyError(res: ServerResponse, e: unknown): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* S-612 —— core `SessionError` → 应答,经 {@link sessionErrorHttp} 那**一只**映射(`src/session-error-wire.ts`)。
|
|
77
|
+
* 回 `true` = 已应答,调用点 `return`;`false` = 不是 `SessionError`,或这个词在这一面上是登记过的留白(站点的既有
|
|
78
|
+
* 处置照旧 —— 通常是继续上抛到全局兜底)。
|
|
79
|
+
*
|
|
80
|
+
* 句子:属主给了固定句就发它(import 面 `invalid_session` 那一句),否则发 core 的原句过既有脱敏(`redactErrorMessage`:
|
|
81
|
+
* 不抛、有上限)。core 的 `corrupt_session` 原句只点名会话 id 与跳过计数(「Session <id> is unreadable: its log has no
|
|
82
|
+
* meta header (N of M records skipped as corrupt)」),SQL 两店的原句点名 `<表>.<列>` 与行号 —— 两者都不含盘上路径
|
|
83
|
+
* (路径只进运维日志:`session_repo_corrupt_read` 行)。
|
|
84
|
+
*/
|
|
85
|
+
export declare function sendSessionError(res: ServerResponse, e: unknown, face: SessionErrorFace): boolean;
|
|
74
86
|
export declare function msg(e: unknown): string;
|
|
75
87
|
//# sourceMappingURL=send.d.ts.map
|
package/dist/http/send.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { SessionPolicyError } from "@sema-agent/core";
|
|
1
|
+
import { SessionError, SessionPolicyError } from "@sema-agent/core";
|
|
2
2
|
import { sessionPolicyErrorHttp } from "../session-policy-wire.js";
|
|
3
|
+
import { sessionErrorHttp } from "../session-error-wire.js";
|
|
4
|
+
import { redactErrorMessage } from "../observability/run-terminal.js";
|
|
3
5
|
export const SSE_RESPONSE_MARK = Symbol("sema.http.sse-response");
|
|
4
6
|
export function isSseResponse(res) {
|
|
5
7
|
return res[SSE_RESPONSE_MARK] === true;
|
|
@@ -44,6 +46,15 @@ export function sendSessionPolicyError(res, e) {
|
|
|
44
46
|
sendError(res, h.status, h.errorCode, e.message);
|
|
45
47
|
return true;
|
|
46
48
|
}
|
|
49
|
+
export function sendSessionError(res, e, face) {
|
|
50
|
+
if (!(e instanceof SessionError))
|
|
51
|
+
return false;
|
|
52
|
+
const wire = sessionErrorHttp(e.code, face);
|
|
53
|
+
if (wire === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
sendError(res, wire.status, wire.errorCode, wire.sentence ?? redactErrorMessage(e.message) ?? "session store error");
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
47
58
|
export function msg(e) {
|
|
48
59
|
return e instanceof Error ? e.message : String(e);
|
|
49
60
|
}
|