@sema-agent/server 7.50.0 → 7.52.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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +83 -20
- package/dist/adoption/plan.d.ts +4 -4
- package/dist/adoption/plan.js +11 -0
- package/dist/approval-card.d.ts +26 -2
- package/dist/approval-card.js +9 -0
- package/dist/approval-hmac.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +2 -2
- package/dist/approval-reconciler.js +1 -1
- package/dist/bench/l8/artifact.d.ts +1 -1
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/live-deps.d.ts +1 -1
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +1 -1
- package/dist/boot/budget-tracing.d.ts +1 -1
- package/dist/boot/budget-tracing.js +2 -5
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
- package/dist/boot/device-lane.d.ts +79 -0
- package/dist/boot/device-lane.js +63 -0
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +48 -2
- package/dist/boot/execution-env.js +57 -5
- package/dist/boot/governance-seams.d.ts +2 -2
- package/dist/boot/leader.d.ts +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +6 -6
- package/dist/boot/org-memory.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +3 -3
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +17 -4
- package/dist/boot/resolve-spec.d.ts +15 -0
- package/dist/boot/resolve-spec.js +40 -15
- package/dist/boot/retention-lane.d.ts +2 -2
- package/dist/boot/runner-deps.d.ts +17 -7
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/runtime-caps.d.ts +23 -0
- package/dist/boot/runtime-caps.js +24 -2
- package/dist/boot/session-faces.d.ts +6 -1
- package/dist/boot/session-faces.js +17 -3
- package/dist/boot/shutdown.d.ts +8 -0
- package/dist/boot/shutdown.js +3 -1
- package/dist/boot/stores.js +40 -1
- package/dist/boot/task-list-lane.d.ts +2 -2
- package/dist/brain.js +51 -31
- package/dist/budget.d.ts +2 -2
- package/dist/capabilities/center-plugins.d.ts +1 -1
- package/dist/capabilities/collab-workflows.d.ts +1 -1
- package/dist/capabilities/hands-lane.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +3 -3
- package/dist/capabilities/skills.d.ts +1 -1
- package/dist/config-center/hot-keys-registry.js +5 -0
- package/dist/config-center/http-client.d.ts +2 -2
- package/dist/config-center/read-face.d.ts +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-center/types.d.ts +10 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +18 -0
- package/dist/config-types.d.ts +90 -18
- package/dist/config.js +91 -19
- package/dist/deployment-governance.d.ts +1 -1
- package/dist/device-enrollment.d.ts +125 -0
- package/dist/device-enrollment.js +156 -0
- package/dist/device-store.d.ts +385 -0
- package/dist/device-store.js +407 -0
- package/dist/device-ws-hub.d.ts +182 -0
- package/dist/device-ws-hub.js +1012 -0
- package/dist/device-ws-protocol.d.ts +429 -0
- package/dist/device-ws-protocol.js +464 -0
- package/dist/env-facts.d.ts +5 -2
- package/dist/env-facts.js +1 -0
- package/dist/execution-lane-caps.d.ts +152 -0
- package/dist/execution-lane-caps.js +166 -0
- package/dist/fleet/fleet-bus.d.ts +1 -1
- package/dist/fleet/fleet-reconciler.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/http/active-run-conflict.d.ts +2 -2
- package/dist/http/principal-gate.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +24 -4
- package/dist/http/routes/admin-memory-optout.d.ts +48 -0
- package/dist/http/routes/admin-memory-optout.js +75 -0
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +21 -21
- package/dist/http/routes/capabilities.js +27 -11
- package/dist/http/routes/diagnostics.d.ts +1 -1
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/memory-bundle.d.ts +1 -1
- package/dist/http/routes/memory-bundle.js +3 -1
- package/dist/http/routes/memory-compliance.d.ts +4 -4
- package/dist/http/routes/memory-origin.d.ts +4 -4
- package/dist/http/routes/memory-origin.js +3 -1
- package/dist/http/routes/memory-policy.js +6 -2
- package/dist/http/routes/retention-ops.d.ts +1 -1
- package/dist/http/routes/rules.d.ts +5 -2
- package/dist/http/routes/rules.js +84 -5
- package/dist/http/routes/run-memory-capture.d.ts +34 -0
- package/dist/http/routes/run-memory-capture.js +97 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +212 -6
- package/dist/http/routes/session-sync.d.ts +4 -6
- package/dist/http/routes/session-sync.js +60 -47
- package/dist/http/routes/sessions.js +61 -188
- package/dist/http/routes/shared-memory.d.ts +1 -1
- package/dist/http/routes/tasks.js +1 -1
- package/dist/http/routes/workflows.js +19 -0
- package/dist/http/server.d.ts +49 -13
- package/dist/http/server.js +109 -13
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/http/wire-types.d.ts +41 -8
- package/dist/leader/endpoint.d.ts +1 -1
- package/dist/leader/leader.d.ts +1 -1
- package/dist/leader/repair-oracle.d.ts +2 -2
- package/dist/leader/repair-oracle.js +1 -1
- package/dist/leader/wire.js +4 -2
- package/dist/main.js +23 -9
- package/dist/memory-bundle-engine.d.ts +1 -1
- package/dist/memory-operator-faces.d.ts +5 -5
- package/dist/memory-origin-fence.d.ts +50 -0
- package/dist/memory-origin-fence.js +15 -0
- package/dist/memory-posture.d.ts +13 -1
- package/dist/memory-posture.js +1 -0
- package/dist/memory-scope.d.ts +6 -1
- package/dist/memory-scope.js +3 -2
- package/dist/model-select.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +17 -5
- package/dist/observability/fail-open.js +17 -5
- package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
- package/dist/orchestration/hardened-vm-runner.js +11 -1
- package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
- package/dist/plugins/background-shell-support.d.ts +2 -2
- package/dist/plugins/blob-backend.d.ts +7 -2
- package/dist/plugins/blob-backend.js +22 -18
- package/dist/plugins/caching-session-store.d.ts +7 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
- package/dist/plugins/device-store-sql.d.ts +130 -0
- package/dist/plugins/device-store-sql.js +574 -0
- package/dist/plugins/file-history-store-sql.d.ts +170 -0
- package/dist/plugins/file-history-store-sql.js +827 -0
- package/dist/plugins/file-run-store.d.ts +44 -5
- package/dist/plugins/file-run-store.js +85 -13
- package/dist/plugins/fork-routing-session-store.d.ts +1 -1
- package/dist/plugins/fork-routing-session-store.js +2 -2
- package/dist/plugins/leader-run-store-sql.d.ts +1 -1
- package/dist/plugins/local-session-store.d.ts +9 -1
- package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
- package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
- package/dist/plugins/memory-origin-law.d.ts +2 -2
- package/dist/plugins/memory-run-store.d.ts +4 -3
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/permission-rule-store-file.d.ts +16 -6
- package/dist/plugins/permission-rule-store-file.js +77 -13
- package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
- package/dist/plugins/permission-rule-store-sql.js +123 -21
- package/dist/plugins/pg-pool.js +59 -0
- package/dist/plugins/pg-safe-json.d.ts +1 -1
- package/dist/plugins/remote-env-device.d.ts +271 -0
- package/dist/plugins/remote-env-device.js +727 -0
- package/dist/plugins/remote-env-k8s.d.ts +1 -1
- package/dist/plugins/remote-scratchpad.js +1 -1
- package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
- package/dist/plugins/retention-store-sql.d.ts +7 -7
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/run-store-sql.d.ts +9 -3
- package/dist/plugins/run-store-sql.js +2 -2
- package/dist/plugins/send-user-file.d.ts +1 -1
- package/dist/plugins/session-placement.d.ts +1 -1
- package/dist/plugins/sql-errors.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +68 -29
- package/dist/plugins/store-backend.js +78 -15
- package/dist/plugins/store-contracts.d.ts +5 -3
- package/dist/plugins/task-list-store-sql.d.ts +1 -1
- package/dist/plugins/tidb-pool.js +59 -0
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/prompts-domain-validate.js +1 -1
- package/dist/resource-suspend.d.ts +1 -1
- package/dist/resource-window.d.ts +3 -3
- package/dist/rules-consent.d.ts +231 -9
- package/dist/rules-consent.js +411 -13
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +15 -1
- package/dist/runs.js +19 -0
- package/dist/runtime-caps-resolver.d.ts +36 -1
- package/dist/runtime-caps-resolver.js +25 -0
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/security.d.ts +5 -5
- package/dist/security.js +1 -1
- package/dist/session-sync.d.ts +52 -37
- package/dist/session-sync.js +19 -16
- package/dist/shared-memory-scope-authorizer.d.ts +1 -1
- package/dist/sighup-idle.d.ts +1 -1
- package/dist/spec-fields.d.ts +22 -1
- package/dist/spec-fields.js +11 -0
- package/dist/store-live-probe.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +25 -0
- package/dist/task-cwd.js +3 -0
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-settings.d.ts +3 -3
- package/dist/task-settings.js +3 -1
- package/dist/task-workflow.d.ts +1 -1
- package/dist/text-bidi.d.ts +2 -2
- package/dist/tool-approval.d.ts +191 -9
- package/dist/tool-approval.js +248 -29
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/ledger-events.d.ts +1 -1
- package/dist/trace/ledger-sink.d.ts +2 -2
- package/dist/trace/project.d.ts +3 -2
- package/dist/trace/project.js +1 -0
- package/dist/turn-activity.d.ts +1 -1
- package/dist/wall-clock-jump-guard.d.ts +3 -3
- package/package.json +2 -2
- package/dist/http/tar.d.ts +0 -17
- package/dist/http/tar.js +0 -53
- package/dist/http/workspace-content.d.ts +0 -11
- package/dist/http/workspace-content.js +0 -20
- package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
- package/dist/plugins/file-snapshot-store-sql.js +0 -213
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, foldDelta, parseAllowRuleText, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_SYNC_DROP_CODES, } from "@sema-agent/core";
|
|
3
|
+
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, parseAllowRuleText, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_SYNC_DROP_CODES, } from "@sema-agent/core";
|
|
4
4
|
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
6
|
export function writerOfSqlRuleStore(store) {
|
|
@@ -14,7 +14,7 @@ export function writerOfSqlRuleStore(store) {
|
|
|
14
14
|
return undefined;
|
|
15
15
|
return { nextDot: nextDot, apply: apply, readRaw: readRaw };
|
|
16
16
|
}
|
|
17
|
-
const
|
|
17
|
+
export const DurableRuleScopeSchema = z.union([
|
|
18
18
|
z.object({ kind: z.literal("global") }).strict(),
|
|
19
19
|
z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
|
|
20
20
|
]);
|
|
@@ -28,12 +28,12 @@ const PersistedAllowRuleSchema = z
|
|
|
28
28
|
tool: z.literal("Bash"),
|
|
29
29
|
match: z.enum(["exact", "prefix"]),
|
|
30
30
|
command: z.string(),
|
|
31
|
-
scope:
|
|
31
|
+
scope: DurableRuleScopeSchema,
|
|
32
32
|
adds: z.array(RuleAddSchema),
|
|
33
33
|
})
|
|
34
34
|
.strict();
|
|
35
35
|
const RuleTombstoneSchema = z
|
|
36
|
-
.object({ rule: z.string(), scope:
|
|
36
|
+
.object({ rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
|
|
37
37
|
.strict();
|
|
38
38
|
const QUARANTINE_REASONS = new Set(Object.entries(RULE_SYNC_DROP_CODES)
|
|
39
39
|
.filter(([, side]) => side.includes("local-quarantined"))
|
|
@@ -42,14 +42,14 @@ const isQuarantineReason = (v) => typeof v === "string" && QUARANTINE_REASONS.ha
|
|
|
42
42
|
const QuarantinedRuleAddSchema = z
|
|
43
43
|
.object({
|
|
44
44
|
rule: z.string(),
|
|
45
|
-
scope:
|
|
45
|
+
scope: DurableRuleScopeSchema,
|
|
46
46
|
add: RuleAddSchema,
|
|
47
47
|
reason: z.custom(isQuarantineReason, { message: "not a registered permission-rule quarantine reason" }),
|
|
48
48
|
atMs: z.number().int().nonnegative().safe(),
|
|
49
49
|
})
|
|
50
50
|
.strict();
|
|
51
51
|
const FrontierSchema = z.record(z.string(), z.number().int().nonnegative().safe());
|
|
52
|
-
const RuleCandidateSchema = z.object({ rule: z.string(), scope:
|
|
52
|
+
const RuleCandidateSchema = z.object({ rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
53
53
|
const RedeemedDotsSchema = z.record(z.string(), RuleDotSchema);
|
|
54
54
|
const RuleEditedSchema = z.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string() }).strict();
|
|
55
55
|
const RuleOffer2Schema = z.union([
|
|
@@ -143,16 +143,41 @@ export const TIDB_PERMISSION_RULE_STATEMENTS = [
|
|
|
143
143
|
principal VARCHAR(512) NOT NULL,
|
|
144
144
|
approval_id VARCHAR(190) NOT NULL,
|
|
145
145
|
-- payload_hash:载荷绑定(F1 ④)。= prepare 时刻候选集的规范摘要;redeem 时与记录上的候选集
|
|
146
|
-
-- 重算比对,不符 ⇒ 拒(
|
|
146
|
+
-- 重算比对,不符 ⇒ 拒(改动/重放不可扩权)。
|
|
147
147
|
payload_hash VARCHAR(190) NOT NULL,
|
|
148
148
|
payload_json LONGTEXT NOT NULL,
|
|
149
149
|
expires_at_ms BIGINT NOT NULL,
|
|
150
150
|
-- consumed_at_ms:一次性消费位(F1 ③)。消费 = 单条条件 UPDATE(WHERE consumed_at_ms IS NULL),
|
|
151
151
|
-- **禁 read-then-write** —— 并发双 redeem 必须恰一成功,那只能由引擎的行锁回答。
|
|
152
152
|
consumed_at_ms BIGINT NULL,
|
|
153
|
+
-- claim_id:#265 A7 的**认领代次**(codex 对抗轮 R1-F1,验真后修)。每一次 consume / reclaim
|
|
154
|
+
-- 铸一枚新的;release / settle 的 WHERE 里**必须**带着它。没有这一列时两条语句问的是"有没有
|
|
155
|
+
-- 认领",不是"这是**谁的**认领"—— 于是一个跑过头被顶替的旧持有者能清掉现持有者的认领(第三条腿
|
|
156
|
+
-- 随即又认领一次),或把自己的陈旧终局写成这张票的答案。围栏把"恰一赢者"从一句话变成一条谓词。
|
|
157
|
+
claim_id VARCHAR(190) NULL,
|
|
158
|
+
-- purpose:#265 A7 的**车道绑定**(codex 对抗轮 R1-F2,验真后修)。两条导入车道共用这张表,而它们
|
|
159
|
+
-- 的票语义**刻意不同**(cc-import 一次性 / local-import 可恢复三态)。不刻这一位,一张 cc 票拿到
|
|
160
|
+
-- local 口就白得三态语义(cc-import 的一次性拒重放在**不改它一行代码**的情况下被绕过),反向则更坏:
|
|
161
|
+
-- 一张 local 票被 cc 口执行完却不落定,local 口随后按崩溃窗把它再执行一遍。不可变:铸票时定,此后
|
|
162
|
+
-- 每一次读写都拿它当匹配项(不符 ⇒ 与"票不存在"同形,零存在性 oracle)。
|
|
163
|
+
purpose VARCHAR(32) NOT NULL,
|
|
164
|
+
-- redeemed_outcome:#265 A7 的**第三态**(SETTLED)。NULL = 还没落定;非 NULL = 这张票的兑付**已经
|
|
165
|
+
-- 结束**,里面是那一次的逐字终局(JSON)。它把票的两态(未认领/已认领)扩成三态
|
|
166
|
+
-- MINTED → CLAIMED → SETTLED,而三态正是"认领之后死进程"这件事唯一能被区分出来的形:
|
|
167
|
+
-- CLAIMED-未结 = 上一次执行中途没了(可重驱),SETTLED = 已经答过了(逐字回放,零二次执行)。
|
|
168
|
+
-- 🔴 只服务 local-import 车道;cc-import 的票**语义不变**(一次性,同票二次 redeem 恒拒)——
|
|
169
|
+
-- 那条车道一个字节都不写这一列,两条车道的语义因此不共享一个开关(purpose 列是那条边界的执法者)。
|
|
170
|
+
redeemed_outcome LONGTEXT NULL,
|
|
171
|
+
-- settled_at_ms:落定**发生的时刻**(codex 对抗轮 R3-F2,验真后修)。清扫的「已落定」那一支必须按
|
|
172
|
+
-- 它判,不能按 expires_at_ms —— 一次**临期恢复**写下终局的那一刻,票早就过期很久了,按过期判会让
|
|
173
|
+
-- 这份刚生出来的权威回执**下一拍**就被删:规则已经写进店里,而属主的丢响应重放拿到 404。
|
|
174
|
+
-- 有了这一列,「落定之后必有一个完整的回放窗」对每一条路都成立,而不只是对准时兑付的那条。
|
|
175
|
+
settled_at_ms BIGINT NULL,
|
|
153
176
|
created_at_ms BIGINT NOT NULL,
|
|
154
177
|
PRIMARY KEY (ticket_id),
|
|
155
178
|
KEY idx_permission_rule_ticket_owner (owner_key),
|
|
179
|
+
-- 保留期腿 R6-F1 的支撑索引:孤儿 pending 的清扫谓词里有一条按 approval_id 的 NOT EXISTS 内查。
|
|
180
|
+
KEY idx_permission_rule_ticket_approval (approval_id),
|
|
156
181
|
-- 保留期腿的支撑索引(同上):WHERE expires_at_ms <= ? 是每 tick 一次的区间扫。
|
|
157
182
|
KEY idx_permission_rule_ticket_expires (expires_at_ms)
|
|
158
183
|
) COLLATE utf8mb4_bin`,
|
|
@@ -181,6 +206,25 @@ export async function assertPermissionRuleApprovalSchema(query, dialect) {
|
|
|
181
206
|
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
182
207
|
}
|
|
183
208
|
}
|
|
209
|
+
export async function assertPermissionRuleTicketSchema(query, dialect) {
|
|
210
|
+
try {
|
|
211
|
+
await query(`SELECT redeemed_outcome, claim_id, purpose, settled_at_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE 1=0`);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
if (!isMissingColumnError(err, dialect))
|
|
215
|
+
throw err;
|
|
216
|
+
throw new Error(`${PERMISSION_RULE_TICKET_TABLE} is missing required columns (redeemed_outcome / claim_id / purpose / settled_at_ms) — refusing to start. This build needs ` +
|
|
217
|
+
`the #265 A7 three-state redemption columns (MINTED → CLAIMED → SETTLED, the claim generation that fences a superseded ` +
|
|
218
|
+
`claimant, and the lane binding that keeps cc-import and local-import tickets from being interchangeable): without them a ` +
|
|
219
|
+
`local-import ticket can be claimed but never settled, so a redemption that already wrote rules to the store answers its ` +
|
|
220
|
+
`owner with a retry-later it can never satisfy. This repository ships no ALTER TABLE migrations, so the table must be recreated: run ` +
|
|
221
|
+
`\`DROP TABLE ${PERMISSION_RULE_TICKET_TABLE};\` on the ${dialect === "pg" ? "PostgreSQL" : "MySQL-protocol"} backend and ` +
|
|
222
|
+
`restart — the schema is recreated at boot. The cost is bounded and small: this table holds minute-TTL single-use import ` +
|
|
223
|
+
`tickets (a governance transient that dies on expiry), so dropping it only makes anyone mid-preview re-run prepare; ` +
|
|
224
|
+
`persisted rules live in ${PERMISSION_RULE_TABLE} and are untouched. ` +
|
|
225
|
+
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
184
228
|
export async function ensurePgPermissionRuleSchema(q) {
|
|
185
229
|
await q(`CREATE TABLE IF NOT EXISTS ${PERMISSION_RULE_TABLE} (
|
|
186
230
|
owner_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -237,10 +281,19 @@ export async function ensurePgPermissionRuleSchema(q) {
|
|
|
237
281
|
payload_json TEXT COLLATE "C" NOT NULL,
|
|
238
282
|
expires_at_ms BIGINT NOT NULL,
|
|
239
283
|
consumed_at_ms BIGINT,
|
|
284
|
+
-- claim_id / purpose / redeemed_outcome:#265 A7 三列(MySQL 孪生的行内注写了各自的理由 ——
|
|
285
|
+
-- 认领代次是 release/settle 的围栏、purpose 是两条导入车道的绑定位、redeemed_outcome 是第三态)。
|
|
286
|
+
claim_id VARCHAR(190) COLLATE "C",
|
|
287
|
+
purpose VARCHAR(32) COLLATE "C" NOT NULL,
|
|
288
|
+
redeemed_outcome TEXT COLLATE "C",
|
|
289
|
+
-- settled_at_ms:落定时刻(MySQL 孪生的行内注写了理由:清扫的已落定支按它判,否则临期恢复出来的
|
|
290
|
+
-- 权威回执下一拍就没)。
|
|
291
|
+
settled_at_ms BIGINT,
|
|
240
292
|
created_at_ms BIGINT NOT NULL,
|
|
241
293
|
PRIMARY KEY (ticket_id)
|
|
242
294
|
)`);
|
|
243
295
|
await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_ticket_owner ON ${PERMISSION_RULE_TICKET_TABLE} (owner_key)`);
|
|
296
|
+
await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_ticket_approval ON ${PERMISSION_RULE_TICKET_TABLE} (approval_id)`);
|
|
244
297
|
await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_ticket_expires ON ${PERMISSION_RULE_TICKET_TABLE} (expires_at_ms)`);
|
|
245
298
|
}
|
|
246
299
|
export function buildRuleOwnerKey(owner) {
|
|
@@ -398,6 +451,7 @@ class SqlPermissionRuleStore {
|
|
|
398
451
|
throw new Error("the SQL permission-rule backend does not implement the sync-join landing pipeline (core's applySyncJoin is not on the package's public export face); " +
|
|
399
452
|
"no sync client is wired in this deployment, so this arm has no caller — see permission-rule-store-sql.ts header");
|
|
400
453
|
}
|
|
454
|
+
assertWriteDeltaScopeDurable(delta);
|
|
401
455
|
await this.ensureBucket();
|
|
402
456
|
const cur = await this.read();
|
|
403
457
|
if (cur.rev !== opts.expectedRev)
|
|
@@ -558,6 +612,10 @@ export class SqlRuleApprovalRecordStore {
|
|
|
558
612
|
}
|
|
559
613
|
}
|
|
560
614
|
export const RULE_IMPORT_TICKET_GC_GRACE_MS = 10 * 60_000;
|
|
615
|
+
export const SQL_DB_NOW_MS = {
|
|
616
|
+
tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)",
|
|
617
|
+
pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint",
|
|
618
|
+
};
|
|
561
619
|
export function buildRulePayloadHash(candidates) {
|
|
562
620
|
return createHash("sha256").update(canonicalJson(candidates), "utf8").digest("hex");
|
|
563
621
|
}
|
|
@@ -575,9 +633,9 @@ export class SqlRuleImportTicketStore {
|
|
|
575
633
|
const nowMs = this.now();
|
|
576
634
|
const expiresAtMs = nowMs + input.ttlMs;
|
|
577
635
|
const payloadHash = buildRulePayloadHash(input.candidates);
|
|
578
|
-
await this.db.query(this.q(`INSERT INTO ${PERMISSION_RULE_TICKET_TABLE} (ticket_id, owner_key, principal, approval_id, payload_hash, payload_json, expires_at_ms, consumed_at_ms, created_at_ms)
|
|
579
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)`, `INSERT INTO ${PERMISSION_RULE_TICKET_TABLE} (ticket_id, owner_key, principal, approval_id, payload_hash, payload_json, expires_at_ms, consumed_at_ms, created_at_ms)
|
|
580
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8)`), [
|
|
636
|
+
await this.db.query(this.q(`INSERT INTO ${PERMISSION_RULE_TICKET_TABLE} (ticket_id, owner_key, principal, approval_id, payload_hash, payload_json, expires_at_ms, consumed_at_ms, claim_id, purpose, created_at_ms)
|
|
637
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`, `INSERT INTO ${PERMISSION_RULE_TICKET_TABLE} (ticket_id, owner_key, principal, approval_id, payload_hash, payload_json, expires_at_ms, consumed_at_ms, claim_id, purpose, created_at_ms)
|
|
638
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, NULL, $8, $9)`), [
|
|
581
639
|
input.ticketId,
|
|
582
640
|
buildRuleOwnerKey({ kind: "principal", principal: input.principal }),
|
|
583
641
|
input.principal,
|
|
@@ -585,44 +643,88 @@ export class SqlRuleImportTicketStore {
|
|
|
585
643
|
payloadHash,
|
|
586
644
|
dialectProtocolJsonEncoder(this.db.dialect)(input.candidates, "permission_rule_ticket.payload_json"),
|
|
587
645
|
expiresAtMs,
|
|
646
|
+
input.purpose,
|
|
588
647
|
nowMs,
|
|
589
648
|
]);
|
|
590
649
|
return { ticketId: input.ticketId, approvalId: input.approvalId, payloadHash, expiresAtMs };
|
|
591
650
|
}
|
|
592
|
-
async consume(ticketId, principal, opts) {
|
|
593
|
-
const
|
|
594
|
-
const decisionNowMs = opts?.decisionNowMs ?? stampMs;
|
|
651
|
+
async consume(ticketId, principal, purpose, opts) {
|
|
652
|
+
const decisionNowMs = opts?.decisionNowMs ?? this.now();
|
|
595
653
|
const ownerKey = buildRuleOwnerKey({ kind: "principal", principal });
|
|
596
654
|
const row = await this.readRow(ticketId);
|
|
597
655
|
if (row === undefined)
|
|
598
656
|
return { ok: false, reason: "unknown" };
|
|
599
657
|
if (row.ownerKey !== ownerKey)
|
|
600
658
|
return { ok: false, reason: "wrong-principal" };
|
|
659
|
+
if (row.purpose !== purpose)
|
|
660
|
+
return { ok: false, reason: "wrong-purpose" };
|
|
601
661
|
if (row.consumedAtMs !== undefined)
|
|
602
662
|
return { ok: false, reason: "consumed" };
|
|
603
663
|
if (row.expiresAtMs <= decisionNowMs)
|
|
604
664
|
return { ok: false, reason: "expired" };
|
|
605
|
-
const
|
|
665
|
+
const claimId = randomUUID();
|
|
666
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.tidb}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND consumed_at_ms IS NULL AND expires_at_ms > ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND consumed_at_ms IS NULL AND expires_at_ms > $5`), [claimId, ticketId, ownerKey, purpose, decisionNowMs]);
|
|
606
667
|
if (affected === 1)
|
|
607
|
-
return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash };
|
|
668
|
+
return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
|
|
608
669
|
return { ok: false, reason: "consumed" };
|
|
609
670
|
}
|
|
610
|
-
async release(ticketId, principal, mustRemainValidMs = 0, opts) {
|
|
671
|
+
async release(ticketId, principal, claimId, mustRemainValidMs = 0, opts) {
|
|
611
672
|
const nowMs = opts?.decisionNowMs ?? this.now();
|
|
612
|
-
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL WHERE ticket_id = ? AND owner_key = ? AND consumed_at_ms IS NOT NULL AND expires_at_ms > ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL WHERE ticket_id = $1 AND owner_key = $2 AND consumed_at_ms IS NOT NULL AND expires_at_ms > $
|
|
673
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = $1 AND owner_key = $2 AND claim_id = $3 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > $4`), [ticketId, buildRuleOwnerKey({ kind: "principal", principal }), claimId, nowMs + mustRemainValidMs]);
|
|
674
|
+
return affected === 1;
|
|
675
|
+
}
|
|
676
|
+
async settle(ticketId, principal, claimId, outcome) {
|
|
677
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET redeemed_outcome = ?, settled_at_ms = ${SQL_DB_NOW_MS.tidb} WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET redeemed_outcome = $1, settled_at_ms = ${SQL_DB_NOW_MS.pg} WHERE ticket_id = $2 AND owner_key = $3 AND claim_id = $4 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL`), [
|
|
678
|
+
dialectProtocolJsonEncoder(this.db.dialect)(outcome, "permission_rule_ticket.redeemed_outcome"),
|
|
679
|
+
ticketId,
|
|
680
|
+
buildRuleOwnerKey({ kind: "principal", principal }),
|
|
681
|
+
claimId,
|
|
682
|
+
]);
|
|
613
683
|
return affected === 1;
|
|
614
684
|
}
|
|
685
|
+
async peek(ticketId, principal, purpose) {
|
|
686
|
+
const row = await this.readRow(ticketId);
|
|
687
|
+
if (row === undefined || row.ownerKey !== buildRuleOwnerKey({ kind: "principal", principal }) || row.purpose !== purpose)
|
|
688
|
+
return undefined;
|
|
689
|
+
if (row.redeemedOutcome !== undefined) {
|
|
690
|
+
let outcome;
|
|
691
|
+
try {
|
|
692
|
+
outcome = JSON.parse(row.redeemedOutcome);
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
throw new Error(`${PERMISSION_RULE_TICKET_TABLE}.redeemed_outcome for ticket ${ticketId} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
696
|
+
}
|
|
697
|
+
return { state: "settled", outcome };
|
|
698
|
+
}
|
|
699
|
+
if (row.consumedAtMs === undefined)
|
|
700
|
+
return { state: "minted" };
|
|
701
|
+
return { state: "claimed", claimAgeMs: Math.max(0, row.dbNowMs - row.consumedAtMs) };
|
|
702
|
+
}
|
|
703
|
+
async reclaim(ticketId, principal, purpose, minClaimAgeMs) {
|
|
704
|
+
const row = await this.readRow(ticketId);
|
|
705
|
+
if (row === undefined || row.ownerKey !== buildRuleOwnerKey({ kind: "principal", principal }) || row.purpose !== purpose)
|
|
706
|
+
return { ok: false };
|
|
707
|
+
if (row.claimId === undefined)
|
|
708
|
+
return { ok: false };
|
|
709
|
+
const claimId = randomUUID();
|
|
710
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.tidb}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND consumed_at_ms <= ${SQL_DB_NOW_MS.tidb} - ? AND redeemed_outcome IS NULL`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND claim_id = $5 AND consumed_at_ms IS NOT NULL AND consumed_at_ms <= ${SQL_DB_NOW_MS.pg} - $6 AND redeemed_outcome IS NULL`), [claimId, ticketId, buildRuleOwnerKey({ kind: "principal", principal }), purpose, row.claimId, minClaimAgeMs]);
|
|
711
|
+
return affected === 1 ? { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId } : { ok: false };
|
|
712
|
+
}
|
|
615
713
|
async readRow(ticketId) {
|
|
616
|
-
const { rows } = await this.db.query(this.q(`SELECT owner_key, approval_id, payload_hash, expires_at_ms, consumed_at_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = ?`, `SELECT owner_key, approval_id, payload_hash, expires_at_ms, consumed_at_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = $1`), [ticketId]);
|
|
714
|
+
const { rows } = await this.db.query(this.q(`SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, redeemed_outcome, ${SQL_DB_NOW_MS.tidb} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = ?`, `SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, redeemed_outcome, ${SQL_DB_NOW_MS.pg} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = $1`), [ticketId]);
|
|
617
715
|
const row = rows[0];
|
|
618
716
|
if (row === undefined)
|
|
619
717
|
return undefined;
|
|
620
718
|
return {
|
|
719
|
+
dbNowMs: Number(row.db_now_ms),
|
|
621
720
|
ownerKey: String(row.owner_key),
|
|
622
721
|
approvalId: String(row.approval_id),
|
|
623
722
|
payloadHash: String(row.payload_hash),
|
|
723
|
+
purpose: String(row.purpose),
|
|
624
724
|
expiresAtMs: Number(row.expires_at_ms),
|
|
625
725
|
...(row.consumed_at_ms === null || row.consumed_at_ms === undefined ? {} : { consumedAtMs: Number(row.consumed_at_ms) }),
|
|
726
|
+
...(row.claim_id === null || row.claim_id === undefined ? {} : { claimId: String(row.claim_id) }),
|
|
727
|
+
...(row.redeemed_outcome === null || row.redeemed_outcome === undefined ? {} : { redeemedOutcome: String(row.redeemed_outcome) }),
|
|
626
728
|
};
|
|
627
729
|
}
|
|
628
730
|
}
|
|
@@ -635,8 +737,8 @@ export function createSqlPermissionRuleStores(db, now) {
|
|
|
635
737
|
approvals: new SqlRuleApprovalRecordStore(db, now),
|
|
636
738
|
tickets: new SqlRuleImportTicketStore(db, now),
|
|
637
739
|
reapExpired: async (nowMs) => {
|
|
638
|
-
const deadTickets = await db.query(q(`DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE expires_at_ms <= ? LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ctid IN (SELECT ctid FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE expires_at_ms <= $1 LIMIT ${RULE_REAP_BATCH})`), [nowMs - RULE_IMPORT_TICKET_GC_GRACE_MS]);
|
|
639
|
-
const orphanPending = await db.query(q(`DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE state = 'pending' AND created_at_ms < ? LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE ctid IN (SELECT ctid FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE state = 'pending' AND created_at_ms < $1 LIMIT ${RULE_REAP_BATCH})`), [nowMs - RULE_PENDING_APPROVAL_RETENTION_MS]);
|
|
740
|
+
const deadTickets = await db.query(q(`DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ?) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.tidb} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.tidb} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ctid IN (SELECT ctid FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= $1) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.pg} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH})`), [nowMs - RULE_IMPORT_TICKET_GC_GRACE_MS]);
|
|
741
|
+
const orphanPending = await db.query(q(`DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE state = 'pending' AND created_at_ms < ? AND NOT EXISTS (SELECT 1 FROM ${PERMISSION_RULE_TICKET_TABLE} t WHERE t.approval_id = ${PERMISSION_RULE_APPROVAL_TABLE}.record_id) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE ctid IN (SELECT a.ctid FROM ${PERMISSION_RULE_APPROVAL_TABLE} a WHERE a.state = 'pending' AND a.created_at_ms < $1 AND NOT EXISTS (SELECT 1 FROM ${PERMISSION_RULE_TICKET_TABLE} t WHERE t.approval_id = a.record_id) LIMIT ${RULE_REAP_BATCH})`), [nowMs - RULE_PENDING_APPROVAL_RETENTION_MS]);
|
|
640
742
|
return deadTickets.affected + orphanPending.affected;
|
|
641
743
|
},
|
|
642
744
|
countBuckets: async () => {
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ensurePgOutcomeLedgerSchema as ensureOutcomeLedgerSchema } from "./outc
|
|
|
8
8
|
import { ensureSchema as ensureImageIndexSchema } from "./image-index-sql.js";
|
|
9
9
|
import { ensurePgImageBakeSchema as ensureImageBakeSchema } from "./image-bake-store-sql.js";
|
|
10
10
|
import { ensurePgApprovalAskSchema } from "./approval-ask-store-sql.js";
|
|
11
|
+
import { ensurePgMemoryOptOutGrantSchema } from "./memory-optout-grant-store-sql.js";
|
|
11
12
|
import { ensurePgAdoptionLogSchema } from "./adoption-log-sql.js";
|
|
12
13
|
import { ensurePgPermissionRuleSchema } from "./permission-rule-store-sql.js";
|
|
13
14
|
import { ensurePgLeaderRunSchema } from "./leader-run-store-sql.js";
|
|
@@ -165,6 +166,63 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
165
166
|
created_at TIMESTAMPTZ(3) NOT NULL
|
|
166
167
|
)`,
|
|
167
168
|
`CREATE INDEX IF NOT EXISTS idx_snapshot_blob_created ON snapshot_blob (created_at)`,
|
|
169
|
+
`CREATE TABLE IF NOT EXISTS file_history_scope (
|
|
170
|
+
scope VARCHAR(64) COLLATE "C" NOT NULL,
|
|
171
|
+
created_at_ms BIGINT NOT NULL,
|
|
172
|
+
PRIMARY KEY (scope)
|
|
173
|
+
)`,
|
|
174
|
+
`CREATE TABLE IF NOT EXISTS file_history_tracked (
|
|
175
|
+
scope VARCHAR(64) COLLATE "C" NOT NULL,
|
|
176
|
+
key_hash CHAR(64) COLLATE "C" NOT NULL,
|
|
177
|
+
track_key TEXT COLLATE "C" NOT NULL,
|
|
178
|
+
key_kind VARCHAR(16) COLLATE "C" NOT NULL,
|
|
179
|
+
key_family VARCHAR(8) COLLATE "C" NOT NULL,
|
|
180
|
+
base_available BOOLEAN NOT NULL,
|
|
181
|
+
observed_later BOOLEAN NOT NULL DEFAULT FALSE,
|
|
182
|
+
created_at_ms BIGINT NOT NULL,
|
|
183
|
+
PRIMARY KEY (scope, key_hash)
|
|
184
|
+
)`,
|
|
185
|
+
`CREATE TABLE IF NOT EXISTS file_history_version (
|
|
186
|
+
scope VARCHAR(64) COLLATE "C" NOT NULL,
|
|
187
|
+
key_hash CHAR(64) COLLATE "C" NOT NULL,
|
|
188
|
+
version INT NOT NULL,
|
|
189
|
+
blob_hash CHAR(64) COLLATE "C" NULL,
|
|
190
|
+
mode INT NULL,
|
|
191
|
+
parent_ident TEXT COLLATE "C" NULL,
|
|
192
|
+
backup_time VARCHAR(40) COLLATE "C" NOT NULL,
|
|
193
|
+
PRIMARY KEY (scope, key_hash, version)
|
|
194
|
+
)`,
|
|
195
|
+
`CREATE INDEX IF NOT EXISTS idx_file_history_version_blob ON file_history_version (blob_hash)`,
|
|
196
|
+
`CREATE TABLE IF NOT EXISTS file_history_boundary (
|
|
197
|
+
scope VARCHAR(64) COLLATE "C" NOT NULL,
|
|
198
|
+
entry_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
199
|
+
published_at_ms BIGINT NOT NULL,
|
|
200
|
+
publish_seq BIGINT NOT NULL,
|
|
201
|
+
PRIMARY KEY (scope, entry_id),
|
|
202
|
+
CONSTRAINT uq_file_history_boundary_seq UNIQUE (scope, publish_seq)
|
|
203
|
+
)`,
|
|
204
|
+
`CREATE TABLE IF NOT EXISTS file_history_boundary_file (
|
|
205
|
+
scope VARCHAR(64) COLLATE "C" NOT NULL,
|
|
206
|
+
entry_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
207
|
+
key_hash CHAR(64) COLLATE "C" NOT NULL,
|
|
208
|
+
version INT NOT NULL,
|
|
209
|
+
PRIMARY KEY (scope, entry_id, key_hash)
|
|
210
|
+
)`,
|
|
211
|
+
`CREATE INDEX IF NOT EXISTS idx_file_history_bf_key ON file_history_boundary_file (scope, key_hash)`,
|
|
212
|
+
`CREATE TABLE IF NOT EXISTS file_history_reaped_entry (
|
|
213
|
+
scope VARCHAR(64) COLLATE "C" NOT NULL,
|
|
214
|
+
entry_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
215
|
+
reaped_at_ms BIGINT NOT NULL,
|
|
216
|
+
PRIMARY KEY (scope, entry_id)
|
|
217
|
+
)`,
|
|
218
|
+
`CREATE TABLE IF NOT EXISTS file_history_blob (
|
|
219
|
+
blob_hash CHAR(64) COLLATE "C" PRIMARY KEY,
|
|
220
|
+
byte_len BIGINT NOT NULL,
|
|
221
|
+
-- bytes NULLABLE = the same two-backend (SQL inline / MinIO index-only) contract as snapshot_blob above.
|
|
222
|
+
bytes BYTEA NULL,
|
|
223
|
+
created_at TIMESTAMPTZ(3) NOT NULL
|
|
224
|
+
)`,
|
|
225
|
+
`CREATE INDEX IF NOT EXISTS idx_file_history_blob_created ON file_history_blob (created_at)`,
|
|
168
226
|
`CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
169
227
|
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
170
228
|
ordinal INT NOT NULL,
|
|
@@ -305,6 +363,7 @@ export async function ensurePgSchema(pool) {
|
|
|
305
363
|
await ensureImageIndexSchema(client);
|
|
306
364
|
await ensureImageBakeSchema(client);
|
|
307
365
|
await ensurePgApprovalAskSchema((text, params) => client.query(text, params));
|
|
366
|
+
await ensurePgMemoryOptOutGrantSchema((text, params) => client.query(text, params));
|
|
308
367
|
await ensurePgAdoptionLogSchema((text, params) => client.query(text, params));
|
|
309
368
|
await ensurePgPermissionRuleSchema((text, params) => client.query(text, params));
|
|
310
369
|
await ensurePgLeaderRunSchema((text, params) => client.query(text, params));
|
|
@@ -10,7 +10,7 @@ export declare function pgLosslessJsonStringify(value: unknown): string;
|
|
|
10
10
|
* 剥离等),CAS/rev 语义的臂必须先 detect 再变换。 */
|
|
11
11
|
export declare function pgHasUnstorable(v: unknown): boolean;
|
|
12
12
|
/** codex R4→R7 终形:协议数据(approval args/checkpoint/ctx/outcome/rules/memory)一律**拒绝式**。
|
|
13
|
-
* 信封方案历经 R4-R7
|
|
13
|
+
* 信封方案历经 R4-R7 四轮交叉复审全灭:行内判别子恒可被 ≤1.240 老行/用户数据撞形(静默解错=approval
|
|
14
14
|
* 信任边界破),进程旗恒有混旗副本窗(写读不同解)。唯一无歧义解=**脏值不进库**:PG 存不了
|
|
15
15
|
* NUL 是后端能力边界,fail-loud 暴露(清晰文案)严格优于修前的 22P05 神秘崩——两者都存不进,
|
|
16
16
|
* 但拒绝式下 review==execute 恒成立(库里只有干净值,展示面=执行面永逐位一致)。 */
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { ExecutionError, FileError, RemoteExecutionError, type ExecutionEnv, type ExecutionEnvFactory, type ExecStreamOptions, type FileInfo, type OutputChunk, type RemoteConnectConfig, type RemoteExecutionEnv, type Result, type SessionToken, type SnapshotId, type VmLifecycleOptions, type WorkspaceHandle, type WriteExpectation, type WriteReceipt } from "@sema-agent/core";
|
|
2
|
+
import { DEVICE_RATE_LIMIT_CODE, type DeviceErrorCode, type DeviceOwner, type DeviceRow, type DeviceStore } from "../device-store.js";
|
|
3
|
+
import type { DeviceEnrollment } from "../device-enrollment.js";
|
|
4
|
+
import type { DeviceWsHub } from "../device-ws-hub.js";
|
|
5
|
+
import type { DeviceInstructionKind, DeviceOutcomeErrorCode } from "../device-ws-protocol.js";
|
|
6
|
+
/** `WorkspaceHandle.provider` 与 `REMOTE_EXEC` 词表的那一个词,单点。 */
|
|
7
|
+
export declare const DEVICE_PROVIDER: "device";
|
|
8
|
+
/** core 没从包根导出 `ExecutionEnvExecOptions` —— 从接口上取,与六条兄弟腿逐字同姿势
|
|
9
|
+
* (`remote-env-host.ts` 的 `type ExecOpts = Parameters<ExecutionEnv["exec"]>[1]`)。 */
|
|
10
|
+
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
11
|
+
/**
|
|
12
|
+
* 设备**结局帧**的错误码 → core `FileErrorCode`。
|
|
13
|
+
*
|
|
14
|
+
* `Record<DeviceOutcomeErrorCode, …>` 是**穷举**的:协议的结局码闭集加词而此处不加行 = 编译红
|
|
15
|
+
* (#157 词表纪律;不是运行时 switch 的默认臂)。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 三条不许折错的线:
|
|
18
|
+
* ① `outcome_unknown` **原样过境**——core 把它排除在 `RETRYABLE_REMOTE_ERROR_CODES` 之外正是为了
|
|
19
|
+
* 「可能已经执行过」这一格;折成 `timeout`/`transport_lost` 会让上层按可重试处理,重驱已提交副作用。
|
|
20
|
+
* ② `target_unavailable`(never-started 的诚实词)在 `FileErrorCode` 上**没有对位词** —— 落
|
|
21
|
+
* `transport_lost`:它的契约是「连接故事已知、重连后可重试」,而 never-started 的重试确实是安全的。
|
|
22
|
+
* 诚实残余:file 面因此**分不出**「从未开始」与「传输中断」,两者都落在同一个可重试桶里(方向对、
|
|
23
|
+
* 粒度粗)。要分得开需要 core 在 `FileErrorCode` 上补 never-started 词 —— 已作 O12 的后续请托登记。
|
|
24
|
+
* ③ exec 专属词(`shell_unavailable`/`spawn_error`/`callback_error`/`suspended`/`auth_failed`)出现在
|
|
25
|
+
* file 面上是**设备端协议违约**(它把一个 exec 结局贴到一次文件操作上)⇒ 落 `unknown`,不猜。
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEVICE_OUTCOME_TO_FILE_ERROR: Readonly<Record<DeviceOutcomeErrorCode, FileError["code"]>>;
|
|
28
|
+
/**
|
|
29
|
+
* 设备**结局帧**的错误码 → core `ExecutionErrorCode`。穷举同上。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 file 专属词(`already_exists`/`is_directory`/`not_directory`/`not_found`/`permission_denied`/
|
|
32
|
+
* `precondition_failed`/`invalid`)出现在 exec 面同样是协议违约 ⇒ `unknown`,不猜。
|
|
33
|
+
* `not_supported` 在 `ExecutionErrorCode` 上无对位词 —— 落 `shell_unavailable`(它的契约就是「命令
|
|
34
|
+
* **从未启动**,因为可执行面解析不了」,与「设备没有这条指令面」同侧;never-started 语义保住)。
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEVICE_OUTCOME_TO_EXEC_ERROR: Readonly<Record<DeviceOutcomeErrorCode, ExecutionError["code"]>>;
|
|
37
|
+
type RejectCode = DeviceErrorCode | typeof DEVICE_RATE_LIMIT_CODE;
|
|
38
|
+
/**
|
|
39
|
+
* 汇聚端的 **typed 拒**(投递门**之前**,§5.3 投递边界:设备从未可能见过这条指令)→ core 错误码。
|
|
40
|
+
*
|
|
41
|
+
* 🔴 全部落在**可安全重试**的那一侧,这是投递边界给的硬事实,不是乐观估计。exec 面用 O12 的
|
|
42
|
+
* `target_unavailable`(「目标在 commit 前拒了,命令从未启动」的诚实词);file 面无对位词,同上落
|
|
43
|
+
* `transport_lost`。
|
|
44
|
+
* 例外 `identity_mismatch` / `revoked` / `not_found`:那是**永久**拒(换设备/吊销/不是你的),重试
|
|
45
|
+
* 只会同样失败 —— exec 面落 `auth_failed`(NEVER retry 的那一格),file 面无对位词落 `permission_denied`
|
|
46
|
+
* (「这条路你走不通,而且不是暂时的」是它最近的语义)。
|
|
47
|
+
*/
|
|
48
|
+
export declare const DEVICE_REJECT_TO_EXEC_ERROR: Readonly<Record<RejectCode, ExecutionError["code"]>>;
|
|
49
|
+
export declare const DEVICE_REJECT_TO_FILE_ERROR: Readonly<Record<RejectCode, FileError["code"]>>;
|
|
50
|
+
export interface DeviceEnvLogger {
|
|
51
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
52
|
+
info?(msg: string, fields?: Record<string, unknown>): void;
|
|
53
|
+
}
|
|
54
|
+
export interface DeviceEnvFactoryDeps {
|
|
55
|
+
hub: DeviceWsHub;
|
|
56
|
+
store: DeviceStore;
|
|
57
|
+
enrollment: DeviceEnrollment;
|
|
58
|
+
/** 指令级默认超时(ms;`DEVICE_EXEC_TIMEOUT_MS`,§4.7)。core 的 per-command `timeout` 可收紧它。 */
|
|
59
|
+
execTimeoutMs?: number;
|
|
60
|
+
/** §4.3.3 蜂道:`deviceCwdHonored` 闸下由 resolve-spec 注册的 per-session cwd(**单写者**)。 */
|
|
61
|
+
perSessionCwd?: ReadonlyMap<string, string>;
|
|
62
|
+
perSessionShellEnv?: ReadonlyMap<string, Record<string, string>>;
|
|
63
|
+
logger?: DeviceEnvLogger;
|
|
64
|
+
}
|
|
65
|
+
interface DeviceEnvConfig {
|
|
66
|
+
hub: DeviceWsHub;
|
|
67
|
+
enrollment: DeviceEnrollment;
|
|
68
|
+
rootSessionId: string;
|
|
69
|
+
deviceId: string;
|
|
70
|
+
owner: DeviceOwner;
|
|
71
|
+
cwd: string;
|
|
72
|
+
shellEnv?: Record<string, string>;
|
|
73
|
+
execTimeoutMs: number;
|
|
74
|
+
/** 设备当前连接声明的指令面(`undefined` = 铸 env 时设备不在线 ⇒ 可选面一律缺席)。 */
|
|
75
|
+
supports: ReadonlySet<DeviceInstructionKind> | undefined;
|
|
76
|
+
logger?: DeviceEnvLogger;
|
|
77
|
+
}
|
|
78
|
+
export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
79
|
+
private readonly cfg;
|
|
80
|
+
/**
|
|
81
|
+
* 🔴 `suspendable: false` 在 core 契约里**同时断言**「工作区在目标上外部持久」(remote-env.ts 的
|
|
82
|
+
* load-bearing 不变量)—— 设备工作区 = 员工机磁盘,真持久 ✅,park-only 降级腿因此合法可用。
|
|
83
|
+
* 谁要把 executor 的工作区改成临时目录,先来改这一行:那一刻这个 `false` 就变成谎话,park 赎回会
|
|
84
|
+
* 落到一个空工作区上而链路上没有任何一层会报错。
|
|
85
|
+
*/
|
|
86
|
+
readonly capabilities: {
|
|
87
|
+
isolation: boolean;
|
|
88
|
+
suspendable: boolean;
|
|
89
|
+
};
|
|
90
|
+
readonly externalContentTarget = true;
|
|
91
|
+
/** #211:设备路径不在 worker 宿主文件系统上 —— host-path 回执消费方据此拒绝把它当宿主路径呈现。 */
|
|
92
|
+
readonly hostLocalPaths = false;
|
|
93
|
+
cwd: string;
|
|
94
|
+
/** 可选面(presence-typed,见文件头「铁律」段):构造期按设备声明装配,缺席就是缺席。 */
|
|
95
|
+
readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
|
|
96
|
+
writeFileExclusive?: (path: string, content: string | Uint8Array, abortSignal?: AbortSignal) => Promise<Result<void, FileError>>;
|
|
97
|
+
writeFileGuarded?: (path: string, content: string | Uint8Array, expect: WriteExpectation, abortSignal?: AbortSignal) => Promise<Result<WriteReceipt, FileError>>;
|
|
98
|
+
private connected;
|
|
99
|
+
/** 拆除是**终态**(六条兄弟腿同纪律:destroy 之后 connect/exec/读面一律 fail-closed,不复活)。 */
|
|
100
|
+
private destroyed;
|
|
101
|
+
constructor(cfg: DeviceEnvConfig);
|
|
102
|
+
/**
|
|
103
|
+
* 铸造时装配的**保护型**可选面(guarded / exclusive 写)。`connect()` 拿它与设备**此刻**的声明对拍。
|
|
104
|
+
*
|
|
105
|
+
* 🔴 为什么只对拍这两条而不是全部 supports(codex R1-F2 的修法边界):这两条是 core 契约里
|
|
106
|
+
* 「缺席 ⇒ 调用方按 advisory 降级并自担 TOCTOU」的那一类 —— 它们的缺席是**放宽**方向。其余面
|
|
107
|
+
* (exec/read/…)缺席时调用会得到一条响亮的 `not_supported`,不构成静默降级。
|
|
108
|
+
*/
|
|
109
|
+
private guardFaceSnapshot;
|
|
110
|
+
/**
|
|
111
|
+
* 设备**此刻**多出来的保护面(相对本 env 铸造时的快照)。非空 = 漂移。
|
|
112
|
+
*
|
|
113
|
+
* 🔴 **为什么这条判据不能只守 `connect()`**(codex R2-F3,判真):`connect()` 一辈子只跑一两次,而
|
|
114
|
+
* 设备的 WS 可以**透明重连** —— 断开、以同一 deviceId 连回来,`this.connected` 全程不变,后续
|
|
115
|
+
* `dispatch` 直接用汇聚端的**当前**连接。于是「弱能力连接上铸的 env + 设备连回时带上了 guarded 面」
|
|
116
|
+
* 这一形跳过了 connect 期的闸:core 仍看不到 guarded 方法、仍走非原子写降级,而没有任何一层再问一次。
|
|
117
|
+
* 所以判据要落在**每一次非原子写之前**(那正是被降级的那条路),connect 期的检查是它的早期快门。
|
|
118
|
+
*/
|
|
119
|
+
private guardFaceDrift;
|
|
120
|
+
/**
|
|
121
|
+
* 铸造时**缺席**的保护面 —— 交给 hub 在投递门上原子复核(codex R3-F1)。
|
|
122
|
+
*
|
|
123
|
+
* adapter 侧的 {@link guardFaceDrift} 是**早期快门**:它与真正的下发之间隔着 `admitSession` 的一次
|
|
124
|
+
* SQL 往返,设备可以在那个窗口里以更强的能力连回来。投递门是本进程里唯一同时持有「最终选中的连接」
|
|
125
|
+
* 与「不可回退的 commit」的地方,所以权威判据在那儿;这里只负责把「我是在没有它们的前提下铸的」这句
|
|
126
|
+
* 话原样送过去。
|
|
127
|
+
*/
|
|
128
|
+
private absentGuardFaces;
|
|
129
|
+
private driftError;
|
|
130
|
+
private resolve;
|
|
131
|
+
absolutePath(p: string): Promise<Result<string, FileError>>;
|
|
132
|
+
joinPath(parts: string[]): Promise<Result<string, FileError>>;
|
|
133
|
+
workspaceHandle(): WorkspaceHandle;
|
|
134
|
+
/**
|
|
135
|
+
* 不建连接:做**准入 + 取活连接**(文件头「唯一范式差」)。
|
|
136
|
+
*
|
|
137
|
+
* 🔴 **绝不换设备**:准入解出的 deviceId 与本 env 铸造时钉住的那一台不符 ⇒ typed
|
|
138
|
+
* `device.identity_mismatch`(lane 是身份不是偏好,§4.5 步 2)。这一格正是 park 赎回期换绑/吊销的
|
|
139
|
+
* 落点:静默改投等于把一次会话的全部工作区假设搬到另一台机器上。
|
|
140
|
+
*/
|
|
141
|
+
connect(config?: RemoteConnectConfig): Promise<{
|
|
142
|
+
ok: true;
|
|
143
|
+
value: WorkspaceHandle;
|
|
144
|
+
} | {
|
|
145
|
+
ok: false;
|
|
146
|
+
error: RemoteExecutionError;
|
|
147
|
+
}>;
|
|
148
|
+
/** 控制面重连(core 在 transport 重挂时调)——语义与 connect 逐字同一条:重新准入 + 身份等值。 */
|
|
149
|
+
reconnect(_sessionToken: SessionToken): Promise<{
|
|
150
|
+
ok: true;
|
|
151
|
+
value: WorkspaceHandle;
|
|
152
|
+
} | {
|
|
153
|
+
ok: false;
|
|
154
|
+
error: RemoteExecutionError;
|
|
155
|
+
}>;
|
|
156
|
+
suspendVM(_options?: VmLifecycleOptions): Promise<{
|
|
157
|
+
ok: true;
|
|
158
|
+
value: SnapshotId;
|
|
159
|
+
} | {
|
|
160
|
+
ok: false;
|
|
161
|
+
error: RemoteExecutionError;
|
|
162
|
+
}>;
|
|
163
|
+
resumeVM(_snapshotId: SnapshotId, _options?: VmLifecycleOptions): Promise<{
|
|
164
|
+
ok: true;
|
|
165
|
+
value: WorkspaceHandle;
|
|
166
|
+
} | {
|
|
167
|
+
ok: false;
|
|
168
|
+
error: RemoteExecutionError;
|
|
169
|
+
}>;
|
|
170
|
+
postResumeInit(): Promise<{
|
|
171
|
+
ok: true;
|
|
172
|
+
value: void;
|
|
173
|
+
} | {
|
|
174
|
+
ok: false;
|
|
175
|
+
error: RemoteExecutionError;
|
|
176
|
+
}>;
|
|
177
|
+
private unsupported;
|
|
178
|
+
/**
|
|
179
|
+
* 解除本 env 与设备的关联。**不动设备上的工作区文件**(与 adb 腿「destroy 只断开不重置手机」同纪律
|
|
180
|
+
* —— 那是员工自己的盘,§11-O6 才是「要不要清理」这个问题的属主)。幂等,永不抛。
|
|
181
|
+
*/
|
|
182
|
+
destroy(): Promise<void>;
|
|
183
|
+
cleanup(): Promise<void>;
|
|
184
|
+
/** 终态/未连接的 fail-closed 门(exec 面)。 */
|
|
185
|
+
private shellGate;
|
|
186
|
+
/** 终态/未连接的 fail-closed 门(文件面)。 */
|
|
187
|
+
private fsGate;
|
|
188
|
+
private timeoutMsFor;
|
|
189
|
+
private dispatch;
|
|
190
|
+
/**
|
|
191
|
+
* 三态结果 → 文件面的 `Result`。
|
|
192
|
+
*
|
|
193
|
+
* **穷举 switch,无 default 臂**:`DeviceDispatchResult` 加臂而此处不加 = 编译红。三态各自的
|
|
194
|
+
* 「重试安全性」在 hub 的类型注里逐字定义,本函数只做翻译,不重新判断。
|
|
195
|
+
*/
|
|
196
|
+
private fileOutcome;
|
|
197
|
+
/** 结局 kind 与请求面不符 = 设备协议违约:响亮拒,**不猜**(否则一个没发生的操作会被读成成功)。 */
|
|
198
|
+
private wrongOutcome;
|
|
199
|
+
exec(command: string, options?: ExecOpts): Promise<Result<{
|
|
200
|
+
stdout: string;
|
|
201
|
+
stderr: string;
|
|
202
|
+
exitCode: number;
|
|
203
|
+
}, ExecutionError>>;
|
|
204
|
+
/**
|
|
205
|
+
* 流式 exec。契约:按序 yield `stdout`/`stderr`,末尾**恰好一个** `exit` 片;失败在 exit 之前 throw
|
|
206
|
+
* `RemoteExecutionError`(core `OutputChunk` 契约)。
|
|
207
|
+
*
|
|
208
|
+
* 🔴 「恰好一个 exit」不是本层的信任,是**汇聚端已判过**的事实(hub 的 `expectsStreamTerminator`
|
|
209
|
+
* 对声明了流的调用强制唯一终止片,缺了就把整条指令判失败而不是静默尾截断)。本层只负责不把它弄丢。
|
|
210
|
+
*/
|
|
211
|
+
execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
|
|
212
|
+
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
213
|
+
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
214
|
+
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
215
|
+
private doReadLink;
|
|
216
|
+
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
217
|
+
readTextFile(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
218
|
+
readTextLines(p: string, options?: {
|
|
219
|
+
maxLines?: number;
|
|
220
|
+
abortSignal?: AbortSignal;
|
|
221
|
+
}): Promise<Result<string[], FileError>>;
|
|
222
|
+
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
223
|
+
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
224
|
+
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
225
|
+
private doWrite;
|
|
226
|
+
/**
|
|
227
|
+
* O13 守卫写:前置条件与写入在设备上的**同一原子步**里发生,回一份落盘对象收据。
|
|
228
|
+
*
|
|
229
|
+
* 🔴 这是 §4.3.1「把裁决绑定到真实落盘对象」的**唯一**承载。它成立的边界要说清:关掉的是「调用方
|
|
230
|
+
* 铸前置条件」与「写入」之间那个窗;更早的策略裁决(例如等过人类审批的写门判决)不在这条收据里 ——
|
|
231
|
+
* core 的方法注对此已逐字声明,本腿不越界宣称。
|
|
232
|
+
*/
|
|
233
|
+
private doGuardedWrite;
|
|
234
|
+
private warnMissingCreated;
|
|
235
|
+
createDir(p: string, options?: {
|
|
236
|
+
recursive?: boolean;
|
|
237
|
+
abortSignal?: AbortSignal;
|
|
238
|
+
}): Promise<Result<void, FileError>>;
|
|
239
|
+
remove(p: string, options?: {
|
|
240
|
+
recursive?: boolean;
|
|
241
|
+
force?: boolean;
|
|
242
|
+
abortSignal?: AbortSignal;
|
|
243
|
+
}): Promise<Result<void, FileError>>;
|
|
244
|
+
createTempDir(prefix?: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
245
|
+
createTempFile(options?: {
|
|
246
|
+
prefix?: string;
|
|
247
|
+
suffix?: string;
|
|
248
|
+
abortSignal?: AbortSignal;
|
|
249
|
+
}): Promise<Result<string, FileError>>;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* device lane 的 `ExecutionEnvFactory`(`boot/execution-env.ts` 的第七臂)。
|
|
253
|
+
*
|
|
254
|
+
* 🔴 **异步工厂,且在铸 env 之前就把绑定读出来** —— 三个理由,每个都不能省:
|
|
255
|
+
* ① `cwd` 是 `FileSystem` 的**同步属性**:缺省值 = 设备 hello 声明的 `workspaceRoot`,那在库里;
|
|
256
|
+
* ② 可选面的 presence(`writeFileGuarded`…)必须在构造期定(core 降级律,见文件头);
|
|
257
|
+
* ③ 「这个 placement 根本没绑设备」是**装配/准入错误**,不是一次运行期业务拒绝 —— 响亮抛,让 spawn
|
|
258
|
+
* 失败(core 对 `isolation:"worktree"` 的同款姿势:宁可 throw,绝不静默给一个不对的 env)。
|
|
259
|
+
* 对比:「绑了但现在不在线」是运行期事实 ⇒ 构造成功,`connect()` 回 typed `device.offline`。
|
|
260
|
+
*/
|
|
261
|
+
export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
|
|
262
|
+
/**
|
|
263
|
+
* §4.3.3 的 cwd 优先序 —— 与 host 腿的 `effectiveHostWorkspace` **同源**:
|
|
264
|
+
* ① 本 session 显式注册的 cwd(`deviceCwdHonored` 闸下由 resolve-spec 写);
|
|
265
|
+
* ② `ctx.parentCwd`(委派子代:自己没注册过 cwd,继承父的工作目录 —— 少了它子代会丢工作目录);
|
|
266
|
+
* ③ 设备 hello 声明并落库的 `workspaceRoot`(缺 cwd 时的解析基准,有明确 wire 契约,不是「executor
|
|
267
|
+
* 默认根」这种空话)。
|
|
268
|
+
*/
|
|
269
|
+
export declare function effectiveDeviceCwd(sessionCwd: string | undefined, parentCwd: string | undefined, row: Pick<DeviceRow, "workspaceRoot">): string;
|
|
270
|
+
export {};
|
|
271
|
+
//# sourceMappingURL=remote-env-device.d.ts.map
|