@sema-agent/server 7.117.0 → 7.119.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 +182 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +58 -28
- package/deploy/sema-up/README.md +2 -0
- package/deploy/sema-up/chart/templates/minio.yaml +2 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
- package/deploy/sema-up/chart/templates/server.yaml +1 -0
- package/deploy/sema-up/chart/values.yaml +7 -1
- package/deploy/sema-up/cluster-up.sh +20 -8
- package/deploy/sema-up/kube-up.sh +29 -16
- package/deploy/sema-up/lib/buckets.sh +21 -0
- package/deploy/sema-up/lib/health.sh +33 -0
- package/deploy/sema-up/sema-up.sh +14 -9
- package/deploy/sema-up/smoke.sh +16 -6
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
- package/dist/approval-ask-audit-store.js +2 -7
- package/dist/approval-card.d.ts +40 -10
- package/dist/approval-card.js +11 -1
- package/dist/approval.d.ts +55 -17
- package/dist/approval.js +74 -33
- package/dist/audit.d.ts +12 -1
- package/dist/audit.js +62 -2
- package/dist/auto-mode-face.d.ts +2 -3
- package/dist/bake-runner/main.js +1 -6
- package/dist/bake-runner/protocol.d.ts +18 -0
- package/dist/bake-runner/protocol.js +22 -2
- package/dist/bake-runner/runner.d.ts +12 -8
- package/dist/bake-runner/runner.js +5 -9
- package/dist/boot/config-center.js +1 -1
- package/dist/boot/coordinators.js +4 -1
- package/dist/boot/device-lane.d.ts +3 -3
- package/dist/boot/device-lane.js +1 -2
- package/dist/boot/leader.d.ts +4 -4
- package/dist/boot/leader.js +1 -2
- package/dist/boot/parked-revive-gate.d.ts +4 -6
- package/dist/boot/parked-revive-gate.js +2 -6
- package/dist/boot/resolve-spec.js +9 -11
- package/dist/boot/runner-deps.js +2 -2
- package/dist/boot/stage-07-capability-layer.js +7 -3
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.js +12 -7
- package/dist/capabilities/center-plugins.d.ts +2 -0
- package/dist/capabilities/center-plugins.js +24 -6
- package/dist/capabilities/code-review-council.js +8 -11
- 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/capabilities/scenarios.d.ts +34 -7
- package/dist/capabilities/scenarios.js +33 -6
- package/dist/config-catalog.js +3 -3
- package/dist/config.d.ts +14 -18
- package/dist/config.js +15 -26
- package/dist/deployment-governance.d.ts +5 -7
- package/dist/deployment-governance.js +4 -5
- package/dist/host-decision.d.ts +114 -31
- package/dist/host-decision.js +39 -2
- package/dist/http/admission.js +12 -6
- package/dist/http/approval-sweeps.js +1 -1
- 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 +8 -3
- package/dist/http/routes/approvals-assistant.d.ts +25 -3
- package/dist/http/routes/approvals-assistant.js +27 -26
- package/dist/http/routes/capabilities.js +6 -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.d.ts +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +47 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +23 -22
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +17 -13
- package/dist/images/manifest.d.ts +7 -1
- package/dist/images/manifest.js +18 -16
- package/dist/memory-store-refusal.d.ts +38 -18
- package/dist/memory-store-refusal.js +25 -7
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +46 -41
- 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 +19 -3
- package/dist/observability/fail-open.js +19 -3
- package/dist/operator-ask.d.ts +6 -2
- 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 +29 -20
- package/dist/outbound-dispatcher.js +105 -16
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- 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/approval-exemption-store.d.ts +4 -0
- package/dist/plugins/approval-exemption-store.js +1 -0
- package/dist/plugins/blob-backend.d.ts +13 -0
- package/dist/plugins/blob-backend.js +26 -0
- package/dist/plugins/local-session-store.d.ts +78 -47
- package/dist/plugins/local-session-store.js +152 -122
- package/dist/plugins/one-time-migrations.d.ts +11 -0
- package/dist/plugins/one-time-migrations.js +128 -4
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
- package/dist/plugins/snapshot-bucket-presence.js +40 -0
- package/dist/plugins/store-backend.d.ts +17 -6
- package/dist/plugins/store-backend.js +46 -16
- package/dist/plugins/store-open-wait.d.ts +33 -0
- package/dist/plugins/store-open-wait.js +73 -0
- 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/rule-entry-list.d.ts +35 -46
- package/dist/rule-entry-list.js +18 -26
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.js +5 -3
- package/dist/runs.d.ts +1 -1
- package/dist/security.d.ts +5 -3
- package/dist/security.js +4 -0
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -11
- package/dist/spec-fields.d.ts +13 -11
- package/dist/spec-fields.js +4 -7
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/store-posture.d.ts +14 -6
- package/dist/store-posture.js +7 -4
- package/dist/task-settings.d.ts +27 -55
- package/dist/task-settings.js +13 -21
- package/dist/tool-approval.d.ts +133 -138
- package/dist/tool-approval.js +175 -66
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/redact.d.ts +6 -0
- package/dist/trace/redact.js +9 -9
- 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 +30 -14
- package/dist/user-input-ledger.js +25 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +74 -3
- package/scripts/drain-check.mysql.sql +43 -17
- package/scripts/drain-check.pg.sql +46 -20
|
@@ -3,6 +3,7 @@ import type { QuestionAnswer } from "@sema-agent/core";
|
|
|
3
3
|
import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
|
|
4
4
|
import { type ToolApprovalCoordinator } from "../../tool-approval.js";
|
|
5
5
|
import { type GovernancePosture } from "../../runtime-governance.js";
|
|
6
|
+
import { type GateIntentView } from "../../approval.js";
|
|
6
7
|
import { type RouteCtx, type RouteDecl, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
|
|
7
8
|
import { type KeyClosureIssue } from "../../request-key-closure.js";
|
|
8
9
|
/** 族的成员 = 路由表行 id(改名 / 删行 ⇒ 下表的键当场编译红)。 */
|
|
@@ -104,6 +105,27 @@ export declare function checkQuestionAnswer(v: unknown): QuestionAnswerCheck;
|
|
|
104
105
|
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
105
106
|
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
106
107
|
export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ApprovalsAssistantRouteId>, ctx: RouteCtx): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* 🔴 [ref]-2 / [ref]②:两条 durable 读面(`GET /v1/approvals` + `/v1/approvals/stream`)的**唯一**
|
|
110
|
+
* 投影 —— 脱敏([ref] 件4)+ 出身归因([ref] 的另一半)一次做完。
|
|
111
|
+
*
|
|
112
|
+
* 出身格的病灶如实记:`governanceForced` 此前只在 409 体上算(`buildActiveRunConflict`),而运维队列
|
|
113
|
+
* 的行不算 —— 同一道 park 门,客户端从 409 看得见「这是运维治理层下的」,从队列看不见。它**不是**一个
|
|
114
|
+
* 落库缺口:判据的两半(行上 `riskDescriptor.shellGateDoctrine` + 本部署的治理姿态)在这条读面上都够得着,
|
|
115
|
+
* 缺的只是**没有人算**。所以补法 = 让两面共用 `governanceOriginOf` 这一个属主(在读面算的已知失真、
|
|
116
|
+
* 以及「根治要 core 在 mint 写进行里」的登记,逐字在 `runtime-governance.ts` 那个函数的顶注,不复述)。
|
|
117
|
+
*
|
|
118
|
+
* 🔴 `true` 才写键,恒不写 `false` —— 与活卡帧 / 409 体 / `card_json` 同一条纪律:缺席 = 「没有治理来源
|
|
119
|
+
* 的证据」,不是「这不是治理门」。
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ **`delegation` 刻意不在这里**(同批如实登记,禁编造键):park 行上**没有**这条信息 —— core 的
|
|
122
|
+
* `Checkpoint` / `PendingAction` / `CheckpointGate` 三个面都不带 `{parentToolCallId, depth, agentName}`
|
|
123
|
+
* (`Checkpoint.state.delegationProvenance` 是 [ref] 的内容安全聚合 `{version, coverageStartTurn,
|
|
124
|
+
* sawExternal, incomplete}`,与 ask 的出处链是两件事)。同步腿的这一格来自 `AskRequest.delegation`,
|
|
125
|
+
* 那条路 park 时不随行。⇒ 无供给,诚实缺席;要它得先有 core 侧的属主面。
|
|
126
|
+
*/
|
|
127
|
+
/** 运维队列行投影要的部署事实:治理姿态(出身归因的部署侧一半)+ 三张审批名单(问的属主:部署名单门问不问这一行的调用)。生产两处都递活配置。 */
|
|
128
|
+
export type PendingProjectionDeployment = GovernancePosture & Pick<GateIntentView, "approvalRequire" | "approvalDeny" | "approvalNeverAuto">;
|
|
107
129
|
export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">,
|
|
108
130
|
/**
|
|
109
131
|
* 🔴 S-454:**第二条 diff 源** —— `GET /v1/approvals` 的 `livePending` 那**同一只**读函数
|
|
@@ -118,9 +140,9 @@ export declare function streamApprovals(req: IncomingMessage, res: ServerRespons
|
|
|
118
140
|
* 与 `governance` 同一条理由:`pollMs` 只有测试传,而本参数是每个生产调用点都必须表态的那一个。
|
|
119
141
|
*/
|
|
120
142
|
live: Pick<ToolApprovalCoordinator, "listLivePending"> | undefined, scope: string | undefined,
|
|
121
|
-
/** [ref]②:出身归因的部署侧一半(
|
|
122
|
-
* `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
|
|
123
|
-
governance?:
|
|
143
|
+
/** [ref]②:出身归因的部署侧一半 + 三张审批名单(问的属主)。缺席 ⇒ 归不出治理出身(键缺席,与 best-effort 同方向)、没有一行读作
|
|
144
|
+
* 部署名单的问(记不住,方向是多问)。**位置在 `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
|
|
145
|
+
governance?: PendingProjectionDeployment, pollMs?: number): Promise<void>;
|
|
124
146
|
export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
|
|
125
147
|
readonly id: "approvals-list";
|
|
126
148
|
readonly path: "/v1/approvals";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { screenedGate } from "../../trace/project.js";
|
|
2
|
-
import { hostDecisionFacts } from "../../host-decision.js";
|
|
2
|
+
import { hostDecisionFacts, readDecidedBy, retiredDecisionKeyFacts, 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, carriesDeploymentListQuestion, 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,8 @@ 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 {
|
|
20
|
+
import { deploymentListAsks } from "../../approval.js";
|
|
21
|
+
import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
|
|
21
22
|
import { sendResumeOutcome } from "../route-ctx.js";
|
|
22
23
|
import { assertNever } from "../route-ctx.js";
|
|
23
24
|
import * as facts from "../../capability-facts.js";
|
|
@@ -25,10 +26,10 @@ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
|
|
|
25
26
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
26
27
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
|
|
27
28
|
const DECIDE_BODY_DECLARED_KEYS = [
|
|
28
|
-
"answer", "boundCallId", "boundInputHash", "checkpointToken", "
|
|
29
|
+
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "reason", "remember", "updatedInput",
|
|
29
30
|
];
|
|
30
31
|
const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
|
|
31
|
-
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "
|
|
32
|
+
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
|
|
32
33
|
const PLAN_REVIEW_BODY_DECLARED_KEYS = [
|
|
33
34
|
"boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
|
|
34
35
|
];
|
|
@@ -356,6 +357,9 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
356
357
|
...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
|
|
357
358
|
...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
|
|
358
359
|
...(s.origin !== undefined ? { origin: s.origin } : {}),
|
|
360
|
+
...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
|
|
361
|
+
...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
|
|
362
|
+
...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
|
|
359
363
|
objective,
|
|
360
364
|
input,
|
|
361
365
|
};
|
|
@@ -637,13 +641,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
637
641
|
const body = (await readJson(req));
|
|
638
642
|
const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
|
|
639
643
|
if (!decision) {
|
|
640
|
-
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?,
|
|
644
|
+
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }", retiredDecisionKeyFacts(body)?.extra);
|
|
641
645
|
return;
|
|
642
646
|
}
|
|
643
647
|
{
|
|
644
648
|
const keyIssue = decisionBodyKeyIssue("approval-decide", body);
|
|
645
649
|
if (keyIssue) {
|
|
646
|
-
|
|
650
|
+
const retired = retiredDecisionKeyFacts(body);
|
|
651
|
+
sendError(res, 400, "request.body_shape", retired?.error ?? keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys, ...retired?.extra });
|
|
647
652
|
return;
|
|
648
653
|
}
|
|
649
654
|
}
|
|
@@ -708,29 +713,22 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
708
713
|
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
714
|
return;
|
|
710
715
|
}
|
|
711
|
-
const
|
|
712
|
-
switch (
|
|
716
|
+
const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
|
|
717
|
+
switch (decidedByReading.kind) {
|
|
713
718
|
case "not_in_proof":
|
|
714
|
-
sendError(res, 400, "
|
|
715
|
-
return;
|
|
716
|
-
case "deny_only":
|
|
717
|
-
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
718
|
-
return;
|
|
719
|
-
case "not_string":
|
|
720
|
-
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
719
|
+
sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
|
|
721
720
|
return;
|
|
722
|
-
case "
|
|
723
|
-
sendError(res, 400, "request.field_invalid",
|
|
721
|
+
case "outside_closed_set":
|
|
722
|
+
sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
|
|
724
723
|
return;
|
|
725
|
-
case "absent":
|
|
726
724
|
case "accepted":
|
|
727
725
|
break;
|
|
728
726
|
default: {
|
|
729
|
-
const unreachable =
|
|
730
|
-
throw new Error(`unhandled
|
|
727
|
+
const unreachable = decidedByReading;
|
|
728
|
+
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
731
729
|
}
|
|
732
730
|
}
|
|
733
|
-
const
|
|
731
|
+
const decidedBy = decidedByReading.decidedBy;
|
|
734
732
|
const binding = {
|
|
735
733
|
checkpointToken: body.checkpointToken,
|
|
736
734
|
boundCallId: body.boundCallId,
|
|
@@ -779,7 +777,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
779
777
|
pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
|
|
780
778
|
}
|
|
781
779
|
: undefined;
|
|
782
|
-
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(
|
|
780
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decidedBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
|
|
783
781
|
const outErrorCode = out.body.errorCode;
|
|
784
782
|
if (pendingGrant !== undefined && out.status === 200 && (outErrorCode !== "cancelled" || out.decisionCommitted === true) && deps.approvalExemptionStore) {
|
|
785
783
|
try {
|
|
@@ -823,14 +821,17 @@ function redactPendingDisclosures(row) {
|
|
|
823
821
|
const next = redactRiskDescriptor(rd);
|
|
824
822
|
return next === rd ? row : { ...row, riskDescriptor: next };
|
|
825
823
|
}
|
|
826
|
-
function projectPendingForWire(row,
|
|
824
|
+
function projectPendingForWire(row, deployment) {
|
|
825
|
+
const governance = deployment;
|
|
827
826
|
const redacted = redactPendingDisclosures(row);
|
|
828
827
|
const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
|
|
829
|
-
const rememberRefused = !
|
|
828
|
+
const rememberRefused = !standingAnswerDurablyRecordable({
|
|
830
829
|
governanceForced: governanceForced === true,
|
|
831
830
|
requiresRealApproval: row.requiresRealApproval === true,
|
|
832
831
|
mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
|
|
833
|
-
|
|
832
|
+
grantBinding: grantBindingOf(row),
|
|
833
|
+
classes: row.classes,
|
|
834
|
+
deploymentListQuestion: carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes),
|
|
834
835
|
});
|
|
835
836
|
return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
|
|
836
837
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
1
|
+
import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
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";
|
|
@@ -23,7 +23,7 @@ import { workflowModelAllowlistFor } from "../../task-workflow.js";
|
|
|
23
23
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
24
24
|
import * as facts from "../../capability-facts.js";
|
|
25
25
|
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
26
|
-
import { coercePermissionModeStrict,
|
|
26
|
+
import { coercePermissionModeStrict, PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
|
|
27
27
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
28
28
|
function routePairingOf(deps, name, m) {
|
|
29
29
|
if (modelKeyPoisonOf(name, deps.config.modelApiKeyEnv ?? {}, process.env, deps.config.modelApiKeys ?? {}) !== undefined)
|
|
@@ -60,7 +60,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
60
60
|
const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
|
|
61
61
|
const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
|
|
62
62
|
if (requestedModeRaw !== null && requestedMode === undefined) {
|
|
63
|
-
sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${
|
|
63
|
+
sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${PERMISSION_MODE_WORDS}`);
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
const entitlementSource = centerEntitlementSourceWired(deps.config);
|
|
@@ -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,8 @@ 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,
|
|
163
|
+
permissionModes: [...PERMISSION_MODES],
|
|
161
164
|
sideQuery: true,
|
|
162
165
|
agentRoster: facts.agentRoster(deps).available,
|
|
163
166
|
retention: retentionAdvertisement(deps.config),
|
|
@@ -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;
|
|
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
114
114
|
baseRev: string;
|
|
115
115
|
origin: {
|
|
116
116
|
at: number;
|
|
117
|
-
cause?: "static" | "
|
|
117
|
+
cause?: "static" | "unattributed" | "observed" | "derived" | undefined;
|
|
118
118
|
taint: "external";
|
|
119
119
|
};
|
|
120
120
|
};
|
|
@@ -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, policyCannotAnswer, 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 { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, 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 { acceptedInputKindOf } 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";
|
|
@@ -31,6 +34,9 @@ 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";
|
|
33
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]}`;
|
|
34
40
|
const ASK_PARKING_RETRY_AFTER_SEC = 2;
|
|
35
41
|
const ASK_ACTOR_LABEL_MAX = 256;
|
|
36
42
|
const AskDecisionActorSchema = ActorAssertionWire.strip();
|
|
@@ -46,7 +52,7 @@ const AskDecisionBodySchema = z
|
|
|
46
52
|
.refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
|
|
47
53
|
.optional(),
|
|
48
54
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
49
|
-
|
|
55
|
+
decidedBy: z.unknown().optional(),
|
|
50
56
|
})
|
|
51
57
|
.strict();
|
|
52
58
|
const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
|
|
@@ -136,11 +142,17 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
|
|
|
136
142
|
staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
|
|
137
143
|
}, taskId, staleMs);
|
|
138
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
|
+
}
|
|
139
150
|
async function acceptLiveUserInput(deps, res, at) {
|
|
140
151
|
if (!deps.userInputLedger)
|
|
141
152
|
return undefined;
|
|
153
|
+
const input = acceptedInputKindOf(at.tier);
|
|
142
154
|
try {
|
|
143
|
-
return await deps.userInputLedger.accept({ input
|
|
155
|
+
return await deps.userInputLedger.accept({ input, messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
|
|
144
156
|
}
|
|
145
157
|
catch (e) {
|
|
146
158
|
deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
|
|
@@ -429,15 +441,15 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
429
441
|
return;
|
|
430
442
|
}
|
|
431
443
|
if (typeof body.text !== "string" || body.text.length === 0) {
|
|
432
|
-
sendError(res, 400, "request.body_shape",
|
|
444
|
+
sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
|
|
433
445
|
return;
|
|
434
446
|
}
|
|
435
447
|
if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
|
|
436
448
|
sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
|
|
437
449
|
return;
|
|
438
450
|
}
|
|
439
|
-
if (body.priority !== undefined && body.priority
|
|
440
|
-
sendError(res, 400, "request.field_invalid",
|
|
451
|
+
if (body.priority !== undefined && !isSystemInjectionPriority(body.priority)) {
|
|
452
|
+
sendError(res, 400, "request.field_invalid", `priority must be ${STEER_PRIORITY_CHOICES} when present`);
|
|
441
453
|
return;
|
|
442
454
|
}
|
|
443
455
|
const text = body.text;
|
|
@@ -495,19 +507,19 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
495
507
|
return "queue-full";
|
|
496
508
|
}
|
|
497
509
|
if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
|
|
498
|
-
parkDuplicateKey = e
|
|
510
|
+
parkDuplicateKey = e;
|
|
499
511
|
return "duplicate-key";
|
|
500
512
|
}
|
|
501
513
|
throw e;
|
|
502
514
|
}
|
|
503
515
|
};
|
|
504
516
|
const sendQueueFull = () => sendError(res, 409, "steering.queue_full", parkQueueFull ?? "the parked steering queue on this checkpoint is full");
|
|
505
|
-
const sendDuplicateKey = () =>
|
|
517
|
+
const sendDuplicateKey = () => sendSteerReplayRefusal(res, parkDuplicateKey);
|
|
506
518
|
const sendParked = (status) => sendJson(res, 202, { taskId, status, delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
|
|
507
519
|
const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
|
|
508
520
|
const live = steerableRuns.get(taskId);
|
|
509
521
|
if (live) {
|
|
510
|
-
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
|
|
522
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live, tier: priority });
|
|
511
523
|
if (acceptance === "refused")
|
|
512
524
|
return;
|
|
513
525
|
try {
|
|
@@ -526,7 +538,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
526
538
|
return;
|
|
527
539
|
}
|
|
528
540
|
if (code === "steering.duplicate_input_id") {
|
|
529
|
-
parkDuplicateKey = e
|
|
541
|
+
parkDuplicateKey = e;
|
|
530
542
|
sendDuplicateKey();
|
|
531
543
|
return;
|
|
532
544
|
}
|
|
@@ -601,7 +613,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
601
613
|
return;
|
|
602
614
|
}
|
|
603
615
|
if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
|
|
604
|
-
parkDuplicateKey = e
|
|
616
|
+
parkDuplicateKey = e;
|
|
605
617
|
mintResult.duplicateKey = true;
|
|
606
618
|
return;
|
|
607
619
|
}
|
|
@@ -831,7 +843,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
831
843
|
});
|
|
832
844
|
return;
|
|
833
845
|
}
|
|
834
|
-
const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
|
|
846
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream, tier: "now" });
|
|
835
847
|
if (acceptance === "refused")
|
|
836
848
|
return;
|
|
837
849
|
try {
|
|
@@ -848,7 +860,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
848
860
|
return;
|
|
849
861
|
}
|
|
850
862
|
if (code === "steering.duplicate_input_id") {
|
|
851
|
-
|
|
863
|
+
sendSteerReplayRefusal(res, e);
|
|
852
864
|
return;
|
|
853
865
|
}
|
|
854
866
|
if (code !== "steering.not_running")
|
|
@@ -1221,7 +1233,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1221
1233
|
return;
|
|
1222
1234
|
}
|
|
1223
1235
|
const messageId = uuidv7();
|
|
1224
|
-
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
|
|
1236
|
+
const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle, tier: undefined });
|
|
1225
1237
|
if (acceptance === "refused")
|
|
1226
1238
|
return;
|
|
1227
1239
|
const marker = await relayAcceptedInput(deps, acceptance, () => handle.steer(redacted));
|
|
@@ -1361,33 +1373,31 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1361
1373
|
}
|
|
1362
1374
|
const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
|
|
1363
1375
|
if (!parsedBody.success) {
|
|
1364
|
-
|
|
1376
|
+
const retired = retiredDecisionKeyFacts(rawBody);
|
|
1377
|
+
if (retired !== undefined) {
|
|
1378
|
+
sendError(res, 400, "request.body_shape", retired.error, retired.extra);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
|
|
1365
1382
|
return;
|
|
1366
1383
|
}
|
|
1367
1384
|
const body = parsedBody.data;
|
|
1368
|
-
const
|
|
1369
|
-
switch (
|
|
1385
|
+
const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
|
|
1386
|
+
switch (decidedByReading.kind) {
|
|
1370
1387
|
case "not_in_proof":
|
|
1371
|
-
sendError(res, 400, "
|
|
1372
|
-
return;
|
|
1373
|
-
case "deny_only":
|
|
1374
|
-
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
1388
|
+
sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
|
|
1375
1389
|
return;
|
|
1376
|
-
case "
|
|
1377
|
-
sendError(res, 400, "request.field_invalid",
|
|
1390
|
+
case "outside_closed_set":
|
|
1391
|
+
sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
|
|
1378
1392
|
return;
|
|
1379
|
-
case "too_wide":
|
|
1380
|
-
sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
|
|
1381
|
-
return;
|
|
1382
|
-
case "absent":
|
|
1383
1393
|
case "accepted":
|
|
1384
1394
|
break;
|
|
1385
1395
|
default: {
|
|
1386
|
-
const unreachable =
|
|
1387
|
-
throw new Error(`unhandled
|
|
1396
|
+
const unreachable = decidedByReading;
|
|
1397
|
+
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
1388
1398
|
}
|
|
1389
1399
|
}
|
|
1390
|
-
const
|
|
1400
|
+
const decidedBy = decidedByReading.decidedBy;
|
|
1391
1401
|
const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
|
|
1392
1402
|
const taskId = match.params.taskId;
|
|
1393
1403
|
const askId = match.params.askId;
|
|
@@ -1513,6 +1523,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1513
1523
|
sendTerminal(row0);
|
|
1514
1524
|
return;
|
|
1515
1525
|
}
|
|
1526
|
+
if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decidedBy)) {
|
|
1527
|
+
sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1516
1530
|
let outcome;
|
|
1517
1531
|
const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
|
|
1518
1532
|
try {
|
|
@@ -1521,7 +1535,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1521
1535
|
decisionActor: actor,
|
|
1522
1536
|
decisionNote: body.note ?? null,
|
|
1523
1537
|
idempotencyKey: body.idempotencyKey ?? null,
|
|
1524
|
-
...(
|
|
1538
|
+
...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(decidedBy)),
|
|
1525
1539
|
...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
|
|
1526
1540
|
});
|
|
1527
1541
|
}
|
|
@@ -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` 无法跨模块共享自增。判据/时机/配对释放逐字未动。
|