@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Model, type PathRuleBases, type PermissionRule } from "@sema-agent/core";
|
|
2
|
+
import { type ConfigProviderWord } from "./config-types.js";
|
|
2
3
|
import { type CenterEndpoint } from "./web-admin-client/center-request.js";
|
|
3
4
|
import { type Autonomy } from "./runtime-governance.js";
|
|
4
5
|
import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
|
|
@@ -106,6 +107,19 @@ export interface ConfigNotice {
|
|
|
106
107
|
fields: Record<string, string>;
|
|
107
108
|
}
|
|
108
109
|
export declare function drainConfigNotices(): ConfigNotice[];
|
|
110
|
+
/**
|
|
111
|
+
* [ref] —— `CONFIG_PROVIDER` 的**唯一**读点。闭集 {@link CONFIG_PROVIDER_WORDS};缺席 / 空串 ⇒ 未表态(`undefined`);
|
|
112
|
+
* 其余一律**拒启**,拒因句列合法词。逐字比较(不 trim、不折大小写)—— 与本仓其余闭集 env 同律([ref]:认不出的词
|
|
113
|
+
* 不许被悄悄猜成某一种意思)。
|
|
114
|
+
*
|
|
115
|
+
* 修前这根旋钮是开放词,而一个认不出的词在同一进程里被读成两种意思:配置源判官(`configProviderKind`)把它当 `local`
|
|
116
|
+
* (读 `config.d/`、执行车道合成步照读 `remote-exec.json`),这里的隐式 host 臂与车道落点披露却按字面 `=== "local"`
|
|
117
|
+
* 判它**不是** local;web-admin 工厂同样按字面只在 `"local"` 上递本地根,于是配置源的根落回 `process.cwd()`、
|
|
118
|
+
* 合成步读的是 `localRoot` —— 两个根分家。收成闭集 + 一个读点之后,这三处读的是同一个值。
|
|
119
|
+
* 调用方两处、读的是同一张闭集:`loadConfig` 开头一次;`run-local` 在 `loadConfig` 之前一次(本入口只装本地配置源,词是 `remote` 就在这里拒 ——
|
|
120
|
+
* 配没配中心地址都一样)。
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseConfigProvider(): ConfigProviderWord | undefined;
|
|
109
123
|
/** F10 (codex audit): drain + log BOTH pre-logger diagnostic collectors (S20 soft-knob warnings + [ref]④ boot
|
|
110
124
|
* notices) in one place. EVERY entrypoint that calls loadConfig() then builds a logger must call this right
|
|
111
125
|
* after (main.ts, run-local.ts) — a new entry that forgets it would silently drop operator-facing diagnostics
|
|
@@ -212,6 +226,25 @@ export declare function configKnobTable(): ConfigKnobRecord[];
|
|
|
212
226
|
export declare const hostBackgroundShellEnabled: () => boolean;
|
|
213
227
|
/** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
|
|
214
228
|
export declare const hostExecSpoolEnabled: () => boolean;
|
|
229
|
+
/** 名单在活配置里的落点(服务进程四只旋钮;bake-runner 那只在它自己的进程里,不在 `ServiceConfig`)。 */
|
|
230
|
+
export type EnvNameAllowlistSeats = Pick<ServiceConfig, "lspEnvAllow" | "remoteExec">;
|
|
231
|
+
/**
|
|
232
|
+
* 沙箱名单(表列 `channel: "sandbox-grant"` 的服务侧旋钮,表派生)此刻交出去的名 —— 起服行 `remote_exec_enabled.sandboxEnvKeys` 读它(`boot/execution-env.ts`,
|
|
233
|
+
* 排序后只出键名)。只有被选中车道的那一只有落点;e2b / k8s 报的是名单点到的键,不含 `SANDBOX_PKG_SOURCE` 推导出的包源变量(那一组是区域常量,装配点另并)。
|
|
234
|
+
*/
|
|
235
|
+
export declare function sandboxEnvNamesInForce(seats: EnvNameAllowlistSeats): readonly string[];
|
|
236
|
+
/** 声明增长后重判的一处命中:哪只旋钮、它交出去的哪个名、判官的拒因({@link ownCredentialNameProblem} 那一句,只回显名)。 */
|
|
237
|
+
export interface OwnCredentialInForce {
|
|
238
|
+
readonly knob: ServerEnvNameAllowlistKnob;
|
|
239
|
+
readonly name: string;
|
|
240
|
+
readonly problem: string;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 解析器头注 ② 那条不变量在**声明增长**那一刻的半边:对活配置里每只名单此刻交出去的名问**同一只**判官({@link ownCredentialNameProblem} → 引擎
|
|
244
|
+
* `inheritEnvPolicyProblem`,读引擎此刻的声明)。读活配置的落点,不重读 env(名单是起服输入;解析那一刻已判过形与当时的声明)。返回每一处命中,空 = 不相交。
|
|
245
|
+
* 调用点只有一处 = 声明增长的那一处(`apply-effective.ts` 的换代声明,真声明之后、席位换装之前)。
|
|
246
|
+
*/
|
|
247
|
+
export declare function ownCredentialHitsInForce(seats: EnvNameAllowlistSeats): OwnCredentialInForce[];
|
|
215
248
|
/** 名单型 env 旋钮的**唯一**解析器(规则全文见上方 `ENV_NAME_ALLOWLIST_KNOBS` 头注)。`undefined` = 没配(未设 / 空串)。
|
|
216
249
|
* 服务侧旋钮要交本进程的姿态(③);runner 旋钮没有姿态,签名上就不收 —— 读者与行形由叶表的类型一起定,传错是编译红。 */
|
|
217
250
|
export declare function parseEnvNameAllowlist(knob: ServerEnvNameAllowlistKnob, singleUserTurnkey: boolean): readonly string[] | undefined;
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
|
9
9
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
10
10
|
import { buildRuleListJudgeOptions } from "./permission-rule-list.js";
|
|
11
11
|
import { isV2ScopeKey, memoryReplicationCoverage, replicatesProjectLayersOf } from "./memory-scope.js";
|
|
12
|
+
import { memorySyncPeerOf } from "./memory-sync-client.js";
|
|
12
13
|
import { assertProjectLayerOwnable } from "./project-identity.js";
|
|
13
14
|
import { projectLayerOwnerOf } from "./project-layer-key.js";
|
|
14
15
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
@@ -26,7 +27,7 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
|
|
|
26
27
|
import {} from "./posture-source.js";
|
|
27
28
|
import { COST_QUOTA_WINDOW_SEC_RANGE, costQuotaWindowSecInRange } from "./observability/cost-quota.js";
|
|
28
29
|
import { NAME_TABLE_BEHAVIOR, TOOL_NAME_ROSTER, describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
|
|
29
|
-
import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
|
|
30
|
+
import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, remoteConfigProviderUrlRefusal, retentionLaneRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
|
|
30
31
|
import { declaredSemaConfigDir } from "./sema-config-dir.js";
|
|
31
32
|
import { lkgPathOverrideOf } from "./config-lkg.js";
|
|
32
33
|
import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude, parseWorkspaceSnapshotPrefix } from "./workspace-space.js";
|
|
@@ -35,7 +36,7 @@ import { laneLivenessOf } from "./exec-bounds.js";
|
|
|
35
36
|
import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
|
|
36
37
|
import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_MOUNT_RESERVED, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS, isK8sQuantity, mountsOverlap, reservedMountCollision } from "./workspace-cache.js";
|
|
37
38
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
|
|
38
|
-
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal,
|
|
39
|
+
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, laneMissingEnv } from "./execution-lane-caps.js";
|
|
39
40
|
import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
|
|
40
41
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
41
42
|
import { MAX_NAMED_INERT_ENTRIES, allowLayerEntryCountIssue, describeInertRuleEntries, inertRuleEntriesOf, inertRuleEntryGuidance, ruleEntryListShapeOk } from "./rule-entry-list.js";
|
|
@@ -195,7 +196,7 @@ function buildExecLaneNotice(remoteExec) {
|
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
198
|
const ENV_VALUE_ECHO_CHARS = 120;
|
|
198
|
-
function parseConfigProvider() {
|
|
199
|
+
export function parseConfigProvider() {
|
|
199
200
|
const raw = process.env.CONFIG_PROVIDER;
|
|
200
201
|
if (raw === undefined || raw === "")
|
|
201
202
|
return undefined;
|
|
@@ -509,11 +510,32 @@ function parseNameListEnv(name, raw, opts) {
|
|
|
509
510
|
}
|
|
510
511
|
const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
511
512
|
const ENV_NAME_ALLOWLIST_WITHHELD = "<withheld: entries are not echoed — a value pasted in place of a name can pass the name check>";
|
|
512
|
-
|
|
513
|
+
const OWN_CREDENTIAL_HANDOFF = {
|
|
514
|
+
"host-put-back": { where: "a put-back list hands the named variable back, as written, to the child the engine withheld it from", whom: "that child" },
|
|
515
|
+
"sandbox-grant": { where: "a sandbox list puts the named variable, as written, into a sandbox the model can read", whom: "the sandbox" },
|
|
516
|
+
};
|
|
517
|
+
function ownCredentialNameProblem(name, channel) {
|
|
513
518
|
const engine = inheritEnvPolicyProblem([name]);
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
519
|
+
if (engine === undefined)
|
|
520
|
+
return undefined;
|
|
521
|
+
const { where, whom } = OWN_CREDENTIAL_HANDOFF[channel];
|
|
522
|
+
return `this deployment's own credential — ${where}; give ${whom} a credential of its own (another name and another value) and list that name instead (the engine refuses it on any allow list: ${engine})`;
|
|
523
|
+
}
|
|
524
|
+
const NAMES_IN_FORCE = {
|
|
525
|
+
LSP_ENV_ALLOW: (c) => c.lspEnvAllow,
|
|
526
|
+
E2B_SANDBOX_ENV: (c) => (c.remoteExec?.provider === "e2b" ? Object.keys(c.remoteExec.sandboxEnv ?? {}) : []),
|
|
527
|
+
K8S_SANDBOX_ENV: (c) => (c.remoteExec?.provider === "k8s" ? Object.keys(c.remoteExec.sandboxEnv ?? {}) : []),
|
|
528
|
+
DOCKER_SANDBOX_ENV: (c) => (c.remoteExec?.provider === "local-docker" ? Object.keys(c.remoteExec.env ?? {}) : []),
|
|
529
|
+
};
|
|
530
|
+
const SERVER_ENV_NAME_ALLOWLIST_KNOBS = Object.keys(NAMES_IN_FORCE);
|
|
531
|
+
export function sandboxEnvNamesInForce(seats) {
|
|
532
|
+
return SERVER_ENV_NAME_ALLOWLIST_KNOBS.filter((knob) => ENV_NAME_ALLOWLIST_KNOBS[knob].channel === "sandbox-grant").flatMap((knob) => NAMES_IN_FORCE[knob](seats));
|
|
533
|
+
}
|
|
534
|
+
export function ownCredentialHitsInForce(seats) {
|
|
535
|
+
return SERVER_ENV_NAME_ALLOWLIST_KNOBS.flatMap((knob) => NAMES_IN_FORCE[knob](seats).flatMap((name) => {
|
|
536
|
+
const problem = ownCredentialNameProblem(name, ENV_NAME_ALLOWLIST_KNOBS[knob].channel);
|
|
537
|
+
return problem === undefined ? [] : [{ knob, name, problem }];
|
|
538
|
+
}));
|
|
517
539
|
}
|
|
518
540
|
export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
519
541
|
const raw = process.env[knob];
|
|
@@ -528,9 +550,7 @@ export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
|
528
550
|
? 'not an environment-variable name (letters, digits and "_", not starting with a digit)'
|
|
529
551
|
: ENV_NAME_ALLOWLIST_KNOB_NAMES.has(seg.toUpperCase())
|
|
530
552
|
? `the name of a name-list knob (${[...ENV_NAME_ALLOWLIST_KNOB_NAMES].join(" / ")}: their raw text is withheld from every child process and sandbox; naming one would put that text back)`
|
|
531
|
-
: spec.channel
|
|
532
|
-
? ownCredentialPutBackProblem(seg)
|
|
533
|
-
: undefined;
|
|
553
|
+
: ownCredentialNameProblem(seg, spec.channel);
|
|
534
554
|
if (problem === undefined)
|
|
535
555
|
return;
|
|
536
556
|
throw new Error(`env ${knob}: entry #${i + 1} of ${segments.length} is ${problem}` +
|
|
@@ -1704,6 +1724,7 @@ function parseMemoryDomain(ctx) {
|
|
|
1704
1724
|
if (memorySyncToken === undefined) {
|
|
1705
1725
|
throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
|
|
1706
1726
|
}
|
|
1727
|
+
memorySyncPeerOf(memorySyncUrl);
|
|
1707
1728
|
if (memorySyncScopeRaw === undefined && memoryScope !== undefined && !isV2ScopeKey(memoryScope)) {
|
|
1708
1729
|
throw new Error(`MEMORY_SYNC_URL is set but MEMORY_SYNC_SCOPE is not set and MEMORY_SCOPE (${JSON.stringify(memoryScope)}) is not a typed scope key — the sync leg replicates exactly ONE scope by name ` +
|
|
1709
1730
|
"(core syncMemoryScope takes the entries whose scope === the sync scope and POSTs them under that same name), so deriving `user:<MEMORY_SCOPE>` would name a scope this deployment never writes and nothing would ever be synced (S-964). " +
|
|
@@ -1902,10 +1923,10 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1902
1923
|
throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")}. ` +
|
|
1903
1924
|
`An unrecognized value used to fall through to the host/in-process lane silently; refusing to start instead (#157).`);
|
|
1904
1925
|
}
|
|
1905
|
-
const missing =
|
|
1926
|
+
const missing = laneMissingEnv(word, process.env);
|
|
1906
1927
|
if (missing.length > 0) {
|
|
1907
|
-
throw new Error(`REMOTE_EXEC=${lane} but required env ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"}
|
|
1908
|
-
`refusing to start rather than silently downgrading to the host/in-process lane (#157; unset REMOTE_EXEC if you meant no sandbox lane).`);
|
|
1928
|
+
throw new Error(`REMOTE_EXEC=${lane} but required env ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} unset, empty or whitespace-only — it counts as absent: ` +
|
|
1929
|
+
`refusing to start rather than silently downgrading to the host/in-process lane (#157; set ${missing.length === 1 ? "it" : "them"}, or unset REMOTE_EXEC if you meant no sandbox lane).`);
|
|
1909
1930
|
}
|
|
1910
1931
|
}
|
|
1911
1932
|
}
|
|
@@ -1951,7 +1972,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1951
1972
|
orphanReclaim: boolEnv("E2B_ORPHAN_RECLAIM", true),
|
|
1952
1973
|
}
|
|
1953
1974
|
:
|
|
1954
|
-
process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE
|
|
1975
|
+
process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE && process.env.K8S_NAMESPACE
|
|
1955
1976
|
? {
|
|
1956
1977
|
provider: "k8s",
|
|
1957
1978
|
image: process.env.K8S_SANDBOX_IMAGE,
|
|
@@ -1959,7 +1980,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1959
1980
|
...(process.env.K8S_TOKEN ? { token: process.env.K8S_TOKEN } : {}),
|
|
1960
1981
|
...(process.env.K8S_CA_CERT_B64 ? { caCert: Buffer.from(process.env.K8S_CA_CERT_B64, "base64").toString("utf8") } : {}),
|
|
1961
1982
|
...(boolEnv("K8S_INSECURE_TLS", false) ? { insecureTls: true } : {}),
|
|
1962
|
-
|
|
1983
|
+
namespace: process.env.K8S_NAMESPACE,
|
|
1963
1984
|
...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
|
|
1964
1985
|
...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
|
|
1965
1986
|
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
|
|
@@ -2602,6 +2623,14 @@ export function loadConfig() {
|
|
|
2602
2623
|
if (centerIdentity !== undefined) {
|
|
2603
2624
|
throw new Error(centerIdentity);
|
|
2604
2625
|
}
|
|
2626
|
+
const remoteSourceRefusal = remoteConfigProviderUrlRefusal({ configProvider: integrations.configProvider, webAdmin: integrations.webAdmin });
|
|
2627
|
+
if (remoteSourceRefusal !== undefined) {
|
|
2628
|
+
throw new Error(remoteSourceRefusal);
|
|
2629
|
+
}
|
|
2630
|
+
const retentionRefusal = retentionLaneRefusal({ retentionPolicy: auth.retentionPolicy, retentionSweep: auth.retentionSweep });
|
|
2631
|
+
if (retentionRefusal !== undefined) {
|
|
2632
|
+
throw new Error(retentionRefusal);
|
|
2633
|
+
}
|
|
2605
2634
|
const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
|
|
2606
2635
|
const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, operatorSingleUser, authTokens } = auth;
|
|
2607
2636
|
const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* M23 回填了无主的父)⇒ 写下的仍是判断时的读数 —— 按律如此;那一行若因此无主,照上一节处置。
|
|
39
39
|
* · **停驻挂点**(`plugins/session-store.ts` 的 `ensureChildSessionDurableWithPromotion`;快路 + 晋升):来源 = **调用方交出的委派树根,只有这一只**,连同
|
|
40
40
|
* 调用方判断那一刻读到的根行住客({@link DelegationRootWitness};见证值,判断用,不进 CAS):请求腿 = 本请求经鉴权解析的会话,住客 = 受理时那一读
|
|
41
|
-
* (`boot/resolve-spec.ts`);`/decide` 的赎回腿 = 行上的 `rootSessionId`,住客 =
|
|
41
|
+
* (`boot/resolve-spec.ts`);`/decide` 的赎回腿 = 行上的 `rootSessionId`,住客 = 赎回腿写口放行的那一位(认领之前解析器那一读;`parked-revive-surface.ts`,每一行都交)。
|
|
42
42
|
* 挂点先读一次根行:它在而住客不是见证的那一位 ⇒ 根被删、同 id 换了住客 ⇒ **拒**(不按新住客晋升 / 认领,{@link assertRootStillWitnessed});它不在 ⇒ 律给哨兵
|
|
43
43
|
* (根真没了)。不回落父:core 的挂点 `ensureChildSessionDurable(sessionId)` 只交子代 id,父 id 在这一写点拿不到;两条候选读法都读不到东西 ——
|
|
44
44
|
* ① 读 `session_meta(子代).placement_parent_session_id`:晋升的拷贝分支恰恰只在子代**没有** placed 行时才走(有 placed 行 ⇒ 快路),这一列按构造为空;
|
|
@@ -134,7 +134,7 @@ export interface PlacedRowWriter {
|
|
|
134
134
|
export declare function createPlacedSessionRow(sessionId: string, placement: PlacementSources, io: PlacedRowWriter, log: OrphanOwnerLog): Promise<void>;
|
|
135
135
|
/**
|
|
136
136
|
* 停驻挂点的**见证**:调用方交出的委派树根 + 调用方判断那一刻读到的根行住客(化身;`null` = 那一读里根行不在)。请求腿 = 鉴权解析的会话与受理时那一读;
|
|
137
|
-
* 赎回腿 =
|
|
137
|
+
* 赎回腿 = 行上的根与写口放行的那一位住客(认领之前解析器那一读)。
|
|
138
138
|
*/
|
|
139
139
|
export interface DelegationRootWitness {
|
|
140
140
|
readonly sessionId: string;
|
|
@@ -92,7 +92,7 @@ export declare const DEPLOYMENT_SHAPE_PREDICATES: Readonly<{
|
|
|
92
92
|
* 签发方配置是身份验签轴、不算租户证据)—— 两个问题、一根轴、一个读口;「鉴权必需」与「多租形」拆成两个声明时只改这一行的判官。
|
|
93
93
|
*/
|
|
94
94
|
multiTenant: DeploymentShapePredicate<"principal">;
|
|
95
|
-
/** 读能力表 `callerCwd` 位(host = 单主才开;device =
|
|
95
|
+
/** 读能力表 `callerCwd` 位(host = 单主才开;device = 路径落在发起者自己的设备上,归属由根会话行执行格上的设备绑定验过,不要求单主)。 */
|
|
96
96
|
callerCwd: DeploymentShapePredicate<"lane" | "principal">;
|
|
97
97
|
/** 调用方给的宿主绝对路径(附加目录 / 项目上下文 / 本机 SendUserFile / 指令文件探针)只在「手就在本 worker 的盘上 ∧ 单主」时指向调用方自己的东西。 */
|
|
98
98
|
callerOwnsWorkerDisk: DeploymentShapePredicate<"lane" | "principal">;
|
|
@@ -18,12 +18,21 @@
|
|
|
18
18
|
* 上是**同一个** `device.enrollment_invalid`;设备读面的「不存在」与「不是你的」是同一个
|
|
19
19
|
* `not_found.device`。细分只进审计。
|
|
20
20
|
*
|
|
21
|
+
* ── 绑定从哪读(一只读)─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
* 根会话 ↔ 设备的绑定是根会话行上这一位住客的执行格 writer(device, D)(`execution-placement.ts` 头注)。准入链**不另读**:
|
|
23
|
+
* 读只有会话店的执行席 `seat.read(root)` 一只({@link DeviceEnrollment.readPlacement},装配点把会话店的执行席交进来 ——
|
|
24
|
+
* device 车道只在 SQL 后端 ⇒ 席恒在,缺 ⇒ 装配期响亮抛);{@link DeviceEnrollment.admitSession} 判的是调用方交进来的**那一读**,
|
|
25
|
+
* 于是提交意图「判已绑 / 未绑」与它记下的住客出自同一次行读。「这一格放在哪台设备上」只有一种读法 `boundDeviceOf`。
|
|
26
|
+
*
|
|
21
27
|
* ── 吊销在途语义(§8-R4)──────────────────────────────────────────────────────────────────────────
|
|
22
28
|
* `revoked` 是终态:①绑定的 session 后续提交 = `device.revoked`(admitSession 步 2,**先于** presence
|
|
23
29
|
* 判定);②吊销同事务把连接租约作废并把 `conn_generation` 推进一格 ⇒ 在途连接的每一次带世代谓词的写
|
|
24
30
|
* 都会影响 0 行,设备侧因此立刻失权(§4.6 步 2);③重新 enrollment = 新 deviceId,不复活旧行。
|
|
25
31
|
*/
|
|
26
|
-
import { type DeviceErrorCode, type DeviceOwner, type DeviceRejectCode, type DeviceEnrollTokenRow, type DeviceRow, type
|
|
32
|
+
import { type DeviceErrorCode, type DeviceOwner, type DeviceRejectCode, type DeviceEnrollTokenRow, type DeviceRow, type DeviceStore, type NewDeviceInput } from "./device-store.js";
|
|
33
|
+
import { type ExecutionDeviceCell, type ExecutionPlacementReading, type ExecutionPlacementSeat } from "./execution-placement.js";
|
|
34
|
+
import type { ExecutionLane } from "./execution-lane-caps.js";
|
|
35
|
+
import type { SessionFace } from "./session-incarnation.js";
|
|
27
36
|
/** typed 拒绝 —— 端点层只负责把它翻成 HTTP(status 已在体内,不许调用方另算)。 */
|
|
28
37
|
export interface DeviceRejection {
|
|
29
38
|
/** [ref]:含两个借用位(`limit.rate_exceeded` 429 / `draining` 503);enrollment 本文件只铸前者,
|
|
@@ -68,10 +77,34 @@ export interface EnrollDeviceValue {
|
|
|
68
77
|
}
|
|
69
78
|
export interface AdmitSessionInput {
|
|
70
79
|
rootSessionId: string;
|
|
80
|
+
/** 调用方读的**那一读**(会话店执行席 `seat.read(rootSessionId)` 的答案,经 {@link DeviceEnrollment.readPlacement})—— 准入链只判它,不另读。 */
|
|
81
|
+
placement: ExecutionPlacementReading | undefined;
|
|
71
82
|
requester: DeviceOwner;
|
|
72
83
|
/** 提交体携带的 deviceId(§4.3.2:仅根任务携;缺席 = 沿用绑定)。 */
|
|
73
84
|
requestedDeviceId?: string;
|
|
74
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* 首绑({@link DeviceEnrollment.bindPlacement})的结果:成立(`device` = 落下的格)、设备闭集的 typed 拒(异设备 `device.claim_conflict` /
|
|
88
|
+
* 设备 404 / 已吊销),或会话行那一侧的三种结局 —— 住客不是判过的那一位 / 行不在(`gone`)、写者在别处(`mirror`)、格在别的车道上(`tree_bound`)。
|
|
89
|
+
* 后三者不在设备闭集里(它们说的是会话,不是设备),由提交点(`http/execution-context.ts` 的 `commitExecutionContext`)各答各的码。
|
|
90
|
+
*/
|
|
91
|
+
export type BindPlacementOutcome = {
|
|
92
|
+
readonly ok: true;
|
|
93
|
+
readonly device: ExecutionDeviceCell;
|
|
94
|
+
} | {
|
|
95
|
+
readonly ok: false;
|
|
96
|
+
readonly reject: DeviceRejection;
|
|
97
|
+
} | {
|
|
98
|
+
readonly ok: false;
|
|
99
|
+
readonly placement: "gone";
|
|
100
|
+
} | {
|
|
101
|
+
readonly ok: false;
|
|
102
|
+
readonly placement: "mirror";
|
|
103
|
+
} | {
|
|
104
|
+
readonly ok: false;
|
|
105
|
+
readonly placement: "tree_bound";
|
|
106
|
+
readonly boundLane: ExecutionLane;
|
|
107
|
+
};
|
|
75
108
|
export interface AdmitSessionValue {
|
|
76
109
|
deviceId: string;
|
|
77
110
|
device: DeviceRow;
|
|
@@ -110,11 +143,15 @@ export interface DeviceEnrollment {
|
|
|
110
143
|
/** §7-T5 五件的第 ①②③ 件的**写入侧**:绑定复合身份 + TTL + 一次性摘要落库(明文一现)。 */
|
|
111
144
|
issueEnrollToken(input: IssueEnrollTokenRequest): Promise<DeviceOutcome<IssuedEnrollToken>>;
|
|
112
145
|
enroll(input: EnrollDeviceInput): Promise<DeviceOutcome<EnrollDeviceValue>>;
|
|
146
|
+
/** 绑定的唯一读:会话店执行席的 `read(root)`(一次行读:住客 + 格;行不在 ⇒ `undefined`;读故障抛,不吞成「未绑」)。 */
|
|
147
|
+
readPlacement(rootSessionId: string): Promise<ExecutionPlacementReading | undefined>;
|
|
148
|
+
/** 首绑(认领之后的初始放置写):按意图那一读的住客定址,设备店一只事务。 */
|
|
113
149
|
bindPlacement(input: {
|
|
114
150
|
rootSessionId: string;
|
|
151
|
+
occupant: SessionFace;
|
|
115
152
|
deviceId: string;
|
|
116
153
|
requester: DeviceOwner;
|
|
117
|
-
}): Promise<
|
|
154
|
+
}): Promise<BindPlacementOutcome>;
|
|
118
155
|
admitSession(input: AdmitSessionInput): Promise<DeviceOutcome<AdmitSessionValue>>;
|
|
119
156
|
revoke(input: {
|
|
120
157
|
deviceId: string;
|
|
@@ -125,6 +162,8 @@ export interface DeviceEnrollment {
|
|
|
125
162
|
}
|
|
126
163
|
export interface DeviceEnrollmentDeps {
|
|
127
164
|
store: DeviceStore;
|
|
165
|
+
/** 会话店的执行席(绑定的唯一载体与唯一读;装配点交会话店的 `executionPlacement`)。 */
|
|
166
|
+
seat: ExecutionPlacementSeat;
|
|
128
167
|
/** 缺席 = 不限速(端点车必须传;见文件头注 ⑤)。 */
|
|
129
168
|
rateLimiter?: DeviceEnrollRateLimiter;
|
|
130
169
|
/** 测试可注入确定性铸造;默认 `mintDeviceId()`。 */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEVICE_ERROR_CODES, DEVICE_ERROR_STATUS, DEVICE_RATE_LIMIT_CODE, assertDeviceIdShape, assertDeviceOwnerShape, assertDevicePubkeyShape, assertEnrollTokenHashShape, assertRootSessionIdShape, hashEnrollToken, mintDeviceId, mintEnrollToken, } from "./device-store.js";
|
|
2
|
+
import { boundDeviceOf } from "./execution-placement.js";
|
|
2
3
|
export const DEFAULT_ENROLL_TOKEN_TTL_SEC = 900;
|
|
3
4
|
export function buildDeviceRejection(errorCode, message) {
|
|
4
5
|
return { code: errorCode, status: DEVICE_ERROR_STATUS[errorCode], message };
|
|
@@ -13,7 +14,7 @@ function rejectNotFound() {
|
|
|
13
14
|
return reject(DEVICE_ERROR_CODES.NOT_FOUND, "device not found");
|
|
14
15
|
}
|
|
15
16
|
export function createDeviceEnrollment(deps) {
|
|
16
|
-
const { store } = deps;
|
|
17
|
+
const { store, seat } = deps;
|
|
17
18
|
const mintId = deps.mintId ?? mintDeviceId;
|
|
18
19
|
async function rateGate(key) {
|
|
19
20
|
if (!deps.rateLimiter)
|
|
@@ -87,21 +88,35 @@ export function createDeviceEnrollment(deps) {
|
|
|
87
88
|
return rejectEnrollment();
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
async function readPlacement(rootSessionId) {
|
|
92
|
+
assertRootSessionIdShape(rootSessionId);
|
|
93
|
+
return seat.read(rootSessionId);
|
|
94
|
+
}
|
|
90
95
|
async function bindPlacement(input) {
|
|
91
96
|
assertRootSessionIdShape(input.rootSessionId);
|
|
92
97
|
assertDeviceIdShape(input.deviceId);
|
|
93
98
|
assertDeviceOwnerShape(input.requester);
|
|
94
|
-
const result = await store.
|
|
99
|
+
const result = await store.placeDeviceExecution({ root: input.rootSessionId, occupant: input.occupant, deviceId: input.deviceId, owner: input.requester });
|
|
95
100
|
switch (result.outcome) {
|
|
96
101
|
case "bound":
|
|
97
|
-
case "
|
|
98
|
-
return { ok: true,
|
|
99
|
-
case "
|
|
102
|
+
case "same":
|
|
103
|
+
return { ok: true, device: result.device };
|
|
104
|
+
case "device_conflict":
|
|
100
105
|
return reject(DEVICE_ERROR_CODES.CLAIM_CONFLICT, "this session is already bound to a different device");
|
|
101
106
|
case "device_not_found":
|
|
102
107
|
return rejectNotFound();
|
|
103
108
|
case "device_revoked":
|
|
104
109
|
return reject(DEVICE_ERROR_CODES.REVOKED, "device has been revoked");
|
|
110
|
+
case "gone":
|
|
111
|
+
return { ok: false, placement: "gone" };
|
|
112
|
+
case "mirror":
|
|
113
|
+
return { ok: false, placement: "mirror" };
|
|
114
|
+
case "tree_bound":
|
|
115
|
+
return { ok: false, placement: "tree_bound", boundLane: result.boundLane };
|
|
116
|
+
default: {
|
|
117
|
+
const unhandled = result;
|
|
118
|
+
throw new Error(`bindPlacement: unknown store outcome ${JSON.stringify(unhandled)}`);
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
async function admitSession(input) {
|
|
@@ -109,22 +124,22 @@ export function createDeviceEnrollment(deps) {
|
|
|
109
124
|
assertDeviceOwnerShape(input.requester);
|
|
110
125
|
if (input.requestedDeviceId !== undefined)
|
|
111
126
|
assertDeviceIdShape(input.requestedDeviceId);
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
127
|
+
const bound = input.placement === undefined ? undefined : boundDeviceOf(input.placement.cell);
|
|
128
|
+
if (!bound)
|
|
114
129
|
return reject(DEVICE_ERROR_CODES.NOT_BOUND, "this session has no device binding");
|
|
115
|
-
if (
|
|
130
|
+
if (bound.owner.tenant !== input.requester.tenant || bound.owner.subject !== input.requester.subject)
|
|
116
131
|
return rejectNotFound();
|
|
117
|
-
if (input.requestedDeviceId !== undefined && input.requestedDeviceId !==
|
|
132
|
+
if (input.requestedDeviceId !== undefined && input.requestedDeviceId !== bound.id) {
|
|
118
133
|
return reject(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, "the requested device is not the one bound to this session");
|
|
119
134
|
}
|
|
120
|
-
const view = await store.readDeviceForAdmission(
|
|
135
|
+
const view = await store.readDeviceForAdmission(bound.id, input.requester);
|
|
121
136
|
if (!view)
|
|
122
137
|
return rejectNotFound();
|
|
123
138
|
if (view.row.status === "revoked")
|
|
124
139
|
return reject(DEVICE_ERROR_CODES.REVOKED, "device has been revoked");
|
|
125
140
|
if (!view.online)
|
|
126
141
|
return reject(DEVICE_ERROR_CODES.OFFLINE, "device is not connected");
|
|
127
|
-
return { ok: true, value: { deviceId:
|
|
142
|
+
return { ok: true, value: { deviceId: bound.id, device: view.row, connGeneration: view.row.connGeneration } };
|
|
128
143
|
}
|
|
129
144
|
async function revoke(input) {
|
|
130
145
|
assertDeviceIdShape(input.deviceId);
|
|
@@ -138,7 +153,7 @@ export function createDeviceEnrollment(deps) {
|
|
|
138
153
|
return rejectNotFound();
|
|
139
154
|
}
|
|
140
155
|
}
|
|
141
|
-
return { issueEnrollToken, enroll, bindPlacement, admitSession, revoke };
|
|
156
|
+
return { issueEnrollToken, enroll, readPlacement, bindPlacement, admitSession, revoke };
|
|
142
157
|
}
|
|
143
158
|
export function createFixedWindowRateLimiter(opts) {
|
|
144
159
|
const now = opts.now ?? (() => Date.now());
|
package/dist/device-store.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DeviceBindOutcome, DeviceRebindOutcome, InMemoryExecutionPlacementSeat } from "./execution-placement.js";
|
|
2
|
+
import type { SessionFace } from "./session-incarnation.js";
|
|
1
3
|
/**
|
|
2
4
|
* device lane 的 HTTP 错误码闭集(v2 §4.8 逐字)。**唯一导出点** —— 端点车(A-3/车C)从这里取,
|
|
3
5
|
* 禁在路由里手抄字面量(§4.8 task-R1-12:「下表即闭集全集,禁四份手抄」)。
|
|
@@ -33,7 +35,7 @@ export declare const DEVICE_ERROR_CODES: {
|
|
|
33
35
|
* (管理面,`http/routes/devices.ts`);adapter/准入链结构上不产它。 */
|
|
34
36
|
readonly REBIND_REV_CONFLICT: "device.rebind_rev_conflict";
|
|
35
37
|
/** O4 换绑动词(v2 稿 §2 步 4):该根会话名下有非终态 run(durable 判据 = run store 的 session
|
|
36
|
-
* claim;hub
|
|
38
|
+
* claim;hub 内存在途计数**不作门**——跨副本不可见;保证 = 设备店换绑事务内锁根行后那一读,池级读只作早拒)—— 体带 `activeTaskId`。铸造点同上一条。 */
|
|
37
39
|
readonly REBIND_ACTIVE_RUN: "device.rebind_active_run";
|
|
38
40
|
};
|
|
39
41
|
/** {@link DEVICE_ERROR_CODES} 的值联合 —— 闭集(未知码 = 编译错误,[ref] 词表纪律)。 */
|
|
@@ -113,14 +115,6 @@ export interface DeviceRow {
|
|
|
113
115
|
createdAtMs: number;
|
|
114
116
|
rev: number;
|
|
115
117
|
}
|
|
116
|
-
export interface DeviceSessionRow {
|
|
117
|
-
/** 键 = **根**会话(task-R1-3:后代继承不另绑)。 */
|
|
118
|
-
rootSessionId: string;
|
|
119
|
-
deviceId: string;
|
|
120
|
-
owner: DeviceOwner;
|
|
121
|
-
boundAtMs: number;
|
|
122
|
-
rev: number;
|
|
123
|
-
}
|
|
124
118
|
export interface DeviceAuditRow {
|
|
125
119
|
id: number;
|
|
126
120
|
atMs: number;
|
|
@@ -147,7 +141,7 @@ export interface DeviceEnrollTokenRow {
|
|
|
147
141
|
export declare const DEVICE_KEY_MAX_BYTES = 190;
|
|
148
142
|
/** `device_id` 列宽(`dev_` + 26 位 Crockford base32 ULID = 30 字节,64 是三倍余量)。 */
|
|
149
143
|
export declare const DEVICE_ID_MAX_BYTES = 64;
|
|
150
|
-
/** `
|
|
144
|
+
/** 根会话 id 的形门上界(换绑路由的路径段;会话行 `session_id` 宽 64,提交入口硬拒 >64 的 sessionId,128 留一倍余量 —— 超宽的 id 等值读不到任何行)。 */
|
|
151
145
|
export declare const DEVICE_SESSION_ID_MAX_BYTES = 128;
|
|
152
146
|
/** `pubkey` / `token_hash` 列宽(Ed25519 b64 = 44,sha256 hex = 64)。 */
|
|
153
147
|
export declare const DEVICE_PUBKEY_MAX_BYTES = 128;
|
|
@@ -176,10 +170,12 @@ export declare function assertDevicePubkeyShape(pubkey: string): void;
|
|
|
176
170
|
export declare function assertNewDeviceShape(input: NewDeviceInput): void;
|
|
177
171
|
export declare function assertIssueTokenShape(input: IssueEnrollTokenInput): void;
|
|
178
172
|
export declare function assertRevokeShape(input: RevokeDeviceInput): void;
|
|
173
|
+
/** device 首绑入参的整形门(三形共用:内存参照实现与两方言从同一处过)。 */
|
|
174
|
+
export declare function assertPlaceDeviceShape(input: PlaceDeviceExecutionInput): void;
|
|
179
175
|
/** O4 换绑入参的整形门(三形共用)。`expectedRev` 收窄为非负整数:它要进 SQL 等值谓词,小数/负数/NaN
|
|
180
176
|
* 在两方言的 BIGINT 比对下不是「不匹配」而是各自的隐式转换歧义面 —— 在门口拒,方言分歧面消失。
|
|
181
177
|
* ⚠️ HTTP 层对坏形先答 400(业务拒绝);这里的抛是**编程/装配错误**的兜底,不是 wire 分支。 */
|
|
182
|
-
export declare function assertRebindShape(input:
|
|
178
|
+
export declare function assertRebindShape(input: RebindDeviceExecutionInput): void;
|
|
183
179
|
export declare function assertAcquireShape(input: AcquireConnectionInput): void;
|
|
184
180
|
/** token 摘要必须是**规范形**(小写 sha256 hex):大小写混形在 VARBINARY/COLLATE "C" 下是两个键,
|
|
185
181
|
* 于是同一个明文 token 能被消费两次 —— 这是 §7-T5 ③「一次性 CAS」的直接跳过面。 */
|
|
@@ -268,73 +264,53 @@ export type ConsumeEnrollTokenResult =
|
|
|
268
264
|
| {
|
|
269
265
|
outcome: "pubkey_taken";
|
|
270
266
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
outcome: "idempotent";
|
|
285
|
-
row: DeviceSessionRow;
|
|
286
|
-
}
|
|
287
|
-
/** 根会话已绑**另一台**设备(或另一个 owner)⇒ `device.claim_conflict`。 */
|
|
288
|
-
| {
|
|
289
|
-
outcome: "conflict";
|
|
290
|
-
row: DeviceSessionRow;
|
|
267
|
+
/**
|
|
268
|
+
* device 车道的**首绑**(§4.3.2;认领之后的初始放置写,§3.3 转换表 place / bind 两行):根会话行上这一位住客的执行格
|
|
269
|
+
* absent → writer(device, D),或迁移回填的 writer(device, ∅) → writer(device, D)。绑定**住在会话行上**(唯一载体;
|
|
270
|
+
* 列与语句的属主 = `plugins/execution-placement-sql.ts`),本店只负责「设备行判 + 写格」同一事务(锁序:设备行 → 根 `session_meta`)。
|
|
271
|
+
*/
|
|
272
|
+
export interface PlaceDeviceExecutionInput {
|
|
273
|
+
/** 根会话(放置键;后代继承,不另绑)。 */
|
|
274
|
+
readonly root: string;
|
|
275
|
+
/** 意图那一读的住客(会话店执行席 `seat.read(root)` 交出的那一位)—— 写按住客谓词定址:住客换了 ⇒ `gone`,永不写到新住客身上。 */
|
|
276
|
+
readonly occupant: SessionFace;
|
|
277
|
+
readonly deviceId: string;
|
|
278
|
+
/** 验证过的复合身份;与设备行的 owner 在**同一事务**内比对,写进格的两属主列(绑定属主 = 设备属主)。 */
|
|
279
|
+
readonly owner: DeviceOwner;
|
|
291
280
|
}
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
|
|
281
|
+
/**
|
|
282
|
+
* 首绑的判别式结果 —— 并发争抢**单赢家,无未定义态**。格的那几臂与判官同一张表(`DeviceBindOutcome`:`bound` / 同设备 `same`(幂等成功)/
|
|
283
|
+
* 异设备 `device_conflict` / 异车道 `tree_bound` / `mirror` / 住客不是判过的那一位或行不在 `gone`),外加设备行的两臂:
|
|
284
|
+
* 设备不存在**或**不是请求方的(反枚举同形,见文件头注)/ 已吊销。
|
|
285
|
+
*/
|
|
286
|
+
export type PlaceDeviceExecutionResult = DeviceBindOutcome | {
|
|
287
|
+
readonly outcome: "device_not_found";
|
|
295
288
|
} | {
|
|
296
|
-
outcome: "device_revoked";
|
|
289
|
+
readonly outcome: "device_revoked";
|
|
297
290
|
};
|
|
298
291
|
/**
|
|
299
|
-
* O4 显式换绑(2026-08-30 v2 稿 §2)。`owner` =
|
|
300
|
-
* explicit operator;operator
|
|
301
|
-
* 判别位在店里就消失(反枚举,文件头注)。`expectedRev` =
|
|
302
|
-
* 防两操作员并发换绑互踩)。
|
|
292
|
+
* O4 显式换绑(2026-08-30 v2 稿 §2;§3.3 转换表 rebind 行)。`occupant` = 换绑门那一读的住客;`owner` = 门判过的**格上**两属主列
|
|
293
|
+
* (HTTP 层已裁决:属主同域或 explicit operator;operator 代操作时传的也是格上的 owner,不是操作员自己)——店内所有谓词恒带它,
|
|
294
|
+
* 判别位在店里就消失(反枚举,文件头注)。`expectedRev` = 壳从执行放置读面拿到的前态(强制显式,防两操作员并发换绑互踩)。
|
|
303
295
|
*/
|
|
304
|
-
export interface
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
296
|
+
export interface RebindDeviceExecutionInput {
|
|
297
|
+
readonly root: string;
|
|
298
|
+
readonly occupant: SessionFace;
|
|
299
|
+
readonly owner: DeviceOwner;
|
|
300
|
+
readonly expectedRev: number;
|
|
301
|
+
readonly targetDeviceId: string;
|
|
309
302
|
}
|
|
310
303
|
/**
|
|
311
|
-
* O4 换绑的判别式结果(门序 = v2 稿 §2
|
|
312
|
-
*
|
|
304
|
+
* O4 换绑的判别式结果(门序 = v2 稿 §2 + 读写序律:目标设备在场 → 锁根行读认领 → 幂等短路 → 前态 CAS → 审计)。格的那几臂与判官同一张表
|
|
305
|
+
* (`DeviceRebindOutcome`:`rebound` 带被换下的那台 / 真幂等 `idempotent` / 前态输 `rev_conflict` 携当前格 / `gone` = 行不在、住客换了、不是 writer(device, D)、
|
|
306
|
+
* 或绑定属主不是判过的那一位 —— 反枚举同形),外加:目标设备不在 / 不是该 owner 的 / **已吊销**(三臂同折 404;吊销不单列 = 判别位在店里就消失);
|
|
307
|
+
* 写事务内锁根行后读到认领 `active_run`(SQL 后端;读写序律只在 SQL 后端立 —— 内存参照实现没有认领表,答不出这一臂,内存走廊的认领判据是路由的早拒)。
|
|
313
308
|
*/
|
|
314
|
-
export type
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
outcome: "
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
/** 真幂等:`toDeviceId` ≡ 当前绑定 ∧ `expectedRev` ≡ 当前 rev ⇒ 回显不加 rev、不写审计。 */
|
|
321
|
-
| {
|
|
322
|
-
outcome: "idempotent";
|
|
323
|
-
row: DeviceSessionRow;
|
|
324
|
-
}
|
|
325
|
-
/** 前态输(rev 不符,含「toDeviceId 同但 rev 错」——前态错就是错)⇒ 携当前行供壳一跳重定位。 */
|
|
326
|
-
| {
|
|
327
|
-
outcome: "rev_conflict";
|
|
328
|
-
row: DeviceSessionRow;
|
|
329
|
-
}
|
|
330
|
-
/** 绑定行不在**或**不是该 owner 的 —— 反枚举同形(HTTP 层折 404 同串)。 */
|
|
331
|
-
| {
|
|
332
|
-
outcome: "binding_not_found";
|
|
333
|
-
}
|
|
334
|
-
/** 目标设备不在 / 不是该 owner 的 / **已吊销** —— v2 稿 §2 步 2:三臂同折 404(设备 id 也是
|
|
335
|
-
* 不可枚举面);吊销不单列 = 判别位在店里就消失,消费层想泄也泄不出来。 */
|
|
336
|
-
| {
|
|
337
|
-
outcome: "device_not_found";
|
|
309
|
+
export type RebindDeviceExecutionResult = DeviceRebindOutcome | {
|
|
310
|
+
readonly outcome: "device_not_found";
|
|
311
|
+
} | {
|
|
312
|
+
readonly outcome: "active_run";
|
|
313
|
+
readonly activeTaskId: string;
|
|
338
314
|
};
|
|
339
315
|
export interface RevokeDeviceInput {
|
|
340
316
|
deviceId: string;
|
|
@@ -415,14 +391,10 @@ export interface DeviceStore {
|
|
|
415
391
|
/** 世代谓词命中 = true;0 行 = 已失权。 */
|
|
416
392
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
417
393
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
|
|
423
|
-
/** purge coordinator 座(§6 注②:core 的会话删除是**逐 store 显式调用**,不是 DB 自动扫;
|
|
424
|
-
* 只建表不接 purge = 删会话后绑定残留 ⇒ sessionId 重用致旧设备粘连/claim conflict/隐私残留)。 */
|
|
425
|
-
deleteByRootSession(rootSessionId: string): Promise<number>;
|
|
394
|
+
/** 首绑(认领之后的初始放置写):设备行判(active ∧ 属主)+ 写格同一事务,按住客谓词定址。 */
|
|
395
|
+
placeDeviceExecution(input: PlaceDeviceExecutionInput): Promise<PlaceDeviceExecutionResult>;
|
|
396
|
+
/** O4 显式换绑(v2 稿 §2 步 2/3/5):目标设备门 + 写事务内锁根行读认领 + 前态 CAS + `session_rebound` 审计行同一事务。 */
|
|
397
|
+
rebindDeviceExecution(input: RebindDeviceExecutionInput): Promise<RebindDeviceExecutionResult>;
|
|
426
398
|
appendAudit(input: AppendAuditInput): Promise<void>;
|
|
427
399
|
listAudit(filter?: ListAuditFilter): Promise<DeviceAuditRow[]>;
|
|
428
400
|
}
|
|
@@ -432,15 +404,19 @@ declare function cloneOwner(o: DeviceOwner): DeviceOwner;
|
|
|
432
404
|
* 语义 oracle。**单线程 JS 的原子性**替代 SQL 事务:每个方法体内没有 `await`(除了返回值),所以
|
|
433
405
|
* 「读-判-写」在事件循环上不可分割 —— 与 SQL 侧的单事务 CAS 语义等价,这也是为什么并发格
|
|
434
406
|
* (两个并发消费同一 token)在这一形上必须给出与真库**同一个**赢家/输家分布。
|
|
407
|
+
*
|
|
408
|
+
* 绑定不在本店:它是会话行上的执行格。构造**必收**测试走廊的会话行 {@link InMemoryExecutionPlacementSeat}(同为参照实现,生产零引用)——
|
|
409
|
+
* 首绑 / 换绑在它上面做(判官与 SQL 语句模块同一组),读者经同一只席的 `read(root)` 读。
|
|
435
410
|
*/
|
|
436
411
|
export declare class InMemoryDeviceStore implements DeviceStore {
|
|
437
412
|
private readonly devices;
|
|
438
|
-
private readonly sessions;
|
|
439
413
|
private readonly tokens;
|
|
440
414
|
private readonly audit;
|
|
441
415
|
private auditSeq;
|
|
442
416
|
private readonly now;
|
|
443
|
-
|
|
417
|
+
private readonly seat;
|
|
418
|
+
constructor(opts: {
|
|
419
|
+
seat: InMemoryExecutionPlacementSeat;
|
|
444
420
|
now?: () => number;
|
|
445
421
|
});
|
|
446
422
|
issueEnrollToken(input: IssueEnrollTokenInput): Promise<DeviceEnrollTokenRow>;
|
|
@@ -454,17 +430,14 @@ export declare class InMemoryDeviceStore implements DeviceStore {
|
|
|
454
430
|
acquireConnection(input: AcquireConnectionInput): Promise<AcquireConnectionResult>;
|
|
455
431
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
456
432
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
|
|
460
|
-
deleteByRootSession(rootSessionId: string): Promise<number>;
|
|
433
|
+
placeDeviceExecution(input: PlaceDeviceExecutionInput): Promise<PlaceDeviceExecutionResult>;
|
|
434
|
+
rebindDeviceExecution(input: RebindDeviceExecutionInput): Promise<RebindDeviceExecutionResult>;
|
|
461
435
|
appendAudit(input: AppendAuditInput): Promise<void>;
|
|
462
436
|
listAudit(filter?: ListAuditFilter): Promise<DeviceAuditRow[]>;
|
|
463
437
|
private isOnline;
|
|
464
438
|
private pushAudit;
|
|
465
439
|
/** 返回**拷贝**:调用方拿到的行改了也不能污染店内状态(SQL 侧天然如此,InMemory 必须显式做)。 */
|
|
466
440
|
private snapshotDevice;
|
|
467
|
-
private snapshotSession;
|
|
468
441
|
}
|
|
469
442
|
export { sameOwner as sameDeviceOwner, cloneOwner as cloneDeviceOwner };
|
|
470
443
|
//# sourceMappingURL=device-store.d.ts.map
|