@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
|
@@ -35,6 +35,7 @@ export function buildServerWiringGates(input) {
|
|
|
35
35
|
postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
|
|
36
36
|
}),
|
|
37
37
|
checkpointStore: input.checkpointStore !== undefined,
|
|
38
|
+
approvalGrant: [...input.approvalGrant],
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
export function buildStaticWiringAudit(facts) {
|
|
@@ -121,6 +122,7 @@ export async function handleDiagnostics(req, res, match, ctx) {
|
|
|
121
122
|
streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
|
|
122
123
|
sessionAutoTitle: deps.config.sessionAutoTitle === true,
|
|
123
124
|
sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
|
|
125
|
+
approvalGrant: deps.config.approvalGrant,
|
|
124
126
|
}),
|
|
125
127
|
});
|
|
126
128
|
return;
|
|
@@ -5,7 +5,7 @@ import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
7
7
|
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
8
|
-
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS,
|
|
8
|
+
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
9
9
|
import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
10
10
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
11
|
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
@@ -27,6 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate
|
|
|
27
27
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
28
28
|
import { buildSyncSubmitBody } from "../sync-submit-body.js";
|
|
29
29
|
import { assertNever } from "../route-ctx.js";
|
|
30
|
+
import { buildCheckpointCtx } from "../checkpoint-ctx.js";
|
|
30
31
|
import { withModelReachScope } from "../../model-reach-scope.js";
|
|
31
32
|
import { createInstructionSelectionLeg } from "../../instruction-selection.js";
|
|
32
33
|
const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
@@ -252,7 +253,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
252
253
|
let ledgerSink;
|
|
253
254
|
let ledgerTerminal = false;
|
|
254
255
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
255
|
-
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
256
|
+
await deps.checkpointStore.putCtx(prepared.spec.sessionId, buildCheckpointCtx({ body: prepared.body, memoryScope: prepared.auth?.memoryScope }));
|
|
256
257
|
}
|
|
257
258
|
if (durableTaskId && deps.runStore) {
|
|
258
259
|
const rs = deps.runStore;
|
|
@@ -804,7 +805,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
804
805
|
}
|
|
805
806
|
commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
|
|
806
807
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
807
|
-
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
808
|
+
await deps.checkpointStore.putCtx(prepared.spec.sessionId, buildCheckpointCtx({ body: prepared.body, memoryScope: prepared.auth?.memoryScope }));
|
|
808
809
|
}
|
|
809
810
|
const syncCancelCtrl = createRunAbort();
|
|
810
811
|
if (prepared.spec.sessionId)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { loadConfig, type ServiceConfig } from "./config.js";
|
|
2
|
-
export { composeExecutionLane, assertExecutionLaneInvariants } from "./boot/execution-lane.js";
|
|
2
|
+
export { composeExecutionLane, assertExecutionLaneInvariants, type ExecutionLaneComposeCtx } from "./boot/execution-lane.js";
|
|
3
3
|
export { createBrain } from "./brain.js";
|
|
4
4
|
export { createSessionStore } from "./plugins/session-store.js";
|
|
5
5
|
export { createStoreBackend, type StoreBackend, type SessionCaptureRecordSource } from "./plugins/store-backend.js";
|
package/dist/leader/wire.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
|
|
8
8
|
import { withUnattendedLeg } from "../unattended-permission-mode.js";
|
|
9
|
+
import { buildCheckpointCtx } from "../http/checkpoint-ctx.js";
|
|
9
10
|
import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
|
|
10
11
|
import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
|
|
11
12
|
import { createDurableAskPolicy } from "../approval.js";
|
|
@@ -267,7 +268,7 @@ export function createLeaderRunner(cfg) {
|
|
|
267
268
|
const provisionWorker = async (sub) => {
|
|
268
269
|
const sessionId = `leader-${sub.workerId}-${Date.now()}`;
|
|
269
270
|
const durable = cfg.durable?.();
|
|
270
|
-
const resumeCtx = { body: { objective: sub.spec.objective }, unattendedLeg: true };
|
|
271
|
+
const resumeCtx = buildCheckpointCtx({ body: { objective: sub.spec.objective }, unattendedLeg: true });
|
|
271
272
|
let durableSpec = {};
|
|
272
273
|
if (durable) {
|
|
273
274
|
const d = durable;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 链(第一个非空者胜,空串视作缺席):`LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `<home>/.ai-agent`。
|
|
5
5
|
*
|
|
6
6
|
* 为什么是一只**纯函数、env 与 home 都由调用方递入**:本函数有两个读者 —— 服务自己的 `loadConfig`(递 `process.env` / `os.homedir()`)与随包发的运维脚本
|
|
7
|
-
* `scripts/
|
|
7
|
+
* `scripts/drain-check-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
|
|
8
8
|
* 设了 `AGENT_DATA_DIR` 的部署不带参数跑脚本会读到另一个目录并报「干净」)。纯叶、零反向依赖 ⇒ 脚本引它不会把整个服务装载起来。
|
|
9
9
|
*
|
|
10
10
|
* ⚠️ 与**配置根**(`configLocalRoot`:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,不吃 `LOCAL_DATA_ROOT`)不是同一条链 —— 两根分家的那一形由
|
|
@@ -19,15 +19,8 @@
|
|
|
19
19
|
* 单向钉挡不住第二种(手滑把 `"local"` 打成 `"locale"` 只会让一个合法声明被静默拒),所以两条都要。
|
|
20
20
|
*/
|
|
21
21
|
import type { ToolContentOrigin } from "@sema-agent/core";
|
|
22
|
-
/** 编译期断言:`T` 必须收敛到 `never`(有残余 ⇒ tsc 红)。 */
|
|
23
|
-
type AssertNever<T extends never> = T;
|
|
24
22
|
/** core `ToolContentOrigin` 的执法副本 —— **唯一**一份(见模块头注)。 */
|
|
25
23
|
export declare const CONTENT_ORIGIN_WORDS: readonly ["local", "execution", "external"];
|
|
26
|
-
type ContentOriginWord = (typeof CONTENT_ORIGIN_WORDS)[number];
|
|
27
|
-
type _ContentOriginCovers = AssertNever<Exclude<ToolContentOrigin, ContentOriginWord>>;
|
|
28
|
-
type _ContentOriginNoStray = AssertNever<Exclude<ContentOriginWord, ToolContentOrigin>>;
|
|
29
|
-
/** (占位使用,防 noUnusedLocals;类型别名零运行时。) */
|
|
30
|
-
export type ContentOriginWordTableGuards = [_ContentOriginCovers, _ContentOriginNoStray];
|
|
31
24
|
/**
|
|
32
25
|
* 词表内?—— 两个平面共用的**那一只**谓词。
|
|
33
26
|
*
|
|
@@ -38,5 +31,4 @@ export type ContentOriginWordTableGuards = [_ContentOriginCovers, _ContentOrigin
|
|
|
38
31
|
export declare function isContentOriginWord(v: unknown): v is ToolContentOrigin;
|
|
39
32
|
/** 坏值的**可打印**摘要(进 400 文案 / warn 元数据)——中和 + 截长,绝不把任意长度的调用方输入原样回显。 */
|
|
40
33
|
export declare function describeBadContentOrigin(v: unknown): string;
|
|
41
|
-
export {};
|
|
42
34
|
//# sourceMappingURL=mcp-content-origin.d.ts.map
|
|
@@ -90,13 +90,21 @@ export interface ModelRefusalVerdict {
|
|
|
90
90
|
* 名 → 码,**值只码**(拒因事实里的上游 id / provider 词 / 坏字段键 / 出路句都不上 wire —— `entry_invalid` 行是没人验过的行,id / provider
|
|
91
91
|
* 可能就是坏行原样;句子是给本机运维的散文,不是契约)。**名筛在这里、只在这里**:键过 settings-schema `isReportableRefusalName`(唯一判据,
|
|
92
92
|
* import 不复制;6.3.0 起 = 标识符白名单 ∧ 无密钥形 ∧ 无 URL userinfo)—— 秘密形 / 空串 / `__proto__` / 不是标识符的串这类出不了门的名不进键、
|
|
93
|
-
*
|
|
93
|
+
* 不计数;引擎内部的判决不受影响;本机日志 / 拒因句里的写名同样先问这一只(S-805,{@link disclosableRefusalName}:不可披露的名只写码)。
|
|
94
94
|
* 逐条空且无组拒 ⇒ `undefined`(键缺席:「没有」只有一种拼法)。
|
|
95
95
|
*/
|
|
96
96
|
export declare function buildModelRefusalsSignal(v: ModelRefusalVerdict | undefined): ModelRefusalsSignal | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* 一个被拒条目名的**日志字段**:可披露 ⇒ `{ [key]: name }`;不可披露 ⇒ `{ [`${key}NameWithheld`]: true }`(缺 `key` 不是无声的:读日志的人知道是被扣下、
|
|
99
|
+
* 不是漏写)。**所有**把被拒条目名写进结构化日志字段的站点调这一只(`refusalWarnFields` / `apply-effective.ts` 的角色与档位绑定 warn、缺省悬空 warn),
|
|
100
|
+
* 不各写一份 `isReportableRefusalName` 判断。
|
|
101
|
+
*/
|
|
102
|
+
export declare function refusedNameLogField(key: string, name: string): Record<string, unknown>;
|
|
103
|
+
/** 一串被拒条目名的日志字段(`sema_registry_models` 行的 `unavailable`):只列可披露的名,被扣下的名**只数个数**(`<key>NamesWithheld`,0 个时键缺席)。 */
|
|
104
|
+
export declare function refusedNamesLogFields(key: string, names: readonly string[]): Record<string, unknown>;
|
|
97
105
|
/** 成因 → 它那一条 warn 的事件名(**穷举**)。两个名字是票面点名的既有/新事件,运维的告警规则按名挂。 */
|
|
98
106
|
export declare function refusalEventOf(reason: ModelEntryRefusalReason): "sema_registry_model_no_brain" | "sema_registry_model_entry_invalid";
|
|
99
|
-
/** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。 */
|
|
107
|
+
/** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。S-805:`model` 只在名可披露时写。 */
|
|
100
108
|
export declare function refusalWarnFields(name: string, r: ModelEntryRefusal): Record<string, unknown>;
|
|
101
109
|
/**
|
|
102
110
|
* 一次**显式选择**命中了不可用条目时的拒因句(HTTP `request.model_unavailable` 的 `error`、run-local 的 stderr、
|
|
@@ -94,6 +94,22 @@ export function buildModelRefusalsSignal(v) {
|
|
|
94
94
|
return undefined;
|
|
95
95
|
return v.plane === undefined ? { entries } : { entries, plane: v.plane };
|
|
96
96
|
}
|
|
97
|
+
function disclosableRefusalName(name) {
|
|
98
|
+
return isReportableRefusalName(name) ? name : undefined;
|
|
99
|
+
}
|
|
100
|
+
export function refusedNameLogField(key, name) {
|
|
101
|
+
const shown = disclosableRefusalName(name);
|
|
102
|
+
return shown !== undefined ? { [key]: shown } : { [`${key}NameWithheld`]: true };
|
|
103
|
+
}
|
|
104
|
+
export function refusedNamesLogFields(key, names) {
|
|
105
|
+
const shown = names.filter((n) => disclosableRefusalName(n) !== undefined);
|
|
106
|
+
const withheld = names.length - shown.length;
|
|
107
|
+
return { [key]: shown, ...(withheld > 0 ? { [`${key}NamesWithheld`]: withheld } : {}) };
|
|
108
|
+
}
|
|
109
|
+
function nameSlot(name) {
|
|
110
|
+
const shown = disclosableRefusalName(name);
|
|
111
|
+
return shown === undefined ? "(name withheld)" : `"${shown}"`;
|
|
112
|
+
}
|
|
97
113
|
export function refusalEventOf(reason) {
|
|
98
114
|
switch (reason) {
|
|
99
115
|
case "no_brain":
|
|
@@ -104,7 +120,7 @@ export function refusalEventOf(reason) {
|
|
|
104
120
|
}
|
|
105
121
|
export function refusalWarnFields(name, r) {
|
|
106
122
|
return {
|
|
107
|
-
model
|
|
123
|
+
...refusedNameLogField("model", name),
|
|
108
124
|
provider: r.provider,
|
|
109
125
|
code: r.code,
|
|
110
126
|
...(r.fields.length > 0 ? { field: r.fields[0], fields: [...r.fields] } : {}),
|
|
@@ -118,7 +134,7 @@ export function modelUnavailableMessage(field, requested, name, r) {
|
|
|
118
134
|
export function defaultModelRefusal(view, leg) {
|
|
119
135
|
if (Object.hasOwn(view.models, DEFAULT_MODEL_REF) && view.models[DEFAULT_MODEL_REF] !== undefined)
|
|
120
136
|
return undefined;
|
|
121
|
-
const refused = Object.entries(view.modelRefusals).map(([n, r]) =>
|
|
137
|
+
const refused = Object.entries(view.modelRefusals).map(([n, r]) => `${nameSlot(n)} [${r.code}]`);
|
|
122
138
|
const unavailable = refused.length > 0 ? ` (every enabled catalog entry is unavailable on this host: ${refused.join(", ")})` : "";
|
|
123
139
|
switch (leg) {
|
|
124
140
|
case "env":
|
|
@@ -163,7 +179,7 @@ export function refusedNameMintedRefusal(view) {
|
|
|
163
179
|
const minted = mintedRefusedNames(view);
|
|
164
180
|
if (minted.length === 0)
|
|
165
181
|
return undefined;
|
|
166
|
-
const pairs = minted.map((m) =>
|
|
182
|
+
const pairs = minted.map((m) => `${nameSlot(m.name)} [${m.code}] → ${nameSlot(m.target)} (${m.via})`);
|
|
167
183
|
return (`unavailable catalog entries would be re-minted onto other entries by the tier/alias expansion [${REFUSED_NAME_MINTED_CODE}]: ${pairs.join(", ")} — ` +
|
|
168
184
|
"an explicit pick of that name (or a role bound to it) would silently run on the other entry's endpoint and account; " +
|
|
169
185
|
"rename the unavailable entry, fix it (see its own warn), or rebind / drop the tier binding that mints the name.");
|
|
@@ -43,9 +43,9 @@ export declare function createCorruptReadFailOpenSeat(tag: FailOpenTag, leg?: (i
|
|
|
43
43
|
*
|
|
44
44
|
* ## 逐命中响亮不降级
|
|
45
45
|
* 这一座里有安全轴的脸(session-policy),所以每命中一行 warn 保留(降成「每进程一次」是降级,S-990 的裁定);**在此之上**,认出的脸另记账:
|
|
46
|
-
* · session-policy 脸 ⇒ `P-DEBT` tag(`server.session-policy.corrupt-read-fail-open`)—— 计数逐次 + 探针行 + 一次性 warn
|
|
47
|
-
* (
|
|
48
|
-
*
|
|
46
|
+
* · session-policy 脸 ⇒ `P-DEBT` tag(`server.session-policy.corrupt-read-fail-open`)—— 计数逐次 + 探针行 + 一次性 warn。core 7.39.2 起按键读拒,
|
|
47
|
+
* 但枚举 / 别的会话抹除 / 数据根重绑三张脸仍按「无规则」(会话规则只许收紧 ⇒ 丢的恰是收紧);座上分不出读法 ⇒ 这一脸每一发都计(含已拒的按键读)。
|
|
48
|
+
* 别的会话抹除的常见形 core **不披露** ⇒ 这一形座上数不到,只能在 CENSUS / CHANGELOG 披露。放行是 core 的裁定、本仓不包层(源头修复纪律),所以**只登债、不改放行**;
|
|
49
49
|
* · 其余脸 ⇒ 不计(见 {@link DurableAbsenceFace})。
|
|
50
50
|
* 记账**先于** warn:记账是总函数,warn 可能抛(core 的 guard 会吞掉那个抛),一次日志故障不许吃掉计数。
|
|
51
51
|
*
|
|
@@ -85,10 +85,11 @@ export function createDurableAbsenceSeat(warn, root, thirdFace) {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
function buildDurableAbsenceNote(thirdFace) {
|
|
88
|
-
return ("a durable read
|
|
89
|
-
"Consequence depends on which store read it: session-policy =
|
|
90
|
-
"
|
|
91
|
-
|
|
88
|
+
return ("a durable read hit unreadable bytes (never a plain ENOENT). " +
|
|
89
|
+
"Consequence depends on which store read it: session-policy = a keyed read (a task's session rules, GET/PUT .../policy) was REFUSED " +
|
|
90
|
+
"(the run fails with code \"corrupt\"), but enumeration (session sync export), another session's erase sweep and a data-root rebind still " +
|
|
91
|
+
"read that row as NO RULES — fix or delete the named file (deleting it means no session rules); session repo = a listing silently " +
|
|
92
|
+
`omitted rows; ${thirdFace}. Inspect the named path before deciding.`);
|
|
92
93
|
}
|
|
93
94
|
const RULE_LANE_REMEDY = "the permission-rule lane is fail-closed until a person inspects the file (moving it aside restarts the lane with an empty log)";
|
|
94
95
|
export const JSONL_REPLAY_SEATS = {
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
2
|
import type { Metrics } from "./metrics.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly note: string;
|
|
9
|
-
}
|
|
10
|
-
/** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
|
|
3
|
+
/**
|
|
4
|
+
* 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。
|
|
5
|
+
* 每项 `{ cls, note }`:`cls` 是判据表的分类(见模块头——只有这两类允许走本封装):`F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";
|
|
6
|
+
* `P-DEBT`=保护型欠账,方向明知不对、显式记债。`note` 说清这条兜底放行了什么、最坏后果是什么(写给未来读遥测的人,不写来历)。
|
|
7
|
+
*/
|
|
11
8
|
export declare const FAIL_OPEN_TAGS: {
|
|
12
9
|
readonly "server.approval-ask-journal.compaction-failed": {
|
|
13
10
|
readonly cls: "F";
|
|
@@ -209,6 +206,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
209
206
|
readonly cls: "F";
|
|
210
207
|
readonly note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。";
|
|
211
208
|
};
|
|
209
|
+
readonly "server.hooks.matcher-unjudged": {
|
|
210
|
+
readonly cls: "F";
|
|
211
|
+
readonly note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 空 / `*` → 字面读法(两档都先走、不占预算)→ 正则形。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。";
|
|
212
|
+
};
|
|
212
213
|
readonly "server.hooks.notice-sink-threw": {
|
|
213
214
|
readonly cls: "F";
|
|
214
215
|
readonly note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。";
|
|
@@ -367,7 +368,7 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
367
368
|
};
|
|
368
369
|
readonly "server.session-policy.corrupt-read-fail-open": {
|
|
369
370
|
readonly cls: "P-DEBT";
|
|
370
|
-
readonly note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)
|
|
371
|
+
readonly note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)坏了。core 7.39.2(#1299,本仓请托)起**按键读**(任务装规则、`GET` / `PUT /v1/sessions/:id/policy`)拒 —— `SessionPolicyError` 码 `corrupt`;但 core 包内 CHANGELOG 自陈另三张脸仍把坏行读成「无规则」:① `listBySession` 跳过坏行(会话同步导出读它 ⇒ 导出里没有这位 principal 的规则,目标端按无规则跑);② 别的会话的 `deleteBySession` 把撕尾行一并扫掉(常见形**不披露**),此后按键读得 ENOENT = 无规则;③ 数据根重绑把撕尾行留在旧键下。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**;SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛(CENSUS 第 81 行 P 终态)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层,只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留。⚠️ **如实全记**:座上分不出是哪一只读法披露的(载荷不带读法),所以这一脸的每一发都计 —— 含已被拒的按键读(那一发本身是 fail-closed);② 的常见形 core 不披露,座上**数不到**。**出路 = core 把三张脸也改成拒,届时撤本 tag**。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。";
|
|
371
372
|
};
|
|
372
373
|
readonly "server.outbound.connect-observer-threw": {
|
|
373
374
|
readonly cls: "F";
|
|
@@ -200,6 +200,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
200
200
|
cls: "F",
|
|
201
201
|
note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。",
|
|
202
202
|
},
|
|
203
|
+
"server.hooks.matcher-unjudged": {
|
|
204
|
+
cls: "F",
|
|
205
|
+
note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 空 / `*` → 字面读法(两档都先走、不占预算)→ 正则形。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。",
|
|
206
|
+
},
|
|
203
207
|
"server.hooks.notice-sink-threw": {
|
|
204
208
|
cls: "F",
|
|
205
209
|
note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。",
|
|
@@ -337,7 +341,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
337
341
|
},
|
|
338
342
|
"server.session-policy.corrupt-read-fail-open": {
|
|
339
343
|
cls: "P-DEBT",
|
|
340
|
-
note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)
|
|
344
|
+
note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)坏了。core 7.39.2(#1299,本仓请托)起**按键读**(任务装规则、`GET` / `PUT /v1/sessions/:id/policy`)拒 —— `SessionPolicyError` 码 `corrupt`;但 core 包内 CHANGELOG 自陈另三张脸仍把坏行读成「无规则」:① `listBySession` 跳过坏行(会话同步导出读它 ⇒ 导出里没有这位 principal 的规则,目标端按无规则跑);② 别的会话的 `deleteBySession` 把撕尾行一并扫掉(常见形**不披露**),此后按键读得 ENOENT = 无规则;③ 数据根重绑把撕尾行留在旧键下。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**;SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛(CENSUS 第 81 行 P 终态)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层,只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留。⚠️ **如实全记**:座上分不出是哪一只读法披露的(载荷不带读法),所以这一脸的每一发都计 —— 含已被拒的按键读(那一发本身是 fail-closed);② 的常见形 core 不披露,座上**数不到**。**出路 = core 把三张脸也改成拒,届时撤本 tag**。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。",
|
|
341
345
|
},
|
|
342
346
|
"server.outbound.connect-observer-threw": {
|
|
343
347
|
cls: "F",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **规则表的唯一编译口** —— 一张工具名规则表(`allow` / `ask` / `deny` 之一)编成一只 `ToolPolicy`。
|
|
3
|
+
*
|
|
4
|
+
* 两类读者,同一只编译口(编译与校验各只有一份,不在别处另写第二份文法):
|
|
5
|
+
* · 请求腿的 `settings.permissions.{allow,ask,deny}` 三桶(`task-settings.ts` 的 `deriveSettingsPolicy`);
|
|
6
|
+
* · 运维的 allow 规则 `APPROVAL_GRANT`(`runtime-governance.ts` 的 `applyRuntimeGovernance`,经部署治理链的单一构造口
|
|
7
|
+
* `deployment-governance.ts` 交进来 —— HTTP 提交 / durable 续跑 / 停驻后台 agent 的跨副本赎回 / run-local 四条腿同装)。
|
|
8
|
+
*
|
|
9
|
+
* 为什么住在一只叶模块里(不留在 `task-settings.ts`):运维那一侧的启动校验在 `config.ts` 里跑(坏值 = 拒启),而
|
|
10
|
+
* `task-settings.ts` 值引 `config.ts` —— 编译口留在那里,`config.ts` 要调它就闭合一只 ESM 装载环。本模块只 import core 与
|
|
11
|
+
* `text-bidi.ts`(纯叶),三处读者引它都不成环。
|
|
12
|
+
*/
|
|
13
|
+
import { type PathRuleBases, type RuleBehavior, type ToolPolicy } from "@sema-agent/core";
|
|
14
|
+
/** 一张表里规则条数的上限 —— 请求腿 wire 门(`task-settings.ts` 的三桶判形)读的也是这一个名字:core 的缺省帽是 256,
|
|
15
|
+
* 不对齐的话第 257 条起编译当场抛,等于服务端自己收下的东西自己反悔。 */
|
|
16
|
+
export declare const MAX_PERMISSION_RULES_PER_LIST = 1000;
|
|
17
|
+
/** A name-keyed rule-list ToolPolicy: a tool named by `names` gets `behavior` (an `allow` hit states the allow with
|
|
18
|
+
* `decisionReason: "rule"`; `ask` tightens to an approval gate; `deny` hard-blocks); every other tool is out of
|
|
19
|
+
* scope (`defaultAction: "allow"` = this policy has no opinion about anything it does not name). Composed deny-wins
|
|
20
|
+
* with the rest of the fold, so `ask` / `deny` only ever tighten and an `allow` never overrides a deny or an ask.
|
|
21
|
+
*
|
|
22
|
+
* `source` = 这张表**写在哪**(请求腿 `settings.permissions.<桶>`;运维腿 `APPROVAL_GRANT`)。它盖进每条规则(core 的 deny / ask
|
|
23
|
+
* 句里点名它:`flagged by permission rule "mcp__figma__*" (settings.permissions.ask)`),编不出来时的错误句也点名它;只是出处,
|
|
24
|
+
* 不带任何优先级(层序在各自的折叠里)。
|
|
25
|
+
*
|
|
26
|
+
* 🔴 [ref] / core [ref] —— 本函数曾是 `new Set(askNames).has(req.toolName)` 的**逐字**比对,那是
|
|
27
|
+
* **安全轴上的静默 fail-open**:CC 自家 settings validator 推荐的 covering 拼法
|
|
28
|
+
* (`"MCP rules do not support patterns in parentheses … use mcp__srv__*"`)在逐字比对下永远配不上真工具名
|
|
29
|
+
* `mcp__figma__get_file` ⇒ 一次不问、直跑、审计静音。两道兜底也各自放行:`unmatchableToolNamesRefusal`
|
|
30
|
+
* 对 `mcp__<peer>__*` 判「合法 MCP 全名」(server 不持挂载表,动态宇宙不能按名单否定),`nameSets` 审计面
|
|
31
|
+
* 报的是「没匹配上的名字」而 covering 名在 core 眼里**是**匹配得上的。同一份 settings 里
|
|
32
|
+
* `deny:["mcp__figma__*"]` 却**真拦** —— deny 走 `createAllowDenyPolicy`,它用的是 core 的
|
|
33
|
+
* `namespacedCoveringEntries/Hit`。同族分家,差的只是解析器。
|
|
34
|
+
*
|
|
35
|
+
* ⇒ **规则表的 policy 面现在同源**:三种态度都由 `createPermissionRulePolicy` 的 covering 车道解,底下是
|
|
36
|
+
* core 的**同一只** `namespacedRuleNameCovers`(协议无关:`mcp__`/`a2a__` 同解;peer 段精确比;工具段 `*`/glob 覆盖)。
|
|
37
|
+
* 本仓**不自写第二套 glob** —— 那正是当年分家的成因。
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ 「同源」的**射程**说准了免得下一个人当成全仓结论(codex 交叉轮 finding-1 追出来的边):
|
|
40
|
+
* · 同源的是**本编译口**认的 covering。表**进门前**还有一道更外层的判据(`config.ts` 的 `unmatchableToolNamesRefusal`,
|
|
41
|
+
* 请求腿经 `task-settings.ts` 的 `rejectUnmatchableSettingsNames`、运维腿经 `config.ts` 的 `approvalGrantRefusal`),它把
|
|
42
|
+
* `<ns>__<peer>` **两段形**(`mcp__figma`)判成「缺段名」拒掉 —— 而 core 三只 policy 都认这个形(实测:
|
|
43
|
+
* `createAllowDenyPolicy`/`createPermissionRulePolicy`/`createApprovalPolicy` 对 `mcp__figma` 一律解成 covering)。
|
|
44
|
+
* 所以各表收 `mcp__figma__*`(等价、且被推荐)、拒 `mcp__figma`。
|
|
45
|
+
* · **同源的射程已扩到 operator 名单腿**([ref] 孪生修,[ref]):`createDurableAskPolicy`(`src/approval.ts`)的
|
|
46
|
+
* deny/neverAuto/require 三集合过去是逐字 `Set.has`,自 core 5.60.1 谓词根导出后改成「精确 ∪ covering」,用的是
|
|
47
|
+
* **同一只** `namespacedRuleNameCovers`。⇒ env `APPROVAL_*` 腿 / 中心下发 `runtime.approvalRequire` 腿 / 规则表三家
|
|
48
|
+
* 现在对 covering 拼法的解读逐字一致,`mcp__figma__*` 在哪条腿上都是真判。
|
|
49
|
+
* · **两段形为什么仍不放开**(理由已换,别再引「approval.ts 还不认」):它一名两义——「整台服务器」与「三段名写漏一段」
|
|
50
|
+
* 在 boot 期不可分辨,所以各条腿一律响亮拒 + 指引给无歧义的 `mcp__figma__*`。放宽是各腿同时变宽的 behavior-facing
|
|
51
|
+
* 改动,走独立批的三问。详见 `unmatchableToolNamesRefusal` 顶注。
|
|
52
|
+
*
|
|
53
|
+
* 三处刻意的取舍,都是亲读 core dist 后定的:
|
|
54
|
+
* · `onInvalidRule:"throw"`(缺省)—— 一条编不出来的规则整表拒(请求腿 → resolve-spec 翻 422;运维腿 → 拒启),抛之前补上
|
|
55
|
+
* 出处名(core 的原文只说 "createPermissionRulePolicy: …",不说是哪张表出的错)。[ref] 的括号形在**更外层**已被拒,够不着
|
|
56
|
+
* 这里;真正新增的是「名字里带反斜杠」「单条 > 512 字符」这两族永不匹配的拼法。
|
|
57
|
+
* ✅ **不对称已收口([ref]①,[ref] 销)**:codex finding-2 当年如实登记的「同样两族拼法在 `deny`
|
|
58
|
+
* 上仍是静默 no-op」已修 —— 判据搬进单点成文的 `unmatchableToolNamesRefusal`,它拿 core 自己的 `validatePermissionRules`
|
|
59
|
+
* 对同一串跑一遍文法(不自铸第二套判词),于是三张表对这两族说同一句话。S-654 (c) 起 `allow` 也收。
|
|
60
|
+
* ⚠️ 存量 resume:带这两族拼法的**已存 body** 在 resume 腿会 422(resolve-spec 翻)。这**不是**新造的姿态——A3
|
|
61
|
+
* (`rejectUnmatchableSettingsNames`)早就对存量 body 里的退役名/短名同样抛,且 `applyTaskSettings` 腿有专门的回归钉。
|
|
62
|
+
* 判据同一条:一个**静默不存在的审批门**比一次响亮失败更贵。codex 建议的「给非法存量条目保留旧的逐字比对」被**驳回**:
|
|
63
|
+
* 那正是把 fail-open 请回来。
|
|
64
|
+
* · `caps.maxRules` 对齐 {@link MAX_PERMISSION_RULES_PER_LIST}(理由见那只常量)。core 的 set-level cap 连 `skip` 都照抛,
|
|
65
|
+
* 所以运维腿的启动校验还要**试编一次**(`approvalGrantRefusal`)才拦得住超帽 —— 判据仍是本函数这一只。
|
|
66
|
+
* · 不再自铸 `decisionReason:"rule"` —— core 的规则策略本就不铸这个键(allow 命中由 core 自己铸 `"rule"`),且 core 侧唯一区分它的
|
|
67
|
+
* 两处(prepare-task 的继承 grant 复用判据、park 的 rule-suggestion 门)对 `"rule"` 与 `undefined` **同待**(只把 `"hook"`
|
|
68
|
+
* 排除在外),dist 亲读在案。审计归因改由 `matchedAskRule` + `source` 承担,后者比前者强:它还让 core 的 auto-mode 分类器
|
|
69
|
+
* **拒绝**替人清掉这条 ask(「运维写下的『每次问我』不是分类器的犹豫」),方向是收紧。
|
|
70
|
+
*
|
|
71
|
+
* 🔴 **S-654 (c)(7.115.0,core 7.40.0 #1065):三种态度同一个编译口,`allow` 也走这里** —— `behavior: "allow"` 的规则命中 = core 带
|
|
72
|
+
* `decisionReason: "rule"` 的放行 —— allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢(与参照实现的 `permissions.allow` 在内置工具上**不同**:那边 `allow: ["Edit"]` 跳过编辑确认,这里不放掉引擎的写门);未命中 = `defaultAction: "allow"` 的裸 allow
|
|
73
|
+
* = 不表态、照常判。修前请求腿的 allow 编成 `createAllowDenyPolicy({ allow })` 的**名单**(名单在场 ⇒ 名单外的工具一律拒),与写它的人要的
|
|
74
|
+
* 「替这些工具表态」方向相反;终端因此从不投这只桶。core 7.40.0 起没人表态的服务器工具调用要问,allow 规则就是那张卡的常设答案
|
|
75
|
+
* (`mcp__<server>__*` 一条覆盖整台服务器)。allow 规则**不越过** deny / ask:折叠 deny > ask > allow,任何一层的 deny / ask 照赢;
|
|
76
|
+
* 它只在没人反对时给出那句表态。core 的 allow 文法只收工具名形(精确名 / `mcp__`·`a2a__` covering 形),内容形放行(`Bash(npm:*)` /
|
|
77
|
+
* `Edit(src/**)` / 参数级)core 拒编,两条腿与 deny / ask 同一个出口(判据 `unmatchableToolNamesRefusal`)。
|
|
78
|
+
* 7.115.0 起运维的 `APPROVAL_GRANT` 也编在这里(无人值守腿没有请求体,那是运维为自己接的服务器表态的唯一位置)。 */
|
|
79
|
+
/** `behavior` = 这张表的态度 —— 型直接是 core 的 `RuleBehavior`(不手抄:core 加一种态度,这里与它同步、读者 tsc 上见到)。 */
|
|
80
|
+
export declare function createRuleListPolicy(names: readonly string[], behavior: RuleBehavior, source: string, pathBases: PathRuleBases | undefined): ToolPolicy;
|
|
81
|
+
//# sourceMappingURL=permission-rule-list.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createPermissionRulePolicy } from "@sema-agent/core";
|
|
2
|
+
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
3
|
+
export const MAX_PERMISSION_RULES_PER_LIST = 1_000;
|
|
4
|
+
export function createRuleListPolicy(names, behavior, source, pathBases) {
|
|
5
|
+
try {
|
|
6
|
+
return createPermissionRulePolicy(names.map((name) => ({ rule: name, behavior, source })), {
|
|
7
|
+
defaultAction: "allow",
|
|
8
|
+
caps: { maxRules: MAX_PERMISSION_RULES_PER_LIST },
|
|
9
|
+
...(pathBases !== undefined ? { pathBases } : {}),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
const coreMsg = (e instanceof Error ? e.message : String(e)).replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
14
|
+
throw new Error(`${source.replace("settings.", "")} has rule(s) core cannot compile — ${coreMsg}`, { cause: e });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=permission-rule-list.js.map
|
|
@@ -419,39 +419,4 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
419
419
|
*/
|
|
420
420
|
export declare function hostExecutionEnvFactory(config?: HostEnvConfig): ExecutionEnvFactory;
|
|
421
421
|
export { localDockerExecutionEnvFactory, type LocalDockerEnvConfig, type LocalDockerEnvDeps, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
422
|
-
import { type LocalDockerEnvConfig, type LocalDockerEnvDeps } from "./remote-env-local-docker.js";
|
|
423
|
-
/**
|
|
424
|
-
* Config for the `remote-docker` provider: run each task in a container on a REMOTE docker daemon
|
|
425
|
-
* (DUAL-MODE-DESIGN §5 — "a user-specified REMOTE host's docker"; registry-core `RemoteExecSpec`).
|
|
426
|
-
*
|
|
427
|
-
* It is {@link LocalDockerEnvConfig} with the endpoint made REQUIRED — that is the whole difference, by design:
|
|
428
|
-
* the container adapter is already daemon-agnostic (workspace `mkdir`'d INSIDE the container, never a host
|
|
429
|
-
* bind-mount; bytes over `docker cp` / `docker exec cat`, which stream through the daemon API), and the endpoint
|
|
430
|
-
* rides as `-H <dockerHost>` on every invocation. So every local-docker knob (memory/cpus/pidsLimit/dropAllCaps/
|
|
431
|
-
* network/timeouts/env/dockerPath) applies unchanged to the remote lane.
|
|
432
|
-
*/
|
|
433
|
-
export interface RemoteDockerEnvConfig extends Omit<LocalDockerEnvConfig, "dockerHost" | "providerLabel"> {
|
|
434
|
-
/** Remote docker daemon endpoint (e.g. `tcp://host:2376` or `ssh://user@host`). REQUIRED for remote-docker. */
|
|
435
|
-
dockerHost: string;
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* `ExecutionEnvFactory` for the TOC `remote-docker` backend (DUAL-MODE-DESIGN §5): a per-task container on
|
|
439
|
-
* SOMEONE ELSE'S docker daemon (offload — the operator's box stays free; isolation:true, suspendable:false).
|
|
440
|
-
* Same lifetime contract as local-docker: unconnected/lazy, one container per task, `destroy()` = `docker rm -f`.
|
|
441
|
-
*
|
|
442
|
-
* 🔴 **Endpoint credentials are the docker CLI's own** — this adapter deliberately carries none. The docker CLI
|
|
443
|
-
* reads `DOCKER_CERT_PATH` + `DOCKER_TLS_VERIFY` from the worker's environment (inherited by every spawn) for a
|
|
444
|
-
* `tcp://` + TLS daemon, and uses `ssh-agent` / `~/.ssh/config` for an `ssh://` one. That keeps key material out
|
|
445
|
-
* of this process's config objects entirely. Consequence to know: `tcp://…:2376` WITHOUT those env vars fails
|
|
446
|
-
* loud at connect (the daemon refuses plaintext) — it does not silently downgrade; a plaintext `tcp://…:2375`
|
|
447
|
-
* daemon is unauthenticated and remains the operator's decision, exactly as in the registry-core schema.
|
|
448
|
-
* (The registry-core `remote-docker` arm additionally models `tlsCertEnv`/`tlsKeyEnv`/`tlsCaEnv`/`sshKeyEnv` =
|
|
449
|
-
* env-NAMEs the control plane resolves to PEM BYTES. Materializing those bytes to short-lived 0600 files and
|
|
450
|
-
* passing `--tlsverify --tlscert/--tlskey/--tlscacert` is the remaining config-surface follow-up, together with
|
|
451
|
-
* the `provider:"remote-docker"` arm on `AppConfig.remoteExec` — see the note on that union in config-types.ts.)
|
|
452
|
-
*
|
|
453
|
-
* Throws at factory-BUILD time on a missing/blank endpoint: a "remote" lane with no endpoint would otherwise
|
|
454
|
-
* quietly become the LOCAL daemon — an isolation-topology surprise, so it fails loud instead.
|
|
455
|
-
*/
|
|
456
|
-
export declare function remoteDockerExecutionEnvFactory(config: RemoteDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
|
|
457
422
|
//# sourceMappingURL=remote-env-host.d.ts.map
|
|
@@ -1373,12 +1373,4 @@ export function hostExecutionEnvFactory(config = {}) {
|
|
|
1373
1373
|
});
|
|
1374
1374
|
}
|
|
1375
1375
|
export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
1376
|
-
import { localDockerExecutionEnvFactory } from "./remote-env-local-docker.js";
|
|
1377
|
-
export function remoteDockerExecutionEnvFactory(config, deps = {}) {
|
|
1378
|
-
const dockerHost = config.dockerHost?.trim();
|
|
1379
|
-
if (!dockerHost) {
|
|
1380
|
-
throw new RemoteExecutionError("connect_failed", "remoteExec provider 'remote-docker' requires a non-empty dockerHost (the remote daemon endpoint, e.g. tcp://host:2376 or ssh://user@host) — use provider 'local-docker' for THIS machine's daemon");
|
|
1381
|
-
}
|
|
1382
|
-
return localDockerExecutionEnvFactory({ ...config, dockerHost, providerLabel: "remote-docker" }, deps);
|
|
1383
|
-
}
|
|
1384
1376
|
//# sourceMappingURL=remote-env-host.js.map
|
|
@@ -27,19 +27,13 @@ export interface LocalDockerEnvConfig {
|
|
|
27
27
|
* DOCKER_HOST override for the docker CLI (e.g. a non-default socket). Passed as `-H <value>` on EVERY docker
|
|
28
28
|
* invocation (not the process env) so it can't leak / drift. Absent = the daemon DOCKER_HOST points at.
|
|
29
29
|
*
|
|
30
|
-
* Pointing this OFF-BOX (`tcp://host:2376`, `ssh://user@host`) is
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `mkdir`'d INSIDE the container (never a bind-mount of a
|
|
34
|
-
* `docker exec cat`, which stream over the daemon API.
|
|
30
|
+
* Pointing this OFF-BOX (`tcp://host:2376`, `ssh://user@host`) is how a REMOTE docker daemon is used — the
|
|
31
|
+
* lane stays `local-docker` (`REMOTE_EXEC=local-docker` + `DOCKER_HOST`; the settings-schema lane table lists
|
|
32
|
+
* `DOCKER_HOST` as this lane's optional coordinate, and there is no separate `remote-docker` lane since 7.0.0).
|
|
33
|
+
* Nothing about the class is host-local: the workspace is `mkdir`'d INSIDE the container (never a bind-mount of a
|
|
34
|
+
* host path) and bytes move by `docker cp` / `docker exec cat`, which stream over the daemon API.
|
|
35
35
|
*/
|
|
36
36
|
dockerHost?: string;
|
|
37
|
-
/**
|
|
38
|
-
* Provider NAME reported on the {@link WorkspaceHandle}. Default `"local-docker"`; the `remote-docker` arm
|
|
39
|
-
* passes `"remote-docker"` so a handle never mislabels which lane it is (the CLASS is shared — `remote-docker`
|
|
40
|
-
* IS this adapter aimed at a remote daemon). Cosmetic/observability only: it selects no behavior.
|
|
41
|
-
*/
|
|
42
|
-
providerLabel?: string;
|
|
43
37
|
/** Container memory limit (docker `--memory`, e.g. "2g"). Absent = no limit. */
|
|
44
38
|
memory?: string;
|
|
45
39
|
/** Container CPU limit (docker `--cpus`, e.g. 1.5). Absent = no limit. */
|
|
@@ -88,8 +82,6 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
88
82
|
private readonly spawnFn;
|
|
89
83
|
/** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
|
|
90
84
|
private readonly containerName;
|
|
91
|
-
/** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */
|
|
92
|
-
private readonly providerName;
|
|
93
85
|
/** Set once the container is up + the workspace dir created. */
|
|
94
86
|
private containerId?;
|
|
95
87
|
private handle?;
|
|
@@ -24,7 +24,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
24
24
|
cfg;
|
|
25
25
|
spawnFn;
|
|
26
26
|
containerName;
|
|
27
|
-
providerName;
|
|
28
27
|
containerId;
|
|
29
28
|
handle;
|
|
30
29
|
destroyed = false;
|
|
@@ -47,7 +46,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
47
46
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
48
47
|
const suffix = randomBytes(4).toString("hex");
|
|
49
48
|
this.containerName = `sema-docker-${sanitizeId(id)}-${suffix}`;
|
|
50
|
-
this.providerName = config.providerLabel?.trim() || PROVIDER;
|
|
51
49
|
}
|
|
52
50
|
resolve(p) {
|
|
53
51
|
return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
|
|
@@ -113,7 +111,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
113
111
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
114
112
|
}
|
|
115
113
|
this.containerId = run.value.stdout.trim() || this.containerName;
|
|
116
|
-
this.handle = { sandboxId: this.containerName, provider:
|
|
114
|
+
this.handle = { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
117
115
|
if (config?.secrets?.length) {
|
|
118
116
|
}
|
|
119
117
|
return ok(this.handle);
|
|
@@ -136,7 +134,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
136
134
|
const insp = await this.docker(["inspect", "-f", "{{.State.Running}}", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs });
|
|
137
135
|
if (insp.ok && insp.value.exitCode === 0 && insp.value.stdout.trim() === "true") {
|
|
138
136
|
this.containerId = this.containerName;
|
|
139
|
-
this.handle ??= { sandboxId: this.containerName, provider:
|
|
137
|
+
this.handle ??= { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
140
138
|
return ok(this.handle);
|
|
141
139
|
}
|
|
142
140
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `container '${this.containerName}' is not running (gone or stopped) — no managed snapshot to resume`) };
|
package/dist/run-local.js
CHANGED
|
@@ -12,7 +12,7 @@ import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
|
|
|
12
12
|
import { withUnattendedLeg } from "./unattended-permission-mode.js";
|
|
13
13
|
import { buildUnattendedPolicyRefusal } from "./tool-approval.js";
|
|
14
14
|
import { createBrain } from "./brain.js";
|
|
15
|
-
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
15
|
+
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, buildApprovalGrantNotice, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
16
16
|
import { applyRuntimeGovernance } from "./runtime-governance.js";
|
|
17
17
|
import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
|
|
18
18
|
import { taskWallClockSec } from "./task-workflow.js";
|
|
@@ -334,6 +334,9 @@ export async function runLocal(argv, deps = {}) {
|
|
|
334
334
|
});
|
|
335
335
|
if (ungatedCompensation)
|
|
336
336
|
logger.warn(ungatedCompensation.event, ungatedCompensation.fields);
|
|
337
|
+
const approvalGrantNotice = buildApprovalGrantNotice(config);
|
|
338
|
+
if (approvalGrantNotice)
|
|
339
|
+
logger.info(approvalGrantNotice.event, approvalGrantNotice.fields);
|
|
337
340
|
if (!deps.brain) {
|
|
338
341
|
const missing = missingGatewayRequirements(config);
|
|
339
342
|
if (missing.length > 0) {
|