@sema-agent/server 7.151.0 → 7.152.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 +266 -0
- package/README.md +15 -3
- package/README.zh-CN.md +10 -2
- package/USAGE.md +93 -64
- package/deploy/sema-up/README.md +5 -4
- package/deploy/sema-up/cluster-up.sh +6 -6
- package/deploy/sema-up/kube-up.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +36 -8
- package/deploy/sema-up/machines.example.yaml +3 -1
- package/deploy/sema-up/sema-up.sh +22 -16
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-ask-machine.d.ts +4 -10
- package/dist/approval-ask-retention.d.ts +1 -1
- package/dist/approval-card.d.ts +10 -12
- package/dist/approval-gate-currency.d.ts +8 -8
- package/dist/approval-gate-currency.js +3 -4
- package/dist/approval-hmac.d.ts +17 -1
- package/dist/approval-hmac.js +7 -1
- package/dist/approval-queue-kind.d.ts +1 -1
- package/dist/approval.d.ts +2 -2
- package/dist/approval.js +2 -2
- package/dist/bind-host.d.ts +1 -3
- package/dist/bind-host.js +2 -3
- package/dist/boot/coordinators.d.ts +1 -1
- package/dist/boot/coordinators.js +9 -10
- package/dist/boot/device-lane.js +2 -0
- package/dist/boot/execution-env.d.ts +1 -0
- package/dist/boot/execution-env.js +31 -5
- package/dist/boot/leader.d.ts +31 -30
- package/dist/boot/leader.js +38 -28
- package/dist/boot/leg-path-adjudication.d.ts +40 -0
- package/dist/boot/leg-path-adjudication.js +18 -0
- package/dist/boot/parked-revive-gate.js +3 -15
- package/dist/boot/reapers.d.ts +1 -1
- package/dist/boot/reapers.js +1 -1
- package/dist/boot/resolve-spec.js +12 -26
- package/dist/boot/runner-deps.d.ts +39 -35
- package/dist/boot/runner-deps.js +92 -89
- package/dist/boot/shutdown.d.ts +6 -0
- package/dist/boot/shutdown.js +10 -4
- package/dist/boot/stage-02-web-admin-pull.js +4 -5
- package/dist/boot/stage-05-execution-env.js +2 -1
- package/dist/boot/stage-06-runners.d.ts +2 -0
- package/dist/boot/stage-06-runners.js +10 -4
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +8 -0
- package/dist/boot/stage-10-http-server.js +11 -15
- package/dist/boot/stores.js +7 -4
- package/dist/boot/web-admin.js +2 -7
- package/dist/capabilities/approvals-push.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +28 -13
- package/dist/capabilities/scenarios.js +2 -2
- package/dist/capability-facts.d.ts +22 -20
- package/dist/capability-facts.js +18 -34
- package/dist/card-rule-offers.d.ts +32 -0
- package/dist/card-rule-offers.js +130 -0
- package/dist/config-catalog.js +12 -11
- package/dist/config-dir-trust.js +4 -2
- package/dist/config-invariants.d.ts +18 -7
- package/dist/config-invariants.js +51 -8
- package/dist/config-lkg.d.ts +5 -19
- package/dist/config-lkg.js +8 -62
- package/dist/config-provider.d.ts +1 -1
- package/dist/config-root-migration.d.ts +2 -2
- package/dist/config-types.d.ts +28 -38
- package/dist/config.d.ts +7 -5
- package/dist/config.js +55 -16
- package/dist/control-socket.d.ts +51 -0
- package/dist/control-socket.js +115 -0
- package/dist/deployment-governance.d.ts +2 -1
- package/dist/device-ws-hub.d.ts +7 -0
- package/dist/device-ws-hub.js +7 -1
- package/dist/elicitation.js +2 -2
- package/dist/entry-problems.d.ts +40 -3
- package/dist/entry-problems.js +3 -1
- package/dist/execution-env-refusal.d.ts +8 -2
- package/dist/execution-env-refusal.js +3 -0
- package/dist/fleet/engine-signal.d.ts +12 -29
- package/dist/fleet/engine-signal.js +1 -7
- package/dist/fleet-client.js +2 -5
- package/dist/gate-settlement.d.ts +1 -8
- package/dist/gate-settlement.js +0 -4
- package/dist/history-anchor.js +1 -1
- package/dist/hitl-delivery.d.ts +2 -3
- package/dist/hitl-delivery.js +0 -2
- package/dist/hooks/hook-llm.js +4 -2
- package/dist/hooks/hook-runner.d.ts +3 -2
- package/dist/hooks/hook-runner.js +16 -1
- package/dist/host-decision.d.ts +11 -11
- package/dist/http/active-run-conflict.d.ts +35 -2
- package/dist/http/active-run-conflict.js +48 -19
- package/dist/http/approval-sweeps.d.ts +1 -1
- package/dist/http/approval-sweeps.js +5 -2
- package/dist/http/checkpoint-ctx.d.ts +2 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/dispatch.d.ts +6 -2
- package/dist/http/dispatch.js +25 -5
- package/dist/http/durable-run-leg.js +0 -1
- package/dist/http/health-reader.d.ts +16 -9
- package/dist/http/health-reader.js +2 -1
- package/dist/http/health-self-report.js +1 -1
- package/dist/http/owner-access.d.ts +2 -3
- package/dist/http/owner-access.js +3 -4
- package/dist/http/principal-gate.d.ts +49 -21
- package/dist/http/principal-gate.js +61 -11
- package/dist/http/request-channel.d.ts +23 -0
- package/dist/http/request-channel.js +9 -0
- package/dist/http/request-guards.js +17 -2
- package/dist/http/resume-ctx-read.js +20 -3
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +111 -41
- package/dist/http/route-ctx.d.ts +73 -7
- package/dist/http/route-ctx.js +17 -0
- package/dist/http/route-table.d.ts +4 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-config-refresh.js +3 -3
- package/dist/http/routes/admin-drain.d.ts +3 -0
- package/dist/http/routes/admin-drain.js +7 -7
- package/dist/http/routes/admin-memory-optout.js +3 -3
- package/dist/http/routes/adoption.js +3 -5
- package/dist/http/routes/approvals-assistant.d.ts +2 -1
- package/dist/http/routes/approvals-assistant.js +95 -48
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +30 -5
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +19 -7
- package/dist/http/routes/images.js +10 -10
- package/dist/http/routes/memory-bundle.js +3 -3
- package/dist/http/routes/memory-compliance.js +3 -3
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +3 -3
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/observability.js +3 -3
- package/dist/http/routes/retention-ops.js +3 -3
- package/dist/http/routes/rules.js +5 -5
- package/dist/http/routes/run-permission-mode.d.ts +2 -1
- package/dist/http/routes/run-permission-mode.js +2 -2
- package/dist/http/routes/runs.d.ts +4 -9
- package/dist/http/routes/runs.js +73 -33
- package/dist/http/routes/session-sync.d.ts +18 -6
- package/dist/http/routes/session-sync.js +38 -36
- package/dist/http/routes/sessions.js +20 -9
- package/dist/http/routes/tasks.js +28 -37
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +9 -3
- package/dist/http/server.js +15 -3
- package/dist/http/submit-ctx.d.ts +1 -1
- package/dist/http/submit-ctx.js +2 -0
- package/dist/human-facing-args.d.ts +33 -0
- package/dist/human-facing-args.js +33 -0
- package/dist/index.d.ts +1 -1
- package/dist/k8s-api-credentials.d.ts +29 -0
- package/dist/k8s-api-credentials.js +25 -0
- package/dist/lane-home-probe.d.ts +1 -1
- package/dist/lane-home-probe.js +2 -1
- package/dist/leader/diffup.d.ts +19 -0
- package/dist/leader/diffup.js +7 -0
- package/dist/leader/leader.d.ts +114 -10
- package/dist/leader/leader.js +69 -5
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +2 -1
- package/dist/leader/run-entry.js +1 -1
- package/dist/leader/wire.d.ts +42 -132
- package/dist/leader/wire.js +81 -67
- package/dist/memory-scope.d.ts +20 -9
- package/dist/memory-scope.js +15 -3
- package/dist/model-entry-headers.d.ts +9 -10
- package/dist/model-entry-headers.js +0 -4
- package/dist/model-entry-refusal.d.ts +7 -4
- package/dist/model-entry-refusal.js +11 -5
- package/dist/model-plane-face.d.ts +1 -1
- package/dist/model-plane-face.js +3 -1
- package/dist/model-route-endpoint.d.ts +13 -7
- package/dist/model-route-endpoint.js +14 -11
- package/dist/model-select.d.ts +22 -0
- package/dist/model-select.js +9 -0
- package/dist/model-unreachable-notice.d.ts +11 -0
- package/dist/model-unreachable-notice.js +4 -4
- package/dist/object-store-buckets.d.ts +640 -6
- package/dist/object-store-buckets.js +166 -0
- package/dist/observability/fail-open.d.ts +20 -16
- package/dist/observability/fail-open.js +20 -16
- package/dist/observability/logger.js +3 -1
- package/dist/operator-roster.d.ts +68 -0
- package/dist/operator-roster.js +32 -0
- package/dist/orchestration/workflow-park-decision.d.ts +1 -1
- package/dist/outbound-dispatcher.d.ts +71 -8
- package/dist/outbound-dispatcher.js +48 -7
- package/dist/owner-faces.d.ts +1 -1
- package/dist/parked-decide.d.ts +2 -2
- package/dist/permission-mode-admission.d.ts +59 -26
- package/dist/permission-mode-admission.js +37 -10
- package/dist/plugins/approval-ask-store-file.d.ts +4 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
- package/dist/plugins/approval-ask-store-sql.js +12 -1
- package/dist/plugins/blob-backend.d.ts +83 -7
- package/dist/plugins/blob-backend.js +57 -6
- package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
- package/dist/plugins/checkpoint-index-columns.js +3 -6
- package/dist/plugins/checkpoint-store-face.d.ts +119 -61
- package/dist/plugins/checkpoint-store-face.js +134 -55
- package/dist/plugins/landed-attendants.d.ts +2 -2
- package/dist/plugins/local-checkpoint-store.d.ts +13 -3
- package/dist/plugins/local-checkpoint-store.js +38 -49
- package/dist/plugins/one-time-migrations.d.ts +51 -21
- package/dist/plugins/one-time-migrations.js +67 -45
- package/dist/plugins/pg-pool.js +11 -9
- package/dist/plugins/remote-env-host.d.ts +29 -10
- package/dist/plugins/remote-env-host.js +99 -19
- package/dist/plugins/remote-env-k8s.d.ts +68 -2
- package/dist/plugins/remote-env-k8s.js +107 -24
- package/dist/plugins/replica-registration.d.ts +1 -1
- package/dist/plugins/replica-registration.js +1 -1
- package/dist/plugins/s3-presign.d.ts +23 -0
- package/dist/plugins/s3-presign.js +37 -3
- package/dist/plugins/scheduler-support.d.ts +2 -2
- package/dist/plugins/scheduler-support.js +0 -3
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-file-ledger.d.ts +2 -2
- package/dist/plugins/send-file-ledger.js +2 -2
- package/dist/plugins/send-user-file.d.ts +3 -4
- package/dist/plugins/send-user-file.js +56 -52
- package/dist/plugins/session-blob-claim-token.d.ts +15 -0
- package/dist/plugins/session-blob-claim-token.js +20 -0
- package/dist/plugins/session-blob-namespace.d.ts +46 -0
- package/dist/plugins/session-blob-namespace.js +116 -0
- package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
- package/dist/plugins/snapshot-bucket-presence.js +8 -2
- package/dist/plugins/sql-json-column.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +44 -2
- package/dist/plugins/store-backend.js +75 -53
- package/dist/plugins/task-attachment-store.d.ts +2 -1
- package/dist/plugins/task-attachment-store.js +5 -1
- package/dist/plugins/tidb-pool.js +19 -24
- package/dist/plugins/workspace-custody.d.ts +4 -3
- package/dist/question.d.ts +2 -1
- package/dist/question.js +5 -13
- package/dist/request-form.d.ts +6 -1
- package/dist/request-form.js +4 -1
- package/dist/rule-issue-reading.d.ts +1 -1
- package/dist/rules-consent.d.ts +1 -1
- package/dist/run-local.js +8 -9
- package/dist/runs.d.ts +17 -8
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +2 -2
- package/dist/sema-config-dir.d.ts +2 -1
- package/dist/session-sync.d.ts +27 -11
- package/dist/session-sync.js +15 -7
- package/dist/stores-sql/blob-index-sql.d.ts +30 -9
- package/dist/stores-sql/blob-index-sql.js +40 -21
- package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
- package/dist/stores-sql/checkpoint-store-sql.js +70 -146
- package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
- package/dist/stores-sql/file-history-store-sql.js +100 -28
- package/dist/stores-sql/retention-store-sql.js +3 -9
- package/dist/task-settings.d.ts +6 -5
- package/dist/task-settings.js +3 -3
- package/dist/tool-approval.d.ts +342 -518
- package/dist/tool-approval.js +498 -1207
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/ledger-events.d.ts +3 -3
- package/dist/trace/project.d.ts +2 -3
- package/dist/trace/redact.js +3 -2
- package/dist/trace/self-minted-wire.d.ts +287 -157
- package/dist/trace/self-minted-wire.js +93 -48
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/read-warnings.js +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/dist/workspace-space.d.ts +10 -4
- package/dist/workspace-space.js +4 -1
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
- package/package.json +6 -2
- package/scripts/embed-minimal.mjs +3 -3
- package/wire-changes/server@7.152.0.json +900 -0
- package/dist/config-d-migration.d.ts +0 -37
- package/dist/config-d-migration.js +0 -60
|
@@ -46,7 +46,8 @@ export interface HostEnvConfig extends LaneHomeConfig {
|
|
|
46
46
|
* user's OWN directory (e.g. the cwd `run-local` was invoked in), not an adapter-created ephemeral sandbox,
|
|
47
47
|
* so the agent's file edits PERSIST and are user-visible (like a local coding agent working in your repo).
|
|
48
48
|
* Takes precedence over `workspaceBase`/`id` (those are ignored). Default unset = ephemeral random + cleanup.
|
|
49
|
-
* 唯一的例外由托管表说:它落在一只本服务铸的临时根里({@link custody} 的 `sharedRoot`)
|
|
49
|
+
* 唯一的例外由托管表说:它落在一只本服务铸的临时根里 ⇒ 本环境(也)持有那只根 —— 铸时就判得出的({@link custody} 的 `sharedRoot`)从铸起共持;
|
|
50
|
+
* 铸时判不出、在那只根的最后一只持有者放手那一拍读得出地落在里面的,当场接过删除({@link RemoteHostExecutionEnv.assumeRoot})。
|
|
50
51
|
*/
|
|
51
52
|
workspaceDir?: string;
|
|
52
53
|
/**
|
|
@@ -72,10 +73,16 @@ export interface HostEnvConfig extends LaneHomeConfig {
|
|
|
72
73
|
}
|
|
73
74
|
/** 工作区托管表交给它铸的环境的那一面(`workspace-custody.ts`;host 车道的读法)。 */
|
|
74
75
|
export interface HostWorkspaceCustodySeat {
|
|
75
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* 铸时那一判:`workspaceDir` 落在这只本服务铸的临时根里(委派子代按父的工作目录落根)⇒ 本环境从铸起与它的持有者共持这只根。
|
|
78
|
+
* 这是删除责任的**提前交接**,不是删除的闸 —— 删不删只由 {@link passOn} 在放手那一拍答。
|
|
79
|
+
*/
|
|
76
80
|
readonly sharedRoot?: string;
|
|
77
|
-
/**
|
|
78
|
-
|
|
81
|
+
/**
|
|
82
|
+
* 删前那一判(本环境放手 `root` 的同一同步段里问;同步、永不抛):「此刻还有谁在 `root` 里」—— 另有持有者 / 有还没离场的环境读得出地落在里面(它当场接过)/
|
|
83
|
+
* 有读不出的(留给退出兜底)⇒ `true`,不删;读得出地没人 ⇒ `false`,本环境在同一同步段里把它移出原路径再删。
|
|
84
|
+
*/
|
|
85
|
+
passOn(self: RemoteHostExecutionEnv, root: string): boolean;
|
|
79
86
|
}
|
|
80
87
|
export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, SchedulerCapability, BackgroundShellCapability, CustodyMember<never> {
|
|
81
88
|
/** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
|
|
@@ -191,8 +198,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
191
198
|
*/
|
|
192
199
|
private drainInflightFs;
|
|
193
200
|
private ensureSpoolRoot;
|
|
194
|
-
/** 本环境对它工作区的租期({@link HostWorkspaceTenure})
|
|
201
|
+
/** 本环境对它工作区的租期({@link HostWorkspaceTenure}):借用的永不删;持有的收场放手、放手那一拍判得没人才删。唯一写处 = 构造、{@link assumeRoot} 与 {@link removeOwnedTrees}。 */
|
|
195
202
|
private tenure;
|
|
203
|
+
/** 离场({@link left}):唯一写处 = {@link removeOwnedTrees} 的放手段。 */
|
|
204
|
+
private hasLeft;
|
|
196
205
|
/** Memoized lazy `mkdir -p` so concurrent first-use prepares the workspace exactly once. */
|
|
197
206
|
private preparing?;
|
|
198
207
|
/**
|
|
@@ -208,10 +217,20 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
208
217
|
get released(): boolean;
|
|
209
218
|
/** 本车道收场不欠同一身份下一只环境任何账(删不掉的根留在退出兜底簿上,不交接)。 */
|
|
210
219
|
readonly leftover: undefined;
|
|
211
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* 离场:本环境自己的收场放手那一段({@link removeOwnedTrees},在飞写落地之后)跑过。删前那一判只问还没离场的环境「你在不在这只根里」——
|
|
222
|
+
* 收场开始了而写还没落地的仍在场(它的写还会落进去);离场之后它不再动工作区(残余镜像写只许落在借用目录 / spool 根上)。
|
|
223
|
+
*/
|
|
224
|
+
get left(): boolean;
|
|
225
|
+
/** 收场落定且已离场 ⇒ 出托管表(超时臂里放手挂在在飞写之后:落定了还没离场 ⇒ 仍在表里,借用的与持有的同一句)。 */
|
|
212
226
|
get settled(): boolean;
|
|
213
|
-
/** 本环境此刻持有的、本服务铸的临时根(
|
|
214
|
-
get
|
|
227
|
+
/** 本环境此刻持有的、本服务铸的临时根(托管表的持有读法读它;正常流至多一只);借用 / 已放手 ⇒ 空。 */
|
|
228
|
+
get heldRoots(): readonly string[];
|
|
229
|
+
/**
|
|
230
|
+
* 删前那一判把 `root` 的删除交给本环境。唯一调用方 = 工厂的 {@link passOnRoot}(与放手者的判在同一同步段;前提:本环境还没离场、工作目录读得出地落在 `root` 里):
|
|
231
|
+
* 持有集合加上它(借用的从此持有;已持有别的根的两只都持有),离场时按同一句律逐根再判。`root` 在盘或读不出 ⇒ 当场登记退出兜底簿(本环境的 prepare 可能早已跑过、不会再登记)。
|
|
232
|
+
*/
|
|
233
|
+
assumeRoot(root: string): void;
|
|
215
234
|
workspaceHandle(): WorkspaceHandle;
|
|
216
235
|
connect(config?: RemoteConnectConfig): Promise<{
|
|
217
236
|
ok: true;
|
|
@@ -507,8 +526,8 @@ export interface HostLegConfig extends Pick<HostEnvConfig, "workspaceDir" | "env
|
|
|
507
526
|
/**
|
|
508
527
|
* host 车道的工厂。**一只身份一只工作区**:经工作区托管表(`workspace-custody.ts`,与 local-docker 车道同一只)铸环境 ——
|
|
509
528
|
* 同一身份(`ctx.taskId ?? ctx.sessionId`)还有一只没收场的环境 ⇒ 交回它(停驻腿的环境 = 续跑腿的环境:引擎 park-only 契约要的「重连到同一工作区」);
|
|
510
|
-
* 正在收场 ⇒
|
|
511
|
-
*
|
|
529
|
+
* 正在收场 ⇒ 等它落定再铸。本服务铸的临时根只在它的最后一只持有者放手那一拍、按表里还没离场的环境此刻的工作目录判得没人之后才删({@link passOnRoot});
|
|
530
|
+
* 铸时那一判({@link heldRootContaining})只把落在持有根里的新腿当场记成共持者。`legOf` 先于交回 / 铸判(它可以拒)。
|
|
512
531
|
* 起服铸**一只**工厂(每次调用另铸一只工厂 = 没有表)。
|
|
513
532
|
*/
|
|
514
533
|
export declare function hostExecutionEnvFactory(config?: HostEnvConfig, legOf?: (ctx: ExecutionEnvFactoryContext) => HostLegConfig): ExecutionEnvFactory;
|
|
@@ -5,7 +5,7 @@ import path, {} from "node:path";
|
|
|
5
5
|
import { anchoredPath, anchorUnder, joinedPath } from "./exec-path-law.js";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, accessSync, constants as FS_CONST } from "node:fs";
|
|
8
|
+
import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, accessSync, renameSync, constants as FS_CONST } from "node:fs";
|
|
9
9
|
import { StringDecoder } from "node:string_decoder";
|
|
10
10
|
import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
|
|
11
11
|
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
@@ -21,7 +21,8 @@ import { composeHostSubprocessEnv, hostSubprocessEnv, withheldFromHostSubprocess
|
|
|
21
21
|
import { nameTooLongCode } from "./remote-env-file-error.js";
|
|
22
22
|
import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
|
|
23
23
|
import { createWorkspaceCustody } from "./workspace-custody.js";
|
|
24
|
-
import { canonicalPathOf, pathIsWithin } from "../memory-scope.js";
|
|
24
|
+
import { canonicalPathOf, pathIsWithin, presentOnDisk } from "../memory-scope.js";
|
|
25
|
+
import { errorCodeOf } from "../observability/err-text.js";
|
|
25
26
|
import { OWNER_ONLY_FILE_MODE } from "./owner-only-file-mode.js";
|
|
26
27
|
import { createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
|
|
27
28
|
const PROVIDER = "host";
|
|
@@ -41,6 +42,7 @@ const unsupported = (op) => ({
|
|
|
41
42
|
});
|
|
42
43
|
const BORROWED = Object.freeze({ kind: "borrowed" });
|
|
43
44
|
const RELEASED = Object.freeze({ kind: "released" });
|
|
45
|
+
const NO_ROOTS = Object.freeze([]);
|
|
44
46
|
const UNSUPPORTED_SCHEDULER_CAPS = {
|
|
45
47
|
supported: false,
|
|
46
48
|
maxScheduledPerScope: 0,
|
|
@@ -132,6 +134,7 @@ export class RemoteHostExecutionEnv {
|
|
|
132
134
|
return this.spoolRootDir;
|
|
133
135
|
}
|
|
134
136
|
tenure;
|
|
137
|
+
hasLeft = false;
|
|
135
138
|
preparing;
|
|
136
139
|
seat = createConnectSeat({
|
|
137
140
|
live: () => (this.prepared && !this.destroyed ? this.handle : undefined),
|
|
@@ -143,14 +146,14 @@ export class RemoteHostExecutionEnv {
|
|
|
143
146
|
if (config.workspaceDir) {
|
|
144
147
|
this.workspaceDir = hostSeatPath(config.workspaceDir);
|
|
145
148
|
const shared = config.custody?.sharedRoot;
|
|
146
|
-
this.tenure = shared !== undefined ? { kind: "held",
|
|
149
|
+
this.tenure = shared !== undefined ? { kind: "held", roots: [hostSeatPath(shared)] } : BORROWED;
|
|
147
150
|
}
|
|
148
151
|
else {
|
|
149
152
|
const base = hostSeatPath(config.workspaceBase ?? os.tmpdir());
|
|
150
153
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
151
154
|
const suffix = randomBytes(4).toString("hex");
|
|
152
155
|
this.workspaceDir = anchorUnder(path, base, `sema-host-${sanitizeId(id)}-${suffix}`);
|
|
153
|
-
this.tenure = { kind: "held",
|
|
156
|
+
this.tenure = { kind: "held", roots: [this.workspaceDir] };
|
|
154
157
|
}
|
|
155
158
|
const inheritEnvProblem = inheritEnvPolicyProblem(config.inheritEnv);
|
|
156
159
|
if (inheritEnvProblem !== undefined)
|
|
@@ -178,11 +181,18 @@ export class RemoteHostExecutionEnv {
|
|
|
178
181
|
return this.teardownSettled;
|
|
179
182
|
}
|
|
180
183
|
leftover = undefined;
|
|
184
|
+
get left() {
|
|
185
|
+
return this.hasLeft;
|
|
186
|
+
}
|
|
181
187
|
get settled() {
|
|
182
|
-
return this.teardownSettled && this.
|
|
188
|
+
return this.teardownSettled && this.hasLeft;
|
|
189
|
+
}
|
|
190
|
+
get heldRoots() {
|
|
191
|
+
return this.tenure.kind === "held" ? this.tenure.roots : NO_ROOTS;
|
|
183
192
|
}
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
assumeRoot(root) {
|
|
194
|
+
this.tenure = { kind: "held", roots: this.tenure.kind === "held" ? [...this.tenure.roots, root] : [root] };
|
|
195
|
+
trackUnlessAbsent(root);
|
|
186
196
|
}
|
|
187
197
|
workspaceHandle() {
|
|
188
198
|
return this.handle;
|
|
@@ -243,17 +253,37 @@ export class RemoteHostExecutionEnv {
|
|
|
243
253
|
catch { }
|
|
244
254
|
this.spoolRootDir = undefined;
|
|
245
255
|
}
|
|
256
|
+
this.hasLeft = true;
|
|
246
257
|
const tenure = this.tenure;
|
|
247
258
|
if (tenure.kind !== "held")
|
|
248
259
|
return;
|
|
249
260
|
this.tenure = RELEASED;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
const tombs = [];
|
|
262
|
+
for (const root of tenure.roots) {
|
|
263
|
+
if (this.cfg.custody?.passOn(this, root) === true)
|
|
264
|
+
continue;
|
|
265
|
+
const tomb = removalTombOf(root);
|
|
266
|
+
try {
|
|
267
|
+
renameSync(root, tomb);
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
if (e?.code === "ENOENT")
|
|
271
|
+
untrackEphemeralHostWorkspace(root);
|
|
272
|
+
else
|
|
273
|
+
trackUnlessAbsent(root);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
trackEphemeralHostWorkspace(tomb);
|
|
277
|
+
untrackEphemeralHostWorkspace(root);
|
|
278
|
+
tombs.push(tomb);
|
|
255
279
|
}
|
|
256
|
-
|
|
280
|
+
for (const tomb of tombs) {
|
|
281
|
+
try {
|
|
282
|
+
await fs.rm(tomb, { recursive: true, force: true });
|
|
283
|
+
untrackEphemeralHostWorkspace(tomb);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
}
|
|
257
287
|
}
|
|
258
288
|
}
|
|
259
289
|
async cleanup() {
|
|
@@ -1297,8 +1327,8 @@ export class RemoteHostExecutionEnv {
|
|
|
1297
1327
|
.mkdir(this.workspaceDir, { recursive: true })
|
|
1298
1328
|
.then(() => {
|
|
1299
1329
|
this.prepared = true;
|
|
1300
|
-
|
|
1301
|
-
|
|
1330
|
+
for (const root of this.heldRoots)
|
|
1331
|
+
trackUnlessAbsent(root);
|
|
1302
1332
|
})
|
|
1303
1333
|
.catch((e) => {
|
|
1304
1334
|
this.preparing = undefined;
|
|
@@ -1437,17 +1467,67 @@ function asError(e) {
|
|
|
1437
1467
|
function signalNumber(signal) {
|
|
1438
1468
|
return os.constants.signals[signal];
|
|
1439
1469
|
}
|
|
1470
|
+
function heldRootContaining(custody, dir) {
|
|
1471
|
+
const found = [];
|
|
1472
|
+
custody.holder((m) => {
|
|
1473
|
+
const root = m.heldRoots.find((r) => pathIsWithin(r, dir, canonicalPathOf));
|
|
1474
|
+
if (root !== undefined)
|
|
1475
|
+
found.push(root);
|
|
1476
|
+
return root !== undefined;
|
|
1477
|
+
});
|
|
1478
|
+
return found[0];
|
|
1479
|
+
}
|
|
1480
|
+
function passOnRoot(custody, self, root) {
|
|
1481
|
+
if (custody.holder((m) => m !== self && m.heldRoots.includes(root)) !== undefined)
|
|
1482
|
+
return true;
|
|
1483
|
+
const unreadable = [];
|
|
1484
|
+
const occupant = custody.holder((m) => {
|
|
1485
|
+
if (m === self || m.left)
|
|
1486
|
+
return false;
|
|
1487
|
+
try {
|
|
1488
|
+
return pathIsWithin(root, m.cwd, canonicalPathOf);
|
|
1489
|
+
}
|
|
1490
|
+
catch (error) {
|
|
1491
|
+
unreadable.push({ dir: m.cwd, error });
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
if (occupant !== undefined) {
|
|
1496
|
+
occupant.assumeRoot(root);
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
const first = unreadable[0];
|
|
1500
|
+
if (first === undefined)
|
|
1501
|
+
return false;
|
|
1502
|
+
trackUnlessAbsent(root);
|
|
1503
|
+
writeLogText("stderr", `RemoteHostExecutionEnv: kept ${root} instead of removing it — could not read whether the working directory ${first.dir} lies inside it ` +
|
|
1504
|
+
`(${errorCodeOf(first.error) ?? "unreadable"}); nobody takes it over, and the exit-time sweep removes it`);
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
function trackUnlessAbsent(root) {
|
|
1508
|
+
let absent;
|
|
1509
|
+
try {
|
|
1510
|
+
absent = !presentOnDisk(root);
|
|
1511
|
+
}
|
|
1512
|
+
catch {
|
|
1513
|
+
absent = false;
|
|
1514
|
+
}
|
|
1515
|
+
if (!absent)
|
|
1516
|
+
trackEphemeralHostWorkspace(root);
|
|
1517
|
+
}
|
|
1518
|
+
function removalTombOf(root) {
|
|
1519
|
+
return anchorUnder(path, path.dirname(root), `.${path.basename(root)}.removing-${randomBytes(4).toString("hex")}`);
|
|
1520
|
+
}
|
|
1440
1521
|
export function hostExecutionEnvFactory(config = {}, legOf = () => ({})) {
|
|
1441
1522
|
const custody = createWorkspaceCustody();
|
|
1442
|
-
const
|
|
1443
|
-
const rootHolding = (dir) => custody.holder((m) => m.heldWorkspace !== undefined && pathIsWithin(m.heldWorkspace, dir, canonicalPathOf))?.heldWorkspace;
|
|
1523
|
+
const passOn = (self, root) => passOnRoot(custody, self, root);
|
|
1444
1524
|
return (ctx) => {
|
|
1445
1525
|
const { minted, ...leg } = legOf(ctx);
|
|
1446
1526
|
const identity = ctx.taskId ?? ctx.sessionId;
|
|
1447
1527
|
return custody.envOf(identity, () => {
|
|
1448
1528
|
const dir = leg.workspaceDir ?? config.workspaceDir;
|
|
1449
|
-
const sharedRoot = dir === undefined ? undefined :
|
|
1450
|
-
const env = new RemoteHostExecutionEnv({ ...config, ...leg, id: config.id ?? identity, custody: {
|
|
1529
|
+
const sharedRoot = dir === undefined ? undefined : heldRootContaining(custody, hostSeatPath(dir));
|
|
1530
|
+
const env = new RemoteHostExecutionEnv({ ...config, ...leg, id: config.id ?? identity, custody: { passOn, ...(sharedRoot !== undefined ? { sharedRoot } : {}) } });
|
|
1451
1531
|
minted?.(env);
|
|
1452
1532
|
return env;
|
|
1453
1533
|
});
|
|
@@ -2,6 +2,7 @@ import { type SpaceReclaimer, type WorkspaceSpacePolicy, type CacheReclaimReason
|
|
|
2
2
|
import { type SpaceCommitFence } from "../workspace-space-commit.js";
|
|
3
3
|
import { type CacheMissMemory, type WorkspaceCacheCapability } from "../workspace-cache.js";
|
|
4
4
|
import { type SandboxEgressGap, type SandboxIpv6 } from "../sandbox-egress-policy.js";
|
|
5
|
+
import { type K8sApiCredentialFact } from "../k8s-api-credentials.js";
|
|
5
6
|
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type ExecResult, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
|
|
6
7
|
import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
|
|
7
8
|
import { type SandboxReadFloor } from "./sandbox-read-floor.js";
|
|
@@ -10,6 +11,37 @@ import { type LaneHomeConfig } from "../lane-home-probe.js";
|
|
|
10
11
|
import { type ConnectResult } from "./remote-env-connect-seat.js";
|
|
11
12
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
12
13
|
export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, buildKillAllScript, parseBgPollOutput, type ParsedBgPoll, } from "./k8s-bg-scripts.js";
|
|
14
|
+
/**
|
|
15
|
+
* This process's in-cluster identity: the platform's `KUBERNETES_SERVICE_*` and the mounted service-account files. `readMounted` throws when the file is unreadable.
|
|
16
|
+
* (In this file a bare `read(` names a control-plane GET — the RBAC table's source reader classifies every one — so the mount reader carries its own name.)
|
|
17
|
+
*/
|
|
18
|
+
export interface InClusterIdentity {
|
|
19
|
+
readonly host: string | undefined;
|
|
20
|
+
readonly port: string | undefined;
|
|
21
|
+
readMounted(file: "token" | "ca.crt"): string;
|
|
22
|
+
}
|
|
23
|
+
/** What the control plane can be reached with — or the ONE missing fact (with the adapter code its request-time refusal carries). */
|
|
24
|
+
export type K8sApiAccess = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly apiUrl: string;
|
|
27
|
+
readonly token: string;
|
|
28
|
+
readonly ca: string | undefined;
|
|
29
|
+
} | {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly fact: K8sApiCredentialFact;
|
|
32
|
+
readonly code: "connect_failed" | "auth_failed";
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The ONE judge of "does this lane have cluster API credentials": explicit config (`K8S_API_URL` / `K8S_TOKEN` / `K8S_CA_CERT_B64`, `K8S_INSECURE_TLS`) first, the
|
|
36
|
+
* in-cluster service account second, per piece. The request-time reader ({@link RemoteK8sExecutionEnv}'s `creds()`), the boot gate and the per-run credential
|
|
37
|
+
* reading all read THIS function — what the boot refuses on is exactly what every request would have thrown on. (No kubeconfig: this lane never reads one.)
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveK8sApiAccess(cfg: {
|
|
40
|
+
readonly apiUrl?: string | undefined;
|
|
41
|
+
readonly token?: string | undefined;
|
|
42
|
+
readonly caCert?: string | undefined;
|
|
43
|
+
readonly insecureTls?: boolean | undefined;
|
|
44
|
+
}, inCluster?: InClusterIdentity): K8sApiAccess;
|
|
13
45
|
export interface K8sEnvConfig extends LaneHomeConfig {
|
|
14
46
|
/** API server URL. Default: in-cluster (`https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT`). */
|
|
15
47
|
apiUrl?: string;
|
|
@@ -194,6 +226,21 @@ export interface K8sRequestOptions {
|
|
|
194
226
|
signal?: AbortSignal;
|
|
195
227
|
contentType?: string;
|
|
196
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The control plane's reading of this lane's credentials, a closed set: `missing` (the judge's fact — nothing to send), `rejected` (the cluster answered HTTP 401),
|
|
231
|
+
* `accepted` (a 2xx answer: the credentials work), `unproven` (any other answer, or none — see {@link RemoteK8sExecutionEnv.apiCredentialReading}).
|
|
232
|
+
*/
|
|
233
|
+
export type ApiCredentialReading = {
|
|
234
|
+
readonly state: "missing";
|
|
235
|
+
readonly fact: K8sApiCredentialFact;
|
|
236
|
+
} | {
|
|
237
|
+
readonly state: "rejected";
|
|
238
|
+
} | {
|
|
239
|
+
readonly state: "accepted";
|
|
240
|
+
} | {
|
|
241
|
+
readonly state: "unproven";
|
|
242
|
+
readonly detail: string;
|
|
243
|
+
};
|
|
197
244
|
export interface K8sEnvDeps {
|
|
198
245
|
request?: (method: string, apiPath: string, body?: unknown, opts?: K8sRequestOptions) => Promise<{
|
|
199
246
|
status: number;
|
|
@@ -203,6 +250,8 @@ export interface K8sEnvDeps {
|
|
|
203
250
|
connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
|
|
204
251
|
/** Worker-side object-store requests (the space pointer, listing, deletes). Default: global `fetch`; either way the request goes out through the egress exit (an endpoint that answers with a redirect is refused, never followed). */
|
|
205
252
|
fetch?: typeof fetch;
|
|
253
|
+
/** This process's in-cluster identity (default: the process env's `KUBERNETES_SERVICE_*` and the mounted service-account files); a test replaces it. */
|
|
254
|
+
inCluster?: () => InClusterIdentity;
|
|
206
255
|
/** 这只环境手里的读地板模块(缺席 ⇒ `createSandboxReadFloor` 按旋钮选;钉注入守地板的模块)。能力块的 `keepsReadFloor` 读它的状态。 */
|
|
207
256
|
readFloor?: SandboxReadFloor;
|
|
208
257
|
}
|
|
@@ -369,8 +418,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
369
418
|
get lifetimeMs(): number;
|
|
370
419
|
private resolve;
|
|
371
420
|
workspaceHandle(): WorkspaceHandle;
|
|
372
|
-
/** Resolved lazily so an in-cluster pod picks up its service account without explicit config. */
|
|
421
|
+
/** Resolved lazily so an in-cluster pod picks up its service account without explicit config ({@link resolveK8sApiAccess} is the one judge). */
|
|
373
422
|
private creds;
|
|
423
|
+
/** The missing credential fact of this env's control-plane access, or `undefined` (nothing missing) — {@link resolveK8sApiAccess}, the one judge. */
|
|
424
|
+
missingApiCredential(): K8sApiCredentialFact | undefined;
|
|
425
|
+
/**
|
|
426
|
+
* The control plane's reading of this lane's credentials, asked once per run before its env is minted ({@link readSandboxApiCredentials}): the judge's missing fact
|
|
427
|
+
* ({@link missingApiCredential}), else one SelfSubjectAccessReview (answered for every authenticated caller) — an HTTP 401 is the cluster rejecting the credentials,
|
|
428
|
+
* a 2xx the cluster accepting them. ONLY a positive proof of rejection refuses: any other answer (403, 5xx, a request that threw or timed out) is `unproven` — another
|
|
429
|
+
* fact (a Role, an outage) that the lane's own connect answers loudly and retries within its own bounds; refusing the run on it would turn every API blip into a failed run.
|
|
430
|
+
* The caller counts an `unproven` reading (a registered fail-open arm). Never throws.
|
|
431
|
+
*/
|
|
432
|
+
apiCredentialReading(): Promise<ApiCredentialReading>;
|
|
374
433
|
/** Control-plane REST call (pod create/get/delete), bounded by rpcTimeoutMs. Injectable for tests. */
|
|
375
434
|
private request;
|
|
376
435
|
private openExec;
|
|
@@ -554,7 +613,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
554
613
|
* snapshotKey()+s3Presign() pair so the two call sites can't diverge.
|
|
555
614
|
*/
|
|
556
615
|
private presignSnapshot;
|
|
557
|
-
/** The key prefix of every snapshot object this env signs — the ONE reading of `keyPrefix` (unset ⇒ the default); its form is judged in {@link presignSnapshot}. */
|
|
616
|
+
/** The key prefix of every snapshot object this env signs — `workspaceSnapshotPrefixOf`, the ONE reading of `keyPrefix` (unset ⇒ the default; the boot bucket check lists under the same reading); its form is judged in {@link presignSnapshot}. */
|
|
558
617
|
private snapshotKeyPrefix;
|
|
559
618
|
/** A lane's partition prefix inside the bucket — the ONE place it is spelled (`presignSnapshot` signs under it; the reclaim reads the listing against it).
|
|
560
619
|
* The partition segment is validated by `presignSnapshot` before a URL is signed. */
|
|
@@ -927,6 +986,13 @@ export declare function createSpaceReclaimer(config: K8sEnvConfig, deps?: K8sEnv
|
|
|
927
986
|
* nodes that join after boot are read at the next boot. Never throws (a read failure is an `unreadable` gap).
|
|
928
987
|
*/
|
|
929
988
|
export declare function checkSandboxEgressPolicy(config: K8sEnvConfig, deps?: K8sEnvDeps): Promise<SandboxEgressGap[]>;
|
|
989
|
+
/**
|
|
990
|
+
* The missing cluster-API credential fact of this k8s config's control-plane access ({@link RemoteK8sExecutionEnv.missingApiCredential}), or `undefined`. The boot
|
|
991
|
+
* gate's judgment: the one judge ({@link resolveK8sApiAccess}) over the config and the process's in-cluster identity. Never throws.
|
|
992
|
+
*/
|
|
993
|
+
export declare function sandboxApiCredentialFact(config: K8sEnvConfig, deps?: K8sEnvDeps): K8sApiCredentialFact | undefined;
|
|
994
|
+
/** The control plane's reading of this lane's credentials ({@link RemoteK8sExecutionEnv.apiCredentialReading}; a control-plane env, no pod). Never throws. */
|
|
995
|
+
export declare function readSandboxApiCredentials(config: K8sEnvConfig, deps?: K8sEnvDeps): Promise<ApiCredentialReading>;
|
|
930
996
|
/** The boot reading of one sandbox Secret seat for this k8s config ({@link RemoteK8sExecutionEnv.secretSeatReading}; a control-plane env, no pod). Never throws. */
|
|
931
997
|
export declare function readSandboxSecretSeat(config: K8sEnvConfig, seat: SandboxSecretSeatKind, deps?: K8sEnvDeps): Promise<SandboxSecretSeat>;
|
|
932
998
|
/**
|
|
@@ -9,7 +9,7 @@ import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, shellQuote,
|
|
|
9
9
|
import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
|
|
10
10
|
import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
|
|
11
11
|
import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
|
|
12
|
-
import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT,
|
|
12
|
+
import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotIdHolds, snapshotKeyPrefixProblem, snapshotNameOwnedBy, workspaceSnapshotPrefixOf } from "../workspace-space.js";
|
|
13
13
|
import { spaceCommitAlive } from "../workspace-space-commit.js";
|
|
14
14
|
import { SESSION_INCARNATION_MISSING, incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
|
|
15
15
|
import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
|
|
@@ -18,6 +18,7 @@ import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failur
|
|
|
18
18
|
import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
|
|
19
19
|
import { buildCapturedOutput, createCutError } from "../exec-cut.js";
|
|
20
20
|
import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
|
|
21
|
+
import { describeMissingK8sCredential } from "../k8s-api-credentials.js";
|
|
21
22
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
22
23
|
import { capabilitiesFor } from "../execution-lane-caps.js";
|
|
23
24
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
|
|
@@ -44,6 +45,42 @@ const unsupported = (op) => ({
|
|
|
44
45
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a Kata pod sandbox (no managed memory snapshot — capabilities.suspendable=false; durable suspend degrades to checkpoint + cold rebuild)`),
|
|
45
46
|
});
|
|
46
47
|
const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
|
|
48
|
+
const processInCluster = () => ({
|
|
49
|
+
host: process.env.KUBERNETES_SERVICE_HOST,
|
|
50
|
+
port: process.env.KUBERNETES_SERVICE_PORT,
|
|
51
|
+
readMounted: (file) => readFileSync(`${SA_DIR}/${file}`, "utf8"),
|
|
52
|
+
});
|
|
53
|
+
export function resolveK8sApiAccess(cfg, inCluster = processInCluster()) {
|
|
54
|
+
const given = (v) => v !== undefined && v.trim() !== "";
|
|
55
|
+
const apiUrl = given(cfg.apiUrl) ? cfg.apiUrl : given(inCluster.host) ? `https://${inCluster.host}:${inCluster.port ?? "443"}` : undefined;
|
|
56
|
+
if (apiUrl === undefined)
|
|
57
|
+
return { ok: false, fact: "api_url", code: "connect_failed" };
|
|
58
|
+
let token = given(cfg.token) ? cfg.token : undefined;
|
|
59
|
+
if (token === undefined) {
|
|
60
|
+
try {
|
|
61
|
+
const mounted = inCluster.readMounted("token").trim();
|
|
62
|
+
token = mounted === "" ? undefined : mounted;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
token = undefined;
|
|
66
|
+
}
|
|
67
|
+
if (token === undefined)
|
|
68
|
+
return { ok: false, fact: "token", code: "auth_failed" };
|
|
69
|
+
}
|
|
70
|
+
let ca = given(cfg.caCert) ? cfg.caCert : undefined;
|
|
71
|
+
if (ca === undefined && !cfg.insecureTls) {
|
|
72
|
+
try {
|
|
73
|
+
const mounted = inCluster.readMounted("ca.crt");
|
|
74
|
+
ca = given(mounted) ? mounted : undefined;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
ca = undefined;
|
|
78
|
+
}
|
|
79
|
+
if (ca === undefined)
|
|
80
|
+
return { ok: false, fact: "ca", code: "auth_failed" };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, apiUrl, token, ca };
|
|
83
|
+
}
|
|
47
84
|
const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
|
|
48
85
|
const WRITE_CHUNK_BYTES = 64 * 1024;
|
|
49
86
|
import { applyPodSpecPatch, ADAPTER_VOLUME_NAMES, SANDBOX_POD_LABELS, SANDBOX_RUN_END_HOLD_ANNOTATION, SANDBOX_RUN_LABEL, SANDBOX_SECRET_SEAT_VERBS, isLabelValue } from "./k8s-exec-protocol.js";
|
|
@@ -179,6 +216,42 @@ export function boundedStoreCall(op, bound) {
|
|
|
179
216
|
});
|
|
180
217
|
});
|
|
181
218
|
}
|
|
219
|
+
function snapshotOpOf(req) {
|
|
220
|
+
if (req.method === "LIST")
|
|
221
|
+
return req.lane === "space" ? "k8s-space/prefix:LIST" : "k8s-park/prefix:LIST";
|
|
222
|
+
if ("object" in req)
|
|
223
|
+
return req.lane === "space" ? "k8s-space/object:DELETE" : "k8s-park/object:DELETE";
|
|
224
|
+
if ("pointer" in req) {
|
|
225
|
+
switch (req.method) {
|
|
226
|
+
case "GET":
|
|
227
|
+
return "k8s-space/latest:GET";
|
|
228
|
+
case "PUT":
|
|
229
|
+
return "k8s-space/latest:PUT";
|
|
230
|
+
case "DELETE":
|
|
231
|
+
return "k8s-space/latest:DELETE";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (req.lane === "park") {
|
|
235
|
+
switch (req.method) {
|
|
236
|
+
case "GET":
|
|
237
|
+
return "k8s-park/archive:GET";
|
|
238
|
+
case "PUT":
|
|
239
|
+
return "k8s-park/archive:PUT";
|
|
240
|
+
case "HEAD":
|
|
241
|
+
return "k8s-park/archive:HEAD";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
switch (req.method) {
|
|
245
|
+
case "GET":
|
|
246
|
+
return "k8s-space/archive:GET";
|
|
247
|
+
case "PUT":
|
|
248
|
+
return "k8s-space/archive:PUT";
|
|
249
|
+
case "DELETE":
|
|
250
|
+
return "k8s-space/archive:DELETE";
|
|
251
|
+
case "HEAD":
|
|
252
|
+
return "k8s-space/archive:HEAD";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
182
255
|
import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
|
|
183
256
|
export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
|
|
184
257
|
export function errorMessageOf(e) {
|
|
@@ -323,29 +396,32 @@ export class RemoteK8sExecutionEnv {
|
|
|
323
396
|
return { sandboxId: this.podName ?? "(unconnected)", provider: PROVIDER, mountPath: this.cwd, ...(this.handleSnapshotId ? { snapshotId: this.handleSnapshotId } : {}) };
|
|
324
397
|
}
|
|
325
398
|
creds() {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
399
|
+
const access = resolveK8sApiAccess(this.cfg, this.deps.inCluster?.());
|
|
400
|
+
if (!access.ok)
|
|
401
|
+
throw new RemoteExecutionError(access.code, describeMissingK8sCredential(access.fact));
|
|
402
|
+
return { apiUrl: access.apiUrl.replace(/\/+$/, ""), token: access.token, ca: access.ca };
|
|
403
|
+
}
|
|
404
|
+
missingApiCredential() {
|
|
405
|
+
const access = resolveK8sApiAccess(this.cfg, this.deps.inCluster?.());
|
|
406
|
+
return access.ok ? undefined : access.fact;
|
|
407
|
+
}
|
|
408
|
+
async apiCredentialReading() {
|
|
409
|
+
const fact = this.missingApiCredential();
|
|
410
|
+
if (fact !== undefined)
|
|
411
|
+
return { state: "missing", fact };
|
|
412
|
+
try {
|
|
413
|
+
const r = await this.request("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", {
|
|
414
|
+
apiVersion: "authorization.k8s.io/v1",
|
|
415
|
+
kind: "SelfSubjectAccessReview",
|
|
416
|
+
spec: { resourceAttributes: { namespace: this.cfg.namespace, verb: "create", resource: "pods" } },
|
|
417
|
+
});
|
|
418
|
+
if (r.status === 401)
|
|
419
|
+
return { state: "rejected" };
|
|
420
|
+
return r.status >= 200 && r.status < 300 ? { state: "accepted" } : { state: "unproven", detail: `HTTP ${r.status}` };
|
|
339
421
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
ca = readFileSync(`${SA_DIR}/ca.crt`, "utf8");
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
throw new RemoteExecutionError("auth_failed", "no k8s CA: set K8S_CA_CERT_B64, K8S_INSECURE_TLS=true (lab), or run in-cluster");
|
|
346
|
-
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
return { state: "unproven", detail: errorMessageOf(e) };
|
|
347
424
|
}
|
|
348
|
-
return { apiUrl: apiUrl.replace(/\/+$/, ""), token, ca };
|
|
349
425
|
}
|
|
350
426
|
async request(method, apiPath, body, opts) {
|
|
351
427
|
if (this.deps.request)
|
|
@@ -1002,12 +1078,13 @@ export class RemoteK8sExecutionEnv {
|
|
|
1002
1078
|
if (snapshotKeyPrefixProblem(this.snapshotKeyPrefix()) !== undefined)
|
|
1003
1079
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "unsafe snapshot key segment (keyPrefix): refusing to build a traversing object key") };
|
|
1004
1080
|
const common = {
|
|
1081
|
+
op: snapshotOpOf(req),
|
|
1005
1082
|
endpoint: s3.endpoint,
|
|
1006
1083
|
bucket: s3.bucket,
|
|
1007
1084
|
accessKey: s3.accessKey,
|
|
1008
1085
|
secretKey: s3.secretKey,
|
|
1009
1086
|
...(s3.region ? { region: s3.region } : {}),
|
|
1010
|
-
expiresSec:
|
|
1087
|
+
...(s3.presignTtlSec !== undefined ? { expiresSec: s3.presignTtlSec } : {}),
|
|
1011
1088
|
};
|
|
1012
1089
|
const base = this.snapshotBase(req);
|
|
1013
1090
|
if (req.method === "LIST")
|
|
@@ -1016,7 +1093,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
1016
1093
|
return { ok: true, value: presignS3Url({ ...common, key: `${base}${object}`, method: req.method }) };
|
|
1017
1094
|
}
|
|
1018
1095
|
snapshotKeyPrefix() {
|
|
1019
|
-
return this.cfg.s3Snapshot
|
|
1096
|
+
return workspaceSnapshotPrefixOf(this.cfg.s3Snapshot);
|
|
1020
1097
|
}
|
|
1021
1098
|
snapshotBase(req) {
|
|
1022
1099
|
const prefix = this.snapshotKeyPrefix();
|
|
@@ -2426,6 +2503,12 @@ export function createSpaceReclaimer(config, deps) {
|
|
|
2426
2503
|
export async function checkSandboxEgressPolicy(config, deps) {
|
|
2427
2504
|
return new RemoteK8sExecutionEnv(config, deps).egressPolicyGaps();
|
|
2428
2505
|
}
|
|
2506
|
+
export function sandboxApiCredentialFact(config, deps) {
|
|
2507
|
+
return new RemoteK8sExecutionEnv(config, deps).missingApiCredential();
|
|
2508
|
+
}
|
|
2509
|
+
export async function readSandboxApiCredentials(config, deps) {
|
|
2510
|
+
return new RemoteK8sExecutionEnv(config, deps).apiCredentialReading();
|
|
2511
|
+
}
|
|
2429
2512
|
export async function readSandboxSecretSeat(config, seat, deps) {
|
|
2430
2513
|
return new RemoteK8sExecutionEnv(config, deps).secretSeatReading(seat);
|
|
2431
2514
|
}
|
|
@@ -5,7 +5,7 @@ export declare const REPLICA_REGISTRATION_TABLE = "replica_registration";
|
|
|
5
5
|
* 本构建的能力名单(常设、只增):每一条读停-迁-起证据的一次性迁移一个名。删码之后名字留着 —— 名单只增不减(钉:`test/replica-registration.test.ts`)。
|
|
6
6
|
* 守卫读证据时传的名在型上就要在这里(`ReplicaCapability`):不在名单里的迁移读不了证据,编译期红。
|
|
7
7
|
*/
|
|
8
|
-
export declare const REPLICA_CAPABILITIES: readonly ["M40", "M41", "M42", "M43", "M51"];
|
|
8
|
+
export declare const REPLICA_CAPABILITIES: readonly ["M40", "M41", "M42", "M43", "M51", "M54"];
|
|
9
9
|
export type ReplicaCapability = (typeof REPLICA_CAPABILITIES)[number];
|
|
10
10
|
/** 留存视界(写者钟):登记早于此的别人的行在下一次登记时删。量级取远大于任一副本的寿命与副本间钟差。 */
|
|
11
11
|
export declare const REPLICA_REGISTRATION_HORIZON_MS: number;
|
|
@@ -2,7 +2,7 @@ import { hostname } from "node:os";
|
|
|
2
2
|
import { processInstanceId } from "../process-instance.js";
|
|
3
3
|
import { serviceVersion } from "../service-version.js";
|
|
4
4
|
export const REPLICA_REGISTRATION_TABLE = "replica_registration";
|
|
5
|
-
export const REPLICA_CAPABILITIES = ["M40", "M41", "M42", "M43", "M51"];
|
|
5
|
+
export const REPLICA_CAPABILITIES = ["M40", "M41", "M42", "M43", "M51", "M54"];
|
|
6
6
|
export const REPLICA_REGISTRATION_HORIZON_MS = 90 * 24 * 60 * 60 * 1000;
|
|
7
7
|
export function buildThisReplicaRegistration() {
|
|
8
8
|
return { instanceId: processInstanceId(), releaseLine: serviceVersion(), capabilities: REPLICA_CAPABILITIES, hostName: hostname(), pid: process.pid };
|