@sema-agent/server 7.119.0 → 7.120.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 +54 -0
- package/USAGE.md +5 -3
- package/dist/boot/config-center.js +5 -5
- package/dist/boot/execution-env.js +3 -2
- package/dist/boot/fatal-exit.d.ts +7 -1
- package/dist/boot/fatal-exit.js +2 -1
- package/dist/boot/runner-deps.d.ts +4 -3
- package/dist/boot/runner-deps.js +7 -3
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/capabilities/builtin-tools.d.ts +31 -8
- package/dist/capabilities/builtin-tools.js +22 -1
- package/dist/capabilities/scenarios.d.ts +8 -6
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/capabilities/send-user-file-tool.d.ts +6 -3
- package/dist/capabilities/send-user-file-tool.js +1 -1
- package/dist/config-catalog.js +2 -2
- package/dist/config-lkg.d.ts +33 -5
- package/dist/config-lkg.js +55 -11
- package/dist/config-types.d.ts +8 -1
- package/dist/config.js +9 -9
- package/dist/deployment-governance.d.ts +3 -1
- package/dist/deployment-governance.js +3 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +4 -0
- package/dist/device-ws-hub.js +17 -9
- package/dist/device-ws-protocol.d.ts +15 -0
- package/dist/http/resume-legs.js +4 -0
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/devices.js +3 -3
- package/dist/http/routes/rules.js +13 -6
- package/dist/list-entry-display.d.ts +14 -0
- package/dist/list-entry-display.js +14 -0
- package/dist/namespaced-rule-name.js +2 -6
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/operator-ask.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +8 -0
- package/dist/plugins/one-time-migrations.js +72 -3
- package/dist/plugins/permission-rule-store-file.js +2 -6
- package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
- package/dist/plugins/permission-rule-store-sql.js +166 -59
- package/dist/plugins/remote-env-device.d.ts +12 -1
- package/dist/plugins/remote-env-device.js +13 -5
- package/dist/plugins/scheduler-support.d.ts +8 -4
- package/dist/plugins/scheduler-support.js +5 -5
- package/dist/rule-entry-list.d.ts +0 -1
- package/dist/rule-entry-list.js +4 -9
- package/dist/rules-consent.d.ts +12 -3
- package/dist/rules-consent.js +5 -4
- package/dist/run-local.js +2 -0
- package/dist/sema-config-dir.d.ts +19 -0
- package/dist/sema-config-dir.js +33 -0
- package/dist/session-policy-wire.js +5 -4
- package/dist/spec-fields.d.ts +5 -4
- package/dist/task-settings.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +16 -9
- package/dist/trace/self-minted-wire.d.ts +14 -0
- package/dist/trace/self-minted-wire.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
- package/package.json +2 -2
package/dist/config-types.d.ts
CHANGED
|
@@ -1994,6 +1994,13 @@ export interface ServiceConfigFlat {
|
|
|
1994
1994
|
* `READ_DENY_BUILTIN_EXCLUDE`=逗号分隔;不在表里的行名 / 空串**启动期响亮拒**。
|
|
1995
1995
|
* 🔴 不变量(判据帖 G5b):删内建**绝不**削弱任务自己显式声明的 deny —— 那是另一层(additions)。 */
|
|
1996
1996
|
readDenyBuiltinExclude?: readonly string[];
|
|
1997
|
+
/** core 7.46.0 #1419:部署**声明**的配置根(`SEMA_CONFIG_DIR`,绝对路径;起服的供给动作建出目录,能不能用由引擎判)—— 交引擎 `RunnerDeps.agentConfigDir`
|
|
1998
|
+
* 与守卫集 `createSensitivePathPolicy` 的同名入参:内建读拒表里以 `.sema` 开头的行在这个根下同样拒,写进根下要问一次。缺席 = 没声明
|
|
1999
|
+
* ⇒ 引擎读字面 `~/.sema`(与 LKG / skill 缓存 / scheduler 店的缺省落点同一处)。读法单一属主 `sema-config-dir.ts`。 */
|
|
2000
|
+
agentConfigDir?: string;
|
|
2001
|
+
/** LKG 落盘路径的覆写(`CONFIG_LKG_PATH`,绝对路径;单一读法 `config-lkg.ts` `lkgPathOverrideOf`)。缺席 = LKG 落在配置根下按箱派生。
|
|
2002
|
+
* 读者:LKG 的落盘 / 读回(`boot/config-center.ts`)与读拒行(部署姿态座席族 —— LKG 在哪,读拒就守哪,`lkgReadDenyRows`)。 */
|
|
2003
|
+
configLkgPath?: string;
|
|
1997
2004
|
/** Structured log level. */
|
|
1998
2005
|
logLevel: "debug" | "info" | "warn" | "error";
|
|
1999
2006
|
/** Per-principal request cap per minute on task/run endpoints. 0 disables. */
|
|
@@ -2127,7 +2134,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
|
|
|
2127
2134
|
* config 这一层它只是一句声明。 */
|
|
2128
2135
|
export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
|
|
2129
2136
|
/** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
|
|
2130
|
-
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
|
|
2137
|
+
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "agentConfigDir" | "configLkgPath">;
|
|
2131
2138
|
/** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
|
|
2132
2139
|
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "drainSecondSignalMinGapMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "engineOrphanLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
2133
2140
|
/** 组:observability(可观测)。 */
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,8 @@ import {} from "./posture-source.js";
|
|
|
20
20
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
21
21
|
import { describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
|
|
22
22
|
import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
|
|
23
|
+
import { declaredSemaConfigDir } from "./sema-config-dir.js";
|
|
24
|
+
import { lkgPathOverrideOf } from "./config-lkg.js";
|
|
23
25
|
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
|
|
24
26
|
import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
|
|
25
27
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
@@ -29,6 +31,7 @@ import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js
|
|
|
29
31
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
30
32
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
31
33
|
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
34
|
+
import { displayListEntry } from "./list-entry-display.js";
|
|
32
35
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
33
36
|
import { allowlistedEnv } from "./server-secret-env.js";
|
|
34
37
|
import { ENV_NAME_ALLOWLIST_KNOBS, ENV_NAME_ALLOWLIST_KNOB_NAMES, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
|
|
@@ -1209,17 +1212,11 @@ function parseDegradeOn(words) {
|
|
|
1209
1212
|
}
|
|
1210
1213
|
return words;
|
|
1211
1214
|
}
|
|
1212
|
-
const PRESENTED_NAME_MAX = 160;
|
|
1213
|
-
function presentToolName(name) {
|
|
1214
|
-
const clean = name.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
1215
|
-
return clean.length <= PRESENTED_NAME_MAX ? clean : `${clean.slice(0, PRESENTED_NAME_MAX)}…(${name.length} chars)`;
|
|
1216
|
-
}
|
|
1217
1215
|
function ccRuleFormGuidance(name) {
|
|
1218
1216
|
if (!name.includes("(") && !name.includes(")"))
|
|
1219
1217
|
return undefined;
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
`Drop the parentheses to gate the tool itself ("${presentToolName(name.split("(")[0] ?? name)}"); ` +
|
|
1218
|
+
return (`CC-form rule specifier — this list matches the WHOLE tool name verbatim, so it can only ever match a tool literally named ${displayListEntry(name)}. ` +
|
|
1219
|
+
`Drop the parentheses to gate the tool itself (${displayListEntry(name.split("(")[0] ?? name)}); ` +
|
|
1223
1220
|
`to decide per COMMAND use \`runtime.commandPolicy\` ({command, decision} — bare argv[0] names); ` +
|
|
1224
1221
|
`to put every shell call behind an approval use MANUAL_MODE_SHELL_GATE=always`);
|
|
1225
1222
|
}
|
|
@@ -1264,7 +1261,7 @@ export function unmatchableToolNamesRefusal(source, names, judgment) {
|
|
|
1264
1261
|
return undefined;
|
|
1265
1262
|
const message = (`${source} has ${bad.length === 1 ? "an entry" : "entries"} that can never take effect — ${bad.length === 1 ? "it" : "each"} would never match a live tool or cannot be compiled into a rule ` +
|
|
1266
1263
|
`(names are read exactly as written: no alias folding, no protocol prefix added): ` +
|
|
1267
|
-
bad.map((b) => `${
|
|
1264
|
+
bad.map((b) => `${displayListEntry(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
1268
1265
|
return { message, names: bad.map((b) => b.name) };
|
|
1269
1266
|
}
|
|
1270
1267
|
const APPROVAL_GRANT_ENV = "APPROVAL_GRANT";
|
|
@@ -1899,6 +1896,8 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1899
1896
|
assertReadDenyBuiltinsResolvable(tiers, exclude);
|
|
1900
1897
|
return { ...(tiers !== undefined ? { readDenyBuiltinTiers: tiers } : {}), ...(exclude !== undefined ? { readDenyBuiltinExclude: exclude } : {}) };
|
|
1901
1898
|
})(),
|
|
1899
|
+
...((dir) => (dir !== undefined ? { agentConfigDir: dir } : {}))(declaredSemaConfigDir()),
|
|
1900
|
+
...((p) => (p !== undefined ? { configLkgPath: p } : {}))(lkgPathOverrideOf(process.env.CONFIG_LKG_PATH)),
|
|
1902
1901
|
...((h) => (h !== undefined ? { remoteExecHomeDir: h } : {}))((() => {
|
|
1903
1902
|
const raw = process.env.REMOTE_EXEC_HOME_DIR;
|
|
1904
1903
|
if (raw === undefined || raw === "")
|
|
@@ -2337,6 +2336,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
2337
2336
|
"backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
|
|
2338
2337
|
"sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled", "lspEnvAllow",
|
|
2339
2338
|
"imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
|
|
2339
|
+
"agentConfigDir", "configLkgPath",
|
|
2340
2340
|
];
|
|
2341
2341
|
const LIMITS_HTTP_GROUP_KEYS = [
|
|
2342
2342
|
"port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
|
|
@@ -11,6 +11,8 @@ export interface DeploymentGovernanceConfigView {
|
|
|
11
11
|
readonly commandPolicy?: ServiceConfig["commandPolicy"];
|
|
12
12
|
readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"];
|
|
13
13
|
readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"];
|
|
14
|
+
/** core 7.46.0 #1419:部署声明的配置根 —— 守卫集的 `.sema/…` 模式在根下同样守(与 Runner 的同名座同一个值,`loadConfig` 铸)。 */
|
|
15
|
+
readonly agentConfigDir?: ServiceConfig["agentConfigDir"];
|
|
14
16
|
/**
|
|
15
17
|
* 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂
|
|
16
18
|
* **誊本门**(`<localDataRoot>/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见
|
|
@@ -185,7 +187,7 @@ type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped";
|
|
|
185
187
|
* · `writeTargetGuard`(守卫集策略自带,shell 写站问「这个绝对目标守不守」)—— 只在本层的形**覆盖守卫的入参形**时带上
|
|
186
188
|
* ({@link WRITE_TARGET_GUARD_DOMAIN}):真身层带;词法层不带(它的 env 对绝对形报错 ⇒ core 的「判不了就守」会把每一次
|
|
187
189
|
* shell 写都判成受守,正与两层射程互补的原设计相反)。这不是给某一形开的特例,是同一条「成员描述它守得住的那部分」的读法;
|
|
188
|
-
* · `projection` / `
|
|
190
|
+
* · `projection` / `pathRules` / `nameSets` —— 守卫集策略不带,本包装也不报(报了就是替它声称一张它没有的拒表)。
|
|
189
191
|
* 修前(core 7.33.x)这里是一只 `{ check }` 字面量,7.34.0 起那只字面量把真身层的守卫剥掉了 —— 沙箱 lane 的 shell 写站
|
|
190
192
|
* 因此看不见部署守卫集(`cp x .env` 在 acceptEdits 下被引擎 place 并答掉)。
|
|
191
193
|
*/
|
|
@@ -87,18 +87,20 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
|
|
|
87
87
|
const sensitivePathLayers = config.sensitiveWritePatterns.length > 0
|
|
88
88
|
? (() => {
|
|
89
89
|
const dataRootOpt = roots.engineDataRoot !== undefined ? { dataRoot: roots.engineDataRoot } : {};
|
|
90
|
+
const configRootOpt = config.agentConfigDir !== undefined ? { agentConfigDir: config.agentConfigDir } : {};
|
|
90
91
|
const realTarget = createSensitivePathPolicy({
|
|
91
92
|
env: pathAdjudication.env,
|
|
92
93
|
patterns: config.sensitiveWritePatterns,
|
|
93
94
|
...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
|
|
94
95
|
...dataRootOpt,
|
|
96
|
+
...configRootOpt,
|
|
95
97
|
});
|
|
96
98
|
const transcriptGuard = config.localDataRoot !== undefined ? [createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })] : [];
|
|
97
99
|
if (pathAdjudication.cwd !== undefined)
|
|
98
100
|
return [realTarget, ...transcriptGuard];
|
|
99
101
|
return [
|
|
100
102
|
scopedToWriteTargetForm(realTarget, "absolute-or-stamped"),
|
|
101
|
-
scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative"),
|
|
103
|
+
scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt, ...configRootOpt }), "unstamped-relative"),
|
|
102
104
|
...transcriptGuard,
|
|
103
105
|
];
|
|
104
106
|
})()
|
package/dist/device-store.d.ts
CHANGED
|
@@ -161,6 +161,13 @@ export declare const DEVICE_TEXT_LIMITS: {
|
|
|
161
161
|
readonly connEpoch: 64;
|
|
162
162
|
readonly replicaId: 128;
|
|
163
163
|
};
|
|
164
|
+
/**
|
|
165
|
+
* 设备侧路径「是不是绝对路径」的**唯一判据**(v1 设备都是 POSIX:`platformOs` 闭集 `darwin` | `linux`;用 `posix` 而不是 `node:path`,
|
|
166
|
+
* server 跑在什么平台上与设备路径语义无关)。两处读它、不写第二份:入籍口(`http/routes/devices.ts`,`workspaceRoot` 不过 ⇒ 400)与
|
|
167
|
+
* 铸 env(`plugins/remote-env-device.ts` `effectiveDeviceCwd`,有效工作目录不过 ⇒ 响亮拒;那一道守的是入籍口收紧之前落库的存量行与
|
|
168
|
+
* 会话 / 委派链来的 cwd)。
|
|
169
|
+
*/
|
|
170
|
+
export declare function isDeviceAbsolutePath(p: string): boolean;
|
|
164
171
|
export declare function assertDeviceOwnerShape(owner: DeviceOwner): void;
|
|
165
172
|
export declare function assertDeviceIdShape(deviceId: string): void;
|
|
166
173
|
export declare function assertRootSessionIdShape(rootSessionId: string): void;
|
package/dist/device-store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomInt } from "node:crypto";
|
|
2
|
+
import { posix } from "node:path";
|
|
2
3
|
export const DEVICE_ERROR_CODES = {
|
|
3
4
|
NOT_BOUND: "device.not_bound",
|
|
4
5
|
OFFLINE: "device.offline",
|
|
@@ -86,6 +87,9 @@ export const DEVICE_TEXT_LIMITS = {
|
|
|
86
87
|
connEpoch: 64,
|
|
87
88
|
replicaId: 128,
|
|
88
89
|
};
|
|
90
|
+
export function isDeviceAbsolutePath(p) {
|
|
91
|
+
return posix.isAbsolute(p);
|
|
92
|
+
}
|
|
89
93
|
export function assertDeviceOwnerShape(owner) {
|
|
90
94
|
assertIdentityKey("owner.tenant", owner.tenant, DEVICE_KEY_MAX_BYTES);
|
|
91
95
|
assertIdentityKey("owner.subject", owner.subject, DEVICE_KEY_MAX_BYTES);
|
package/dist/device-ws-hub.js
CHANGED
|
@@ -425,7 +425,20 @@ export function createDeviceWsHub(options) {
|
|
|
425
425
|
clearTimeout(conn.handshakeTimer);
|
|
426
426
|
conn.handshakeTimer = null;
|
|
427
427
|
releasePreAuthSlot(conn);
|
|
428
|
-
|
|
428
|
+
const helloAck = {
|
|
429
|
+
t: "helloAck",
|
|
430
|
+
ok: true,
|
|
431
|
+
heartbeatIntervalMs,
|
|
432
|
+
maxInflight: maxInflightPerDevice,
|
|
433
|
+
gen: conn.genStr,
|
|
434
|
+
pendingInstructions: pendingIds,
|
|
435
|
+
chunkWatermarks,
|
|
436
|
+
reconnectGraceMs,
|
|
437
|
+
cancelGraceMs,
|
|
438
|
+
chunkGapTimeoutMs,
|
|
439
|
+
executionCeilingGraceMs: execGraceMs,
|
|
440
|
+
};
|
|
441
|
+
sendServerFrame(conn, helloAck);
|
|
429
442
|
logger?.info?.("device_connected", { deviceId: row.deviceId, generation: acquired.generation, epoch: frame.epoch, replicaId });
|
|
430
443
|
metrics?.inc("device_connected_total");
|
|
431
444
|
}
|
|
@@ -469,12 +482,7 @@ export function createDeviceWsHub(options) {
|
|
|
469
482
|
}
|
|
470
483
|
}
|
|
471
484
|
function failOnChunkGap(p) {
|
|
472
|
-
|
|
473
|
-
settle(p, {
|
|
474
|
-
ok: false,
|
|
475
|
-
errorCode: "outcome_unknown",
|
|
476
|
-
message: `the output stream had a gap (missing chunk streamSeq ${missing}) so the result cannot be reported faithfully — ${OUTCOME_UNKNOWN_SENTENCE}`,
|
|
477
|
-
});
|
|
485
|
+
settle(p, outcomeUnknown(`the output stream had a gap (missing chunk streamSeq ${p.nextStreamSeq}) so the result cannot be reported faithfully`));
|
|
478
486
|
}
|
|
479
487
|
function chunkPayloadBytes(frame) {
|
|
480
488
|
return frame.dataB64 === undefined ? 0 : Math.floor((frame.dataB64.length * 3) / 4);
|
|
@@ -549,11 +557,11 @@ export function createDeviceWsHub(options) {
|
|
|
549
557
|
if (complaint !== null) {
|
|
550
558
|
metrics?.inc("device_stream_terminator_missing_total");
|
|
551
559
|
sendServerFrame(conn, { t: "resultAck", gen: conn.genStr, instructionId: p.instructionId });
|
|
552
|
-
settle(p,
|
|
560
|
+
settle(p, outcomeUnknown(complaint));
|
|
553
561
|
return;
|
|
554
562
|
}
|
|
555
563
|
sendServerFrame(conn, { t: "resultAck", gen: conn.genStr, instructionId: p.instructionId });
|
|
556
|
-
settle(p, outcome);
|
|
564
|
+
settle(p, !outcome.ok && outcome.errorCode === "outcome_unknown" ? outcomeUnknown(outcome.message) : outcome);
|
|
557
565
|
}
|
|
558
566
|
function onDeviceText(conn, text) {
|
|
559
567
|
conn.lastFrameAt = Date.now();
|
|
@@ -235,6 +235,21 @@ export type DeviceHelloAckFrame = {
|
|
|
235
235
|
gen: string;
|
|
236
236
|
pendingInstructions: string[];
|
|
237
237
|
chunkWatermarks: Record<string, number>;
|
|
238
|
+
/**
|
|
239
|
+
* 四个**宽限位**(additive,协议 §5.6 不升版):本连接所在 server 副本**此刻生效**的四只预算,值与汇聚端
|
|
240
|
+
* 计时器读的是同一只常量 —— 不是文档里的「默认值」,是这台部署的实际值。executor 据此定自己的重连
|
|
241
|
+
* 退避上界 / 取消应答时限 / 补片节奏 / 执行兜底预算,不必抄文档里的数。
|
|
242
|
+
* · `reconnectGraceMs`:断连后在途不判丢的窗(R4;运维旋钮 `DEVICE_RECONNECT_GRACE_MS`);
|
|
243
|
+
* · `cancelGraceMs`:`cancel` 送达后等终局回执的窗(R11);
|
|
244
|
+
* · `chunkGapTimeoutMs`:等缺失 `streamSeq` 的上限(R8);
|
|
245
|
+
* · `executionCeilingGraceMs`:`timeoutMs` 之后云侧兜底再等的宽限(R12:`timeoutMs` + 本值)。
|
|
246
|
+
* **可选位**:7.120+ 服务端恒发;缺席 = 老服务端,按协议 §1.5 回落文档缺省(不得当 0)。型上标可选,是为了让拿这只型读老服务端帧的
|
|
247
|
+
* 消费方被编译器逼着处理缺席;本服务自己的铸点按 `Required<DeviceHelloAckFrame>` 构造(`device-ws-hub.ts`),漏一位编译红。
|
|
248
|
+
*/
|
|
249
|
+
reconnectGraceMs?: number;
|
|
250
|
+
cancelGraceMs?: number;
|
|
251
|
+
chunkGapTimeoutMs?: number;
|
|
252
|
+
executionCeilingGraceMs?: number;
|
|
238
253
|
};
|
|
239
254
|
export type DeviceHelloRejectFrame = {
|
|
240
255
|
t: "helloReject";
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -69,6 +69,10 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
69
69
|
write_face_damaged: false,
|
|
70
70
|
skill_grants_damaged: false,
|
|
71
71
|
constraint_chain_missing: false,
|
|
72
|
+
hands_coordinates_damaged: false,
|
|
73
|
+
installation_record_absent: true,
|
|
74
|
+
installation_record_misaligned: false,
|
|
75
|
+
installation_record_invalid: false,
|
|
72
76
|
};
|
|
73
77
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
74
78
|
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
@@ -161,6 +161,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
161
161
|
approvalDecisionNote: true,
|
|
162
162
|
approvalDecidedBy: true,
|
|
163
163
|
permissionModes: [...PERMISSION_MODES],
|
|
164
|
+
sessionReadRedaction: true,
|
|
164
165
|
sideQuery: true,
|
|
165
166
|
agentRoster: facts.agentRoster(deps).available,
|
|
166
167
|
retention: retentionAdvertisement(deps.config),
|
|
@@ -2,7 +2,7 @@ import { sendError, sendJson } from "../send.js";
|
|
|
2
2
|
import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
3
|
import { assertNever } from "../route-ctx.js";
|
|
4
4
|
import * as facts from "../../capability-facts.js";
|
|
5
|
-
import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, sameDeviceOwner, } from "../../device-store.js";
|
|
5
|
+
import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, isDeviceAbsolutePath, sameDeviceOwner, } from "../../device-store.js";
|
|
6
6
|
import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
|
|
7
7
|
import { deviceOwnerDomainOf } from "../device-placement.js";
|
|
8
8
|
import { echoKeyName } from "../../request-key-closure.js";
|
|
@@ -109,8 +109,8 @@ function parseEnrollBody(res, body) {
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
if (workspaceRoot
|
|
113
|
-
sendError(res, 400, "request.field_invalid", "workspaceRoot
|
|
112
|
+
if (!isDeviceAbsolutePath(workspaceRoot)) {
|
|
113
|
+
sendError(res, 400, "request.field_invalid", "workspaceRoot must be an absolute POSIX path (it is the device-side root every instruction resolves against)");
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
return { token: token, pubkey: pubkey, platformOs: os, platformArch: platformArch, displayName: displayName, workspaceRoot: workspaceRoot };
|
|
@@ -4,10 +4,10 @@ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sen
|
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
5
|
import * as facts from "../../capability-facts.js";
|
|
6
6
|
import { createLogger } from "../../observability/logger.js";
|
|
7
|
-
import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
7
|
+
import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
8
8
|
import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
|
|
9
9
|
import { HttpError } from "../../security.js";
|
|
10
|
-
import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
|
|
10
|
+
import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
|
|
11
11
|
import { PERSISTED_RULE_MATCHES, isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
|
|
12
12
|
const logger = createLogger();
|
|
13
13
|
export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
@@ -26,7 +26,7 @@ const CcImportPrepareBodySchema = z
|
|
|
26
26
|
.object({
|
|
27
27
|
layer: z.enum(["userSettings", "projectSettings", "localSettings"]),
|
|
28
28
|
path: z.string().min(1).max(1024),
|
|
29
|
-
root: z.string().
|
|
29
|
+
root: z.string().max(1024),
|
|
30
30
|
content: z.string().max(MAX_LAYER_CONTENT_CHARS),
|
|
31
31
|
})
|
|
32
32
|
.strict())
|
|
@@ -34,6 +34,9 @@ const CcImportPrepareBodySchema = z
|
|
|
34
34
|
.max(MAX_LAYERS),
|
|
35
35
|
})
|
|
36
36
|
.strict();
|
|
37
|
+
function ccImportRootRefused(layer) {
|
|
38
|
+
return !isValidDurableScope({ kind: "project", root: layer.root });
|
|
39
|
+
}
|
|
37
40
|
const TICKET_CONTROL_CHARS_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
|
|
38
41
|
const TicketStringSchema = z.string().min(1).max(190).refine((t) => !TICKET_CONTROL_CHARS_RE.test(t));
|
|
39
42
|
const CcImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
|
|
@@ -170,6 +173,10 @@ export async function handleRules(req, res, match, ctx) {
|
|
|
170
173
|
sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
|
|
171
174
|
return;
|
|
172
175
|
}
|
|
176
|
+
if (parsed.data.layers.some(ccImportRootRefused)) {
|
|
177
|
+
sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
173
180
|
const prepared = await lane.prepareImport(principal, parsed.data.layers);
|
|
174
181
|
if (!prepared.ok) {
|
|
175
182
|
sendError(res, 413, "request.payload_too_large", "too many allow entries across the submitted settings layers");
|
|
@@ -231,7 +238,7 @@ async function handleRuleList(res, search, caller, lane, config) {
|
|
|
231
238
|
const scopeRaw = q.get("scope");
|
|
232
239
|
const scopeFilter = scopeRaw === null ? undefined : parseRuleScope(scopeRaw);
|
|
233
240
|
if (scopeRaw !== null && scopeFilter === undefined) {
|
|
234
|
-
sendError(res, 400, "request.query_invalid",
|
|
241
|
+
sendError(res, 400, "request.query_invalid", RULE_SCOPE_REFUSAL_SENTENCE);
|
|
235
242
|
return;
|
|
236
243
|
}
|
|
237
244
|
const cursorRaw = q.get("cursor");
|
|
@@ -305,7 +312,7 @@ async function handleRuleWrite(res, rawBody, caller, lane, config) {
|
|
|
305
312
|
}
|
|
306
313
|
const scope = parseRuleScope(parsed.data.scope);
|
|
307
314
|
if (scope === undefined) {
|
|
308
|
-
sendError(res, 400, "request.body_shape",
|
|
315
|
+
sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
|
|
309
316
|
return;
|
|
310
317
|
}
|
|
311
318
|
const target = resolveTargetPrincipal(parsed.data.principal, caller, config);
|
|
@@ -354,7 +361,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, config) {
|
|
|
354
361
|
}
|
|
355
362
|
const scope = parseRuleScope(parsed.data.scope);
|
|
356
363
|
if (scope === undefined) {
|
|
357
|
-
sendError(res, 400, "request.body_shape",
|
|
364
|
+
sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
|
|
358
365
|
return;
|
|
359
366
|
}
|
|
360
367
|
const target = resolveTargetPrincipal(parsed.data.principal, caller, config);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **机读位的值** —— 通告 `detail` 里的规则文本(`permission.allowed_tool_not_granted` 的 `detail.entries[].entry`、`permission.rule_row_dropped` 的
|
|
3
|
+
* `detail.rows[].rule`)在会话流行与运维日志行上一律是这一只:条目的脱敏原值。显示形不进机读位(壳若再加引号就是两层)。
|
|
4
|
+
*/
|
|
5
|
+
export declare function redactListEntry(entry: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* **人读句里条目的唯一写法** —— 本仓把规则条目 / 工具名原串写进任何一句给人读的话(拒句、起服拒启行)一律调这一只:引擎的条目写者
|
|
8
|
+
* `renderListEntry`(core 7.46.0 #1428 包根导出:JSON 串形带引号,看不见 / 印成空白的字符写成 `\uXXXX`,超长截断)—— 与引擎自己的通告句同一个渲染;
|
|
9
|
+
* 判定拿原串。
|
|
10
|
+
* 🔴 凭据先认、再渲染:渲染把看不见的字符写成 ASCII 转义,之后的脱敏器就认不出夹着零宽字符的凭据 ⇒ 原串上认出凭据时渲染它的脱敏值;
|
|
11
|
+
* 没有凭据的条目渲染**原串**(格式字符留给渲染器写成 `\uXXXX` —— 人要看得见它,不能先被脱敏器剥掉)。
|
|
12
|
+
*/
|
|
13
|
+
export declare function displayListEntry(entry: string): string;
|
|
14
|
+
//# sourceMappingURL=list-entry-display.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { renderListEntry } from "@sema-agent/core";
|
|
2
|
+
import { redactSecretsWithArms } from "./trace/redact.js";
|
|
3
|
+
function scanListEntry(entry) {
|
|
4
|
+
const scan = redactSecretsWithArms(entry);
|
|
5
|
+
return { redacted: scan.text, credential: scan.arms.size > 0 };
|
|
6
|
+
}
|
|
7
|
+
export function redactListEntry(entry) {
|
|
8
|
+
return scanListEntry(entry).redacted;
|
|
9
|
+
}
|
|
10
|
+
export function displayListEntry(entry) {
|
|
11
|
+
const scan = scanListEntry(entry);
|
|
12
|
+
return renderListEntry(scan.credential ? scan.redacted : entry);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=list-entry-display.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_TABLE, isNamespacedCoveringRuleName, judgeRuleToolName, parsePermissionRule, protocolOf } from "@sema-agent/core";
|
|
2
|
-
import {
|
|
2
|
+
import { displayListEntry } from "./list-entry-display.js";
|
|
3
3
|
export function ruleEntryToolName(entry) {
|
|
4
4
|
return parsePermissionRule(entry).toolName;
|
|
5
5
|
}
|
|
@@ -11,10 +11,6 @@ export function unmatchableRuleNameOf(name) {
|
|
|
11
11
|
const ns = emptyToolSegmentOf(name);
|
|
12
12
|
return ns === undefined ? undefined : { kind: "empty_tool_segment", ns };
|
|
13
13
|
}
|
|
14
|
-
function presentName(name) {
|
|
15
|
-
const clean = name.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
16
|
-
return clean.length <= 160 ? clean : `${clean.slice(0, 160)}…(${name.length} chars)`;
|
|
17
|
-
}
|
|
18
14
|
export function describeUnmatchableRuleName(v, name) {
|
|
19
15
|
switch (v.kind) {
|
|
20
16
|
case "retired":
|
|
@@ -22,7 +18,7 @@ export function describeUnmatchableRuleName(v, name) {
|
|
|
22
18
|
case "unmintable_segment":
|
|
23
19
|
return v.guidance;
|
|
24
20
|
case "unprefixed_namespace":
|
|
25
|
-
return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${
|
|
21
|
+
return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => displayListEntry(`${e.prefix}${name}`)).join(" or ")}`;
|
|
26
22
|
case "empty_tool_segment":
|
|
27
23
|
return describeEmptyToolSegment(v.ns);
|
|
28
24
|
default: {
|
|
@@ -394,6 +394,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
394
394
|
readonly cls: "F";
|
|
395
395
|
readonly note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。";
|
|
396
396
|
};
|
|
397
|
+
readonly "server.config-lkg.landed-path-unresolved": {
|
|
398
|
+
readonly cls: "P-DEBT";
|
|
399
|
+
readonly note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。";
|
|
400
|
+
};
|
|
397
401
|
};
|
|
398
402
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
399
403
|
export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
@@ -367,6 +367,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
367
367
|
cls: "F",
|
|
368
368
|
note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。",
|
|
369
369
|
},
|
|
370
|
+
"server.config-lkg.landed-path-unresolved": {
|
|
371
|
+
cls: "P-DEBT",
|
|
372
|
+
note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。",
|
|
373
|
+
},
|
|
370
374
|
};
|
|
371
375
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
372
376
|
switch (frameType) {
|
package/dist/operator-ask.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare function operatorAsk(rule: string, message: string): OperatorAskR
|
|
|
68
68
|
export declare function operatorAlwaysHumanAsk(rule: string, message: string): OperatorAskResult;
|
|
69
69
|
/**
|
|
70
70
|
* 🔴 **S-958 —— 治理层的叶策略包装**:`leaf` 产的每一只 ask 盖部署出身(经 {@link withDeploymentAuthorship}),allow / deny 原物交回;
|
|
71
|
-
* 成员(`projection` / `
|
|
71
|
+
* 成员(`projection` / `pathRules` / `writeTargetGuard` / `nameSets`)经 core `wrapToolPolicy` 随行(契约「包装至少拒内层所拒」:
|
|
72
72
|
* 只加一位、从不放宽)。同步叶保持同步返回;叶抛照抛(fail-closed 的属主仍是叶)。
|
|
73
73
|
*
|
|
74
74
|
* **只收叶**:治理层(`runtime-governance.ts` `applyRuntimeGovernance`)对每一片叶各盖一次,再交 core `combinePolicies` 折**一次**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
+
import { type UndurableScopeRuleRow, type UnreadableRuleBucket } from "./permission-rule-store-sql.js";
|
|
3
4
|
/** 迁移读数(启动日志一行写全;测试按它断言,不靠日志文本)。 */
|
|
4
5
|
export interface OneTimeMigrationReport {
|
|
5
6
|
/** M1:退役快照索引表 —— `dropped` = 本次真 drop 了,`absent` = 早已不在(第二跑/全新库)。 */
|
|
@@ -33,6 +34,13 @@ export interface OneTimeMigrationReport {
|
|
|
33
34
|
readonly workflowParkIndexKey: "reshaped" | "current" | "file-ledger";
|
|
34
35
|
/** M9:改形时原样搬进新表的指针行数(`current` / `file-ledger` 时恒 0)。非零 = 这次迁移真重建了一张表,运维该看见一次。 */
|
|
35
36
|
readonly workflowParkIndexRowsCarried: number;
|
|
37
|
+
/** M11:本次从持久规则桶里删掉的、scope 不被引擎 durable 谓词收的行(一行一条,启动日志逐行 warn;local 车道恒空,见头注 M11 段)。 */
|
|
38
|
+
readonly undurableScopeRuleRows: readonly UndurableScopeRuleRow[];
|
|
39
|
+
/** M11:连续争不过在线写者、留到下一次起服的桶数(那期间坏行照旧由引擎读侧丢并告知;非零 ⇒ warn)。 */
|
|
40
|
+
readonly undurableScopeRuleBucketsDeferred: number;
|
|
41
|
+
/** M11:读腿整桶拒的桶(唯一判官判不过:形 / 指纹 / behavior 闭集 / 墓碑规范文本 / 单一验证器)—— 一个字节没动,一桶一条拒因
|
|
42
|
+
* (不带行内容),启动日志逐桶 warn;local 车道恒空(见头注 M11 段)。 */
|
|
43
|
+
readonly undurableScopeRuleBucketsUnreadable: readonly UnreadableRuleBucket[];
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
|
|
@@ -5,6 +5,7 @@ import { ensureIndexes } from "./ensure-index.js";
|
|
|
5
5
|
import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn } from "./sql-json-column.js";
|
|
6
6
|
import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
|
|
7
7
|
import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
8
|
+
import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
|
|
8
9
|
export const ONE_TIME_MIGRATIONS = [
|
|
9
10
|
{
|
|
10
11
|
id: "M1",
|
|
@@ -133,6 +134,21 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
133
134
|
"已存的规则对写下它们的 principal 的 run 照旧生效;出路 = 删会话(`DELETE /v1/sessions/:id`,规则随之删)",
|
|
134
135
|
reportKeys: [],
|
|
135
136
|
},
|
|
137
|
+
{
|
|
138
|
+
id: "M11",
|
|
139
|
+
introducedIn: "7.120.0",
|
|
140
|
+
releasedOn: "2026-10-08",
|
|
141
|
+
removableAfter: "2026-11-07",
|
|
142
|
+
removal: "删 migrateOn 的 M11 步(`permission_rule` 在场探针 + `purgeUndurableScopeRows` 调用)与 `permission-rule-store-sql.ts` 的 `purgeUndurableScopeRows` / " +
|
|
143
|
+
"`purgeAllBuckets` / `purgeOneBucket` / `contentFreeRefusal` / `PurgeAccumulator` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurge` / " +
|
|
144
|
+
"`UndurableScopePurgeDb` / `UNDURABLE_PURGE_PAGE` / `UNDURABLE_PURGE_OCC_ATTEMPTS`、本文件的 `removedRuleRowNote`,以及 `BucketRefusedError` 的 `verdict` 位(若届时再无别的读者;" +
|
|
145
|
+
"判官 `decodeBucketRow` 与写法 `writeBucket` 是读腿 / 写臂本身,留);报告、日志行(含逐行的 `permission_rule_row_removed` 与逐桶的 " +
|
|
146
|
+
"`permission_rule_bucket_unreadable`)与 `NO_SQL_MIGRATIONS` 的 `undurableScopeRuleRows` / `undurableScopeRuleBucketsDeferred` / `undurableScopeRuleBucketsUnreadable`",
|
|
147
|
+
afterRemoval: "引擎读侧的既有响亮臂:scope root 不是完整绝对路径的存量行(跳过了带本条的版本直接升的库,或窗关之后别的写者写进来的)在有效视图里被丢、" +
|
|
148
|
+
"每次 run 一条 `permission.rule_row_dropped`(点名规则与 root)—— 那一行不参与任何检查,但撤销口删不动它(引擎的写臂对这种 scope 同样拒);" +
|
|
149
|
+
"出路 = 运维在库里把那只桶的那一行删掉(整桶 OCC 回写,指纹按 `buildRuleBucketChecksum` 重算)",
|
|
150
|
+
reportKeys: ["undurableScopeRuleRows", "undurableScopeRuleBucketsDeferred", "undurableScopeRuleBucketsUnreadable"],
|
|
151
|
+
},
|
|
136
152
|
];
|
|
137
153
|
export function migrationWindowOpen(window, today) {
|
|
138
154
|
return window.removableAfter === null || today <= window.removableAfter;
|
|
@@ -149,6 +165,9 @@ export const NO_SQL_MIGRATIONS = {
|
|
|
149
165
|
allowRuleShadowRowsUnreadable: 0,
|
|
150
166
|
workflowParkIndexKey: "file-ledger",
|
|
151
167
|
workflowParkIndexRowsCarried: 0,
|
|
168
|
+
undurableScopeRuleRows: [],
|
|
169
|
+
undurableScopeRuleBucketsDeferred: 0,
|
|
170
|
+
undurableScopeRuleBucketsUnreadable: [],
|
|
152
171
|
};
|
|
153
172
|
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
154
173
|
const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
|
|
@@ -293,8 +312,16 @@ async function migrateOn(db, dialect) {
|
|
|
293
312
|
let retiredSnapshotTable = "absent";
|
|
294
313
|
let retiredSnapshotRows = 0;
|
|
295
314
|
let retiredSteerSeatColumn = "absent";
|
|
315
|
+
let undurableScopeRuleRows = [];
|
|
316
|
+
let undurableScopeRuleBucketsDeferred = 0;
|
|
317
|
+
let undurableScopeRuleBucketsUnreadable = [];
|
|
296
318
|
const destroyed = [];
|
|
297
319
|
try {
|
|
320
|
+
if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
|
|
321
|
+
({ removed: undurableScopeRuleRows, bucketsDeferred: undurableScopeRuleBucketsDeferred, bucketsUnreadable: undurableScopeRuleBucketsUnreadable } = await purgeUndurableScopeRows(db, dialect));
|
|
322
|
+
for (const r of undurableScopeRuleRows)
|
|
323
|
+
destroyed.push(`removed permission_rule ${r.kind} (bucket ${r.ownerKey}, ${r.behavior} ${r.tool}, scope root ${JSON.stringify(r.scopeRoot)})`);
|
|
324
|
+
}
|
|
298
325
|
const counted = !snapshotTablePresent
|
|
299
326
|
? undefined
|
|
300
327
|
: await db.rows(`SELECT COUNT(*) AS n FROM ${RETIRED_SNAPSHOT_TABLE}`).then((rows) => Number(rows[0]?.n ?? 0), async (e) => {
|
|
@@ -323,7 +350,7 @@ async function migrateOn(db, dialect) {
|
|
|
323
350
|
return {
|
|
324
351
|
retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
|
|
325
352
|
sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
|
|
326
|
-
workflowParkIndexKey, workflowParkIndexRowsCarried,
|
|
353
|
+
workflowParkIndexKey, workflowParkIndexRowsCarried, undurableScopeRuleRows, undurableScopeRuleBucketsDeferred, undurableScopeRuleBucketsUnreadable,
|
|
327
354
|
};
|
|
328
355
|
}
|
|
329
356
|
const WORKFLOW_PARK_INDEX_TABLE = "workflow_agent_session";
|
|
@@ -525,7 +552,8 @@ export function migrateRetiredAskOrigin(type, data) {
|
|
|
525
552
|
export function logOneTimeMigrations(log, backend, report) {
|
|
526
553
|
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
|
|
527
554
|
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
|
|
528
|
-
report.workflowParkIndexKey === "reshaped"
|
|
555
|
+
report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
|
|
556
|
+
report.undurableScopeRuleBucketsUnreadable.length > 0;
|
|
529
557
|
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
530
558
|
backend,
|
|
531
559
|
retiredSnapshotTable: report.retiredSnapshotTable,
|
|
@@ -541,6 +569,9 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
541
569
|
allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
|
|
542
570
|
workflowParkIndexKey: report.workflowParkIndexKey,
|
|
543
571
|
workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
|
|
572
|
+
undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
|
|
573
|
+
undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
|
|
574
|
+
undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
|
|
544
575
|
removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
|
|
545
576
|
note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
|
|
546
577
|
"steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
|
|
@@ -551,10 +582,48 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
551
582
|
"under an earlier reading from resuming under the current one; M8: parked rows' allow-layer disclosure written by an earlier engine " +
|
|
552
583
|
"under its old key shadowedRule rewritten to allowRuleShadowed; M9: the workflow park pointer index rebuilt with the key (session, run), " +
|
|
553
584
|
"its rows carried over; M10 writes nothing at boot — on the local backend, while its window is open, the first task submitted with a principal " +
|
|
554
|
-
"on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim
|
|
585
|
+
"on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim; " +
|
|
586
|
+
"M11: persisted permission rules whose project scope root is not a complete absolute path — accepted by earlier versions, dropped on every read " +
|
|
587
|
+
"by the current engine and not removable through DELETE /v1/rules — removed, one permission_rule_row_removed line per row; a bucket the read path " +
|
|
588
|
+
"refuses in whole is left untouched, one permission_rule_bucket_unreadable line per bucket). Each step " +
|
|
555
589
|
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
556
590
|
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
557
591
|
"version that still carries a step at least once before upgrading past its deletion.",
|
|
558
592
|
});
|
|
593
|
+
for (const row of report.undurableScopeRuleRows) {
|
|
594
|
+
log.warn("permission_rule_row_removed", {
|
|
595
|
+
backend,
|
|
596
|
+
ownerKey: row.ownerKey,
|
|
597
|
+
kind: row.kind,
|
|
598
|
+
behavior: row.behavior,
|
|
599
|
+
tool: row.tool,
|
|
600
|
+
scopeRoot: row.scopeRoot,
|
|
601
|
+
note: removedRuleRowNote(row.kind),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
for (const bucket of report.undurableScopeRuleBucketsUnreadable) {
|
|
605
|
+
log.warn("permission_rule_bucket_unreadable", {
|
|
606
|
+
backend,
|
|
607
|
+
ownerKey: bucket.ownerKey,
|
|
608
|
+
why: bucket.why,
|
|
609
|
+
note: "a persisted permission-rule bucket that the read path refuses in whole was left untouched by the one-time removal of rows whose project scope root " +
|
|
610
|
+
"is not a complete absolute path (removing rows from a bucket the read path refuses would turn that refusal into a readable bucket): every read of it " +
|
|
611
|
+
"keeps being refused, as before this release, until someone repairs or deletes that row by hand",
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function removedRuleRowNote(kind) {
|
|
616
|
+
switch (kind) {
|
|
617
|
+
case "rule":
|
|
618
|
+
return "a persisted permission rule whose project scope root is not a complete absolute path was removed by a one-time migration: the current engine drops such a row on every read (it protected nothing) and refuses any write naming that scope, so it could not be revoked through DELETE /v1/rules; re-add it with the project's full path if it is still wanted";
|
|
619
|
+
case "revoked-rule":
|
|
620
|
+
return "a persisted permission rule whose project scope root is not a complete absolute path, and which had already been revoked (every approval behind it is removed by a tombstone in the same bucket), was removed by a one-time migration: it granted nothing before and grants nothing now — the revocation stands, no action needed";
|
|
621
|
+
case "tombstone":
|
|
622
|
+
return "a revocation record (tombstone) whose project scope root is not a complete absolute path was removed by a one-time migration: it can only cover rules with that same scope, which were removed with it or were never there, so nothing it revoked comes back — no action needed";
|
|
623
|
+
default: {
|
|
624
|
+
const unknown = kind;
|
|
625
|
+
throw new Error(`M11: no log sentence for removed-row kind ${JSON.stringify(unknown)}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
559
628
|
}
|
|
560
629
|
//# sourceMappingURL=one-time-migrations.js.map
|