@sema-agent/server 7.116.1 → 7.118.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 -0
- package/USAGE.md +65 -41
- package/dist/approval-card.d.ts +82 -18
- package/dist/approval-card.js +46 -4
- package/dist/approval.d.ts +4 -12
- package/dist/approval.js +4 -26
- package/dist/boot/budget-tracing.js +2 -3
- package/dist/boot/config-center.d.ts +2 -2
- package/dist/boot/config-center.js +5 -7
- package/dist/boot/parked-revive-gate.d.ts +1 -7
- package/dist/boot/parked-revive-gate.js +2 -12
- package/dist/boot/resolve-spec.js +18 -16
- package/dist/boot/runner-deps.d.ts +5 -3
- package/dist/boot/runner-deps.js +6 -4
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/brain.d.ts +17 -8
- package/dist/brain.js +10 -7
- package/dist/capabilities/hands-lane.d.ts +25 -0
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +2 -2
- package/dist/config-catalog.js +3 -2
- package/dist/config-center/apply-effective.d.ts +6 -16
- package/dist/config-center/apply-effective.js +0 -6
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +12 -16
- package/dist/config.d.ts +32 -29
- package/dist/config.js +20 -27
- package/dist/deployment-governance.d.ts +13 -19
- package/dist/deployment-governance.js +7 -14
- package/dist/governance-overrides.d.ts +20 -0
- package/dist/governance-overrides.js +8 -0
- package/dist/host-decision.d.ts +83 -25
- package/dist/host-decision.js +31 -1
- package/dist/http/admission.js +13 -7
- package/dist/http/approval-sweeps.js +16 -13
- package/dist/http/checkpoint-ctx.d.ts +83 -24
- package/dist/http/checkpoint-ctx.js +68 -8
- package/dist/http/mcp-dial-deadline.d.ts +35 -0
- package/dist/http/mcp-dial-deadline.js +21 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
- package/dist/http/memory-compliance-failure-reply.js +17 -8
- package/dist/http/memory-store-refusal-reply.d.ts +14 -11
- package/dist/http/memory-store-refusal-reply.js +16 -5
- package/dist/http/resume-legs.js +18 -18
- package/dist/http/retired-skill-manifest.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +21 -24
- package/dist/http/routes/capabilities.js +5 -3
- package/dist/http/routes/mcp-reconnect.d.ts +17 -0
- package/dist/http/routes/mcp-reconnect.js +6 -1
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +53 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -6
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/memory-store-refusal.d.ts +42 -17
- package/dist/memory-store-refusal.js +25 -6
- package/dist/model-select.d.ts +16 -5
- package/dist/model-select.js +7 -3
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +45 -39
- package/dist/namespaced-rule-name.js +36 -22
- package/dist/observability/corrupt-read-seat.d.ts +4 -0
- package/dist/observability/corrupt-read-seat.js +1 -0
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/observability/run-terminal.js +21 -1
- package/dist/operator-ask.d.ts +4 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
- package/dist/orchestration/workflow-agent-session-index.js +80 -14
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
- package/dist/orchestration/workflow-completion-inbox.js +39 -17
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +7 -2
- package/dist/outbound-dispatcher.d.ts +47 -20
- package/dist/outbound-dispatcher.js +198 -23
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-list.d.ts +20 -31
- package/dist/permission-rule-vocab.d.ts +4 -3
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
- package/dist/plugins/approval-ask-store-sql.js +0 -3
- package/dist/plugins/one-time-migrations.d.ts +25 -0
- package/dist/plugins/one-time-migrations.js +192 -6
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +20 -1
- package/dist/plugins/web-search.js +39 -10
- package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
- package/dist/plugins/workflow-run-store-sql.js +5 -7
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +1 -1
- package/dist/router/route-orchestration.d.ts +2 -23
- package/dist/rule-entry-list.d.ts +88 -0
- package/dist/rule-entry-list.js +102 -0
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +4 -2
- package/dist/run-local.js +4 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -0
- package/dist/runtime-governance.d.ts +5 -21
- package/dist/runtime-governance.js +4 -8
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -6
- package/dist/spec-fields.d.ts +13 -24
- package/dist/spec-fields.js +13 -17
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/task-settings.d.ts +18 -18
- package/dist/task-settings.js +63 -32
- package/dist/tool-approval.d.ts +77 -145
- package/dist/tool-approval.js +80 -62
- package/dist/trace/core-keyset-guard.d.ts +6 -6
- package/dist/trace/engine-notice-wire.d.ts +4 -6
- package/dist/trace/engine-notice-wire.js +14 -1
- package/dist/trace/self-minted-wire.d.ts +16 -2
- package/dist/trace/self-minted-wire.js +7 -0
- package/dist/user-input-ledger.d.ts +24 -12
- package/dist/user-input-ledger.js +33 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +103 -7
- package/scripts/drain-check.mysql.sql +59 -8
- package/scripts/drain-check.pg.sql +66 -10
package/dist/http/resume-legs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, taskNotificationDedupKey, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
|
|
4
|
-
import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
|
|
4
|
+
import { decideParkedAgent, denyWorkflowParkForked, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
|
|
5
5
|
const sessionFaceUnavailable = async (sessionId) => {
|
|
6
6
|
throw new Error(`parked decide cannot verify which occupant session ${sessionId} belongs to: this deployment's session store has no sessionFaceOf read (every built-in store has one) — refusing rather than resuming unverified`);
|
|
7
7
|
};
|
|
@@ -15,7 +15,7 @@ import { recordResumeResourceWindow } from "../resource-window.js";
|
|
|
15
15
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
16
16
|
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
|
|
17
17
|
import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
|
|
18
|
-
import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate,
|
|
18
|
+
import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
|
|
19
19
|
import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
|
|
20
20
|
import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
|
|
21
21
|
import { withPrincipal } from "../observability/principal-context.js";
|
|
@@ -42,7 +42,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
|
|
|
42
42
|
import { commitSessionInheritEnv } from "./session-inherit-env.js";
|
|
43
43
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
|
|
44
44
|
import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
|
|
45
|
-
import {
|
|
45
|
+
import { resumeBlockedByPolicyAnswer, storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "./checkpoint-ctx.js";
|
|
46
46
|
import { httpErrorCode } from "./send.js";
|
|
47
47
|
function isCheckpointReopenedFailure(r) {
|
|
48
48
|
const code = failureCodeOf(r.terminal);
|
|
@@ -274,7 +274,7 @@ export function createResumeLegs(ctx) {
|
|
|
274
274
|
const rememberableTool = paForGrant !== undefined &&
|
|
275
275
|
paForGrant.toolName &&
|
|
276
276
|
paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
277
|
-
|
|
277
|
+
standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
|
|
278
278
|
? paForGrant.toolName
|
|
279
279
|
: null;
|
|
280
280
|
const boundGrant = onResumeCommitted
|
|
@@ -345,6 +345,10 @@ export function createResumeLegs(ctx) {
|
|
|
345
345
|
void ctxPromise.catch(() => { });
|
|
346
346
|
return denyWorkflowParksUnreadable(origin);
|
|
347
347
|
}
|
|
348
|
+
if (origin?.kind === "workflow_park_forked") {
|
|
349
|
+
void ctxPromise.catch(() => { });
|
|
350
|
+
return denyWorkflowParkForked(origin);
|
|
351
|
+
}
|
|
348
352
|
if (origin?.kind === "workflow") {
|
|
349
353
|
void ctxPromise.catch(() => { });
|
|
350
354
|
const plan = planWorkflowParkedDecide(origin, parkedReq);
|
|
@@ -408,26 +412,21 @@ export function createResumeLegs(ctx) {
|
|
|
408
412
|
});
|
|
409
413
|
return { spec, engineNotices };
|
|
410
414
|
};
|
|
411
|
-
const
|
|
412
|
-
deps.logger?.warn?.("
|
|
413
|
-
return
|
|
415
|
+
const refuseStoredBodyReplay = (sessionId, refusal) => {
|
|
416
|
+
deps.logger?.warn?.("resume_refused_stored_body", { sessionId, ...storedBodyReplayLogFields(refusal) });
|
|
417
|
+
return storedBodyReplayAnswer(refusal);
|
|
414
418
|
};
|
|
415
419
|
const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
|
|
416
|
-
|
|
417
|
-
|
|
420
|
+
const replay = storedBodyReplayRefusal(ctx);
|
|
421
|
+
if (replay !== undefined)
|
|
422
|
+
return { ok: false, answer: refuseStoredBodyReplay(auth.sessionId, replay) };
|
|
418
423
|
try {
|
|
419
424
|
const { spec, engineNotices } = await resolveResumeSpec(ctx, req, auth);
|
|
420
425
|
return { ok: true, spec, engineNotices };
|
|
421
426
|
}
|
|
422
427
|
catch (e) {
|
|
423
428
|
if (e instanceof HttpError) {
|
|
424
|
-
return {
|
|
425
|
-
ok: false,
|
|
426
|
-
answer: {
|
|
427
|
-
status: 409,
|
|
428
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", blockedBy: httpErrorCode(e.status, e.code), ...(e.extra ?? {}) },
|
|
429
|
-
},
|
|
430
|
-
};
|
|
429
|
+
return { ok: false, answer: resumeBlockedByPolicyAnswer(e.message, httpErrorCode(e.status, e.code), e.extra) };
|
|
431
430
|
}
|
|
432
431
|
throw e;
|
|
433
432
|
}
|
|
@@ -1348,8 +1347,9 @@ export function createResumeLegs(ctx) {
|
|
|
1348
1347
|
return refuse(resumeCtxCorruptAnswer(ctxRead.where));
|
|
1349
1348
|
if (ctxRead.kind === "absent")
|
|
1350
1349
|
return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
|
|
1351
|
-
|
|
1352
|
-
|
|
1350
|
+
const replay = storedBodyReplayRefusal(ctxRead.ctx);
|
|
1351
|
+
if (replay !== undefined)
|
|
1352
|
+
return refuse(refuseStoredBodyReplay(sessionId, replay));
|
|
1353
1353
|
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
1354
1354
|
}
|
|
1355
1355
|
async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 「带」= 键在场、值是什么都算(`null` / 空对象同样是「我带了 manifest」)—— 与引擎的退役键表读法同形(`RETIRED_TASK_SPEC_KEYS["skills[].manifest"]`:`at !== undefined`)。
|
|
10
10
|
* 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
|
|
11
11
|
*
|
|
12
|
-
* 🔴 本模块**零运行期 import**(
|
|
12
|
+
* 🔴 本模块**零运行期 import**(随包脚本要在没有服务其余部分的位置加载它;`checkpoint-ctx.js` 7.117.0 起经 `rule-entry-list.js` 依赖引擎包根 —— 包形里有它)—— 往里加 import 要先想想脚本那一头。
|
|
13
13
|
*/
|
|
14
14
|
/** 一条技能(提交体 `skills[]` / `agents[].skills[]` 的元素)是否还带退役的 `manifest` 键。 */
|
|
15
15
|
export declare function skillCarriesRetiredManifest(skill: unknown): boolean;
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -162,9 +162,9 @@ export interface CheckpointCtx {
|
|
|
162
162
|
*/
|
|
163
163
|
unattendedLeg?: true;
|
|
164
164
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
165
|
+
* 体里 `settings.permissions.allow` 的**读法记号**(闭集只有当前读法一词 `"allow_layer"`):在场 = 这一行是当前读法下写下的、体里带非空 `allow`,
|
|
166
|
+
* 它按「交给引擎的 allow 层」写、也按它读。**服务端写、不来自请求体**(与 {@link unattendedLeg} 同为 `body` 的兄弟键)。
|
|
167
|
+
* 缺席(≤7.114.0,名单时代)或旧词 `"statement"`(7.115.0–7.116.x,表态时代)而体里带非空 `allow` ⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注)。
|
|
168
168
|
* 写点只有 `buildCheckpointCtx`(同模块;新的持久化写);续跑腿的写回传原 ctx / 其展开 ⇒ 记号随行。体里没有 `allow` 的行不写它。
|
|
169
169
|
*/
|
|
170
170
|
permissionsAllowReading?: import("./checkpoint-ctx.js").PermissionsAllowReading;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { screenedGate } from "../../trace/project.js";
|
|
2
|
-
import { hostDecisionFacts } from "../../host-decision.js";
|
|
2
|
+
import { hostDecisionFacts, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } 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,
|
|
5
|
+
import { APPROVAL_GATE_KINDS, standingAnswerDurablyRecordable } from "../../tool-approval.js";
|
|
6
6
|
import { isParkedRunStatus } from "../../plugins/store-contracts.js";
|
|
7
7
|
import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
|
|
8
8
|
import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
@@ -17,7 +17,7 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
|
17
17
|
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
18
18
|
import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
|
|
19
19
|
import { governanceOriginOf } from "../../runtime-governance.js";
|
|
20
|
-
import { answerGrantsBeyondCall } from "../../standing-answer-grant.js";
|
|
20
|
+
import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
|
|
21
21
|
import { sendResumeOutcome } from "../route-ctx.js";
|
|
22
22
|
import { assertNever } from "../route-ctx.js";
|
|
23
23
|
import * as facts from "../../capability-facts.js";
|
|
@@ -25,10 +25,10 @@ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
|
|
|
25
25
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
26
26
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
|
|
27
27
|
const DECIDE_BODY_DECLARED_KEYS = [
|
|
28
|
-
"answer", "boundCallId", "boundInputHash", "checkpointToken", "
|
|
28
|
+
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "reason", "remember", "updatedInput",
|
|
29
29
|
];
|
|
30
30
|
const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
|
|
31
|
-
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "
|
|
31
|
+
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
|
|
32
32
|
const PLAN_REVIEW_BODY_DECLARED_KEYS = [
|
|
33
33
|
"boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
|
|
34
34
|
];
|
|
@@ -356,6 +356,9 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
356
356
|
...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
|
|
357
357
|
...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
|
|
358
358
|
...(s.origin !== undefined ? { origin: s.origin } : {}),
|
|
359
|
+
...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
|
|
360
|
+
...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
|
|
361
|
+
...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
|
|
359
362
|
objective,
|
|
360
363
|
input,
|
|
361
364
|
};
|
|
@@ -637,7 +640,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
637
640
|
const body = (await readJson(req));
|
|
638
641
|
const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
|
|
639
642
|
if (!decision) {
|
|
640
|
-
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?,
|
|
643
|
+
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }");
|
|
641
644
|
return;
|
|
642
645
|
}
|
|
643
646
|
{
|
|
@@ -708,29 +711,22 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
708
711
|
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");
|
|
709
712
|
return;
|
|
710
713
|
}
|
|
711
|
-
const
|
|
712
|
-
switch (
|
|
714
|
+
const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
|
|
715
|
+
switch (decidedByReading.kind) {
|
|
713
716
|
case "not_in_proof":
|
|
714
|
-
sendError(res, 400, "
|
|
717
|
+
sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
|
|
715
718
|
return;
|
|
716
|
-
case "
|
|
717
|
-
sendError(res, 400, "request.
|
|
719
|
+
case "outside_closed_set":
|
|
720
|
+
sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
|
|
718
721
|
return;
|
|
719
|
-
case "not_string":
|
|
720
|
-
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
721
|
-
return;
|
|
722
|
-
case "too_wide":
|
|
723
|
-
sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
|
|
724
|
-
return;
|
|
725
|
-
case "absent":
|
|
726
722
|
case "accepted":
|
|
727
723
|
break;
|
|
728
724
|
default: {
|
|
729
|
-
const unreachable =
|
|
730
|
-
throw new Error(`unhandled
|
|
725
|
+
const unreachable = decidedByReading;
|
|
726
|
+
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
731
727
|
}
|
|
732
728
|
}
|
|
733
|
-
const
|
|
729
|
+
const decidedBy = decidedByReading.decidedBy;
|
|
734
730
|
const binding = {
|
|
735
731
|
checkpointToken: body.checkpointToken,
|
|
736
732
|
boundCallId: body.boundCallId,
|
|
@@ -779,7 +775,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
779
775
|
pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
|
|
780
776
|
}
|
|
781
777
|
: undefined;
|
|
782
|
-
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(
|
|
778
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decidedBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
|
|
783
779
|
const outErrorCode = out.body.errorCode;
|
|
784
780
|
if (pendingGrant !== undefined && out.status === 200 && (outErrorCode !== "cancelled" || out.decisionCommitted === true) && deps.approvalExemptionStore) {
|
|
785
781
|
try {
|
|
@@ -826,11 +822,12 @@ function redactPendingDisclosures(row) {
|
|
|
826
822
|
function projectPendingForWire(row, governance) {
|
|
827
823
|
const redacted = redactPendingDisclosures(row);
|
|
828
824
|
const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
|
|
829
|
-
const rememberRefused = !
|
|
825
|
+
const rememberRefused = !standingAnswerDurablyRecordable({
|
|
830
826
|
governanceForced: governanceForced === true,
|
|
831
827
|
requiresRealApproval: row.requiresRealApproval === true,
|
|
832
828
|
mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
|
|
833
|
-
|
|
829
|
+
grantBinding: grantBindingOf(row),
|
|
830
|
+
classes: row.classes,
|
|
834
831
|
});
|
|
835
832
|
return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
|
|
836
833
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
|
-
import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
|
|
2
|
+
import { modelImageInput, isModelAllowlisted, selectableCatalog } from "../../model-select.js";
|
|
3
3
|
import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
|
|
4
4
|
import { projectSealedKeyCapability } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
@@ -120,7 +120,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
120
120
|
subagentSteer: facts.subagentSteer(deps).available,
|
|
121
121
|
subagentResume: facts.subagentSteer(deps).available,
|
|
122
122
|
taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config) },
|
|
123
|
-
settings: { permissions: { allowGrant: true } },
|
|
123
|
+
settings: { permissions: { allowGrant: true, allowLayer: true } },
|
|
124
124
|
permissionModeAuto: {
|
|
125
125
|
accepted: true,
|
|
126
126
|
classifierSeat: deps.autoModeFace?.seatMounted === true,
|
|
@@ -137,6 +137,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
137
137
|
taskAgents: deps.config.requirePrincipal !== true,
|
|
138
138
|
retainBackgroundProcesses: deps.config.requirePrincipal !== true,
|
|
139
139
|
interactiveTools: true,
|
|
140
|
+
approvalReach: true,
|
|
140
141
|
disableSkills: true,
|
|
141
142
|
userInvokedSkills: true,
|
|
142
143
|
skillToolLists: true,
|
|
@@ -158,6 +159,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
158
159
|
approvalsStreamLive: projectApprovalsStreamLiveCapability(deps),
|
|
159
160
|
streamApproval: facts.streamApproval(deps).available,
|
|
160
161
|
approvalDecisionNote: true,
|
|
162
|
+
approvalDecidedBy: true,
|
|
161
163
|
sideQuery: true,
|
|
162
164
|
agentRoster: facts.agentRoster(deps).available,
|
|
163
165
|
retention: retentionAdvertisement(deps.config),
|
|
@@ -238,7 +240,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
238
240
|
id: m.id,
|
|
239
241
|
provider: m.provider,
|
|
240
242
|
reasoning: m.reasoning,
|
|
241
|
-
vision:
|
|
243
|
+
...(modelImageInput(m) !== "undeclared" ? { vision: modelImageInput(m) === "yes" } : {}),
|
|
242
244
|
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
243
245
|
...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
|
|
244
246
|
...((levels) => (levels !== undefined ? { supportedEffortLevels: [...levels] } : {}))(reasoningEffortLevelsOf(m)),
|
|
@@ -36,6 +36,23 @@
|
|
|
36
36
|
* (`isParkedRunStatus` / `isTerminalRunStatus`)与 `runProvenElsewhere`(手抄已收编成一只谓词,S-430 起同住 `plugins/store-contracts.ts`)。
|
|
37
37
|
* 显式 operator 可重拨任何租户的会话(监督动作,与 notify / steer 同臂;**不是** bare `isOperator`)。
|
|
38
38
|
*
|
|
39
|
+
* ── 应答期限(S-431)────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
* 修前这一口对 `live.reconnectMcpServer(server)` 是裸 `await`:server 侧没有期限,上界只来自 core 自己的启动期钟
|
|
41
|
+
* (env `MCP_TIMEOUT`,未设落 SDK 缺省 60s;拨号之前还有一次不在那条钟里的旧连接 `close()`),一台拨不通 / 半开的远端能把这一口
|
|
42
|
+
* HTTP 请求挂很久。现在与 `GET /v1/sessions/:id/mcp` 面板读**同一只**数 `MCP_DIAL_DEADLINE_MS`(`../mcp-dial-deadline.ts`),
|
|
43
|
+
* 到点答 **503 `state.mcp_reconnect_unsettled`**。
|
|
44
|
+
* 🔴 **只界应答,不取消重拨**:core 的 `TaskStream.reconnectMcpServer(server)` 不收 `AbortSignal`(d.ts 逐字:单参),
|
|
45
|
+
* 本面没有取消把手可递,也不伪造一个。到点之后重拨**继续跑**,落定时照常把名册写上这条 run(成功)或撤走(拨号失败)—— 所以
|
|
46
|
+
* 503 说的是「这一口没等到结果」,**不是**「失败」,文案与契约 §G.7c 都按这个读法写。重发**不作保证**:core 的在途守卫
|
|
47
|
+
* (`redialsInFlight`,`dist/core/mcp.js`)只罩**拨号本身**(关旧 + 连 + 列),拨号一落定就放开,而 `redialMcpServer`
|
|
48
|
+
* (`dist/core/runner/mcp-redial.js`)随后还要 `await commitMcpRosterChanges`(重建工具面)—— 那一段里守卫已不在,
|
|
49
|
+
* 此刻再发会开第二次「先关后拨」。所以文案只说「拨号还在跑时 core 会拒第二次」,不说「重发一定得 refused」,也不把重发
|
|
50
|
+
* 推荐成探针(对抗复审 [medium],验真后采纳;守卫延到名册提交之后是 core 的活,本仓不自造第二份在途判)。
|
|
51
|
+
* 为什么是 503 而不是 200 + 第四个 `outcome` 词:`outcome` 三词是 core 结果的投影,这里**没有结果**,编一个词就是替
|
|
52
|
+
* core 说话(与 `unsupported` 那一行「没有 core 结果就不铸 `prefix`」同一条纪律);503 是 `state.*` 族的唯一状态
|
|
53
|
+
* (附录 A 判据 3),与 `state.mcp_probe_incomplete` 同族同档,但**不复用**它的码 —— 那一码的语义钉在 probe 面上。
|
|
54
|
+
* 本面**不**带 `Retry-After`:重发不是无代价的重试(见上),一个会被通用重试臂自动执行的头在一次交易动词上是错的方向。
|
|
55
|
+
*
|
|
39
56
|
* ── 计费/门 ─────────────────────────────────────────────────────────────────────────────────────
|
|
40
57
|
* 零模型工作(不进 billable;不吃 drain / roster-pending);mutating ⇒ `rateLimited`。能力位 `mcpReconnect`
|
|
41
58
|
* = 持久 run 账面 ∧ 会话属主面(两道 501 的同一批 deps 推导出来,「说 yes 但 501」结构上不可能)。
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { isParkedRunStatus, isTerminalRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
|
|
3
3
|
import { redactSecrets } from "../../trace/redact.js";
|
|
4
|
+
import { DeadlineExceeded, MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
|
|
4
5
|
import { sendJson, sendError } from "../send.js";
|
|
5
6
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
7
|
import {} from "../route-ctx.js";
|
|
@@ -99,11 +100,15 @@ export async function handleMcpReconnect(req, res, match, ctx) {
|
|
|
99
100
|
const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
|
|
100
101
|
if (live) {
|
|
101
102
|
try {
|
|
102
|
-
const result = await live.reconnectMcpServer(server);
|
|
103
|
+
const result = await raceDeadline(live.reconnectMcpServer(server), MCP_DIAL_DEADLINE_MS);
|
|
103
104
|
sendJson(res, 200, { taskId: activeTaskId, sessionId, ...projectResult(result) });
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
107
|
catch (e) {
|
|
108
|
+
if (e instanceof DeadlineExceeded) {
|
|
109
|
+
sendError(res, 503, "state.mcp_reconnect_unsettled", "the MCP re-dial did not settle within the time this endpoint waits, so there is no outcome to report — the re-dial itself was NOT cancelled (the engine takes no cancellation for it) and may still finish, so do not assume it failed; do not resend blindly either: the engine refuses a second re-dial of the same server only while the dial itself is still running, and a request sent after that point starts another close-then-dial on a server that may now be healthy, so check the server's state before re-sending");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
107
112
|
const code = e.code;
|
|
108
113
|
const msg = e instanceof Error ? e.message : String(e);
|
|
109
114
|
switch (code) {
|
|
@@ -37,7 +37,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
|
|
|
37
37
|
const code = codeOf(err);
|
|
38
38
|
if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
|
|
39
39
|
return true;
|
|
40
|
-
if (sentMemoryStoreRefusal(res, err))
|
|
40
|
+
if (sentMemoryStoreRefusal(res, err, ctx.deps.logger))
|
|
41
41
|
return true;
|
|
42
42
|
ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
|
|
43
43
|
return false;
|
|
@@ -12,7 +12,7 @@ const EraseRequestSchema = z.object({
|
|
|
12
12
|
allowUnevidenced: z.boolean().optional(),
|
|
13
13
|
});
|
|
14
14
|
function sentClassifiedFailure(res, ctx, face, err) {
|
|
15
|
-
if (sentMemoryComplianceFailure(res, err, { kind: "operator" }))
|
|
15
|
+
if (sentMemoryComplianceFailure(res, err, { kind: "operator" }, ctx.deps.logger))
|
|
16
16
|
return true;
|
|
17
17
|
ctx.deps.logger?.warn?.(`memory_compliance_${face}_failed`, { err: String(err) });
|
|
18
18
|
return false;
|
|
@@ -54,7 +54,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
|
|
|
54
54
|
if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
|
-
if (face !== "clearances" && sentMemoryStoreRefusal(res, err))
|
|
57
|
+
if (face !== "clearances" && sentMemoryStoreRefusal(res, err, ctx.deps.logger))
|
|
58
58
|
return true;
|
|
59
59
|
if (err instanceof ControlPlaneCorruptError) {
|
|
60
60
|
sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { persistedPlaneOf } from "../../terminal.js";
|
|
2
2
|
import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
|
|
4
|
+
import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
|
|
5
5
|
import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
|
|
6
6
|
import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
|
|
7
7
|
import { redactSecrets } from "../../trace/redact.js";
|
|
@@ -14,13 +14,16 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
15
15
|
import { streamSseLog } from "../sse-log.js";
|
|
16
16
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
17
|
-
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
18
|
-
import {
|
|
17
|
+
import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
|
|
18
|
+
import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
|
|
19
|
+
import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
19
20
|
import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
|
|
20
21
|
import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
|
|
21
22
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
22
23
|
import { readTurnActivityMs } from "../../turn-activity.js";
|
|
23
24
|
import { mintCancelledResult } from "../../run-cancel-context.js";
|
|
25
|
+
import { userInputLaneOf } from "../../user-input-ledger.js";
|
|
26
|
+
import { INJECTION_TIERS } from "../../trace/injection-tier.js";
|
|
24
27
|
import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
|
|
25
28
|
import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
26
29
|
import { ActorAssertionWire } from "@sema-agent/settings-schema";
|
|
@@ -30,6 +33,10 @@ import { mintAndDriveDurableRun } from "../durable-run-leg.js";
|
|
|
30
33
|
import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
|
|
31
34
|
import { withPrincipal } from "../../observability/principal-context.js";
|
|
32
35
|
import { readResumeCtx } from "../resume-ctx-read.js";
|
|
36
|
+
import { storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "../checkpoint-ctx.js";
|
|
37
|
+
const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
|
|
38
|
+
const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
|
|
39
|
+
const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
|
|
33
40
|
const ASK_PARKING_RETRY_AFTER_SEC = 2;
|
|
34
41
|
const ASK_ACTOR_LABEL_MAX = 256;
|
|
35
42
|
const AskDecisionActorSchema = ActorAssertionWire.strip();
|
|
@@ -45,7 +52,7 @@ const AskDecisionBodySchema = z
|
|
|
45
52
|
.refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
|
|
46
53
|
.optional(),
|
|
47
54
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
48
|
-
|
|
55
|
+
decidedBy: z.unknown().optional(),
|
|
49
56
|
})
|
|
50
57
|
.strict();
|
|
51
58
|
const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
|
|
@@ -135,11 +142,16 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
|
|
|
135
142
|
staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
|
|
136
143
|
}, taskId, staleMs);
|
|
137
144
|
}
|
|
145
|
+
function sendSteerReplayRefusal(res, refusal) {
|
|
146
|
+
const detail = refusal?.detail;
|
|
147
|
+
const differs = detail !== null && typeof detail === "object" ? readClosedWordSeat(detail, "differs") : undefined;
|
|
148
|
+
sendError(res, 409, "steering.duplicate_input_id", refusal instanceof Error ? refusal.message : "this steering input id was already accepted with different values", differs !== undefined ? { detail: { differs } } : undefined);
|
|
149
|
+
}
|
|
138
150
|
async function acceptLiveUserInput(deps, res, at) {
|
|
139
151
|
if (!deps.userInputLedger)
|
|
140
152
|
return undefined;
|
|
141
153
|
try {
|
|
142
|
-
return await deps.userInputLedger.accept({ input:
|
|
154
|
+
return await deps.userInputLedger.accept({ input: userInputLaneOf(at.tier), messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
|
|
143
155
|
}
|
|
144
156
|
catch (e) {
|
|
145
157
|
deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
|
|
@@ -428,15 +440,15 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
428
440
|
return;
|
|
429
441
|
}
|
|
430
442
|
if (typeof body.text !== "string" || body.text.length === 0) {
|
|
431
|
-
sendError(res, 400, "request.body_shape",
|
|
443
|
+
sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
|
|
432
444
|
return;
|
|
433
445
|
}
|
|
434
446
|
if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
|
|
435
447
|
sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
|
|
436
448
|
return;
|
|
437
449
|
}
|
|
438
|
-
if (body.priority !== undefined && body.priority
|
|
439
|
-
sendError(res, 400, "request.field_invalid",
|
|
450
|
+
if (body.priority !== undefined && !isSystemInjectionPriority(body.priority)) {
|
|
451
|
+
sendError(res, 400, "request.field_invalid", `priority must be ${STEER_PRIORITY_CHOICES} when present`);
|
|
440
452
|
return;
|
|
441
453
|
}
|
|
442
454
|
const text = body.text;
|
|
@@ -494,19 +506,19 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
494
506
|
return "queue-full";
|
|
495
507
|
}
|
|
496
508
|
if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
|
|
497
|
-
parkDuplicateKey = e
|
|
509
|
+
parkDuplicateKey = e;
|
|
498
510
|
return "duplicate-key";
|
|
499
511
|
}
|
|
500
512
|
throw e;
|
|
501
513
|
}
|
|
502
514
|
};
|
|
503
515
|
const sendQueueFull = () => sendError(res, 409, "steering.queue_full", parkQueueFull ?? "the parked steering queue on this checkpoint is full");
|
|
504
|
-
const sendDuplicateKey = () =>
|
|
516
|
+
const sendDuplicateKey = () => sendSteerReplayRefusal(res, parkDuplicateKey);
|
|
505
517
|
const sendParked = (status) => sendJson(res, 202, { taskId, status, delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
|
|
506
518
|
const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
|
|
507
519
|
const live = steerableRuns.get(taskId);
|
|
508
520
|
if (live) {
|
|
509
|
-
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
|
|
521
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live, tier: priority });
|
|
510
522
|
if (acceptance === "refused")
|
|
511
523
|
return;
|
|
512
524
|
try {
|
|
@@ -525,7 +537,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
525
537
|
return;
|
|
526
538
|
}
|
|
527
539
|
if (code === "steering.duplicate_input_id") {
|
|
528
|
-
parkDuplicateKey = e
|
|
540
|
+
parkDuplicateKey = e;
|
|
529
541
|
sendDuplicateKey();
|
|
530
542
|
return;
|
|
531
543
|
}
|
|
@@ -600,7 +612,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
600
612
|
return;
|
|
601
613
|
}
|
|
602
614
|
if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
|
|
603
|
-
parkDuplicateKey = e
|
|
615
|
+
parkDuplicateKey = e;
|
|
604
616
|
mintResult.duplicateKey = true;
|
|
605
617
|
return;
|
|
606
618
|
}
|
|
@@ -634,6 +646,11 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
634
646
|
mintResult.noResumeCtx = true;
|
|
635
647
|
return;
|
|
636
648
|
}
|
|
649
|
+
const unservable = storedBodyReplayRefusal(resumeRead.ctx);
|
|
650
|
+
if (unservable !== undefined) {
|
|
651
|
+
mintResult.unservable = unservable;
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
637
654
|
const cpScope = encodeCheckpointScope(run.owner);
|
|
638
655
|
const wakeToken = mintCheckpointToken();
|
|
639
656
|
await cs.put(wakeToken, {
|
|
@@ -688,6 +705,12 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
688
705
|
sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read — no wake card was minted and nothing was queued. Ask whoever operates this deployment to repair or remove the row named in `where` before steering this session again.", { where: mintResult.corruptCtxWhere });
|
|
689
706
|
return;
|
|
690
707
|
}
|
|
708
|
+
if (mintResult.unservable !== undefined) {
|
|
709
|
+
deps.logger?.warn?.("steer_wake_park_refused_stored_body", { taskId, sessionId: run.sessionId, ...storedBodyReplayLogFields(mintResult.unservable) });
|
|
710
|
+
const { status, body: { error, errorCode, ...rest } } = storedBodyReplayAnswer(mintResult.unservable);
|
|
711
|
+
sendError(res, status, errorCode, error, rest);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
691
714
|
if (mintResult.noResumeCtx) {
|
|
692
715
|
sendError(res, 409, "conflict.resume_context_unavailable", "run already ended and this session's resume context is gone — a steer queued now could never be delivered by POST /v1/sessions/:id/wake; submit a new task instead");
|
|
693
716
|
return;
|
|
@@ -819,7 +842,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
819
842
|
});
|
|
820
843
|
return;
|
|
821
844
|
}
|
|
822
|
-
const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
|
|
845
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream, tier: "now" });
|
|
823
846
|
if (acceptance === "refused")
|
|
824
847
|
return;
|
|
825
848
|
try {
|
|
@@ -836,7 +859,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
836
859
|
return;
|
|
837
860
|
}
|
|
838
861
|
if (code === "steering.duplicate_input_id") {
|
|
839
|
-
|
|
862
|
+
sendSteerReplayRefusal(res, e);
|
|
840
863
|
return;
|
|
841
864
|
}
|
|
842
865
|
if (code !== "steering.not_running")
|
|
@@ -1209,7 +1232,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1209
1232
|
return;
|
|
1210
1233
|
}
|
|
1211
1234
|
const messageId = uuidv7();
|
|
1212
|
-
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
|
|
1235
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle, tier: undefined });
|
|
1213
1236
|
if (acceptance === "refused")
|
|
1214
1237
|
return;
|
|
1215
1238
|
const marker = await relayAcceptedInput(deps, acceptance, () => handle.steer(redacted));
|
|
@@ -1349,33 +1372,26 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1349
1372
|
}
|
|
1350
1373
|
const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
|
|
1351
1374
|
if (!parsedBody.success) {
|
|
1352
|
-
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label /
|
|
1375
|
+
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
|
|
1353
1376
|
return;
|
|
1354
1377
|
}
|
|
1355
1378
|
const body = parsedBody.data;
|
|
1356
|
-
const
|
|
1357
|
-
switch (
|
|
1379
|
+
const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
|
|
1380
|
+
switch (decidedByReading.kind) {
|
|
1358
1381
|
case "not_in_proof":
|
|
1359
|
-
sendError(res, 400, "
|
|
1382
|
+
sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
|
|
1360
1383
|
return;
|
|
1361
|
-
case "
|
|
1362
|
-
sendError(res, 400, "request.
|
|
1384
|
+
case "outside_closed_set":
|
|
1385
|
+
sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
|
|
1363
1386
|
return;
|
|
1364
|
-
case "not_string":
|
|
1365
|
-
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
1366
|
-
return;
|
|
1367
|
-
case "too_wide":
|
|
1368
|
-
sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
|
|
1369
|
-
return;
|
|
1370
|
-
case "absent":
|
|
1371
1387
|
case "accepted":
|
|
1372
1388
|
break;
|
|
1373
1389
|
default: {
|
|
1374
|
-
const unreachable =
|
|
1375
|
-
throw new Error(`unhandled
|
|
1390
|
+
const unreachable = decidedByReading;
|
|
1391
|
+
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
1376
1392
|
}
|
|
1377
1393
|
}
|
|
1378
|
-
const
|
|
1394
|
+
const decidedBy = decidedByReading.decidedBy;
|
|
1379
1395
|
const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
|
|
1380
1396
|
const taskId = match.params.taskId;
|
|
1381
1397
|
const askId = match.params.askId;
|
|
@@ -1501,6 +1517,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1501
1517
|
sendTerminal(row0);
|
|
1502
1518
|
return;
|
|
1503
1519
|
}
|
|
1520
|
+
if (policyCannotAllow(decidedBy, body.decision === "approve", storedCardDemandsAPerson(row0.cardJson))) {
|
|
1521
|
+
sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1504
1524
|
let outcome;
|
|
1505
1525
|
const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
|
|
1506
1526
|
try {
|
|
@@ -1509,7 +1529,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1509
1529
|
decisionActor: actor,
|
|
1510
1530
|
decisionNote: body.note ?? null,
|
|
1511
1531
|
idempotencyKey: body.idempotencyKey ?? null,
|
|
1512
|
-
...(
|
|
1532
|
+
...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(decidedBy)),
|
|
1513
1533
|
...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
|
|
1514
1534
|
});
|
|
1515
1535
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 单域独占状态(lens1 §C2 的 12 条里的 5 条)全部收进 `SessionsLocal`,每个 server 实例一份:
|
|
8
8
|
* SSE 连接坑计数 `sseConnections`、owner 查探许可 `sseOwnerLookups`、`/mcp` 面板的短 TTL single-flight
|
|
9
|
-
* 缓存 `mcpStatusCache`
|
|
9
|
+
* 缓存 `mcpStatusCache` 与它的时限常量(应答期限 `MCP_DIAL_DEADLINE_MS` 与 `POST …/mcp/reconnect` 共读,见 `../mcp-dial-deadline.ts`)。
|
|
10
10
|
*
|
|
11
11
|
* ⚠️ 搬运例外(仅此一处,语义等价):两个原为闭包 `let` 的计数器进了 local 盒子,于是 6 个使用点写成
|
|
12
12
|
* `sse.sseConnections` / `sse.sseOwnerLookups`——`let` 无法跨模块共享自增。判据/时机/配对释放逐字未动。
|