@sema-agent/server 7.108.0 → 7.110.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 +60 -1
- package/README.md +8 -0
- package/README.zh-CN.md +5 -0
- package/USAGE.md +19 -29
- package/dist/approval-ask-machine.d.ts +9 -25
- package/dist/approval-ask-machine.js +5 -2
- package/dist/approval-card.d.ts +33 -3
- package/dist/approval-card.js +26 -2
- package/dist/bench/s1/live-deps.d.ts +4 -5
- package/dist/bench/s1/live-deps.js +9 -10
- package/dist/boot/leader.js +0 -2
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +3 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +6 -12
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +17 -3
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/capabilities/scenarios.d.ts +3 -2
- package/dist/config-catalog.js +2 -4
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +5 -11
- package/dist/config.js +21 -12
- package/dist/deployment-governance.d.ts +8 -2
- package/dist/deployment-governance.js +9 -8
- package/dist/hooks/hook-runner.d.ts +2 -2
- package/dist/hooks/hook-runner.js +40 -26
- package/dist/http/admission.js +8 -2
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/resume-legs.js +6 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/runs.d.ts +0 -1
- package/dist/http/routes/runs.js +22 -0
- package/dist/http/routes/sessions.js +12 -2
- package/dist/http/routes/tasks.js +5 -1
- package/dist/http/server.d.ts +7 -2
- package/dist/http/wire-types.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/instruction-source-notice.d.ts +3 -3
- package/dist/leader/diffup.d.ts +53 -0
- package/dist/leader/diffup.js +95 -1
- package/dist/leader/leader.d.ts +0 -6
- package/dist/leader/leader.js +1 -1
- package/dist/leader/repair-wire.js +1 -1
- package/dist/leader/wire.d.ts +3 -10
- package/dist/leader/wire.js +39 -13
- package/dist/memory-layer-legacy-lock.d.ts +3 -3
- package/dist/model-entry-headers.d.ts +5 -2
- package/dist/model-entry-headers.js +4 -4
- package/dist/model-unreachable-notice.d.ts +2 -2
- package/dist/observability/corrupt-read-seat.d.ts +6 -0
- package/dist/observability/corrupt-read-seat.js +6 -0
- package/dist/observability/fail-open.d.ts +13 -5
- package/dist/observability/fail-open.js +13 -5
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/run-terminal.js +10 -0
- package/dist/operator-ask.d.ts +9 -8
- package/dist/operator-ask.js +9 -1
- package/dist/plugins/approval-ask-store-file.d.ts +2 -1
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
- package/dist/plugins/approval-ask-store-sql.js +7 -0
- package/dist/plugins/file-run-store.js +2 -2
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/pg-pool.js +15 -0
- package/dist/plugins/run-store-sql.d.ts +1 -1
- package/dist/plugins/run-store-sql.js +3 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/store-backend.d.ts +4 -0
- package/dist/plugins/store-backend.js +8 -0
- package/dist/plugins/store-contracts.d.ts +7 -0
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/plugins/tidb-pool.js +17 -0
- package/dist/plugins/user-input-ledger-file.d.ts +33 -0
- package/dist/plugins/user-input-ledger-file.js +98 -0
- package/dist/plugins/user-input-ledger-sql.d.ts +44 -0
- package/dist/plugins/user-input-ledger-sql.js +101 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +18 -5
- package/dist/run-local.js +26 -12
- package/dist/runs.d.ts +8 -1
- package/dist/runs.js +6 -2
- package/dist/runtime-governance.d.ts +23 -11
- package/dist/runtime-governance.js +7 -12
- package/dist/session-records-skipped-notice.d.ts +4 -3
- package/dist/task-settings.d.ts +0 -6
- package/dist/task-settings.js +0 -1
- package/dist/tool-approval.d.ts +44 -15
- package/dist/tool-approval.js +12 -8
- package/dist/trace/core-keyset-guard.d.ts +17 -2
- package/dist/trace/engine-notice-wire.d.ts +30 -35
- package/dist/trace/engine-notice-wire.js +23 -23
- package/dist/trace/project.js +2 -3
- package/dist/trace/self-minted-wire.d.ts +14 -0
- package/dist/trace/self-minted-wire.js +6 -0
- package/dist/unattended-permission-mode.d.ts +54 -93
- package/dist/unattended-permission-mode.js +8 -61
- package/dist/user-input-ledger.d.ts +296 -0
- package/dist/user-input-ledger.js +337 -0
- package/package.json +4 -4
package/dist/run-local.js
CHANGED
|
@@ -9,7 +9,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
|
|
11
11
|
import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
|
|
12
|
-
import {
|
|
12
|
+
import { withUnattendedLeg } from "./unattended-permission-mode.js";
|
|
13
|
+
import { buildUnattendedPolicyRefusal } from "./tool-approval.js";
|
|
13
14
|
import { createBrain } from "./brain.js";
|
|
14
15
|
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
15
16
|
import { applyRuntimeGovernance } from "./runtime-governance.js";
|
|
@@ -130,8 +131,14 @@ const RUN_LOCAL_APPROVAL_SEAT = { kind: "live" };
|
|
|
130
131
|
export function applyLocalGovernance(base, config, workspaceDir, engineDataRoot) {
|
|
131
132
|
return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config, RUN_LOCAL_APPROVAL_SEAT) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
|
|
132
133
|
}
|
|
134
|
+
const NO_TTY_REFUSAL_REASON = "this run-local invocation has no TTY, so there is no one to put this approval to; it is refused fail-closed instead of being asked.";
|
|
135
|
+
const UNDISPLAYABLE_ARGS_REFUSAL_REASON = "the call's arguments cannot be displayed, so there is nothing a person could review; it is refused fail-closed instead of being asked.";
|
|
133
136
|
export function createLocalApprover(io) {
|
|
134
137
|
let tail = Promise.resolve();
|
|
138
|
+
const refuseWithoutPerson = (reason) => {
|
|
139
|
+
io.noteRefusedWithoutPerson();
|
|
140
|
+
return buildUnattendedPolicyRefusal(reason);
|
|
141
|
+
};
|
|
135
142
|
return async (req, signal) => {
|
|
136
143
|
if (!io.isTty) {
|
|
137
144
|
io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
|
|
@@ -139,13 +146,13 @@ export function createLocalApprover(io) {
|
|
|
139
146
|
`APPROVAL_DENY refuses outright without asking), AUTONOMY, MANUAL_MODE_SHELL_GATE, runtime.commandPolicy (config.d/governance.json). ` +
|
|
140
147
|
`Run it in a terminal to answer interactively, or relax the knob (APPROVAL_AUTO_BUDGET auto-approves that many APPROVAL_REQUIRE calls; ` +
|
|
141
148
|
`a separate .env for this CLI keeps a server's lists off it).`);
|
|
142
|
-
return
|
|
149
|
+
return refuseWithoutPerson(NO_TTY_REFUSAL_REASON);
|
|
143
150
|
}
|
|
144
151
|
const argsShown = argsDisplayOf(req.args);
|
|
145
152
|
if (!argsShown.ok) {
|
|
146
153
|
io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — its arguments cannot be displayed ` +
|
|
147
154
|
`(${terminalSafeText(argsShown.reason)}), so there is nothing a person could review; refusing instead of asking.`);
|
|
148
|
-
return
|
|
155
|
+
return refuseWithoutPerson(UNDISPLAYABLE_ARGS_REFUSAL_REASON);
|
|
149
156
|
}
|
|
150
157
|
const prior = tail;
|
|
151
158
|
let release;
|
|
@@ -221,24 +228,28 @@ export async function promptYesNoOnTty(prompt, signal, streams = { input: proces
|
|
|
221
228
|
rl.close();
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
|
-
function
|
|
231
|
+
function unfinishedStopOf(result, refusedWithoutPerson) {
|
|
225
232
|
if (result.terminal.kind !== "completed")
|
|
226
233
|
return undefined;
|
|
227
234
|
if (result.haltedOnUserRejection === true)
|
|
228
235
|
return "haltedOnUserRejection";
|
|
229
236
|
if (result.haltedByUser === true)
|
|
230
237
|
return "haltedByUser";
|
|
238
|
+
if (refusedWithoutPerson > 0)
|
|
239
|
+
return "approvalUnanswered";
|
|
231
240
|
return undefined;
|
|
232
241
|
}
|
|
233
|
-
function
|
|
242
|
+
function buildUnfinishedStopFooter(stop, sessionId) {
|
|
234
243
|
switch (stop) {
|
|
235
244
|
case "haltedOnUserRejection":
|
|
236
245
|
return `task stopped (haltedOnUserRejection): a tool call was refused, so the run did not finish its work — continue it with --session ${sessionId}`;
|
|
237
246
|
case "haltedByUser":
|
|
238
247
|
return `task stopped (haltedByUser): the run was halted before it finished its work — continue it with --session ${sessionId}`;
|
|
248
|
+
case "approvalUnanswered":
|
|
249
|
+
return `task incomplete (approval unanswered): a tool call needed an approval that no person could give in this invocation (see the "denied" lines above), so the run did not finish its work — continue it with --session ${sessionId}`;
|
|
239
250
|
default: {
|
|
240
251
|
const unhandled = stop;
|
|
241
|
-
throw new Error(`
|
|
252
|
+
throw new Error(`unfinished-stop word "${String(unhandled)}" has no footer`);
|
|
242
253
|
}
|
|
243
254
|
}
|
|
244
255
|
}
|
|
@@ -421,10 +432,14 @@ export async function runLocal(argv, deps = {}) {
|
|
|
421
432
|
catch (e) {
|
|
422
433
|
logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "catalog candidate absent — built-ins serve" });
|
|
423
434
|
}
|
|
435
|
+
let refusedWithoutPerson = 0;
|
|
424
436
|
const onAsk = createLocalApprover({
|
|
425
437
|
isTty: deps.isTty ?? process.stdin.isTTY === true,
|
|
426
438
|
askYesNo: deps.askYesNo ?? promptYesNoOnTty,
|
|
427
439
|
printErr,
|
|
440
|
+
noteRefusedWithoutPerson: () => {
|
|
441
|
+
refusedWithoutPerson++;
|
|
442
|
+
},
|
|
428
443
|
});
|
|
429
444
|
const engineOnError = (err, ctx) => {
|
|
430
445
|
if (ctx.phase === "memory") {
|
|
@@ -568,7 +583,6 @@ export async function runLocal(argv, deps = {}) {
|
|
|
568
583
|
...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
|
|
569
584
|
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
570
585
|
};
|
|
571
|
-
const unattendedLeg = createUnattendedLeg(unattendedPermissionMode(config), runnerDeps);
|
|
572
586
|
const spec = withUnattendedLeg(applyLocalGovernance({
|
|
573
587
|
objective: mention.cleanedObjective,
|
|
574
588
|
sessionId,
|
|
@@ -585,7 +599,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
585
599
|
...(mcp ? { mcp } : {}),
|
|
586
600
|
...(a2a ? { a2a } : {}),
|
|
587
601
|
...(Object.keys(limits).length > 0 ? { limits } : {}),
|
|
588
|
-
}, config, workspaceDir, memoryEngine?.root),
|
|
602
|
+
}, config, workspaceDir, memoryEngine?.root), onAsk);
|
|
589
603
|
logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: executionLaneOf(config.remoteExec?.provider) });
|
|
590
604
|
const settleSessionBackground = async () => {
|
|
591
605
|
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, scope);
|
|
@@ -623,14 +637,14 @@ export async function runLocal(argv, deps = {}) {
|
|
|
623
637
|
await fileBackend.dispose();
|
|
624
638
|
return 1;
|
|
625
639
|
}
|
|
626
|
-
const
|
|
640
|
+
const unfinishedStop = unfinishedStopOf(result, refusedWithoutPerson);
|
|
627
641
|
if (args.json) {
|
|
628
642
|
print(JSON.stringify(result, null, 2));
|
|
629
643
|
}
|
|
630
644
|
else if (result.terminal.kind === "completed") {
|
|
631
645
|
print(result.result);
|
|
632
|
-
if (
|
|
633
|
-
printErr(
|
|
646
|
+
if (unfinishedStop !== undefined)
|
|
647
|
+
printErr(buildUnfinishedStopFooter(unfinishedStop, sessionId));
|
|
634
648
|
}
|
|
635
649
|
else {
|
|
636
650
|
const plane = outwardPlane(result.terminal);
|
|
@@ -639,7 +653,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
639
653
|
print(salvage);
|
|
640
654
|
printErr(`task ${plane.status}${plane.errorCode ? ` (${plane.errorCode})` : ""}${plane.errorMessage ? `: ${plane.errorMessage}` : ""}`);
|
|
641
655
|
}
|
|
642
|
-
if (result.terminal.kind !== "completed" ||
|
|
656
|
+
if (result.terminal.kind !== "completed" || unfinishedStop !== undefined)
|
|
643
657
|
exitCode = 1;
|
|
644
658
|
if (memorySync) {
|
|
645
659
|
try {
|
package/dist/runs.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type ToolApprovalCoordinator } from "./tool-approval.js";
|
|
|
9
9
|
import { type FleetRunPublisher } from "./fleet/fleet-bus.js";
|
|
10
10
|
import { type WorkflowCompletionInbox } from "./orchestration/workflow-completion-inbox.js";
|
|
11
11
|
import type { VerifyRoundsSpec } from "./http/verify-rounds.js";
|
|
12
|
+
import type { UserInputLedger } from "./user-input-ledger.js";
|
|
12
13
|
/**
|
|
13
14
|
* stoppedBy (core 1.252): a service cancel is a USER stop — mark the run's still-running background
|
|
14
15
|
* children BEFORE core's teardown reaps them (bare abort = attribution falls back to "system"; core's own
|
|
@@ -368,7 +369,13 @@ pendingEngineNotices?: readonly {
|
|
|
368
369
|
* `TrustedRunInternals` 的白名单会在编译期先红一次(它只开部署方可以决定的那一格)。
|
|
369
370
|
* 缺席 ⇒ 座席与本位存在之前逐字相同。
|
|
370
371
|
*/
|
|
371
|
-
trustedInternals?: Pick<RunInternals, "workflowParkedResume"
|
|
372
|
+
trustedInternals?: Pick<RunInternals, "workflowParkedResume">,
|
|
373
|
+
/**
|
|
374
|
+
* S-928 用户输入受理账本(`src/user-input-ledger.ts`;路由层 `deps.userInputLedger`)。两手:开通告口那一拍(模型调用之前)把本会话
|
|
375
|
+
* 已收敛、未告知的崩溃丢失铸成 `task.user_steer_undrained{cause:"crashed"}`;腿收场时把本腿上的 steer 受理 settle 掉。
|
|
376
|
+
* 缺席 ⇒ 两手都不做(与本参数存在之前逐字相同)。
|
|
377
|
+
*/
|
|
378
|
+
userInputLedger?: UserInputLedger): Promise<void>;
|
|
372
379
|
/** How often a running instance refreshes its run's updated_at (liveness, independent of events).
|
|
373
380
|
* Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
|
|
374
381
|
* reaper would race live runs. [ref]:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
|
package/dist/runs.js
CHANGED
|
@@ -173,7 +173,7 @@ export function evictIfConflict(runner, sessionId, result) {
|
|
|
173
173
|
runner.sessions.evict?.(sessionId);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals) {
|
|
176
|
+
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals, userInputLedger) {
|
|
177
177
|
const startedAt = Date.now();
|
|
178
178
|
metrics?.addGauge("runs_active", 1);
|
|
179
179
|
fleetPublisher?.onStart();
|
|
@@ -270,9 +270,11 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
270
270
|
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
271
271
|
sessionId: spec.sessionId,
|
|
272
272
|
...(pendingEngineNotices !== undefined ? { pending: pendingEngineNotices } : {}),
|
|
273
|
-
durable: (row) =>
|
|
273
|
+
durable: (row) => append("engine_notice", row).then(() => true, () => (recordFailOpen("server.engine-notice.durable-append-failed", `leg=bg code=${row.code} task=${taskId}`), false)),
|
|
274
274
|
});
|
|
275
275
|
try {
|
|
276
|
+
if (userInputLedger && spec.sessionId)
|
|
277
|
+
await userInputLedger.announceUndrained({ sessionId: spec.sessionId, taskId });
|
|
276
278
|
await emitPendingWorkflowCompletions(workflowCompletionInbox, spec.sessionId, elicitOwner ?? owner, (frame) => {
|
|
277
279
|
const { type, ...rest } = frame;
|
|
278
280
|
const f = frame;
|
|
@@ -538,6 +540,8 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
538
540
|
...(spec.sessionId ? { transcriptId: spec.sessionId } : {}),
|
|
539
541
|
});
|
|
540
542
|
}
|
|
543
|
+
if (steerableStreamRef !== undefined)
|
|
544
|
+
await userInputLedger?.settleLeg(steerableStreamRef);
|
|
541
545
|
if (inflight?.get(taskId) === cancelCtrl)
|
|
542
546
|
inflight.delete(taskId);
|
|
543
547
|
if (preemptable?.get(taskId) === preemptCtrl)
|
|
@@ -61,11 +61,11 @@ export declare const COARSE_SHELL_TOOLS_MIRROR: readonly string[];
|
|
|
61
61
|
* OK). Called at config-apply (config-center/facade.ts `applyRuntimeHot`) so a malformed rule that BYPASSED the
|
|
62
62
|
* registry schema (a hand-edited config.d / a non-conformant publish) is rejected LOUDLY rather than
|
|
63
63
|
* silently mis-compiling — review MEDIUM: an out-of-enum `decision` (e.g. "DENY") would otherwise fall through
|
|
64
|
-
* {@link
|
|
64
|
+
* {@link compileCommandPolicyLayers}'s `decision==="deny"?…:"ask"` to ASK, silently WEAKENING an intended deny. */
|
|
65
65
|
export declare function validateCommandRules(rules: CommandRule[]): string[];
|
|
66
66
|
/**
|
|
67
|
-
* Compile `runtime.commandPolicy` (per-command 3-way rules) into
|
|
68
|
-
* is nothing to govern. The registry model (per-command `allow`/`ask`/`deny`) and core's helper model
|
|
67
|
+
* Compile `runtime.commandPolicy` (per-command 3-way rules) into the command gate's LEAF policies (`[]` when there
|
|
68
|
+
* is nothing to govern). The registry model (per-command `allow`/`ask`/`deny`) and core's helper model
|
|
69
69
|
* (`allow[]` + `deny[]` + `defaultAction`) don't map 1:1, so the compilation picks a mode by the rules present:
|
|
70
70
|
*
|
|
71
71
|
* - **Allowlist mode** — triggered when ANY rule resolves to `allow`. Only `allow` commands pass; `deny` wins;
|
|
@@ -81,7 +81,12 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
|
|
|
81
81
|
* strictest-wins (without this, the `allow` rule would shadow the `ask` for that command in allowlist mode).
|
|
82
82
|
*
|
|
83
83
|
* Either way the result only ever ADDS asks/denies — composed onto the baseline via `combinePolicies`
|
|
84
|
-
* (deny-wins) by {@link applyRuntimeGovernance}, so it is tighten-only.
|
|
84
|
+
* (deny-wins) by {@link applyRuntimeGovernance}, so it is tighten-only.
|
|
85
|
+
*
|
|
86
|
+
* 🔴 **S-958:返回叶,不在这里折**(改名自 `compileCommandPolicy`,修前返回 blocklist 形里 `combinePolicies(coarseDeny, askList)` 的
|
|
87
|
+
* 一只折叠)。复合命令(`a && b`、管道、`;`)上两只叶都按「解析不了 ⇒ 问」问 ⇒ 那只折叠带出 `foldedAskClasses`,而治理层随后要给每只
|
|
88
|
+
* ask 盖部署出身 —— 盖在折叠的输出上就是展开一只 core 担保过身份的对象(core #1266 7.39.0 按名拒)。叶交出去,由
|
|
89
|
+
* {@link applyRuntimeGovernance} 逐叶盖位、折**一次**:本函数不知道出身这件事,治理层只有一个折叠点。 (Assumes commands are pre-validated by
|
|
85
90
|
* {@link validateCommandRules}; an un-matchable command here is simply inert, never a parse error.)
|
|
86
91
|
*
|
|
87
92
|
* 🔴 **S-496:词表外的 `decision` 折 `deny`,不折 `ask`。** {@link validateCommandRules} 的头注写下的那条
|
|
@@ -91,7 +96,7 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
|
|
|
91
96
|
* 收口:`VALID_DECISIONS` 之外的值一律折最严的 `deny`(fail-closed,[ref] 的运行期 miss 臂,下方自陈),
|
|
92
97
|
* 闭集本身则由契约 zod 派生 ⇒ 契约加一个判词,`RANK` 与桶分配的穷尽 switch 双双**编译红**。
|
|
93
98
|
*/
|
|
94
|
-
export declare function
|
|
99
|
+
export declare function compileCommandPolicyLayers(rules: CommandRule[] | undefined): ToolPolicy[];
|
|
95
100
|
/**
|
|
96
101
|
* Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2):
|
|
97
102
|
* - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the
|
|
@@ -188,9 +193,9 @@ export interface GovernancePosture {
|
|
|
188
193
|
* (checkpoint 行没有这一列,core 的 `CheckpointGate`/`PendingAction`/`Checkpoint` 顶层都不带它)。反向
|
|
189
194
|
* (checkpoint → ask)今天**没有读口**:两条 list 读口都硬过滤 `state='STREAM_PENDING'`,而 park 完的行是
|
|
190
195
|
* `PARKED`;唯一精确的连接键是 ask 行上的 `gate_token`,要按它反查得给店加一个新读口(SQL 面 = 双库集成
|
|
191
|
-
* 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)
|
|
192
|
-
*
|
|
193
|
-
* 比缺席坏得多(`http/active-run-conflict.ts` 的 `resumeEntryForGate` 的 default 臂是同一条纪律)。故诚实缺席。
|
|
196
|
+
* 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)、`parentToolCallId` 与这一问的
|
|
197
|
+
* 类集(S-944);前两维从 checkpoint 行推不出来,类集在 park 行上老行缺席、续跑不补 —— 二腿/子代/老行形上会算出一个
|
|
198
|
+
* **错**的 id —— 在「等的谁」这条通道上,错 id 比缺席坏得多(`http/active-run-conflict.ts` 的 `resumeEntryForGate` 的 default 臂是同一条纪律)。故诚实缺席。
|
|
194
199
|
*/
|
|
195
200
|
export declare function governanceOriginOf(gate: {
|
|
196
201
|
riskDescriptor?: {
|
|
@@ -216,20 +221,27 @@ export declare function governanceOriginOf(gate: {
|
|
|
216
221
|
* 本仓的类级门是 test/operator-knob-client-posture-matrix.test.ts 的 旋钮×表态×lane 矩阵):
|
|
217
222
|
* `SENSITIVE_WRITE_PATTERNS` 是 [ref] 那次搬家漏下的兄弟旋钮——同一个病灶原样复发在 sensitive-path
|
|
218
223
|
* DENY 腿上(唯一合成点在 deriveSettingsPolicy 的 fsWriteGate 闭包,bypassPermissions/键缺席/settings
|
|
219
|
-
* 整缺席三形整条腿不建)。同方修:调用方(resolve-spec)把 operator 守卫集预铸成 `
|
|
220
|
-
* 从这里**无条件施加**——它只在守卫段上产 deny(core 语义 deny/"safety"),对其余目标返回的是
|
|
224
|
+
* 整缺席三形整条腿不建)。同方修:调用方(resolve-spec)把 operator 守卫集预铸成 `sensitivePathLayers`(叶;S-958 起不再是
|
|
225
|
+
* 一只预折的策略,见下段)从这里**无条件施加**——它只在守卫段上产 deny(core 语义 deny/"safety"),对其余目标返回的是
|
|
221
226
|
* `action:"allow"`(**不是**「无意见」这种第三态:core 的 ToolPolicy 没有弃权值。它在
|
|
222
227
|
* `combinePolicies` 的 deny/ask 优先折叠里不构成一票,所以在**折叠语境**下等价于弃权;若日后有人把
|
|
223
228
|
* 这条策略单独当成唯一 policy 用,那个 allow 就是真放行——别照抄这句话去别的语境)。于是对 bypass
|
|
224
229
|
* 只挡守卫段、不给普通目标加 ask 门([ref] 表第四行「bypass=不加门非开门」保持逐字)。策略在这里
|
|
225
230
|
* 只组合不构造:裁决 env 的 lane 分形(host 真 fs / 沙箱 deferred 代理)是 resolve-spec 的属主知识,
|
|
226
231
|
* governance 层不重复它。
|
|
232
|
+
*
|
|
233
|
+
* 🔴 **S-958(core #1266 7.39.0)—— 治理层只有一个折叠点**:两种输入都交**叶**(`compileCommandPolicyLayers` 的那几只、
|
|
234
|
+
* `sensitivePathLayers` 的那几只),本函数对每一片叶各盖一次部署出身(`createDeploymentAuthoredPolicy`),再交 core
|
|
235
|
+
* `combinePolicies` 折一次;折叠是最外层,输出经标记观察器原物交回。修前是「先折、再在折叠输出上展开盖位」——折叠带出
|
|
236
|
+
* `foldedAskClasses` 时交给 core 的是副本,7.39.0 起按名拒(工具不跑、无卡)。
|
|
227
237
|
*/
|
|
228
238
|
export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
|
|
229
239
|
autonomy?: Autonomy;
|
|
230
240
|
commandPolicy?: CommandRule[];
|
|
231
241
|
manualModeShellGate?: ShellGateDoctrine;
|
|
232
|
-
|
|
242
|
+
/** 守卫集的**叶**(`deployment-governance.ts` `createDeploymentGovernanceInputs` 预铸;S-958 起是叶的列表,不是一只预折的策略)。
|
|
243
|
+
* 空列表与缺席同义。 */
|
|
244
|
+
sensitivePathLayers?: readonly ToolPolicy[];
|
|
233
245
|
/** [ref]/[ref] `governanceForced` 的标记表(缺省 = **当前 run 腿的 ALS 表**,见
|
|
234
246
|
* `governance-ask-marks.ts` 顶注「作用域」段)。注入口只为测试与将来的多实例形。 */
|
|
235
247
|
askMarks?: GovernanceAskMarks;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, wrapToolPolicy, } from "@sema-agent/core";
|
|
2
2
|
import { GovernanceConfig } from "@sema-agent/settings-schema";
|
|
3
3
|
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createDeploymentAuthoredPolicy, operatorAsk } from "./operator-ask.js";
|
|
5
5
|
import { heldRuleName } from "./tool-rule-names.js";
|
|
6
6
|
const COMMAND_RULE_SHAPE = GovernanceConfig.shape.commandPolicy.unwrap().element.shape;
|
|
7
7
|
const COMMAND_DECISIONS = COMMAND_RULE_SHAPE.decision.options;
|
|
@@ -57,9 +57,9 @@ export function validateCommandRules(rules) {
|
|
|
57
57
|
}
|
|
58
58
|
return errors;
|
|
59
59
|
}
|
|
60
|
-
export function
|
|
60
|
+
export function compileCommandPolicyLayers(rules) {
|
|
61
61
|
if (!rules || rules.length === 0)
|
|
62
|
-
return
|
|
62
|
+
return [];
|
|
63
63
|
const RANK = { deny: 3, ask: 2, allow: 1 };
|
|
64
64
|
const strictest = new Map();
|
|
65
65
|
for (const r of rules) {
|
|
@@ -98,9 +98,7 @@ export function compileCommandPolicy(rules) {
|
|
|
98
98
|
if (ask.length > 0)
|
|
99
99
|
policies.push(createCommandAskListPolicy(ask));
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
return undefined;
|
|
103
|
-
return policies.length === 1 ? policies[0] : combinePolicies(...policies);
|
|
101
|
+
return policies;
|
|
104
102
|
}
|
|
105
103
|
export function autonomyOverrides(autonomy) {
|
|
106
104
|
switch (autonomy) {
|
|
@@ -125,7 +123,7 @@ export function governanceOriginOf(gate, governance) {
|
|
|
125
123
|
function createGovernanceAskMarkingPolicy(inner, marksOf) {
|
|
126
124
|
const note = (d, req) => {
|
|
127
125
|
marksOf()?.setMarked(req.toolCallId, d.action === "ask");
|
|
128
|
-
return d
|
|
126
|
+
return d;
|
|
129
127
|
};
|
|
130
128
|
return wrapToolPolicy(inner, (req, signal) => {
|
|
131
129
|
const decision = inner.check(req, signal);
|
|
@@ -145,11 +143,8 @@ function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS
|
|
|
145
143
|
export function applyRuntimeGovernance(base, governance) {
|
|
146
144
|
const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
|
|
147
145
|
const overrides = { ...autonomyOverrides(governance.autonomy) };
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const governancePolicy = governancePolicies.length > 0
|
|
151
|
-
? createGovernanceAskMarkingPolicy(governancePolicies.length === 1 ? governancePolicies[0] : combinePolicies(...governancePolicies), marksOf)
|
|
152
|
-
: undefined;
|
|
146
|
+
const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? [])];
|
|
147
|
+
const governancePolicy = layers.length > 0 ? createGovernanceAskMarkingPolicy(combinePolicies(...layers.map(createDeploymentAuthoredPolicy)), marksOf) : undefined;
|
|
153
148
|
if (governancePolicy !== undefined)
|
|
154
149
|
overrides.toolPolicy = governancePolicy;
|
|
155
150
|
if (governance.manualModeShellGate === "always")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { FileStorageCorruptReadInfo, SessionRepo } from "@sema-agent/core";
|
|
2
|
-
import { type EngineNoticeRouter
|
|
3
|
-
|
|
1
|
+
import type { EngineNoticeCode, FileStorageCorruptReadInfo, SessionRepo } from "@sema-agent/core";
|
|
2
|
+
import { type EngineNoticeRouter } from "./trace/engine-notice-wire.js";
|
|
3
|
+
/** 码常量型 = core 码册(#1240 入册:受众 user、铸者 `NOTICE_MINTER` = `"server"`)。 */
|
|
4
|
+
export declare const SESSION_RECORDS_SKIPPED: EngineNoticeCode;
|
|
4
5
|
/**
|
|
5
6
|
* 披露座上的「作用域在场则记」腿(装配点 = `plugins/store-backend.ts` 里喂给 `LocalSessionStore` 的那只
|
|
6
7
|
* `FileSessionRepo` 的 `onCorruptRead`)。只认 `kind:"replay"` 这一级(`record` 由它汇总,`file` 没有会话归属)。
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -137,12 +137,6 @@ export declare function effectiveThinking(reasoningEffort: unknown, ultracode: b
|
|
|
137
137
|
* field is the explicit per-turn intent → it WINS over a bundle defaultMode). Creates a minimal settings object when
|
|
138
138
|
* no `body.settings` bundle was sent. The result flows through {@link applyTaskSettings} (tighten-only). */
|
|
139
139
|
export declare function withPermissionMode(settings: ParsedTaskSettings | undefined, mode: SettingsPermissionMode): ParsedTaskSettings;
|
|
140
|
-
/**
|
|
141
|
-
* 7.106.0 合并复审 X8(opus O-6)—— 请求**没有表态**模式时引擎实际跑的那个词。core 7.34.0 [ref]「Absent = `default`」
|
|
142
|
-
* (装树 `dist/core/runner/prepare-task.js` 的 `requested ?? "default"`)。只给**观测面**用(hook 载荷的 `permission_mode`,CC 恒带这个键):
|
|
143
|
-
* spec 过境点照旧不写键(`effectivePermissionMode` 缺席 ⇒ 不替调用方选 —— 那是引擎的默认,不是本仓的表态)。
|
|
144
|
-
*/
|
|
145
|
-
export declare const ENGINE_ABSENT_PERMISSION_MODE: "default";
|
|
146
140
|
/**
|
|
147
141
|
* [ref] §6-1 —— 本请求的**生效** permission mode,单点。优先序逐字同 {@link withPermissionMode}:
|
|
148
142
|
* 顶层 `body.permissionMode`(本轮显式表态)赢过 bundle 的 `settings.permissions.defaultMode`;两处
|
package/dist/task-settings.js
CHANGED
|
@@ -47,7 +47,6 @@ export function withPermissionMode(settings, mode) {
|
|
|
47
47
|
const base = settings ?? {};
|
|
48
48
|
return { ...base, permissions: { ...base.permissions, defaultMode: mode } };
|
|
49
49
|
}
|
|
50
|
-
export const ENGINE_ABSENT_PERMISSION_MODE = "default";
|
|
51
50
|
export function effectivePermissionMode(body, settings) {
|
|
52
51
|
const requested = coercePermissionMode(body.permissionMode) ?? settings?.permissions?.defaultMode;
|
|
53
52
|
const kill = settings?.permissions?.disableAutoMode;
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HostDecidedBy, HostSettledBy } from "./host-decision.js";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
3
|
+
import { type AskClass, type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
4
4
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
5
5
|
import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
6
6
|
import { type ApprovalAskStore, type AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
@@ -285,6 +285,19 @@ export interface ToolApprovalFrame {
|
|
|
285
285
|
* ask」,治理策略产的与普通策略产的在这一个词上同形 ⇒ 顶替不了(逐字论证见 `governance-ask-marks.ts` 顶注)。
|
|
286
286
|
*/
|
|
287
287
|
origin?: string;
|
|
288
|
+
/**
|
|
289
|
+
* **S-934 ②(core 7.37.0 #1214;ADDITIVE,`"tool_approval"` only)** —— 这一问的**类**:`classes` 的首员(core `ASK_CLASSES` 序),
|
|
290
|
+
* `MODE_ARBITRATION` 的行键(权限模式读的就是它 / 它们)。引擎在每一个铸 `origin` 的站盖章(策略自报的值在盖章处被替换),本仓经
|
|
291
|
+
* `approval-card.ts` 的 `readAskClassPair(req)` 窄读(判法 = core 屏 `screenAskClassMembers`,S-955 ⑥ / #1249,本仓零自写对判;屏拒 ⇒ 两键都不投 + 计一次
|
|
292
|
+
* S-249 集外 tag)。**与 {@link classes} 同在同缺**;缺席 = 老引擎 / 不盖类的站(共享制品读确认,core KL-1214-a),不是「无类」。
|
|
293
|
+
* display:壳按类渲卡(cli 卡面、client-core CC-272),durable 审批席按类答;**本仓不按它裁决**(无人值守腿的按类作答读 core 交来的
|
|
294
|
+
* `AskRequest.classes` 本身)。同一对的其余面:`card_json` 卡面 / `approval_request` 呈卡帧 / 断连重放帧(同一份素材)、运维队列行
|
|
295
|
+
* (`parkRowFacts`,读停车行孪生座 `PendingAction.tool_approval.askClass`);inbox 行不带(core `CheckpointSummary` 不回显)。
|
|
296
|
+
*/
|
|
297
|
+
askClass?: AskClass;
|
|
298
|
+
/** S-934 ②:这一问属于的**全部**类(core `ASK_CLASSES` 序,`classes[0] === askClass`,非空)—— 读整只集合,别只读 {@link askClass}(一问可同属
|
|
299
|
+
* 几类)。与 `askClass` 同在同缺、同一只窄读;逐字同序过境,不排序不去重。 */
|
|
300
|
+
classes?: AskClass[];
|
|
288
301
|
/**
|
|
289
302
|
* **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**:
|
|
290
303
|
* 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。
|
|
@@ -825,6 +838,13 @@ export type UnattendedPolicyRefusal = Extract<AskOutcome, {
|
|
|
825
838
|
settledBy: Extract<HostSettledBy, "policy">;
|
|
826
839
|
reason: string;
|
|
827
840
|
};
|
|
841
|
+
/**
|
|
842
|
+
* {@link UnattendedPolicyRefusal} 的**唯一**构造(纯数据 ⇒ `build*`)。来源词读店落行的**同一个**常量(`POLICY_REFUSAL_DECISION`,NP-2):
|
|
843
|
+
* 行上记的与交给 core 的在构造上是一个词。两个铸点读它:协调器 deny 政策的「无人可答」({@link ToolApprovalCoordinator.unattendedOutcome})
|
|
844
|
+
* 与 run-local 本地席没有人可问的两臂(S-956:无 TTY / 参数展示不出 —— `src/run-local.ts` `createLocalApprover`)。`reason` = 铸点的
|
|
845
|
+
* 闭集拒因句(core 以 `policy note` 围栏交给模型),不带调用方的字节。
|
|
846
|
+
*/
|
|
847
|
+
export declare function buildUnattendedPolicyRefusal(reason: string): UnattendedPolicyRefusal;
|
|
828
848
|
/** 「无人可答」终局的**闭集** —— core `AskOutcome` 联合里只有这两臂由 {@link ToolApprovalCoordinator.unattendedOutcome}
|
|
829
849
|
* 铸:park 政策的路由答 `"unavailable"`,deny 政策的 {@link UnattendedPolicyRefusal}。没有布尔臂(S-689:裸 `false`
|
|
830
850
|
* 是人的拒绝,这个口从不替人说话)。 */
|
|
@@ -1440,23 +1460,31 @@ export declare class ToolApprovalCoordinator {
|
|
|
1440
1460
|
sessionId?: string;
|
|
1441
1461
|
}) => ((req: AskRequest) => Promise<AskOutcome>);
|
|
1442
1462
|
/**
|
|
1443
|
-
* 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref]
|
|
1463
|
+
* 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref] 换判据;S-944 / SN-C1 换读口)。
|
|
1464
|
+
*
|
|
1465
|
+
* 回 `true` = 「这次调用在这条腿上从来没有过持久出处」⇒ 一揽子放行可以短路;
|
|
1466
|
+
* 回 `false` = 这次调用在腿内有行(任何状态、任何 ask 身份),**或**读不出来(不知道 = 不放宽)⇒ 调用方必须往下走真出卡。
|
|
1467
|
+
*
|
|
1468
|
+
* 为什么只读不写:短路的全部价值就是「不铸卡、不落行、不占 pending」,做一次按调用坐标的读保住这个价值,
|
|
1469
|
+
* 而 `ensureAsk` 会写行。为什么一行都没有就放行:这次调用在这条腿上的行只可能由**这次调用的某一问**铸出来,
|
|
1470
|
+
* 一行都没有 ⇒ 从来没有过一份能反驳本地判据的持久出处(这也是常见形:grant 命中的调用多半是头一次到)。
|
|
1444
1471
|
*
|
|
1445
|
-
*
|
|
1446
|
-
* 回 `false` = 行在(任何状态),**或**读不出来(不知道 = 不放宽)⇒ 调用方必须往下走真出卡。
|
|
1472
|
+
* ## 争议按**调用**判,不按 ask 身份判(S-944 / SN-C1)
|
|
1447
1473
|
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1474
|
+
* 争议的真源是调用:人对这次调用说过什么、这次调用还有没有一张卡挂着。ask 身份(`deriveAskId`)只是其中**一问**的主键,而身份会加轴 ——
|
|
1475
|
+
* S-944 把这一问的类集并进身份后,按身份点读会把两类行读成「不存在」:① 同一调用另一类集的那一问(core #1218 的委派第二张卡与首卡);
|
|
1476
|
+
* ② 升级前按旧派生写的行(同一 resume token 重投 = 同一 `legKey`,跨版本重入同一调用)。两形都让一揽子放行代答、人拒 / 挂着的卡被绕过
|
|
1477
|
+
* (codex 车内 r1 [high] + [ref] gpt-6 腿 A 同指)。⇒ 读口 = 店的 `listByCall`(四列 = 这次调用在这条腿上的坐标,精确匹配),本函数不派生 askId。
|
|
1478
|
+
* **身份可以加轴,争议判据不得依赖身份。**
|
|
1451
1479
|
*
|
|
1452
1480
|
* ## 判据的单一原则:**短路不得产生与「没有 grant 时那条路」不同的终局**
|
|
1453
1481
|
*
|
|
1454
|
-
* 没有 grant
|
|
1455
|
-
* 走 `replayTerminalAskRow`(deny 回放 `false`、park 回放 `"unavailable"`)
|
|
1456
|
-
*
|
|
1457
|
-
* ——
|
|
1482
|
+
* 没有 grant 时,这次调用在腿内的任何一行都让下面 `askBroadcast` 有确定去处:同身份的终局行(DECIDED/PARKED/DENIED/VOID)
|
|
1483
|
+
* 走 `replayTerminalAskRow`(deny 回放 `false`、park 回放 `"unavailable"`);同身份的活 STREAM_PENDING 行走 `ensureAsk` 的幂等命中
|
|
1484
|
+
* (采信行上的卡与 deadline,等真决议);别的身份的行(另一类集 / 旧派生)⇒ 本问的身份无行 ⇒ 出卡再问(fail-closed)。短路一旦 return,
|
|
1485
|
+
* 这些腿结构上都够不着 —— 所以只要这次调用在腿内有行,短路就是在用一个**更宽**的答案顶替那条路。⇒ 判据 = `rows.length === 0`。
|
|
1458
1486
|
*
|
|
1459
|
-
*
|
|
1487
|
+
* 这条原则是分几次到位的,每次漏的都是「行 = 真源」的一部分:
|
|
1460
1488
|
* · **[ref]**(codex R1-[high]):首版只查**本地** peek,行上标治理的那一半漏了 —— 本地标表是
|
|
1461
1489
|
* 进程内表,failover / 重启后幂等命中既有行时它恒空。
|
|
1462
1490
|
* · **[ref]**(2026-08-19 三轴组复审,refuter 真运行复现三形):补丁把行读到手里,却只看
|
|
@@ -1466,12 +1494,13 @@ export declare class ToolApprovalCoordinator {
|
|
|
1466
1494
|
* · **codex 交叉复审**([ref] 同批,验真后采纳):只补终态判据仍漏「卡还在别处挂着」——
|
|
1467
1495
|
* STREAM_PENDING 行意味着已有一张卡在等人,凭 grant 抢跑会让行上的终局与真实执行长期矛盾
|
|
1468
1496
|
* (那张卡随后可被人/另一副本决成相反答案,或 TTL 到点被收敛成幻影 gate)。
|
|
1469
|
-
*
|
|
1497
|
+
* · **S-944 / SN-C1**(见上节):读口按身份点读,身份加轴后漏掉同调用别的问与旧派生的行。
|
|
1498
|
+
* 几次都不是各自独立的 bug,是同一句话没说全,故最终判据收成一条而不是几个分支。
|
|
1470
1499
|
*
|
|
1471
|
-
* 零额外 IO(
|
|
1500
|
+
* 零额外 IO(一次读),且 fail-closed **by construction**:将来 `AskState` 加词、ask 身份再加轴,都不需要动这里。
|
|
1472
1501
|
*
|
|
1473
1502
|
* 🔴 **已登记的残余([ref],PARTIAL,本批不修)**:这一步是一次**非原子的 check-then-act** ——
|
|
1474
|
-
*
|
|
1503
|
+
* 另一副本正在为同一调用落行、而本次按调用坐标的读(`listByCall`)抢在其提交前完成时,本副本仍会短路。真解 = 把出处
|
|
1475
1504
|
* 判定与 grant 消费合并成**同一次**原子存储操作(带条件 upsert / CAS),属店语义级改动,须本模块属主
|
|
1476
1505
|
* 立件(与上面 R4-F1/[ref] 两条同源残余同一处置)。「行不在 ⇒ 不短路」不是修法:grant 命中的 ask
|
|
1477
1506
|
* 多半头一次到,那等于把一揽子放行整只废掉。窗的上界由本判据定死:行**一提交**窗即关。
|
package/dist/tool-approval.js
CHANGED
|
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
|
|
|
8
8
|
import { createLogger } from "./observability/logger.js";
|
|
9
9
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
10
10
|
import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
11
|
-
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
11
|
+
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
12
12
|
import { MAX_SETTLED_BY_CHARS, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth, settledByFitsColumn } from "./plugins/approval-ask-store-sql.js";
|
|
13
13
|
import { settleByPolicy } from "./approval-policy-settlement.js";
|
|
14
14
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
@@ -210,6 +210,9 @@ export function parseToolApprovalResponse(body) {
|
|
|
210
210
|
}
|
|
211
211
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
212
212
|
}
|
|
213
|
+
export function buildUnattendedPolicyRefusal(reason) {
|
|
214
|
+
return { allow: false, settledBy: POLICY_REFUSAL_DECISION.settledBy, reason };
|
|
215
|
+
}
|
|
213
216
|
function unattendedRefusalReason(arm) {
|
|
214
217
|
return ("UNATTENDED_APPROVAL_POLICY=deny: this deployment declares that no one attends its approvals, so an approval " +
|
|
215
218
|
`request no one can answer is refused here instead of being parked for a person (arm: ${arm}).`);
|
|
@@ -399,7 +402,7 @@ export class ToolApprovalCoordinator {
|
|
|
399
402
|
outcomeWord = "unavailable";
|
|
400
403
|
break;
|
|
401
404
|
case "deny":
|
|
402
|
-
outcome =
|
|
405
|
+
outcome = buildUnattendedPolicyRefusal(unattendedRefusalReason(site.arm));
|
|
403
406
|
outcomeWord = "deny";
|
|
404
407
|
break;
|
|
405
408
|
default: {
|
|
@@ -776,14 +779,13 @@ export class ToolApprovalCoordinator {
|
|
|
776
779
|
async sessionGrantUncontestedByRow(origin, req) {
|
|
777
780
|
if (!this.askStore)
|
|
778
781
|
return true;
|
|
779
|
-
const
|
|
780
|
-
const askId = deriveAskId(sourceTaskId, origin.taskId, req.toolCallId, origin.legKey, req.delegation?.parentToolCallId);
|
|
782
|
+
const call = { taskId: origin.taskId, legKey: origin.legKey, toolCallId: req.toolCallId, sourceTaskId: req.sourceTaskId ?? origin.taskId };
|
|
781
783
|
try {
|
|
782
|
-
const
|
|
783
|
-
return
|
|
784
|
+
const rows = await this.withStoreDeadline(this.askStore.listByCall(call), "listByCall(session-grant-provenance)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
785
|
+
return rows.length === 0;
|
|
784
786
|
}
|
|
785
787
|
catch (err) {
|
|
786
|
-
this.noteStoreError(err, "
|
|
788
|
+
this.noteStoreError(err, "listByCall(session-grant-provenance)");
|
|
787
789
|
return false;
|
|
788
790
|
}
|
|
789
791
|
}
|
|
@@ -1060,6 +1062,7 @@ export class ToolApprovalCoordinator {
|
|
|
1060
1062
|
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
|
|
1061
1063
|
const mandateWord = readClosedWordSeat(req, "mandate");
|
|
1062
1064
|
const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
|
|
1065
|
+
const askClassPair = readAskClassPair(req);
|
|
1063
1066
|
const frame = {
|
|
1064
1067
|
type: "tool_approval",
|
|
1065
1068
|
approvalId: id,
|
|
@@ -1087,6 +1090,7 @@ export class ToolApprovalCoordinator {
|
|
|
1087
1090
|
...(req.mandated === true ? { mandated: true } : {}),
|
|
1088
1091
|
...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
|
|
1089
1092
|
...(originWord !== undefined ? { origin: originWord } : {}),
|
|
1093
|
+
...(askClassPair ?? {}),
|
|
1090
1094
|
...(typeof req.persistedRuleShadowed === "string" && req.persistedRuleShadowed !== ""
|
|
1091
1095
|
? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
|
|
1092
1096
|
: {}),
|
|
@@ -1138,7 +1142,7 @@ export class ToolApprovalCoordinator {
|
|
|
1138
1142
|
const sourceTaskId = req.sourceTaskId ?? origin.taskId;
|
|
1139
1143
|
const runId = origin.taskId;
|
|
1140
1144
|
const legKey = origin.legKey;
|
|
1141
|
-
askId = deriveAskId(sourceTaskId, runId,
|
|
1145
|
+
askId = deriveAskId(sourceTaskId, runId, legKey, req);
|
|
1142
1146
|
batchId = this.batchIdFor(askId, sourceTaskId, runId, legKey);
|
|
1143
1147
|
try {
|
|
1144
1148
|
const existingOrCreated = await this.withStoreDeadline(this.askStore.ensureAsk({
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* ⑨⑩([ref],2026-08-31)把两个最大的 wire 面也纳入:TaskSpec 按「server 有无写点」记 Wired/Unwired,
|
|
18
18
|
* TaskResult 按「转发/摘除」记 Forwarded/Stripped —— 谁改 resolve-spec.ts / runs.ts 的发射点谁同步本文件。
|
|
19
19
|
*/
|
|
20
|
-
import type { TaskNotificationPayload, SemaProvenance, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, PendingAction, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult, SideQueryResult, WiringManifest, GateOutcome, GateDisposition, Settlement, PausedCause, WorkflowRun, ReadRootGrantCandidate, McpReconnectResult, McpServerStatus, NodeLspManagerOptions, RunnerDeps } from "@sema-agent/core";
|
|
20
|
+
import type { TaskNotificationPayload, SemaProvenance, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, PendingAction, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult, SideQueryResult, WiringManifest, GateOutcome, GateDisposition, Settlement, PausedCause, TerminalCause, WorkflowRun, ReadRootGrantCandidate, McpReconnectResult, McpServerStatus, NodeLspManagerOptions, RunnerDeps, SessionMemoryStatus } from "@sema-agent/core";
|
|
21
21
|
import type { ToolApprovalFrame } from "../tool-approval.js";
|
|
22
22
|
/** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。
|
|
23
23
|
* 🔴 **导出**([ref] U1/U2):本仓「词表闭集,未处置成员=编译红」的钉法只许有**一个**形 ——
|
|
@@ -43,7 +43,7 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
|
|
|
43
43
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
44
44
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
45
45
|
type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
|
|
46
|
-
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "readRootCandidate" | "mandated" | "mandate";
|
|
46
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "readRootCandidate" | "mandated" | "mandate" | "askClass" | "classes";
|
|
47
47
|
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd" | "authoredBy";
|
|
48
48
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
49
49
|
type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
|
|
@@ -157,6 +157,17 @@ type PausedCauseForwarded = "kind" | "gate" | "checkpointId" | "restoreMode";
|
|
|
157
157
|
type PausedCauseStripped = "token";
|
|
158
158
|
type _GuardPausedCause = AssertAllKeysHandled<Exclude<keyof PausedCause, PausedCauseForwarded | PausedCauseStripped>>;
|
|
159
159
|
type _GuardPausedCauseReverse = AssertAllKeysHandled<Exclude<PausedCauseForwarded | PausedCauseStripped, keyof PausedCause>>;
|
|
160
|
+
type NonPausedTerminalKeys = Exclude<TerminalCause, {
|
|
161
|
+
kind: "paused";
|
|
162
|
+
}> extends infer C ? (C extends object ? keyof C : never) : never;
|
|
163
|
+
type TerminalArmForwarded = "kind" | "code" | "message" | "nestedPause" | "detail" | "reason";
|
|
164
|
+
type TerminalArmStripped = never;
|
|
165
|
+
type _GuardTerminalArm = AssertAllKeysHandled<Exclude<NonPausedTerminalKeys, TerminalArmForwarded | TerminalArmStripped>>;
|
|
166
|
+
type _GuardTerminalArmReverse = AssertAllKeysHandled<Exclude<TerminalArmForwarded | TerminalArmStripped, NonPausedTerminalKeys>>;
|
|
167
|
+
type SessionMemoryStatusProjected = "captureOptedOut" | "committedCount" | "foldedCount" | "optOutSource" | "lastCaptureAt";
|
|
168
|
+
type SessionMemoryStatusExcluded = "writeHome";
|
|
169
|
+
type _GuardSessionMemoryStatus = AssertAllKeysHandled<Exclude<keyof SessionMemoryStatus, SessionMemoryStatusProjected | SessionMemoryStatusExcluded>>;
|
|
170
|
+
type _GuardSessionMemoryStatusReverse = AssertAllKeysHandled<Exclude<SessionMemoryStatusProjected | SessionMemoryStatusExcluded, keyof SessionMemoryStatus>>;
|
|
160
171
|
type SideQueryResultForwarded = "text" | "toolCalls" | "model" | "servedModel" | "degraded" | "responseModel" | "usage" | "usageMissing" | "stopReason" | "errorMessage";
|
|
161
172
|
type SideQueryResultStripped = never;
|
|
162
173
|
type _GuardSideQueryResult = AssertAllKeysHandled<Exclude<keyof SideQueryResult, SideQueryResultForwarded | SideQueryResultStripped>>;
|
|
@@ -226,6 +237,10 @@ type RunnerDepsLedgerWired = "fileHistoryCapture";
|
|
|
226
237
|
type _GuardLedgerRunnerDepsReverse = AssertAllKeysHandled<Exclude<RunnerDepsLedgerWired, keyof RunnerDeps>>;
|
|
227
238
|
export type CoreKeysetGuards = [
|
|
228
239
|
_GuardNotification,
|
|
240
|
+
_GuardTerminalArm,
|
|
241
|
+
_GuardTerminalArmReverse,
|
|
242
|
+
_GuardSessionMemoryStatus,
|
|
243
|
+
_GuardSessionMemoryStatusReverse,
|
|
229
244
|
_GuardNotificationReverse,
|
|
230
245
|
_GuardProvenance,
|
|
231
246
|
_GuardProvenanceReverse,
|