@sema-agent/server 7.129.1 → 7.131.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 +102 -0
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/USAGE.md +48 -9
- package/dist/audit.js +4 -1
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/resolve-spec.js +43 -16
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +7 -2
- package/dist/capabilities/builtin-tools.js +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.js +19 -6
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +34 -0
- package/dist/capabilities/scenarios.js +20 -4
- package/dist/capabilities/select-environment-tool.js +2 -1
- package/dist/capabilities/send-user-file-tool.d.ts +0 -1
- package/dist/capabilities/send-user-file-tool.js +2 -2
- package/dist/capabilities/skills.js +1 -1
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +16 -5
- package/dist/config-invariants.js +2 -2
- package/dist/config-provider.js +2 -2
- package/dist/config-types.d.ts +6 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +18 -17
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- package/dist/exec-bounds.d.ts +6 -6
- package/dist/hooks/hook-llm.d.ts +11 -1
- package/dist/hooks/hook-llm.js +11 -14
- package/dist/hooks/hook-runner.d.ts +55 -9
- package/dist/hooks/hook-runner.js +86 -16
- package/dist/http/admission.js +2 -2
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +12 -11
- package/dist/http/checkpoint-ctx.js +4 -4
- package/dist/http/dispatch.js +1 -1
- package/dist/http/resume-legs.js +6 -5
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +13 -5
- package/dist/http/routes/runs.js +18 -14
- package/dist/http/routes/sessions.js +22 -10
- package/dist/http/routes/tasks.js +16 -11
- package/dist/http/scenario-read-face.d.ts +14 -0
- package/dist/http/scenario-read-face.js +5 -0
- package/dist/http/send.d.ts +10 -0
- package/dist/http/send.js +6 -0
- package/dist/http/server.d.ts +16 -4
- package/dist/http/server.js +2 -2
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp-assertion-header.d.ts +2 -2
- package/dist/mcp-assertion-header.js +3 -3
- package/dist/model-entry-refusal.d.ts +1 -7
- package/dist/model-route-endpoint.d.ts +51 -11
- package/dist/model-route-endpoint.js +45 -23
- package/dist/model-route-gap.d.ts +5 -4
- package/dist/model-route-gap.js +1 -1
- package/dist/model-select.d.ts +58 -0
- package/dist/model-select.js +31 -0
- package/dist/namespaced-rule-name.d.ts +7 -4
- package/dist/namespaced-rule-name.js +4 -3
- package/dist/observability/fail-open.d.ts +16 -7
- package/dist/observability/fail-open.js +10 -2
- package/dist/plugins/local-session-store.js +2 -2
- package/dist/plugins/one-time-migrations.d.ts +21 -1
- package/dist/plugins/one-time-migrations.js +33 -1
- package/dist/plugins/remote-env-adb.js +5 -4
- package/dist/plugins/remote-env-local-docker.js +5 -4
- package/dist/plugins/remote-env-ssh.js +5 -4
- package/dist/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +16 -9
- package/dist/rule-entry-list.d.ts +124 -17
- package/dist/rule-entry-list.js +140 -24
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +8 -7
- package/dist/runs.d.ts +42 -2
- package/dist/runs.js +45 -2
- package/dist/runtime-caps-resolver.d.ts +7 -0
- package/dist/runtime-caps-resolver.js +1 -0
- package/dist/session-titler.d.ts +4 -1
- package/dist/session-titler.js +13 -3
- package/dist/spec-fields.d.ts +8 -4
- package/dist/stored-request-hooks-m20.d.ts +23 -0
- package/dist/stored-request-hooks-m20.js +124 -0
- package/dist/stored-request-tool-axes-m19.d.ts +29 -0
- package/dist/stored-request-tool-axes-m19.js +27 -0
- package/dist/task-mcp.d.ts +55 -60
- package/dist/task-mcp.js +197 -96
- package/dist/task-settings.d.ts +41 -5
- package/dist/task-settings.js +39 -25
- package/dist/tool-axis-words.d.ts +14 -17
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +29 -0
- package/dist/trace/ledger-sink.js +11 -0
- package/dist/trace/project.d.ts +6 -0
- package/dist/trace/project.js +11 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/user-authored-text.d.ts +7 -0
- package/dist/user-authored-text.js +3 -0
- package/dist/web-admin-client/apply-effective.js +10 -5
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +3 -2
|
@@ -3,6 +3,7 @@ import { Client } from "ssh2";
|
|
|
3
3
|
import { createSshHostKeyVerifier } from "../ssh-host-key.js";
|
|
4
4
|
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode } from "./remote-shell.js";
|
|
5
5
|
import { signalNumber } from "./host-platform.js";
|
|
6
|
+
import { commandBoundsOf, livenessCutMessage } from "../exec-bounds.js";
|
|
6
7
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
7
8
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
8
9
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
@@ -335,7 +336,7 @@ export class RemoteSshExecutionEnv {
|
|
|
335
336
|
w?.();
|
|
336
337
|
};
|
|
337
338
|
let lastChunk = Date.now();
|
|
338
|
-
const
|
|
339
|
+
const liveness = commandBoundsOf({ readTimeoutMs: options?.readTimeoutMs }, undefined).liveness;
|
|
339
340
|
let streamRef;
|
|
340
341
|
let onAbort;
|
|
341
342
|
let paused = false;
|
|
@@ -422,7 +423,7 @@ export class RemoteSshExecutionEnv {
|
|
|
422
423
|
throw failure;
|
|
423
424
|
if (finished)
|
|
424
425
|
break;
|
|
425
|
-
const waitMs =
|
|
426
|
+
const waitMs = liveness !== undefined ? Math.max(1, liveness.ms - (Date.now() - lastChunk)) : 30_000;
|
|
426
427
|
let waitTimer;
|
|
427
428
|
try {
|
|
428
429
|
await new Promise((r) => {
|
|
@@ -434,8 +435,8 @@ export class RemoteSshExecutionEnv {
|
|
|
434
435
|
if (waitTimer !== undefined)
|
|
435
436
|
clearTimeout(waitTimer);
|
|
436
437
|
}
|
|
437
|
-
if (
|
|
438
|
-
throw new RemoteExecutionError("timeout",
|
|
438
|
+
if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms)
|
|
439
|
+
throw new RemoteExecutionError("timeout", livenessCutMessage(liveness));
|
|
439
440
|
}
|
|
440
441
|
while (queue.length)
|
|
441
442
|
yield queue.shift();
|
package/dist/question.d.ts
CHANGED
|
@@ -23,20 +23,29 @@ export interface QuestionFrame {
|
|
|
23
23
|
/** "question" only: `serverNowMs + expiresInMs` — absolute deadline on the server's clock. */
|
|
24
24
|
expiresAtMs?: number;
|
|
25
25
|
}
|
|
26
|
-
/** The per-run context `onQuestion` recovers via ALS. `emit` delivers a frame to the run's
|
|
27
|
-
* (the run's own cancel signal) releases a question parked awaiting a human when the run aborts.
|
|
26
|
+
/** The per-run context `onQuestion` recovers via ALS. `emit` delivers a frame to the run's stream; `abortSignal`
|
|
27
|
+
* (the run's own cancel signal) releases a question parked awaiting a human when the run aborts.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 ONE LAW for every leg: a question frame that enters a run's stream has a LEDGER TWIN (the row
|
|
30
|
+
* `GET /v1/runs/:id/events` replays — a page reload recovers the open card from it); the leg decides only whether it
|
|
31
|
+
* ALSO writes a live socket. The background leg has no socket, so its `emit` IS the durable append; the synchronous
|
|
32
|
+
* stream leg lands the same frame in the ledger first and then writes the SSE connection (`createLedgerTwinEmit`).
|
|
33
|
+
* The ledger carries the CARD, not the answer door: `POST /v1/questions/:id/respond` stays live-only (this replica's
|
|
34
|
+
* memory, `ttlMs`), so a replayed card is answerable only until its own `expiresAtMs` and only on this replica. */
|
|
28
35
|
export interface QuestionRunContext {
|
|
29
36
|
taskId: string;
|
|
30
37
|
owner: string | null;
|
|
31
38
|
emit: (frame: QuestionFrame) => void | Promise<void>;
|
|
32
39
|
abortSignal?: AbortSignal;
|
|
33
|
-
/** OPTIONAL reachability predicate for this leg's delivery face
|
|
34
|
-
* (the park-vs-live adjudication probe)
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* read "live" and hand the leg to a face nobody can reach.
|
|
39
|
-
* background leg (
|
|
40
|
+
/** OPTIONAL reachability predicate for this leg's LIVE delivery face — one reading, two consumers:
|
|
41
|
+
* {@link QuestionCoordinator.hasLiveContext} (the park-vs-live adjudication probe) and the OPEN frame itself (a face
|
|
42
|
+
* that cannot show the card right now refuses the ask on the spot, the same failed-delivery arm, instead of parking it
|
|
43
|
+
* to TTL on a ledger row nobody is watching). CONSTRAINT: a leg whose face can go away WHILE the leg keeps running MUST
|
|
44
|
+
* supply it — `/v1/tasks/stream` under `x-detach-on-disconnect: true` keeps executing inside this ALS scope after the
|
|
45
|
+
* client left. Without the predicate the adjudication would read "live" and hand the leg to a face nobody can reach.
|
|
46
|
+
* ABSENT ⇒ always reachable, which is exactly right for the background leg (its face IS the durable events tail,
|
|
47
|
+
* readable whenever a client attaches). The CLOSE frame never asks it: it is a record, and it lands in the ledger twin
|
|
48
|
+
* whether or not the socket is still there. */
|
|
40
49
|
deliverable?: () => boolean;
|
|
41
50
|
}
|
|
42
51
|
/** The per-run question window the deployment owns (core caps nothing). Env: `QUESTION_MAX_CONCURRENT_PER_RUN` /
|
package/dist/question.js
CHANGED
|
@@ -3,6 +3,16 @@ import { uuidv7 } from "@sema-agent/core";
|
|
|
3
3
|
import { redactDeep } from "./trace/redact.js";
|
|
4
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
5
5
|
const MAX_QUESTIONS_BYTES = 16384;
|
|
6
|
+
function isFaceReachable(ctx) {
|
|
7
|
+
if (ctx.deliverable === undefined)
|
|
8
|
+
return true;
|
|
9
|
+
try {
|
|
10
|
+
return ctx.deliverable();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
export const DEFAULT_QUESTION_THROTTLE = {
|
|
7
17
|
maxConcurrentPerRun: 2,
|
|
8
18
|
maxTotalPerRun: 20,
|
|
@@ -103,13 +113,20 @@ export class QuestionCoordinator {
|
|
|
103
113
|
ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
104
114
|
this.pending.set(id, { settle, owner: ctx.owner, taskId: ctx.taskId });
|
|
105
115
|
const emitted = (async () => {
|
|
116
|
+
if (!isFaceReachable(ctx))
|
|
117
|
+
throw new Error("question delivery face unreachable — the open frame has nobody to show it to");
|
|
106
118
|
const now = Date.now();
|
|
107
|
-
|
|
119
|
+
await ctx.emit({ type: "question", questionId: id, questions, serverNowMs: now, expiresInMs: this.throttle.ttlMs, expiresAtMs: now + this.throttle.ttlMs });
|
|
120
|
+
return isFaceReachable(ctx) ? "ok" : "landed-face-gone";
|
|
108
121
|
})()
|
|
109
|
-
.then(() =>
|
|
122
|
+
.then((r) => r, () => "failed");
|
|
110
123
|
const delivery = await Promise.race([emitted, answerP.then(() => "stalled")]);
|
|
111
|
-
if (delivery === "ok") {
|
|
124
|
+
if (delivery === "ok" || delivery === "landed-face-gone") {
|
|
112
125
|
rc.total += 1;
|
|
126
|
+
if (delivery === "landed-face-gone") {
|
|
127
|
+
recordFailOpen("server.question.open-frame-undelivered", `question=${id} face-gone-while-landing`);
|
|
128
|
+
settle(UNAVAILABLE, "unanswered");
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
else if (delivery === "failed") {
|
|
115
132
|
recordFailOpen("server.question.open-frame-undelivered", `question=${id}`);
|
|
@@ -146,16 +163,7 @@ export class QuestionCoordinator {
|
|
|
146
163
|
}
|
|
147
164
|
hasLiveContext() {
|
|
148
165
|
const ctx = this.als.getStore();
|
|
149
|
-
|
|
150
|
-
return false;
|
|
151
|
-
if (ctx.deliverable === undefined)
|
|
152
|
-
return true;
|
|
153
|
-
try {
|
|
154
|
-
return ctx.deliverable();
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
166
|
+
return ctx !== undefined && isFaceReachable(ctx);
|
|
159
167
|
}
|
|
160
168
|
countersFor(taskId) {
|
|
161
169
|
let rc = this.counters.get(taskId);
|
|
@@ -54,9 +54,9 @@ export type ContentSteerBody = {
|
|
|
54
54
|
export type InterruptSteerBody = {
|
|
55
55
|
readonly [K in (typeof INTERRUPT_STEER_BODY_KEYS)[number]]?: unknown;
|
|
56
56
|
};
|
|
57
|
-
/**
|
|
57
|
+
/** 送话口的体形(闭集;词外是编译错)。`interrupt` = `POST /v1/runs/:id/interrupt` 整扇门(bare 与带 `text` 两形同一张受理集)。 */
|
|
58
58
|
export type SteerBodyForm = "run" | "content" | "interrupt";
|
|
59
|
-
/** 送话口的顶层键裁决(
|
|
59
|
+
/** 送话口的顶层键裁决(各形同一只;`form` 选受理集)。 */
|
|
60
60
|
export declare function steerBodyKeyIssue(raw: unknown, form: SteerBodyForm): KeyClosureIssue | null;
|
|
61
61
|
export {};
|
|
62
62
|
//# sourceMappingURL=request-key-closure.d.ts.map
|
|
@@ -28,25 +28,28 @@ const TASK_BODY_DECLARED_KEYS = [
|
|
|
28
28
|
"resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
|
|
29
29
|
"rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
|
|
30
30
|
"sessionId", "settings", "skills", "suggestNextPrompts", "systemPrompt", "taskId", "toolMaterializeStrategy",
|
|
31
|
-
"userInvokedSkills", "verify", "verifyRounds",
|
|
31
|
+
"userAttachments", "userInvokedSkills", "verify", "verifyRounds",
|
|
32
32
|
];
|
|
33
33
|
const TASK_BODY_SCENARIO_KEYS = ["repo"];
|
|
34
34
|
const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
|
|
35
|
-
const UNSUPPORTED_BODY_KEYS = ["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "
|
|
36
|
-
const UNSUPPORTED_BODY =
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const UNSUPPORTED_BODY_KEYS = ["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "writeFace"];
|
|
36
|
+
const UNSUPPORTED_BODY = {
|
|
37
|
+
keys: new Set(UNSUPPORTED_BODY_KEYS),
|
|
38
|
+
label: "unsupported (an engine seat this door does not read from the request body — set by the deployment)",
|
|
39
|
+
};
|
|
40
|
+
const OBJECTIVE_ONLY_BODY_KEYS = ["userAttachments"];
|
|
41
|
+
function closedBodyKeyIssue(raw, accepted, unsupported, door) {
|
|
39
42
|
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
40
43
|
return null;
|
|
41
44
|
const buckets = { unknown: [], unsupported: [] };
|
|
42
|
-
scanKeyClosure(raw, "", accepted,
|
|
45
|
+
scanKeyClosure(raw, "", accepted, unsupported.keys, buckets);
|
|
43
46
|
if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
|
|
44
47
|
return null;
|
|
45
48
|
buckets.unknown.sort();
|
|
46
49
|
buckets.unsupported.sort();
|
|
47
50
|
const parts = [
|
|
48
51
|
...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, `unknown (not ${door} key — check the spelling)`)] : []),
|
|
49
|
-
...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported,
|
|
52
|
+
...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, unsupported.label)] : []),
|
|
50
53
|
];
|
|
51
54
|
return {
|
|
52
55
|
message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
|
|
@@ -57,7 +60,7 @@ function closedBodyKeyIssue(raw, accepted, door) {
|
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
export function taskBodyKeyIssue(raw) {
|
|
60
|
-
return closedBodyKeyIssue(raw, ACCEPTED_BODY, "a task-request");
|
|
63
|
+
return closedBodyKeyIssue(raw, ACCEPTED_BODY, UNSUPPORTED_BODY, "a task-request");
|
|
61
64
|
}
|
|
62
65
|
const RUN_STEER_BODY_KEYS = ["mode", "priority", "text"];
|
|
63
66
|
const CONTENT_STEER_BODY_KEYS = ["content"];
|
|
@@ -67,7 +70,11 @@ const STEER_DOORS = {
|
|
|
67
70
|
content: { accepted: new Set(CONTENT_STEER_BODY_KEYS), door: "a steer-request" },
|
|
68
71
|
interrupt: { accepted: new Set(INTERRUPT_STEER_BODY_KEYS), door: "an interrupt-request" },
|
|
69
72
|
};
|
|
73
|
+
const STEER_UNSUPPORTED = {
|
|
74
|
+
keys: new Set([...UNSUPPORTED_BODY_KEYS, ...OBJECTIVE_ONLY_BODY_KEYS]),
|
|
75
|
+
label: "unsupported (an engine seat this door does not read from the request body — set by the deployment, or carried only by a new objective)",
|
|
76
|
+
};
|
|
70
77
|
export function steerBodyKeyIssue(raw, form) {
|
|
71
|
-
return closedBodyKeyIssue(raw, STEER_DOORS[form].accepted, STEER_DOORS[form].door);
|
|
78
|
+
return closedBodyKeyIssue(raw, STEER_DOORS[form].accepted, STEER_UNSUPPORTED, STEER_DOORS[form].door);
|
|
72
79
|
}
|
|
73
80
|
//# sourceMappingURL=request-key-closure.js.map
|
|
@@ -11,13 +11,22 @@
|
|
|
11
11
|
* 请求 allow 桶、技能两只清单、运维表四张表同一只、同一句)。
|
|
12
12
|
* ② (7.116.0–7.122.x 的「引擎读不到的一形」—— 工具段为空的协议名 `mcp__srv__` 四张表都拒 —— 随 core 7.48.0 #1400 退役:引擎判官给它判词
|
|
13
13
|
* `unmintable_segment`,条目由下面 ③ 读成 `tool_name`,与对端段铸不出的名字同一条路。)
|
|
14
|
-
* ③ **引擎读成空的条目**({@link inertRuleEntriesOf},判据 = 引擎包根 `compileAllowList(entries, behavior, 名册)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* ③ **引擎读成空的条目**({@link inertRuleEntriesOf},判据 = 引擎包根 `compileAllowList(entries, behavior, 名册)`;原因词是引擎的 `ALLOW_ENTRY_INERT_REASONS`)。
|
|
15
|
+
* 条目的工具名也过引擎判官:退役名 / 无前缀短名 / 对端段或工具段铸不出、为空读成 `tool_name`,带判词与 1:1 现名;名册里有那只工具、条目只差大小写 ⇒ `near_miss`(`BASH` ⇒ 现名 `Bash`)。
|
|
16
|
+
* **名册只有一张**({@link TOOL_NAME_ROSTER}:引擎目录 ∪ 本仓自铸的工具名),每一处读条目名的地方都传它 —— 请求体的规则表(`settings.permissions.*`)、技能清单、选择器清单、
|
|
17
|
+
* 运维 / 中心的审批名单读同一份,没有「这一处空名册、那一处带名册」的两种读法。
|
|
18
|
+
* **读成空的哪些原因词是拒面,按方向定**:**限制侧**(读成空 = 静默放行:技能 `disallowedTools`、`excludeTools` / 子代 `denyTools`、`settings.permissions.deny` / `ask`、运维 / 中心的审批名单)四类全拒 ——
|
|
19
|
+
* 近似名 · 退役名 · 文法读不出 · 不带内容形的工具带内容;**放行侧**(读成空 = 想放的什么也没放:`settings.permissions.allow`、根 / 子代 `allowTools`、技能 `allowedTools`)**只拒近似名**
|
|
20
|
+
* ({@link allowSideReadsAsNothing}):近似名现名唯一、确定是笔误;退役名 / 内容形 / 文法读不出 / 协议名对端段通配照 7.130.0 受理、交引擎既有通告(引擎侧的终形是把退役名归一到现名并在用户面点名,到货前放行侧不拒 ——
|
|
21
|
+
* 壳转投的 CC 用户 settings 里这些极常见,尤其内容形里的 `$VAR`(`Bash(echo $HOME)`)读成 `grammar`,拒了 = 每次提交 400)。运维的 `APPROVAL_GRANT` 是运维自己的配置:任何读成空都拒启(写错了当场改)。
|
|
18
22
|
* 技能拒侧那一拒与引擎 #1372(core 7.43.0:那只技能不激活)是同一件事的两处 —— 7.118.0 重估过、本版**不退**:本仓的续跑腿把引擎的
|
|
19
23
|
* `checkpoint.config_refused`(`config.skill_deny_unreadable`)答得不一(资源停车续跑腿 500、`/decide` 先结了卡、`/wake` 先铸了 run 行),先统一那一层再退
|
|
20
|
-
* (
|
|
24
|
+
* (重估读数与退役条件在下面 {@link skillDenyInertEntryOf} 头注)。
|
|
25
|
+
* ④ **工具名选择器清单**(请求顶层 `allowTools` / `excludeTools`、子代定义的 `allowTools` / `denyTools`;引擎的可用性选择器,**不是**规则条目表):读成空的条目在受理口**开跑前拒并点名现名**
|
|
26
|
+
* (拒面按 ③ 的方向分),拒句点名有界({@link MAX_NAMED_INERT_ENTRIES} 条 + 计数),清单先过条数帽({@link inertToolListEntryOf})。
|
|
27
|
+
* **真未知名**(拼写合法、没有近似也没有退役对应 —— 懒挂载的合法形,例如一台之后才连上的 MCP 服务器的工具)不是读成空:原样保留、运维行告知一次,不拒。
|
|
28
|
+
* 选择器读一条条目 = 引擎 `judgeToolNameList` 的读法({@link selectorEntryName}):先取工具名那一段再只判名字(限制侧 `Tool(content)` ⇒ 整只工具,内容里写什么都不读;放行侧内容形引擎告「什么也不授」,受理口不拒);
|
|
29
|
+
* `*` 在放行侧是「本层不设名单」、在限制侧读成空;限制侧 `mcp__*` / `a2a__*`(整个协议的拒)合法。
|
|
21
30
|
*/
|
|
22
31
|
import { compileAllowList } from "@sema-agent/core";
|
|
23
32
|
/** ① 形:字符串数组(空串也是字符串 —— 引擎把它读成 `grammar` 并通告 / 拒,见 ③)。 */
|
|
@@ -43,35 +52,133 @@ export declare function allowLayerEntryCountIssue(field: string, list: readonly
|
|
|
43
52
|
} | undefined;
|
|
44
53
|
/** 一条引擎读成空的条目(引擎的形:原因词,名字层另带判词 / 1:1 现名;包根不导出这只具名型 ⇒ 从 `compileAllowList` 的返回型取,不手抄)。 */
|
|
45
54
|
export type InertRuleEntry = ReturnType<typeof compileAllowList>["inert"][number];
|
|
46
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* ③ 引擎读一张**规则条目表**(`behavior` 方向)时读成空的条目(引擎的整表读法:allow 表里的 `*` 让整表读成空;deny 表里的 `*` 只读空它自己)。条目名过引擎判官,
|
|
57
|
+
* 名册是本仓**唯一**的一张({@link TOOL_NAME_ROSTER},`namespaced-rule-name.ts`:引擎目录 ∪ 本仓自铸的工具名)—— 两个方向、每一处读条目名的地方(请求体的规则表、技能清单、选择器清单、
|
|
58
|
+
* 运维 / 中心的审批名单)传同一张,没有「这一处空名册、那一处带名册」的两种读法。工具名选择器清单另走 {@link inertToolListEntryOf}。
|
|
59
|
+
*/
|
|
47
60
|
export declare function inertRuleEntriesOf(list: readonly string[], behavior: "allow" | "deny"): readonly InertRuleEntry[];
|
|
48
61
|
/**
|
|
49
62
|
* 原因词 → 「怎么写」(闭集穷举:引擎加一个原因词 ⇒ 编译红)。句子只说这一条为什么什么也不做、换成哪种写法;原因词本身原样带在句子里。
|
|
50
63
|
* 名字层两词带引擎给的判词 / 1:1 现名(有就点名)。
|
|
51
64
|
*/
|
|
52
65
|
export declare function inertRuleEntryGuidance(i: InertRuleEntry): string;
|
|
53
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* 拒句 / 通告句里点名条目的**条数上界**(点名有界,同引擎自己通告的「最多五条 + and N more」一个形:体再大,一句话的长度不随条目数长)。
|
|
68
|
+
* 每条点名至多约 600 字节(条目回显 ≤ 122 字符 + 原因词 + 引擎的怎么写),五条 + 尾 ≤ 3.5 KB。
|
|
69
|
+
*/
|
|
70
|
+
export declare const MAX_NAMED_INERT_ENTRIES = 5;
|
|
71
|
+
/** ③ 的句子(`<field> entry "<e>" reads as nothing (<reason>): <怎么写>`;多条用分号连;只点名前 {@link MAX_NAMED_INERT_ENTRIES} 条,其余写成 `; and K more`)。全部读成空的句子的**唯一组句器**。 */
|
|
54
72
|
export declare function describeInertRuleEntries(field: string, inert: readonly InertRuleEntry[]): string;
|
|
55
73
|
/** 提交体里技能所在的层(命中句与排空读数点名用):顶层 `skills[]` / 子代 `agents[].skills[]`。 */
|
|
56
74
|
export type SkillListLayer = "skills" | "agents.skills";
|
|
57
|
-
/**
|
|
58
|
-
export
|
|
59
|
-
|
|
75
|
+
/** 清单的方向:`allow` = 放行名单(读成空 = 想放的什么也没放 = 静默收窄),`deny` = 限制名单(读成空 = 放行)。续跑重放按方向分流(见 {@link inertToolListEntryOf})。 */
|
|
76
|
+
export type ToolListSide = "allow" | "deny";
|
|
77
|
+
/**
|
|
78
|
+
* 每一张清单(闭集;"层")的方向、读法与拒码。拒码沿用该清单此前的码:技能与请求规则表 ⇒ 请求字段非法;子代定义的两张选择器 ⇒ 引擎的子代名单拒码;顶层两张选择器 ⇒ 引擎的
|
|
79
|
+
* 根 run 名单拒码(同一张表的文法形 / `*` 引擎仍拒这一码,一张表一个码)。续跑腿把同一只码放进 409 的 `blockedBy`。加一张清单 ⇒ 这里加一行,{@link toolListsOf} 的扫描一起编译红。
|
|
80
|
+
*/
|
|
81
|
+
declare const TOOL_LIST_FACES: {
|
|
82
|
+
readonly skills: {
|
|
83
|
+
readonly side: "deny";
|
|
84
|
+
readonly kind: "rule_table";
|
|
85
|
+
readonly code: "request.field_invalid";
|
|
86
|
+
};
|
|
87
|
+
readonly "agents.skills": {
|
|
88
|
+
readonly side: "deny";
|
|
89
|
+
readonly kind: "rule_table";
|
|
90
|
+
readonly code: "request.field_invalid";
|
|
91
|
+
};
|
|
92
|
+
readonly "skills.allowedTools": {
|
|
93
|
+
readonly side: "allow";
|
|
94
|
+
readonly kind: "rule_table";
|
|
95
|
+
readonly code: "request.field_invalid";
|
|
96
|
+
};
|
|
97
|
+
readonly "agents.skills.allowedTools": {
|
|
98
|
+
readonly side: "allow";
|
|
99
|
+
readonly kind: "rule_table";
|
|
100
|
+
readonly code: "request.field_invalid";
|
|
101
|
+
};
|
|
102
|
+
readonly "agents.denyTools": {
|
|
103
|
+
readonly side: "deny";
|
|
104
|
+
readonly kind: "selector";
|
|
105
|
+
readonly code: "config.agent.unknown_tool";
|
|
106
|
+
};
|
|
107
|
+
readonly "agents.allowTools": {
|
|
108
|
+
readonly side: "allow";
|
|
109
|
+
readonly kind: "selector";
|
|
110
|
+
readonly code: "config.agent.unknown_tool";
|
|
111
|
+
};
|
|
112
|
+
readonly excludeTools: {
|
|
113
|
+
readonly side: "deny";
|
|
114
|
+
readonly kind: "selector";
|
|
115
|
+
readonly code: "config.tool_face_control";
|
|
116
|
+
};
|
|
117
|
+
readonly allowTools: {
|
|
118
|
+
readonly side: "allow";
|
|
119
|
+
readonly kind: "selector";
|
|
120
|
+
readonly code: "config.tool_face_control";
|
|
121
|
+
};
|
|
122
|
+
readonly "settings.permissions.allow": {
|
|
123
|
+
readonly side: "allow";
|
|
124
|
+
readonly kind: "rule_table";
|
|
125
|
+
readonly code: "request.field_invalid";
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/** 提交体里读成空即拒的工具名清单所在的层(闭集 = {@link TOOL_LIST_FACES} 的键)。 */
|
|
129
|
+
export type ToolListLayer = keyof typeof TOOL_LIST_FACES;
|
|
130
|
+
/**
|
|
131
|
+
* 请求体 `settings.permissions.deny` / `ask`(规则编译器读的收紧表)所在的层 —— **不在** {@link toolListsOf} 的扫描里:它们的判与句子的唯一铸点在 `config.ts` 的
|
|
132
|
+
* `unmatchableToolNamesRefusal`(经 `task-settings.ts` 的 `rulePolicyBucketsRefusal`,单表桶序;拒点是 `applyTaskSettings`,422),存体预判(`http/stored-body-replay.ts`)调同一只函数(home 同源于部署配置),把命中装成本型。
|
|
133
|
+
* 层名进这个联合只为让命中能带坐标进 409 体与运维日志。
|
|
134
|
+
*/
|
|
135
|
+
export type RulePolicyLayer = "settings.permissions.deny" | "settings.permissions.ask";
|
|
136
|
+
/** 一份提交体里第一处「工具名清单里有引擎读成空的条目」:在哪一层、哪个方向、拒码、拒句(超帽时另带机读字段,同 `settings.permissions.*` 桶超帽的形)。 */
|
|
137
|
+
export interface InertToolListEntry {
|
|
138
|
+
readonly layer: ToolListLayer | RulePolicyLayer;
|
|
139
|
+
readonly side: ToolListSide;
|
|
140
|
+
readonly code: string;
|
|
60
141
|
readonly message: string;
|
|
142
|
+
readonly extra?: Readonly<Record<string, unknown>>;
|
|
61
143
|
}
|
|
62
144
|
/**
|
|
63
|
-
*
|
|
145
|
+
* 存体预判里请求设置的命中装成本型(拒点自己抛:桶门在 `resolveSpec` 抛 400、deny / ask 的名字层与文法在 `applyTaskSettings` 抛 422,都不经本型)—— 两个来源一只装配函数:
|
|
146
|
+
* 桶门命中(三个桶任一,带机读字段 `extra`)与 deny / ask 的规则判命中(限制侧,无 `extra`)。
|
|
147
|
+
*/
|
|
148
|
+
export declare function replayInertEntry(layer: ToolListLayer | RulePolicyLayer, side: ToolListSide, code: string, message: string, extra?: Readonly<Record<string, unknown>>): InertToolListEntry;
|
|
149
|
+
/** 技能层的命中(排空脚本只数这两层)。 */
|
|
150
|
+
export interface SkillDenyInertEntry extends InertToolListEntry {
|
|
151
|
+
readonly layer: SkillListLayer;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* ③ 的拒侧在**提交体**上的判,技能那两层(顶层 `skills[]` 与子代 `agents[].skills[]`;第一处命中即答)。全部工具名清单的判是 {@link inertToolListEntryOf}(调用点两处都调它);
|
|
155
|
+
* 本函数单独导出是给随包排空脚本:那条判据(`skill-deny-inert`)只数技能这两层,SQL 车道的孪生也只看技能的清单。
|
|
64
156
|
*
|
|
65
|
-
*
|
|
66
|
-
* 是结构性的)与 `http/checkpoint-ctx.ts` `storedBodyReplayRefusal`(存量体重放前的**预判**,五个续跑问点同调 —— 含 SLA 拒扫的跳过臂与终态 steer
|
|
67
|
-
* 铸唤醒卡之前,这两处不经 `resolveSpec`)。提交腿 ⇒ 400 `request.field_invalid`;续跑腿 ⇒ 409 `resume_blocked_by_policy`(`blockedBy: request.field_invalid`,
|
|
68
|
-
* 句子同一句,铸点 `checkpoint-ctx.ts` `resumeBlockedByPolicyAnswer`),拒在认领之前,行 / 卡 / ctx 不动。随包排空判据 `skill-deny-inert`
|
|
69
|
-
* (`scripts/drain-check-local.mjs` 调本函数)。判据就是引擎的读法(`compileAllowList(entries, "deny", 名册 = 空集)`,与 {@link inertRuleEntriesOf} 同一只;
|
|
70
|
-
* 7.118.0 起条目名也过引擎判官,退役名 / 对端段铸不出读成 `tool_name` —— 技能清单的名字层在受理口不再另判)。
|
|
157
|
+
* 判据就是引擎的读法({@link inertRuleEntriesOf};7.118.0 起条目名也过引擎判官,退役名 / 对端段铸不出读成 `tool_name` —— 技能清单的名字层在受理口不再另判)。
|
|
71
158
|
* 7.118.0 重估(core 7.43.0 #1372 到货:读成空的拒侧条目让那只技能不激活)—— **本版不退**。实测(本判退掉之后跑同一批夹具):引擎的拒到本仓各续跑腿的
|
|
72
159
|
* 答法不一 —— 资源停车续跑腿把 `checkpoint.config_refused` 答成 500 `internal.error`;工具审批 `/decide` 在引擎拒之前已把卡结掉;`/wake` 在引擎拒之前已铸
|
|
73
160
|
* run 行;子代 `agents[].skills[]` 不在引擎的认领前屏里(续跑放行、技能在调用时被拒)。退役条件:本仓续跑腿对引擎 `checkpoint.config_refused` 的答法
|
|
74
161
|
* 收成一只(拒在认领前、行 / 卡不动、同一个 409)之后,本判与本仓的预判一起退,排空判据随之重估。
|
|
75
162
|
*/
|
|
76
163
|
export declare function skillDenyInertEntryOf(body: unknown): SkillDenyInertEntry | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* **凡工具名清单,读成空的条目 = 开跑前拒并点名现名;真未知名只告** —— 提交体上全部这类清单的判(见 {@link TOOL_LIST_FACES}:技能的 `disallowedTools` 两层、子代定义的 `denyTools` / `allowTools`、
|
|
166
|
+
* 顶层 `excludeTools` / `allowTools`、请求的 `settings.permissions.allow`;第一处命中即答,顺序见 {@link toolListsOf})。判官只有一只:引擎的条目读法(名册 = {@link TOOL_NAME_ROSTER}),
|
|
167
|
+
* 本函数只是「在体的哪几处取清单」;句子只有一个有界的组句器({@link describeInertRuleEntries}),清单先过条数帽({@link allowLayerEntryCountIssue})。
|
|
168
|
+
*
|
|
169
|
+
* 为什么限制侧四类全拒、放行侧只拒近似名:引擎 7.51.x 把「没有任何工具答应的名字」改成条目原样保留 + 运维行告知一次。限制侧读成空 = **静默放行**(`denyTools:["BASH"]` 以为禁了 `Bash`,子代照用);
|
|
170
|
+
* 放行侧读成空 = 想放的什么也没放 —— 其中**近似名**(现名唯一)是确定的笔误,拒并点名现名;**退役名**、**内容形**与**文法读不出**在放行侧照 7.130.0 受理、交引擎既有通告(见头注 ③)。
|
|
171
|
+
* **真未知名**(没有近似也没有退役对应)不在读成空之列,原样保留、告运维一次(之后挂上同名工具即生效)。请求的 `settings.permissions.deny` / `ask`
|
|
172
|
+
* 与运维 / 中心的审批名单读同一张名册,走另一只入口({@link unmatchableToolNamesRefusal},`config.ts`),不在本函数(存体预判调同一只判据、home 同源于部署配置,见 `http/stored-body-replay.ts`)。
|
|
173
|
+
*
|
|
174
|
+
* `sides` = 只看哪个方向的清单(缺省两个都看)。**条数帽是资源帽,不分方向**:超帽的清单(整只拒,不逐条读)在任一方向的过滤下都命中 —— 引擎每次准备都逐条播种这些表,一份超帽的清单不论条目读得出读不出都会卡住事件循环。
|
|
175
|
+
* **提交腿两个方向都拒**;**续跑重放按方向分流**:限制方向(`deny`)的存量条目读成空 ⇒ 拒(409:放宽方向不能默认,那条限制从没生效过);放行方向(`allow`)的存量条目读成空 ⇒ 照旧续跑、条目原样留着
|
|
176
|
+
* (升级前它本就这样跑着、没那只工具,继续是安全方向)+ 一行运维 warn。
|
|
177
|
+
*
|
|
178
|
+
* 🔴 **调用点两处**:`resolveSpec`(`boot/resolve-spec.ts`,**拒点** —— 新提交与五条续跑腿都经过它)与 `http/stored-body-replay.ts` `storedBodyReplayRefusal`
|
|
179
|
+
* (存量体重放前的**预判**,五个续跑问点同调,只问限制方向)。提交腿 ⇒ 400,码 = 该层的拒码;续跑腿 ⇒ 409 `resume_blocked_by_policy`(`blockedBy` = 同一只码,句子同一句,铸点
|
|
180
|
+
* `checkpoint-ctx.ts` `resumeBlockedByPolicyAnswer`),拒在认领之前,行 / 卡 / ctx 不动。
|
|
181
|
+
*/
|
|
182
|
+
export declare function inertToolListEntryOf(body: unknown, sides?: readonly ToolListSide[]): InertToolListEntry | undefined;
|
|
183
|
+
export {};
|
|
77
184
|
//# sourceMappingURL=rule-entry-list.d.ts.map
|
package/dist/rule-entry-list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { compileAllowList } from "@sema-agent/core";
|
|
1
|
+
import { PROTOCOL_TABLE, compileAllowList, parsePermissionRule } from "@sema-agent/core";
|
|
2
2
|
import { displayListEntry } from "./list-entry-display.js";
|
|
3
|
-
import {
|
|
3
|
+
import { TOOL_NAME_ROSTER } from "./namespaced-rule-name.js";
|
|
4
4
|
export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
5
5
|
export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
|
|
6
6
|
export function describeAllowLayerOverCap(field, count) {
|
|
@@ -13,11 +13,15 @@ export function allowLayerEntryCountIssue(field, list) {
|
|
|
13
13
|
return { message: describeAllowLayerOverCap(field, list.length), count: list.length, cap: MAX_ALLOW_LAYER_ENTRIES };
|
|
14
14
|
}
|
|
15
15
|
export function inertRuleEntriesOf(list, behavior) {
|
|
16
|
-
return compileAllowList(list, behavior,
|
|
16
|
+
return compileAllowList(list, behavior, TOOL_NAME_ROSTER).inert;
|
|
17
|
+
}
|
|
18
|
+
function peerSegmentIsWild(entry) {
|
|
19
|
+
const at = entry.indexOf("__");
|
|
20
|
+
return at >= 0 && (entry.slice(at + 2).split("__")[0] ?? "").includes("*");
|
|
17
21
|
}
|
|
18
22
|
export function inertRuleEntryGuidance(i) {
|
|
19
23
|
const reason = i.reason;
|
|
20
|
-
const write = i.currentName !== undefined ? ` — write ${displayListEntry(i.currentName)}` : "";
|
|
24
|
+
const write = i.currentName !== undefined && !peerSegmentIsWild(i.entry) ? ` — write ${displayListEntry(i.currentName)}` : "";
|
|
21
25
|
switch (reason) {
|
|
22
26
|
case "tool_name":
|
|
23
27
|
return `the tool name can never match a mounted tool (${i.verdict ?? "stale"})${write}`;
|
|
@@ -39,30 +43,85 @@ export function inertRuleEntryGuidance(i) {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
export const MAX_NAMED_INERT_ENTRIES = 5;
|
|
42
47
|
export function describeInertRuleEntries(field, inert) {
|
|
43
|
-
|
|
48
|
+
const named = inert.slice(0, MAX_NAMED_INERT_ENTRIES).map((i) => `${field} entry ${displayListEntry(i.entry)} reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
|
|
49
|
+
const more = inert.length - MAX_NAMED_INERT_ENTRIES;
|
|
50
|
+
return more > 0 ? `${named}; and ${more} more` : named;
|
|
51
|
+
}
|
|
52
|
+
const TOOL_LIST_FACES = {
|
|
53
|
+
"skills": { side: "deny", kind: "rule_table", code: "request.field_invalid" },
|
|
54
|
+
"agents.skills": { side: "deny", kind: "rule_table", code: "request.field_invalid" },
|
|
55
|
+
"skills.allowedTools": { side: "allow", kind: "rule_table", code: "request.field_invalid" },
|
|
56
|
+
"agents.skills.allowedTools": { side: "allow", kind: "rule_table", code: "request.field_invalid" },
|
|
57
|
+
"agents.denyTools": { side: "deny", kind: "selector", code: "config.agent.unknown_tool" },
|
|
58
|
+
"agents.allowTools": { side: "allow", kind: "selector", code: "config.agent.unknown_tool" },
|
|
59
|
+
"excludeTools": { side: "deny", kind: "selector", code: "config.tool_face_control" },
|
|
60
|
+
"allowTools": { side: "allow", kind: "selector", code: "config.tool_face_control" },
|
|
61
|
+
"settings.permissions.allow": { side: "allow", kind: "rule_table", code: "request.field_invalid" },
|
|
62
|
+
};
|
|
63
|
+
const LIST_OVER_CAP_CODE = "request.field_invalid";
|
|
64
|
+
export function replayInertEntry(layer, side, code, message, extra) {
|
|
65
|
+
return { layer, side, code, message, ...(extra !== undefined ? { extra } : {}) };
|
|
44
66
|
}
|
|
45
67
|
const isPlainRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const inert = inertRuleEntriesOf(deny, "deny");
|
|
53
|
-
if (inert.length === 0)
|
|
54
|
-
return undefined;
|
|
55
|
-
return `${describeInertRuleEntries("skill.disallowedTools", inert)} — a deny entry that reads as nothing refuses nothing, so it is refused here rather than taken as in effect`;
|
|
68
|
+
const REFUSAL_TAIL = {
|
|
69
|
+
deny: "a deny entry that reads as nothing refuses nothing, so it is refused here rather than taken as in effect",
|
|
70
|
+
allow: "an allow entry that reads as nothing admits nothing, so it is refused here rather than taken as in effect",
|
|
71
|
+
};
|
|
72
|
+
function selectorEntryName(entry) {
|
|
73
|
+
return entry !== entry.trim() ? entry : parsePermissionRule(entry).toolName;
|
|
56
74
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
const isProtocolWideName = (name) => PROTOCOL_TABLE.some((e) => name === `${e.prefix}*`);
|
|
76
|
+
function selectorInertEntriesOf(list, side) {
|
|
77
|
+
return list.flatMap((entry) => {
|
|
78
|
+
if (entry === "*" && side === "allow")
|
|
79
|
+
return [];
|
|
80
|
+
const name = selectorEntryName(entry);
|
|
81
|
+
if (side === "deny" && isProtocolWideName(name))
|
|
82
|
+
return [];
|
|
83
|
+
const inert = compileAllowList([name], "deny", TOOL_NAME_ROSTER).inert.map((i) => ({ ...i, entry }));
|
|
84
|
+
return side === "allow" ? inert.filter((i) => allowSideReadsAsNothing(i.reason)) : inert;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function allowSideReadsAsNothing(reason) {
|
|
88
|
+
switch (reason) {
|
|
89
|
+
case "near_miss":
|
|
90
|
+
return true;
|
|
91
|
+
case "grammar":
|
|
92
|
+
case "tool_name":
|
|
93
|
+
case "content_unsupported":
|
|
94
|
+
case "wildcard_all":
|
|
95
|
+
case "namespace_wildcard":
|
|
96
|
+
return false;
|
|
97
|
+
case "earlier_grammar":
|
|
98
|
+
throw new Error(`allow-entry inert reason ${JSON.stringify(reason)} belongs to the session-rule store's bucket reading, never to an allow list`);
|
|
99
|
+
default: {
|
|
100
|
+
const unhandled = reason;
|
|
101
|
+
throw new Error(`unhandled allow-entry inert reason: ${String(unhandled)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function requestAllowRuleInertEntriesOf(list) {
|
|
106
|
+
return list.flatMap((entry) => compileAllowList([entry], "allow", TOOL_NAME_ROSTER).inert.filter((i) => allowSideReadsAsNothing(i.reason)));
|
|
107
|
+
}
|
|
108
|
+
function inertEntriesOfList(list, layer) {
|
|
109
|
+
const face = TOOL_LIST_FACES[layer];
|
|
110
|
+
if (face.kind === "selector")
|
|
111
|
+
return selectorInertEntriesOf(list, face.side);
|
|
112
|
+
return face.side === "deny" ? inertRuleEntriesOf(list, "deny") : requestAllowRuleInertEntriesOf(list);
|
|
113
|
+
}
|
|
114
|
+
function* toolListsOf(body) {
|
|
60
115
|
const skills = body["skills"];
|
|
61
116
|
if (Array.isArray(skills)) {
|
|
62
117
|
for (const skill of skills) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
118
|
+
if (!isPlainRecord(skill))
|
|
119
|
+
continue;
|
|
120
|
+
yield { layer: "skills", field: "skill.disallowedTools", prefix: "", list: skill["disallowedTools"] };
|
|
121
|
+
}
|
|
122
|
+
for (const skill of skills) {
|
|
123
|
+
if (isPlainRecord(skill))
|
|
124
|
+
yield { layer: "skills.allowedTools", field: "skill.allowedTools", prefix: "", list: skill["allowedTools"] };
|
|
66
125
|
}
|
|
67
126
|
}
|
|
68
127
|
const agents = body["agents"];
|
|
@@ -73,12 +132,69 @@ export function skillDenyInertEntryOf(body) {
|
|
|
73
132
|
if (!Array.isArray(agentSkills))
|
|
74
133
|
continue;
|
|
75
134
|
for (let j = 0; j < agentSkills.length; j++) {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
|
|
135
|
+
const skill = agentSkills[j];
|
|
136
|
+
if (isPlainRecord(skill))
|
|
137
|
+
yield { layer: "agents.skills", field: "skill.disallowedTools", prefix: `agents[${i}]: skills[${j}]: `, list: skill["disallowedTools"] };
|
|
79
138
|
}
|
|
80
139
|
}
|
|
140
|
+
for (let i = 0; i < agents.length; i++) {
|
|
141
|
+
const agent = agents[i];
|
|
142
|
+
const agentSkills = isPlainRecord(agent) ? agent["skills"] : undefined;
|
|
143
|
+
if (!Array.isArray(agentSkills))
|
|
144
|
+
continue;
|
|
145
|
+
for (let j = 0; j < agentSkills.length; j++) {
|
|
146
|
+
const skill = agentSkills[j];
|
|
147
|
+
if (isPlainRecord(skill))
|
|
148
|
+
yield { layer: "agents.skills.allowedTools", field: "skill.allowedTools", prefix: `agents[${i}]: skills[${j}]: `, list: skill["allowedTools"] };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (let i = 0; i < agents.length; i++) {
|
|
152
|
+
const agent = agents[i];
|
|
153
|
+
if (!isPlainRecord(agent))
|
|
154
|
+
continue;
|
|
155
|
+
yield { layer: "agents.denyTools", field: `agents[${i}].denyTools`, prefix: "", list: agent["denyTools"] };
|
|
156
|
+
yield { layer: "agents.allowTools", field: `agents[${i}].allowTools`, prefix: "", list: agent["allowTools"] };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
yield { layer: "excludeTools", field: "excludeTools", prefix: "", list: body["excludeTools"] };
|
|
160
|
+
yield { layer: "allowTools", field: "allowTools", prefix: "", list: body["allowTools"] };
|
|
161
|
+
const settings = body["settings"];
|
|
162
|
+
const permissions = isPlainRecord(settings) ? settings["permissions"] : undefined;
|
|
163
|
+
if (isPlainRecord(permissions))
|
|
164
|
+
yield { layer: "settings.permissions.allow", field: "settings.permissions.allow", prefix: "", list: permissions["allow"] };
|
|
165
|
+
}
|
|
166
|
+
function entryOfList(l) {
|
|
167
|
+
const face = TOOL_LIST_FACES[l.layer];
|
|
168
|
+
if (Array.isArray(l.list)) {
|
|
169
|
+
const over = allowLayerEntryCountIssue(l.field, l.list);
|
|
170
|
+
if (over !== undefined)
|
|
171
|
+
return { layer: l.layer, side: face.side, code: LIST_OVER_CAP_CODE, message: `${l.prefix}${over.message}`, extra: { field: l.field, reason: "over_cap", count: over.count, cap: over.cap } };
|
|
172
|
+
}
|
|
173
|
+
if (!ruleEntryListShapeOk(l.list))
|
|
174
|
+
return undefined;
|
|
175
|
+
const inert = inertEntriesOfList(l.list, l.layer);
|
|
176
|
+
if (inert.length === 0)
|
|
177
|
+
return undefined;
|
|
178
|
+
return { layer: l.layer, side: face.side, code: face.code, message: `${l.prefix}${describeInertRuleEntries(l.field, inert)} — ${REFUSAL_TAIL[face.side]}` };
|
|
179
|
+
}
|
|
180
|
+
function firstInertEntry(body, acceptLayer, acceptSide) {
|
|
181
|
+
if (!isPlainRecord(body))
|
|
182
|
+
return undefined;
|
|
183
|
+
for (const l of toolListsOf(body)) {
|
|
184
|
+
if (!acceptLayer(l.layer))
|
|
185
|
+
continue;
|
|
186
|
+
const entry = entryOfList(l);
|
|
187
|
+
if (entry === undefined)
|
|
188
|
+
continue;
|
|
189
|
+
if (entry.extra !== undefined || acceptSide(entry.side))
|
|
190
|
+
return entry;
|
|
81
191
|
}
|
|
82
192
|
return undefined;
|
|
83
193
|
}
|
|
194
|
+
export function skillDenyInertEntryOf(body) {
|
|
195
|
+
return firstInertEntry(body, (layer) => layer === "skills" || layer === "agents.skills", () => true);
|
|
196
|
+
}
|
|
197
|
+
export function inertToolListEntryOf(body, sides = ["allow", "deny"]) {
|
|
198
|
+
return firstInertEntry(body, () => true, (side) => sides.includes(side));
|
|
199
|
+
}
|
|
84
200
|
//# sourceMappingURL=rule-entry-list.js.map
|
package/dist/run-local.d.ts
CHANGED
|
@@ -54,15 +54,15 @@ export declare class UsageError extends Error {
|
|
|
54
54
|
* set: …" list rather than letting the first brain call crash with a cryptic auth error. Returns a list of
|
|
55
55
|
* missing items (empty = OK).
|
|
56
56
|
*
|
|
57
|
-
* 🔴 S-623:这里**不再**判网关 base URL
|
|
58
|
-
*
|
|
57
|
+
* 🔴 S-623:这里**不再**判网关 base URL。「将要服务的那张表里每一条 openai 路由都解析到显式端点」是配置源应用之后的拒启判据
|
|
58
|
+
* (`model-route-endpoint.ts` 的 `judgeServedModelRoute`,本入口在预检之前就判过 —— 没设网关的配置根本走不到
|
|
59
59
|
* 这里),原来那条臂(a)是它的一份更弱的手抄:注释要求「本机用户必须指向自己的网关」,判据却只查 `/v1` 后缀,
|
|
60
60
|
* 旧的出厂缺省值恰好以 `/v1` 结尾 ⇒ 缺省值照样过(本仓自己也被骗)。`/v1` 后缀本身也一并删,理由三条:
|
|
61
61
|
* ① 同一个问题两个判官 —— `serve` 从不要求 `/v1`,同一份 .env 在 `serve` 起得来、在 `run-local` 被拒;
|
|
62
62
|
* ② 它会误拒真实存在的 OpenAI 兼容端点(DeepSeek 根路径直接服务 `/chat/completions`,本仓
|
|
63
63
|
* `src/bench/s1/live-deps.ts` 用的正是 `https://api.deepseek.com`;Gemini 的兼容面 `…/v1beta/openai`、Azure 的
|
|
64
64
|
* `…/openai/deployments/<name>` 也都不以 `/v1` 结尾);
|
|
65
|
-
* ③ 规则集只许变小:端点判据只有
|
|
65
|
+
* ③ 规则集只许变小:端点判据只有 `model-route-endpoint.ts` 那一只。
|
|
66
66
|
*
|
|
67
67
|
* NOTE: with an injected mock `brain` the gateway is irrelevant (no real call is made), so the caller skips
|
|
68
68
|
* this check in that case — the check is about a REAL run, not the assembly.
|