@sema-agent/server 7.150.0 → 7.152.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +451 -0
- package/README.md +15 -3
- package/README.zh-CN.md +10 -2
- package/USAGE.md +144 -87
- package/deploy/sema-up/README.md +11 -4
- package/deploy/sema-up/chart/Chart.yaml +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
- package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
- package/deploy/sema-up/chart/templates/server.yaml +5 -3
- package/deploy/sema-up/chart/values-full-single.yaml +3 -1
- package/deploy/sema-up/chart/values.yaml +8 -1
- package/deploy/sema-up/cluster-up.sh +7 -6
- package/deploy/sema-up/kube-up.sh +6 -6
- package/deploy/sema-up/lib/flagshape.sh +36 -8
- package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
- package/deploy/sema-up/lib/helm-values-kube.py +2 -0
- package/deploy/sema-up/machines.example.yaml +3 -1
- package/deploy/sema-up/sema-up.sh +22 -16
- package/dist/adoption/import.js +3 -3
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-ask-machine.d.ts +4 -10
- package/dist/approval-ask-retention.d.ts +1 -1
- package/dist/approval-card.d.ts +12 -14
- package/dist/approval-gate-currency.d.ts +9 -9
- package/dist/approval-gate-currency.js +3 -4
- package/dist/approval-hmac.d.ts +17 -1
- package/dist/approval-hmac.js +7 -1
- package/dist/approval-queue-kind.d.ts +1 -1
- package/dist/approval.d.ts +2 -2
- package/dist/approval.js +2 -2
- package/dist/auth-bridge.d.ts +20 -0
- package/dist/auth-bridge.js +9 -0
- package/dist/background-session-reap.d.ts +1 -1
- package/dist/bake-runner/main.d.ts +4 -2
- package/dist/bind-host.d.ts +3 -6
- package/dist/bind-host.js +4 -7
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/coordinators.d.ts +1 -1
- package/dist/boot/coordinators.js +9 -10
- package/dist/boot/device-lane.js +2 -0
- package/dist/boot/engine-lease.d.ts +5 -0
- package/dist/boot/engine-lease.js +1 -0
- package/dist/boot/execution-env.d.ts +9 -14
- package/dist/boot/execution-env.js +85 -22
- package/dist/boot/execution-lane.js +32 -5
- package/dist/boot/late-binds.d.ts +8 -4
- package/dist/boot/late-binds.js +7 -1
- package/dist/boot/leader.d.ts +31 -30
- package/dist/boot/leader.js +38 -28
- package/dist/boot/leg-path-adjudication.d.ts +40 -0
- package/dist/boot/leg-path-adjudication.js +18 -0
- package/dist/boot/memory-consolidation.d.ts +3 -2
- package/dist/boot/memory-consolidation.js +6 -6
- package/dist/boot/parked-revive-gate.js +3 -15
- package/dist/boot/reapers.d.ts +6 -1
- package/dist/boot/reapers.js +13 -2
- package/dist/boot/resolve-spec.d.ts +3 -3
- package/dist/boot/resolve-spec.js +43 -48
- package/dist/boot/runner-deps.d.ts +46 -35
- package/dist/boot/runner-deps.js +94 -91
- package/dist/boot/session-age-out.d.ts +36 -0
- package/dist/boot/session-age-out.js +101 -0
- package/dist/boot/session-faces.d.ts +5 -14
- package/dist/boot/session-faces.js +9 -5
- package/dist/boot/shutdown.d.ts +6 -0
- package/dist/boot/shutdown.js +10 -4
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-02-web-admin-pull.js +4 -5
- package/dist/boot/stage-05-execution-env.d.ts +5 -3
- package/dist/boot/stage-05-execution-env.js +3 -2
- package/dist/boot/stage-06-runners.d.ts +9 -4
- package/dist/boot/stage-06-runners.js +24 -9
- package/dist/boot/stage-07-capability-layer.d.ts +8 -4
- package/dist/boot/stage-07-capability-layer.js +3 -2
- package/dist/boot/stage-08-reapers.d.ts +8 -4
- package/dist/boot/stage-08-reapers.js +4 -2
- package/dist/boot/stage-09-leader.d.ts +12 -5
- package/dist/boot/stage-09-leader.js +14 -5
- package/dist/boot/stage-10-http-server.d.ts +19 -5
- package/dist/boot/stage-10-http-server.js +14 -21
- package/dist/boot/stores.js +7 -4
- package/dist/boot/web-admin.js +5 -8
- package/dist/boot/workflow-orchestration.d.ts +7 -2
- package/dist/boot/workflow-orchestration.js +24 -7
- package/dist/bounded-rotation.d.ts +29 -0
- package/dist/bounded-rotation.js +24 -0
- package/dist/capabilities/approvals-push.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +28 -13
- package/dist/capabilities/scenarios.js +2 -2
- package/dist/capability-facts.d.ts +43 -32
- package/dist/capability-facts.js +45 -51
- package/dist/card-rule-offers.d.ts +32 -0
- package/dist/card-rule-offers.js +130 -0
- package/dist/center-credential.d.ts +25 -6
- package/dist/center-credential.js +25 -5
- package/dist/config-catalog.d.ts +26 -7
- package/dist/config-catalog.js +26 -22
- package/dist/config-dir-trust.js +4 -2
- package/dist/config-invariants.d.ts +20 -8
- package/dist/config-invariants.js +55 -12
- package/dist/config-lkg.d.ts +5 -19
- package/dist/config-lkg.js +8 -62
- package/dist/config-provider.d.ts +1 -1
- package/dist/config-root-migration.d.ts +2 -2
- package/dist/config-types.d.ts +38 -42
- package/dist/config.d.ts +58 -7
- package/dist/config.js +90 -40
- package/dist/control-socket.d.ts +51 -0
- package/dist/control-socket.js +115 -0
- package/dist/deployment-governance.d.ts +2 -1
- package/dist/deployment-shape.d.ts +9 -1
- package/dist/deployment-shape.js +2 -1
- package/dist/device-protocol.d.ts +5 -4
- package/dist/device-protocol.js +1 -1
- package/dist/device-ws-hub.d.ts +7 -0
- package/dist/device-ws-hub.js +10 -3
- package/dist/device-ws-protocol.d.ts +63 -10
- package/dist/device-ws-protocol.js +56 -25
- package/dist/elicitation.js +2 -2
- package/dist/engine-identity.d.ts +19 -7
- package/dist/engine-identity.js +10 -2
- package/dist/entry-mode-clamp-notice.d.ts +2 -1
- package/dist/entry-problems.d.ts +40 -3
- package/dist/entry-problems.js +3 -1
- package/dist/execution-env-refusal.d.ts +29 -0
- package/dist/execution-env-refusal.js +8 -0
- package/dist/execution-lane-caps.d.ts +3 -1
- package/dist/field-conflict.d.ts +1 -1
- package/dist/file-history-capture.d.ts +11 -4
- package/dist/file-history-capture.js +9 -6
- package/dist/file-history-disclosure.d.ts +7 -5
- package/dist/file-history-disclosure.js +7 -5
- package/dist/fleet/drain-entry.d.ts +4 -0
- package/dist/fleet/engine-signal.d.ts +12 -29
- package/dist/fleet/engine-signal.js +1 -7
- package/dist/fleet-client.js +2 -5
- package/dist/gate-settlement.d.ts +1 -8
- package/dist/gate-settlement.js +0 -4
- package/dist/history-anchor.js +1 -1
- package/dist/hitl-delivery.d.ts +2 -3
- package/dist/hitl-delivery.js +0 -2
- package/dist/hooks/hook-llm.js +4 -2
- package/dist/hooks/hook-runner.d.ts +3 -2
- package/dist/hooks/hook-runner.js +18 -3
- package/dist/host-decision.d.ts +11 -11
- package/dist/host-lsp-manager.d.ts +2 -2
- package/dist/http/active-run-conflict.d.ts +35 -2
- package/dist/http/active-run-conflict.js +48 -19
- package/dist/http/admission.js +17 -2
- package/dist/http/approval-sweeps.d.ts +1 -1
- package/dist/http/approval-sweeps.js +10 -7
- package/dist/http/checkpoint-ctx.d.ts +4 -0
- package/dist/http/checkpoint-ctx.js +2 -0
- package/dist/http/dispatch.d.ts +8 -3
- package/dist/http/dispatch.js +59 -32
- package/dist/http/durable-run-leg.js +16 -17
- package/dist/http/health-reader.d.ts +16 -9
- package/dist/http/health-reader.js +2 -1
- package/dist/http/health-self-report.d.ts +9 -1
- package/dist/http/health-self-report.js +3 -1
- package/dist/http/owner-access.d.ts +33 -14
- package/dist/http/owner-access.js +40 -10
- package/dist/http/principal-gate.d.ts +49 -21
- package/dist/http/principal-gate.js +61 -11
- package/dist/http/request-channel.d.ts +23 -0
- package/dist/http/request-channel.js +9 -0
- package/dist/http/request-guards.js +17 -2
- package/dist/http/resume-ctx-read.js +42 -3
- package/dist/http/resume-legs.d.ts +4 -3
- package/dist/http/resume-legs.js +367 -117
- package/dist/http/route-ctx.d.ts +91 -15
- package/dist/http/route-ctx.js +17 -0
- package/dist/http/route-table.d.ts +5 -1
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-config-refresh.js +3 -3
- package/dist/http/routes/admin-drain.d.ts +3 -0
- package/dist/http/routes/admin-drain.js +7 -7
- package/dist/http/routes/admin-memory-optout.d.ts +1 -1
- package/dist/http/routes/admin-memory-optout.js +3 -3
- package/dist/http/routes/adoption.js +3 -5
- package/dist/http/routes/agents-roster.js +2 -3
- package/dist/http/routes/approvals-assistant.d.ts +2 -1
- package/dist/http/routes/approvals-assistant.js +99 -51
- package/dist/http/routes/background.d.ts +3 -3
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +38 -10
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +19 -7
- package/dist/http/routes/fleet.js +10 -8
- package/dist/http/routes/images.js +10 -10
- package/dist/http/routes/memory-bundle.js +3 -3
- package/dist/http/routes/memory-compliance.js +3 -3
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +3 -3
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/observability.js +3 -3
- package/dist/http/routes/projects.d.ts +3 -2
- package/dist/http/routes/projects.js +4 -5
- package/dist/http/routes/retention-ops.js +3 -3
- package/dist/http/routes/rules.js +5 -5
- package/dist/http/routes/run-permission-mode.d.ts +2 -1
- package/dist/http/routes/run-permission-mode.js +2 -2
- package/dist/http/routes/runs.d.ts +4 -9
- package/dist/http/routes/runs.js +171 -65
- package/dist/http/routes/session-sync.d.ts +22 -9
- package/dist/http/routes/session-sync.js +204 -107
- package/dist/http/routes/sessions-list.js +13 -7
- package/dist/http/routes/sessions.js +30 -15
- package/dist/http/routes/tasks.js +46 -41
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +30 -30
- package/dist/http/server.js +16 -4
- package/dist/http/submit-ctx.d.ts +1 -1
- package/dist/http/submit-ctx.js +7 -4
- package/dist/http/wire-types.d.ts +14 -4
- package/dist/human-facing-args.d.ts +33 -0
- package/dist/human-facing-args.js +33 -0
- package/dist/identity-source.d.ts +20 -3
- package/dist/identity-source.js +14 -0
- package/dist/index.d.ts +5 -1
- package/dist/k8s-api-credentials.d.ts +29 -0
- package/dist/k8s-api-credentials.js +25 -0
- package/dist/lane-home-probe.d.ts +3 -7
- package/dist/lane-home-probe.js +2 -2
- package/dist/leader/diffup.d.ts +19 -0
- package/dist/leader/diffup.js +7 -0
- package/dist/leader/leader.d.ts +116 -11
- package/dist/leader/leader.js +69 -5
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +2 -1
- package/dist/leader/run-entry.js +1 -1
- package/dist/leader/wire.d.ts +42 -132
- package/dist/leader/wire.js +81 -67
- package/dist/leg-facts.d.ts +13 -5
- package/dist/leg-facts.js +2 -1
- package/dist/memory-scope.d.ts +23 -11
- package/dist/memory-scope.js +18 -6
- package/dist/memory-sync-client.d.ts +30 -2
- package/dist/memory-sync-client.js +16 -9
- package/dist/model-entry-headers.d.ts +9 -10
- package/dist/model-entry-headers.js +0 -4
- package/dist/model-entry-refusal.d.ts +7 -4
- package/dist/model-entry-refusal.js +11 -5
- package/dist/model-plane-face.d.ts +1 -1
- package/dist/model-plane-face.js +3 -1
- package/dist/model-route-endpoint.d.ts +13 -7
- package/dist/model-route-endpoint.js +14 -11
- package/dist/model-select.d.ts +22 -0
- package/dist/model-select.js +9 -0
- package/dist/model-unreachable-notice.d.ts +11 -0
- package/dist/model-unreachable-notice.js +4 -4
- package/dist/object-store-buckets.d.ts +640 -6
- package/dist/object-store-buckets.js +166 -0
- package/dist/observability/err-text.d.ts +7 -0
- package/dist/observability/err-text.js +5 -0
- package/dist/observability/fail-open.d.ts +25 -17
- package/dist/observability/fail-open.js +25 -17
- package/dist/observability/logger.js +3 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/operator-roster.d.ts +68 -0
- package/dist/operator-roster.js +32 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
- package/dist/orchestration/workflow-notify-journal.js +12 -1
- package/dist/orchestration/workflow-park-decision.d.ts +70 -18
- package/dist/orchestration/workflow-park-decision.js +49 -10
- package/dist/orchestration/workflow-park-holder.d.ts +65 -0
- package/dist/orchestration/workflow-park-holder.js +98 -0
- package/dist/orchestration/workflow-record-fence.d.ts +36 -0
- package/dist/orchestration/workflow-record-fence.js +42 -0
- package/dist/outbound-dispatcher.d.ts +71 -8
- package/dist/outbound-dispatcher.js +48 -7
- package/dist/owner-faces.d.ts +2 -2
- package/dist/parked-decide.d.ts +106 -55
- package/dist/parked-decide.js +153 -117
- package/dist/peer-directory.d.ts +2 -2
- package/dist/peer-inbound-face.d.ts +2 -6
- package/dist/peer-inbound-face.js +0 -3
- package/dist/permission-mode-admission.d.ts +74 -5
- package/dist/permission-mode-admission.js +56 -3
- package/dist/plugins/adoption-log-sql.d.ts +3 -1
- package/dist/plugins/adoption-log-sql.js +7 -1
- package/dist/plugins/approval-ask-store-file.d.ts +14 -2
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +10 -1
- package/dist/plugins/approval-ask-store-memory.js +35 -10
- package/dist/plugins/approval-ask-store-sql.d.ts +69 -4
- package/dist/plugins/approval-ask-store-sql.js +45 -5
- package/dist/plugins/approval-exemption-store.js +3 -0
- package/dist/plugins/approval-nonce-store.js +2 -0
- package/dist/plugins/blob-backend.d.ts +83 -7
- package/dist/plugins/blob-backend.js +57 -6
- package/dist/plugins/breaker-state-sql.js +2 -0
- package/dist/plugins/caching-session-store.d.ts +9 -15
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
- package/dist/plugins/checkpoint-index-columns.js +3 -6
- package/dist/plugins/checkpoint-store-face.d.ts +119 -61
- package/dist/plugins/checkpoint-store-face.js +134 -55
- package/dist/plugins/device-store-sql.js +6 -0
- package/dist/plugins/error-origin.d.ts +18 -0
- package/dist/plugins/error-origin.js +10 -0
- package/dist/plugins/execution-placement-sql.js +6 -6
- package/dist/plugins/image-bake-store-sql.js +6 -0
- package/dist/plugins/image-index-sql.js +4 -0
- package/dist/plugins/landed-attendants.d.ts +212 -0
- package/dist/plugins/landed-attendants.js +161 -0
- package/dist/plugins/leader-run-store-sql.js +4 -0
- package/dist/plugins/local-checkpoint-store.d.ts +13 -3
- package/dist/plugins/local-checkpoint-store.js +38 -49
- package/dist/plugins/local-session-live-reference.d.ts +40 -0
- package/dist/plugins/local-session-live-reference.js +39 -0
- package/dist/plugins/local-session-store.d.ts +71 -33
- package/dist/plugins/local-session-store.js +129 -27
- package/dist/plugins/memory-engine-pg.js +4 -0
- package/dist/plugins/memory-engine-sql-apply.js +5 -2
- package/dist/plugins/memory-engine-tidb.js +4 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
- package/dist/plugins/memory-sync-store-pg.js +5 -0
- package/dist/plugins/memory-sync-store-tidb.js +5 -0
- package/dist/plugins/one-time-migrations.d.ts +104 -80
- package/dist/plugins/one-time-migrations.js +226 -132
- package/dist/plugins/org-delivery-quota-store.js +4 -1
- package/dist/plugins/outcome-ledger-sql.js +2 -0
- package/dist/plugins/permission-rule-store-sql.js +11 -4
- package/dist/plugins/pg-cost-quota.js +4 -0
- package/dist/plugins/pg-pool.js +35 -10
- package/dist/plugins/pg-rate-limiter.js +2 -0
- package/dist/plugins/posix-shell-fs.d.ts +6 -1
- package/dist/plugins/posix-shell-fs.js +4 -1
- package/dist/plugins/remote-env-adb.d.ts +19 -15
- package/dist/plugins/remote-env-adb.js +41 -15
- package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
- package/dist/plugins/remote-env-connect-seat.js +72 -0
- package/dist/plugins/remote-env-device.d.ts +22 -5
- package/dist/plugins/remote-env-device.js +106 -8
- package/dist/plugins/remote-env-e2b.d.ts +15 -6
- package/dist/plugins/remote-env-e2b.js +42 -32
- package/dist/plugins/remote-env-file-error.js +6 -2
- package/dist/plugins/remote-env-host.d.ts +46 -10
- package/dist/plugins/remote-env-host.js +124 -52
- package/dist/plugins/remote-env-k8s.d.ts +109 -41
- package/dist/plugins/remote-env-k8s.js +160 -54
- package/dist/plugins/remote-env-local-docker.d.ts +13 -8
- package/dist/plugins/remote-env-local-docker.js +34 -41
- package/dist/plugins/remote-env-ssh.d.ts +18 -12
- package/dist/plugins/remote-env-ssh.js +60 -59
- package/dist/plugins/remote-shell.d.ts +98 -1
- package/dist/plugins/remote-shell.js +218 -0
- package/dist/plugins/replica-registration.d.ts +1 -1
- package/dist/plugins/replica-registration.js +1 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
- package/dist/plugins/resume-anchor-store-sql.js +12 -5
- package/dist/plugins/retention-lane-store-sql.js +4 -0
- package/dist/plugins/retention-sql.d.ts +20 -2
- package/dist/plugins/retention-sql.js +11 -0
- package/dist/plugins/run-store-sql.d.ts +21 -2
- package/dist/plugins/run-store-sql.js +31 -7
- package/dist/plugins/s3-presign.d.ts +23 -0
- package/dist/plugins/s3-presign.js +37 -3
- package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
- package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
- package/dist/plugins/scheduler-support.d.ts +2 -2
- package/dist/plugins/scheduler-support.js +0 -3
- package/dist/plugins/schema-consistency.d.ts +4 -2
- package/dist/plugins/schema-consistency.js +19 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-file-ledger.d.ts +2 -2
- package/dist/plugins/send-file-ledger.js +5 -2
- package/dist/plugins/send-user-file.d.ts +3 -4
- package/dist/plugins/send-user-file.js +56 -52
- package/dist/plugins/serving-requirement.d.ts +26 -7
- package/dist/plugins/serving-requirement.js +11 -4
- package/dist/plugins/session-birth.d.ts +6 -2
- package/dist/plugins/session-blob-claim-token.d.ts +15 -0
- package/dist/plugins/session-blob-claim-token.js +20 -0
- package/dist/plugins/session-blob-namespace.d.ts +46 -0
- package/dist/plugins/session-blob-namespace.js +116 -0
- package/dist/plugins/session-capture-record-store-sql.js +2 -0
- package/dist/plugins/session-ext-table.d.ts +14 -1
- package/dist/plugins/session-ext-table.js +5 -1
- package/dist/plugins/session-row-sql.d.ts +49 -2
- package/dist/plugins/session-row-sql.js +56 -13
- package/dist/plugins/session-store-sql.d.ts +34 -18
- package/dist/plugins/session-store-sql.js +98 -17
- package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
- package/dist/plugins/shared-memory-store-sql.js +6 -2
- package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
- package/dist/plugins/snapshot-bucket-presence.js +8 -2
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/sql-driver.js +18 -3
- package/dist/plugins/sql-errors.d.ts +11 -6
- package/dist/plugins/sql-errors.js +5 -6
- package/dist/plugins/sql-json-column.d.ts +1 -1
- package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
- package/dist/plugins/stop-then-start-sentence.js +14 -10
- package/dist/plugins/stop-then-start.d.ts +109 -0
- package/dist/plugins/stop-then-start.js +104 -0
- package/dist/plugins/store-backend.d.ts +84 -15
- package/dist/plugins/store-backend.js +236 -72
- package/dist/plugins/store-boundary.d.ts +57 -0
- package/dist/plugins/store-boundary.js +139 -0
- package/dist/plugins/store-contracts.d.ts +17 -0
- package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
- package/dist/plugins/store-fault-sentence-m53.js +25 -0
- package/dist/plugins/store-fault.d.ts +26 -7
- package/dist/plugins/store-fault.js +18 -2
- package/dist/plugins/task-attachment-store.d.ts +2 -1
- package/dist/plugins/task-attachment-store.js +14 -1
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +10 -8
- package/dist/plugins/tidb-cost-quota.js +4 -0
- package/dist/plugins/tidb-pool.js +21 -25
- package/dist/plugins/tidb-rate-limiter.js +4 -0
- package/dist/plugins/user-input-ledger-sql.js +4 -0
- package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
- package/dist/plugins/workflow-run-store-sql.js +64 -4
- package/dist/plugins/workspace-custody.d.ts +4 -3
- package/dist/plugins/workspace-fill.d.ts +4 -7
- package/dist/plugins/workspace-fill.js +2 -1
- package/dist/plugins/write-behind-counter.js +5 -0
- package/dist/project-context.d.ts +0 -2
- package/dist/project-context.js +4 -8
- package/dist/project-registry.d.ts +46 -0
- package/dist/project-registry.js +44 -0
- package/dist/question.d.ts +2 -1
- package/dist/question.js +5 -13
- package/dist/request-form.d.ts +6 -1
- package/dist/request-form.js +4 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +4 -0
- package/dist/request-value-closure.js +10 -0
- package/dist/rule-issue-reading.d.ts +1 -1
- package/dist/rules-consent.d.ts +1 -1
- package/dist/run-acceptance.d.ts +3 -2
- package/dist/run-acceptance.js +4 -6
- package/dist/run-local.js +9 -9
- package/dist/runs.d.ts +54 -16
- package/dist/runs.js +67 -13
- package/dist/runtime-governance.d.ts +5 -4
- package/dist/security.d.ts +30 -32
- package/dist/security.js +41 -7
- package/dist/sema-config-dir.d.ts +2 -1
- package/dist/server-secret-env.d.ts +12 -10
- package/dist/server-secret-env.js +24 -15
- package/dist/session-bindings.d.ts +82 -0
- package/dist/session-bindings.js +18 -0
- package/dist/session-children-retire.d.ts +2 -2
- package/dist/session-children-retire.js +2 -6
- package/dist/session-deletion.d.ts +16 -1
- package/dist/session-error-wire.d.ts +3 -3
- package/dist/session-error-wire.js +3 -3
- package/dist/session-incarnation.d.ts +10 -0
- package/dist/session-incarnation.js +10 -1
- package/dist/session-kind.d.ts +57 -0
- package/dist/session-kind.js +22 -0
- package/dist/session-row.d.ts +21 -1
- package/dist/session-row.js +7 -1
- package/dist/session-sync-kernel.d.ts +24 -14
- package/dist/session-sync-kernel.js +3 -6
- package/dist/session-sync.d.ts +25 -103
- package/dist/session-sync.js +14 -76
- package/dist/session-titler.d.ts +10 -3
- package/dist/session-titler.js +19 -6
- package/dist/ssh-host-key.d.ts +5 -0
- package/dist/ssh-host-key.js +3 -0
- package/dist/stores-sql/blob-index-sql.d.ts +30 -9
- package/dist/stores-sql/blob-index-sql.js +40 -21
- package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
- package/dist/stores-sql/checkpoint-store-sql.js +70 -146
- package/dist/stores-sql/file-history-store-sql.d.ts +113 -15
- package/dist/stores-sql/file-history-store-sql.js +205 -95
- package/dist/stores-sql/pg-session-storage.d.ts +34 -7
- package/dist/stores-sql/pg-session-storage.js +37 -13
- package/dist/stores-sql/retention-store-sql.d.ts +1 -9
- package/dist/stores-sql/retention-store-sql.js +5 -19
- package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
- package/dist/stores-sql/session-policy-store-sql.js +61 -44
- package/dist/stores-sql/session-write-hooks.d.ts +11 -0
- package/dist/stores-sql/tidb-session-store.d.ts +58 -7
- package/dist/stores-sql/tidb-session-store.js +37 -13
- package/dist/submission-exit.d.ts +3 -2
- package/dist/submission-exit.js +9 -1
- package/dist/task-cwd.d.ts +2 -2
- package/dist/task-settings.d.ts +7 -5
- package/dist/task-settings.js +3 -3
- package/dist/terminal.d.ts +33 -2
- package/dist/terminal.js +139 -9
- package/dist/tool-approval.d.ts +465 -507
- package/dist/tool-approval.js +606 -1222
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/ledger-events.d.ts +3 -3
- package/dist/trace/project.d.ts +2 -3
- package/dist/trace/redact.js +3 -2
- package/dist/trace/self-minted-wire.d.ts +571 -178
- package/dist/trace/self-minted-wire.js +189 -56
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/read-warnings.js +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/dist/workspace-space.d.ts +10 -4
- package/dist/workspace-space.js +4 -1
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +79 -28
- package/package.json +7 -3
- package/scripts/embed-minimal.mjs +3 -3
- package/wire-changes/server@7.151.0.json +762 -0
- package/wire-changes/server@7.152.0.json +900 -0
- package/dist/config-d-migration.d.ts +0 -37
- package/dist/config-d-migration.js +0 -60
- package/dist/http/session-inherit-env.d.ts +0 -36
- package/dist/http/session-inherit-env.js +0 -6
|
@@ -2,6 +2,7 @@ import { type SshHostKeyPolicy } from "../ssh-host-key.js";
|
|
|
2
2
|
import { type LaneLogger } from "./remote-shell.js";
|
|
3
3
|
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
4
4
|
import { type LaneHomeConfig } from "../lane-home-probe.js";
|
|
5
|
+
import { type ConnectResult } from "./remote-env-connect-seat.js";
|
|
5
6
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
6
7
|
export interface SshEnvConfig extends LaneHomeConfig {
|
|
7
8
|
host: string;
|
|
@@ -40,13 +41,6 @@ export interface SshEnvConfig extends LaneHomeConfig {
|
|
|
40
41
|
/** 运维日志座(起服装配递本服务的 logger):暂存写「已有目标的权限位没带过去」那一行经它(缺席 ⇒ 暂存编排自建一只,见 `remote-shell.ts` `StagedWriteLane.logger`)。 */
|
|
41
42
|
logger?: LaneLogger;
|
|
42
43
|
}
|
|
43
|
-
type ConnectResult = {
|
|
44
|
-
ok: true;
|
|
45
|
-
value: WorkspaceHandle;
|
|
46
|
-
} | {
|
|
47
|
-
ok: false;
|
|
48
|
-
error: RemoteExecutionError;
|
|
49
|
-
};
|
|
50
44
|
/**
|
|
51
45
|
* Test seam ([ref] §9.3 fault-injection): inject a `connectOnce` that replays an error sequence, so the
|
|
52
46
|
* retry policy (retryable codes / maxAttempts / "first fail → second success") is deterministically testable
|
|
@@ -80,8 +74,12 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
80
74
|
private readonly homeSeat;
|
|
81
75
|
/** 家目录声明 × 实测的判定,按连接记(同一条连接只问一次;重连换了连接再问 —— 登录用户不变时答案相同,代价一次往返;没声明且已量到绝对路径之后不再问)。 */
|
|
82
76
|
private homeChecked?;
|
|
83
|
-
/**
|
|
84
|
-
|
|
77
|
+
/**
|
|
78
|
+
* 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序(记住的拒 ⇒ 原样再答 · 同代际的连接在飞 ⇒ 等它,调用方的取消只退它自己的等待 · 已连 ⇒ 当下的把手 · 否则起一条),
|
|
79
|
+
* 并持有这只环境记住的拒 —— 两件立定的事实:目标机答了家目录而与声明不一致(`env.home_unconfirmed`;同一目标机同一登录用户同一答案:不再问、不再发命令),
|
|
80
|
+
* 目标机出示的主机密钥过不了部署钉的判(`env.host_key_rejected`;同一配置同一主机每次同答:不再拨号)。此后每个面原样回它。
|
|
81
|
+
*/
|
|
82
|
+
private readonly seat;
|
|
85
83
|
/** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在远端登录 shell 里(目标机的 profile / env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
86
84
|
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
87
85
|
readonly cdPath: "unknown";
|
|
@@ -100,8 +98,8 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
100
98
|
ok: false;
|
|
101
99
|
error: RemoteExecutionError;
|
|
102
100
|
}>;
|
|
103
|
-
/**
|
|
104
|
-
private
|
|
101
|
+
/** 一只建连的拒在 exec / 文件面上的那一句:拒句原文(cause = 那只拒,文件面据它答 `target_unavailable`)。 */
|
|
102
|
+
private refusalOnExecFace;
|
|
105
103
|
/** [ref] 四轮复审:三路(connect/reconnect/懒连接)唯一的连接发起点——同代际在飞记录 join,settle
|
|
106
104
|
* 身份清;代际抓拍一次贯穿全部重试轮,每轮起跳先查(陈旧/destroyed=不再开 socket,用不可重试的
|
|
107
105
|
* aborted 码即刻收敛 withRetry)。五轮复审:共享尝试零 caller 信号(取消面全在 per-caller race)。 */
|
|
@@ -162,8 +160,16 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
162
160
|
/** 文件面探针的 exec(车PL 验修 ③):只用绝对路径,**不进工作目录**、没有 cwd 守卫 —— 工作目录进不去时探针照样答真话(`permission_denied` /
|
|
163
161
|
* `not_found` 由命令自己的 stderr 归),core 的 `isDeadCwd` 不会把「进不去」读成「没了」。 */
|
|
164
162
|
private fsExec;
|
|
165
|
-
/** buffered exec
|
|
163
|
+
/** buffered exec(命令面 + 文件面探针):stdout 走 8 MiB 滚动尾(模型的 `yes` / `cat big` 撑不爆 worker),截掉的字节在结果里如实标出。 */
|
|
166
164
|
private execCall;
|
|
165
|
+
/**
|
|
166
|
+
* 绑定读一答的往返(`remote-shell.ts` 的脚本经它跑):与文件面探针同一起动(不进工作目录、没有 cwd 守卫)、数据界;stdout **逐字节**收、只留开头
|
|
167
|
+
* `cap + 1` 字节 —— 本答该有多长由读法定(`cap`),比它长的答由读法拒,从不解码一段尾巴(命令面那只 8 MiB 滚动尾在这里不许用)。
|
|
168
|
+
*/
|
|
169
|
+
private boundExec;
|
|
170
|
+
/** buffered exec 的唯一体:`call` 定了整串、stderr 的 started 行剥法与收口(`remote-shell.ts` `createCwdGuardedCall`);stdout 的收法由 `sink` 定
|
|
171
|
+
* (命令面 = 滚动尾;绑定读 = 封顶的逐字节头)。`sink.tail` = 切断时随错误带出的 stdout(绑定读不带)。 */
|
|
172
|
+
private runBuffered;
|
|
167
173
|
execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
|
|
168
174
|
private sftp;
|
|
169
175
|
absolutePath(p: string): Promise<Result<string, FileError>>;
|
|
@@ -11,6 +11,7 @@ import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTail
|
|
|
11
11
|
import { capabilitiesFor } from "../execution-lane-caps.js";
|
|
12
12
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
13
13
|
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
14
|
+
import { connectRefusal, createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
|
|
14
15
|
const PROVIDER = "ssh";
|
|
15
16
|
const ok = (value) => ({ ok: true, value });
|
|
16
17
|
const unsupported = (op) => ({
|
|
@@ -33,7 +34,11 @@ export class RemoteSshExecutionEnv {
|
|
|
33
34
|
}
|
|
34
35
|
homeSeat;
|
|
35
36
|
homeChecked;
|
|
36
|
-
|
|
37
|
+
seat = createConnectSeat({
|
|
38
|
+
live: () => (this.conn !== undefined && !this.destroyed ? this.workspaceHandle() : undefined),
|
|
39
|
+
inFlight: () => (this.inFlight !== undefined && this.inFlight.epoch === this.connEpoch ? this.inFlight.promise : undefined),
|
|
40
|
+
establish: (config) => withdrawOnAbort(this.startConnect("establish"), config?.abortSignal),
|
|
41
|
+
});
|
|
37
42
|
cdPath = "unknown";
|
|
38
43
|
withheldEnv = [];
|
|
39
44
|
cfg;
|
|
@@ -66,26 +71,10 @@ export class RemoteSshExecutionEnv {
|
|
|
66
71
|
async connect(config) {
|
|
67
72
|
if (this.destroyed)
|
|
68
73
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
|
|
69
|
-
|
|
70
|
-
return ok(this.workspaceHandle());
|
|
71
|
-
return this.raceConnAbort(this.startConnect("establish"), config?.abortSignal);
|
|
74
|
+
return this.seat.connect(config);
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return shared;
|
|
76
|
-
if (signal.aborted)
|
|
77
|
-
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
78
|
-
let onAbort;
|
|
79
|
-
const aborted = new Promise((res) => {
|
|
80
|
-
onAbort = () => res("aborted");
|
|
81
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
82
|
-
});
|
|
83
|
-
const raced = await Promise.race([shared, aborted]);
|
|
84
|
-
if (onAbort)
|
|
85
|
-
signal.removeEventListener("abort", onAbort);
|
|
86
|
-
if (raced === "aborted")
|
|
87
|
-
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while waiting") };
|
|
88
|
-
return raced;
|
|
76
|
+
refusalOnExecFace(refusal) {
|
|
77
|
+
return new ExecutionError("shell_unavailable", refusal.error.message, refusal.error);
|
|
89
78
|
}
|
|
90
79
|
startConnect(kind) {
|
|
91
80
|
if (this.inFlight && this.inFlight.epoch === this.connEpoch)
|
|
@@ -115,10 +104,15 @@ export class RemoteSshExecutionEnv {
|
|
|
115
104
|
}
|
|
116
105
|
async ensureConnected(signal) {
|
|
117
106
|
for (let round = 0; round < 3; round++) {
|
|
118
|
-
|
|
119
|
-
|
|
107
|
+
const held = this.seat.held;
|
|
108
|
+
if (held)
|
|
109
|
+
return { ok: false, error: this.refusalOnExecFace(held) };
|
|
120
110
|
const transport = await this.ensureTransport(signal);
|
|
121
|
-
if (!transport.ok
|
|
111
|
+
if (!transport.ok) {
|
|
112
|
+
const now = this.seat.held;
|
|
113
|
+
return now ? { ok: false, error: this.refusalOnExecFace(now) } : transport;
|
|
114
|
+
}
|
|
115
|
+
if (!this.homeSeat.asks)
|
|
122
116
|
return transport;
|
|
123
117
|
const conn = this.conn;
|
|
124
118
|
if (!conn)
|
|
@@ -127,7 +121,7 @@ export class RemoteSshExecutionEnv {
|
|
|
127
121
|
const checked = { conn, verdict: this.homeVerdict(conn) };
|
|
128
122
|
this.homeChecked = checked;
|
|
129
123
|
void checked.verdict.then((r) => {
|
|
130
|
-
if (!r.ok && this.
|
|
124
|
+
if (!r.ok && this.seat.held === undefined && this.homeChecked === checked)
|
|
131
125
|
this.homeChecked = undefined;
|
|
132
126
|
});
|
|
133
127
|
}
|
|
@@ -185,10 +179,9 @@ export class RemoteSshExecutionEnv {
|
|
|
185
179
|
const verdict = this.homeSeat.settle(probe);
|
|
186
180
|
if (verdict === undefined)
|
|
187
181
|
return resolve(ok(undefined));
|
|
188
|
-
const error = new ExecutionError("shell_unavailable", verdict.refusal);
|
|
189
182
|
if (verdict.answered)
|
|
190
|
-
this.
|
|
191
|
-
resolve({ ok: false, error });
|
|
183
|
+
return resolve({ ok: false, error: this.refusalOnExecFace(this.seat.answer(connectRefusal("home_unconfirmed", verdict.refusal))) });
|
|
184
|
+
resolve({ ok: false, error: new ExecutionError("shell_unavailable", verdict.refusal) });
|
|
192
185
|
};
|
|
193
186
|
const timer = setTimeout(() => settle({ ok: false, reason: `no answer within ${this.cfg.readyTimeoutMs}ms` }), this.cfg.readyTimeoutMs);
|
|
194
187
|
try {
|
|
@@ -222,33 +215,14 @@ export class RemoteSshExecutionEnv {
|
|
|
222
215
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH env destroyed — no lazy reconnect after teardown") };
|
|
223
216
|
if (signal?.aborted)
|
|
224
217
|
return { ok: false, error: new ExecutionError("aborted", "aborted before SSH lazy-connect") };
|
|
225
|
-
|
|
226
|
-
return ok(undefined);
|
|
227
|
-
const shared = this.startConnect("establish");
|
|
228
|
-
let r;
|
|
229
|
-
if (signal) {
|
|
230
|
-
let onAbort;
|
|
231
|
-
const aborted = new Promise((res) => {
|
|
232
|
-
onAbort = () => res("aborted");
|
|
233
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
234
|
-
});
|
|
235
|
-
const raced = await Promise.race([shared, aborted]);
|
|
236
|
-
if (onAbort)
|
|
237
|
-
signal.removeEventListener("abort", onAbort);
|
|
238
|
-
if (raced === "aborted")
|
|
239
|
-
return { ok: false, error: new ExecutionError("aborted", "aborted while waiting for SSH lazy-connect") };
|
|
240
|
-
r = raced;
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
r = await shared;
|
|
244
|
-
}
|
|
218
|
+
const r = await this.seat.connect(signal ? { abortSignal: signal } : undefined);
|
|
245
219
|
if (r.ok) {
|
|
246
220
|
if (this.conn)
|
|
247
221
|
return ok(undefined);
|
|
248
222
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH connection dropped right after connect") };
|
|
249
223
|
}
|
|
250
224
|
if (r.error.code === "aborted")
|
|
251
|
-
return { ok: false, error: new ExecutionError("aborted", r.error.message, r.error) };
|
|
225
|
+
return { ok: false, error: new ExecutionError("aborted", signal?.aborted ? "aborted while waiting for SSH lazy-connect" : r.error.message, r.error) };
|
|
252
226
|
return { ok: false, error: new ExecutionError("shell_unavailable", `SSH lazy-connect failed: ${r.error.message}`, r.error) };
|
|
253
227
|
}
|
|
254
228
|
connectOnce(signal, opEpoch) {
|
|
@@ -273,7 +247,7 @@ export class RemoteSshExecutionEnv {
|
|
|
273
247
|
hostVerifier: (key) => {
|
|
274
248
|
const verdict = verifyHostKey(key);
|
|
275
249
|
if (!verdict.ok) {
|
|
276
|
-
hostKeyRejection = verdict.reason;
|
|
250
|
+
hostKeyRejection = { reason: verdict.reason, standing: verdict.standing };
|
|
277
251
|
return false;
|
|
278
252
|
}
|
|
279
253
|
return true;
|
|
@@ -303,10 +277,9 @@ export class RemoteSshExecutionEnv {
|
|
|
303
277
|
}
|
|
304
278
|
catch {
|
|
305
279
|
}
|
|
306
|
-
|
|
307
|
-
ok: false,
|
|
308
|
-
|
|
309
|
-
});
|
|
280
|
+
if (hostKeyRejection === undefined)
|
|
281
|
+
return resolve({ ok: false, error: this.connErr(e) });
|
|
282
|
+
resolve(hostKeyRejection.standing ? this.seat.answer(connectRefusal("host_key_rejected", hostKeyRejection.reason, e)) : { ok: false, error: new RemoteExecutionError("connect_failed", hostKeyRejection.reason, e) });
|
|
310
283
|
});
|
|
311
284
|
try {
|
|
312
285
|
c.connect(opts);
|
|
@@ -328,6 +301,9 @@ export class RemoteSshExecutionEnv {
|
|
|
328
301
|
async reconnect(_sessionToken) {
|
|
329
302
|
if (this.destroyed)
|
|
330
303
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
|
|
304
|
+
const held = this.seat.held;
|
|
305
|
+
if (held)
|
|
306
|
+
return held;
|
|
331
307
|
if (this.inFlight && this.inFlight.epoch === this.connEpoch && this.inFlight.kind === "replace")
|
|
332
308
|
return this.inFlight.promise;
|
|
333
309
|
this.connEpoch++;
|
|
@@ -356,7 +332,33 @@ export class RemoteSshExecutionEnv {
|
|
|
356
332
|
fsExec(command, abortSignal) {
|
|
357
333
|
return this.execCall(createCwdGuardedCall("ssh", command, undefined, undefined), { abortSignal });
|
|
358
334
|
}
|
|
359
|
-
|
|
335
|
+
execCall(call, options) {
|
|
336
|
+
const outBuf = new RollingTailBuffer();
|
|
337
|
+
return this.runBuffered(call, options, {
|
|
338
|
+
push: (d) => outBuf.push(d),
|
|
339
|
+
tail: outBuf,
|
|
340
|
+
take: () => {
|
|
341
|
+
const out = outBuf.result();
|
|
342
|
+
return markTruncated(out.text, out.droppedBytes);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
boundExec(cmd, cap, abortSignal) {
|
|
347
|
+
const chunks = [];
|
|
348
|
+
let kept = 0;
|
|
349
|
+
return this.runBuffered(createCwdGuardedCall("ssh", cmd, undefined, undefined), { abortSignal, timeout: this.cfg.dataTimeoutMs / 1000 }, {
|
|
350
|
+
push: (d) => {
|
|
351
|
+
if (kept > cap)
|
|
352
|
+
return;
|
|
353
|
+
const piece = d.length > cap + 1 - kept ? d.subarray(0, cap + 1 - kept) : d;
|
|
354
|
+
chunks.push(piece);
|
|
355
|
+
kept += piece.length;
|
|
356
|
+
},
|
|
357
|
+
tail: undefined,
|
|
358
|
+
take: () => new Uint8Array(Buffer.concat(chunks)),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async runBuffered(call, options, sink) {
|
|
360
362
|
const ready = await this.ensureConnected(options?.abortSignal);
|
|
361
363
|
if (!ready.ok)
|
|
362
364
|
return ready;
|
|
@@ -365,7 +367,6 @@ export class RemoteSshExecutionEnv {
|
|
|
365
367
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH not connected — call connect() first") };
|
|
366
368
|
const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
|
|
367
369
|
return new Promise((resolve) => {
|
|
368
|
-
const outBuf = new RollingTailBuffer();
|
|
369
370
|
const errBuf = new RollingTailBuffer();
|
|
370
371
|
const recordStderr = (bytes) => {
|
|
371
372
|
if (bytes.length > 0)
|
|
@@ -381,7 +382,7 @@ export class RemoteSshExecutionEnv {
|
|
|
381
382
|
};
|
|
382
383
|
const captured = () => {
|
|
383
384
|
recordStderr(call.endStderr());
|
|
384
|
-
return buildCapturedOutput(
|
|
385
|
+
return buildCapturedOutput(sink.tail, errBuf);
|
|
385
386
|
};
|
|
386
387
|
let settled = false;
|
|
387
388
|
let streamRef;
|
|
@@ -427,7 +428,7 @@ export class RemoteSshExecutionEnv {
|
|
|
427
428
|
}
|
|
428
429
|
stream
|
|
429
430
|
.on("data", (d) => {
|
|
430
|
-
|
|
431
|
+
sink.push(d);
|
|
431
432
|
options?.onStdout?.(d.toString());
|
|
432
433
|
})
|
|
433
434
|
.on("close", (code, signal) => {
|
|
@@ -438,9 +439,8 @@ export class RemoteSshExecutionEnv {
|
|
|
438
439
|
const held = call.endStderr();
|
|
439
440
|
const won = finish(() => {
|
|
440
441
|
recordStderr(held);
|
|
441
|
-
const out = outBuf.result();
|
|
442
442
|
const err = errBuf.result();
|
|
443
|
-
return call.settle(ok({ stdout:
|
|
443
|
+
return call.settle(ok({ stdout: sink.take(), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
|
|
444
444
|
});
|
|
445
445
|
if (won)
|
|
446
446
|
emitStderr(held);
|
|
@@ -688,6 +688,7 @@ export class RemoteSshExecutionEnv {
|
|
|
688
688
|
run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
|
|
689
689
|
resolve: (p) => this.resolve(p),
|
|
690
690
|
tempDir: () => null,
|
|
691
|
+
boundLane: { run: (cmd, cap, abortSignal) => this.boundExec(cmd, cap, abortSignal), payload: "base64", boundSec: this.cfg.dataTimeoutMs / 1000 },
|
|
691
692
|
}));
|
|
692
693
|
}
|
|
693
694
|
async readLink(p, abortSignal) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared shell/path helpers for the remote `RemoteExecutionEnv` adapters (ssh / adb / k8s / local-docker, and e2b where it
|
|
3
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, FileError, type FileInfo, type RemoteExecutionError, type Result, type SymlinkChain } from "@sema-agent/core";
|
|
5
|
+
import { ExecutionError, FileError, type BoundFileObject, 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):
|
|
@@ -206,6 +206,103 @@ export type ShellChainRun = (cmd: string) => Promise<Result<{
|
|
|
206
206
|
* 三条远端腿的文件面探针不带 cwd(车PL 验修 ③)⇒ 这条命令不会因为某个工作目录不可用而不跑。
|
|
207
207
|
*/
|
|
208
208
|
export declare function runSymlinkChain(absPath: string, run: ShellChainRun): Promise<Result<SymlinkChain, FileError>>;
|
|
209
|
+
/**
|
|
210
|
+
* 绑定读脚本的退出码闭集(与 shell / 工具自己的 1 / 2 / 124–127 / 128+ 不撞)。读法只在 {@link boundReadRefusal}(穷举 switch,无 default 臂)。
|
|
211
|
+
* · `link` —— 预检:叶是链接(没有 `O_NOFOLLOW` 可用,先查);
|
|
212
|
+
* · `moved` —— 打开之后的那一次观测不在规范键上(叶 / 父段打开时是链接、打开后被改名 / 删掉),或身份 / 类型不等于开答时;
|
|
213
|
+
* · `kind` —— 既非普通文件也非目录(预检或打开之后);
|
|
214
|
+
* · `over` —— 这一答量到的大小超过 `max`(先量后读:超了一个字节都不读);
|
|
215
|
+
* · `platform` —— 给不出那一次观测或缺工具(见上段)⇒ `not_supported`;
|
|
216
|
+
* · `name` —— 列目录时有一个名字带换行:一行一条的答装不下它而不生歧义(名字里的换行能伪造出一条假条目),整答拒、不列一半。
|
|
217
|
+
*/
|
|
218
|
+
export declare const BOUND_READ_EXITS: {
|
|
219
|
+
readonly link: 90;
|
|
220
|
+
readonly moved: 91;
|
|
221
|
+
readonly kind: 92;
|
|
222
|
+
readonly over: 93;
|
|
223
|
+
readonly platform: 94;
|
|
224
|
+
readonly name: 95;
|
|
225
|
+
};
|
|
226
|
+
export type BoundReadExit = keyof typeof BOUND_READ_EXITS;
|
|
227
|
+
/** 一答的操作闭集(工厂的四个成员各一答;`open` 是 `openBound` 那一答)。 */
|
|
228
|
+
export type BoundReadOp = "open" | "size" | "readBytes" | "list";
|
|
229
|
+
/**
|
|
230
|
+
* 绑定读的 POSIX sh 脚本 —— **常量**(不拼任何值;路径与参数经位置参数 `$1…$6` 进来):五条远端腿发的是逐字节同一串。
|
|
231
|
+
* 位置参数:`$1` 规范键 · `$2` 操作 · `$3` 开答时的身份 · `$4` 开答时的类型 · `$5` `max` · `$6` 载荷编码(`raw` / `base64`)。
|
|
232
|
+
*
|
|
233
|
+
* 步骤(每一答都整走一遍):
|
|
234
|
+
* ① 平台与工具:`/proc/self/fd` 不在、或缺工具 ⇒ `platform`,**在打开任何东西之前**;
|
|
235
|
+
* ② 预检:叶是链接 ⇒ `link`;在场却既非普通文件也非目录(FIFO / 设备 / 套接字)⇒ `kind` —— 预检只缩小窗口(shell 没有 `O_NOFOLLOW` / `O_NONBLOCK`),关不上它;
|
|
236
|
+
* ③ 一次打开、整条答共用:复合命令重定向 `{ …; } < "$f"`(**不用** `exec 3<`:ksh93 给 `exec` 开的 > 2 号描述符置 close-on-exec,子进程读不到它;
|
|
237
|
+
* 复合命令重定向的 0 号描述符照常继承 —— 外部命令、命令替换、shell 自己都看见同一只打开);打开失败 ⇒ shell 自己的报错 + 退 1(读法归 stderr 分类);
|
|
238
|
+
* ④ 那一次观测:`readlink /proc/self/fd/0`(内核给出「这只打开的对象此刻叫什么」;`/proc/self` 在外部命令里就是它自己,它继承的 0 号描述符就是这次打开)
|
|
239
|
+
* 须逐字等于规范键,否则 `moved` —— 命令替换会吞掉**全部**尾随换行,所以读数后面垫一个哨兵字符、只剥 `readlink` 自己那一个换行(名字以换行结尾的对象
|
|
240
|
+
* 与规范键不等);身份与类型 `stat -L -c '%d:%i %s %F' /proc/self/fd/0`(跟的是魔法链接:这只打开的对象本身,不是按路径再查;`%F` 是人读的词,
|
|
241
|
+
* GNU 按区域设置翻译 ⇒ 整只脚本在 `LC_ALL=C` 下跑,工具的报错句也因此是共享分类器认得的英文);
|
|
242
|
+
* 类型须是普通文件或目录,否则 `kind`;带了开答身份的答(size / read / list)⇒ 身份**与类型**须都等于开答时,否则 `moved`;
|
|
243
|
+
* ⑤ 作答,只经这一次打开:`open` 一行 `<身份> <file|directory>`;`size` 一行十进制;`readBytes` 先量(> `max` ⇒ 打出量到的大小、`over`,一个字节不读),
|
|
244
|
+
* 一行十进制大小 + 载荷 —— 大小 > 0 读恰好那么多(与 core 读口同读:开读那一刻的大小),= 0(procfs 一类)读到流尽、至多 `max + 1`;
|
|
245
|
+
* base64 腿的 `head | base64` 管道里 `head` 自己的退出码经 4 号描述符带出来(复合命令重定向,不用 `exec`):读失败 ⇒ 退 1,不交 `base64` 的 0;
|
|
246
|
+
* `list` 经 `cd -P /proc/self/fd/0` 进**这只打开的目录**,逐条 lstat 语义一行 `<十六进制模式> <大小> <mtime 秒> <名>`;名字带换行 ⇒ `name`。
|
|
247
|
+
* 全程不用 `!`(某些登录 shell 的历史展开)、不用 `pipefail`(dash 没有);读的截短另由读法按大小对拍兜住({@link createShellRebindingAnswers})。
|
|
248
|
+
*/
|
|
249
|
+
export declare const BOUND_READ_SCRIPT: string;
|
|
250
|
+
/** 一答的入参(开答之后的三答带开答时的身份与类型)。 */
|
|
251
|
+
export interface BoundReadArgs {
|
|
252
|
+
readonly bound?: BoundPlacement;
|
|
253
|
+
readonly max?: number;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 一答发给目标的整串(**纯数据** ⇒ `build*`):先判 `timeout(1)` 在不在(不在 ⇒ `platform`,有界给不出就不答),再以 `timeout <boundSec 向下取整,至少 1> sh -c <脚本>` 跑 ——
|
|
257
|
+
* 显式套一层 `sh`,把方言钉在目标的 POSIX `/bin/sh` 上(容器的命令 shell 可能是 bash、e2b 起的是登录 bash、ssh 交给远端登录 shell)。
|
|
258
|
+
* 整条包在一个 `{ …; }` 里,只有一个退出状态(与 {@link buildSymlinkChainCommand} 同一条理由)。值全经 {@link shellQuote} 进位置参数,脚本串里不拼值。
|
|
259
|
+
*/
|
|
260
|
+
export declare function buildBoundReadCommand(abs: string, op: BoundReadOp, args: BoundReadArgs, payload: BoundReadLane["payload"], boundSec: number): string;
|
|
261
|
+
/**
|
|
262
|
+
* 一条远端腿交给绑定读的那一条传输(各腿只造这一只闭包;读法与归码不在腿上)。
|
|
263
|
+
* 🔴 字节回程律:`stdout` **逐字节**(任何丢弃 / 截尾的收集器都不许接在这里 —— ssh 的命令面收集器是 8 MiB 滚动尾,所以 ssh 另造一只按本答上界封顶的);
|
|
264
|
+
* 收集器有上界的腿保留至多 `stdoutCap + 1` 字节,读法见到比上界长的答即拒(不解码一段尾巴)。
|
|
265
|
+
*/
|
|
266
|
+
export interface BoundReadLane {
|
|
267
|
+
run(cmd: string, stdoutCap: number, abortSignal?: AbortSignal): Promise<Result<{
|
|
268
|
+
stdout: Uint8Array;
|
|
269
|
+
stderr: string;
|
|
270
|
+
exitCode: number;
|
|
271
|
+
}, ExecutionError | RemoteExecutionError>>;
|
|
272
|
+
/** 载荷在这条腿的 stdout 上怎么走:`raw`(二进制收集器:local-docker / adb)或 `base64`(文本面:k8s 的 WS 文本帧 / e2b SDK 的字符串 / ssh)。 */
|
|
273
|
+
readonly payload: "raw" | "base64";
|
|
274
|
+
/** 一答在目标上的墙钟(整秒,`timeout(1)`)= 该腿的数据界。 */
|
|
275
|
+
readonly boundSec: number;
|
|
276
|
+
}
|
|
277
|
+
/** 开答定下的那只对象(工厂与答者之间的唯一凭据):身份 `<dev>:<inode>` + 类型。 */
|
|
278
|
+
export interface BoundPlacement {
|
|
279
|
+
readonly identity: string;
|
|
280
|
+
readonly kind: "file" | "directory";
|
|
281
|
+
}
|
|
282
|
+
/** 一只目录条目(没有 `path`:工厂把它锚在规范键之下,答者给的拼法不作数)。 */
|
|
283
|
+
export type BoundListEntry = Omit<FileInfo, "path">;
|
|
284
|
+
/**
|
|
285
|
+
* 逐答重绑的答者(传输那一半):每一答都是一次完整的重绑 —— 打开、核对它就是 `bound`、经这一次打开作答。shell 腿 = {@link createShellRebindingAnswers};
|
|
286
|
+
* 设备腿 = 一条 `openBound` 协议指令。
|
|
287
|
+
*/
|
|
288
|
+
export interface RebindingAnswers {
|
|
289
|
+
open(abortSignal?: AbortSignal): Promise<Result<BoundPlacement, FileError>>;
|
|
290
|
+
size(bound: BoundPlacement): Promise<Result<number, FileError>>;
|
|
291
|
+
readBytes(bound: BoundPlacement, max: number, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
292
|
+
list(bound: BoundPlacement, abortSignal?: AbortSignal): Promise<Result<BoundListEntry[], FileError>>;
|
|
293
|
+
}
|
|
294
|
+
/** 五条远端腿的答者:四个成员各一答,每一答 = {@link BOUND_READ_SCRIPT} 的一次往返(`createShellRebindingAnswers` 返回活对象 ⇒ `create*`)。 */
|
|
295
|
+
export declare function createShellRebindingAnswers(abs: string, lane: BoundReadLane): RebindingAnswers;
|
|
296
|
+
/**
|
|
297
|
+
* 逐答重绑的 `BoundFileObject`(活对象 ⇒ `create*`):每个成员一答;`close()` 不发往返(远端什么都不持有)。
|
|
298
|
+
* 工厂自己的三道闸(不信任答者):读的不是文件 / 列的不是目录 ⇒ 不发往返、照 core 句拒;`max` 不是非负安全整数 ⇒ `invalid`;
|
|
299
|
+
* 答回的字节比 `max` 多 ⇒ `invalid`(二道闸,同 {@link symlinkChainHopsFromStdout} 的「再切一次」);条目名不是单段 ⇒ 整答拒,条目锚在 `abs` 之下(执行缝路径律)。
|
|
300
|
+
*/
|
|
301
|
+
export declare function createRebindingBoundFile(abs: string, bound: BoundPlacement, answers: RebindingAnswers): BoundFileObject;
|
|
302
|
+
/** `openBound` 的整只半场(开答 → 工厂)。答者给的身份与类型就是工厂之后每一答对拍的凭据。 */
|
|
303
|
+
export declare function openRebinding(abs: string, answers: RebindingAnswers, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
|
|
304
|
+
/** 远端腿 `openBound` 的一行调用(经基座的腿与 e2b 直调;本版 ssh 缓装、不暴露):同一只脚本、同一只读法、同一只工厂。 */
|
|
305
|
+
export declare function openBoundOverShell(abs: string, lane: BoundReadLane, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
|
|
209
306
|
/**
|
|
210
307
|
* 脚本删一只私有目录没删掉时写到 **stdout** 的那一条记录的头:`<头> <目录路径>\0`(NUL 收尾 —— 路径里可以有换行)。
|
|
211
308
|
* 残留记录走 stdout、错误走 stderr:两条通道不混,分类器读到的 stderr 里没有路径文本。记录不改脚本的退出码:写照它自己的结局答,残留另行留痕。
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ExecutionError, FileError, SYMLINK_CHAIN_MAX_HOPS } from "@sema-agent/core";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import { anchorUnder } from "./exec-path-law.js";
|
|
4
|
+
import { isCanonicalBase64 } from "../device-ws-protocol.js";
|
|
2
5
|
import { NAME_MAX_BYTES, classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
6
|
+
import { echoCallerText } from "../caller-text-echo.js";
|
|
3
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
4
8
|
import { createLogger } from "../observability/logger.js";
|
|
5
9
|
import { seatText } from "../trace/exit-seat.js";
|
|
@@ -156,6 +160,220 @@ export async function runSymlinkChain(absPath, run) {
|
|
|
156
160
|
return { ok: false, error: classifyFsStderr(r.value.stderr, absPath, "unknown") };
|
|
157
161
|
return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout, absPath) } };
|
|
158
162
|
}
|
|
163
|
+
export const BOUND_READ_EXITS = { link: 90, moved: 91, kind: 92, over: 93, platform: 94, name: 95 };
|
|
164
|
+
const BOUND_READ_TIMEOUT_EXITS = [124, 137, 143];
|
|
165
|
+
export const BOUND_READ_SCRIPT = [
|
|
166
|
+
"f=$1; op=$2; want=$3; wk=$4; max=$5; enc=$6; LC_ALL=C; export LC_ALL; nl=$(printf '\\n.'); nl=${nl%.}",
|
|
167
|
+
`[ -d /proc/self/fd ] || exit ${BOUND_READ_EXITS.platform}`,
|
|
168
|
+
`for t in readlink stat head; do command -v "$t" >/dev/null 2>&1 || exit ${BOUND_READ_EXITS.platform}; done`,
|
|
169
|
+
`if [ "$enc" = base64 ]; then command -v base64 >/dev/null 2>&1 || exit ${BOUND_READ_EXITS.platform}; fi`,
|
|
170
|
+
`if [ -L "$f" ]; then exit ${BOUND_READ_EXITS.link}; fi`,
|
|
171
|
+
`if [ -e "$f" ]; then if [ -f "$f" ] || [ -d "$f" ]; then :; else exit ${BOUND_READ_EXITS.kind}; fi; fi`,
|
|
172
|
+
"{",
|
|
173
|
+
` p=$(readlink /proc/self/fd/0 && printf .) || exit ${BOUND_READ_EXITS.platform}`,
|
|
174
|
+
' p=${p%.}; p=${p%"$nl"}',
|
|
175
|
+
` s=$(stat -L -c '%d:%i %s %F' /proc/self/fd/0) || exit ${BOUND_READ_EXITS.platform}`,
|
|
176
|
+
` [ "$p" = "$f" ] || exit ${BOUND_READ_EXITS.moved}`,
|
|
177
|
+
' id=${s%% *}; r=${s#* }; sz=${r%% *}; t=${r#* }',
|
|
178
|
+
` case $t in "regular file"|"regular empty file") k=file ;; directory) k=directory ;; *) exit ${BOUND_READ_EXITS.kind} ;; esac`,
|
|
179
|
+
` if [ "$op" = open ]; then :; elif [ "$id" = "$want" ] && [ "$k" = "$wk" ]; then :; else exit ${BOUND_READ_EXITS.moved}; fi`,
|
|
180
|
+
" case $op in",
|
|
181
|
+
` open) printf '%s %s\\n' "$id" "$k" ;;`,
|
|
182
|
+
` size) printf '%s\\n' "$sz" ;;`,
|
|
183
|
+
` readBytes) if [ "$sz" -le "$max" ]; then :; else printf '%s\\n' "$sz"; exit ${BOUND_READ_EXITS.over}; fi`,
|
|
184
|
+
' n=$sz; [ "$sz" -gt 0 ] || n=$((max + 1))',
|
|
185
|
+
` printf '%s\\n' "$sz"`,
|
|
186
|
+
' if [ "$enc" = base64 ]; then { rc=$( { { head -c "$n"; echo "$?" >&4; } | base64 >&3; } 4>&1 ); } 3>&1; [ "$rc" = 0 ] || exit 1; else head -c "$n"; fi ;;',
|
|
187
|
+
" list) cd -P /proc/self/fd/0 || exit 1",
|
|
188
|
+
" set --",
|
|
189
|
+
` for n in * .[!.]* ..?*; do case $n in *"$nl"*) exit ${BOUND_READ_EXITS.name} ;; esac; if [ -e "$n" ] || [ -L "$n" ]; then set -- "$@" "$n"; fi; done`,
|
|
190
|
+
` if [ "$#" -gt 0 ]; then stat -c '%f %s %Y %n' -- "$@"; fi ;;`,
|
|
191
|
+
" *) exit 1 ;;",
|
|
192
|
+
" esac",
|
|
193
|
+
" exit",
|
|
194
|
+
'} < "$f"',
|
|
195
|
+
"exit 1",
|
|
196
|
+
].join("\n");
|
|
197
|
+
export function buildBoundReadCommand(abs, op, args, payload, boundSec) {
|
|
198
|
+
const argv = [abs, op, args.bound?.identity ?? "-", args.bound?.kind ?? "-", String(args.max ?? 0), payload].map(shellQuote).join(" ");
|
|
199
|
+
return `{ command -v timeout >/dev/null 2>&1 || exit ${BOUND_READ_EXITS.platform}; exec timeout ${Math.max(1, Math.floor(boundSec))} sh -c ${shellQuote(BOUND_READ_SCRIPT)} sema-bound-read ${argv}; }`;
|
|
200
|
+
}
|
|
201
|
+
const BOUND_SMALL_ANSWER_BYTES = 4096;
|
|
202
|
+
const BOUND_LIST_ANSWER_BYTES = 16 * 1024 * 1024;
|
|
203
|
+
const BOUND_IDENTITY_RE = /^\d+:\d+$/;
|
|
204
|
+
const BOUND_DECIMAL_RE = /^\d+$/;
|
|
205
|
+
function overCapError(abs, max, bytes) {
|
|
206
|
+
return new FileError("invalid", `The held file is over the ${max}-byte read cap (${bytes} bytes or more); it was not read`, abs);
|
|
207
|
+
}
|
|
208
|
+
function malformedAnswer(abs, what) {
|
|
209
|
+
return new FileError("unknown", `the target's bound-read answer was malformed (${what}); nothing was read`, abs);
|
|
210
|
+
}
|
|
211
|
+
function boundReadRefusal(word, abs, max, stdout) {
|
|
212
|
+
switch (word) {
|
|
213
|
+
case "link":
|
|
214
|
+
return new FileError("precondition_failed", "The path is now a link, not the object that was checked", abs);
|
|
215
|
+
case "moved":
|
|
216
|
+
return new FileError("precondition_failed", "The path named another object than the one opened", abs);
|
|
217
|
+
case "kind":
|
|
218
|
+
return new FileError("invalid", "Neither a regular file nor a directory", abs);
|
|
219
|
+
case "over":
|
|
220
|
+
return overCapError(abs, max, BOUND_DECIMAL_RE.test(stdout.trim()) ? stdout.trim() : "more than the cap");
|
|
221
|
+
case "platform":
|
|
222
|
+
return new FileError("not_supported", "This target cannot bind a read: it gives no /proc/self/fd observation of an opened file (not Linux, or /proc is not mounted), or it lacks timeout / readlink / stat / head / base64", abs);
|
|
223
|
+
case "name":
|
|
224
|
+
return new FileError("invalid", "A name in this directory contains a newline, which this listing cannot carry unambiguously; the directory was not listed", abs);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function boundReadExitOf(code) {
|
|
228
|
+
for (const word of Object.keys(BOUND_READ_EXITS))
|
|
229
|
+
if (BOUND_READ_EXITS[word] === code)
|
|
230
|
+
return word;
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
function answerCap(op, max, payload) {
|
|
234
|
+
switch (op) {
|
|
235
|
+
case "open":
|
|
236
|
+
case "size":
|
|
237
|
+
return BOUND_SMALL_ANSWER_BYTES;
|
|
238
|
+
case "list":
|
|
239
|
+
return BOUND_LIST_ANSWER_BYTES;
|
|
240
|
+
case "readBytes": {
|
|
241
|
+
const body = payload === "base64" ? Math.ceil((max + 1) / 3) * 4 : max + 1;
|
|
242
|
+
return BOUND_SMALL_ANSWER_BYTES + body + Math.ceil(body / 76) + 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function runBoundOp(abs, op, args, lane, abortSignal) {
|
|
247
|
+
const max = args.max ?? 0;
|
|
248
|
+
const cap = answerCap(op, max, lane.payload);
|
|
249
|
+
const r = await lane.run(buildBoundReadCommand(abs, op, args, lane.payload, lane.boundSec), cap, abortSignal);
|
|
250
|
+
if (!r.ok)
|
|
251
|
+
return { ok: false, error: r.error.code === "timeout" ? new FileError("timeout", `the bound read ran past the lane's bound: ${r.error.message}`, abs, r.error) : fileErrorFromExec(r.error, abs) };
|
|
252
|
+
const { exitCode, stderr } = r.value;
|
|
253
|
+
const stdout = Buffer.from(r.value.stdout);
|
|
254
|
+
if (exitCode !== 0) {
|
|
255
|
+
const word = boundReadExitOf(exitCode);
|
|
256
|
+
if (word !== undefined)
|
|
257
|
+
return { ok: false, error: boundReadRefusal(word, abs, max, stdout.subarray(0, 64).toString("latin1")) };
|
|
258
|
+
if (BOUND_READ_TIMEOUT_EXITS.includes(exitCode))
|
|
259
|
+
return { ok: false, error: new FileError("timeout", `the bound read on the target ran past its ${lane.boundSec}s bound and was killed; nothing was read`, abs) };
|
|
260
|
+
return { ok: false, error: classifyFsStderr(stderr, abs, "unknown") };
|
|
261
|
+
}
|
|
262
|
+
if (stdout.length > cap)
|
|
263
|
+
return { ok: false, error: malformedAnswer(abs, `over the ${cap}-byte bound of a ${op} answer`) };
|
|
264
|
+
return { ok: true, value: stdout };
|
|
265
|
+
}
|
|
266
|
+
function bytesOfReadAnswer(abs, out, payload) {
|
|
267
|
+
const nl = out.indexOf(0x0a);
|
|
268
|
+
const head = nl < 0 ? "" : out.subarray(0, nl).toString("latin1");
|
|
269
|
+
if (!BOUND_DECIMAL_RE.test(head))
|
|
270
|
+
return { ok: false, error: malformedAnswer(abs, "no size line") };
|
|
271
|
+
const size = Number(head);
|
|
272
|
+
const rest = out.subarray(nl + 1);
|
|
273
|
+
let bytes;
|
|
274
|
+
if (payload === "base64") {
|
|
275
|
+
const text = rest.toString("latin1").replace(/\s+/g, "");
|
|
276
|
+
if (!isCanonicalBase64(text))
|
|
277
|
+
return { ok: false, error: malformedAnswer(abs, "the payload is not canonical base64") };
|
|
278
|
+
bytes = Buffer.from(text, "base64");
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
bytes = rest;
|
|
282
|
+
}
|
|
283
|
+
if (size > 0 && bytes.length !== size) {
|
|
284
|
+
return { ok: false, error: new FileError("unknown", `the file was ${size} bytes when the read started but ${bytes.length} arrived (it changed while it was read, or the transfer was cut); nothing was read`, abs) };
|
|
285
|
+
}
|
|
286
|
+
return { ok: true, value: new Uint8Array(bytes) };
|
|
287
|
+
}
|
|
288
|
+
function entriesOfListAnswer(abs, out) {
|
|
289
|
+
const entries = [];
|
|
290
|
+
for (const line of out.toString("utf8").split("\n")) {
|
|
291
|
+
if (line === "")
|
|
292
|
+
continue;
|
|
293
|
+
const m = /^([0-9a-fA-F]+) (\d+) (\d+) (.+)$/.exec(line);
|
|
294
|
+
if (m === null)
|
|
295
|
+
return { ok: false, error: malformedAnswer(abs, "a listing line is not `<mode> <size> <mtime> <name>`") };
|
|
296
|
+
const type = parseInt(m[1], 16) & 0o170000;
|
|
297
|
+
const kind = type === 0o100000 ? "file" : type === 0o040000 ? "directory" : type === 0o120000 ? "symlink" : undefined;
|
|
298
|
+
if (kind === undefined)
|
|
299
|
+
continue;
|
|
300
|
+
entries.push({ name: m[4], kind, size: Number(m[2]), mtimeMs: Number(m[3]) * 1000 });
|
|
301
|
+
}
|
|
302
|
+
return { ok: true, value: entries };
|
|
303
|
+
}
|
|
304
|
+
export function createShellRebindingAnswers(abs, lane) {
|
|
305
|
+
return {
|
|
306
|
+
async open(abortSignal) {
|
|
307
|
+
const r = await runBoundOp(abs, "open", {}, lane, abortSignal);
|
|
308
|
+
if (!r.ok)
|
|
309
|
+
return r;
|
|
310
|
+
const [identity, kind, ...more] = r.value.toString("latin1").trim().split(" ");
|
|
311
|
+
if (identity === undefined || !BOUND_IDENTITY_RE.test(identity) || (kind !== "file" && kind !== "directory") || more.length > 0)
|
|
312
|
+
return { ok: false, error: malformedAnswer(abs, "the open answer is not `<dev>:<inode> <file|directory>`") };
|
|
313
|
+
return { ok: true, value: { identity, kind } };
|
|
314
|
+
},
|
|
315
|
+
async size(bound) {
|
|
316
|
+
const r = await runBoundOp(abs, "size", { bound }, lane);
|
|
317
|
+
if (!r.ok)
|
|
318
|
+
return r;
|
|
319
|
+
const text = r.value.toString("latin1").trim();
|
|
320
|
+
if (!BOUND_DECIMAL_RE.test(text))
|
|
321
|
+
return { ok: false, error: malformedAnswer(abs, "the size answer is not a decimal") };
|
|
322
|
+
return { ok: true, value: Number(text) };
|
|
323
|
+
},
|
|
324
|
+
async readBytes(bound, max, abortSignal) {
|
|
325
|
+
const r = await runBoundOp(abs, "readBytes", { bound, max }, lane, abortSignal);
|
|
326
|
+
return r.ok ? bytesOfReadAnswer(abs, r.value, lane.payload) : r;
|
|
327
|
+
},
|
|
328
|
+
async list(bound, abortSignal) {
|
|
329
|
+
const r = await runBoundOp(abs, "list", { bound }, lane, abortSignal);
|
|
330
|
+
return r.ok ? entriesOfListAnswer(abs, r.value) : r;
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export function createRebindingBoundFile(abs, bound, answers) {
|
|
335
|
+
return {
|
|
336
|
+
identity: bound.identity,
|
|
337
|
+
kind: bound.kind,
|
|
338
|
+
size: () => answers.size(bound),
|
|
339
|
+
async readBytes(max, abortSignal) {
|
|
340
|
+
if (bound.kind !== "file")
|
|
341
|
+
return { ok: false, error: new FileError("is_directory", "The held object is a directory", abs) };
|
|
342
|
+
if (!Number.isSafeInteger(max) || max < 0)
|
|
343
|
+
return { ok: false, error: new FileError("invalid", `the read cap must be a non-negative integer (got ${String(max)})`, abs) };
|
|
344
|
+
const r = await answers.readBytes(bound, max, abortSignal);
|
|
345
|
+
if (!r.ok)
|
|
346
|
+
return r;
|
|
347
|
+
if (r.value.length > max)
|
|
348
|
+
return { ok: false, error: overCapError(abs, max, r.value.length) };
|
|
349
|
+
return r;
|
|
350
|
+
},
|
|
351
|
+
async list(abortSignal) {
|
|
352
|
+
if (bound.kind !== "directory")
|
|
353
|
+
return { ok: false, error: new FileError("not_directory", "The held object is a file", abs) };
|
|
354
|
+
const r = await answers.list(bound, abortSignal);
|
|
355
|
+
if (!r.ok)
|
|
356
|
+
return r;
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const e of r.value) {
|
|
359
|
+
if (e.name === "" || e.name === "." || e.name === ".." || e.name.includes("/"))
|
|
360
|
+
return { ok: false, error: malformedAnswer(abs, `a listed name is not one path segment: ${JSON.stringify(echoCallerText(e.name, { maxChars: 80 }))}`) };
|
|
361
|
+
out.push({ ...e, path: anchorUnder(posix, abs, e.name) });
|
|
362
|
+
}
|
|
363
|
+
return { ok: true, value: out };
|
|
364
|
+
},
|
|
365
|
+
close: async () => undefined,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export async function openRebinding(abs, answers, abortSignal) {
|
|
369
|
+
const opened = await answers.open(abortSignal);
|
|
370
|
+
if (!opened.ok)
|
|
371
|
+
return opened;
|
|
372
|
+
return { ok: true, value: createRebindingBoundFile(abs, opened.value, answers) };
|
|
373
|
+
}
|
|
374
|
+
export function openBoundOverShell(abs, lane, abortSignal) {
|
|
375
|
+
return openRebinding(abs, createShellRebindingAnswers(abs, lane), abortSignal);
|
|
376
|
+
}
|
|
159
377
|
const STAGED_FILE = "data";
|
|
160
378
|
const STAGE_DIR_REFUSED_EXIT = 3;
|
|
161
379
|
const STAGE_DIR_UNSEALED_EXIT = 4;
|