@sema-agent/server 7.137.0 → 7.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +600 -142
- package/README.md +7 -3
- package/README.zh-CN.md +6 -3
- package/USAGE.md +132 -41
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +23 -16
- package/dist/approval-card.js +1 -0
- package/dist/audit.js +1 -0
- package/dist/background-agent-name.d.ts +5 -0
- package/dist/background-agent-name.js +5 -0
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/budget-tracing.d.ts +1 -12
- package/dist/boot/deployment-mods.d.ts +46 -16
- package/dist/boot/deployment-mods.js +82 -6
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +12 -18
- package/dist/boot/governance-seams.js +10 -24
- package/dist/boot/leader.d.ts +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/org-delivery.js +15 -2
- package/dist/boot/resolve-spec.d.ts +4 -1
- package/dist/boot/resolve-spec.js +29 -19
- package/dist/boot/retention-lane.d.ts +14 -8
- package/dist/boot/retention-lane.js +2 -8
- package/dist/boot/runner-deps.d.ts +10 -4
- package/dist/boot/runner-deps.js +2 -0
- 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-04-stores.d.ts +1 -12
- package/dist/boot/stage-05-execution-env.d.ts +3 -12
- package/dist/boot/stage-05-execution-env.js +8 -5
- package/dist/boot/stage-06-runners.d.ts +3 -12
- package/dist/boot/stage-06-runners.js +5 -3
- package/dist/boot/stage-07-capability-layer.d.ts +3 -12
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/boot/stage-08-reapers.d.ts +3 -12
- package/dist/boot/stage-09-leader.d.ts +3 -12
- package/dist/boot/stage-09-leader.js +2 -3
- package/dist/boot/stage-10-http-server.d.ts +3 -12
- package/dist/boot/stage-10-http-server.js +5 -3
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.d.ts +5 -2
- package/dist/boot/web-admin.js +5 -5
- package/dist/brain.js +5 -5
- package/dist/budget.d.ts +17 -15
- package/dist/budget.js +2 -2
- package/dist/capabilities/mcp-probe.d.ts +36 -11
- package/dist/capabilities/mcp-probe.js +44 -5
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +21 -7
- package/dist/capability-facts.js +13 -3
- package/dist/checkpoint-identity.d.ts +13 -6
- package/dist/config-catalog.js +14 -8
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +25 -12
- package/dist/config.d.ts +33 -0
- package/dist/config.js +52 -20
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +7 -7
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/gate-settlement.d.ts +71 -0
- package/dist/gate-settlement.js +40 -0
- package/dist/hooks/classic-stations.d.ts +1 -0
- package/dist/hooks/classic-stations.js +1 -0
- package/dist/hooks/command-spawn.d.ts +2 -2
- package/dist/hooks/hook-runner.js +14 -14
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +10 -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/dispatch.d.ts +4 -0
- package/dist/http/dispatch.js +3 -0
- package/dist/http/durable-run-leg.js +12 -6
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/memory-compliance-failure-reply.js +3 -0
- package/dist/http/resume-legs.d.ts +5 -11
- package/dist/http/resume-legs.js +179 -104
- package/dist/http/route-ctx.d.ts +44 -59
- package/dist/http/route-ctx.js +2 -5
- package/dist/http/routes/a2a-serve.js +10 -4
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/agents-roster.js +5 -1
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/capabilities.js +12 -10
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/mcp-reconnect.d.ts +3 -2
- package/dist/http/routes/mcp-reconnect.js +7 -5
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +3 -3
- package/dist/http/routes/runs.js +11 -7
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +119 -13
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +6 -8
- package/dist/http/routes/tasks.js +800 -782
- package/dist/http/server.d.ts +61 -39
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +22 -18
- package/dist/identity-source.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/leader/wire.d.ts +7 -0
- package/dist/leader/wire.js +5 -4
- package/dist/mcp-assertion-header.d.ts +51 -40
- package/dist/mcp-assertion-header.js +10 -11
- package/dist/mcp-authorization-face.d.ts +27 -0
- package/dist/mcp-authorization-face.js +15 -0
- package/dist/memory-operator-faces.d.ts +13 -4
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +187 -92
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/model-route-gap.d.ts +6 -22
- package/dist/model-route-gap.js +7 -5
- package/dist/mods-declaration.d.ts +110 -0
- package/dist/mods-declaration.js +109 -0
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/err-text.d.ts +5 -0
- package/dist/observability/err-text.js +9 -0
- package/dist/observability/fail-open.d.ts +17 -1
- package/dist/observability/fail-open.js +17 -1
- package/dist/observability/run-terminal.js +0 -1
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -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 +49 -11
- package/dist/parked-decide.js +25 -26
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +156 -0
- package/dist/peer-seat.js +273 -0
- package/dist/permission-rule-vocab.d.ts +7 -7
- package/dist/permission-rule-vocab.js +3 -3
- package/dist/plain-string-record.d.ts +38 -0
- package/dist/plain-string-record.js +38 -0
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/background-agent-store-sql.js +6 -3
- package/dist/plugins/caching-session-store.d.ts +11 -2
- package/dist/plugins/caching-session-store.js +15 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +21 -25
- package/dist/plugins/checkpoint-store-sql.js +13 -15
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +8 -7
- 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/k8s-pod-reaper.d.ts +2 -8
- package/dist/plugins/k8s-pod-reaper.js +3 -3
- package/dist/plugins/local-checkpoint-store.d.ts +1 -11
- package/dist/plugins/local-checkpoint-store.js +0 -1
- package/dist/plugins/local-session-store.d.ts +183 -45
- package/dist/plugins/local-session-store.js +412 -105
- package/dist/plugins/mailbox-store-sql.d.ts +11 -3
- package/dist/plugins/mailbox-store-sql.js +103 -15
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-sync-store-pg.d.ts +16 -5
- package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
- package/dist/plugins/one-time-migrations.d.ts +117 -6
- package/dist/plugins/one-time-migrations.js +383 -14
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +34 -29
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +41 -22
- package/dist/plugins/pg-session-storage.js +136 -55
- package/dist/plugins/remote-env-adb.d.ts +22 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +32 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +19 -3
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +23 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +13 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +30 -5
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +18 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +23 -8
- package/dist/plugins/retention-store-sql.js +13 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +63 -12
- package/dist/plugins/session-fork.js +38 -21
- package/dist/plugins/session-ownership.d.ts +7 -0
- package/dist/plugins/session-ownership.js +5 -0
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.d.ts +17 -0
- package/dist/plugins/session-workspace-cell.js +38 -0
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +26 -14
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-store.d.ts +74 -28
- package/dist/plugins/tidb-session-store.js +132 -53
- package/dist/plugins/tool-result-store-sql.d.ts +16 -8
- package/dist/plugins/tool-result-store-sql.js +26 -17
- package/dist/plugins/worktree-isolation.d.ts +3 -2
- package/dist/plugins/worktree-isolation.js +1 -7
- package/dist/project-identity.d.ts +1 -14
- package/dist/project-identity.js +0 -4
- 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/rules-consent.js +6 -4
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +36 -26
- package/dist/runs.d.ts +9 -2
- package/dist/runs.js +9 -2
- package/dist/sandbox-git-plane.d.ts +2 -2
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +15 -8
- package/dist/server-secret-env.js +4 -2
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -10
- package/dist/session-incarnation.js +5 -3
- 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/session-workspace.d.ts +130 -0
- package/dist/session-workspace.js +83 -0
- package/dist/spec-fields.d.ts +1 -1
- package/dist/standing-answer-grant.d.ts +2 -13
- package/dist/standing-answer-grant.js +0 -7
- package/dist/task-a2a.d.ts +44 -14
- package/dist/task-a2a.js +46 -20
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +33 -20
- package/dist/task-mcp.js +46 -22
- package/dist/tool-approval.d.ts +38 -8
- package/dist/tool-approval.js +57 -13
- package/dist/trace/core-keyset-guard.d.ts +33 -18
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.d.ts +20 -1
- package/dist/trace/exit-seat.js +3 -0
- package/dist/trace/extension-call-face.d.ts +33 -0
- package/dist/trace/extension-call-face.js +39 -0
- package/dist/trace/project.d.ts +26 -7
- package/dist/trace/project.js +30 -3
- package/dist/trace/redact.js +42 -13
- package/dist/trace/self-minted-wire.d.ts +240 -12
- package/dist/trace/self-minted-wire.js +100 -4
- package/dist/trace/wire-projection-faces.d.ts +6 -6
- package/dist/trace/wire-projection-faces.js +6 -5
- package/dist/url-refusal.d.ts +1 -1
- package/dist/user-authored-text.d.ts +2 -2
- package/dist/user-authored-text.js +16 -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/center-request.d.ts +3 -2
- package/dist/web-admin-client/center-request.js +2 -3
- package/dist/web-admin-client/skills-mcp.d.ts +8 -4
- package/dist/web-admin-client/skills-mcp.js +32 -18
- package/dist/web-admin-client/types.d.ts +11 -0
- package/package.json +3 -3
- package/wire-changes/server@7.138.0.json +446 -0
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
- package/dist/http/mcp-remote-text.d.ts +0 -5
- package/dist/http/mcp-remote-text.js +0 -7
|
@@ -1,13 +1,12 @@
|
|
|
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";
|
|
12
11
|
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
13
12
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
@@ -21,7 +20,7 @@ const unsupported = (op) => ({
|
|
|
21
20
|
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
21
|
});
|
|
23
22
|
export class RemoteLocalDockerExecutionEnv {
|
|
24
|
-
capabilities = { isolation: true, suspendable: false, canonicalPathAuthoritative: true };
|
|
23
|
+
capabilities = { isolation: true, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
|
|
25
24
|
cwd;
|
|
26
25
|
get homeDir() {
|
|
27
26
|
return this.homeSeat.home;
|
|
@@ -383,71 +382,48 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
383
382
|
lines = lines.slice(0, options.maxLines);
|
|
384
383
|
return ok(lines);
|
|
385
384
|
}
|
|
385
|
+
stagedLane(content) {
|
|
386
|
+
return { upload: (dest, signal) => this.pipeIn(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
|
|
387
|
+
}
|
|
386
388
|
async writeFile(p, content, abortSignal) {
|
|
387
389
|
const ready = await this.fsReady(p);
|
|
388
390
|
if (!ready.ok)
|
|
389
391
|
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
|
-
}
|
|
392
|
+
return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
|
|
417
393
|
}
|
|
418
394
|
async appendFile(p, content, abortSignal) {
|
|
395
|
+
const ready = await this.fsReady(p);
|
|
396
|
+
if (!ready.ok)
|
|
397
|
+
return ready;
|
|
419
398
|
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);
|
|
399
|
+
return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.pipeIn(content, abs, signal, ">>")));
|
|
400
|
+
}
|
|
401
|
+
async pipeIn(content, dest, abortSignal, redirect = ">") {
|
|
402
|
+
const r = await this.fsExec(`cat ${redirect} ${shellQuote(dest)}`, abortSignal, typeof content === "string" ? Buffer.from(content, "utf8") : content);
|
|
403
|
+
if (!r.ok)
|
|
404
|
+
return { ok: false, error: fileErrorFromExec(r.error, dest) };
|
|
405
|
+
if (r.value.exitCode !== 0)
|
|
406
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr, dest) };
|
|
407
|
+
return ok(undefined);
|
|
433
408
|
}
|
|
434
409
|
posixFsInst;
|
|
435
410
|
get posixFs() {
|
|
436
411
|
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
|
-
},
|
|
412
|
+
run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
|
|
447
413
|
resolve: (p) => this.resolve(p),
|
|
448
414
|
tempDir: () => this.cwd,
|
|
449
415
|
}));
|
|
450
416
|
}
|
|
417
|
+
async fsExec(cmd, abortSignal, input) {
|
|
418
|
+
if (this.destroyed)
|
|
419
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
420
|
+
if (!this.containerId) {
|
|
421
|
+
const c = await this.connect();
|
|
422
|
+
if (!c.ok)
|
|
423
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`) };
|
|
424
|
+
}
|
|
425
|
+
return this.docker(this.execArgv(cmd, undefined, undefined, input !== undefined), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
|
|
426
|
+
}
|
|
451
427
|
async fileInfo(p, abortSignal) {
|
|
452
428
|
return this.posixFs.statFileInfo(p, abortSignal);
|
|
453
429
|
}
|
|
@@ -484,8 +460,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
484
460
|
withHostFlag(args) {
|
|
485
461
|
return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
|
|
486
462
|
}
|
|
487
|
-
execArgv(command, cwd, env) {
|
|
488
|
-
const args = ["exec"];
|
|
463
|
+
execArgv(command, cwd, env, stdin = false) {
|
|
464
|
+
const args = stdin ? ["exec", "-i"] : ["exec"];
|
|
489
465
|
const dir = cwd != null ? this.resolve(cwd) : this.cwd;
|
|
490
466
|
args.push("-w", dir);
|
|
491
467
|
const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
|
|
@@ -501,7 +477,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
501
477
|
return new Promise((resolve) => {
|
|
502
478
|
let settled = false;
|
|
503
479
|
let forceSettleTimer;
|
|
504
|
-
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
|
|
480
|
+
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: [opts?.input !== undefined ? "pipe" : "ignore", "pipe", "pipe"], env: this.cliEnv() });
|
|
505
481
|
const killGrace = () => {
|
|
506
482
|
try {
|
|
507
483
|
child.kill("SIGKILL");
|
|
@@ -524,11 +500,11 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
524
500
|
const timeoutMs = opts?.timeoutMs ?? this.cfg.controlTimeoutMs;
|
|
525
501
|
const timer = setTimeout(() => {
|
|
526
502
|
killGrace();
|
|
527
|
-
finish({ ok: false, error:
|
|
503
|
+
finish({ ok: false, error: createCutError({ code: "timeout", message: `docker ${args[0]} exceeded ${timeoutMs}ms` }, buildCapturedOutput(outTail, errBuf)) });
|
|
528
504
|
}, timeoutMs);
|
|
529
505
|
const onAbort = () => {
|
|
530
506
|
killGrace();
|
|
531
|
-
finish({ ok: false, error:
|
|
507
|
+
finish({ ok: false, error: createCutError({ code: "aborted", message: "docker command aborted" }, buildCapturedOutput(outTail, errBuf)) });
|
|
532
508
|
};
|
|
533
509
|
if (opts?.signal) {
|
|
534
510
|
if (opts.signal.aborted)
|
|
@@ -541,10 +517,21 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
541
517
|
outTail.push(d); opts?.onStdout?.(d.toString()); });
|
|
542
518
|
child.stderr?.on("data", (d) => { errBuf.push(d); opts?.onStderr?.(d.toString()); });
|
|
543
519
|
child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `docker spawn failed (is '${this.cfg.dockerPath}' installed?): ${e.message}`, e) }));
|
|
520
|
+
let stdinFailure;
|
|
521
|
+
if (opts?.input !== undefined) {
|
|
522
|
+
if (!child.stdin) {
|
|
523
|
+
killGrace();
|
|
524
|
+
return finish({ ok: false, error: new ExecutionError("spawn_error", `docker ${args[0]}: no stdin pipe for the payload`) });
|
|
525
|
+
}
|
|
526
|
+
child.stdin.on("error", (e) => { stdinFailure = e.message; });
|
|
527
|
+
child.stdin.end(Buffer.from(opts.input.buffer, opts.input.byteOffset, opts.input.byteLength));
|
|
528
|
+
}
|
|
544
529
|
child.on("close", (code, signal) => {
|
|
545
530
|
if (forceSettleTimer)
|
|
546
531
|
clearTimeout(forceSettleTimer);
|
|
547
532
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 1);
|
|
533
|
+
if (exitCode === 0 && stdinFailure !== undefined)
|
|
534
|
+
return finish({ ok: false, error: new ExecutionError("unknown", `docker ${args[0]}: the payload could not be written to stdin (${stdinFailure})`) });
|
|
548
535
|
const err = errBuf.result();
|
|
549
536
|
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
550
537
|
if (binary) {
|
|
@@ -59,14 +59,21 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
59
59
|
* (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
|
|
60
60
|
* 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
|
|
61
61
|
* 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
|
|
62
|
-
* 布尔字面量零影响(
|
|
62
|
+
* 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
|
|
63
63
|
* core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
|
|
64
64
|
* 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
|
|
65
65
|
*/
|
|
66
|
+
/** `sharesProcessTable`(core 7.57.0,必填):命令跑在 SSH 目标机上;目标恰是本机(或本进程所在容器的宿主)时命令与本进程同表,这一形今天没有配置面可声明 `true`。 */
|
|
67
|
+
/** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
|
|
66
68
|
readonly capabilities: {
|
|
67
|
-
isolation:
|
|
68
|
-
suspendable:
|
|
69
|
-
canonicalPathAuthoritative:
|
|
69
|
+
isolation: false;
|
|
70
|
+
suspendable: false;
|
|
71
|
+
canonicalPathAuthoritative: true;
|
|
72
|
+
sharesProcessTable: false;
|
|
73
|
+
hostLocalPaths: false;
|
|
74
|
+
externalContentTarget: false;
|
|
75
|
+
io: true;
|
|
76
|
+
keepsReadFloor: false;
|
|
70
77
|
};
|
|
71
78
|
/** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at mountPath. */
|
|
72
79
|
cwd: string;
|
|
@@ -184,7 +191,14 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
184
191
|
abortSignal?: AbortSignal;
|
|
185
192
|
}): Promise<Result<string[], FileError>>;
|
|
186
193
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
194
|
+
/** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
|
|
195
|
+
* 上传 = sftp 整份写到私有目录里的暂存件;提交 = 文件面探针 exec。两者同一身份(sftp 子系统与 exec 都是这条连接的登录用户)。
|
|
196
|
+
* sftp 写不收取消信号 ⇒ 取消在传输结束之后、提交之前生效(目标未碰)。 */
|
|
197
|
+
private stagedLane;
|
|
198
|
+
/** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `sftp.writeFile(abs)` 就地 O_TRUNC:传输中途断 ⇒ 目标 = 半份新件。 */
|
|
187
199
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
200
|
+
/** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份就地重写(O(文件);中途断 ⇒ 半份)。
|
|
201
|
+
* 目录不收新名字 / 私有目录收不成私有时的退路 = sftp 直接追加到目标。 */
|
|
188
202
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
189
203
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
190
204
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
@@ -1,9 +1,10 @@
|
|
|
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";
|
|
9
10
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
@@ -18,7 +19,7 @@ function exitCodeForSignalKill(signal) {
|
|
|
18
19
|
return 128 + (signalNumber(`SIG${signal}`) ?? 9);
|
|
19
20
|
}
|
|
20
21
|
export class RemoteSshExecutionEnv {
|
|
21
|
-
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true };
|
|
22
|
+
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
|
|
22
23
|
cwd;
|
|
23
24
|
conn;
|
|
24
25
|
sftpPromise;
|
|
@@ -363,12 +364,29 @@ export class RemoteSshExecutionEnv {
|
|
|
363
364
|
return new Promise((resolve) => {
|
|
364
365
|
const outBuf = new RollingTailBuffer();
|
|
365
366
|
const errBuf = new RollingTailBuffer();
|
|
367
|
+
const recordStderr = (bytes) => {
|
|
368
|
+
if (bytes.length > 0)
|
|
369
|
+
errBuf.push(bytes);
|
|
370
|
+
};
|
|
371
|
+
const emitStderr = (bytes) => {
|
|
372
|
+
if (bytes.length > 0)
|
|
373
|
+
options?.onStderr?.(bytes.toString());
|
|
374
|
+
};
|
|
375
|
+
const takeStderr = (bytes) => {
|
|
376
|
+
recordStderr(bytes);
|
|
377
|
+
emitStderr(bytes);
|
|
378
|
+
};
|
|
379
|
+
const captured = () => {
|
|
380
|
+
recordStderr(call.endStderr());
|
|
381
|
+
return buildCapturedOutput(outBuf, errBuf);
|
|
382
|
+
};
|
|
366
383
|
let settled = false;
|
|
367
384
|
let streamRef;
|
|
368
|
-
const finish = (
|
|
385
|
+
const finish = (outcome) => {
|
|
369
386
|
if (settled)
|
|
370
|
-
return;
|
|
387
|
+
return false;
|
|
371
388
|
settled = true;
|
|
389
|
+
const r = outcome();
|
|
372
390
|
clearTimeout(timer);
|
|
373
391
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
374
392
|
conn.removeListener("close", onConnClose);
|
|
@@ -378,10 +396,14 @@ export class RemoteSshExecutionEnv {
|
|
|
378
396
|
catch {
|
|
379
397
|
}
|
|
380
398
|
resolve(r);
|
|
399
|
+
return true;
|
|
400
|
+
};
|
|
401
|
+
const cut = (c) => {
|
|
402
|
+
finish(() => ({ ok: false, error: createCutError(c, captured()) }));
|
|
381
403
|
};
|
|
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") });
|
|
404
|
+
const timer = setTimeout(() => cut({ code: "timeout", message: `ssh exec exceeded ${timeoutMs}ms (provider/command hang)` }), timeoutMs);
|
|
405
|
+
const onAbort = () => cut({ code: "aborted", message: "ssh exec aborted" });
|
|
406
|
+
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
407
|
conn.once("close", onConnClose);
|
|
386
408
|
if (options?.abortSignal) {
|
|
387
409
|
if (options.abortSignal.aborted)
|
|
@@ -390,7 +412,7 @@ export class RemoteSshExecutionEnv {
|
|
|
390
412
|
}
|
|
391
413
|
conn.exec(call.command, (err, stream) => {
|
|
392
414
|
if (err)
|
|
393
|
-
return finish({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) });
|
|
415
|
+
return finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) }));
|
|
394
416
|
streamRef = stream;
|
|
395
417
|
if (settled) {
|
|
396
418
|
try {
|
|
@@ -407,25 +429,20 @@ export class RemoteSshExecutionEnv {
|
|
|
407
429
|
})
|
|
408
430
|
.on("close", (code, signal) => {
|
|
409
431
|
if (code == null && !signal) {
|
|
410
|
-
return finish({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") });
|
|
432
|
+
return finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") }));
|
|
411
433
|
}
|
|
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
434
|
const exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 1);
|
|
420
|
-
|
|
435
|
+
const held = call.endStderr();
|
|
436
|
+
const won = finish(() => {
|
|
437
|
+
recordStderr(held);
|
|
438
|
+
const out = outBuf.result();
|
|
439
|
+
const err = errBuf.result();
|
|
440
|
+
return call.settle(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
|
|
441
|
+
});
|
|
442
|
+
if (won)
|
|
443
|
+
emitStderr(held);
|
|
421
444
|
})
|
|
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
|
-
});
|
|
445
|
+
.stderr.on("data", (d) => takeStderr(call.filterStderr(d)));
|
|
429
446
|
});
|
|
430
447
|
});
|
|
431
448
|
}
|
|
@@ -562,7 +579,7 @@ export class RemoteSshExecutionEnv {
|
|
|
562
579
|
async sftp(abortSignal) {
|
|
563
580
|
const ready = await this.ensureConnected(abortSignal);
|
|
564
581
|
if (!ready.ok)
|
|
565
|
-
return Promise.reject(ready.error.code === "aborted" ?
|
|
582
|
+
return Promise.reject(ready.error.code === "aborted" ? fileErrorFromExec(ready.error) : new Error(ready.error.message));
|
|
566
583
|
if (!this.conn)
|
|
567
584
|
return Promise.reject(new Error("SSH not connected"));
|
|
568
585
|
if (!this.sftpPromise) {
|
|
@@ -604,41 +621,37 @@ export class RemoteSshExecutionEnv {
|
|
|
604
621
|
return { ok: false, error: this.fileError(e, abs) };
|
|
605
622
|
}
|
|
606
623
|
}
|
|
624
|
+
stagedLane(data) {
|
|
625
|
+
return {
|
|
626
|
+
upload: async (dest, signal) => {
|
|
627
|
+
try {
|
|
628
|
+
const sftp = await this.sftp(signal);
|
|
629
|
+
await new Promise((resolve, reject) => sftp.writeFile(dest, data, (err) => (err ? reject(err) : resolve())));
|
|
630
|
+
return ok(undefined);
|
|
631
|
+
}
|
|
632
|
+
catch (e) {
|
|
633
|
+
return { ok: false, error: this.fileError(e, dest) };
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
run: (cmd, signal) => this.fsExec(cmd, signal),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
607
639
|
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
|
-
}
|
|
640
|
+
return writeFileStaged(this.stagedLane(bytesOf(content)), this.resolve(p), abortSignal);
|
|
626
641
|
}
|
|
627
642
|
async appendFile(p, content, abortSignal) {
|
|
628
643
|
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);
|
|
644
|
+
const data = bytesOf(content);
|
|
645
|
+
return appendFileStaged(this.stagedLane(data), abs, abortSignal, (ctx) => ctx.inPlace(async (signal) => {
|
|
646
|
+
try {
|
|
647
|
+
const sftp = await this.sftp(signal);
|
|
648
|
+
await new Promise((resolve, reject) => sftp.appendFile(abs, data, (err) => (err ? reject(err) : resolve())));
|
|
649
|
+
return ok(undefined);
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
return { ok: false, error: this.fileError(e, abs) };
|
|
653
|
+
}
|
|
654
|
+
}));
|
|
642
655
|
}
|
|
643
656
|
async fileInfo(p, abortSignal) {
|
|
644
657
|
const abs = this.resolve(p);
|
|
@@ -734,4 +747,7 @@ export class RemoteSshExecutionEnv {
|
|
|
734
747
|
export function sshExecutionEnvFactory(config) {
|
|
735
748
|
return (_ctx) => new RemoteSshExecutionEnv(config);
|
|
736
749
|
}
|
|
750
|
+
function bytesOf(content) {
|
|
751
|
+
return typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
752
|
+
}
|
|
737
753
|
//# sourceMappingURL=remote-env-ssh.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared shell/path helpers for the
|
|
3
|
-
* security-sensitive quoting can't drift between adapters (council design/#4, #5).
|
|
2
|
+
* Shared shell/path helpers for the remote `RemoteExecutionEnv` adapters (ssh / adb / k8s / local-docker, and e2b where it
|
|
3
|
+
* shells out). Kept in ONE place so the security-sensitive quoting can't drift between adapters (council design/#4, #5).
|
|
4
4
|
*/
|
|
5
|
-
import { ExecutionError,
|
|
5
|
+
import { ExecutionError, FileError, type FileInfo, type RemoteExecutionError, type Result, type SymlinkChain } from "@sema-agent/core";
|
|
6
6
|
import type { ChildProcess } from "node:child_process";
|
|
7
7
|
/**
|
|
8
8
|
* D5 pipe-destroy grace (parity with core's vendored `EXEC_FORCE_SETTLE_GRACE_MS`, harness/env/nodejs.js):
|
|
@@ -204,4 +204,101 @@ export type ShellChainRun = (cmd: string) => Promise<Result<{
|
|
|
204
204
|
* 三条远端腿的文件面探针不带 cwd(车PL 验修 ③)⇒ 这条命令不会因为某个工作目录不可用而不跑。
|
|
205
205
|
*/
|
|
206
206
|
export declare function runSymlinkChain(absPath: string, run: ShellChainRun): Promise<Result<SymlinkChain, FileError>>;
|
|
207
|
+
/**
|
|
208
|
+
* 脚本删一只私有目录没删掉时写到 **stdout** 的那一条记录的头:`<头> <目录路径>\0`(NUL 收尾 —— 路径里可以有换行)。
|
|
209
|
+
* 残留记录走 stdout、错误走 stderr:两条通道不混,分类器读到的 stderr 里没有路径文本。记录不改脚本的退出码:写照它自己的结局答,残留另行留痕。
|
|
210
|
+
*/
|
|
211
|
+
export declare const STAGE_DIR_LEFT_MARK = "sema: staging directory left behind:";
|
|
212
|
+
/** 一次远端写的暂存位置(纯数据):`dir` = 目标目录下的私有目录,`file` = 其中那只暂存文件。 */
|
|
213
|
+
export interface StagingPaths {
|
|
214
|
+
readonly dir: string;
|
|
215
|
+
readonly file: string;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 远端写的暂存位置(纯数据 ⇒ `build*`):私有目录在**目标同目录**(替换的提交是同一文件系统内的 rename),名 = `.<目标名>.stage-<12 位十六进制>`;
|
|
219
|
+
* 目标名已贴着 NAME_MAX 时逐码点截短到放得下 —— 一个目录装不下的暂存名会把一次就地形本来接得住的写拒掉。这只名字铸一次、用到底:
|
|
220
|
+
* 提交脚本跟一跳链接到别的目录里重新暂存时,用的仍是这只名字(已在上限之内,放到哪个目录都装得下)。`nonce` 只给测试钉序用。
|
|
221
|
+
*/
|
|
222
|
+
export declare function buildStagingPaths(abs: string, nonce?: string): StagingPaths;
|
|
223
|
+
/**
|
|
224
|
+
* 准备脚本(纯数据 ⇒ `build*`):建目标的父目录(`mkdir -p`,失败 ⇒ 退出 1),再建私有目录({@link privateMkdir};失败 ⇒ 退出 {@link STAGE_DIR_REFUSED_EXIT}),
|
|
225
|
+
* 再收紧({@link hardenPrivate};不成 ⇒ 放弃这只目录({@link abandonPrivate})、写 {@link STAGE_DIR_UNSEALED_MESSAGE}、退出 {@link STAGE_DIR_UNSEALED_EXIT} —— 编排退到直接写目标),
|
|
226
|
+
* 再核里面没有 `data`({@link emptyPrivate};不成 ⇒ 放弃这只目录、写明原因、退出 1 —— 响亮答错,不退;原因句不带路径:stderr 只给分类器读)。
|
|
227
|
+
* 非零退出之后目标机上没有这次写的私有目录(删不掉的那一只由 {@link STAGE_DIR_LEFT_MARK} 记录点名)。**退出 0 ⇔ 目录是读回核实过的私有的、且是空的。**
|
|
228
|
+
*/
|
|
229
|
+
export declare function buildStagingPrepareCommand(stage: StagingPaths): string;
|
|
230
|
+
/**
|
|
231
|
+
* 整文件替换的提交脚本(POSIX sh,纯数据 ⇒ `build*`)。一次往返,**整条命令只有一个退出状态**(`{ …; }`,理由同
|
|
232
|
+
* {@link buildSymlinkChainCommand} 头注)。`d` = 私有目录,`s` = 其中的暂存件,`t` = 目标。
|
|
233
|
+
*
|
|
234
|
+
* 1. 叶是符号链接 ⇒ 读**一跳**(`readlink`,不带 `-f`;相对目标锚在链接自己的目录,不折 `..`,与链视图同一词法;
|
|
235
|
+
* 链接文本的结尾换行原样保留 —— `-n` 不加换行,`$(…)` 会吞掉结尾换行,所以输出后垫一个 `.` 再剥掉;`-n` 不被认 ⇒ 读不出一跳 ⇒ 第 3 步经链接就地写):那一跳是常规文件或还不存在
|
|
236
|
+
* ⇒ 提交落到那一跳上(链接保持是链接);否则(链 / 非常规)⇒ 走第 3 步就地写。
|
|
237
|
+
* 2. 目标是常规文件或不存在 ⇒ 私有目录不在目标的目录里(跟了一跳)⇒ 先在目标目录里用**同一只私有目录名**、同样生来 0700、同样收紧过地**重新暂存**一份
|
|
238
|
+
* (目标机本地拷贝,不过网络;拷成了就改用新的那只、删原私有目录;拷不成就删掉新建的那只、收紧不成就放弃它,用原暂存件 ——
|
|
239
|
+
* 删不掉的都写一条 {@link STAGE_DIR_LEFT_MARK} 记录),
|
|
240
|
+
* 让 rename 不跨文件系统(与 core 把暂存放在最终目标旁边同形);
|
|
241
|
+
* 已有文件的权限位带到暂存件上(`stat -c %a`,BSD 退到 `stat -f %Lp`;带不过去不拦)—— **在 `mv` 之前**,换上去的那一刻权限位已是目标的;
|
|
242
|
+
* `mv -f` 换上去,成功 ⇒ 删私有目录(删不掉 ⇒ 一条 {@link STAGE_DIR_LEFT_MARK} 记录,仍 `exit 0`:目标已是新件)。
|
|
243
|
+
* 3. 其余(链接链 / FIFO / 设备 / 目录 / `mv` 被拒,如单文件绑定挂载的 EBUSY)⇒ 已落定的暂存件就地拷进目标(与 core 的回落同形:
|
|
244
|
+
* 非原子,但不再经过网络传输),见 {@link takeAndWrite}:先把暂存件**打开**(读不了 ⇒ 先补上属主读位,仍读不了 ⇒ 非零退出、目标未碰),
|
|
245
|
+
* 再**删掉暂存名**(删不掉 ⇒ 非零退出、目标未碰,残留目录由脚本点名),然后才从已打开的那份拷进目标,最后关 fd、删私有目录(删不掉只记录)。于是
|
|
246
|
+
* 「目标被动过」⇒「暂存名已不在」,残留至多是一只空目录(或文件系统为打开着的文件留的隐藏项)。退出码 = 拷入那一步的退出码
|
|
247
|
+
* (目录 ⇒ `Is a directory` 经 {@link classifyFsStderr} 归 `is_directory`)。
|
|
248
|
+
*/
|
|
249
|
+
export declare function buildStagedCommitCommand(stage: StagingPaths, target: string): string;
|
|
250
|
+
/**
|
|
251
|
+
* 追加的提交脚本(纯数据 ⇒ `build*`):取走暂存件并写目标({@link takeAndWrite},次序同 {@link buildStagedCommitCommand} 第 3 步:先开、再删暂存名、后写、再关 fd 删私有目录),
|
|
252
|
+
* 一次 `cat >>` 加到目标尾(跟随链接、不换 inode、不碰已有字节)。暂存名删不掉 ⇒ 目标一个字节没加。
|
|
253
|
+
*/
|
|
254
|
+
export declare function buildStagedAppendCommand(stage: StagingPaths, target: string): string;
|
|
255
|
+
/** 清掉一次写的私有目录({@link removalOf}:只删 `data` 与这只目录;不在 ⇒ 什么都不做;退出 0 ⇔ 不在了)。纯数据 ⇒ `build*`。 */
|
|
256
|
+
export declare function buildStagingDiscardCommand(stage: StagingPaths): string;
|
|
257
|
+
/**
|
|
258
|
+
* 一次暂存写交给 {@link writeFileStaged} / {@link appendFileStaged} 的两只面 —— 各腿的传输留在腿上,规则在这里。
|
|
259
|
+
* 🔴 **`upload` 与 `run` 必须是同一个身份、在同一台机器上**:私有目录由 `run` 建(0700,属写入方),`upload` 要进得去;提交由 `run` 做,
|
|
260
|
+
* 要拿得走、清得掉 `upload` 落下的东西。一条腿的传输若以别的身份落盘(例如容器守护进程以 root 拷入、而命令以镜像用户跑),那条腿必须改走
|
|
261
|
+
* 与 `run` 同一身份的传输,不能让编排迁就它。
|
|
262
|
+
*/
|
|
263
|
+
export interface StagedWriteLane {
|
|
264
|
+
/** 把**整份**字节放到 `dest`(新建;`dest` 的目录已在且是写入方的私有目录)—— 腿自己的传输(sftp / adb push / 容器内 `cat` 收 stdin / 分块 exec / provider 文件 API)。 */
|
|
265
|
+
upload(dest: string, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
266
|
+
/** 目标机上的一次 shell 往返(腿的文件面探针 exec:只用绝对路径,不进工作目录)。`abortSignal` 只有准备那一步带(提交一旦发出不再听取消)。 */
|
|
267
|
+
run(cmd: string, abortSignal?: AbortSignal): ReturnType<ShellChainRun>;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 退路拿到的两样(编排为每一次退路造一只,见 {@link createDirectContext}):`signal` = 这次写的取消信号;`inPlace(step)` = 把**写目标的那一步**包起来
|
|
271
|
+
* (连同那一步自己的通道准备:ssh 在步内拿 sftp 通道)。取消的读法只看那一步有没有被包住:包住的那一步**答了** `aborted` ⇒ 改写成 `unknown`(可能已有字节到了目标,
|
|
272
|
+
* 保守;通道准备里被取消也算在内);那一步开始之前取消已发生 ⇒ `aborted`、那一步不跑;那一步不收取消、照样写完(ssh 的 sftp)⇒ 结果照实(成功);
|
|
273
|
+
* 没包的步骤(读旧)答的 `aborted` 原样透出(目标未碰)。编排不猜退路走到了哪儿。
|
|
274
|
+
*/
|
|
275
|
+
export interface StagedDirectContext {
|
|
276
|
+
readonly signal: AbortSignal | undefined;
|
|
277
|
+
inPlace<T>(step: (abortSignal?: AbortSignal) => Promise<Result<T, FileError>>): Promise<Result<T, FileError>>;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 不暂存时的退路 —— 目录不收新名字(建私有目录被 `permission_denied` 拒)或私有目录收不成私有:腿直接对目标做的那一步(core 同一条回落:文件本身可能可写)。
|
|
281
|
+
* 两只文件面、五条腿**必传**:写面 = 腿的上传直落目标;追加面 = 能追加写的腿(ssh / local-docker / k8s)直接追加到目标,只能把整份文件落到一个路径上的腿
|
|
282
|
+
* (e2b 的 `files.write`、adb 的 push)读旧 + 拼 + 整份写目标(adb 的这一形就是 7.138.x 的追加)。字节流向目标的那一步必须经 `ctx.inPlace`(读旧不包)。
|
|
283
|
+
*/
|
|
284
|
+
export type StagedWriteDirect = (ctx: StagedDirectContext) => Promise<Result<void, FileError>>;
|
|
285
|
+
/**
|
|
286
|
+
* 只能把整份文件落到一个路径上的腿(e2b 的 `files.write`、adb 的 push)在退路里的追加 —— 两腿共用这一只,各自只给读与写:
|
|
287
|
+
* 读旧(`not_found` ⇒ 按空;别的读错原样透出、不写)+ 按字节拼(二进制安全)+ 整份写目标。读旧**不**经 `ctx.inPlace`(取消落在这里 ⇒ `aborted`、目标未碰),
|
|
288
|
+
* 整份写经 `ctx.inPlace`(见 {@link StagedDirectContext})。adb 的这一形就是 7.138.x 的追加。
|
|
289
|
+
*/
|
|
290
|
+
export declare function rewriteAppend(read: (abortSignal?: AbortSignal) => Promise<Result<Uint8Array, FileError>>, write: (bytes: Uint8Array, abortSignal?: AbortSignal) => Promise<Result<void, FileError>>, add: string | Uint8Array, ctx: StagedDirectContext): Promise<Result<void, FileError>>;
|
|
291
|
+
/**
|
|
292
|
+
* 远端整文件替换(ssh / adb / local-docker / k8s / e2b 五条腿调同一只;父目录由准备那一步建)。契约:成功 ⇒ 目标整份是新内容;传输失败 ⇒ 目标整份是旧内容;
|
|
293
|
+
* `aborted` ⇒ 目标未碰。私有目录清不掉 ⇒ 错误消息点名残留的目录。目录不收新名字、或私有目录收不成私有 ⇒ 不暂存,腿的上传直接落目标一次(旧形,core 同一条回落;非原子)。
|
|
294
|
+
* 提交往返本身断了(传输层错误)⇒ 答 `unknown`,消息写明提交跑没跑不可知。
|
|
295
|
+
*/
|
|
296
|
+
export declare function writeFileStaged(lane: StagedWriteLane, abs: string, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
297
|
+
/**
|
|
298
|
+
* 远端追加(五条腿调同一只;`lane.upload` 上传的是**要加的那几个字节**)。暂存那条路的契约:成功 ⇒ 目标 = 旧内容 + 新字节;传输失败 / `aborted` ⇒
|
|
299
|
+
* 目标未碰;已有字节从不被重写、inode 不换。`direct` = 目录不收新名字 / 私有目录收不成私有时腿直接写目标的那一步(必传,见 {@link StagedWriteDirect}),
|
|
300
|
+
* 不在上面的契约里:非原子 —— 中途断 ⇒ 目标可能只加上了一部分(整份写目标的 e2b / adb:已有字节被重写,目标可能没了或只剩半份);取消落在**写目标那一步开始之前**
|
|
301
|
+
* (读旧)⇒ `aborted`、目标未碰;写目标那一步因取消中断 ⇒ `unknown`、目标可能只写了一半,那一步自己的通道准备算在步内(保守,见头注与 {@link StagedDirectContext})。
|
|
302
|
+
*/
|
|
303
|
+
export declare function appendFileStaged(lane: StagedWriteLane, abs: string, abortSignal: AbortSignal | undefined, direct: StagedWriteDirect): Promise<Result<void, FileError>>;
|
|
207
304
|
//# sourceMappingURL=remote-shell.d.ts.map
|