@sema-agent/server 7.138.0 → 7.140.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 +457 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +91 -41
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/runner.js +1 -1
- package/dist/adoption/sql.js +1 -1
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +26 -26
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/live-deps.js +3 -2
- package/dist/bench/s1/row.d.ts +5 -2
- package/dist/bench/s1/row.js +2 -22
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/adoption.js +1 -1
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +28 -34
- package/dist/boot/execution-lane.d.ts +9 -1
- package/dist/boot/execution-lane.js +6 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-boundary.d.ts +14 -51
- package/dist/boot/memory-boundary.js +10 -20
- package/dist/boot/memory-consolidation.d.ts +15 -11
- package/dist/boot/memory-consolidation.js +25 -14
- package/dist/boot/resolve-spec.d.ts +4 -6
- package/dist/boot/resolve-spec.js +10 -13
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +6 -4
- package/dist/boot/runner-deps.js +3 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-03-store-backend.d.ts +2 -6
- package/dist/boot/stage-03-store-backend.js +2 -3
- package/dist/boot/stage-04-stores.d.ts +2 -6
- package/dist/boot/stage-04-stores.js +5 -5
- package/dist/boot/stage-05-execution-env.d.ts +2 -6
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +2 -6
- package/dist/boot/stage-06-runners.js +2 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -6
- package/dist/boot/stage-08-reapers.d.ts +2 -6
- package/dist/boot/stage-09-leader.d.ts +2 -6
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.d.ts +2 -6
- package/dist/boot/stage-10-http-server.js +7 -6
- package/dist/boot/stores.d.ts +3 -6
- package/dist/boot/stores.js +27 -40
- package/dist/boot/task-list-lane.d.ts +1 -1
- package/dist/boot/task-list-lane.js +1 -1
- package/dist/boot/web-admin.js +37 -11
- package/dist/capabilities/execution-lane.d.ts +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +9 -8
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +33 -37
- package/dist/config-provider.js +51 -30
- package/dist/config-types.d.ts +21 -7
- package/dist/config.d.ts +33 -0
- package/dist/config.js +46 -16
- package/dist/delegated-session-owner.d.ts +4 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/deployment-shape.d.ts +10 -9
- package/dist/deployment-shape.js +4 -4
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +115 -105
- package/dist/execution-lane-caps.js +76 -82
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
- package/dist/http/memory-compliance-failure-reply.js +5 -0
- package/dist/http/memory-layer-reply.d.ts +30 -0
- package/dist/http/memory-layer-reply.js +24 -0
- package/dist/http/resume-legs.js +63 -36
- package/dist/http/retired-service-deps.d.ts +1 -1
- package/dist/http/retired-service-deps.js +1 -1
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/adoption.js +1 -1
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +8 -1
- package/dist/http/routes/memory-consolidation.js +12 -1
- package/dist/http/routes/memory-origin.js +14 -2
- package/dist/http/routes/memory-policy.js +5 -0
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +143 -22
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +41 -33
- package/dist/http/server.d.ts +40 -44
- package/dist/http/server.js +13 -2
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/identity-source.d.ts +1 -1
- package/dist/identity-source.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/lane-home-probe.d.ts +68 -42
- package/dist/lane-home-probe.js +53 -30
- package/dist/leader/wire.js +5 -4
- package/dist/memory-bundle-engine.d.ts +16 -9
- package/dist/memory-bundle-engine.js +29 -6
- package/dist/memory-operator-faces.d.ts +85 -30
- package/dist/memory-operator-faces.js +136 -30
- package/dist/memory-project-layer.d.ts +3 -21
- package/dist/memory-project-layer.js +11 -8
- package/dist/memory-scope-stores.d.ts +73 -0
- package/dist/memory-scope-stores.js +86 -0
- package/dist/memory-scope.d.ts +29 -5
- package/dist/memory-scope.js +16 -4
- package/dist/memory-sync-client.d.ts +70 -18
- package/dist/memory-sync-client.js +215 -117
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/adoption-log-sql.js +4 -4
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +44 -26
- package/dist/plugins/approval-nonce-store.js +4 -4
- package/dist/plugins/blob-backend.d.ts +4 -4
- package/dist/plugins/blob-backend.js +7 -7
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-index-columns.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +21 -23
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +68 -122
- package/dist/plugins/ensure-index.d.ts +1 -2
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +9 -8
- package/dist/plugins/file-history-store-sql.js +9 -9
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/image-bake-store-sql.js +9 -9
- package/dist/plugins/image-index-sql.js +11 -11
- package/dist/plugins/leader-run-store-sql.js +4 -4
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/mailbox-store-sql.d.ts +2 -1
- package/dist/plugins/mailbox-store-sql.js +4 -4
- package/dist/plugins/memory-engine-tidb.js +5 -5
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-sync-store-tidb.js +3 -3
- package/dist/plugins/one-time-migrations.d.ts +117 -8
- package/dist/plugins/one-time-migrations.js +408 -38
- package/dist/plugins/org-delivery-quota-store.js +2 -2
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/outcome-ledger-sql.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
- package/dist/plugins/permission-rule-store-sql.js +29 -32
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +18 -19
- package/dist/plugins/remote-env-adb.js +91 -66
- package/dist/plugins/remote-env-device.d.ts +23 -31
- package/dist/plugins/remote-env-device.js +19 -17
- package/dist/plugins/remote-env-e2b.d.ts +27 -21
- package/dist/plugins/remote-env-e2b.js +91 -45
- package/dist/plugins/remote-env-file-error.d.ts +3 -1
- package/dist/plugins/remote-env-file-error.js +9 -3
- package/dist/plugins/remote-env-host.d.ts +31 -34
- package/dist/plugins/remote-env-host.js +91 -72
- package/dist/plugins/remote-env-k8s.d.ts +13 -29
- package/dist/plugins/remote-env-k8s.js +63 -55
- package/dist/plugins/remote-env-local-docker.d.ts +24 -12
- package/dist/plugins/remote-env-local-docker.js +49 -61
- package/dist/plugins/remote-env-ssh.d.ts +11 -19
- package/dist/plugins/remote-env-ssh.js +74 -57
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/resume-anchor-store-sql.js +2 -2
- package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
- package/dist/plugins/retention-lane-store-sql.js +2 -2
- package/dist/plugins/retention-store-sql.d.ts +10 -7
- package/dist/plugins/retention-store-sql.js +13 -7
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +20 -15
- package/dist/plugins/schema-consistency.d.ts +21 -11
- package/dist/plugins/schema-consistency.js +15 -9
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/schema-ensure-legs.d.ts +2 -3
- package/dist/plugins/schema-ensure-legs.js +2 -2
- package/dist/plugins/schema-live-columns.d.ts +1 -1
- package/dist/plugins/session-capture-record-store-sql.js +1 -1
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-rules-column.d.ts +1 -1
- package/dist/plugins/session-rules-column.js +1 -1
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.js +1 -1
- package/dist/plugins/shared-memory-store-sql.js +2 -2
- package/dist/plugins/sql-driver.d.ts +10 -5
- package/dist/plugins/sql-driver.js +6 -6
- package/dist/plugins/sql-errors.js +1 -1
- package/dist/plugins/store-backend.d.ts +5 -7
- package/dist/plugins/store-backend.js +23 -11
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/task-list-store-sql.js +11 -11
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +13 -7
- package/dist/plugins/tidb-session-storage.js +4 -4
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +127 -59
- package/dist/plugins/tool-result-store-sql.d.ts +5 -4
- package/dist/plugins/tool-result-store-sql.js +21 -13
- package/dist/plugins/user-input-ledger-sql.js +2 -2
- package/dist/plugins/workflow-journal-store-sql.js +5 -5
- package/dist/plugins/workflow-run-store-sql.js +19 -19
- package/dist/plugins/write-behind-counter.d.ts +2 -3
- package/dist/plugins/write-behind-counter.js +5 -5
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/resource-suspend.d.ts +14 -12
- package/dist/resource-suspend.js +1 -1
- package/dist/resource-window.d.ts +4 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +34 -35
- package/dist/runs.d.ts +10 -5
- package/dist/runs.js +52 -66
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +20 -10
- package/dist/session-incarnation.js +5 -3
- package/dist/session-policy-wire.d.ts +1 -1
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/sql-engine-posture.d.ts +1 -1
- package/dist/sql-engine-posture.js +1 -1
- package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
- package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
- package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
- package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
- package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
- package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-a2a.js +5 -6
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +22 -9
- package/dist/task-mcp.js +17 -9
- package/dist/terminal.d.ts +27 -0
- package/dist/terminal.js +6 -0
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/redact.d.ts +3 -1
- package/dist/trace/redact.js +80 -28
- package/dist/trace/self-minted-wire.d.ts +169 -2
- package/dist/trace/self-minted-wire.js +54 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/candidate-gate.d.ts +11 -7
- package/dist/web-admin-client/candidate-gate.js +1 -1
- package/dist/web-admin-client/http-client.d.ts +5 -4
- package/dist/web-admin-client/http-client.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +7 -17
- package/dist/web-admin-client/read-warnings.js +5 -9
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/wire-changes/server@7.140.0.json +184 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -23,7 +23,7 @@ export interface LocalDockerEnvConfig extends LaneHomeConfig {
|
|
|
23
23
|
* lane stays `local-docker` (`REMOTE_EXEC=local-docker` + `DOCKER_HOST`; the settings-schema lane table lists
|
|
24
24
|
* `DOCKER_HOST` as this lane's optional coordinate, and there is no separate `remote-docker` lane since 7.0.0).
|
|
25
25
|
* Nothing about the class is host-local: the workspace is `mkdir`'d INSIDE the container (never a bind-mount of a
|
|
26
|
-
* host path) and bytes move by `docker
|
|
26
|
+
* host path) and bytes move by `docker exec -i … cat >` / `docker exec cat`, which stream over the daemon API.
|
|
27
27
|
*/
|
|
28
28
|
dockerHost?: string;
|
|
29
29
|
/** Container memory limit (docker `--memory`, e.g. "2g"). Absent = no limit. */
|
|
@@ -61,17 +61,11 @@ export interface LocalDockerEnvDeps {
|
|
|
61
61
|
spawn?: typeof spawn;
|
|
62
62
|
}
|
|
63
63
|
export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv {
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
readonly capabilities: {
|
|
67
|
-
isolation: boolean;
|
|
68
|
-
suspendable: boolean;
|
|
69
|
-
canonicalPathAuthoritative: boolean;
|
|
70
|
-
sharesProcessTable: boolean;
|
|
71
|
-
};
|
|
64
|
+
/** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
|
|
65
|
+
readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
|
|
72
66
|
/** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
|
|
73
67
|
cwd: string;
|
|
74
|
-
/** core `ExecutionEnv.homeDir`([ref]):本容器里执行用户的 home ——
|
|
68
|
+
/** core `ExecutionEnv.homeDir`([ref]):本容器里执行用户的 home —— 声明在场 ⇒ 声明(开环境那一拍与 `$HOME` 对证过),否则开容器那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
|
|
75
69
|
get homeDir(): string | undefined;
|
|
76
70
|
/** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在容器里(镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
77
71
|
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
@@ -92,7 +86,7 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
92
86
|
private connecting?;
|
|
93
87
|
/** 家目录:环境答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。 */
|
|
94
88
|
private homeRefusal?;
|
|
95
|
-
/** 家目录的座(
|
|
89
|
+
/** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则开容器那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
|
|
96
90
|
private readonly homeSeat;
|
|
97
91
|
constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps);
|
|
98
92
|
private resolve;
|
|
@@ -163,10 +157,27 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
163
157
|
maxLines?: number;
|
|
164
158
|
abortSignal?: AbortSignal;
|
|
165
159
|
}): Promise<Result<string[], FileError>>;
|
|
160
|
+
/** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
|
|
161
|
+
* 上传 = 容器内 `cat` 从 stdin 收整份字节落到私有目录里的暂存件({@link pipeIn});提交 = 容器内一次 shell 往返。两者同一身份 ——
|
|
162
|
+
* 同一只 argv 构造({@link execArgv}),都是镜像的用户。不用 `docker cp`:它由守护进程以 root 落盘,镜像用户 ≠ root 时暂存件属 root、
|
|
163
|
+
* 提交方拿不走也清不掉,「目录不收新名字」也探不出来(root 哪里都写得进)。 */
|
|
164
|
+
private stagedLane;
|
|
165
|
+
/** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `docker cp` 直落目标(以守护进程的 root 身份):传输中途断 / 取消时目标剩什么取决于守护进程的解包实现,本适配器不依赖它。 */
|
|
166
166
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
167
|
+
/** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份 cp(O(文件);中途断 ⇒ 目标没了或半份)。
|
|
168
|
+
* 目录不收新名字 / 私有目录收不成私有时的退路 = 容器内 `cat >>` 从 stdin 直接追加到目标(同一身份)。 */
|
|
167
169
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
170
|
+
/**
|
|
171
|
+
* 把整份内容经 stdin 交给容器里的 `cat`,落到 `dest`(`>` 建或覆盖;`>>` 追加)。走与提交 / 文件面探针同一只往返({@link fsExec},argv 只多一个 `-i`)
|
|
172
|
+
* ⇒ 同一身份、同一工作目录、同一道守卫;二进制安全(载荷不经 shell 转义,也不在宿主落临时件)。stdin 没写完而进程退 0 ⇒ 按失败答(载荷未必整份到了)。
|
|
173
|
+
*/
|
|
174
|
+
private pipeIn;
|
|
168
175
|
private posixFsInst?;
|
|
169
176
|
private get posixFs();
|
|
177
|
+
/** 文件面的一次容器内 shell 往返(基座、暂存写的准备 / 上传 / 提交 / 清理共用)。承 fsReady 契约(其余三腿走公有 exec、守卫天然在;本腿直用私有
|
|
178
|
+
* docker,必须自带):destroyed 守卫 + 懒连接,fail closed 不抛 —— 消息保留 "destroyed" 字样(测试钉的就是它)。`input` 在场 ⇒ 同一只 argv 多一个
|
|
179
|
+
* `-i`、载荷走 stdin({@link pipeIn});身份与工作目录与不带载荷的往返逐字相同。 */
|
|
180
|
+
private fsExec;
|
|
170
181
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
171
182
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
172
183
|
readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
@@ -200,7 +211,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
200
211
|
/** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
|
|
201
212
|
private withHostFlag;
|
|
202
213
|
/** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
|
|
203
|
-
* argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`.
|
|
214
|
+
* argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`.
|
|
215
|
+
* `stdin` ⇒ `-i` (the payload of {@link pipeIn}); nothing else differs, so a piped upload runs as the same user as every other exec. */
|
|
204
216
|
private execArgv;
|
|
205
217
|
/** Like {@link execArgv} but for an internal FileSystem helper command (uses the workspace cwd + adapter env). */
|
|
206
218
|
private execRaw;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
2
|
import { spawn } from "node:child_process";
|
|
5
3
|
import { hostSubprocessEnv } from "../server-secret-env.js";
|
|
6
4
|
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
7
5
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
6
|
+
import { shellQuote, armPipeDestroyGrace, appendFileStaged, writeFileStaged } from "./remote-shell.js";
|
|
9
7
|
import { signalNumber } from "./host-platform.js";
|
|
10
8
|
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
9
|
+
import { buildCapturedOutput, createCutError } from "../exec-cut.js";
|
|
11
10
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
11
|
+
import { capabilitiesFor } from "../execution-lane-caps.js";
|
|
12
12
|
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
13
13
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
14
14
|
const PROVIDER = "local-docker";
|
|
@@ -21,7 +21,7 @@ const unsupported = (op) => ({
|
|
|
21
21
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a local-docker target (v1: a container is an isolation boundary but not a managed memory snapshot; capabilities.suspendable=false)`),
|
|
22
22
|
});
|
|
23
23
|
export class RemoteLocalDockerExecutionEnv {
|
|
24
|
-
capabilities = {
|
|
24
|
+
capabilities = capabilitiesFor({ remoteExec: { provider: "local-docker" } });
|
|
25
25
|
cwd;
|
|
26
26
|
get homeDir() {
|
|
27
27
|
return this.homeSeat.home;
|
|
@@ -50,7 +50,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
50
50
|
controlTimeoutMs: config.controlTimeoutMs ?? DEFAULT_CONTROL_TIMEOUT_MS,
|
|
51
51
|
};
|
|
52
52
|
this.cwd = this.cfg.mountPath;
|
|
53
|
-
this.homeSeat = new LaneHomeSeat(
|
|
53
|
+
this.homeSeat = new LaneHomeSeat(PROVIDER, config);
|
|
54
54
|
this.spawnFn = deps.spawn ?? spawn;
|
|
55
55
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
56
56
|
const suffix = randomBytes(4).toString("hex");
|
|
@@ -383,71 +383,48 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
383
383
|
lines = lines.slice(0, options.maxLines);
|
|
384
384
|
return ok(lines);
|
|
385
385
|
}
|
|
386
|
+
stagedLane(content) {
|
|
387
|
+
return { upload: (dest, signal) => this.pipeIn(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
|
|
388
|
+
}
|
|
386
389
|
async writeFile(p, content, abortSignal) {
|
|
387
390
|
const ready = await this.fsReady(p);
|
|
388
391
|
if (!ready.ok)
|
|
389
392
|
return ready;
|
|
390
|
-
|
|
391
|
-
const dir = path.posix.dirname(abs);
|
|
392
|
-
if (dir && dir !== "." && dir !== "/") {
|
|
393
|
-
const mk = await this.docker(this.execRaw(`mkdir -p ${shellQuote(dir)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
394
|
-
if (!mk.ok)
|
|
395
|
-
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
396
|
-
if (mk.value.exitCode !== 0)
|
|
397
|
-
return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
|
|
398
|
-
}
|
|
399
|
-
let local;
|
|
400
|
-
try {
|
|
401
|
-
local = path.join(os.tmpdir(), `docker-cp-${Date.now()}-${randomBytes(6).toString("hex")}`);
|
|
402
|
-
await fs.writeFile(local, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
|
|
403
|
-
const cp = await this.docker(["cp", local, `${this.containerName}:${abs}`], { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
404
|
-
if (!cp.ok)
|
|
405
|
-
return { ok: false, error: fileErrorFromExec(cp.error, abs) };
|
|
406
|
-
if (cp.value.exitCode !== 0)
|
|
407
|
-
return { ok: false, error: classifyFsStderr(cp.value.stderr, abs) };
|
|
408
|
-
return ok(undefined);
|
|
409
|
-
}
|
|
410
|
-
catch (e) {
|
|
411
|
-
return { ok: false, error: new FileError("unknown", errMsg(e), abs) };
|
|
412
|
-
}
|
|
413
|
-
finally {
|
|
414
|
-
if (local)
|
|
415
|
-
await fs.rm(local, { force: true }).catch(() => { });
|
|
416
|
-
}
|
|
393
|
+
return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
|
|
417
394
|
}
|
|
418
395
|
async appendFile(p, content, abortSignal) {
|
|
396
|
+
const ready = await this.fsReady(p);
|
|
397
|
+
if (!ready.ok)
|
|
398
|
+
return ready;
|
|
419
399
|
const abs = this.resolve(p);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
const merged = new Uint8Array(base.length + add.length);
|
|
430
|
-
merged.set(base, 0);
|
|
431
|
-
merged.set(add, base.length);
|
|
432
|
-
return this.writeFile(abs, merged, abortSignal);
|
|
400
|
+
return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.pipeIn(content, abs, signal, ">>")));
|
|
401
|
+
}
|
|
402
|
+
async pipeIn(content, dest, abortSignal, redirect = ">") {
|
|
403
|
+
const r = await this.fsExec(`cat ${redirect} ${shellQuote(dest)}`, abortSignal, typeof content === "string" ? Buffer.from(content, "utf8") : content);
|
|
404
|
+
if (!r.ok)
|
|
405
|
+
return { ok: false, error: fileErrorFromExec(r.error, dest) };
|
|
406
|
+
if (r.value.exitCode !== 0)
|
|
407
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, dest) };
|
|
408
|
+
return ok(undefined);
|
|
433
409
|
}
|
|
434
410
|
posixFsInst;
|
|
435
411
|
get posixFs() {
|
|
436
412
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
437
|
-
run:
|
|
438
|
-
if (this.destroyed)
|
|
439
|
-
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
440
|
-
if (!this.containerId) {
|
|
441
|
-
const c = await this.connect();
|
|
442
|
-
if (!c.ok)
|
|
443
|
-
return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`) };
|
|
444
|
-
}
|
|
445
|
-
return this.docker(this.execRaw(cmd), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
446
|
-
},
|
|
413
|
+
run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
|
|
447
414
|
resolve: (p) => this.resolve(p),
|
|
448
415
|
tempDir: () => this.cwd,
|
|
449
416
|
}));
|
|
450
417
|
}
|
|
418
|
+
async fsExec(cmd, abortSignal, input) {
|
|
419
|
+
if (this.destroyed)
|
|
420
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
421
|
+
if (!this.containerId) {
|
|
422
|
+
const c = await this.connect();
|
|
423
|
+
if (!c.ok)
|
|
424
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`, c.error) };
|
|
425
|
+
}
|
|
426
|
+
return this.docker(this.execArgv(cmd, undefined, undefined, input !== undefined), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
|
|
427
|
+
}
|
|
451
428
|
async fileInfo(p, abortSignal) {
|
|
452
429
|
return this.posixFs.statFileInfo(p, abortSignal);
|
|
453
430
|
}
|
|
@@ -484,8 +461,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
484
461
|
withHostFlag(args) {
|
|
485
462
|
return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
|
|
486
463
|
}
|
|
487
|
-
execArgv(command, cwd, env) {
|
|
488
|
-
const args = ["exec"];
|
|
464
|
+
execArgv(command, cwd, env, stdin = false) {
|
|
465
|
+
const args = stdin ? ["exec", "-i"] : ["exec"];
|
|
489
466
|
const dir = cwd != null ? this.resolve(cwd) : this.cwd;
|
|
490
467
|
args.push("-w", dir);
|
|
491
468
|
const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
|
|
@@ -501,7 +478,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
501
478
|
return new Promise((resolve) => {
|
|
502
479
|
let settled = false;
|
|
503
480
|
let forceSettleTimer;
|
|
504
|
-
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
|
|
481
|
+
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: [opts?.input !== undefined ? "pipe" : "ignore", "pipe", "pipe"], env: this.cliEnv() });
|
|
505
482
|
const killGrace = () => {
|
|
506
483
|
try {
|
|
507
484
|
child.kill("SIGKILL");
|
|
@@ -524,11 +501,11 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
524
501
|
const timeoutMs = opts?.timeoutMs ?? this.cfg.controlTimeoutMs;
|
|
525
502
|
const timer = setTimeout(() => {
|
|
526
503
|
killGrace();
|
|
527
|
-
finish({ ok: false, error:
|
|
504
|
+
finish({ ok: false, error: createCutError({ code: "timeout", message: `docker ${args[0]} exceeded ${timeoutMs}ms` }, buildCapturedOutput(outTail, errBuf)) });
|
|
528
505
|
}, timeoutMs);
|
|
529
506
|
const onAbort = () => {
|
|
530
507
|
killGrace();
|
|
531
|
-
finish({ ok: false, error:
|
|
508
|
+
finish({ ok: false, error: createCutError({ code: "aborted", message: "docker command aborted" }, buildCapturedOutput(outTail, errBuf)) });
|
|
532
509
|
};
|
|
533
510
|
if (opts?.signal) {
|
|
534
511
|
if (opts.signal.aborted)
|
|
@@ -541,10 +518,21 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
541
518
|
outTail.push(d); opts?.onStdout?.(d.toString()); });
|
|
542
519
|
child.stderr?.on("data", (d) => { errBuf.push(d); opts?.onStderr?.(d.toString()); });
|
|
543
520
|
child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `docker spawn failed (is '${this.cfg.dockerPath}' installed?): ${e.message}`, e) }));
|
|
521
|
+
let stdinFailure;
|
|
522
|
+
if (opts?.input !== undefined) {
|
|
523
|
+
if (!child.stdin) {
|
|
524
|
+
killGrace();
|
|
525
|
+
return finish({ ok: false, error: new ExecutionError("spawn_error", `docker ${args[0]}: no stdin pipe for the payload`) });
|
|
526
|
+
}
|
|
527
|
+
child.stdin.on("error", (e) => { stdinFailure = e.message; });
|
|
528
|
+
child.stdin.end(Buffer.from(opts.input.buffer, opts.input.byteOffset, opts.input.byteLength));
|
|
529
|
+
}
|
|
544
530
|
child.on("close", (code, signal) => {
|
|
545
531
|
if (forceSettleTimer)
|
|
546
532
|
clearTimeout(forceSettleTimer);
|
|
547
533
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 1);
|
|
534
|
+
if (exitCode === 0 && stdinFailure !== undefined)
|
|
535
|
+
return finish({ ok: false, error: new ExecutionError("unknown", `docker ${args[0]}: the payload could not be written to stdin (${stdinFailure})`) });
|
|
548
536
|
const err = errBuf.result();
|
|
549
537
|
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
550
538
|
if (binary) {
|
|
@@ -610,7 +598,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
610
598
|
return ok(undefined);
|
|
611
599
|
const c = await this.connect();
|
|
612
600
|
if (!c.ok)
|
|
613
|
-
return { ok: false, error:
|
|
601
|
+
return { ok: false, error: fileErrorFromExec(c.error, this.resolve(p)) };
|
|
614
602
|
return ok(undefined);
|
|
615
603
|
}
|
|
616
604
|
dockerToRemoteError(e, fallback) {
|
|
@@ -53,23 +53,8 @@ export interface SshEnvDeps {
|
|
|
53
53
|
connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
|
|
54
54
|
}
|
|
55
55
|
export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
* 就是**命令真正跑的那块文件系统**的权威答案(`posix-shell-fs` 的 `canonicalPath` 原语,在真机上解到落点)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
|
|
59
|
-
* (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
|
|
60
|
-
* 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
|
|
61
|
-
* 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
|
|
62
|
-
* 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
|
|
63
|
-
* core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
|
|
64
|
-
* 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
|
|
65
|
-
*/
|
|
66
|
-
/** `sharesProcessTable`(core 7.57.0,必填):命令跑在 SSH 目标机上;目标恰是本机(或本进程所在容器的宿主)时命令与本进程同表,这一形今天没有配置面可声明 `true`。 */
|
|
67
|
-
readonly capabilities: {
|
|
68
|
-
isolation: boolean;
|
|
69
|
-
suspendable: boolean;
|
|
70
|
-
canonicalPathAuthoritative: boolean;
|
|
71
|
-
sharesProcessTable: boolean;
|
|
72
|
-
};
|
|
56
|
+
/** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
|
|
57
|
+
readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
|
|
73
58
|
/** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at mountPath. */
|
|
74
59
|
cwd: string;
|
|
75
60
|
private conn?;
|
|
@@ -86,9 +71,9 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
86
71
|
* =重连风暴);②共享尝试**不携带任何 caller 信号**(首创者的 pre-abort 不得毒化 joiner;单 caller
|
|
87
72
|
* 取消只退它自己的等待,底层握手余命 ≤ readyTimeout 有界,彻底取消归 destroy 代际闸)。 */
|
|
88
73
|
private inFlight?;
|
|
89
|
-
/** core `ExecutionEnv.homeDir`([ref]):目标机上登录用户的 home ——
|
|
74
|
+
/** core `ExecutionEnv.homeDir`([ref]):目标机上登录用户的 home —— 声明在场 ⇒ 声明(开环境那一拍与 `$HOME` 对证过),否则连上之后实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
|
|
90
75
|
get homeDir(): string | undefined;
|
|
91
|
-
/** 家目录的座(
|
|
76
|
+
/** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则连上之后实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
|
|
92
77
|
private readonly homeSeat;
|
|
93
78
|
/** 家目录声明 × 实测的判定,按连接记(同一条连接只问一次;重连换了连接再问 —— 登录用户不变时答案相同,代价一次往返;没声明且已量到绝对路径之后不再问)。 */
|
|
94
79
|
private homeChecked?;
|
|
@@ -186,7 +171,14 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
186
171
|
abortSignal?: AbortSignal;
|
|
187
172
|
}): Promise<Result<string[], FileError>>;
|
|
188
173
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
174
|
+
/** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
|
|
175
|
+
* 上传 = sftp 整份写到私有目录里的暂存件;提交 = 文件面探针 exec。两者同一身份(sftp 子系统与 exec 都是这条连接的登录用户)。
|
|
176
|
+
* sftp 写不收取消信号 ⇒ 取消在传输结束之后、提交之前生效(目标未碰)。 */
|
|
177
|
+
private stagedLane;
|
|
178
|
+
/** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `sftp.writeFile(abs)` 就地 O_TRUNC:传输中途断 ⇒ 目标 = 半份新件。 */
|
|
189
179
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
180
|
+
/** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份就地重写(O(文件);中途断 ⇒ 半份)。
|
|
181
|
+
* 目录不收新名字 / 私有目录收不成私有时的退路 = sftp 直接追加到目标。 */
|
|
190
182
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
191
183
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
192
184
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Client } from "ssh2";
|
|
3
3
|
import { createSshHostKeyVerifier } from "../ssh-host-key.js";
|
|
4
|
-
import { buildCwdEnvCommand, createCwdGuardedCall,
|
|
4
|
+
import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, kindFromMode, writeFileStaged } from "./remote-shell.js";
|
|
5
5
|
import { signalNumber } from "./host-platform.js";
|
|
6
6
|
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
7
|
+
import { buildCapturedOutput, createCutError } from "../exec-cut.js";
|
|
7
8
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
8
9
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
10
|
+
import { capabilitiesFor } from "../execution-lane-caps.js";
|
|
9
11
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
10
12
|
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
11
13
|
const PROVIDER = "ssh";
|
|
@@ -18,7 +20,7 @@ function exitCodeForSignalKill(signal) {
|
|
|
18
20
|
return 128 + (signalNumber(`SIG${signal}`) ?? 9);
|
|
19
21
|
}
|
|
20
22
|
export class RemoteSshExecutionEnv {
|
|
21
|
-
capabilities = {
|
|
23
|
+
capabilities = capabilitiesFor({ remoteExec: { provider: "ssh" } });
|
|
22
24
|
cwd;
|
|
23
25
|
conn;
|
|
24
26
|
sftpPromise;
|
|
@@ -51,7 +53,7 @@ export class RemoteSshExecutionEnv {
|
|
|
51
53
|
hostKey: config.hostKey,
|
|
52
54
|
};
|
|
53
55
|
this.cwd = this.cfg.mountPath;
|
|
54
|
-
this.homeSeat = new LaneHomeSeat(
|
|
56
|
+
this.homeSeat = new LaneHomeSeat(PROVIDER, config);
|
|
55
57
|
}
|
|
56
58
|
resolve(p) {
|
|
57
59
|
return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
|
|
@@ -363,12 +365,29 @@ export class RemoteSshExecutionEnv {
|
|
|
363
365
|
return new Promise((resolve) => {
|
|
364
366
|
const outBuf = new RollingTailBuffer();
|
|
365
367
|
const errBuf = new RollingTailBuffer();
|
|
368
|
+
const recordStderr = (bytes) => {
|
|
369
|
+
if (bytes.length > 0)
|
|
370
|
+
errBuf.push(bytes);
|
|
371
|
+
};
|
|
372
|
+
const emitStderr = (bytes) => {
|
|
373
|
+
if (bytes.length > 0)
|
|
374
|
+
options?.onStderr?.(bytes.toString());
|
|
375
|
+
};
|
|
376
|
+
const takeStderr = (bytes) => {
|
|
377
|
+
recordStderr(bytes);
|
|
378
|
+
emitStderr(bytes);
|
|
379
|
+
};
|
|
380
|
+
const captured = () => {
|
|
381
|
+
recordStderr(call.endStderr());
|
|
382
|
+
return buildCapturedOutput(outBuf, errBuf);
|
|
383
|
+
};
|
|
366
384
|
let settled = false;
|
|
367
385
|
let streamRef;
|
|
368
|
-
const finish = (
|
|
386
|
+
const finish = (outcome) => {
|
|
369
387
|
if (settled)
|
|
370
|
-
return;
|
|
388
|
+
return false;
|
|
371
389
|
settled = true;
|
|
390
|
+
const r = outcome();
|
|
372
391
|
clearTimeout(timer);
|
|
373
392
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
374
393
|
conn.removeListener("close", onConnClose);
|
|
@@ -378,10 +397,14 @@ export class RemoteSshExecutionEnv {
|
|
|
378
397
|
catch {
|
|
379
398
|
}
|
|
380
399
|
resolve(r);
|
|
400
|
+
return true;
|
|
401
|
+
};
|
|
402
|
+
const cut = (c) => {
|
|
403
|
+
finish(() => ({ ok: false, error: createCutError(c, captured()) }));
|
|
381
404
|
};
|
|
382
|
-
const timer = setTimeout(() =>
|
|
383
|
-
const onAbort = () =>
|
|
384
|
-
const onConnClose = () => finish({ ok: false, error: new ExecutionError("transport_lost", "ssh transport lost mid-command (connection closed before exit status) — reconnect + retry per idempotency") });
|
|
405
|
+
const timer = setTimeout(() => cut({ code: "timeout", message: `ssh exec exceeded ${timeoutMs}ms (provider/command hang)` }), timeoutMs);
|
|
406
|
+
const onAbort = () => cut({ code: "aborted", message: "ssh exec aborted" });
|
|
407
|
+
const onConnClose = () => finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "ssh transport lost mid-command (connection closed before exit status) — reconnect + retry per idempotency") }));
|
|
385
408
|
conn.once("close", onConnClose);
|
|
386
409
|
if (options?.abortSignal) {
|
|
387
410
|
if (options.abortSignal.aborted)
|
|
@@ -390,7 +413,7 @@ export class RemoteSshExecutionEnv {
|
|
|
390
413
|
}
|
|
391
414
|
conn.exec(call.command, (err, stream) => {
|
|
392
415
|
if (err)
|
|
393
|
-
return finish({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) });
|
|
416
|
+
return finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) }));
|
|
394
417
|
streamRef = stream;
|
|
395
418
|
if (settled) {
|
|
396
419
|
try {
|
|
@@ -407,25 +430,20 @@ export class RemoteSshExecutionEnv {
|
|
|
407
430
|
})
|
|
408
431
|
.on("close", (code, signal) => {
|
|
409
432
|
if (code == null && !signal) {
|
|
410
|
-
return finish({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") });
|
|
433
|
+
return finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") }));
|
|
411
434
|
}
|
|
412
|
-
const tail = call.endStderr();
|
|
413
|
-
if (tail.length > 0) {
|
|
414
|
-
errBuf.push(tail);
|
|
415
|
-
options?.onStderr?.(tail.toString());
|
|
416
|
-
}
|
|
417
|
-
const out = outBuf.result();
|
|
418
|
-
const err = errBuf.result();
|
|
419
435
|
const exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 1);
|
|
420
|
-
|
|
436
|
+
const held = call.endStderr();
|
|
437
|
+
const won = finish(() => {
|
|
438
|
+
recordStderr(held);
|
|
439
|
+
const out = outBuf.result();
|
|
440
|
+
const err = errBuf.result();
|
|
441
|
+
return call.settle(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
|
|
442
|
+
});
|
|
443
|
+
if (won)
|
|
444
|
+
emitStderr(held);
|
|
421
445
|
})
|
|
422
|
-
.stderr.on("data", (d) =>
|
|
423
|
-
const c = call.filterStderr(d);
|
|
424
|
-
if (c.length === 0)
|
|
425
|
-
return;
|
|
426
|
-
errBuf.push(c);
|
|
427
|
-
options?.onStderr?.(c.toString());
|
|
428
|
-
});
|
|
446
|
+
.stderr.on("data", (d) => takeStderr(call.filterStderr(d)));
|
|
429
447
|
});
|
|
430
448
|
});
|
|
431
449
|
}
|
|
@@ -562,7 +580,7 @@ export class RemoteSshExecutionEnv {
|
|
|
562
580
|
async sftp(abortSignal) {
|
|
563
581
|
const ready = await this.ensureConnected(abortSignal);
|
|
564
582
|
if (!ready.ok)
|
|
565
|
-
return Promise.reject(ready.error.code === "aborted" ? new FileError("
|
|
583
|
+
return Promise.reject(ready.error.code === "aborted" ? fileErrorFromExec(ready.error) : new FileError("target_unavailable", ready.error.message, undefined, ready.error));
|
|
566
584
|
if (!this.conn)
|
|
567
585
|
return Promise.reject(new Error("SSH not connected"));
|
|
568
586
|
if (!this.sftpPromise) {
|
|
@@ -604,41 +622,37 @@ export class RemoteSshExecutionEnv {
|
|
|
604
622
|
return { ok: false, error: this.fileError(e, abs) };
|
|
605
623
|
}
|
|
606
624
|
}
|
|
625
|
+
stagedLane(data) {
|
|
626
|
+
return {
|
|
627
|
+
upload: async (dest, signal) => {
|
|
628
|
+
try {
|
|
629
|
+
const sftp = await this.sftp(signal);
|
|
630
|
+
await new Promise((resolve, reject) => sftp.writeFile(dest, data, (err) => (err ? reject(err) : resolve())));
|
|
631
|
+
return ok(undefined);
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
return { ok: false, error: this.fileError(e, dest) };
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
run: (cmd, signal) => this.fsExec(cmd, signal),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
607
640
|
async writeFile(p, content, abortSignal) {
|
|
608
|
-
|
|
609
|
-
const dir = path.posix.dirname(abs);
|
|
610
|
-
if (dir && dir !== "." && dir !== "/") {
|
|
611
|
-
const mk = await this.fsExec(`mkdir -p -- ${shellQuote(dir)}`, abortSignal);
|
|
612
|
-
if (!mk.ok)
|
|
613
|
-
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
614
|
-
if (mk.value.exitCode !== 0)
|
|
615
|
-
return { ok: false, error: new FileError("unknown", mk.value.stderr.trim() || `mkdir -p failed for ${dir}`, dir) };
|
|
616
|
-
}
|
|
617
|
-
try {
|
|
618
|
-
const sftp = await this.sftp(abortSignal);
|
|
619
|
-
const data = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
620
|
-
await new Promise((resolve, reject) => sftp.writeFile(abs, data, (err) => (err ? reject(err) : resolve())));
|
|
621
|
-
return ok(undefined);
|
|
622
|
-
}
|
|
623
|
-
catch (e) {
|
|
624
|
-
return { ok: false, error: this.fileError(e, abs) };
|
|
625
|
-
}
|
|
641
|
+
return writeFileStaged(this.stagedLane(bytesOf(content)), this.resolve(p), abortSignal);
|
|
626
642
|
}
|
|
627
643
|
async appendFile(p, content, abortSignal) {
|
|
628
644
|
const abs = this.resolve(p);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
merged.set(add, base.length);
|
|
641
|
-
return this.writeFile(abs, merged, abortSignal);
|
|
645
|
+
const data = bytesOf(content);
|
|
646
|
+
return appendFileStaged(this.stagedLane(data), abs, abortSignal, (ctx) => ctx.inPlace(async (signal) => {
|
|
647
|
+
try {
|
|
648
|
+
const sftp = await this.sftp(signal);
|
|
649
|
+
await new Promise((resolve, reject) => sftp.appendFile(abs, data, (err) => (err ? reject(err) : resolve())));
|
|
650
|
+
return ok(undefined);
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
return { ok: false, error: this.fileError(e, abs) };
|
|
654
|
+
}
|
|
655
|
+
}));
|
|
642
656
|
}
|
|
643
657
|
async fileInfo(p, abortSignal) {
|
|
644
658
|
const abs = this.resolve(p);
|
|
@@ -734,4 +748,7 @@ export class RemoteSshExecutionEnv {
|
|
|
734
748
|
export function sshExecutionEnvFactory(config) {
|
|
735
749
|
return (_ctx) => new RemoteSshExecutionEnv(config);
|
|
736
750
|
}
|
|
751
|
+
function bytesOf(content) {
|
|
752
|
+
return typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
753
|
+
}
|
|
737
754
|
//# sourceMappingURL=remote-env-ssh.js.map
|