@sema-agent/server 7.113.0 → 7.115.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 +109 -2
- package/USAGE.md +36 -3
- package/deploy/sema-up/cluster-up.sh +13 -1
- package/deploy/sema-up/kube-up.sh +13 -1
- package/deploy/sema-up/preflight.sh +13 -1
- package/deploy/sema-up/sema-up.sh +13 -1
- package/deploy/sema-up/smoke.sh +13 -1
- package/dist/adoption/wire.d.ts +0 -3
- package/dist/approval-card.d.ts +21 -24
- package/dist/approval-card.js +12 -0
- package/dist/approval.js +5 -2
- package/dist/boot/exit-boundary.d.ts +3 -2
- package/dist/boot/exit-boundary.js +3 -0
- package/dist/boot/memory-boundary.d.ts +2 -1
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/resolve-spec.js +6 -3
- package/dist/boot/shutdown.d.ts +19 -0
- package/dist/boot/shutdown.js +12 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/config-catalog.js +8 -6
- package/dist/config-center/apply-effective.js +11 -11
- package/dist/config-invariants.d.ts +27 -0
- package/dist/config-invariants.js +54 -39
- package/dist/config-provider.d.ts +16 -0
- package/dist/config-provider.js +11 -2
- package/dist/config-types.d.ts +24 -11
- package/dist/config-types.js +0 -1
- package/dist/config.d.ts +41 -34
- package/dist/config.js +78 -81
- package/dist/deployment-governance.d.ts +15 -4
- package/dist/deployment-governance.js +20 -2
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +5 -1
- package/dist/execution-lane-caps.d.ts +54 -30
- package/dist/execution-lane-caps.js +48 -20
- package/dist/fleet/fleet-bus.d.ts +1 -22
- package/dist/hooks/hook-runner.d.ts +112 -6
- package/dist/hooks/hook-runner.js +138 -32
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +9 -4
- package/dist/http/approval-sweeps.js +20 -0
- package/dist/http/checkpoint-ctx.d.ts +68 -0
- package/dist/http/checkpoint-ctx.js +36 -0
- package/dist/http/durable-run-leg.js +2 -1
- package/dist/http/resume-legs.js +9 -0
- package/dist/http/route-ctx.d.ts +7 -0
- package/dist/http/routes/a2a-serve.d.ts +1 -3
- package/dist/http/routes/a2a-serve.js +0 -2
- package/dist/http/routes/admin-memory-optout.d.ts +0 -2
- package/dist/http/routes/admin-memory-optout.js +0 -1
- package/dist/http/routes/capabilities.js +3 -2
- package/dist/http/routes/diagnostics.d.ts +5 -0
- package/dist/http/routes/diagnostics.js +2 -0
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/tasks.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/leader/wire.js +2 -1
- package/dist/local-data-root.d.ts +1 -1
- package/dist/mcp-content-origin.d.ts +0 -8
- package/dist/model-entry-refusal.d.ts +10 -2
- package/dist/model-entry-refusal.js +19 -3
- package/dist/observability/corrupt-read-seat.d.ts +3 -3
- package/dist/observability/corrupt-read-seat.js +5 -4
- package/dist/observability/fail-open.d.ts +10 -9
- package/dist/observability/fail-open.js +5 -1
- package/dist/permission-rule-list.d.ts +81 -0
- package/dist/permission-rule-list.js +17 -0
- package/dist/plugins/remote-env-host.d.ts +0 -35
- package/dist/plugins/remote-env-host.js +0 -8
- package/dist/plugins/remote-env-local-docker.d.ts +5 -13
- package/dist/plugins/remote-env-local-docker.js +2 -4
- package/dist/run-local.js +4 -1
- package/dist/runtime-governance.d.ts +21 -1
- package/dist/runtime-governance.js +6 -1
- package/dist/security.js +2 -2
- package/dist/session-policy-wire.d.ts +5 -4
- package/dist/store-posture.d.ts +7 -7
- package/dist/store-posture.js +0 -1
- package/dist/task-settings.d.ts +19 -5
- package/dist/task-settings.js +19 -30
- package/dist/tool-approval.d.ts +16 -4
- package/dist/tool-approval.js +2 -1
- package/dist/trace/core-keyset-guard.d.ts +9 -4
- package/dist/trace/project.js +9 -0
- package/package.json +4 -4
- package/scripts/{retired-mode-seed-drain-local.mjs → drain-check-local.mjs} +147 -26
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import vm from "node:vm";
|
|
2
3
|
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
3
4
|
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, } from "@sema-agent/settings-schema/hooks";
|
|
4
5
|
import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
|
|
@@ -57,7 +58,9 @@ function toAssistantTurnError(errorKind) {
|
|
|
57
58
|
return "unknown";
|
|
58
59
|
return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
|
|
59
60
|
}
|
|
60
|
-
const
|
|
61
|
+
export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
|
|
62
|
+
export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 100;
|
|
63
|
+
const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
|
|
61
64
|
function matcherReadsWireToolName(event) {
|
|
62
65
|
return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
|
|
63
66
|
}
|
|
@@ -77,25 +80,92 @@ function engineToolAliases() {
|
|
|
77
80
|
engineToolAliasTable = table;
|
|
78
81
|
return table;
|
|
79
82
|
}
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
let matcherEvalBox;
|
|
84
|
+
function matcherPatternMatchesWithin(matcher, name, budget) {
|
|
85
|
+
const remaining = budget.deadlineMs - budget.now();
|
|
86
|
+
if (remaining <= 0)
|
|
87
|
+
return "over_budget";
|
|
88
|
+
matcherEvalBox ??= {
|
|
89
|
+
script: new vm.Script("matcherPatternMatches(matcher, name)"),
|
|
90
|
+
context: vm.createContext({ matcherPatternMatches, matcher: "", name: "" }),
|
|
91
|
+
};
|
|
92
|
+
matcherEvalBox.context.matcher = matcher;
|
|
93
|
+
matcherEvalBox.context.name = name;
|
|
94
|
+
try {
|
|
95
|
+
const verdict = matcherEvalBox.script.runInContext(matcherEvalBox.context, { timeout: Math.max(1, Math.ceil(remaining)) });
|
|
96
|
+
return verdict === true ? true : verdict === false ? false : "invalid";
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT")
|
|
100
|
+
return "over_budget";
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function deadAliasInMatcher(matcher, depth, budget) {
|
|
105
|
+
if (matcher.length > MAX_MATCHER_EVAL_CHARS)
|
|
106
|
+
return { kind: "unjudged", reason: "oversize" };
|
|
107
|
+
if (matcher === "" || matcher === "*")
|
|
108
|
+
return undefined;
|
|
109
|
+
const names = matcherLiteralNames(matcher);
|
|
110
|
+
if (names !== undefined) {
|
|
111
|
+
for (const [alias, canonical] of engineToolAliases()) {
|
|
112
|
+
if (names.includes(alias) && !names.includes(canonical))
|
|
113
|
+
return { kind: "dead", alias, canonical };
|
|
114
|
+
}
|
|
82
115
|
return undefined;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
}
|
|
117
|
+
switch (depth) {
|
|
118
|
+
case "literal":
|
|
119
|
+
return undefined;
|
|
120
|
+
case "regex":
|
|
121
|
+
break;
|
|
122
|
+
default: {
|
|
123
|
+
const unreachable = depth;
|
|
124
|
+
return unreachable;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const selects = (name) => {
|
|
128
|
+
const verdict = matcherPatternMatchesWithin(matcher, name, budget);
|
|
129
|
+
return verdict === "invalid" ? "invalid_regex" : verdict;
|
|
130
|
+
};
|
|
131
|
+
for (const [alias, canonical] of engineToolAliases()) {
|
|
132
|
+
const selectsAlias = selects(alias);
|
|
133
|
+
if (typeof selectsAlias === "string")
|
|
134
|
+
return { kind: "unjudged", reason: selectsAlias };
|
|
135
|
+
if (!selectsAlias)
|
|
136
|
+
continue;
|
|
137
|
+
const selectsCanonical = selects(canonical);
|
|
138
|
+
if (typeof selectsCanonical === "string")
|
|
139
|
+
return { kind: "unjudged", reason: selectsCanonical };
|
|
140
|
+
if (!selectsCanonical)
|
|
141
|
+
return { kind: "dead", alias, canonical };
|
|
89
142
|
}
|
|
90
143
|
return undefined;
|
|
91
144
|
}
|
|
92
|
-
export function
|
|
145
|
+
export function buildDeadToolMatcherRefusal(issue) {
|
|
146
|
+
return {
|
|
147
|
+
message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
|
|
148
|
+
field: "settings.hooks",
|
|
149
|
+
detail: { event: issue.event, alias: issue.alias, canonical: issue.canonical },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function deadToolMatcherIssue(config, depth, budget = {}) {
|
|
153
|
+
const now = budget.now ?? (() => performance.now());
|
|
154
|
+
const deadline = { deadlineMs: now() + (budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS), now };
|
|
93
155
|
for (const [event, groups] of Object.entries(config)) {
|
|
156
|
+
if (!matcherReadsWireToolName(event))
|
|
157
|
+
continue;
|
|
94
158
|
for (const group of groups ?? []) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
159
|
+
if (group.matcher === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
const verdict = deadAliasInMatcher(group.matcher, depth, deadline);
|
|
162
|
+
if (verdict === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
if (verdict.kind === "unjudged") {
|
|
165
|
+
recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
|
|
166
|
+
continue;
|
|
98
167
|
}
|
|
168
|
+
return { event, alias: verdict.alias, canonical: verdict.canonical };
|
|
99
169
|
}
|
|
100
170
|
}
|
|
101
171
|
return undefined;
|
|
@@ -144,9 +214,31 @@ export function parseHooksConfig(raw) {
|
|
|
144
214
|
}
|
|
145
215
|
return { config };
|
|
146
216
|
}
|
|
147
|
-
export function
|
|
148
|
-
|
|
149
|
-
|
|
217
|
+
export function matcherLiteralNames(matcher) {
|
|
218
|
+
const names = [];
|
|
219
|
+
let first = -1;
|
|
220
|
+
let last = -1;
|
|
221
|
+
for (let i = 0; i <= matcher.length; i++) {
|
|
222
|
+
const c = i < matcher.length ? matcher.charCodeAt(i) : 0x7c;
|
|
223
|
+
if (c === 0x7c || c === 0x2c) {
|
|
224
|
+
if (first !== -1)
|
|
225
|
+
names.push(matcher.slice(first, last + 1));
|
|
226
|
+
first = -1;
|
|
227
|
+
last = -1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (c === 0x20)
|
|
231
|
+
continue;
|
|
232
|
+
const nameChar = (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f || c === 0x2d;
|
|
233
|
+
if (!nameChar)
|
|
234
|
+
return undefined;
|
|
235
|
+
if (first === -1)
|
|
236
|
+
first = i;
|
|
237
|
+
last = i;
|
|
238
|
+
}
|
|
239
|
+
return names;
|
|
240
|
+
}
|
|
241
|
+
function matcherPatternMatches(matcher, value) {
|
|
150
242
|
if (matcher === value)
|
|
151
243
|
return true;
|
|
152
244
|
try {
|
|
@@ -156,6 +248,14 @@ export function hookMatcherMatches(matcher, value) {
|
|
|
156
248
|
return "invalid";
|
|
157
249
|
}
|
|
158
250
|
}
|
|
251
|
+
export function hookMatcherMatches(matcher, value) {
|
|
252
|
+
if (matcher === undefined || matcher === "" || matcher === "*")
|
|
253
|
+
return true;
|
|
254
|
+
const names = matcherLiteralNames(matcher);
|
|
255
|
+
if (names !== undefined)
|
|
256
|
+
return names.includes(value);
|
|
257
|
+
return matcherPatternMatches(matcher, value);
|
|
258
|
+
}
|
|
159
259
|
export function hookIfMatches(cond, payload) {
|
|
160
260
|
if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
|
|
161
261
|
return "unevaluable";
|
|
@@ -762,6 +862,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
762
862
|
let currentInput = input;
|
|
763
863
|
let rewritten = false;
|
|
764
864
|
let asked;
|
|
865
|
+
let allowedFor;
|
|
765
866
|
let unknownDecision = false;
|
|
766
867
|
const contexts = [];
|
|
767
868
|
const deadlineAt = Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000;
|
|
@@ -775,11 +876,12 @@ export function createTaskHooks(config, ctx) {
|
|
|
775
876
|
}
|
|
776
877
|
if (!m)
|
|
777
878
|
continue;
|
|
879
|
+
const groupInput = currentInput;
|
|
778
880
|
const payload = {
|
|
779
881
|
...basePayload(ctx, tctx.permissionMode),
|
|
780
882
|
hook_event_name: "PreToolUse",
|
|
781
883
|
tool_name: toolName,
|
|
782
|
-
tool_input:
|
|
884
|
+
tool_input: groupInput,
|
|
783
885
|
tool_use_id: tctx.toolCallId,
|
|
784
886
|
};
|
|
785
887
|
const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
|
|
@@ -820,10 +922,13 @@ export function createTaskHooks(config, ctx) {
|
|
|
820
922
|
asked = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
|
|
821
923
|
}
|
|
822
924
|
if (decision === "allow" || decision === undefined) {
|
|
823
|
-
|
|
925
|
+
const rewrites = h.updatedInput !== null && typeof h.updatedInput === "object";
|
|
926
|
+
if (rewrites) {
|
|
824
927
|
currentInput = h.updatedInput;
|
|
825
928
|
rewritten = true;
|
|
826
929
|
}
|
|
930
|
+
if (decision === "allow")
|
|
931
|
+
allowedFor = { input: rewrites ? h.updatedInput : groupInput };
|
|
827
932
|
}
|
|
828
933
|
else if (decision !== "deny" && decision !== "ask") {
|
|
829
934
|
unknownDecision = true;
|
|
@@ -853,13 +958,11 @@ export function createTaskHooks(config, ctx) {
|
|
|
853
958
|
...buildHookContextField(contexts),
|
|
854
959
|
};
|
|
855
960
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
};
|
|
862
|
-
}
|
|
961
|
+
const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...buildHookContextField(contexts) };
|
|
962
|
+
if (allowedFor !== undefined && allowedFor.input === currentInput)
|
|
963
|
+
return { action: "allow", ...carried };
|
|
964
|
+
if (rewritten || contexts.length > 0)
|
|
965
|
+
return carried;
|
|
863
966
|
return undefined;
|
|
864
967
|
};
|
|
865
968
|
hooks.preToolUse = preToolUseHook;
|
|
@@ -1231,6 +1334,7 @@ export function composeHooks(deployment, task) {
|
|
|
1231
1334
|
let current = input;
|
|
1232
1335
|
const contexts = [];
|
|
1233
1336
|
let asked;
|
|
1337
|
+
let allowedFor;
|
|
1234
1338
|
for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
|
|
1235
1339
|
if (!slot)
|
|
1236
1340
|
continue;
|
|
@@ -1250,6 +1354,10 @@ export function composeHooks(deployment, task) {
|
|
|
1250
1354
|
asked = r;
|
|
1251
1355
|
break;
|
|
1252
1356
|
case "allow":
|
|
1357
|
+
if (updatedInput !== undefined)
|
|
1358
|
+
current = updatedInput;
|
|
1359
|
+
allowedFor = { input: current };
|
|
1360
|
+
break;
|
|
1253
1361
|
case undefined:
|
|
1254
1362
|
if (updatedInput !== undefined)
|
|
1255
1363
|
current = updatedInput;
|
|
@@ -1269,13 +1377,11 @@ export function composeHooks(deployment, task) {
|
|
|
1269
1377
|
...buildHookContextField(contexts),
|
|
1270
1378
|
};
|
|
1271
1379
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1380
|
+
const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
|
|
1381
|
+
if (allowedFor !== undefined && allowedFor.input === current)
|
|
1382
|
+
return { action: "allow", ...carried };
|
|
1383
|
+
if (current !== input || contexts.length > 0)
|
|
1384
|
+
return carried;
|
|
1279
1385
|
return undefined;
|
|
1280
1386
|
};
|
|
1281
1387
|
}
|
package/dist/hosted-posture.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* 比手抄的值更难发现烂掉,[ref] 的第一排查点就是它;
|
|
14
14
|
* · 能力授予面的赦免挂在 **`requirePrincipal !== true`** 上 —— 但**三条面各有各的谓词,不是一只**
|
|
15
15
|
* (异源复核实测,别照「它们都一样」写):
|
|
16
|
-
* · `settings.hooks` —— `
|
|
17
|
-
* ⇒
|
|
16
|
+
* · `settings.hooks` —— `task-settings.ts` 的 `taskHooksHonored`(唯一属主;resolve-spec 装配 / 受理面 / 能力位三处同调),判据**只有**
|
|
17
|
+
* `requirePrincipal !== true` ⇒ 采纳,否则忽略(**不看车道**,任何 `REMOTE_EXEC` 都一样);
|
|
18
18
|
* · `additionalDirectories` / `additionalReadDirectories` —— **只有** `cwdHonored`
|
|
19
19
|
* (= `provider === "host" ∧ requirePrincipal !== true`);
|
|
20
20
|
* · `settings.env` / `cwd` —— `cwdHonored ‖ deviceCwdHonored`,而
|
package/dist/http/admission.js
CHANGED
|
@@ -2,12 +2,12 @@ import { isThinkingLevel, expandTiers } from "@sema-agent/core";
|
|
|
2
2
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
|
|
3
3
|
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
4
4
|
import { HttpError } from "../security.js";
|
|
5
|
-
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
|
+
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
6
6
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
7
7
|
import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
8
8
|
import { selfOrchestrationDenial } from "../task-workflow.js";
|
|
9
9
|
import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
|
|
10
|
-
import { deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
|
|
10
|
+
import { buildDeadToolMatcherRefusal, deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
|
|
11
11
|
import { judgeRequestWebSearch } from "../plugins/web-search.js";
|
|
12
12
|
import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
13
13
|
import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
|
|
@@ -200,9 +200,10 @@ export function createAdmission(ctx) {
|
|
|
200
200
|
sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
|
|
201
201
|
return null;
|
|
202
202
|
}
|
|
203
|
-
const deadMatcher = deadToolMatcherIssue(hooksParsed.config);
|
|
203
|
+
const deadMatcher = deadToolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
|
|
204
204
|
if (deadMatcher !== undefined) {
|
|
205
|
-
|
|
205
|
+
const refusal = buildDeadToolMatcherRefusal(deadMatcher);
|
|
206
|
+
sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
|
|
206
207
|
return null;
|
|
207
208
|
}
|
|
208
209
|
}
|
|
@@ -420,6 +421,10 @@ export function createAdmission(ctx) {
|
|
|
420
421
|
sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
|
|
421
422
|
return null;
|
|
422
423
|
}
|
|
424
|
+
if (l.brainCallGuardrailMs !== undefined) {
|
|
425
|
+
sendError(res, 400, "request.field_invalid", "limits.brainCallGuardrailMs is not caller-settable here — the engine's default wall on a single brain call applies (one hour); remove it");
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
423
428
|
for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
|
|
424
429
|
const v = l[k];
|
|
425
430
|
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
@@ -7,6 +7,8 @@ import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
9
|
import { readinessOf, billableRefusalOf } from "./readiness.js";
|
|
10
|
+
import { readResumeCtx } from "./resume-ctx-read.js";
|
|
11
|
+
import { allowListReadingUnstated } from "./checkpoint-ctx.js";
|
|
10
12
|
const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
|
|
11
13
|
function blockedScalars(body) {
|
|
12
14
|
const out = {};
|
|
@@ -64,17 +66,35 @@ export function createApprovalSweeps(ctx) {
|
|
|
64
66
|
return false;
|
|
65
67
|
}
|
|
66
68
|
};
|
|
69
|
+
const allowListUnservable = async (sessionId) => {
|
|
70
|
+
const read = await readResumeCtx(cs.getCtx(sessionId));
|
|
71
|
+
if (read.kind === "corrupt")
|
|
72
|
+
return false;
|
|
73
|
+
return read.kind === "ok" && allowListReadingUnstated(read.ctx);
|
|
74
|
+
};
|
|
67
75
|
let parkedSkipped = 0;
|
|
76
|
+
const allowListSkipped = [];
|
|
68
77
|
for (const { sessionId } of expired) {
|
|
69
78
|
if (await isParkedOwned(sessionId)) {
|
|
70
79
|
parkedSkipped += 1;
|
|
71
80
|
continue;
|
|
72
81
|
}
|
|
82
|
+
if (await allowListUnservable(sessionId)) {
|
|
83
|
+
allowListSkipped.push(sessionId);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
73
86
|
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
|
|
74
87
|
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, ...blockedScalars(r.body) }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
|
|
75
88
|
}
|
|
76
89
|
if (parkedSkipped > 0)
|
|
77
90
|
deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
|
|
91
|
+
if (allowListSkipped.length > 0) {
|
|
92
|
+
deps.logger?.warn?.("deny_sweep_allow_list_reading_skipped", {
|
|
93
|
+
count: allowListSkipped.length,
|
|
94
|
+
sessionIds: allowListSkipped,
|
|
95
|
+
note: "these expired approvals belong to tasks suspended by a server version that read settings.permissions.allow as a whitelist; this version does not replay them, so the SLA deny cannot be delivered. They stay suspended (session lock held) until the reaper collects them at their terminal time — cancel them (POST /v1/runs/:id/cancel) to release the sessions now",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
78
98
|
}
|
|
79
99
|
async function redeemParkedAsk(req) {
|
|
80
100
|
const cs = deps.checkpointStore;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 续跑重建输入(`checkpoint_ctx` 的行体,{@link CheckpointCtx})的**写形**,以及持久化体里 `settings.permissions.allow` 的**读法记号** —— S-1034(7.115.0)。
|
|
3
|
+
*
|
|
4
|
+
* ## 病
|
|
5
|
+
* 7.115.0 把 `settings.permissions.allow` 从「名单」(名单外的工具一律拒)改成「allow 规则」(命中 = 带理由的放行,未命中 = 不表态、照常判)。
|
|
6
|
+
* 续跑家族重建 spec 时把**持久化的提交体**原样重放进 `resolveSpec`(`resume-legs.ts` 的 `resolveResumeSpec`)——
|
|
7
|
+
* 一条在 ≤7.114.0 上停驻、体里带非空 `allow` 的行,升级后续跑就被按新读法读:名单外的工具不再被拒。持久面上的静默语义翻转(放宽方向)。
|
|
8
|
+
* 坏形 `allow`(`Task` / `mcp__figma` / `Bash(npm:*)` …)的旧行则每次续跑都 409、SLA 拒扫腿逐拍重试直到收割 —— 一张本来放不开任何门的停驻被砖化。
|
|
9
|
+
*
|
|
10
|
+
* ## 规则(一条;持久数据的「响亮拒 + 一次性迁移」,零双读)
|
|
11
|
+
* 续跑重放持久化体之前问一句:**体里的 `allow` 在生效(非空数组)而本行没有「按表态读」的记号 ⇒ 拒**。
|
|
12
|
+
* 行留停驻、字节不动(拒发生在认领之前,run 行、卡、ctx 一个字节都不写);出路 = 作为新任务重提,这条任务若还停驻着先取消(`task_done` 唤醒停驻与 workflow 宿主那条腿没有可取消的 run)。
|
|
13
|
+
* **没有**「按旧名单语义继续读」的臂:那是第二种读法,正是本规则要消灭的东西。
|
|
14
|
+
* · 记号 = ≥7.115.0 写下的行在体里带非空 `allow` 时,ctx 上的 `permissionsAllowReading: "statement"`(闭集一词)。
|
|
15
|
+
* 写点 = 每一次**新的**持久化写({@link buildCheckpointCtx} 是它们的唯一构造口);续跑腿的写回传的是读进来的那只 ctx(或其展开),记号随行,不重新判。
|
|
16
|
+
* · 体里没有 `allow`(或空数组 —— 7.114.0 的名单判据 `acceptedRuleList` 对空数组同样读成「没有名单」)的行不写记号、不受本规则影响。
|
|
17
|
+
*
|
|
18
|
+
* ## 判据与 7.114.0 的关系(如实)
|
|
19
|
+
* 7.114.0 只在三桶整体合法时才读名单;判据这里只看「`allow` 是非空数组」,比 7.114.0 的「名单生效」宽一格(坏桶的行 7.114.0 也不读名单)——
|
|
20
|
+
* 方向 = 多拒,不会漏拒;而坏桶的体当年就被 fresh 受理门拒在门外,盘上本不该有。
|
|
21
|
+
*
|
|
22
|
+
* ## 何时删(一次性迁移,不是长期双读)
|
|
23
|
+
* 本判据、拒臂、记号写点与 `CheckpointCtx.permissionsAllowReading` 一起删,删除条件 = 所有 ≤7.114.0 停驻的行都已结束或被收割
|
|
24
|
+
* (停驻行的终局时刻 = 落盘起 `TERMINAL_BACKSTOP_MS`,见 `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删:
|
|
25
|
+
* 届时盘上不会再有没记号却带 `allow` 的行,而有记号的行读不读它都一样。
|
|
26
|
+
*/
|
|
27
|
+
import type { CheckpointCtx } from "./route-ctx.js";
|
|
28
|
+
import type { TaskRequestBody } from "./wire-types.js";
|
|
29
|
+
/** 读法记号的闭集(一词):这一行体里的 `settings.permissions.allow` 是按「allow 规则(表态)」写下、也该按它读的。 */
|
|
30
|
+
export declare const PERMISSIONS_ALLOW_READINGS: readonly ["statement"];
|
|
31
|
+
export type PermissionsAllowReading = (typeof PERMISSIONS_ALLOW_READINGS)[number];
|
|
32
|
+
/**
|
|
33
|
+
* 体里的 `settings.permissions.allow` 是否**在生效**:非空数组(与 7.114.0 名单判据 `acceptedRuleList` 同一个「在」——空数组 = 没有)。
|
|
34
|
+
* 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
|
|
35
|
+
*/
|
|
36
|
+
export declare function bodyCarriesAllowList(body: TaskRequestBody): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 一次**新的**持久化写的 ctx(提交腿 / `POST /v1/runs` 族 / leader worker)—— 写记号的唯一口。
|
|
39
|
+
*
|
|
40
|
+
* 调用方是「本进程刚按表态读过这份体」的写点:fresh 提交(受理门按 7.115.0 读过 `allow`),或已经过了续跑拒臂的重放腿
|
|
41
|
+
* (`startFreshHostTurn` 把宿主的存量体交给 `POST /v1/runs` 同一条链 —— 它在那之前调过 `resolveResumeSpecOrBlocked`,没记号却带 `allow` 的体到不了这里)。
|
|
42
|
+
* 续跑腿的**写回**不经本函数:它传读进来的 ctx(或其展开),记号随行;在写回口重新判会把一条没记号的旧行「洗」成新行。
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildCheckpointCtx(fields: {
|
|
45
|
+
body: TaskRequestBody;
|
|
46
|
+
memoryScope?: string | undefined;
|
|
47
|
+
unattendedLeg?: true;
|
|
48
|
+
}): CheckpointCtx;
|
|
49
|
+
/**
|
|
50
|
+
* 本行能否被按 7.115.0 的读法重放:体里 `allow` 在生效而行上没有记号 ⇒ **不能**(`true` = 拒)。
|
|
51
|
+
* 记号的值不在闭集里(手改 / 将来的写者)⇒ 同样拒:认不得的记号不是证据。
|
|
52
|
+
*/
|
|
53
|
+
export declare function allowListReadingUnstated(ctx: CheckpointCtx): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 拒的答复 —— 码与落点照「退役 mode-seed 行」的先例(`checkpoint.unsupported_version`,409,`retriable:true` = 那张卡仍 pending;**不是**「原样重试会成功」)。
|
|
56
|
+
* 拒在认领之前,所以没有 `taskId`(行没抢到就不造键)。句子说原因(这条任务停在 `allow` 还是名单的版本上)与出路(作为新任务重提,仍停驻的先取消;要「只许这些工具」用会话规则 `toolAllow` / 每请求 `excludeTools`)。
|
|
57
|
+
*
|
|
58
|
+
* 🔴 形是给冻结门看的(`api-error-text-freeze` 的 resume 家族收集器):`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
|
|
59
|
+
*/
|
|
60
|
+
export declare function allowListReadingRefusal(): {
|
|
61
|
+
status: number;
|
|
62
|
+
body: {
|
|
63
|
+
error: string;
|
|
64
|
+
errorCode: string;
|
|
65
|
+
retriable: true;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=checkpoint-ctx.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const PERMISSIONS_ALLOW_READINGS = ["statement"];
|
|
2
|
+
export function bodyCarriesAllowList(body) {
|
|
3
|
+
const settings = body?.settings;
|
|
4
|
+
if (settings === null || typeof settings !== "object" || Array.isArray(settings))
|
|
5
|
+
return false;
|
|
6
|
+
const permissions = settings["permissions"];
|
|
7
|
+
if (permissions === null || typeof permissions !== "object" || Array.isArray(permissions))
|
|
8
|
+
return false;
|
|
9
|
+
const allow = permissions["allow"];
|
|
10
|
+
return Array.isArray(allow) && allow.length > 0;
|
|
11
|
+
}
|
|
12
|
+
export function buildCheckpointCtx(fields) {
|
|
13
|
+
return {
|
|
14
|
+
body: fields.body,
|
|
15
|
+
...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
|
|
16
|
+
...(fields.unattendedLeg === true ? { unattendedLeg: true } : {}),
|
|
17
|
+
...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "statement" } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function allowListReadingUnstated(ctx) {
|
|
21
|
+
if (!bodyCarriesAllowList(ctx.body))
|
|
22
|
+
return false;
|
|
23
|
+
const reading = ctx.permissionsAllowReading;
|
|
24
|
+
return !(typeof reading === "string" && PERMISSIONS_ALLOW_READINGS.includes(reading));
|
|
25
|
+
}
|
|
26
|
+
export function allowListReadingRefusal() {
|
|
27
|
+
return {
|
|
28
|
+
status: 409,
|
|
29
|
+
body: {
|
|
30
|
+
error: "this task was suspended by an earlier server version, which read settings.permissions.allow as a whitelist (every tool not on the list was refused); this version reads that list as a statement that the named tools are allowed, not as a whitelist, so continuing would give the task more tools than it was submitted with. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel). To allow only certain tools, use the session rules' toolAllow (PUT /v1/sessions/:id/policy) or the request's excludeTools.",
|
|
31
|
+
errorCode: "checkpoint.unsupported_version",
|
|
32
|
+
retriable: true,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=checkpoint-ctx.js.map
|
|
@@ -8,6 +8,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
|
|
|
8
8
|
import { commitSessionInheritEnv } from "./session-inherit-env.js";
|
|
9
9
|
import { clearTurnActivity, readTurnActivityMs } from "../turn-activity.js";
|
|
10
10
|
import { recordResourceWindow } from "../resource-window.js";
|
|
11
|
+
import { buildCheckpointCtx } from "./checkpoint-ctx.js";
|
|
11
12
|
export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
12
13
|
const { deps } = ctx;
|
|
13
14
|
const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
|
|
@@ -31,7 +32,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
|
31
32
|
return { status: placementReject.status, body: devicePlacementErrorBody(placementReject) };
|
|
32
33
|
commitSessionInheritEnv(deps, args.prepared.spec.sessionId, args.prepared.body);
|
|
33
34
|
if (deps.checkpointStore) {
|
|
34
|
-
await deps.checkpointStore.putCtx(sessionId, { body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope });
|
|
35
|
+
await deps.checkpointStore.putCtx(sessionId, buildCheckpointCtx({ body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope }));
|
|
35
36
|
}
|
|
36
37
|
const anchorOwner = args.prepared.auth?.principal ?? null;
|
|
37
38
|
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -42,6 +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 { allowListReadingRefusal, allowListReadingUnstated } from "./checkpoint-ctx.js";
|
|
45
46
|
import { httpErrorCode } from "./send.js";
|
|
46
47
|
function isCheckpointReopenedFailure(r) {
|
|
47
48
|
const code = failureCodeOf(r.terminal);
|
|
@@ -398,7 +399,13 @@ export function createResumeLegs(ctx) {
|
|
|
398
399
|
});
|
|
399
400
|
return { spec, engineNotices };
|
|
400
401
|
};
|
|
402
|
+
const refuseAllowListReading = (sessionId) => {
|
|
403
|
+
deps.logger?.warn?.("resume_refused_allow_list_reading", { sessionId, note: "this session's stored request carries settings.permissions.allow written by a server that read it as a whitelist; it is not replayed under the current reading — the task stays suspended until someone cancels it" });
|
|
404
|
+
return allowListReadingRefusal();
|
|
405
|
+
};
|
|
401
406
|
const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
|
|
407
|
+
if (allowListReadingUnstated(ctx))
|
|
408
|
+
return { ok: false, answer: refuseAllowListReading(auth.sessionId) };
|
|
402
409
|
try {
|
|
403
410
|
const { spec, engineNotices } = await resolveResumeSpec(ctx, req, auth);
|
|
404
411
|
return { ok: true, spec, engineNotices };
|
|
@@ -1332,6 +1339,8 @@ export function createResumeLegs(ctx) {
|
|
|
1332
1339
|
return refuse(resumeCtxCorruptAnswer(ctxRead.where));
|
|
1333
1340
|
if (ctxRead.kind === "absent")
|
|
1334
1341
|
return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
|
|
1342
|
+
if (allowListReadingUnstated(ctxRead.ctx))
|
|
1343
|
+
return refuse(refuseAllowListReading(sessionId));
|
|
1335
1344
|
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
1336
1345
|
}
|
|
1337
1346
|
async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -161,6 +161,13 @@ export interface CheckpointCtx {
|
|
|
161
161
|
* 传原 ctx / 其展开 ⇒ 标随行。缺席 = 不是无人值守腿(HTTP 提交腿;7.106.0 前落盘的行 —— 那时这些腿不铸模式,checkpoint 里也没有)。
|
|
162
162
|
*/
|
|
163
163
|
unattendedLeg?: true;
|
|
164
|
+
/**
|
|
165
|
+
* 🔴 S-1034(7.115.0)—— 体里 `settings.permissions.allow` 的**读法记号**:在场 = 这一行是 ≥7.115.0 写下的、体里带非空 `allow`,
|
|
166
|
+
* 它按「allow 规则(表态)」写、也按它读。**服务端写、不来自请求体**(与 {@link unattendedLeg} 同为 `body` 的兄弟键)。
|
|
167
|
+
* 缺席而体里带非空 `allow` = ≤7.114.0 停驻的行(那时 `allow` 是名单)⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注)。
|
|
168
|
+
* 写点只有 `buildCheckpointCtx`(同模块;新的持久化写);续跑腿的写回传原 ctx / 其展开 ⇒ 记号随行。体里没有 `allow` 的行不写它。
|
|
169
|
+
*/
|
|
170
|
+
permissionsAllowReading?: import("./checkpoint-ctx.js").PermissionsAllowReading;
|
|
164
171
|
}
|
|
165
172
|
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
166
173
|
export interface DriveResumeArgs {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import {
|
|
2
|
+
import type { A2ATaskState } from "@sema-agent/core";
|
|
3
3
|
import type { A2aServeConfig } from "../../config-types.js";
|
|
4
4
|
import { type RunRecord } from "../../plugins/store-contracts.js";
|
|
5
5
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
@@ -69,8 +69,6 @@ export declare function a2aStateForRun(run: Pick<RunRecord, "status" | "errorCod
|
|
|
69
69
|
* `capabilities.a2aServe` 的子串,好让 `route-shape-roster` 层E 如实认出它们。
|
|
70
70
|
*/
|
|
71
71
|
export declare function handleA2a(req: IncomingMessage, res: ServerResponse, match: RouteMatch<A2aRpcRouteId>, ctx: RouteCtx): Promise<void>;
|
|
72
|
-
/** 九词闭集的对外可见锚(测试用它证明我们只发闭集词;也让「加一个自造词」在这里就被看见)。 */
|
|
73
|
-
export declare const A2A_SERVE_TASK_STATES: readonly A2ATaskState[];
|
|
74
72
|
export declare const A2A_SERVE_ROUTES: readonly [{
|
|
75
73
|
readonly id: "a2a-agent-card";
|
|
76
74
|
readonly path: "/.well-known/agent-card.json";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { persistedPlaneOf } from "../../terminal.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal.js";
|
|
4
|
-
import { A2A_TASK_STATES } from "@sema-agent/core";
|
|
5
4
|
import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
|
|
6
5
|
import { HttpError } from "../../security.js";
|
|
7
6
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
@@ -396,7 +395,6 @@ export async function handleA2a(req, res, match, ctx) {
|
|
|
396
395
|
function sendRpc(res, body) {
|
|
397
396
|
sendJson(res, 200, body);
|
|
398
397
|
}
|
|
399
|
-
export const A2A_SERVE_TASK_STATES = A2A_TASK_STATES;
|
|
400
398
|
export const A2A_SERVE_ROUTES = [
|
|
401
399
|
{ id: "a2a-agent-card", path: "/.well-known/agent-card.json", methods: ["GET"] },
|
|
402
400
|
];
|
|
@@ -42,8 +42,6 @@
|
|
|
42
42
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
43
43
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
44
44
|
export declare const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
|
|
45
|
-
/** 带参口(`:principal`)⇒ 路由声明表(`http/route-table.ts`)里走 `pattern` 行,不是 `path` 行。 */
|
|
46
|
-
export declare const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE: RegExp;
|
|
47
45
|
export declare function handleMemoryOptOut(req: IncomingMessage, res: ServerResponse, match: RouteMatch<MemoryOptOutRouteId>, ctx: RouteCtx): Promise<void>;
|
|
48
46
|
export declare const MEMORY_OPTOUT_ROUTES: readonly [{
|
|
49
47
|
readonly id: "memory-optout-grants-list";
|
|
@@ -4,7 +4,6 @@ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../pri
|
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
5
|
import { memoryOptOutPrincipalShapeProblem } from "../../plugins/memory-optout-grant-store-sql.js";
|
|
6
6
|
export const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
|
|
7
|
-
export const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE = /^\/v1\/admin\/memory-optout\/([^/]+)$/;
|
|
8
7
|
const PutBodySchema = z.object({ allow: z.boolean(), note: z.string().min(1).max(500).optional() }).strict();
|
|
9
8
|
export async function handleMemoryOptOut(req, res, match, ctx) {
|
|
10
9
|
const { deps } = ctx;
|
|
@@ -24,7 +24,7 @@ import { projectApprovalsStreamLiveCapability } from "../../capabilities/approva
|
|
|
24
24
|
import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
|
|
25
25
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
26
26
|
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
27
|
-
import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS } from "../../task-settings.js";
|
|
27
|
+
import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
|
|
28
28
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
29
29
|
import { sessionBackgroundFacesAvailable } from "./background.js";
|
|
30
30
|
function routePairingOf(deps, name, m) {
|
|
@@ -121,7 +121,8 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
121
121
|
scratchpadDir: true,
|
|
122
122
|
subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
|
|
123
123
|
subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
|
|
124
|
-
taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: deps.config
|
|
124
|
+
taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config) },
|
|
125
|
+
settings: { permissions: { allowGrant: true } },
|
|
125
126
|
permissionModeAuto: {
|
|
126
127
|
accepted: true,
|
|
127
128
|
classifierSeat: deps.autoModeFace?.seatMounted === true,
|
|
@@ -32,6 +32,9 @@ export interface ServerWiringGates {
|
|
|
32
32
|
streamAskWindowMs: PostureKnobReading<number>;
|
|
33
33
|
sessionAutoTitle: PostureKnobReading<boolean>;
|
|
34
34
|
checkpointStore: boolean;
|
|
35
|
+
/** 7.115.0:运维的 allow 规则(`APPROVAL_GRANT`)原样 —— 空表 = 旋钮未设。规则名是工具名不是秘密,所以本面给全名单(条数 = 长度);
|
|
36
|
+
* 本位**只**在运维面(operator-only,门在路由上),不进 `/v1/capabilities` 的客户端能力位:它说的是部署替谁表了态,不是调用方能用什么。 */
|
|
37
|
+
approvalGrant: readonly string[];
|
|
35
38
|
}
|
|
36
39
|
/** `buildServerWiringGates` 的入参 = 三个谓词各自的**真输入**(不接受已算好的布尔:那样这里读到的就
|
|
37
40
|
* 不再是装配现场的事实,而是别处算完可能已漂的复述)。 */
|
|
@@ -49,6 +52,8 @@ export interface ServerWiringGateInput {
|
|
|
49
52
|
streamAskWindowMsSource: PostureSource;
|
|
50
53
|
sessionAutoTitle: boolean;
|
|
51
54
|
sessionAutoTitleSource: PostureSource;
|
|
55
|
+
/** 7.115.0:`config.approvalGrant` 本身(读面不重算)。 */
|
|
56
|
+
approvalGrant: readonly string[];
|
|
52
57
|
}
|
|
53
58
|
/** 纯数据产物(`build*`):三个门的当前读数。`streamApproval` 走**单一谓词**
|
|
54
59
|
* `resolveStreamApprovalGate` —— 与 `/v1/capabilities` 的 `streamApproval` 位、与回决口的 501 门同一个
|