@sema-agent/server 7.113.0 → 7.114.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 +37 -2
- package/USAGE.md +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -1
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/resolve-spec.js +2 -2
- package/dist/config-catalog.js +6 -5
- package/dist/config-invariants.d.ts +27 -0
- package/dist/config-invariants.js +54 -39
- package/dist/config-types.d.ts +4 -10
- package/dist/config-types.js +0 -1
- package/dist/config.d.ts +0 -16
- package/dist/config.js +42 -68
- 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/hooks/hook-runner.d.ts +85 -5
- package/dist/hooks/hook-runner.js +109 -16
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +2 -2
- package/dist/http/routes/capabilities.js +2 -2
- 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 +5 -1
- package/dist/observability/fail-open.js +5 -1
- 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/store-posture.d.ts +7 -7
- package/dist/store-posture.js +0 -1
- package/dist/task-settings.d.ts +13 -0
- package/dist/task-settings.js +3 -0
- package/package.json +3 -3
package/dist/config.js
CHANGED
|
@@ -10,7 +10,7 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
|
10
10
|
import { isV2ScopeKey, memoryReplicationCoverage, replicationCovers } from "./memory-scope.js";
|
|
11
11
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
12
12
|
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
13
|
-
import { CONFIG_PROVIDER_WORDS,
|
|
13
|
+
import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
|
|
14
14
|
import { resolveLocalDataRoot } from "./local-data-root.js";
|
|
15
15
|
import { buildWriteProtectionPosture } from "./write-protection.js";
|
|
16
16
|
import { normalizeSshFingerprint } from "./ssh-host-key.js";
|
|
@@ -18,7 +18,9 @@ import { buildReadFacePostureNotice, readFaceForPosture } from "./read-face-post
|
|
|
18
18
|
import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
|
|
19
19
|
import {} from "./posture-source.js";
|
|
20
20
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
21
|
-
import {
|
|
21
|
+
import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
|
|
22
|
+
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
|
|
23
|
+
import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
|
|
22
24
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
23
25
|
import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
|
|
24
26
|
import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
|
|
@@ -484,15 +486,6 @@ function boolEnv(name, def) {
|
|
|
484
486
|
export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
|
|
485
487
|
export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
|
|
486
488
|
export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
|
|
487
|
-
export const REQUIRED_ENVS_BY_LANE = {
|
|
488
|
-
e2b: ["E2B_API_KEY"],
|
|
489
|
-
k8s: ["K8S_SANDBOX_IMAGE"],
|
|
490
|
-
ssh: ["SSH_HOST", "SSH_USER", "SSH_PRIVATE_KEY"],
|
|
491
|
-
adb: ["ADB_SERIAL"],
|
|
492
|
-
host: [],
|
|
493
|
-
"local-docker": ["DOCKER_IMAGE"],
|
|
494
|
-
device: [],
|
|
495
|
-
};
|
|
496
489
|
function parseReadDenyPatternsEnv(raw) {
|
|
497
490
|
let arr;
|
|
498
491
|
try {
|
|
@@ -862,7 +855,7 @@ function parseStoreDomain(ctx) {
|
|
|
862
855
|
!!(process.env.MYSQL_HOST || process.env.PG_HOST);
|
|
863
856
|
const bareBoot = !dbBackendSet && !sqlSignal;
|
|
864
857
|
const multiTenantBareBoot = bareBoot && requirePrincipal;
|
|
865
|
-
const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql",
|
|
858
|
+
const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", STORE_BACKENDS);
|
|
866
859
|
if (multiTenantBareBoot) {
|
|
867
860
|
CONFIG_NOTICES.push({
|
|
868
861
|
level: "warn",
|
|
@@ -873,7 +866,7 @@ function parseStoreDomain(ctx) {
|
|
|
873
866
|
});
|
|
874
867
|
}
|
|
875
868
|
const sqlEngineExplicit = !!process.env.DB_BACKEND && dbBackend !== "local" && dbBackend !== "memory";
|
|
876
|
-
const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory",
|
|
869
|
+
const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", SESSION_BACKENDS);
|
|
877
870
|
const sessionBackend = sessionBackendRaw === "mysql" ? "tidb" : sessionBackendRaw;
|
|
878
871
|
const localDataRoot = resolveLocalDataRoot(process.env, homedir());
|
|
879
872
|
const dbHostSet = dbBackend === "pg" ? !!process.env.PG_HOST : !!process.env.MYSQL_HOST;
|
|
@@ -1645,11 +1638,7 @@ function parseMemoryConsolidationDriver() {
|
|
|
1645
1638
|
function parseMemoryDomain(ctx) {
|
|
1646
1639
|
const { requirePrincipal } = ctx;
|
|
1647
1640
|
const memoryEngineEnabled = parseMemoryEngineWord(env("MEMORY_ENGINE", "on"));
|
|
1648
|
-
const
|
|
1649
|
-
if (!["file", "pg", "tidb"].includes(memoryEngineBackendRaw)) {
|
|
1650
|
-
throw new Error(`MEMORY_ENGINE_BACKEND must be file|pg|tidb, got "${memoryEngineBackendRaw}"`);
|
|
1651
|
-
}
|
|
1652
|
-
const memoryEngineBackend = memoryEngineBackendRaw;
|
|
1641
|
+
const memoryEngineBackend = enumEnv("MEMORY_ENGINE_BACKEND", "file", MEMORY_ENGINE_BACKENDS);
|
|
1653
1642
|
const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
|
|
1654
1643
|
const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
|
|
1655
1644
|
const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
|
|
@@ -1865,12 +1854,12 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1865
1854
|
{
|
|
1866
1855
|
const lane = process.env.REMOTE_EXEC;
|
|
1867
1856
|
if (lane !== undefined && lane !== "") {
|
|
1868
|
-
const
|
|
1869
|
-
if (
|
|
1870
|
-
throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${
|
|
1857
|
+
const word = REMOTE_EXEC_PROVIDER_WORDS.find((w) => w === lane);
|
|
1858
|
+
if (word === undefined) {
|
|
1859
|
+
throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")}. ` +
|
|
1871
1860
|
`An unrecognized value used to fall through to the host/in-process lane silently; refusing to start instead (#157).`);
|
|
1872
1861
|
}
|
|
1873
|
-
const missing =
|
|
1862
|
+
const missing = laneRequiredEnv(word).filter((name) => !process.env[name]);
|
|
1874
1863
|
if (missing.length > 0) {
|
|
1875
1864
|
throw new Error(`REMOTE_EXEC=${lane} but required env ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} missing/empty — ` +
|
|
1876
1865
|
`refusing to start rather than silently downgrading to the host/in-process lane (#157; unset REMOTE_EXEC if you meant no sandbox lane).`);
|
|
@@ -2407,54 +2396,39 @@ export function loadConfig() {
|
|
|
2407
2396
|
const integrations = parseIntegrationsDomain({ requirePrincipal, configProvider });
|
|
2408
2397
|
const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
|
|
2409
2398
|
const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
|
|
2410
|
-
const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
|
|
2411
2399
|
const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2400
|
+
const doorRefusal = halfOpenDirectDoorRefusal({
|
|
2401
|
+
directApprovalDoor,
|
|
2402
|
+
durableApproval: dgDurable,
|
|
2403
|
+
approvalHmacKeys: dgHmacKeys,
|
|
2404
|
+
principalJwtPubkeys,
|
|
2405
|
+
principalJwtIss,
|
|
2406
|
+
principalJwtAud,
|
|
2407
|
+
requirePrincipal,
|
|
2408
|
+
operatorPrincipals: dgOperators,
|
|
2409
|
+
});
|
|
2410
|
+
if (doorRefusal !== undefined) {
|
|
2411
|
+
throw new Error(doorRefusal);
|
|
2412
|
+
}
|
|
2413
|
+
const operatorsRefusal = operatorPrincipalsRequiredRefusal({ durableApproval: dgDurable, requirePrincipal, operatorPrincipals: dgOperators });
|
|
2414
|
+
if (operatorsRefusal !== undefined) {
|
|
2415
|
+
throw new Error(operatorsRefusal);
|
|
2416
|
+
}
|
|
2417
|
+
const bakeOperatorsRefusal = bakeOperatorPrincipalsRefusal({ imageBakes: orchestration.imageBakes, operatorPrincipals: dgOperators });
|
|
2418
|
+
if (bakeOperatorsRefusal !== undefined) {
|
|
2419
|
+
throw new Error(bakeOperatorsRefusal);
|
|
2420
|
+
}
|
|
2421
|
+
const bakeRunnerRefusal = bakeRunnerCredentialRefusal({ imageBakes: orchestration.imageBakes, authTokens });
|
|
2422
|
+
if (bakeRunnerRefusal !== undefined) {
|
|
2423
|
+
throw new Error(bakeRunnerRefusal);
|
|
2424
|
+
}
|
|
2425
|
+
const leaderStoreRefusal = leaderDurableStoreRefusal({ leaderEnabled: orchestration.leaderEnabled, dbBackend: store.dbBackend });
|
|
2426
|
+
if (leaderStoreRefusal !== undefined) {
|
|
2427
|
+
throw new Error(leaderStoreRefusal);
|
|
2431
2428
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
if (imageBakesEnabled && dgOperators.length === 0) {
|
|
2436
|
-
throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
|
|
2437
|
-
}
|
|
2438
|
-
if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
|
|
2439
|
-
throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
|
|
2440
|
-
}
|
|
2441
|
-
if (orchestration.leaderEnabled && store.dbBackend !== "mysql" && store.dbBackend !== "pg") {
|
|
2442
|
-
throw new Error(`LEADER_ENABLED=true but DB_BACKEND=${store.dbBackend} (no external SQL store) is refused: a leader run is an ` +
|
|
2443
|
-
"N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
|
|
2444
|
-
"a per-process Map — a cross-replica GET 404s, a restart forgets the run, and a retried POST starts a SECOND " +
|
|
2445
|
-
"push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
|
|
2446
|
-
"(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)");
|
|
2447
|
-
}
|
|
2448
|
-
if (orchestration.requireExecutionLane && orchestration.remoteExec === undefined) {
|
|
2449
|
-
throw new Error(`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
|
|
2450
|
-
`With no lane the engine falls back to its in-process stub environment, where at least the Bash tool is ` +
|
|
2451
|
-
`not mounted, yet every task still returns status=completed — indistinguishable from "the model chose ` +
|
|
2452
|
-
`not to use its tools". (What the roster actually keeps is a per-run fact: read the wiring_manifest ` +
|
|
2453
|
-
`event and count entries whose mountedBy is "hands".) ` +
|
|
2454
|
-
`Cause: REMOTE_EXEC is unset/empty AND CONFIG_PROVIDER is not "local". ` +
|
|
2455
|
-
`Fix: set REMOTE_EXEC to one of ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")} (and that lane's required env), ` +
|
|
2456
|
-
`or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
|
|
2457
|
-
`or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
|
|
2429
|
+
const laneRequiredRefusal = executionLaneRequiredRefusal(orchestration);
|
|
2430
|
+
if (laneRequiredRefusal !== undefined) {
|
|
2431
|
+
throw new Error(laneRequiredRefusal);
|
|
2458
2432
|
}
|
|
2459
2433
|
{
|
|
2460
2434
|
const envLaneDispatchable = !isRosterGateArmed(integrations.configCenter);
|
package/dist/env-facts.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageCapabilities } from "./plugins/store-contracts.js";
|
|
2
|
+
import type { RemoteExecProvider } from "./execution-lane-caps.js";
|
|
2
3
|
export interface SandboxEnvFacts {
|
|
3
4
|
profile?: string;
|
|
4
5
|
capabilities?: string[];
|
|
@@ -53,7 +54,8 @@ export declare function buildEnvFacts(opts: {
|
|
|
53
54
|
* codex 复审 medium:k8s 必须按**有效快照能力**分叉,不能只看车道名——s3Snapshot 未配=park-only,
|
|
54
55
|
* resume 是全新 pod 且**工作区不恢复**,「文件已从快照恢复」那句会让模型信赖不存在的文件(比缺席
|
|
55
56
|
* 更糟的谎)。 */
|
|
56
|
-
export declare function resumeFactsForLane(provider:
|
|
57
|
+
export declare function resumeFactsForLane(provider: RemoteExecProvider | undefined, // 车道词 = settings-schema 词表正本(经本仓读口取型,不手抄;表加词 ⇒ 下方穷举 switch 编译红)
|
|
58
|
+
caps?: {
|
|
57
59
|
k8sSnapshot?: boolean;
|
|
58
60
|
}): SandboxEnvFacts["resumeFacts"] | undefined;
|
|
59
61
|
/** Path-safe session segment: the session id can be CALLER-SUPPLIED (body.sessionId), so it must not be able to
|
package/dist/env-facts.js
CHANGED
|
@@ -42,8 +42,12 @@ export function resumeFactsForLane(provider, caps) {
|
|
|
42
42
|
case "host":
|
|
43
43
|
case undefined:
|
|
44
44
|
return { scratch: "preserved" };
|
|
45
|
-
|
|
45
|
+
case "local-docker":
|
|
46
46
|
return undefined;
|
|
47
|
+
default: {
|
|
48
|
+
const unreachable = provider;
|
|
49
|
+
return unreachable;
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
export function scratchpadSessionSegment(sessionId) {
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
+
import { type ExecutionLane as DeclarableLane, type ExecutionLaneRow } from "@sema-agent/settings-schema/execution-lanes";
|
|
1
2
|
import type { ServiceConfigFlat } from "./config-types.js";
|
|
2
3
|
/** 一位能力的三态。`single-user-only` = 车道支持,但只在单用户部署(`requirePrincipal !== true`)开 ——
|
|
3
4
|
* 多租户下这一位必须关(host 家族的 cwd/scheduler/backgroundShell/LSP 都是这一档,理由同源:
|
|
4
5
|
* 一个租户的输入不许把 agent 指向 worker 宿主上另一个租户/运维的文件与进程)。 */
|
|
5
6
|
export type LaneCapability = "supported" | "unsupported" | "single-user-only";
|
|
6
|
-
/** `REMOTE_EXEC`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
export type RemoteExecProvider = NonNullable<ServiceConfigFlat["remoteExec"]>["provider"];
|
|
11
|
-
/** 闭集:执行车道全集 = provider 词 + `in-process`(`REMOTE_EXEC` 未设的进程内 stub 形,配置面无 provider
|
|
12
|
-
* 可派生 —— 它是「没有 remoteExec」这个状态的名字)。加词 ⇒ {@link executionLaneCaps} 的穷举 switch 与
|
|
13
|
-
* {@link EXECUTION_LANE_WORDS} 两处都编译红,这正是要的(「加词必须逐位表态」)。 */
|
|
7
|
+
/** `REMOTE_EXEC` 能写下去的车道词全集 = settings-schema 词表正本的 `ExecutionLane`(**派生**,本仓零手抄)。 */
|
|
8
|
+
export type RemoteExecProvider = DeclarableLane;
|
|
9
|
+
/** 闭集:执行车道全集 = 表的可声明车道 + `in-process`(`REMOTE_EXEC` 未设的进程内 stub 形 —— 它是「没有 remoteExec」
|
|
10
|
+
* 这个状态的名字,不在表里)。表加词 ⇒ {@link executionLaneCaps} 的穷举 switch 编译红,这正是要的(「加词必须逐位表态」)。 */
|
|
14
11
|
export type ExecutionLane = "in-process" | RemoteExecProvider;
|
|
15
12
|
/**
|
|
16
13
|
* per-lane 能力位。每一位的注里点名**今天真正做判决的那处代码**——对账测试按这些坐标跑真函数,
|
|
@@ -88,12 +85,13 @@ export interface ExecutionLaneCaps {
|
|
|
88
85
|
* · `single-user-only` = 所有任务落在**同一个共享目标**上(worker 宿主:host;一台常驻 SSH 对端:ssh;一台 ADB 设备:adb)
|
|
89
86
|
* ⇒ 只许单用户部署用。
|
|
90
87
|
* 本位**没有** `unsupported` 这一档(那会读成「谁都不许用」,不是车道事实)—— 型上收窄掉,每条车道必须在两档里表态。
|
|
91
|
-
* 🔴
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
88
|
+
* 🔴 **7.114.0 起本位的值不在本仓写**:七条可声明车道读 settings-schema 词表正本的表行(`executionLaneRow(lane).tenantSeparation`;
|
|
89
|
+
* 中心写闸的 `requirePrincipal ∧ single-user-only` 交叉判与本仓 `multi-tenant-shared-lane` 拒启读的是同一个值,不会分叉);
|
|
90
|
+
* 只有 `in-process` 一格是本仓自己的事实(它不在表里)。
|
|
91
|
+
* 唯一消费点是 `config-invariants.ts` 的 `multi-tenant-shared-lane`(经 {@link laneCapabilityHolds} 落成布尔)。本位值与本仓**运行期事实**
|
|
92
|
+
* 不矛盾的对账 = `test/execution-lanes-schema-conformance.test.ts`(隔离位 / 共享文件平面 ⇒ 租户档的蕴含 + 唯一一条「不隔离却分租户」
|
|
93
|
+
* 的车道 = device 点名);ssh / adb 「两租户同一目标」的行为预言机 = `test/multi-tenant-shared-lane.test.ts`
|
|
94
|
+
* (两个租户的任务铸出的 env 报同一个 `workspaceHandle().sandboxId`)。
|
|
97
95
|
*/
|
|
98
96
|
readonly tenantSeparation: Exclude<LaneCapability, "unsupported">;
|
|
99
97
|
}
|
|
@@ -117,23 +115,49 @@ export interface ExecutionLaneCaps {
|
|
|
117
115
|
* 恰是诚实形(§4.3.4)。
|
|
118
116
|
*/
|
|
119
117
|
export declare function executionLaneCaps(lane: ExecutionLane): ExecutionLaneCaps;
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
/**
|
|
119
|
+
* 车道普查全集 = 本仓内部状态名 `in-process` + 词表正本的可声明车道(次序 = 正本声明序;对账测试按它逐 lane 跑真判别式)。
|
|
120
|
+
* 由正本派生、不手维护(手维护的「全表」已被代码反证漏过行)。
|
|
121
|
+
*/
|
|
123
122
|
export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
|
|
123
|
+
/** 一个开放字符串是不是闭集里的词(不做前缀 / 大小写宽容)。 */
|
|
124
|
+
export declare function isExecutionLane(word: string): word is ExecutionLane;
|
|
124
125
|
/**
|
|
125
|
-
* `REMOTE_EXEC` **真能写下去的**那几个词
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* `enumValues: ["e2b","k8s",…]` 字面量(字段静态型是 `readonly string[]`,与 {@link RemoteExecProvider}
|
|
130
|
-
* 零类型联系)。病形与 S-382 逮到的 `WEB_SEARCH_PROVIDER` 逐字同族:给
|
|
131
|
-
* `ServiceConfigFlat["remoteExec"]` 加一条 provider 臂(config-types.ts 里写着的下一条就是
|
|
132
|
-
* `remote-docker`),本文件的穷举 switch 与词表**会**编译红,而目录行不会 —— 于是运维在
|
|
133
|
-
* `GET /v1/config/catalog` 上读到的可写词集静默少一个,新车道成了「配了但目录说不认识」。
|
|
134
|
-
* 收成一只派生数组之后,目录行**就用它本人**(身份相等有钉),全仓只剩这一张表。
|
|
126
|
+
* `REMOTE_EXEC` **真能写下去的**那几个词 = settings-schema 词表正本的 `EXECUTION_LANE_WORDS`(冻结副本,次序逐字同)。
|
|
127
|
+
* 读者:配置目录 `REMOTE_EXEC` 行的 `enumValues`(**就用它本人**,身份相等有钉 —— S-426 修过「目录手抄一份词表」的病)、
|
|
128
|
+
* [ref] 拒启句的 `valid:` 列表、`REQUIRE_EXECUTION_LANE` 拒启句的出路、`multi-tenant-shared-lane` 拒启句的出路车道。
|
|
129
|
+
* 7.114.0 起全仓唯一的车道词来源是正本;本常量只是它在本仓的那一个读口。
|
|
135
130
|
*/
|
|
136
131
|
export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
|
|
132
|
+
/** 一条可声明车道在词表正本里的那一行(必需 / 可选 env 四栏 + 租户档)。加载期自检过完备性,所以对任何 {@link RemoteExecProvider} 都有行。 */
|
|
133
|
+
export declare function executionLaneRow(lane: RemoteExecProvider): ExecutionLaneRow;
|
|
134
|
+
/**
|
|
135
|
+
* 一条车道**必需**的 env 键名 = 正本表行的 `requiredEnv ∪ requiredSecretEnv`(次序:非密坐标在前、密钥在后 —— 与 7.113.0 本仓手写表逐字同)。
|
|
136
|
+
* 读者:`config.ts` 的 [ref] 拒启门(点了名却缺件 ⇒ 点名缺哪几键)、部署面门(`test/deploy-lanes-contract.test.ts`:`lanes.json` 每行登记
|
|
137
|
+
* 本车道必需键、根 / review compose 透传全部车道必需键之并)、`scripts/gen-lanes-json.mjs`(从同一张正本生成 `lanes.json` 的必需键列)。
|
|
138
|
+
* device 没有必需 env(设备是运行期连进来的;它的 boot 前提是跨键的,在跨域不变量区拒启)。
|
|
139
|
+
*/
|
|
140
|
+
export declare function laneRequiredEnv(lane: RemoteExecProvider): readonly string[];
|
|
141
|
+
/**
|
|
142
|
+
* S-598 件③ / S-1008 —— **「`REQUIRE_EXECUTION_LANE=true` ∧ 没有任何车道被点名 ⇒ 拒启」的判据与拒因句的唯一属主**(`modelProviderRefusal` /
|
|
143
|
+
* `modelRouteEndpointRefusal` 同形:属主在叶模块,执法点各自调它,登记在 `config-invariants.ts`)。
|
|
144
|
+
*
|
|
145
|
+
* 🔴 病(S-1008,test d327-g6-engine-7112 OBS-4 / OBS-5;2026-10-03 亲核):修前**两份句子**。运行期真拒启的是 `config.ts` 手写 throw
|
|
146
|
+
* (点名 `REQUIRE_EXECUTION_LANE` / `REMOTE_EXEC` + 修法,用户在 stderr 读到的就是它);注册表条目另写了一句短的
|
|
147
|
+
* 「the deployment asked to refuse this state」,而它只在 `config-center/stage-limits.ts` 的全表校验里被调 —— 候选视图只装
|
|
148
|
+
* `BATCH1_LIMIT_KEYS` 的限额键,带不上这两键 ⇒ 对本条恒 `next === live`,**那一句没有任何读者**(test 六个拒启格 stderr 里零命中)。
|
|
149
|
+
* 改一份另一份不跟。现形:boot 的 throw 与注册表条目的 `check` **都**调本函数 —— 一只判据、一句话,没有第二份文本可漂。
|
|
150
|
+
*
|
|
151
|
+
* 判据读**装配结果**(`executionLaneOf` = 全仓唯一的缺席归一读口,缺席 ⇒ `in-process`),不重判 env:`remoteExec` 缺席**就是**
|
|
152
|
+
* 「`REMOTE_EXEC` 未设/空串 ∧ `CONFIG_PROVIDER ≠ local`」这条三元链尾臂的落点;点了名却配不齐 / 认不出归 [ref] 那条既有拒启(两句话各说各的问题)。
|
|
153
|
+
*
|
|
154
|
+
* 句子的量词纪律(S-1008 ②):stub 车道上**手带族**(`wiring_manifest.tools.entries[].mountedBy === "hands"`)整族不挂,名册里其余工具
|
|
155
|
+
* 照常执行(test R1n:`Now` 真执行成功)—— 不写「零工具」;名册**总条数**随 core 版本走,不写进句子(S-1008 ③)。
|
|
156
|
+
*/
|
|
157
|
+
export declare function executionLaneRequiredRefusal(deployment: {
|
|
158
|
+
readonly requireExecutionLane: boolean;
|
|
159
|
+
readonly remoteExec?: ServiceConfigFlat["remoteExec"];
|
|
160
|
+
}): string | undefined;
|
|
137
161
|
/**
|
|
138
162
|
* `config.remoteExec?.provider` → 车道词。**全仓唯一**的缺席归一读口(S-602 门 D:`?? "in-process"` 只许住在这里,
|
|
139
163
|
* `?? "host"` 与 `lane === undefined || lane === "host"` 同义折零容忍)。
|
|
@@ -144,9 +168,9 @@ export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
|
|
|
144
168
|
*
|
|
145
169
|
* 两个重载 = 类型收窄([ref] §2.2 A):喂**已装配的** config 形(`RemoteExecProvider | undefined`)⇒
|
|
146
170
|
* 返回型里没有 `undefined`,调用点不必为一条不可达的臂写兜底(兜底写多了就是下一处折词);喂开放字符串
|
|
147
|
-
* (测试、外部输入)⇒ 返回含 `undefined
|
|
148
|
-
* `RemoteExecProvider
|
|
149
|
-
* `
|
|
171
|
+
* (测试、外部输入)⇒ 返回含 `undefined`,逼调用点表态。第一个重载的成立依据是**构造**的:
|
|
172
|
+
* `RemoteExecProvider` 是正本元组 `EXECUTION_LANE_WORDS` 的元素型,而 {@link isExecutionLane} 的集合正是由那只元组
|
|
173
|
+
* (加 `in-process`)建成 ⇒ 对 `RemoteExecProvider` 的每一个值 `isExecutionLane` 恒真(对账格逐词跑过)。
|
|
150
174
|
*/
|
|
151
175
|
export declare function executionLaneOf(provider: RemoteExecProvider | undefined): ExecutionLane;
|
|
152
176
|
export declare function executionLaneOf(provider: string | undefined): ExecutionLane | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { EXECUTION_LANE_WORDS as DECLARABLE_LANE_WORDS, EXECUTION_LANES } from "@sema-agent/settings-schema/execution-lanes";
|
|
2
3
|
const NO = "unsupported";
|
|
3
4
|
const YES = "supported";
|
|
4
5
|
const SU = "single-user-only";
|
|
@@ -38,7 +39,7 @@ export function executionLaneCaps(lane) {
|
|
|
38
39
|
hostFilePlane: YES,
|
|
39
40
|
coreRemoteExecutionEnv: YES,
|
|
40
41
|
canonicalPathAuthority: YES,
|
|
41
|
-
tenantSeparation:
|
|
42
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
42
43
|
};
|
|
43
44
|
case "e2b":
|
|
44
45
|
return {
|
|
@@ -56,7 +57,7 @@ export function executionLaneCaps(lane) {
|
|
|
56
57
|
hostFilePlane: NO,
|
|
57
58
|
coreRemoteExecutionEnv: YES,
|
|
58
59
|
canonicalPathAuthority: NO,
|
|
59
|
-
tenantSeparation:
|
|
60
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
60
61
|
};
|
|
61
62
|
case "k8s":
|
|
62
63
|
return {
|
|
@@ -74,7 +75,7 @@ export function executionLaneCaps(lane) {
|
|
|
74
75
|
hostFilePlane: NO,
|
|
75
76
|
coreRemoteExecutionEnv: YES,
|
|
76
77
|
canonicalPathAuthority: YES,
|
|
77
|
-
tenantSeparation:
|
|
78
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
78
79
|
};
|
|
79
80
|
case "ssh":
|
|
80
81
|
return {
|
|
@@ -92,7 +93,7 @@ export function executionLaneCaps(lane) {
|
|
|
92
93
|
hostFilePlane: NO,
|
|
93
94
|
coreRemoteExecutionEnv: YES,
|
|
94
95
|
canonicalPathAuthority: YES,
|
|
95
|
-
tenantSeparation:
|
|
96
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
96
97
|
};
|
|
97
98
|
case "adb":
|
|
98
99
|
return {
|
|
@@ -110,7 +111,7 @@ export function executionLaneCaps(lane) {
|
|
|
110
111
|
hostFilePlane: NO,
|
|
111
112
|
coreRemoteExecutionEnv: YES,
|
|
112
113
|
canonicalPathAuthority: YES,
|
|
113
|
-
tenantSeparation:
|
|
114
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
114
115
|
};
|
|
115
116
|
case "local-docker":
|
|
116
117
|
return {
|
|
@@ -128,7 +129,7 @@ export function executionLaneCaps(lane) {
|
|
|
128
129
|
hostFilePlane: NO,
|
|
129
130
|
coreRemoteExecutionEnv: YES,
|
|
130
131
|
canonicalPathAuthority: NO,
|
|
131
|
-
tenantSeparation:
|
|
132
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
132
133
|
};
|
|
133
134
|
case "device":
|
|
134
135
|
return {
|
|
@@ -146,25 +147,52 @@ export function executionLaneCaps(lane) {
|
|
|
146
147
|
hostFilePlane: NO,
|
|
147
148
|
coreRemoteExecutionEnv: YES,
|
|
148
149
|
canonicalPathAuthority: YES,
|
|
149
|
-
tenantSeparation:
|
|
150
|
+
tenantSeparation: executionLaneRow(lane).tenantSeparation,
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
host: true,
|
|
156
|
-
e2b: true,
|
|
157
|
-
k8s: true,
|
|
158
|
-
ssh: true,
|
|
159
|
-
adb: true,
|
|
160
|
-
"local-docker": true,
|
|
161
|
-
device: true,
|
|
162
|
-
};
|
|
154
|
+
export const ALL_EXECUTION_LANES = Object.freeze(["in-process", ...DECLARABLE_LANE_WORDS]);
|
|
155
|
+
const EXECUTION_LANE_SET = new Set(ALL_EXECUTION_LANES);
|
|
163
156
|
export function isExecutionLane(word) {
|
|
164
|
-
return
|
|
157
|
+
return EXECUTION_LANE_SET.has(word);
|
|
158
|
+
}
|
|
159
|
+
export const REMOTE_EXEC_PROVIDER_WORDS = Object.freeze([...DECLARABLE_LANE_WORDS]);
|
|
160
|
+
const LANE_ROWS = (() => {
|
|
161
|
+
const rows = new Map();
|
|
162
|
+
for (const row of EXECUTION_LANES) {
|
|
163
|
+
if (rows.has(row.lane))
|
|
164
|
+
throw new Error(`@sema-agent/settings-schema EXECUTION_LANES lists lane "${row.lane}" twice — the lane table is the single source; refusing to guess which row is meant`);
|
|
165
|
+
rows.set(row.lane, row);
|
|
166
|
+
}
|
|
167
|
+
const missing = DECLARABLE_LANE_WORDS.filter((lane) => !rows.has(lane));
|
|
168
|
+
if (missing.length > 0)
|
|
169
|
+
throw new Error(`@sema-agent/settings-schema EXECUTION_LANES has no row for lane(s) ${missing.join(", ")} — the lane table is the single source; refusing to start with a lane whose required env / tenant separation is unknown`);
|
|
170
|
+
return rows;
|
|
171
|
+
})();
|
|
172
|
+
export function executionLaneRow(lane) {
|
|
173
|
+
const row = LANE_ROWS.get(lane);
|
|
174
|
+
if (row === undefined)
|
|
175
|
+
throw new Error(`execution lane "${lane}" has no row in the settings-schema lane table (load-time completeness check was bypassed)`);
|
|
176
|
+
return row;
|
|
177
|
+
}
|
|
178
|
+
export function laneRequiredEnv(lane) {
|
|
179
|
+
const row = executionLaneRow(lane);
|
|
180
|
+
return [...row.requiredEnv, ...row.requiredSecretEnv];
|
|
181
|
+
}
|
|
182
|
+
export function executionLaneRequiredRefusal(deployment) {
|
|
183
|
+
if (!deployment.requireExecutionLane || executionLaneOf(deployment.remoteExec?.provider) !== "in-process")
|
|
184
|
+
return undefined;
|
|
185
|
+
return (`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
|
|
186
|
+
`With no lane the engine falls back to its in-process stub environment: the hands band (the roster entries whose ` +
|
|
187
|
+
`wiring_manifest mountedBy is "hands", Bash among them) is not mounted, while the rest of the roster still executes, ` +
|
|
188
|
+
`and every task still returns status=completed — for any request that needs a hand, indistinguishable from "the model ` +
|
|
189
|
+
`chose not to use its tools". (How much of the roster is mounted is a per-run fact: read the wiring_manifest event and ` +
|
|
190
|
+
`count entries whose mountedBy is "hands" — never judge by tool names.) ` +
|
|
191
|
+
`Cause: REMOTE_EXEC is unset/empty AND CONFIG_PROVIDER is not "local". ` +
|
|
192
|
+
`Fix: set REMOTE_EXEC to one of ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")} (and that lane's required env), ` +
|
|
193
|
+
`or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
|
|
194
|
+
`or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
|
|
165
195
|
}
|
|
166
|
-
export const ALL_EXECUTION_LANES = Object.freeze(Object.keys(EXECUTION_LANE_WORDS).filter(isExecutionLane));
|
|
167
|
-
export const REMOTE_EXEC_PROVIDER_WORDS = Object.freeze(ALL_EXECUTION_LANES.filter((lane) => lane !== "in-process"));
|
|
168
196
|
export function executionLaneOf(provider) {
|
|
169
197
|
if (provider === undefined)
|
|
170
198
|
return "in-process";
|
|
@@ -36,17 +36,97 @@ export interface ParseHooksConfigResult {
|
|
|
36
36
|
error?: string;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* 🔴 **S-924 → S-995(clay [ref] ③;八病审计 SG H6-d;test 7.112.0 验收 OBS ①)—— 工具事件的 matcher 选中了工具**别名**、却选不中它的正名 ⇒ fresh 提交面响亮拒。**
|
|
40
|
+
*
|
|
41
|
+
* 病:三个工具事件(PreToolUse / PostToolUse / PostToolUseFailure)的 matcher 都按调用的 **wire 名**匹配(本文件顶注
|
|
42
|
+
* 「tool_name 是哪一个名」;core `NAME_READINGS` 三行均为 `wire`)。引擎自带工具 `ListAgents` 的别名 `ListPeers` 写进 matcher
|
|
43
|
+
* ⇒ **永不命中、一句话都没有**(post 两事件自 7.107.0 / core 7.36.0 起如此,PreToolUse 一直如此)。静默不命中 = 写的人以为在生效的 hook
|
|
44
|
+
* 其实没跑,硬破坏三句要求的「响亮」没有着落。7.112.0(S-924)只拒**字面**别名;同一个错误写成 `^ListPeers$` / `ListPeers.*` / `(ListPeers)` 照样 200(S-995)。
|
|
45
|
+
*
|
|
46
|
+
* 规则(一条;字面形是它的特例):{@link deadToolMatcherIssue} 对 core 静态目录里**每一条退役别名**问「这只 matcher 选不选它 / 选不选它的正名」:
|
|
47
|
+
* matcher **命中别名 ∧ 不命中它的正名** ⇒ `400 request.field_invalid`(`admission.ts` fresh 门)。「选不选」与运行期 {@link hookMatcherMatches} 同一套读法,按**一条顺序**判(两档共用,7.114.0 修复轮):
|
|
48
|
+
* ① **长度上限**:超 {@link MAX_MATCHER_EVAL_CHARS} 的 matcher 两档都不读 ⇒ 未判(reason = oversize);
|
|
49
|
+
* ② 空 / `*` 命中一切 ⇒ 永不「死」;
|
|
50
|
+
* ③ **字面 / 名字表读法**({@link matcherLiteralNames},与运行期共用:`|` 与 `,` 分隔、每段 trim〔容忍两端与分隔符两侧空白〕、不折大小写、不剥词内空白)
|
|
51
|
+
* 两档都先走、每只 matcher 读一次、**不占求值预算**,按名字表成员答 —— 所以两档在字面子集上答案逐位相同是构造出来的(R5f 语料再钉);
|
|
52
|
+
* ④ 只剩**正则形**:**`"regex"` 档**(hooks 会被采纳的车道,`taskHooksHonored`)问运行期正则臂的那一只函数 {@link matcherPatternMatches}(同一个函数对象,锚定正则),
|
|
53
|
+
* 经 `node:vm` 的 `timeout` 带一次受理共享的预算调用({@link matcherPatternMatchesWithin});**`"literal"` 档**(多租户车道:hooks 整只被 resolveSpec 忽略)
|
|
54
|
+
* = **不适用**:该车道没有被守的对象(hooks 不装配),不判、不算未判、不登记 —— **零正则求值、零调用方正则执行**(7.112.0 的字面判保留:裸 `ListPeers` / `Read|ListPeers` 照旧 400)。
|
|
55
|
+
* · **为什么两档**(codex r1 [high] + 主会话裁):受理面是**所有**车道,而运行期只有单用户车道跑 hooks。在受理线程上跑调用方的正则 = 给多租户加一个事件循环 DoS 面
|
|
56
|
+
* (实测:`(.*)*` ×10 + `X` 对 9 字符名字首次执行 1.7 s,18 字符嵌套形 15 s,24 字符 157 s;8 MiB 交替串编译 + 求值 9 s)—— 预算只界住一次请求、界不住「每个请求再拿一份」。
|
|
57
|
+
* 但也不能因此在多租户车道「不判决」(那会放宽已发行为:裸 `ListPeers` 的 400)。所以两车道同一条规则、两档深度。谓词 `taskHooksHonored` 是唯一属主(与 resolveSpec / capabilities 同调)。
|
|
58
|
+
* · **为什么不进 {@link parseHooksConfig}**(codex r1 [high],S-924):`parseHooksConfig` 同时是 resume 重放(`parseTaskSettings` 宽容层)的校验器,
|
|
59
|
+
* 它判不过 ⇒ **整只 hooks 不采纳**。存量 body 里同一份 hooks 常常还有会真拦的 PreToolUse 钩子 —— 一个永不命中的别名 matcher 把它们一并带走,
|
|
60
|
+
* 升级前被拦的 Bash 升级后放行(安全轴上的回退)。所以这条规则住在 fresh 专用的判据里,与 `taskSettingsKeyIssue` 同姿势
|
|
61
|
+
* (「fresh 400 / resume 宽容」双层:会砖存量的规则只咬 fresh)。resume 照旧整只采纳,别名那一组继续永不命中(与 7.111.0 逐字同),其余照跑。
|
|
62
|
+
* · **哪些事件** = 契约 `HOOK_EVENT_MATCHER_FIELD[event] === "tool_name"` ∧ 引擎属主 `HOOK_EVENT_OWNER[event] === "engine"`
|
|
63
|
+
* (今天恰是上面三个;`PermissionRequest` / `PermissionDenied` 也匹配 `tool_name` 但属壳,壳自己跑,不是本 runner 的读法)。**不手抄事件表**([ref])。
|
|
64
|
+
* · **哪些名字算别名** = core 静态目录(`describeToolCatalog()`)里某条 `face.aliases` 的成员,且它自己不是另一条目的 wire 名({@link engineToolAliases})。
|
|
65
|
+
* **只读这一只真源**([ref])。本仓自身不声明带别名的工具;嵌入方经 `resolveSpec` 席挂的别名与 MCP 工具名在受理面看不见(名册是每任务装配的),**不在射程**。
|
|
66
|
+
* · **正名同在则不拒**:`正名|别名` / `List.*` 会命中正名,不是「静默不命中」,且正是 7.107.0 CHANGELOG 给的「滚动期两名都写」补偿形。
|
|
67
|
+
* 只拒「别名那一路永远打不中、而它的正名不在选中集里」—— 含 `Read|ListPeers`(7.112.0 已拒;主会话裁:规则形采「按条目逐条问」,不放宽;卡面整 matcher「匹配不到任何现役名」形会把它放回 200)。
|
|
68
|
+
* 多拒(相对卡面形)三例与出路:`Read|ListPeers` / `ListPeers|mcp__.*`(别名支永不命中,出路:改成正名)/ 排除式 `^(?!ListAgents).*`(出路:`^(?!ListAgents|ListPeers).*` 两名都不命中 ⇒ 放)。
|
|
69
|
+
* · **空白与大小写(S-995 / sema-test [ref] 起,运行期与 CC 2.1.280 同形)**:字面 / 名字表读法({@link matcherLiteralNames};`|` 与 `,` 分隔)容忍两端与分隔符两侧空白 ⇒ `" ListPeers"` / `"ListPeers "` / `"Read | ListPeers"` 会命中别名 ⇒ 落进本规则 400;
|
|
70
|
+
* **区分大小写、不剥词内空白** ⇒ `"listpeers"` / `"List Peers"` 对别名与正名都选不中,照旧受理(CC 同形)。
|
|
71
|
+
* · **不在本规则内**:两边都选不中的 matcher(大小写错 / 词内空白 / 纯错字;目录之外的 MCP / 嵌入方工具名同样「选不中目录里任何名」,受理面无从区分错字与它们)=
|
|
72
|
+
* **「选不到目录里任何名」另一类,候设计(台账另立号,主会话裁)**。
|
|
73
|
+
*
|
|
74
|
+
* 🔴 **「未判」臂过 `recordFailOpen`**(F 类 tag `MATCHER_UNJUDGED_TAG`;主会话裁,CLAUDE.md「合法 F 类回退走登记 tag」):一只 matcher 没能被判 ⇒ 照旧受理 + 登记一笔,
|
|
75
|
+
* detail = `event=… reason=… matcherChars=…`(**不带 matcher 原字节**,S-716 姿势)。三种 reason 同一个 tag:
|
|
76
|
+
* `oversize` —— 两档:matcher 超 {@link MAX_MATCHER_EVAL_CHARS} 字符不读(多租户车道上 hooks 不装配,这一笔只为让超长输入显形);
|
|
77
|
+
* `over_budget` ——「regex」档正则形:一次受理共享一份 {@link ADMISSION_MATCHER_EVAL_BUDGET_MS} 墙钟预算,用尽 ⇒ 这一只与其后各只正则形未判(`vm` 的 `timeout` 打断回溯,含 tier-up 之后的原生码);
|
|
78
|
+
* `invalid_regex` ——「regex」档正则形编不过(运行期另有每次调用的 `hook_matcher_invalid`;这是确定性的坏配置,不是负载 —— 拒它是新检测臂,不在本规则)。
|
|
79
|
+
* 放行最坏后果 = 那一只 matcher 没被这条判据守住、照旧永不命中,**不是安全闸**(受理面有效性判);反方向「越界即拒」会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。
|
|
80
|
+
* 拒放集合相对 7.113.0 的一处放宽(披露在 CHANGELOG / MIGRATION):超过上限的字面别名 matcher 7.113.0 拒(当时字面判不设上限),自 7.114.0 两档都判未判、受理。
|
|
81
|
+
*
|
|
82
|
+
* 错误句只回显**目录里的**别名与正名(闭集字符串),不回显调用方的原字节(S-716 姿势)。
|
|
83
|
+
*/
|
|
84
|
+
/** 超此长度的 matcher 不在受理面读(两档同一个上限;未判,reason = oversize)。与 {@link MAX_HOOK_COMMAND_CHARS} 同量纲的服务侧防线:真实 matcher(精确名 / 交替 / `mcp__.*`)远在其内;目录全部工具名交替 ~0.5 KiB。 */
|
|
85
|
+
export declare const MAX_MATCHER_EVAL_CHARS = 8192;
|
|
86
|
+
/** 一次 fresh 受理里 regex 档**正则形**求值共享的墙钟预算(毫秒)。字面形不占它。良性求值 ~0.1 ms;灾难回溯的 matcher 把预算吃完就停,后面的正则形组直接「未判」。 */
|
|
87
|
+
export declare const ADMISSION_MATCHER_EVAL_BUDGET_MS = 100;
|
|
88
|
+
/** 受理面判据的求值深度(见上头注):`"regex"` = 字面形按名字表答、正则形问运行期同一只正则求值(带预算);`"literal"` = 7.112.0 的字面判(零正则,正则形不适用)。 */
|
|
89
|
+
export type MatcherEvalDepth = "regex" | "literal";
|
|
90
|
+
/**
|
|
91
|
+
* **仅供测试注入**的求值预算(生产调用点不传 ⇒ 预算 = {@link ADMISSION_MATCHER_EVAL_BUDGET_MS}、时钟 = `performance.now`)。
|
|
92
|
+
* 让「预算是一次受理共享的」「耗尽之后的组」这类格子按求值次序断言,不按机器速度断言(`test/hooks-alias-matcher-s995.test.ts` R4)。
|
|
41
93
|
*/
|
|
42
|
-
export
|
|
94
|
+
export interface MatcherEvalBudget {
|
|
95
|
+
readonly budgetMs?: number;
|
|
96
|
+
readonly now?: () => number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* fresh 提交面专用判据(S-924 / S-995):配置里第一条「永远打不中」的工具事件 matcher 的拒句;没有 ⇒ undefined。
|
|
100
|
+
* 入参是 {@link parseHooksConfig} 已校验过的配置;`depth` 由调用点按车道定(见头注:`taskHooksHonored(config) ? "regex" : "literal"`)。
|
|
101
|
+
* `budget` **仅供测试注入**({@link MatcherEvalBudget});生产调用点只传前两个实参。
|
|
102
|
+
* **只有 `admission.ts` 的 fresh 提交门调它** —— resume 重放不调(理由见上面的头注)。未判的 matcher 过 `recordFailOpen`(tag `MATCHER_UNJUDGED_TAG`)。
|
|
103
|
+
*/
|
|
104
|
+
export declare function deadToolMatcherIssue(config: HooksConfig, depth: MatcherEvalDepth, budget?: MatcherEvalBudget): string | undefined;
|
|
43
105
|
/**
|
|
44
106
|
* 把 wire 上的 untrusted `settings.hooks` 校验成契约 {@link HooksConfig} + 服务侧上限。
|
|
45
107
|
* 错误返回 error 字符串而不 throw——submit 路径 400 fail-loud、resume 防御路径 warn-drop,由调用方选。
|
|
46
108
|
*/
|
|
47
109
|
export declare function parseHooksConfig(raw: unknown): ParseHooksConfigResult;
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
110
|
+
/**
|
|
111
|
+
* matcher 的**字面 / 名字表读法**(S-995 / sema-test [ref];与 CC 2.1.280 同形):matcher 只含名字字符(`[A-Za-z0-9_-]`)、竖线、**逗号**与**空格**时,它不是正则而是「名字列表」——
|
|
112
|
+
* 按 `[|,]` 拆、**每段 trim**、丢空段;返回这张名字表。含任何别的字符(`.` / `*` / 括号 / 制表 / 换行 …)⇒ `undefined` = 不是字面读法,整体当正则(由调用方处置,**一字不动**)。
|
|
113
|
+
* 大小写敏感、词内空白不剥(`"Ba sh"` 是一个词 `Ba sh`)。**对所有带 matcher 的事件同一只读法**(工具事件 / StopFailure 的 `error` 词 / PreCompact·PostCompact 的 `trigger` 词 /
|
|
114
|
+
* `if` 条件的名部),不按事件分档。
|
|
115
|
+
*
|
|
116
|
+
* 等价性的依据(收车档 ⑥ / ⑦):
|
|
117
|
+
* · 黑盒 —— sema-test `repro/cc-r219-mcpstar-hookmatcher-20261003/`([ref],CC 2.1.280 真 TUI + 真 command 钩子):`" Bash"` / `"Bash "` / `" Bash "` / `"Read | Bash"` 命中 3/3,
|
|
118
|
+
* `"Ba sh"`(词内空白)/ `bash` / `BASH` 不命中(零告警),`^Bash$` / `Bash.*` / `(Bash)` / `Read|Bash` 照命中;PreToolUse 与 PostToolUse 同向。**逗号没有黑盒读数**,只有语料。
|
|
119
|
+
* · 语料 —— `sema-internal/blueprints/cc-decoded/cli280.js:216638` `uAe(matchValue, matcher, flag, …)` → `$mr(matcher, flag, …)`:`flag` 真时字符集门 `/^[a-zA-Z0-9_|, -]+$/` 过了才
|
|
120
|
+
* `matcher.split(/[|,]/).map(trim).filter(Boolean)` 做成员判;过不了才 `new RegExp(matcher)`(CC 的正则路径**不锚定**,本仓锚定 `^(?:…)$` —— 既有差异,不动)。
|
|
121
|
+
* 本函数的字符集与拆符与 CC 逐字同(空白只认空格 0x20)。
|
|
122
|
+
* · 已知差异(如实登记,本车不做):CC 的空白 / 逗号放行**按事件分档**(`dAe` 集合:工具事件 / PreCompact / PostCompact 等放,StopFailure 不放),本函数不分事件(第二批裁:同一只读法);
|
|
123
|
+
* CC 正则路径不锚定、本仓锚定。两条登记在 CHANGELOG 7.114.0 线 · 车 UW 已知边界。
|
|
124
|
+
* 零正则:单趟逐字符扫描(7.114.0 修复轮起;只为非空段分配名字,不按分隔符个数分配)。受理面两档({@link deadToolMatcherIssue})与运行期求值**共用这一只**读法,且两档都先走它、不占求值预算 ——
|
|
125
|
+
* 两档在字面子集上答案逐位相同是**构造出来的**,不是对拍出来的(超过长度上限的 matcher 两档都不读、同判未判)。
|
|
126
|
+
*/
|
|
127
|
+
export declare function matcherLiteralNames(matcher: string): readonly string[] | undefined;
|
|
128
|
+
/** CC matcher 语义:空/`*`/缺省匹配一切;**字面 / 名字表读法**({@link matcherLiteralNames}:`|` 与 `,` 分隔、每段 trim、不剥词内空白、区分大小写)按名字表成员判;
|
|
129
|
+
* 其余整体当 JS 正则({@link matcherPatternMatches};锚定全匹配;S-995 起正则路径一字不动);非法正则 → 该条不匹配(fail-closed 到"不跑",由调用方记账一次)。 */
|
|
50
130
|
export declare function hookMatcherMatches(matcher: string | undefined, value: string): boolean | "invalid";
|
|
51
131
|
/** `if` 条目条件(CC-exact,对照 CC 2.1.187/198 实测行为核验的真语义,两版一致):
|
|
52
132
|
* 值 = permission-rule 模式(`ToolName` 或 `ToolName(arg-模式)`),【不是】表达式语言——按当前工具调用
|