@sema-agent/server 7.125.1 → 7.126.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 +195 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +263 -140
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +1 -1
- package/deploy/sema-up/chart/values.yaml +12 -0
- package/deploy/sema-up/cluster-up.sh +10 -10
- package/deploy/sema-up/kube-up.sh +11 -10
- package/deploy/sema-up/lib/dataroot.sh +1 -1
- package/deploy/sema-up/lib/envf.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +5 -5
- package/deploy/sema-up/lib/lanes.py +1 -1
- package/deploy/sema-up/lib/parse-machines.py +2 -2
- package/deploy/sema-up/lib/shell-access.sh +1 -1
- package/deploy/sema-up/preflight.sh +4 -4
- package/deploy/sema-up/sema-up.sh +33 -33
- package/deploy/sema-up/smoke.sh +3 -3
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
- package/dist/approval-card.d.ts +20 -4
- package/dist/approval-content-kind.d.ts +38 -22
- package/dist/approval-content-kind.js +20 -4
- package/dist/audit.js +2 -0
- package/dist/bench/s1/arms.d.ts +9 -37
- package/dist/bench/s1/arms.js +4 -61
- package/dist/bench/s1/live-deps.d.ts +4 -10
- package/dist/bench/s1/live-deps.js +1 -222
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/reviewer.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +5 -7
- package/dist/bench/s1/row.js +0 -2
- package/dist/bench/s1/run-firm.d.ts +2 -2
- package/dist/bench/s1/run-firm.js +1 -4
- package/dist/bench/s1/runner-ctx.d.ts +17 -29
- package/dist/bench/s1/runner-ctx.js +3 -7
- package/dist/bench/s1/tasks.d.ts +9 -46
- package/dist/bench/s1/tasks.js +4 -63
- package/dist/boot/execution-env.d.ts +38 -26
- package/dist/boot/execution-env.js +71 -38
- package/dist/boot/memory-boundary.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +0 -3
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.d.ts +0 -2
- package/dist/boot/resolve-spec.js +12 -17
- package/dist/boot/retention-lane.d.ts +12 -1
- package/dist/boot/retention-lane.js +2 -0
- package/dist/boot/runner-deps.d.ts +5 -5
- package/dist/boot/runner-deps.js +5 -16
- package/dist/boot/session-faces.d.ts +5 -1
- package/dist/boot/session-faces.js +37 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -0
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-05-execution-env.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.js +7 -4
- package/dist/boot/stage-06-runners.d.ts +2 -3
- package/dist/boot/stage-06-runners.js +8 -7
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +3 -4
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +4 -3
- package/dist/boot/web-admin.d.ts +5 -0
- package/dist/boot/web-admin.js +28 -14
- package/dist/brain.d.ts +2 -2
- package/dist/brain.js +1 -1
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +27 -22
- package/dist/capabilities/memory-notice.js +15 -17
- package/dist/capabilities/scenarios.d.ts +9 -14
- package/dist/capabilities/scenarios.js +5 -50
- package/dist/capability-facts.d.ts +22 -11
- package/dist/capability-facts.js +13 -6
- package/dist/config-catalog.js +11 -4
- package/dist/config-provider.d.ts +5 -1
- package/dist/config-provider.js +23 -4
- package/dist/config-types.d.ts +14 -1
- package/dist/config.js +39 -6
- package/dist/deployment-governance.d.ts +19 -21
- package/dist/deployment-governance.js +7 -8
- package/dist/env-name-allowlist-knobs.d.ts +5 -5
- package/dist/env-name-allowlist-knobs.js +1 -0
- package/dist/execution-lane-caps.js +2 -2
- package/dist/hooks/hook-runner.d.ts +84 -30
- package/dist/hooks/hook-runner.js +74 -41
- package/dist/http/admission.js +12 -9
- package/dist/http/durable-run-leg.d.ts +5 -4
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/idempotency.d.ts +28 -6
- package/dist/http/idempotency.js +10 -2
- package/dist/http/principal-gate.d.ts +14 -0
- package/dist/http/principal-gate.js +7 -0
- package/dist/http/request-guards.d.ts +2 -1
- package/dist/http/request-guards.js +14 -14
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +5 -4
- package/dist/http/route-ctx.d.ts +8 -6
- package/dist/http/routes/a2a-serve.js +11 -7
- package/dist/http/routes/approvals-assistant.js +18 -3
- package/dist/http/routes/background.d.ts +1 -1
- package/dist/http/routes/background.js +1 -1
- package/dist/http/routes/capabilities.js +10 -2
- package/dist/http/routes/mcp-reconnect.js +1 -1
- package/dist/http/routes/runs.d.ts +4 -3
- package/dist/http/routes/runs.js +12 -13
- package/dist/http/routes/session-sync.js +14 -11
- package/dist/http/routes/sessions.js +9 -9
- package/dist/http/routes/side-query.js +6 -19
- package/dist/http/routes/tasks.js +12 -11
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +2 -2
- package/dist/http/submit-replay.d.ts +15 -8
- package/dist/http/submit-replay.js +6 -0
- package/dist/http/wire-types.d.ts +7 -9
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/internal-run-env.d.ts +30 -0
- package/dist/internal-run-env.js +11 -0
- package/dist/leader/wire.js +3 -2
- package/dist/memory-scope.d.ts +19 -7
- package/dist/memory-scope.js +25 -8
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/metrics.js +0 -3
- package/dist/observability/run-terminal.js +69 -4
- package/dist/permission-rule-vocab.d.ts +3 -2
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plan-cache-probe.d.ts +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
- package/dist/plugins/checkpoint-store-sql.js +57 -11
- package/dist/plugins/file-run-store.d.ts +25 -21
- package/dist/plugins/file-run-store.js +70 -34
- package/dist/plugins/local-checkpoint-store.d.ts +8 -5
- package/dist/plugins/local-checkpoint-store.js +5 -15
- package/dist/plugins/local-session-store.d.ts +66 -7
- package/dist/plugins/local-session-store.js +168 -101
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
- package/dist/plugins/memory-run-store.d.ts +19 -3
- package/dist/plugins/memory-run-store.js +38 -3
- package/dist/plugins/one-time-migrations.d.ts +141 -39
- package/dist/plugins/one-time-migrations.js +32 -143
- package/dist/plugins/pg-pool.js +11 -1
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +9 -5
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +1 -0
- package/dist/plugins/remote-env-e2b.d.ts +4 -0
- package/dist/plugins/remote-env-e2b.js +2 -1
- package/dist/plugins/remote-env-host.d.ts +12 -0
- package/dist/plugins/remote-env-host.js +11 -1
- package/dist/plugins/remote-env-k8s.d.ts +227 -39
- package/dist/plugins/remote-env-k8s.js +450 -75
- package/dist/plugins/remote-env-local-docker.d.ts +4 -0
- package/dist/plugins/remote-env-local-docker.js +2 -1
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/retention-store-sql.d.ts +4 -1
- package/dist/plugins/retention-store-sql.js +11 -9
- package/dist/plugins/run-store-sql.d.ts +44 -6
- package/dist/plugins/run-store-sql.js +92 -27
- package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/store-backend.js +1 -1
- package/dist/plugins/store-contracts.d.ts +17 -3
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-storage.js +3 -2
- package/dist/plugins/tidb-session-store.d.ts +2 -0
- package/dist/plugins/tidb-session-store.js +6 -2
- package/dist/request-key-closure.d.ts +12 -0
- package/dist/request-key-closure.js +3 -3
- package/dist/router/route-orchestration.d.ts +3 -11
- package/dist/router/route-orchestration.js +0 -4
- package/dist/rule-entry-list.js +0 -4
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +5 -4
- package/dist/run-local.js +23 -17
- package/dist/runs.js +1 -0
- package/dist/security.d.ts +7 -0
- package/dist/security.js +5 -0
- package/dist/server-secret-env.d.ts +10 -4
- package/dist/server-secret-env.js +4 -1
- package/dist/session-incarnation.d.ts +46 -2
- package/dist/session-incarnation.js +14 -3
- package/dist/session-sync-kernel.d.ts +10 -3
- package/dist/spec-fields.d.ts +24 -8
- package/dist/spec-fields.js +21 -2
- package/dist/task-workflow.d.ts +6 -4
- package/dist/task-workflow.js +3 -2
- package/dist/tool-approval.d.ts +78 -6
- package/dist/tool-approval.js +7 -3
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/engine-notice-wire.d.ts +26 -1
- package/dist/trace/engine-notice-wire.js +7 -2
- package/dist/trace/project.d.ts +8 -3
- package/dist/trace/project.js +15 -1
- package/dist/trace/self-minted-wire.d.ts +104 -2
- package/dist/trace/self-minted-wire.js +35 -0
- package/dist/web-admin-client/apply-effective.d.ts +13 -7
- package/dist/web-admin-client/apply-effective.js +11 -26
- package/dist/web-admin-client/apply-ledger.d.ts +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +39 -4
- package/dist/web-admin-client/candidate-gate.js +9 -2
- package/dist/web-admin-client/facade.d.ts +2 -2
- package/dist/web-admin-client/http-client.d.ts +3 -1
- package/dist/web-admin-client/http-client.js +4 -3
- package/dist/web-admin-client/read-warnings.d.ts +33 -5
- package/dist/web-admin-client/read-warnings.js +41 -6
- package/dist/web-admin-client/types.d.ts +11 -5
- package/dist/workspace-cache.d.ts +45 -0
- package/dist/workspace-cache.js +30 -0
- package/dist/workspace-space-commit.d.ts +116 -0
- package/dist/workspace-space-commit.js +117 -0
- package/dist/workspace-space.d.ts +75 -9
- package/dist/workspace-space.js +52 -1
- package/package.json +4 -4
- package/dist/boot/session-incarnation-seat.d.ts +0 -41
- package/dist/boot/session-incarnation-seat.js +0 -69
- package/dist/capabilities/code-review-council.d.ts +0 -49
- package/dist/capabilities/code-review-council.js +0 -111
- package/dist/capabilities/collab-wire.d.ts +0 -36
- package/dist/capabilities/collab-wire.js +0 -2
- package/dist/capabilities/collab-workflows.d.ts +0 -47
- package/dist/capabilities/collab-workflows.js +0 -90
- package/dist/capabilities/prompts/code-review.d.ts +0 -25
- package/dist/capabilities/prompts/code-review.js +0 -39
- package/dist/capabilities/prompts/team.d.ts +0 -10
- package/dist/capabilities/prompts/team.js +0 -6
- package/dist/capabilities/team.d.ts +0 -113
- package/dist/capabilities/team.js +0 -70
|
@@ -84,7 +84,8 @@ export declare function createRequestGuards(ctx: {
|
|
|
84
84
|
fleetWide: boolean;
|
|
85
85
|
gateOwner: string | null;
|
|
86
86
|
};
|
|
87
|
-
runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null
|
|
87
|
+
runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
|
|
88
|
+
sessionOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
|
|
88
89
|
runSessionAcceptOk: (req: IncomingMessage, res: ServerResponse, run: {
|
|
89
90
|
sessionId: string | null;
|
|
90
91
|
}, face: string, notFoundError?: string) => boolean;
|
|
@@ -2,7 +2,7 @@ import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
|
2
2
|
import { HttpError, principalFrom } from "../security.js";
|
|
3
3
|
import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
|
|
4
4
|
import { sendError } from "./send.js";
|
|
5
|
-
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
|
|
5
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sessionOwnerReadable, devOpenDeployment, sendPrincipalRefusal } from "./principal-gate.js";
|
|
6
6
|
import { quotaWindowResetsAt } from "../observability/cost-quota.js";
|
|
7
7
|
const MAX_BODY = 8 * 1024 * 1024;
|
|
8
8
|
export function leaseRefusalOf(adm) {
|
|
@@ -150,9 +150,8 @@ export function createRequestGuards(ctx) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
function isFleetWide(req) {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
|
|
153
|
+
const tt = deps.config.traceToken;
|
|
154
|
+
return devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined || (!!tt && authorized(req, tt));
|
|
156
155
|
}
|
|
157
156
|
function sessionOwnerScope(req) {
|
|
158
157
|
const fleetWide = isFleetWide(req);
|
|
@@ -160,27 +159,28 @@ export function createRequestGuards(ctx) {
|
|
|
160
159
|
return { fleetWide, gateOwner };
|
|
161
160
|
}
|
|
162
161
|
function sessionOwnerScopeForWrite(req) {
|
|
163
|
-
const
|
|
164
|
-
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
165
|
-
const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
|
|
162
|
+
const fleetWide = devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined;
|
|
166
163
|
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
167
164
|
return { fleetWide, gateOwner };
|
|
168
165
|
}
|
|
169
|
-
function
|
|
166
|
+
function ownerGate(req, res, readable, notFound) {
|
|
170
167
|
const principal = gatedPrincipal(req, deps.config);
|
|
171
168
|
if (deps.config.requirePrincipal && !principal) {
|
|
172
169
|
sendPrincipalRefusal(res, req, deps.config);
|
|
173
170
|
return false;
|
|
174
171
|
}
|
|
175
|
-
if (!
|
|
176
|
-
|
|
177
|
-
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
178
|
-
else
|
|
179
|
-
sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
|
|
172
|
+
if (!readable(principal)) {
|
|
173
|
+
notFound();
|
|
180
174
|
return false;
|
|
181
175
|
}
|
|
182
176
|
return true;
|
|
183
177
|
}
|
|
178
|
+
function runOwnerOk(req, res, owner) {
|
|
179
|
+
return ownerGate(req, res, (principal) => runOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE));
|
|
180
|
+
}
|
|
181
|
+
function sessionOwnerOk(req, res, owner) {
|
|
182
|
+
return ownerGate(req, res, (principal) => sessionOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE));
|
|
183
|
+
}
|
|
184
184
|
const sessionAssertAbsentWarned = new Set();
|
|
185
185
|
function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
|
|
186
186
|
if (!run.sessionId)
|
|
@@ -229,6 +229,6 @@ export function createRequestGuards(ctx) {
|
|
|
229
229
|
}
|
|
230
230
|
return Buffer.concat(chunks);
|
|
231
231
|
}
|
|
232
|
-
return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody };
|
|
232
|
+
return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody };
|
|
233
233
|
}
|
|
234
234
|
//# sourceMappingURL=request-guards.js.map
|
|
@@ -15,7 +15,7 @@ import type { IncomingMessage } from "node:http";
|
|
|
15
15
|
import { CheckpointError, type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
|
|
16
16
|
import type { DecideBinding } from "./wire-types.js";
|
|
17
17
|
import type { CheckpointStoreFull } from "../plugins/store-backend.js";
|
|
18
|
-
import {
|
|
18
|
+
import type { HostDecision } from "../host-decision.js";
|
|
19
19
|
import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
|
|
20
20
|
import type { RouteCtxBase, DriveResumeArgs, HostLegOutcome, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
|
|
21
21
|
import { type TrustedRunInternals } from "./durable-run-leg.js";
|
|
@@ -74,7 +74,7 @@ export declare function createResumeLegs(ctx: {
|
|
|
74
74
|
readonly ok: true;
|
|
75
75
|
readonly prepared: PreparedPlanReview;
|
|
76
76
|
}>;
|
|
77
|
-
drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
|
|
77
|
+
drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, hostDecision: HostDecision, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
|
|
78
78
|
status: number;
|
|
79
79
|
body: object;
|
|
80
80
|
}>;
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -9,7 +9,6 @@ import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../se
|
|
|
9
9
|
import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
10
10
|
import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
|
|
11
11
|
import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../run-cancel-context.js";
|
|
12
|
-
import {} from "../host-decision.js";
|
|
13
12
|
import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
|
|
14
13
|
import { recordResumeResourceWindow } from "../resource-window.js";
|
|
15
14
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
@@ -68,6 +67,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
68
67
|
ledger_event_unknown: true,
|
|
69
68
|
real_approval_damaged: false,
|
|
70
69
|
real_approval_forged: false,
|
|
70
|
+
requirements_damaged: false,
|
|
71
71
|
read_face_damaged: false,
|
|
72
72
|
read_face_forged: false,
|
|
73
73
|
write_face_damaged: false,
|
|
@@ -162,7 +162,7 @@ function notResumableAnswer() {
|
|
|
162
162
|
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
163
163
|
}
|
|
164
164
|
export function createResumeLegs(ctx) {
|
|
165
|
-
const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
|
|
165
|
+
const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
|
|
166
166
|
const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
|
|
167
167
|
async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
|
|
168
168
|
const cs = deps.checkpointStore;
|
|
@@ -1377,7 +1377,7 @@ export function createResumeLegs(ctx) {
|
|
|
1377
1377
|
return refuse(refuseStoredBodyReplay(sessionId, replay));
|
|
1378
1378
|
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
1379
1379
|
}
|
|
1380
|
-
async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
|
|
1380
|
+
async function drivePlanReview(prepared, decision, editedPlan, reason, hostDecision, req, acceptEarly, permissionModeAfter) {
|
|
1381
1381
|
const { sessionId, token, cp, ctx } = prepared;
|
|
1382
1382
|
if ((await deps.checkpointStore.findPendingTokenBySession(sessionId, undefined, { gateKinds: ["plan_review"] })) !== token)
|
|
1383
1383
|
return notResumableAnswer();
|
|
@@ -1399,6 +1399,7 @@ export function createResumeLegs(ctx) {
|
|
|
1399
1399
|
decision,
|
|
1400
1400
|
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
1401
1401
|
...(reason ? { reason } : {}),
|
|
1402
|
+
hostDecision,
|
|
1402
1403
|
};
|
|
1403
1404
|
const out = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx: rebuildCtx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1404
1405
|
return out;
|
|
@@ -1406,7 +1407,7 @@ export function createResumeLegs(ctx) {
|
|
|
1406
1407
|
const routeCtxBase = {
|
|
1407
1408
|
deps,
|
|
1408
1409
|
registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
|
|
1409
|
-
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
1410
|
+
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
1410
1411
|
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
|
|
1411
1412
|
legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
|
|
1412
1413
|
};
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -72,11 +72,11 @@ export interface RouteHelpers {
|
|
|
72
72
|
usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
73
73
|
safeDecode(seg: string): string | null;
|
|
74
74
|
isFleetWide(req: IncomingMessage): boolean;
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
|
|
75
|
+
/** run 面的属主门(谓词 `runOwnerReadable`;非属主 ⇒ 404 `not_found.run` + 长文,与 run 读口的 unknown 臂同码同串,[ref])。 */
|
|
76
|
+
runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
|
|
77
|
+
/** 会话面的属主门(谓词 `sessionOwnerReadable`,S-1452:没有身份边界的部署不钉人,与会话列表同读;非属主 ⇒ 404 `not_found.session` +
|
|
78
|
+
* {@link SESSION_NOT_FOUND_MESSAGE},与会话读口的 unknown 臂同码同串 —— G52 [ref] / S-989)。每一个按会话寻址的属主判都走它。 */
|
|
79
|
+
sessionOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
|
|
80
80
|
sessionOwnerScope(req: IncomingMessage): {
|
|
81
81
|
fleetWide: boolean;
|
|
82
82
|
gateOwner: string | null;
|
|
@@ -461,7 +461,9 @@ export interface RouteLegs {
|
|
|
461
461
|
readonly prepared: PreparedPlanReview;
|
|
462
462
|
}>;
|
|
463
463
|
/** plan_review 三态腿的**驱动段**(S-609):只吃 prepare 的结果,用它选定的 token,不再选卡、不再读 ctx。 */
|
|
464
|
-
drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined,
|
|
464
|
+
drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined,
|
|
465
|
+
/** core 7.49.0 #1396:决策事实(谁决定的 + 归属),语义属主 = `resume-legs.ts` 的同名形参顶注。 */
|
|
466
|
+
hostDecision: HostDecision, req: IncomingMessage | undefined,
|
|
465
467
|
/** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
|
|
466
468
|
acceptEarly?: boolean,
|
|
467
469
|
/** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在把计划带出 plan 的决定
|
|
@@ -123,7 +123,11 @@ function asNonEmptyString(v, what, max) {
|
|
|
123
123
|
throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be at most ${max} characters`);
|
|
124
124
|
return v;
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
function asString(v, what) {
|
|
127
|
+
if (typeof v !== "string")
|
|
128
|
+
throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be a string`);
|
|
129
|
+
return v;
|
|
130
|
+
}
|
|
127
131
|
const MAX_MESSAGE_ID_CHARS = 200;
|
|
128
132
|
const MAX_TASK_ID_CHARS = 128;
|
|
129
133
|
const MAX_SKILL_ID_CHARS = 128;
|
|
@@ -224,7 +228,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
|
224
228
|
if (message.role !== "user")
|
|
225
229
|
throw new RpcError(JSONRPC_INVALID_PARAMS, `params.message.role must be "user" (this is the inbound leg — an agent-role message has no meaning here)`);
|
|
226
230
|
const objective = objectiveFromParts(message.parts);
|
|
227
|
-
const contextId = message.contextId === undefined ? undefined :
|
|
231
|
+
const contextId = message.contextId === undefined ? undefined : asString(message.contextId, "params.message.contextId");
|
|
228
232
|
const messageId = asNonEmptyString(message.messageId, "params.message.messageId", MAX_MESSAGE_ID_CHARS);
|
|
229
233
|
const skill = skillFromMetadata(message.metadata, serve);
|
|
230
234
|
const skillId = skill?.id;
|
|
@@ -238,19 +242,19 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
|
238
242
|
.update(skillId ?? "")
|
|
239
243
|
.digest("hex");
|
|
240
244
|
const submitter = caller.principal ?? undefined;
|
|
241
|
-
const
|
|
245
|
+
const idem = { key: idemKeyOf(fingerprint, caller, "run"), bodyFingerprint: fingerprint };
|
|
242
246
|
const body = {
|
|
243
247
|
objective,
|
|
244
248
|
taskId: durableIdempotencyTaskId(idemIdentityOf(caller), fingerprint),
|
|
245
249
|
...(contextId !== undefined ? { sessionId: contextId } : {}),
|
|
246
250
|
...(scenario !== undefined ? { scenario } : {}),
|
|
247
251
|
};
|
|
248
|
-
const keyed = (await runStore.getRunByIdem(
|
|
249
|
-
const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId }, "durable", deps, runStore) : undefined;
|
|
252
|
+
const keyed = (await runStore.getRunByIdem(idem.key).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
|
|
253
|
+
const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId, idem }, "durable", deps, runStore) : undefined;
|
|
250
254
|
if (keyed && replay?.status === 202) {
|
|
251
255
|
return taskFromRun(keyed, isStaleRun(keyed, deps.config.runStaleSec));
|
|
252
256
|
}
|
|
253
|
-
const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(
|
|
257
|
+
const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idem, "durable", body.taskId)) : undefined;
|
|
254
258
|
let receipt;
|
|
255
259
|
if (replay !== undefined) {
|
|
256
260
|
receipt = replay;
|
|
@@ -271,7 +275,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
|
271
275
|
throw rpcErrorFromTypedFailure(typed, skillId);
|
|
272
276
|
if (!prepared)
|
|
273
277
|
return undefined;
|
|
274
|
-
receipt = await createDurableRun(req, res, ctx, prepared,
|
|
278
|
+
receipt = await createDurableRun(req, res, ctx, prepared, idem);
|
|
275
279
|
}
|
|
276
280
|
if (!receipt)
|
|
277
281
|
return undefined;
|
|
@@ -31,10 +31,10 @@ const DECIDE_BODY_DECLARED_KEYS = [
|
|
|
31
31
|
const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
|
|
32
32
|
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
|
|
33
33
|
const PLAN_REVIEW_BODY_DECLARED_KEYS = [
|
|
34
|
-
"boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
|
|
34
|
+
"boundCallId", "decidedBy", "decision", "editedPlan", "permissionModeAfter", "reason",
|
|
35
35
|
];
|
|
36
36
|
const PLAN_REVIEW_PROOF_COVERED_KEYS = ["boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason"];
|
|
37
|
-
const PLAN_REVIEW_NOT_IN_PROOF_KEYS = [];
|
|
37
|
+
const PLAN_REVIEW_NOT_IN_PROOF_KEYS = ["decidedBy"];
|
|
38
38
|
const PROOF_SLOT_OF = {
|
|
39
39
|
answer: "answer",
|
|
40
40
|
boundCallId: "boundCallId",
|
|
@@ -522,6 +522,21 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
522
522
|
sendError(res, 400, "request.field_conflict", "decision 'edit' requires a non-empty editedPlan (the operator's revised plan)");
|
|
523
523
|
return;
|
|
524
524
|
}
|
|
525
|
+
const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
|
|
526
|
+
switch (decidedByReading.kind) {
|
|
527
|
+
case "not_in_proof":
|
|
528
|
+
sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
|
|
529
|
+
return;
|
|
530
|
+
case "outside_closed_set":
|
|
531
|
+
sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
|
|
532
|
+
return;
|
|
533
|
+
case "accepted":
|
|
534
|
+
break;
|
|
535
|
+
default: {
|
|
536
|
+
const unreachable = decidedByReading;
|
|
537
|
+
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
525
540
|
if (decision === "edit" && !isWellFormedUnicode(body.editedPlan)) {
|
|
526
541
|
sendError(res, 400, "request.field_invalid", "editedPlan must be well-formed Unicode text (a lone surrogate cannot be signed unambiguously)");
|
|
527
542
|
return;
|
|
@@ -626,7 +641,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
626
641
|
return;
|
|
627
642
|
}
|
|
628
643
|
}
|
|
629
|
-
const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
|
|
644
|
+
const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, hostDecisionFacts(decidedByReading.decidedBy, deciderPrincipal), req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
|
|
630
645
|
sendResumeOutcome(res, out, deps.logger);
|
|
631
646
|
return;
|
|
632
647
|
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `rateLimited`。能力位 `background: { listFace, exitFaces }` 与两口的 501 从同一个谓词推导(会话属主面在场)。
|
|
30
30
|
*
|
|
31
31
|
* 🔴 **停止口在无凭据写门内**(`credentialGated: ["POST"]`;车OL F1 = 7.102.0 合并复审 gpt-6 腿 [high]):门的两个合取项它都中
|
|
32
|
-
* —— ① 授权的唯一输入是 principal 头(属主判 `
|
|
32
|
+
* —— ① 授权的唯一输入是 principal 头(属主判 `sessionOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
|
|
33
33
|
* 无 service 凭证的部署上它收调用方自报的头);② 不可撤销、爆炸半径跨租户(知道会话 id + 自报它的属主 ⇒ 停掉那个会话的后台
|
|
34
34
|
* 进程,`includeRetained:true` 连显式 retain 的活服务一起停)。于是无 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` 的部署上它与
|
|
35
35
|
* 其余改写门同码同句 503 `auth.service_token_required`(在路由之前,`dispatch.ts` 那一道),同一只会话级收敛函数的另一个调用点
|
|
@@ -68,7 +68,7 @@ export async function handleBackground(req, res, match, ctx) {
|
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
71
|
-
if (!isOperator && !ctx.helpers.
|
|
71
|
+
if (!isOperator && !ctx.helpers.sessionOwnerOk(req, res, owner))
|
|
72
72
|
return;
|
|
73
73
|
const scope = owner ?? "default";
|
|
74
74
|
switch (id) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
2
|
import { modelImageInput, isAtMentionable, selectableCatalog } from "../../model-select.js";
|
|
3
3
|
import { buildDroppedEntriesWire } from "../../web-admin-client/read-warnings.js";
|
|
4
|
+
import { buildCandidateRejectedWire } from "../../web-admin-client/candidate-gate.js";
|
|
4
5
|
import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
|
|
5
6
|
import { projectSealedKeyCapability } from "../../sealed-key.js";
|
|
6
7
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
@@ -205,7 +206,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
205
206
|
manualCompact: facts.durableRuns(deps).available,
|
|
206
207
|
configCatalog: facts.configCatalog(deps).available,
|
|
207
208
|
sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
|
|
208
|
-
workspaceSync: ((
|
|
209
|
+
workspaceSync: ((space, cache) => space.available || cache.available ? { ...(space.available ? space.policy : {}), ...(cache.available ? { cache: cache.cache } : {}) } : undefined)(facts.workspaceSpace(deps.config), facts.workspaceCache(deps.config)),
|
|
209
210
|
}));
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
@@ -256,7 +257,14 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
256
257
|
}));
|
|
257
258
|
const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
|
|
258
259
|
const defaultName = (nonAlias.find(([, m]) => m === deps.config.model) ?? nonAlias.find(([, m]) => m.id === deps.config.model.id))?.[0];
|
|
259
|
-
|
|
260
|
+
const candidateRejection = deps.candidateRejectionState?.();
|
|
261
|
+
sendJson(res, 200, {
|
|
262
|
+
models,
|
|
263
|
+
default: deps.config.model.id,
|
|
264
|
+
defaultModel: { id: deps.config.model.id, ...(defaultName ? { name: defaultName } : {}) },
|
|
265
|
+
droppedEntries: buildDroppedEntriesWire(deps.config.droppedCatalogEntries),
|
|
266
|
+
...(candidateRejection !== undefined ? { candidateRejected: buildCandidateRejectedWire(candidateRejection) } : {}),
|
|
267
|
+
});
|
|
260
268
|
return;
|
|
261
269
|
}
|
|
262
270
|
default:
|
|
@@ -94,7 +94,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
|
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
97
|
-
if (!isOperator && !ctx.helpers.
|
|
97
|
+
if (!isOperator && !ctx.helpers.sessionOwnerOk(req, res, owner))
|
|
98
98
|
return;
|
|
99
99
|
const activeTaskId = await runStore.getActiveTaskId(sessionId);
|
|
100
100
|
const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type RunIdemBinding } from "../../plugins/store-contracts.js";
|
|
2
3
|
import { ActorAssertionWire } from "@sema-agent/settings-schema";
|
|
3
4
|
import type { RouteCtx, PreparedTaskSubmission, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
4
5
|
/** 行上的 `decision_actor`(裸 JSON)投成回显形;形不认 ⇒ undefined(调用点省略该键,不 500)。
|
|
@@ -31,9 +32,9 @@ export declare function handleRunVerbs(req: IncomingMessage, res: ServerResponse
|
|
|
31
32
|
* 防御性复检(缺席 ⇒ 501),但两个调用方都在自己的入口先判过,那才是各自面上的正确错误形。
|
|
32
33
|
*/
|
|
33
34
|
export declare function createDurableRun(req: IncomingMessage, res: ServerResponse, ctx: RouteCtx, prepared: PreparedTaskSubmission,
|
|
34
|
-
/**
|
|
35
|
-
* +
|
|
36
|
-
|
|
35
|
+
/** 这次提交的账本绑定(键 `idemKeyOf(…, "run")` + 请求体指纹:`POST /v1/runs` = `Idempotency-Key` 头 + 线上收到的体;A2A =
|
|
36
|
+
* `message.messageId` + 内容指纹)—— 落 `task_run.idem_key` / `idem_body_fingerprint`,键同时是同进程在飞合流的键。undefined = 调用方没带键。 */
|
|
37
|
+
idem: RunIdemBinding | undefined): Promise<{
|
|
37
38
|
status: number;
|
|
38
39
|
body: unknown;
|
|
39
40
|
} | null>;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { redactSecrets } from "../../trace/redact.js";
|
|
|
7
7
|
import { HttpError, isUuidV7, encodeCheckpointScope } from "../../security.js";
|
|
8
8
|
import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
|
|
9
9
|
import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
|
|
10
|
-
import { idemCallerOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
|
|
10
|
+
import { idemCallerOf, idemKeyOf, inflightKeyOf, submissionIdemOf } from "../idempotency.js";
|
|
11
11
|
import { streamSseLog } from "../sse-log.js";
|
|
12
12
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
13
13
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
|
|
@@ -188,19 +188,18 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
188
188
|
const runStore = runs.runStore;
|
|
189
189
|
const rawIdem = headerStr(req.headers["idempotency-key"]);
|
|
190
190
|
const submitter = gatedPrincipal(req, deps.config);
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
keyedBody = await readJson(req);
|
|
191
|
+
const keyedBody = rawIdem ? await readJson(req) : undefined;
|
|
192
|
+
const idem = rawIdem ? submissionIdemOf(rawIdem, idemCallerOf(req, ctx), keyedBody) : undefined;
|
|
193
|
+
if (idem) {
|
|
195
194
|
const clientTaskId = clientTaskIdOf(keyedBody);
|
|
196
195
|
if (clientTaskId === false) {
|
|
197
196
|
sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
|
|
198
197
|
return;
|
|
199
198
|
}
|
|
200
|
-
const inflight = idemCache.peek(inflightKeyOf(
|
|
201
|
-
const keyedRow = inflight ? undefined : await runStore.getRunByIdem(
|
|
199
|
+
const inflight = idemCache.peek(inflightKeyOf(idem, "durable", clientTaskId));
|
|
200
|
+
const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idem.key);
|
|
202
201
|
if (inflight || keyedRow) {
|
|
203
|
-
const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
|
|
202
|
+
const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
|
|
204
203
|
sendJson(res, resp.status, resp.body);
|
|
205
204
|
return;
|
|
206
205
|
}
|
|
@@ -210,7 +209,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
210
209
|
const prepared = await prepareSpec(req, res, keyedBody);
|
|
211
210
|
if (!prepared)
|
|
212
211
|
return;
|
|
213
|
-
const resp = await createDurableRun(req, res, ctx, prepared,
|
|
212
|
+
const resp = await createDurableRun(req, res, ctx, prepared, idem);
|
|
214
213
|
if (!resp)
|
|
215
214
|
return;
|
|
216
215
|
sendJson(res, resp.status, resp.body);
|
|
@@ -1586,7 +1585,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1586
1585
|
}
|
|
1587
1586
|
return assertNever(match.decl.id);
|
|
1588
1587
|
}
|
|
1589
|
-
export async function createDurableRun(req, res, ctx, prepared,
|
|
1588
|
+
export async function createDurableRun(req, res, ctx, prepared, idem) {
|
|
1590
1589
|
const { deps } = ctx;
|
|
1591
1590
|
const { idemCache } = ctx.registry;
|
|
1592
1591
|
const { leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
@@ -1603,7 +1602,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
|
|
|
1603
1602
|
return null;
|
|
1604
1603
|
}
|
|
1605
1604
|
const submitter = gatedPrincipal(req, deps.config);
|
|
1606
|
-
const caller = { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
|
|
1605
|
+
const caller = { principal: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
|
|
1607
1606
|
if (clientTaskId) {
|
|
1608
1607
|
const existing = await runStore.getRun(clientTaskId);
|
|
1609
1608
|
if (existing)
|
|
@@ -1611,11 +1610,11 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
|
|
|
1611
1610
|
}
|
|
1612
1611
|
if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
1613
1612
|
return null;
|
|
1614
|
-
return await idemCache.run(
|
|
1613
|
+
return await idemCache.run(idem !== undefined ? inflightKeyOf(idem, "durable", clientTaskId) : undefined, async () => {
|
|
1615
1614
|
const out = await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
|
|
1616
1615
|
prepared,
|
|
1617
1616
|
...(clientTaskId !== undefined ? { clientTaskId } : {}),
|
|
1618
|
-
...(
|
|
1617
|
+
...(idem !== undefined ? { idem } : {}),
|
|
1619
1618
|
fleetScope: submitter ?? "default",
|
|
1620
1619
|
elicitOwner: submitter ?? null,
|
|
1621
1620
|
source,
|
|
@@ -44,7 +44,6 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
const { backend, sessionStorage, historyStore } = sync;
|
|
47
|
-
const ownerOf = sessionStorage.ownerOf.bind(sessionStorage);
|
|
48
47
|
const scope = sessionOwnerScopeForWrite(req);
|
|
49
48
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
50
49
|
sendUnauthorized(res, "identity");
|
|
@@ -55,7 +54,8 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
55
54
|
sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
|
|
56
55
|
return;
|
|
57
56
|
}
|
|
58
|
-
const
|
|
57
|
+
const gateFace = await sessionStorage.sessionFaceOf(sessionId);
|
|
58
|
+
const owner = gateFace === undefined ? undefined : gateFace.owner;
|
|
59
59
|
const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
|
|
60
60
|
const pullGateOk = () => {
|
|
61
61
|
if (owner === undefined) {
|
|
@@ -75,13 +75,10 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
75
75
|
}
|
|
76
76
|
return true;
|
|
77
77
|
};
|
|
78
|
-
const replaySessionRow = async (row,
|
|
78
|
+
const replaySessionRow = async (row, face) => {
|
|
79
79
|
if (Object.keys(row).length === 0)
|
|
80
|
-
return;
|
|
81
|
-
|
|
82
|
-
if (face === undefined || face.owner !== importedOwner)
|
|
83
|
-
return;
|
|
84
|
-
await sessionStorage.patchSessionMeta(sessionId, row, face);
|
|
80
|
+
return true;
|
|
81
|
+
return face !== undefined && (await sessionStorage.patchSessionMeta(sessionId, row, face)) !== undefined;
|
|
85
82
|
};
|
|
86
83
|
if (match.decl.id === "session-sync-manifest") {
|
|
87
84
|
if (!pullGateOk())
|
|
@@ -331,7 +328,10 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
331
328
|
if (deps.resumeAnchorStore)
|
|
332
329
|
for (const a of anchors)
|
|
333
330
|
await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
|
|
334
|
-
await replaySessionRow(row,
|
|
331
|
+
if (!(await replaySessionRow(row, gateFace))) {
|
|
332
|
+
sendError(res, 409, "conflict", "sync conflict");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
335
|
sendJson(res, 200, {
|
|
336
336
|
relation: "identical",
|
|
337
337
|
basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
|
|
@@ -521,9 +521,13 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
521
521
|
if (anchorStore)
|
|
522
522
|
for (const a of pending.anchors)
|
|
523
523
|
await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
|
|
524
|
-
await replaySessionRow(pending.row,
|
|
524
|
+
const rowLanded = await replaySessionRow(pending.row, committed.face);
|
|
525
525
|
sessionStorage.evict?.(sessionId);
|
|
526
526
|
cleanupStaging(stagingId);
|
|
527
|
+
if (!rowLanded) {
|
|
528
|
+
sendError(res, 409, "conflict", "sync conflict");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
527
531
|
sendJson(res, 200, { relation: committed.relation });
|
|
528
532
|
}
|
|
529
533
|
catch (e) {
|
|
@@ -567,7 +571,6 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
567
571
|
function sendSessionSyncUnavailable(res, why) {
|
|
568
572
|
switch (why) {
|
|
569
573
|
case "store_backend_absent":
|
|
570
|
-
case "session_owner_face_absent":
|
|
571
574
|
case "session_row_faces_absent":
|
|
572
575
|
sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
|
|
573
576
|
return;
|
|
@@ -9,7 +9,7 @@ import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.
|
|
|
9
9
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
10
10
|
import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
11
11
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
12
|
-
import { gatedPrincipal, explicitOperatorOk,
|
|
12
|
+
import { gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
13
13
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
14
14
|
import * as facts from "../../capability-facts.js";
|
|
15
15
|
import { rosterAgentId } from "./agents-roster.js";
|
|
@@ -54,7 +54,7 @@ async function readLastLegMcp(deps, sessionId, principal) {
|
|
|
54
54
|
const latest = await runStore.latestRunForSession(sessionId);
|
|
55
55
|
if (latest === undefined)
|
|
56
56
|
return undefined;
|
|
57
|
-
if (!
|
|
57
|
+
if (!sessionOwnerReadable(principal, latest.owner, deps.config) && !explicitOperatorOk(principal, deps.config.operatorPrincipals))
|
|
58
58
|
return undefined;
|
|
59
59
|
const events = await runStore.getEvents(latest.taskId, 0);
|
|
60
60
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
@@ -70,7 +70,7 @@ async function readLastLegMcp(deps, sessionId, principal) {
|
|
|
70
70
|
}
|
|
71
71
|
export async function handleSessions(req, res, match, ctx) {
|
|
72
72
|
const { deps } = ctx;
|
|
73
|
-
const { readJson, safeDecode,
|
|
73
|
+
const { readJson, safeDecode, sessionOwnerOk, sessionOwnerScopeForWrite } = ctx.helpers;
|
|
74
74
|
const readOwnedSessionView = async (sessionAudit, sessionId, readOpts) => {
|
|
75
75
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
76
76
|
if (ownerOf !== undefined) {
|
|
@@ -79,7 +79,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
79
79
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
80
80
|
return undefined;
|
|
81
81
|
}
|
|
82
|
-
if (!
|
|
82
|
+
if (!sessionOwnerOk(req, res, owner))
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
85
|
let audit;
|
|
@@ -95,7 +95,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
95
95
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
96
96
|
return undefined;
|
|
97
97
|
}
|
|
98
|
-
if (!
|
|
98
|
+
if (!sessionOwnerOk(req, res, audit.owner))
|
|
99
99
|
return undefined;
|
|
100
100
|
return audit;
|
|
101
101
|
};
|
|
@@ -265,7 +265,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
265
265
|
return;
|
|
266
266
|
}
|
|
267
267
|
owner = head.owner;
|
|
268
|
-
if (!
|
|
268
|
+
if (!sessionOwnerOk(req, res, owner ?? null))
|
|
269
269
|
return;
|
|
270
270
|
leafId = head.leafId;
|
|
271
271
|
}
|
|
@@ -275,7 +275,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
275
275
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
|
-
if (!
|
|
278
|
+
if (!sessionOwnerOk(req, res, owner ?? null))
|
|
279
279
|
return;
|
|
280
280
|
leafId = await getLeafId(headSession);
|
|
281
281
|
}
|
|
@@ -359,7 +359,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
359
359
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
360
360
|
return;
|
|
361
361
|
}
|
|
362
|
-
if (!
|
|
362
|
+
if (!sessionOwnerOk(req, res, owner ?? null)) {
|
|
363
363
|
cleanup();
|
|
364
364
|
return;
|
|
365
365
|
}
|
|
@@ -666,7 +666,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
666
666
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
667
667
|
return;
|
|
668
668
|
}
|
|
669
|
-
if (!
|
|
669
|
+
if (!sessionOwnerOk(req, res, face.owner))
|
|
670
670
|
return;
|
|
671
671
|
const read = readSessionRowPatch(await readJson(req));
|
|
672
672
|
if ("problem" in read) {
|