@sema-agent/server 7.103.2 → 7.104.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 +346 -2
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +271 -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 +4 -0
- 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 +13 -28
- package/dist/config.js +53 -70
- 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/http/admission.d.ts +11 -4
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.js +3 -3
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.js +9 -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/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 +27 -2
- package/dist/http/routes/session-sync.js +8 -14
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +44 -23
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.js +4 -4
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +21 -4
- package/dist/http/server.js +7 -4
- 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 +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.d.ts +15 -1
- package/dist/observability/run-terminal.js +4 -0
- 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/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/run-store-sql.d.ts +12 -4
- package/dist/plugins/run-store-sql.js +15 -13
- 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/store-backend.d.ts +7 -3
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +8 -2
- 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/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/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-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 +1 -1
- package/dist/trace/project.js +9 -0
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +2 -2
|
@@ -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 } from "../../tool-approval.js";
|
|
5
|
+
import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody } 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";
|
|
@@ -27,30 +27,6 @@ const DECIDE_BODY_DECLARED_KEYS = [
|
|
|
27
27
|
];
|
|
28
28
|
const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
|
|
29
29
|
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
|
|
30
|
-
const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
|
|
31
|
-
const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
|
|
32
|
-
export function decideBodyKeyIssue(raw) {
|
|
33
|
-
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
34
|
-
return null;
|
|
35
|
-
const buckets = { unknown: [], unsupported: [] };
|
|
36
|
-
scanKeyClosure(raw, "", ACCEPTED_DECIDE_BODY, UNSUPPORTED_DECIDE_BODY, buckets);
|
|
37
|
-
if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
|
|
38
|
-
return null;
|
|
39
|
-
buckets.unknown.sort();
|
|
40
|
-
buckets.unsupported.sort();
|
|
41
|
-
const parts = [
|
|
42
|
-
...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a decide-request key — check the spelling)")] : []),
|
|
43
|
-
...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (a sibling approval door's field, or a path parameter — never read from this body)")] : []),
|
|
44
|
-
];
|
|
45
|
-
return {
|
|
46
|
-
message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
|
|
47
|
-
`(a misspelled \`remember\` or \`boundInputHash\` silently turns off an exemption / the decision binding). ` +
|
|
48
|
-
`${parts.join("; ")}. ` +
|
|
49
|
-
`Accepted: ${[...ACCEPTED_DECIDE_BODY].sort().join(",")}`,
|
|
50
|
-
unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
|
|
51
|
-
unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
30
|
const PLAN_REVIEW_BODY_DECLARED_KEYS = [
|
|
55
31
|
"boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
|
|
56
32
|
];
|
|
@@ -71,29 +47,108 @@ function buildApprovalEnvelope(sessionId, covered, reading) {
|
|
|
71
47
|
Object.assign(envelope, { [PROOF_SLOT_OF[key]]: reading[key] });
|
|
72
48
|
return envelope;
|
|
73
49
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
50
|
+
const DECISION_DOOR_FAMILY = {
|
|
51
|
+
"approval-decide": {
|
|
52
|
+
accepted: DECIDE_BODY_DECLARED_KEYS,
|
|
53
|
+
requestNoun: "decide-request",
|
|
54
|
+
costliestTypo: "a misspelled `remember` or `boundInputHash` silently turns off an exemption / the decision binding",
|
|
55
|
+
},
|
|
56
|
+
"assistant-task-plan-review": {
|
|
57
|
+
accepted: PLAN_REVIEW_BODY_DECLARED_KEYS,
|
|
58
|
+
requestNoun: "plan-review-request",
|
|
59
|
+
costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task read-only after you approved its plan",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const SUBMIT_BODY_NEAR_MISS_KEYS = ["permissionMode"];
|
|
63
|
+
const SUBMIT_ENDPOINT = "POST /v1/tasks";
|
|
64
|
+
function neighborLabel(n) {
|
|
65
|
+
const where = n.where.join(" / ");
|
|
66
|
+
switch (n.family) {
|
|
67
|
+
case "sibling_field":
|
|
68
|
+
return `unsupported (a body field of ${where} — send it there, this door never reads it)`;
|
|
69
|
+
case "path_segment":
|
|
70
|
+
return `unsupported (a path segment of ${where} — identity travels in the URL, never in a body)`;
|
|
71
|
+
case "submit_body":
|
|
72
|
+
return `unsupported (a submit-body key of ${where} — set when the task is submitted, this door never reads it)`;
|
|
73
|
+
default: {
|
|
74
|
+
const unreachable = n.family;
|
|
75
|
+
throw new Error(`decision-door key closure: unknown neighbor family ${String(unreachable)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function doorEndpointOf(routes, id) {
|
|
80
|
+
const decl = routes.find((r) => "id" in r && r.id === id);
|
|
81
|
+
if (decl === undefined || !("pattern" in decl) || decl.label === undefined) {
|
|
82
|
+
throw new Error(`decision-door family: route "${id}" is not a labelled pattern row of the route table — the key-closure hints are derived from it`);
|
|
83
|
+
}
|
|
84
|
+
const groups = new RegExp(`${decl.pattern.source}|`, decl.pattern.flags).exec("")?.groups ?? {};
|
|
85
|
+
return { endpoint: `${decl.methods.join("|")} ${decl.label}`, pathSegments: Object.keys(groups) };
|
|
86
|
+
}
|
|
87
|
+
export function deriveDecisionDoorKeyTable(family, routes) {
|
|
88
|
+
const ids = Object.keys(family);
|
|
89
|
+
const endpoint = new Map(ids.map((id) => [id, doorEndpointOf(routes, id)]));
|
|
90
|
+
const collect = (pairs) => {
|
|
91
|
+
const m = new Map();
|
|
92
|
+
for (const [k, where] of pairs)
|
|
93
|
+
m.set(k, [...(m.get(k) ?? []), where]);
|
|
94
|
+
return m;
|
|
95
|
+
};
|
|
96
|
+
const siblingWhere = collect(ids.flatMap((id) => family[id].accepted.map((k) => [k, endpoint.get(id).endpoint])));
|
|
97
|
+
const pathWhere = collect(ids.flatMap((id) => endpoint.get(id).pathSegments.map((k) => [k, endpoint.get(id).endpoint])));
|
|
98
|
+
const submitWhere = collect(SUBMIT_BODY_NEAR_MISS_KEYS.map((k) => [k, SUBMIT_ENDPOINT]));
|
|
99
|
+
const sources = [
|
|
100
|
+
["sibling_field", siblingWhere],
|
|
101
|
+
["path_segment", pathWhere],
|
|
102
|
+
["submit_body", submitWhere],
|
|
103
|
+
];
|
|
104
|
+
for (let i = 0; i < sources.length; i++) {
|
|
105
|
+
for (let j = i + 1; j < sources.length; j++) {
|
|
106
|
+
const [fa, a] = sources[i];
|
|
107
|
+
const [fb, b] = sources[j];
|
|
108
|
+
const both = [...a.keys()].filter((k) => b.has(k));
|
|
109
|
+
if (both.length > 0)
|
|
110
|
+
throw new Error(`decision-door family: key(s) ${both.join(", ")} belong to both ${fa} and ${fb} — a refused key must have exactly one destination`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const derive = (self) => {
|
|
114
|
+
const accepted = new Set(family[self].accepted);
|
|
115
|
+
const neighbors = new Map();
|
|
116
|
+
for (const [fam, source] of sources) {
|
|
117
|
+
for (const [k, where] of source)
|
|
118
|
+
if (!accepted.has(k))
|
|
119
|
+
neighbors.set(k, { family: fam, where });
|
|
120
|
+
}
|
|
121
|
+
return { accepted, neighbors, requestNoun: family[self].requestNoun, costliestTypo: family[self].costliestTypo };
|
|
122
|
+
};
|
|
123
|
+
return { "approval-decide": derive("approval-decide"), "assistant-task-plan-review": derive("assistant-task-plan-review") };
|
|
124
|
+
}
|
|
125
|
+
export function decisionBodyKeyIssue(door, raw, table = DECISION_DOOR_KEY_TABLE) {
|
|
80
126
|
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
81
127
|
return null;
|
|
128
|
+
const d = table[door];
|
|
82
129
|
const buckets = { unknown: [], unsupported: [] };
|
|
83
|
-
scanKeyClosure(raw, "",
|
|
130
|
+
scanKeyClosure(raw, "", d.accepted, new Set(d.neighbors.keys()), buckets);
|
|
84
131
|
if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
|
|
85
132
|
return null;
|
|
86
133
|
buckets.unknown.sort();
|
|
87
134
|
buckets.unsupported.sort();
|
|
135
|
+
const byLabel = new Map();
|
|
136
|
+
for (const k of buckets.unsupported) {
|
|
137
|
+
const n = d.neighbors.get(k);
|
|
138
|
+
if (n === undefined)
|
|
139
|
+
throw new Error(`decision-door key closure: "${k}" was classified unsupported but has no destination`);
|
|
140
|
+
const label = neighborLabel(n);
|
|
141
|
+
byLabel.set(label, [...(byLabel.get(label) ?? []), k]);
|
|
142
|
+
}
|
|
88
143
|
const parts = [
|
|
89
|
-
...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown,
|
|
90
|
-
...
|
|
144
|
+
...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, `unknown (not a ${d.requestNoun} key — check the spelling)`)] : []),
|
|
145
|
+
...[...byLabel].map(([label, keys]) => keyClosureClause(keys, label)),
|
|
91
146
|
];
|
|
92
147
|
return {
|
|
93
148
|
message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
|
|
94
|
-
`(
|
|
149
|
+
`(${d.costliestTypo}). ` +
|
|
95
150
|
`${parts.join("; ")}. ` +
|
|
96
|
-
`Accepted: ${[...
|
|
151
|
+
`Accepted: ${[...d.accepted].sort().join(",")}`,
|
|
97
152
|
unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
|
|
98
153
|
unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
|
|
99
154
|
};
|
|
@@ -437,7 +492,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
437
492
|
return;
|
|
438
493
|
}
|
|
439
494
|
{
|
|
440
|
-
const keyIssue =
|
|
495
|
+
const keyIssue = decisionBodyKeyIssue("assistant-task-plan-review", body);
|
|
441
496
|
if (keyIssue) {
|
|
442
497
|
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
443
498
|
return;
|
|
@@ -570,7 +625,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
570
625
|
return;
|
|
571
626
|
}
|
|
572
627
|
{
|
|
573
|
-
const keyIssue =
|
|
628
|
+
const keyIssue = decisionBodyKeyIssue("approval-decide", body);
|
|
574
629
|
if (keyIssue) {
|
|
575
630
|
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
576
631
|
return;
|
|
@@ -637,18 +692,29 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
637
692
|
sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
|
|
638
693
|
return;
|
|
639
694
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
695
|
+
const settledByVerdict = judgeSettledByBody(body.settledBy, decision, deps.config.directDoorActive);
|
|
696
|
+
switch (settledByVerdict.kind) {
|
|
697
|
+
case "not_in_proof":
|
|
698
|
+
sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
|
|
699
|
+
return;
|
|
700
|
+
case "deny_only":
|
|
701
|
+
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
702
|
+
return;
|
|
703
|
+
case "not_string":
|
|
704
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
705
|
+
return;
|
|
706
|
+
case "too_wide":
|
|
707
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
|
|
708
|
+
return;
|
|
709
|
+
case "absent":
|
|
710
|
+
case "accepted":
|
|
711
|
+
break;
|
|
712
|
+
default: {
|
|
713
|
+
const unreachable = settledByVerdict;
|
|
714
|
+
throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
|
|
715
|
+
}
|
|
651
716
|
}
|
|
717
|
+
const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
|
|
652
718
|
const binding = {
|
|
653
719
|
checkpointToken: body.checkpointToken,
|
|
654
720
|
boundCallId: body.boundCallId,
|
|
@@ -697,7 +763,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
697
763
|
pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
|
|
698
764
|
}
|
|
699
765
|
: undefined;
|
|
700
|
-
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(
|
|
766
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
|
|
701
767
|
const outErrorCode = out.body.errorCode;
|
|
702
768
|
if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
|
|
703
769
|
try {
|
|
@@ -832,4 +898,5 @@ export const APPROVALS_ASSISTANT_ROUTES = [
|
|
|
832
898
|
{ id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: true, methods: ["POST"] },
|
|
833
899
|
{ prefix: "/v1/approvals", billable: true },
|
|
834
900
|
];
|
|
901
|
+
const DECISION_DOOR_KEY_TABLE = deriveDecisionDoorKeyTable(DECISION_DOOR_FAMILY, APPROVALS_ASSISTANT_ROUTES);
|
|
835
902
|
//# sourceMappingURL=approvals-assistant.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
2
|
import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
|
|
3
3
|
import { resolveModelApiKey } from "../../key-resolver.js";
|
|
4
|
-
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
4
|
+
import { isSealedKeyPoison, projectSealedKeyCapability } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
6
6
|
import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
|
|
7
7
|
import { autonomyOverrides } from "../../runtime-governance.js";
|
|
@@ -176,7 +176,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
176
176
|
rewindFiles: Boolean(deps.fileHistoryStore),
|
|
177
177
|
restoreFiles: Boolean(deps.fileHistoryStore),
|
|
178
178
|
rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
|
|
179
|
-
fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore),
|
|
179
|
+
fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore, deps.config.fileHistoryCapture),
|
|
180
180
|
scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
|
|
181
181
|
sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
|
|
182
182
|
sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
|
|
@@ -199,6 +199,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
199
199
|
})(),
|
|
200
200
|
manualCompact: Boolean(deps.runStore),
|
|
201
201
|
configCatalog: deps.config.configCatalogEnabled === true && deps.config.operatorPrincipals.length > 0,
|
|
202
|
+
sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
|
|
202
203
|
});
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
@@ -23,6 +23,7 @@ export async function handleConfigCatalog(req, res, match, ctx) {
|
|
|
23
23
|
sendJson(res, 200, buildConfigCatalog({
|
|
24
24
|
config: deps.config,
|
|
25
25
|
env: process.env,
|
|
26
|
+
boot: { ...(deps.memoryConsolidationValve !== undefined ? { memoryConsolidationValve: deps.memoryConsolidationValve } : {}) },
|
|
26
27
|
knobs: configKnobTable(),
|
|
27
28
|
centerDomains: DOMAINS,
|
|
28
29
|
centerManagedKeys: deps.configCenterManagedKeys?.(),
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* ── 为什么缺席形是 **404 族**而不是 501(D4)───────────────────────────────────────────────────────
|
|
13
13
|
* 本仓的 501 `capability.*` 说的是「**这家店**在本部署上不存在,换部署形态才行」(记忆引擎没接线、
|
|
14
|
-
* 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off
|
|
15
|
-
*
|
|
14
|
+
* 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`),或
|
|
15
|
+
* 出厂缺省派生成关(S-824 / [ref]:未设而整理座落主模型 —— 起服时有一行提示点名 `MODEL_CHEAP_ID`),不是部署能力的
|
|
16
|
+
* 缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
|
|
16
17
|
* 归属 / provenance=off)在**启动期**就被拒了(`boot/memory-consolidation.ts` 的
|
|
17
18
|
* `assertConsolidationDriverWirable`)—— 那些机器根本起不来,不会有一个"挂着的 501"去代表它们。
|
|
18
19
|
* ⇒ 剩下的唯一缺席成因就是「阀门没开」,而它的诚实形是**这条路径不存在**:挂载条件写在 server.ts 的
|
|
@@ -2,7 +2,7 @@ import { formatUserScope } from "@sema-agent/core";
|
|
|
2
2
|
import { parseMemorySyncRequest } from "../../memory-sync.js";
|
|
3
3
|
import { sendJson, sendError } from "../send.js";
|
|
4
4
|
import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
|
|
5
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
5
|
+
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
6
6
|
import { assertNever } from "../route-ctx.js";
|
|
7
7
|
const ORG_PREFIX = "org:";
|
|
8
8
|
export const MEMORY_EXPORT_MAX_LIMIT = 500;
|
|
@@ -68,7 +68,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
|
|
|
68
68
|
case "memory-export": {
|
|
69
69
|
const principal = gatedPrincipal(req, deps.config);
|
|
70
70
|
if (!principal) {
|
|
71
|
-
|
|
71
|
+
sendUnauthorized(res, "identity");
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
|
|
@@ -128,7 +128,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
|
|
|
128
128
|
case "memory-sync-scope": {
|
|
129
129
|
const principal = gatedPrincipal(req, deps.config);
|
|
130
130
|
if (!principal) {
|
|
131
|
-
|
|
131
|
+
sendUnauthorized(res, "identity");
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { sendJson, sendError, httpErrorCode } from "../send.js";
|
|
3
|
-
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
3
|
+
import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
5
|
import { createLogger } from "../../observability/logger.js";
|
|
6
6
|
import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
@@ -90,7 +90,7 @@ export async function handleRules(req, res, match, ctx) {
|
|
|
90
90
|
return;
|
|
91
91
|
const principal = gatedPrincipal(req, deps.config);
|
|
92
92
|
if (principal === undefined) {
|
|
93
|
-
|
|
93
|
+
sendUnauthorized(res, "identity");
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
const lane = deps.ruleConsent;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -16,7 +16,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
16
16
|
import { streamSseLog } from "../sse-log.js";
|
|
17
17
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
18
18
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
19
|
-
import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
19
|
+
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
20
|
import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
|
|
21
21
|
import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
|
|
22
22
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
@@ -45,6 +45,7 @@ const AskDecisionBodySchema = z
|
|
|
45
45
|
.refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
|
|
46
46
|
.optional(),
|
|
47
47
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
48
|
+
settledBy: z.unknown().optional(),
|
|
48
49
|
})
|
|
49
50
|
.strict();
|
|
50
51
|
const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
|
|
@@ -1287,10 +1288,33 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1287
1288
|
}
|
|
1288
1289
|
const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
|
|
1289
1290
|
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");
|
|
1291
|
+
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
|
|
1291
1292
|
return;
|
|
1292
1293
|
}
|
|
1293
1294
|
const body = parsedBody.data;
|
|
1295
|
+
const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
|
|
1296
|
+
switch (settledByVerdict.kind) {
|
|
1297
|
+
case "not_in_proof":
|
|
1298
|
+
sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
|
|
1299
|
+
return;
|
|
1300
|
+
case "deny_only":
|
|
1301
|
+
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
1302
|
+
return;
|
|
1303
|
+
case "not_string":
|
|
1304
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
1305
|
+
return;
|
|
1306
|
+
case "too_wide":
|
|
1307
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
|
|
1308
|
+
return;
|
|
1309
|
+
case "absent":
|
|
1310
|
+
case "accepted":
|
|
1311
|
+
break;
|
|
1312
|
+
default: {
|
|
1313
|
+
const unreachable = settledByVerdict;
|
|
1314
|
+
throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
|
|
1294
1318
|
const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
|
|
1295
1319
|
const taskId = match.params.taskId;
|
|
1296
1320
|
const askId = match.params.askId;
|
|
@@ -1424,6 +1448,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1424
1448
|
decisionActor: actor,
|
|
1425
1449
|
decisionNote: body.note ?? null,
|
|
1426
1450
|
idempotencyKey: body.idempotencyKey ?? null,
|
|
1451
|
+
...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
|
|
1427
1452
|
...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
|
|
1428
1453
|
});
|
|
1429
1454
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
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;
|
|
@@ -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);
|
|
@@ -459,20 +459,14 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
catch (e) {
|
|
462
|
+
await pending.handle.abort().catch(() => undefined);
|
|
463
|
+
cleanupStaging(stagingId);
|
|
462
464
|
if (e instanceof PrecheckError) {
|
|
463
|
-
await pending.handle.abort().catch(() => undefined);
|
|
464
|
-
cleanupStaging(stagingId);
|
|
465
465
|
sendError(res, e.status, "invalid_entries", e.message);
|
|
466
466
|
return;
|
|
467
467
|
}
|
|
468
|
-
if (
|
|
469
|
-
await pending.handle.abort().catch(() => undefined);
|
|
470
|
-
cleanupStaging(stagingId);
|
|
471
|
-
sendError(res, 422, "invalid_entries", "staged session log failed validation");
|
|
468
|
+
if (sendSessionError(res, e, "import"))
|
|
472
469
|
return;
|
|
473
|
-
}
|
|
474
|
-
await pending.handle.abort().catch(() => undefined);
|
|
475
|
-
cleanupStaging(stagingId);
|
|
476
470
|
throw e;
|
|
477
471
|
}
|
|
478
472
|
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) {
|
|
@@ -7,9 +7,10 @@ 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
9
|
import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
10
|
-
import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
|
-
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.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);
|
|
@@ -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;
|