@sema-agent/server 7.150.0 → 7.151.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 +185 -0
- package/USAGE.md +53 -25
- package/deploy/sema-up/README.md +7 -1
- package/deploy/sema-up/chart/Chart.yaml +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
- package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
- package/deploy/sema-up/chart/templates/server.yaml +5 -3
- package/deploy/sema-up/chart/values-full-single.yaml +3 -1
- package/deploy/sema-up/chart/values.yaml +8 -1
- package/deploy/sema-up/cluster-up.sh +1 -0
- package/deploy/sema-up/kube-up.sh +1 -1
- package/deploy/sema-up/lib/flagshape.sh +1 -1
- package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
- package/deploy/sema-up/lib/helm-values-kube.py +2 -0
- package/dist/adoption/import.js +3 -3
- package/dist/approval-ask-machine.d.ts +1 -1
- package/dist/approval-card.d.ts +2 -2
- package/dist/approval-gate-currency.d.ts +1 -1
- package/dist/auth-bridge.d.ts +20 -0
- package/dist/auth-bridge.js +9 -0
- package/dist/background-session-reap.d.ts +1 -1
- package/dist/bake-runner/main.d.ts +4 -2
- package/dist/bind-host.d.ts +2 -3
- package/dist/bind-host.js +2 -4
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/engine-lease.d.ts +5 -0
- package/dist/boot/engine-lease.js +1 -0
- package/dist/boot/execution-env.d.ts +8 -14
- package/dist/boot/execution-env.js +54 -17
- package/dist/boot/execution-lane.js +32 -5
- package/dist/boot/late-binds.d.ts +8 -4
- package/dist/boot/late-binds.js +7 -1
- package/dist/boot/memory-consolidation.d.ts +3 -2
- package/dist/boot/memory-consolidation.js +6 -6
- package/dist/boot/reapers.d.ts +5 -0
- package/dist/boot/reapers.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -3
- package/dist/boot/resolve-spec.js +31 -22
- package/dist/boot/runner-deps.d.ts +9 -2
- package/dist/boot/runner-deps.js +3 -3
- package/dist/boot/session-age-out.d.ts +36 -0
- package/dist/boot/session-age-out.js +101 -0
- package/dist/boot/session-faces.d.ts +5 -14
- package/dist/boot/session-faces.js +9 -5
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-05-execution-env.d.ts +5 -3
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +7 -4
- package/dist/boot/stage-06-runners.js +15 -6
- package/dist/boot/stage-07-capability-layer.d.ts +7 -4
- package/dist/boot/stage-07-capability-layer.js +2 -1
- package/dist/boot/stage-08-reapers.d.ts +7 -4
- package/dist/boot/stage-08-reapers.js +4 -2
- package/dist/boot/stage-09-leader.d.ts +11 -5
- package/dist/boot/stage-09-leader.js +12 -3
- package/dist/boot/stage-10-http-server.d.ts +11 -5
- package/dist/boot/stage-10-http-server.js +3 -6
- package/dist/boot/web-admin.js +3 -1
- package/dist/boot/workflow-orchestration.d.ts +7 -2
- package/dist/boot/workflow-orchestration.js +24 -7
- package/dist/bounded-rotation.d.ts +29 -0
- package/dist/bounded-rotation.js +24 -0
- package/dist/capability-facts.d.ts +22 -13
- package/dist/capability-facts.js +28 -18
- package/dist/center-credential.d.ts +25 -6
- package/dist/center-credential.js +25 -5
- package/dist/config-catalog.d.ts +26 -7
- package/dist/config-catalog.js +14 -11
- package/dist/config-invariants.d.ts +2 -1
- package/dist/config-invariants.js +4 -4
- package/dist/config-types.d.ts +10 -4
- package/dist/config.d.ts +51 -2
- package/dist/config.js +35 -24
- package/dist/deployment-shape.d.ts +9 -1
- package/dist/deployment-shape.js +2 -1
- package/dist/device-protocol.d.ts +5 -4
- package/dist/device-protocol.js +1 -1
- package/dist/device-ws-hub.js +3 -2
- package/dist/device-ws-protocol.d.ts +63 -10
- package/dist/device-ws-protocol.js +56 -25
- package/dist/engine-identity.d.ts +19 -7
- package/dist/engine-identity.js +10 -2
- package/dist/entry-mode-clamp-notice.d.ts +2 -1
- package/dist/execution-env-refusal.d.ts +23 -0
- package/dist/execution-env-refusal.js +5 -0
- package/dist/execution-lane-caps.d.ts +3 -1
- package/dist/field-conflict.d.ts +1 -1
- package/dist/file-history-capture.d.ts +11 -4
- package/dist/file-history-capture.js +9 -6
- package/dist/file-history-disclosure.d.ts +7 -5
- package/dist/file-history-disclosure.js +7 -5
- package/dist/fleet/drain-entry.d.ts +4 -0
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/host-lsp-manager.d.ts +2 -2
- package/dist/http/admission.js +17 -2
- package/dist/http/approval-sweeps.js +5 -5
- package/dist/http/checkpoint-ctx.d.ts +2 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/dispatch.d.ts +2 -1
- package/dist/http/dispatch.js +34 -27
- package/dist/http/durable-run-leg.js +16 -16
- package/dist/http/health-self-report.d.ts +9 -1
- package/dist/http/health-self-report.js +2 -0
- package/dist/http/owner-access.d.ts +31 -11
- package/dist/http/owner-access.js +37 -6
- package/dist/http/resume-ctx-read.js +25 -3
- package/dist/http/resume-legs.d.ts +2 -1
- package/dist/http/resume-legs.js +259 -79
- package/dist/http/route-ctx.d.ts +18 -8
- package/dist/http/route-table.d.ts +1 -1
- package/dist/http/routes/admin-memory-optout.d.ts +1 -1
- package/dist/http/routes/agents-roster.js +2 -3
- package/dist/http/routes/approvals-assistant.js +4 -3
- package/dist/http/routes/background.d.ts +3 -3
- package/dist/http/routes/capabilities.js +8 -5
- package/dist/http/routes/fleet.js +10 -8
- package/dist/http/routes/projects.d.ts +3 -2
- package/dist/http/routes/projects.js +4 -5
- package/dist/http/routes/runs.js +102 -36
- package/dist/http/routes/session-sync.d.ts +4 -3
- package/dist/http/routes/session-sync.js +172 -77
- package/dist/http/routes/sessions-list.js +13 -7
- package/dist/http/routes/sessions.js +10 -6
- package/dist/http/routes/tasks.js +30 -16
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/server.d.ts +21 -27
- package/dist/http/server.js +2 -2
- package/dist/http/submit-ctx.js +5 -4
- package/dist/http/wire-types.d.ts +14 -4
- package/dist/identity-source.d.ts +20 -3
- package/dist/identity-source.js +14 -0
- package/dist/index.d.ts +4 -0
- package/dist/lane-home-probe.d.ts +3 -7
- package/dist/lane-home-probe.js +0 -1
- package/dist/leader/leader.d.ts +2 -1
- package/dist/leg-facts.d.ts +13 -5
- package/dist/leg-facts.js +2 -1
- package/dist/memory-scope.d.ts +5 -4
- package/dist/memory-scope.js +4 -4
- package/dist/memory-sync-client.d.ts +30 -2
- package/dist/memory-sync-client.js +16 -9
- package/dist/observability/err-text.d.ts +7 -0
- package/dist/observability/err-text.js +5 -0
- package/dist/observability/fail-open.d.ts +7 -3
- package/dist/observability/fail-open.js +7 -3
- package/dist/observability/metrics.js +1 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
- package/dist/orchestration/workflow-notify-journal.js +12 -1
- package/dist/orchestration/workflow-park-decision.d.ts +70 -18
- package/dist/orchestration/workflow-park-decision.js +49 -10
- package/dist/orchestration/workflow-park-holder.d.ts +65 -0
- package/dist/orchestration/workflow-park-holder.js +98 -0
- package/dist/orchestration/workflow-record-fence.d.ts +36 -0
- package/dist/orchestration/workflow-record-fence.js +42 -0
- package/dist/owner-faces.d.ts +1 -1
- package/dist/parked-decide.d.ts +104 -53
- package/dist/parked-decide.js +153 -117
- package/dist/peer-directory.d.ts +2 -2
- package/dist/peer-inbound-face.d.ts +2 -6
- package/dist/peer-inbound-face.js +0 -3
- package/dist/permission-mode-admission.d.ts +41 -5
- package/dist/permission-mode-admission.js +29 -3
- package/dist/plugins/adoption-log-sql.d.ts +3 -1
- package/dist/plugins/adoption-log-sql.js +7 -1
- package/dist/plugins/approval-ask-store-file.d.ts +10 -2
- package/dist/plugins/approval-ask-store-file.js +8 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -1
- package/dist/plugins/approval-ask-store-memory.js +28 -9
- package/dist/plugins/approval-ask-store-sql.d.ts +43 -2
- package/dist/plugins/approval-ask-store-sql.js +34 -5
- package/dist/plugins/approval-exemption-store.js +3 -0
- package/dist/plugins/approval-nonce-store.js +2 -0
- package/dist/plugins/breaker-state-sql.js +2 -0
- package/dist/plugins/caching-session-store.d.ts +9 -15
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/device-store-sql.js +6 -0
- package/dist/plugins/error-origin.d.ts +18 -0
- package/dist/plugins/error-origin.js +10 -0
- package/dist/plugins/execution-placement-sql.js +6 -6
- package/dist/plugins/image-bake-store-sql.js +6 -0
- package/dist/plugins/image-index-sql.js +4 -0
- package/dist/plugins/landed-attendants.d.ts +212 -0
- package/dist/plugins/landed-attendants.js +161 -0
- package/dist/plugins/leader-run-store-sql.js +4 -0
- package/dist/plugins/local-session-live-reference.d.ts +40 -0
- package/dist/plugins/local-session-live-reference.js +39 -0
- package/dist/plugins/local-session-store.d.ts +71 -33
- package/dist/plugins/local-session-store.js +129 -27
- package/dist/plugins/memory-engine-pg.js +4 -0
- package/dist/plugins/memory-engine-sql-apply.js +5 -2
- package/dist/plugins/memory-engine-tidb.js +4 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
- package/dist/plugins/memory-sync-store-pg.js +5 -0
- package/dist/plugins/memory-sync-store-tidb.js +5 -0
- package/dist/plugins/one-time-migrations.d.ts +57 -63
- package/dist/plugins/one-time-migrations.js +162 -90
- package/dist/plugins/org-delivery-quota-store.js +4 -1
- package/dist/plugins/outcome-ledger-sql.js +2 -0
- package/dist/plugins/permission-rule-store-sql.js +11 -4
- package/dist/plugins/pg-cost-quota.js +4 -0
- package/dist/plugins/pg-pool.js +24 -1
- package/dist/plugins/pg-rate-limiter.js +2 -0
- package/dist/plugins/posix-shell-fs.d.ts +6 -1
- package/dist/plugins/posix-shell-fs.js +4 -1
- package/dist/plugins/remote-env-adb.d.ts +19 -15
- package/dist/plugins/remote-env-adb.js +41 -15
- package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
- package/dist/plugins/remote-env-connect-seat.js +72 -0
- package/dist/plugins/remote-env-device.d.ts +22 -5
- package/dist/plugins/remote-env-device.js +106 -8
- package/dist/plugins/remote-env-e2b.d.ts +15 -6
- package/dist/plugins/remote-env-e2b.js +42 -32
- package/dist/plugins/remote-env-file-error.js +6 -2
- package/dist/plugins/remote-env-host.d.ts +17 -0
- package/dist/plugins/remote-env-host.js +30 -38
- package/dist/plugins/remote-env-k8s.d.ts +41 -39
- package/dist/plugins/remote-env-k8s.js +53 -30
- package/dist/plugins/remote-env-local-docker.d.ts +13 -8
- package/dist/plugins/remote-env-local-docker.js +34 -41
- package/dist/plugins/remote-env-ssh.d.ts +18 -12
- package/dist/plugins/remote-env-ssh.js +60 -59
- package/dist/plugins/remote-shell.d.ts +98 -1
- package/dist/plugins/remote-shell.js +218 -0
- package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
- package/dist/plugins/resume-anchor-store-sql.js +12 -5
- package/dist/plugins/retention-lane-store-sql.js +4 -0
- package/dist/plugins/retention-sql.d.ts +20 -2
- package/dist/plugins/retention-sql.js +11 -0
- package/dist/plugins/run-store-sql.d.ts +21 -2
- package/dist/plugins/run-store-sql.js +31 -7
- package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
- package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
- package/dist/plugins/schema-consistency.d.ts +4 -2
- package/dist/plugins/schema-consistency.js +19 -2
- package/dist/plugins/send-file-ledger.js +3 -0
- package/dist/plugins/serving-requirement.d.ts +26 -7
- package/dist/plugins/serving-requirement.js +11 -4
- package/dist/plugins/session-birth.d.ts +6 -2
- package/dist/plugins/session-capture-record-store-sql.js +2 -0
- package/dist/plugins/session-ext-table.d.ts +14 -1
- package/dist/plugins/session-ext-table.js +5 -1
- package/dist/plugins/session-row-sql.d.ts +49 -2
- package/dist/plugins/session-row-sql.js +56 -13
- package/dist/plugins/session-store-sql.d.ts +34 -18
- package/dist/plugins/session-store-sql.js +98 -17
- package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
- package/dist/plugins/shared-memory-store-sql.js +6 -2
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/sql-driver.js +18 -3
- package/dist/plugins/sql-errors.d.ts +11 -6
- package/dist/plugins/sql-errors.js +5 -6
- package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
- package/dist/plugins/stop-then-start-sentence.js +14 -10
- package/dist/plugins/stop-then-start.d.ts +109 -0
- package/dist/plugins/stop-then-start.js +104 -0
- package/dist/plugins/store-backend.d.ts +40 -13
- package/dist/plugins/store-backend.js +169 -27
- package/dist/plugins/store-boundary.d.ts +57 -0
- package/dist/plugins/store-boundary.js +139 -0
- package/dist/plugins/store-contracts.d.ts +17 -0
- package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
- package/dist/plugins/store-fault-sentence-m53.js +25 -0
- package/dist/plugins/store-fault.d.ts +26 -7
- package/dist/plugins/store-fault.js +18 -2
- package/dist/plugins/task-attachment-store.js +9 -0
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +10 -8
- package/dist/plugins/tidb-cost-quota.js +4 -0
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-rate-limiter.js +4 -0
- package/dist/plugins/user-input-ledger-sql.js +4 -0
- package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
- package/dist/plugins/workflow-run-store-sql.js +64 -4
- package/dist/plugins/workspace-fill.d.ts +4 -7
- package/dist/plugins/workspace-fill.js +2 -1
- package/dist/plugins/write-behind-counter.js +5 -0
- package/dist/project-context.d.ts +0 -2
- package/dist/project-context.js +4 -8
- package/dist/project-registry.d.ts +46 -0
- package/dist/project-registry.js +44 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +4 -0
- package/dist/request-value-closure.js +10 -0
- package/dist/run-acceptance.d.ts +3 -2
- package/dist/run-acceptance.js +4 -6
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +37 -8
- package/dist/runs.js +53 -12
- package/dist/runtime-governance.d.ts +5 -4
- package/dist/security.d.ts +30 -32
- package/dist/security.js +41 -7
- package/dist/server-secret-env.d.ts +12 -10
- package/dist/server-secret-env.js +24 -15
- package/dist/session-bindings.d.ts +82 -0
- package/dist/session-bindings.js +18 -0
- package/dist/session-children-retire.d.ts +2 -2
- package/dist/session-children-retire.js +2 -6
- package/dist/session-deletion.d.ts +16 -1
- package/dist/session-error-wire.d.ts +3 -3
- package/dist/session-error-wire.js +3 -3
- package/dist/session-incarnation.d.ts +10 -0
- package/dist/session-incarnation.js +10 -1
- package/dist/session-kind.d.ts +57 -0
- package/dist/session-kind.js +22 -0
- package/dist/session-row.d.ts +21 -1
- package/dist/session-row.js +7 -1
- package/dist/session-sync-kernel.d.ts +24 -14
- package/dist/session-sync-kernel.js +3 -6
- package/dist/session-sync.d.ts +4 -98
- package/dist/session-sync.js +1 -71
- package/dist/session-titler.d.ts +10 -3
- package/dist/session-titler.js +19 -6
- package/dist/ssh-host-key.d.ts +5 -0
- package/dist/ssh-host-key.js +3 -0
- package/dist/stores-sql/file-history-store-sql.d.ts +52 -2
- package/dist/stores-sql/file-history-store-sql.js +108 -70
- package/dist/stores-sql/pg-session-storage.d.ts +34 -7
- package/dist/stores-sql/pg-session-storage.js +37 -13
- package/dist/stores-sql/retention-store-sql.d.ts +1 -9
- package/dist/stores-sql/retention-store-sql.js +2 -10
- package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
- package/dist/stores-sql/session-policy-store-sql.js +61 -44
- package/dist/stores-sql/session-write-hooks.d.ts +11 -0
- package/dist/stores-sql/tidb-session-store.d.ts +58 -7
- package/dist/stores-sql/tidb-session-store.js +37 -13
- package/dist/submission-exit.d.ts +3 -2
- package/dist/submission-exit.js +9 -1
- package/dist/task-cwd.d.ts +2 -2
- package/dist/task-settings.d.ts +6 -5
- package/dist/terminal.d.ts +33 -2
- package/dist/terminal.js +139 -9
- package/dist/tool-approval.d.ts +140 -6
- package/dist/tool-approval.js +165 -72
- package/dist/trace/self-minted-wire.d.ts +290 -27
- package/dist/trace/self-minted-wire.js +101 -13
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +78 -27
- package/package.json +2 -2
- package/wire-changes/server@7.151.0.json +762 -0
- package/dist/http/session-inherit-env.d.ts +0 -36
- package/dist/http/session-inherit-env.js +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
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, type BoundFileObject } from "@sema-agent/core";
|
|
2
2
|
import { type DeviceOwner, type DeviceRejectCode, type DeviceRow, type DeviceStore } from "../device-store.js";
|
|
3
3
|
import type { DeviceEnrollment } from "../device-enrollment.js";
|
|
4
4
|
import { type ExecutionPlacementSeat } from "../execution-placement.js";
|
|
@@ -48,6 +48,13 @@ type RejectCode = DeviceRejectCode;
|
|
|
48
48
|
*/
|
|
49
49
|
export declare const DEVICE_REJECT_TO_EXEC_ERROR: Readonly<Record<RejectCode, ExecutionError["code"]>>;
|
|
50
50
|
export declare const DEVICE_REJECT_TO_FILE_ERROR: Readonly<Record<RejectCode, FileError["code"]>>;
|
|
51
|
+
/**
|
|
52
|
+
* 生命周期面(connect/reconnect)的 typed 拒 → `RemoteExecutionErrorCode`。
|
|
53
|
+
*
|
|
54
|
+
* `connect_failed` = 本次到不了(**可重试**,core `RETRYABLE_REMOTE_ERROR_CODES` 含它);
|
|
55
|
+
* `auth_failed` = 永久拒(NEVER retry —— 换绑/吊销/不是你的设备,重试一万次也是同一答)。
|
|
56
|
+
*/
|
|
57
|
+
export declare const DEVICE_REJECT_TO_REMOTE_ERROR: Readonly<Record<RejectCode, RemoteExecutionError["code"]>>;
|
|
51
58
|
export interface DeviceEnvLogger {
|
|
52
59
|
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
53
60
|
info?(msg: string, fields?: Record<string, unknown>): void;
|
|
@@ -102,9 +109,15 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
|
102
109
|
readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
|
|
103
110
|
writeFileExclusive?: (path: string, content: string | Uint8Array, abortSignal?: AbortSignal) => Promise<Result<void, FileError>>;
|
|
104
111
|
writeFileGuarded?: (path: string, content: string | Uint8Array, expect: WriteExpectation, abortSignal?: AbortSignal) => Promise<Result<WriteReceipt, FileError>>;
|
|
112
|
+
openBound?: (path: string, abortSignal?: AbortSignal) => Promise<Result<BoundFileObject, FileError>>;
|
|
105
113
|
private connected;
|
|
106
114
|
/** 在飞的那一次准入(单飞记忆)——见 {@link admitShared}。settle 即清,**不缓存败诺**。 */
|
|
107
115
|
private connectPromise?;
|
|
116
|
+
/**
|
|
117
|
+
* 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序 —— 准入在飞 ⇒ 等那一次(调用方的取消只退它自己的等待)· 已连 ⇒ 当下的把手、不再准入 · 否则准入一次。
|
|
118
|
+
* 本车道不记拒:准入每次都是对绑定 / 吊销 / 在线的一次新读(拒码按生命周期表拼:{@link DEVICE_REJECT_TO_REMOTE_ERROR})。
|
|
119
|
+
*/
|
|
120
|
+
private readonly seat;
|
|
108
121
|
/** 拆除是**终态**(六条兄弟腿同纪律:destroy 之后 connect/exec/读面一律 fail-closed,不复活)。 */
|
|
109
122
|
private destroyed;
|
|
110
123
|
constructor(cfg: DeviceEnvConfig);
|
|
@@ -170,10 +183,8 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
|
170
183
|
/**
|
|
171
184
|
* 懒连接:**首次调用任一面**(exec / execStream / 文件面)时自动走一次准入。
|
|
172
185
|
*
|
|
173
|
-
* 🔴 为什么这条必须在 adapter 里(而不是「谁用谁先 connect」)
|
|
174
|
-
*
|
|
175
|
-
* `.connect()` 只有 bench 与生命周期面自己)。所以一条「设了未连闸、又不自连」的腿,它的每一次工具
|
|
176
|
-
* 调用都被自家的闸挡回,而设备侧一条指令帧都收不到。e2b(首用建 VM)/ k8s(`connectPromise ??=`)/
|
|
186
|
+
* 🔴 为什么这条必须在 adapter 里(而不是「谁用谁先 connect」):面可能在任何人连它之前就被调到(工厂交出之前的附件物化层、
|
|
187
|
+
* 委派子代的放置检查)。所以一条「设了未连闸、又不自连」的腿,它的每一次工具调用都被自家的闸挡回,而设备侧一条指令帧都收不到。e2b(首用建 VM)/ k8s(`connectPromise ??=`)/
|
|
177
188
|
* ssh(single-flight join)/ local-docker 四腿早就是懒连接;本腿从此同形,`connect()` 的语义
|
|
178
189
|
* (准入 + 取活连接 + 保护面对拍)一个字不动,只是触发时机与兄弟一致。
|
|
179
190
|
*
|
|
@@ -262,6 +273,12 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
|
262
273
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
263
274
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
264
275
|
private doReadLink;
|
|
276
|
+
private doOpenBound;
|
|
277
|
+
/**
|
|
278
|
+
* 逐答重绑的答者:每一答一条 `openBound` 指令,结局 kind **按 op 精确对拍**(`open` / `size` / `list` ⇒ `stat`,`readBytes` ⇒ `read`;答错 = 协议违约,不猜)。
|
|
279
|
+
* 开答之后的三答带开答时的 `identity` + `kind`,执行器在本机核对这一次打开的对象与它们都相等;每一答前照文件面同一道门(已拆 ⇒ 拒;首用准入)。
|
|
280
|
+
*/
|
|
281
|
+
private boundAnswers;
|
|
265
282
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
266
283
|
readTextFile(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
267
284
|
readTextLines(p: string, options?: {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
2
|
import { anchoredPath, anchorUnder, joinedPath } from "./exec-path-law.js";
|
|
3
3
|
import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
|
|
4
|
+
import { openRebinding } from "./remote-shell.js";
|
|
4
5
|
import { capabilitiesFor } from "../execution-lane-caps.js";
|
|
5
6
|
import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
|
|
6
7
|
import { boundDeviceOf } from "../execution-placement.js";
|
|
7
8
|
import { createCutError, createExecFailure } from "../exec-cut.js";
|
|
9
|
+
import { createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
|
|
8
10
|
export const DEVICE_PROVIDER = "device";
|
|
9
11
|
const ok = (value) => ({ ok: true, value });
|
|
10
12
|
const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -89,12 +91,12 @@ export const DEVICE_REJECT_TO_FILE_ERROR = {
|
|
|
89
91
|
"limit.rate_exceeded": "target_unavailable",
|
|
90
92
|
"draining": "target_unavailable",
|
|
91
93
|
};
|
|
92
|
-
const DEVICE_REJECT_TO_REMOTE_ERROR = {
|
|
94
|
+
export const DEVICE_REJECT_TO_REMOTE_ERROR = {
|
|
93
95
|
"device.not_bound": "connect_failed",
|
|
94
96
|
"device.offline": "connect_failed",
|
|
95
97
|
"device.busy": "connect_failed",
|
|
96
98
|
"device.attached_elsewhere": "connect_failed",
|
|
97
|
-
"device.claim_conflict": "
|
|
99
|
+
"device.claim_conflict": "connect_failed",
|
|
98
100
|
"device.provider_mismatch": "auth_failed",
|
|
99
101
|
"device.identity_mismatch": "auth_failed",
|
|
100
102
|
"device.revoked": "auth_failed",
|
|
@@ -110,6 +112,9 @@ function rejectText(reject) {
|
|
|
110
112
|
}
|
|
111
113
|
const ENV_TERMINAL = { kind: "env_terminal" };
|
|
112
114
|
const ENV_TERMINAL_MESSAGE = "execution env already destroyed";
|
|
115
|
+
function connectAnswerOf(admitted) {
|
|
116
|
+
return admitted.ok ? { ok: true, value: admitted.value } : { ok: false, error: new RemoteExecutionError(admitted.failure.remote, admitted.failure.message) };
|
|
117
|
+
}
|
|
113
118
|
function failureFor(code, message) {
|
|
114
119
|
return { remote: DEVICE_REJECT_TO_REMOTE_ERROR[code], exec: DEVICE_REJECT_TO_EXEC_ERROR[code], file: DEVICE_REJECT_TO_FILE_ERROR[code], message };
|
|
115
120
|
}
|
|
@@ -137,8 +142,14 @@ export class RemoteDeviceExecutionEnv {
|
|
|
137
142
|
readLink;
|
|
138
143
|
writeFileExclusive;
|
|
139
144
|
writeFileGuarded;
|
|
145
|
+
openBound;
|
|
140
146
|
connected = false;
|
|
141
147
|
connectPromise;
|
|
148
|
+
seat = createConnectSeat({
|
|
149
|
+
live: () => (this.connected && !this.destroyed ? this.workspaceHandle() : undefined),
|
|
150
|
+
inFlight: () => this.connectPromise?.then(connectAnswerOf),
|
|
151
|
+
establish: (config) => withdrawOnAbort(this.admitShared().then(connectAnswerOf), config?.abortSignal),
|
|
152
|
+
});
|
|
142
153
|
destroyed = false;
|
|
143
154
|
constructor(cfg) {
|
|
144
155
|
this.cfg = cfg;
|
|
@@ -150,6 +161,8 @@ export class RemoteDeviceExecutionEnv {
|
|
|
150
161
|
this.writeFileExclusive = (p, c, s) => this.doWrite("writeFileExclusive", p, c, undefined, s);
|
|
151
162
|
if (cfg.supports?.has("writeFileGuarded") === true)
|
|
152
163
|
this.writeFileGuarded = (p, c, e, s) => this.doGuardedWrite(p, c, e, s);
|
|
164
|
+
if (cfg.supports?.has("openBound") === true)
|
|
165
|
+
this.openBound = (p, s) => this.doOpenBound(p, s);
|
|
153
166
|
}
|
|
154
167
|
guardFaceSnapshot() {
|
|
155
168
|
const out = new Set();
|
|
@@ -196,12 +209,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
196
209
|
};
|
|
197
210
|
}
|
|
198
211
|
async connect(config) {
|
|
199
|
-
|
|
200
|
-
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
201
|
-
const admitted = await this.admitShared();
|
|
202
|
-
if (admitted.ok)
|
|
203
|
-
return ok(admitted.value);
|
|
204
|
-
return { ok: false, error: new RemoteExecutionError(admitted.failure.remote, admitted.failure.message) };
|
|
212
|
+
return this.seat.connect(config);
|
|
205
213
|
}
|
|
206
214
|
async admitShared() {
|
|
207
215
|
const inflight = this.connectPromise;
|
|
@@ -577,6 +585,96 @@ export class RemoteDeviceExecutionEnv {
|
|
|
577
585
|
return ok(o.linkTarget);
|
|
578
586
|
});
|
|
579
587
|
}
|
|
588
|
+
async doOpenBound(p, abortSignal) {
|
|
589
|
+
const gated = await this.fsGate(p);
|
|
590
|
+
if (gated)
|
|
591
|
+
return gated;
|
|
592
|
+
const abs = this.resolve(p);
|
|
593
|
+
return openRebinding(abs, this.boundAnswers(abs), abortSignal);
|
|
594
|
+
}
|
|
595
|
+
boundAnswers(abs) {
|
|
596
|
+
const send = async (args, signal, onChunk) => {
|
|
597
|
+
const gated = await this.fsGate(abs);
|
|
598
|
+
if (gated)
|
|
599
|
+
return gated;
|
|
600
|
+
return this.dispatch("openBound", { path: abs, ...args }, { timeoutMs: this.cfg.execTimeoutMs, ...(signal === undefined ? {} : { signal }), ...(onChunk === undefined ? {} : { onChunk }) });
|
|
601
|
+
};
|
|
602
|
+
const violation = (what) => ({ ok: false, error: new FileError("unknown", `device returned an openBound outcome ${what} — protocol violation`, abs) });
|
|
603
|
+
return {
|
|
604
|
+
open: async (signal) => {
|
|
605
|
+
const res = await send({ op: "open" }, signal);
|
|
606
|
+
if ("ok" in res)
|
|
607
|
+
return res;
|
|
608
|
+
return this.fileOutcome(res, abs, (o) => {
|
|
609
|
+
if (o.kind !== "stat")
|
|
610
|
+
return this.wrongOutcome("stat", o.kind, abs);
|
|
611
|
+
if (typeof o.identity !== "string" || o.identity === "" || o.info === undefined)
|
|
612
|
+
return violation("without identity and info for the open answer");
|
|
613
|
+
if (o.info.kind !== "file" && o.info.kind !== "dir")
|
|
614
|
+
return violation(`naming a ${o.info.kind} (an executor refuses anything but a regular file or a directory with invalid)`);
|
|
615
|
+
return ok({ identity: o.identity, kind: o.info.kind === "dir" ? "directory" : "file" });
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
size: async (bound) => {
|
|
619
|
+
const res = await send({ op: "size", identity: bound.identity, kind: bound.kind }, undefined);
|
|
620
|
+
if ("ok" in res)
|
|
621
|
+
return res;
|
|
622
|
+
return this.fileOutcome(res, abs, (o) => {
|
|
623
|
+
if (o.kind !== "stat")
|
|
624
|
+
return this.wrongOutcome("stat", o.kind, abs);
|
|
625
|
+
if (o.info === undefined)
|
|
626
|
+
return violation("without info for the size answer");
|
|
627
|
+
return ok(o.info.size);
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
readBytes: async (bound, max, signal) => {
|
|
631
|
+
const parts = [];
|
|
632
|
+
let got = 0;
|
|
633
|
+
let stray;
|
|
634
|
+
const res = await send({ op: "readBytes", identity: bound.identity, kind: bound.kind, max }, signal, (c) => {
|
|
635
|
+
if (c.kind !== "data") {
|
|
636
|
+
stray ??= c.kind;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (c.data === undefined || c.data.length === 0)
|
|
640
|
+
return;
|
|
641
|
+
got += c.data.length;
|
|
642
|
+
if (got <= max)
|
|
643
|
+
parts.push(c.data);
|
|
644
|
+
});
|
|
645
|
+
if ("ok" in res)
|
|
646
|
+
return res;
|
|
647
|
+
return this.fileOutcome(res, abs, (o) => {
|
|
648
|
+
if (o.kind !== "read")
|
|
649
|
+
return this.wrongOutcome("read", o.kind, abs);
|
|
650
|
+
if (stray !== undefined)
|
|
651
|
+
return violation(`carrying a ${stray} chunk for a readBytes answer (only data chunks belong to it)`);
|
|
652
|
+
if (got > max)
|
|
653
|
+
return { ok: false, error: new FileError("invalid", `The held file is over the ${max}-byte read cap (${got} bytes or more arrived); it was not read`, abs) };
|
|
654
|
+
return ok(new Uint8Array(Buffer.concat(parts)));
|
|
655
|
+
});
|
|
656
|
+
},
|
|
657
|
+
list: async (bound, signal) => {
|
|
658
|
+
const res = await send({ op: "list", identity: bound.identity, kind: bound.kind }, signal);
|
|
659
|
+
if ("ok" in res)
|
|
660
|
+
return res;
|
|
661
|
+
return this.fileOutcome(res, abs, (o) => {
|
|
662
|
+
if (o.kind !== "stat")
|
|
663
|
+
return this.wrongOutcome("stat", o.kind, abs);
|
|
664
|
+
if (o.entries === undefined)
|
|
665
|
+
return violation("without entries for the list answer");
|
|
666
|
+
const entries = [];
|
|
667
|
+
for (const w of o.entries) {
|
|
668
|
+
const kind = w.kind === "dir" ? "directory" : w.kind === "symlink" ? "symlink" : w.kind === "file" ? "file" : undefined;
|
|
669
|
+
if (kind === undefined)
|
|
670
|
+
continue;
|
|
671
|
+
entries.push({ name: posix.basename(w.path), kind, size: w.size, mtimeMs: w.mtimeMs });
|
|
672
|
+
}
|
|
673
|
+
return ok(entries);
|
|
674
|
+
});
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
580
678
|
async readBinaryFile(p, abortSignal) {
|
|
581
679
|
const gated = await this.fsGate(p);
|
|
582
680
|
if (gated)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* iterator MAY throw `RemoteExecutionError` mid-stream before an `exit` chunk (seam contract, OutputChunk).
|
|
21
21
|
*/
|
|
22
22
|
import { Sandbox, type SandboxOpts, type SandboxConnectOpts } from "e2b";
|
|
23
|
-
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type ExecResult, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
|
|
23
|
+
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 ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
|
|
24
24
|
import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
|
|
25
25
|
import { type SandboxReadFloor } from "./sandbox-read-floor.js";
|
|
26
26
|
import { type LaneHomeConfig } from "../lane-home-probe.js";
|
|
@@ -127,9 +127,12 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
127
127
|
private destroyed;
|
|
128
128
|
/** 家目录的座(主张在场 ⇒ 每只沙箱对证 · 否则建沙箱那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。 */
|
|
129
129
|
private readonly homeSeat;
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
132
|
-
|
|
130
|
+
/**
|
|
131
|
+
* 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序(记住的拒 ⇒ 原样再答 · 一走在飞 ⇒ 等它 · 已连 ⇒ 当下的把手 · 否则建一只沙箱),并持有这只环境记住的拒 ——
|
|
132
|
+
* 记一次、此后每次连接 / 恢复原样回它(同一模板同一个答案:不再建沙箱、不再问)。两件立定的事实写它:家目录(沙箱答了而与主张不一致,或答的不是绝对路径;`env.home_unconfirmed`)
|
|
133
|
+
* 与恢复点的填充(换一台沙箱也是同一个答的拒:种子不是项目包、摘要不符、镜像没有 `tar`、镜像预置撞名 —— `FillOutcome.standing`;`env.sandbox_refused`)。
|
|
134
|
+
*/
|
|
135
|
+
private readonly seat;
|
|
133
136
|
/** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). `target` = the snapshot a resume
|
|
134
137
|
* restores (`undefined` = a fresh sandbox): {@link resumeVM} shares only a resume of the SAME snapshot — a connect for anything else is not the
|
|
135
138
|
* workspace it is asked to restore (it waits that one out, then releases the sandbox it left). */
|
|
@@ -223,8 +226,9 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
223
226
|
}>;
|
|
224
227
|
/**
|
|
225
228
|
* 家目录:座要问({@link LaneHomeSeat.asks}:主张在场 ⇒ 对证;没主张 ⇒ 量到绝对路径为止)⇒ 在这只沙箱里问一次 `$HOME`(前台 `commands.run`,与命令同一个
|
|
226
|
-
* 模板缺省用户、同一份沙箱 env),读数交座。返回这只沙箱的拒(
|
|
227
|
-
*
|
|
229
|
+
* 模板缺省用户、同一份沙箱 env),读数交座。返回这只沙箱的拒(拒句 = 同一只判官的句子),或 `undefined`(照用)。
|
|
230
|
+
* 沙箱答了的拒是立定的拒(`env.home_unconfirmed`,建连座记住它:同一模板同一个答案);没答上(非零退出 / RPC 失败 / 输出里没有标记行)⇒ 这一次不开、不记(`connect_failed`),
|
|
231
|
+
* 下一次重问;调用方中止 ⇒ `aborted`、不交座。
|
|
228
232
|
* 上界 = 控制面 RPC 的墙钟(`rpcTimeoutMs`:问一个环境变量,不是跑用户命令)。
|
|
229
233
|
*/
|
|
230
234
|
private homeVerdict;
|
|
@@ -295,6 +299,11 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
295
299
|
* 🔴 exit≠0 归 `unknown` 而不是 `not_found`(与本腿 `readLink` 那一格刻意分家,同 `posix-shell-fs` 的理由):
|
|
296
300
|
* 「叶不是软链」与「叶不存在」在这条命令上都是 exit 0 + 空 stdout,所以 exit≠0 一定不是「文件不在」。 */
|
|
297
301
|
canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
|
|
302
|
+
/** 绑定读(逐答重绑,Linux 读法)。本腿不并进 `posix-shell-fs` 基座(原生 SDK 腿),但脚本、读法与工厂与四条 shell 腿**是同一只**
|
|
303
|
+
* (`remote-shell.ts` `openBoundOverShell`),这里只多一条传输:{@link execInternal}(适配器自己的固定命令那一起动,与链脚本同一形)、数据界。
|
|
304
|
+
* 🔴 字节只经 stdout 的 base64 回来 —— `files.read` 按路径读,不能用;也不「拷进暂存件再按路径读」(同身份进程可换暂存件或它的祖先目录)。 */
|
|
305
|
+
openBound(p: string, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
|
|
306
|
+
private get boundLane();
|
|
298
307
|
canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
299
308
|
exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
300
309
|
createDir(p: string, options?: {
|
|
@@ -7,7 +7,8 @@ import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
|
|
|
7
7
|
import { ADAPTER_START, BOOTSTRAP_START, createSandboxReadFloor, e2bFloorCommand, engineStart, } from "./sandbox-read-floor.js";
|
|
8
8
|
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
9
9
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
10
|
-
import {
|
|
10
|
+
import { connectRefusal, createConnectSeat } from "./remote-env-connect-seat.js";
|
|
11
|
+
import { appendFileStaged, openBoundOverShell, readyUpload, rewriteAppend, numEnvOr, runSymlinkChain, shellQuote, writeFileStaged } from "./remote-shell.js";
|
|
11
12
|
import { buildLivenessCut, commandBoundsOf, firstBoundOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
|
|
12
13
|
import { createCutError } from "../exec-cut.js";
|
|
13
14
|
import { BackgroundShellManager } from "./background-shell-support.js";
|
|
@@ -37,7 +38,22 @@ export class RemoteContainerExecutionEnv {
|
|
|
37
38
|
handle;
|
|
38
39
|
destroyed = false;
|
|
39
40
|
homeSeat;
|
|
40
|
-
|
|
41
|
+
seat = createConnectSeat({
|
|
42
|
+
live: () => (this.sbx !== undefined && !this.paused ? this.workspaceHandle() : undefined),
|
|
43
|
+
inFlight: () => this.connecting?.promise,
|
|
44
|
+
establish: (config) => {
|
|
45
|
+
if (this.paused)
|
|
46
|
+
return Promise.resolve({ ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") });
|
|
47
|
+
const mine = { target: undefined, promise: this.doConnect(config) };
|
|
48
|
+
this.connecting = mine;
|
|
49
|
+
const settled = () => {
|
|
50
|
+
if (this.connecting === mine)
|
|
51
|
+
this.connecting = undefined;
|
|
52
|
+
};
|
|
53
|
+
void mine.promise.then(settled, settled);
|
|
54
|
+
return mine.promise;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
41
57
|
connecting;
|
|
42
58
|
activeCommands = 0;
|
|
43
59
|
paused = false;
|
|
@@ -156,20 +172,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
156
172
|
}
|
|
157
173
|
if (this.destroyed)
|
|
158
174
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
159
|
-
|
|
160
|
-
return { ok: false, error: this.connectRefusal };
|
|
161
|
-
if (this.paused)
|
|
162
|
-
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
|
|
163
|
-
if (this.sbx)
|
|
164
|
-
return ok(this.workspaceHandle());
|
|
165
|
-
if (this.connecting)
|
|
166
|
-
return this.connecting.promise;
|
|
167
|
-
const mine = { target: undefined, promise: this.doConnect(config) };
|
|
168
|
-
this.connecting = mine;
|
|
169
|
-
const r = await mine.promise;
|
|
170
|
-
if (!r.ok && this.connecting === mine)
|
|
171
|
-
this.connecting = undefined;
|
|
172
|
-
return r;
|
|
175
|
+
return this.seat.connect(config);
|
|
173
176
|
}
|
|
174
177
|
async doConnect(config) {
|
|
175
178
|
try {
|
|
@@ -200,9 +203,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
200
203
|
if (!filled.ok || this.destroyed) {
|
|
201
204
|
await sbx.kill().catch(() => { });
|
|
202
205
|
this.cfg.metrics?.inc("remote_env_connect_total", { provider: PROVIDER, result: "error" });
|
|
203
|
-
|
|
204
|
-
this.connectRefusal = filled.error;
|
|
205
|
-
return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
|
|
206
|
+
return filled.ok ? { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") } : this.seat.answer(filled);
|
|
206
207
|
}
|
|
207
208
|
this.sbx = sbx;
|
|
208
209
|
this.paused = false;
|
|
@@ -256,8 +257,9 @@ export class RemoteContainerExecutionEnv {
|
|
|
256
257
|
resumeVM(snapshotId, options) {
|
|
257
258
|
if (this.destroyed)
|
|
258
259
|
return Promise.resolve({ ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") });
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
const held = this.seat.held;
|
|
261
|
+
if (held)
|
|
262
|
+
return Promise.resolve(held);
|
|
261
263
|
if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
|
|
262
264
|
return Promise.resolve(ok(this.workspaceHandle()));
|
|
263
265
|
const inFlight = this.connecting;
|
|
@@ -269,10 +271,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
269
271
|
if (this.connecting === mine)
|
|
270
272
|
this.connecting = undefined;
|
|
271
273
|
};
|
|
272
|
-
mine.promise.then(
|
|
273
|
-
if (!r.ok)
|
|
274
|
-
forget();
|
|
275
|
-
}, forget);
|
|
274
|
+
mine.promise.then(forget, forget);
|
|
276
275
|
return mine.promise;
|
|
277
276
|
}
|
|
278
277
|
async resumeWalk(inFlight, snapshotId, options) {
|
|
@@ -280,6 +279,9 @@ export class RemoteContainerExecutionEnv {
|
|
|
280
279
|
await inFlight.promise;
|
|
281
280
|
if (this.destroyed)
|
|
282
281
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
|
|
282
|
+
const held = this.seat.held;
|
|
283
|
+
if (held)
|
|
284
|
+
return held;
|
|
283
285
|
if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
|
|
284
286
|
return ok(this.workspaceHandle());
|
|
285
287
|
}
|
|
@@ -329,9 +331,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
329
331
|
if (!filled.ok) {
|
|
330
332
|
await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), undefined, this.cfg.dataTimeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_fill_refused_repause_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
|
|
331
333
|
this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
|
|
332
|
-
|
|
333
|
-
this.connectRefusal = filled.error;
|
|
334
|
-
return { ok: false, error: filled.error };
|
|
334
|
+
return this.seat.answer(filled);
|
|
335
335
|
}
|
|
336
336
|
this.sbx = sbx;
|
|
337
337
|
this.paused = false;
|
|
@@ -383,10 +383,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
383
383
|
const verdict = this.homeSeat.settle(probe);
|
|
384
384
|
if (verdict === undefined)
|
|
385
385
|
return undefined;
|
|
386
|
-
|
|
387
|
-
if (verdict.answered)
|
|
388
|
-
this.connectRefusal = error;
|
|
389
|
-
return error;
|
|
386
|
+
return verdict.answered ? this.seat.answer(connectRefusal("home_unconfirmed", verdict.refusal)).error : new RemoteExecutionError("connect_failed", verdict.refusal);
|
|
390
387
|
}
|
|
391
388
|
fillAt(sbx, root, origin) {
|
|
392
389
|
return this.cfg.workspaceFill === undefined ? Promise.resolve({ ok: true }) : this.cfg.workspaceFill.fill(this.fillChannel(sbx), root, origin);
|
|
@@ -968,6 +965,19 @@ export class RemoteContainerExecutionEnv {
|
|
|
968
965
|
async canonicalChain(p, abortSignal) {
|
|
969
966
|
return runSymlinkChain(this.resolve(p), (cmd) => this.execInternal(cmd, { abortSignal }));
|
|
970
967
|
}
|
|
968
|
+
async openBound(p, abortSignal) {
|
|
969
|
+
return openBoundOverShell(this.resolve(p), this.boundLane, abortSignal);
|
|
970
|
+
}
|
|
971
|
+
get boundLane() {
|
|
972
|
+
return {
|
|
973
|
+
run: async (cmd, _cap, abortSignal) => {
|
|
974
|
+
const r = await this.execInternal(cmd, { cwd: "/", timeout: this.cfg.dataTimeoutMs / 1000, ...(abortSignal !== undefined ? { abortSignal } : {}) });
|
|
975
|
+
return r.ok ? { ok: true, value: { stdout: Buffer.from(r.value.stdout, "utf8"), stderr: r.value.stderr, exitCode: r.value.exitCode } } : r;
|
|
976
|
+
},
|
|
977
|
+
payload: "base64",
|
|
978
|
+
boundSec: this.cfg.dataTimeoutMs / 1000,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
971
981
|
async canonicalPath(p, abortSignal) {
|
|
972
982
|
const abs = this.resolve(p);
|
|
973
983
|
const r = await this.execInternal(`realpath -- ${shellQuote(abs)}`, { abortSignal });
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { FileError, RemoteExecutionError } from "@sema-agent/core";
|
|
2
|
+
import { isExecutionEnvRefusalCode } from "../execution-env-refusal.js";
|
|
3
|
+
function sayEnvNotOpened(code) {
|
|
4
|
+
return code === "connect_failed" || isExecutionEnvRefusalCode(code);
|
|
5
|
+
}
|
|
2
6
|
function neverOpened(e) {
|
|
3
|
-
if (e.code
|
|
7
|
+
if (sayEnvNotOpened(e.code))
|
|
4
8
|
return true;
|
|
5
9
|
const cause = e.cause;
|
|
6
|
-
return cause instanceof RemoteExecutionError && cause.code
|
|
10
|
+
return cause instanceof RemoteExecutionError && sayEnvNotOpened(cause.code);
|
|
7
11
|
}
|
|
8
12
|
export function fileErrorFromExec(e, path) {
|
|
9
13
|
const code = e.code === "aborted" ? "aborted" : neverOpened(e) ? "target_unavailable" : "unknown";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PlatformPath } from "node:path";
|
|
1
2
|
import { FileError, type InheritEnvPolicy, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, 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 ExecutionEnvFactoryContext, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions, type SecretEnvFinding } from "@sema-agent/core";
|
|
2
3
|
import { type LaneHomeConfig } from "../lane-home-probe.js";
|
|
3
4
|
import { type CustodyMember } from "./workspace-custody.js";
|
|
@@ -194,6 +195,11 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
194
195
|
private tenure;
|
|
195
196
|
/** Memoized lazy `mkdir -p` so concurrent first-use prepares the workspace exactly once. */
|
|
196
197
|
private preparing?;
|
|
198
|
+
/**
|
|
199
|
+
* 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序 —— 建工作目录在飞 ⇒ 等那一次(调用方的取消只退它自己的等待)· 已建好 ⇒ 当下的把手 · 否则建一次。
|
|
200
|
+
* 本车道不记拒:主张证不了的家目录在构造时就拒了(环境不铸),建目录的失败是那一次的(下一次重来)。
|
|
201
|
+
*/
|
|
202
|
+
private readonly seat;
|
|
197
203
|
private readonly handle;
|
|
198
204
|
constructor(config?: HostEnvConfig);
|
|
199
205
|
/** 收场的进度(托管表读):活着 ⇒ 缺席;{@link destroy} 开始 ⇒ 那一次收场。 */
|
|
@@ -214,6 +220,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
214
220
|
ok: false;
|
|
215
221
|
error: RemoteExecutionError;
|
|
216
222
|
}>;
|
|
223
|
+
/** 工作目录已建好时 `connect` 的那一答。 */
|
|
224
|
+
private preparedAnswer;
|
|
225
|
+
/** 建工作目录失败时 `connect` 的那一答(那一次的失败:下一次重来)。 */
|
|
226
|
+
private prepareFailed;
|
|
217
227
|
suspendVM(_options?: VmLifecycleOptions): Promise<{
|
|
218
228
|
ok: true;
|
|
219
229
|
value: SnapshotId;
|
|
@@ -474,6 +484,13 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
474
484
|
private childHome;
|
|
475
485
|
private fileError;
|
|
476
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* host 环境配置座的拼法(构造期的 `workspaceDir` / `workspaceBase` / 托管 `sharedRoot`,与工厂托管判读的腿目录同一只):与执行缝同一只读者
|
|
489
|
+
* (`exec-path-law.ts` 的 `anchoredPath`)—— 绝对的原样,相对的锚在本进程工作目录上,不折 `..`、不解链接。win32 的盘相对拼写(`C:work`)相对的是
|
|
490
|
+
* **那只盘**上的工作目录:锚在本进程在那只盘上的工作目录(`resolve("C:")`)下、余下原样接上(不这样读,它会被当成一段名字接在工作目录后面:`<cwd>\C:work`)。
|
|
491
|
+
* `pathApi` 只为让 POSIX 上的钉量 win32 形;生产读本机的 `path`。
|
|
492
|
+
*/
|
|
493
|
+
export declare function hostSeatPath(p: string, pathApi?: PlatformPath): string;
|
|
477
494
|
/**
|
|
478
495
|
* `ExecutionEnvFactory` for the TOC `host` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment makes
|
|
479
496
|
* its agent run DIRECTLY on the worker's machine (no container) — the local-coding-agent default. One fresh
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { writeLogText } from "../observability/logger.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
4
|
+
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,
|
|
8
|
+
import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, accessSync, 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";
|
|
@@ -13,7 +13,7 @@ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
|
|
|
13
13
|
import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
14
14
|
import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
|
|
15
15
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
16
|
-
import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, physicalPathOf
|
|
16
|
+
import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, physicalPathOf, } from "@sema-agent/core";
|
|
17
17
|
import { capabilitiesFor } from "../execution-lane-caps.js";
|
|
18
18
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
|
|
19
19
|
import { LaneHomeSeat, laneHomeRefusal } from "../lane-home-probe.js";
|
|
@@ -21,7 +21,9 @@ 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
25
|
import { OWNER_ONLY_FILE_MODE } from "./owner-only-file-mode.js";
|
|
26
|
+
import { createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
|
|
25
27
|
const PROVIDER = "host";
|
|
26
28
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
27
29
|
const inflightFsDrainWaitMs = () => numEnvOr("HOST_FS_DRAIN_WAIT_MS", 10_000, 1);
|
|
@@ -131,18 +133,23 @@ export class RemoteHostExecutionEnv {
|
|
|
131
133
|
}
|
|
132
134
|
tenure;
|
|
133
135
|
preparing;
|
|
136
|
+
seat = createConnectSeat({
|
|
137
|
+
live: () => (this.prepared && !this.destroyed ? this.handle : undefined),
|
|
138
|
+
inFlight: () => (this.prepared ? undefined : this.preparing?.then(() => this.preparedAnswer(), (e) => this.prepareFailed(e))),
|
|
139
|
+
establish: (config) => withdrawOnAbort(this.ensurePrepared().then(() => this.preparedAnswer(), (e) => this.prepareFailed(e)), config?.abortSignal),
|
|
140
|
+
});
|
|
134
141
|
handle;
|
|
135
142
|
constructor(config = {}) {
|
|
136
143
|
if (config.workspaceDir) {
|
|
137
|
-
this.workspaceDir =
|
|
144
|
+
this.workspaceDir = hostSeatPath(config.workspaceDir);
|
|
138
145
|
const shared = config.custody?.sharedRoot;
|
|
139
|
-
this.tenure = shared !== undefined ? { kind: "held", root:
|
|
146
|
+
this.tenure = shared !== undefined ? { kind: "held", root: hostSeatPath(shared) } : BORROWED;
|
|
140
147
|
}
|
|
141
148
|
else {
|
|
142
|
-
const base = config.workspaceBase ?? os.tmpdir();
|
|
149
|
+
const base = hostSeatPath(config.workspaceBase ?? os.tmpdir());
|
|
143
150
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
144
151
|
const suffix = randomBytes(4).toString("hex");
|
|
145
|
-
this.workspaceDir = path
|
|
152
|
+
this.workspaceDir = anchorUnder(path, base, `sema-host-${sanitizeId(id)}-${suffix}`);
|
|
146
153
|
this.tenure = { kind: "held", root: this.workspaceDir };
|
|
147
154
|
}
|
|
148
155
|
const inheritEnvProblem = inheritEnvPolicyProblem(config.inheritEnv);
|
|
@@ -185,18 +192,14 @@ export class RemoteHostExecutionEnv {
|
|
|
185
192
|
return unsupported("connect(snapshotId)");
|
|
186
193
|
if (this.destroyed)
|
|
187
194
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
await this.ensurePrepared();
|
|
192
|
-
}
|
|
193
|
-
catch (e) {
|
|
194
|
-
return { ok: false, error: new RemoteExecutionError("connect_failed", `failed to create host workspace ${this.workspaceDir}: ${errMsg(e)}`, asError(e)) };
|
|
195
|
-
}
|
|
196
|
-
if (config?.secrets?.length) {
|
|
197
|
-
}
|
|
195
|
+
return this.seat.connect(config);
|
|
196
|
+
}
|
|
197
|
+
preparedAnswer() {
|
|
198
198
|
return ok(this.handle);
|
|
199
199
|
}
|
|
200
|
+
prepareFailed(e) {
|
|
201
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `failed to create host workspace ${this.workspaceDir}: ${errMsg(e)}`, asError(e)) };
|
|
202
|
+
}
|
|
200
203
|
async suspendVM(_options) {
|
|
201
204
|
return unsupported("suspendVM");
|
|
202
205
|
}
|
|
@@ -1327,7 +1330,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1327
1330
|
return false;
|
|
1328
1331
|
const real = (x) => {
|
|
1329
1332
|
try {
|
|
1330
|
-
return
|
|
1333
|
+
return physicalPathOf(x);
|
|
1331
1334
|
}
|
|
1332
1335
|
catch {
|
|
1333
1336
|
return undefined;
|
|
@@ -1408,6 +1411,13 @@ export class RemoteHostExecutionEnv {
|
|
|
1408
1411
|
return new FileError("unknown", msg, p);
|
|
1409
1412
|
}
|
|
1410
1413
|
}
|
|
1414
|
+
export function hostSeatPath(p, pathApi = path) {
|
|
1415
|
+
if (pathApi.sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(p)) {
|
|
1416
|
+
const driveCwd = pathApi.resolve(p.slice(0, 2));
|
|
1417
|
+
return p.length === 2 ? driveCwd : anchorUnder(pathApi, driveCwd, p.slice(2));
|
|
1418
|
+
}
|
|
1419
|
+
return anchoredPath(pathApi, process.cwd(), p);
|
|
1420
|
+
}
|
|
1411
1421
|
function sanitizeId(id) {
|
|
1412
1422
|
return id.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "task";
|
|
1413
1423
|
}
|
|
@@ -1430,36 +1440,18 @@ function signalNumber(signal) {
|
|
|
1430
1440
|
export function hostExecutionEnvFactory(config = {}, legOf = () => ({})) {
|
|
1431
1441
|
const custody = createWorkspaceCustody();
|
|
1432
1442
|
const heldElsewhere = (self, root) => custody.holder((m) => m !== self && m.heldWorkspace === root) !== undefined;
|
|
1443
|
+
const rootHolding = (dir) => custody.holder((m) => m.heldWorkspace !== undefined && pathIsWithin(m.heldWorkspace, dir, canonicalPathOf))?.heldWorkspace;
|
|
1433
1444
|
return (ctx) => {
|
|
1434
1445
|
const { minted, ...leg } = legOf(ctx);
|
|
1435
1446
|
const identity = ctx.taskId ?? ctx.sessionId;
|
|
1436
1447
|
return custody.envOf(identity, () => {
|
|
1437
1448
|
const dir = leg.workspaceDir ?? config.workspaceDir;
|
|
1438
|
-
const sharedRoot = dir === undefined ? undefined :
|
|
1449
|
+
const sharedRoot = dir === undefined ? undefined : rootHolding(hostSeatPath(dir));
|
|
1439
1450
|
const env = new RemoteHostExecutionEnv({ ...config, ...leg, id: config.id ?? identity, custody: { heldElsewhere, ...(sharedRoot !== undefined ? { sharedRoot } : {}) } });
|
|
1440
1451
|
minted?.(env);
|
|
1441
1452
|
return env;
|
|
1442
1453
|
});
|
|
1443
1454
|
};
|
|
1444
1455
|
}
|
|
1445
|
-
function dirWithin(dir, root) {
|
|
1446
|
-
const inside = (d, r) => {
|
|
1447
|
-
const rel = path.relative(r, d);
|
|
1448
|
-
return rel === "" || (rel.split(path.sep)[0] !== ".." && !path.isAbsolute(rel));
|
|
1449
|
-
};
|
|
1450
|
-
return inside(path.resolve(dir), path.resolve(root)) || inside(physicalPathOf(dir), physicalPathOf(root));
|
|
1451
|
-
}
|
|
1452
|
-
function physicalPathOf(p) {
|
|
1453
|
-
let head = path.resolve(p);
|
|
1454
|
-
const tail = [];
|
|
1455
|
-
while (!existsSync(head)) {
|
|
1456
|
-
const up = path.dirname(head);
|
|
1457
|
-
if (up === head)
|
|
1458
|
-
return path.resolve(p);
|
|
1459
|
-
tail.unshift(path.basename(head));
|
|
1460
|
-
head = up;
|
|
1461
|
-
}
|
|
1462
|
-
return path.join(realpathSync(head), ...tail);
|
|
1463
|
-
}
|
|
1464
1456
|
export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
1465
1457
|
//# sourceMappingURL=remote-env-host.js.map
|