@sema-agent/server 7.135.0 → 7.137.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 +237 -2
- package/USAGE.md +146 -41
- package/deploy/sema-up/README.md +15 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
- package/dist/approval-card.d.ts +60 -4
- package/dist/approval-card.js +18 -1
- package/dist/approval-content-kind.d.ts +7 -0
- package/dist/approval-content-kind.js +8 -1
- package/dist/approval-gate-currency.d.ts +48 -6
- package/dist/approval-gate-currency.js +16 -9
- package/dist/approval-hmac.d.ts +13 -1
- package/dist/approval-hmac.js +1 -1
- package/dist/approval.js +3 -3
- package/dist/auth-bridge.d.ts +34 -5
- package/dist/auth-bridge.js +22 -5
- package/dist/boot/budget-tracing.d.ts +1 -2
- package/dist/boot/budget-tracing.js +2 -2
- package/dist/boot/execution-env.d.ts +24 -1
- package/dist/boot/execution-env.js +69 -6
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +15 -1
- package/dist/boot/resolve-spec.d.ts +4 -0
- package/dist/boot/resolve-spec.js +24 -22
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-04-stores.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +4 -2
- package/dist/boot/stage-05-execution-env.js +11 -2
- package/dist/boot/stage-06-runners.d.ts +4 -2
- package/dist/boot/stage-06-runners.js +5 -1
- package/dist/boot/stage-07-capability-layer.d.ts +5 -2
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +5 -2
- package/dist/boot/stage-08-reapers.js +2 -1
- package/dist/boot/stage-09-leader.d.ts +5 -2
- package/dist/boot/stage-10-http-server.d.ts +5 -2
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +2 -3
- package/dist/boot/web-admin.js +10 -17
- package/dist/brain.js +12 -3
- package/dist/broker/broker.d.ts +89 -0
- package/dist/broker/broker.js +287 -0
- package/dist/broker/constants.d.ts +39 -0
- package/dist/broker/constants.js +15 -0
- package/dist/broker/http1.d.ts +111 -0
- package/dist/broker/http1.js +256 -0
- package/dist/broker/lease-book.d.ts +87 -0
- package/dist/broker/lease-book.js +142 -0
- package/dist/broker/lease-file.d.ts +60 -0
- package/dist/broker/lease-file.js +127 -0
- package/dist/broker/main.d.ts +2 -0
- package/dist/broker/main.js +20 -0
- package/dist/budget.d.ts +3 -3
- package/dist/budget.js +4 -0
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/engine-capabilities.js +1 -0
- package/dist/capabilities/mcp-probe.d.ts +5 -4
- package/dist/capabilities/scenarios.d.ts +23 -8
- package/dist/capabilities/scenarios.js +13 -8
- package/dist/capabilities/skills.d.ts +28 -5
- package/dist/capabilities/skills.js +8 -0
- package/dist/capability-facts.d.ts +3 -7
- package/dist/capability-facts.js +3 -5
- package/dist/center-credential-renewer.js +5 -1
- package/dist/center-credential.d.ts +18 -4
- package/dist/center-credential.js +18 -3
- package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
- package/dist/checkpoint-ctx-seal-m28.js +1 -1
- package/dist/checkpoint-identity.d.ts +3 -3
- package/dist/config-catalog.js +21 -16
- package/dist/config-d-migration.js +1 -1
- package/dist/config-invariants.d.ts +1 -1
- package/dist/config-invariants.js +8 -0
- package/dist/config-lkg.d.ts +0 -38
- package/dist/config-lkg.js +1 -83
- package/dist/config-types.d.ts +21 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +60 -16
- package/dist/deployment-governance.d.ts +2 -2
- package/dist/deployment-governance.js +7 -9
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/elicitation.d.ts +8 -7
- package/dist/elicitation.js +9 -8
- package/dist/file-history-disclosure.d.ts +8 -4
- package/dist/file-history-disclosure.js +24 -6
- package/dist/fleet-client.d.ts +32 -1
- package/dist/fleet-client.js +24 -4
- package/dist/fleet-lease.d.ts +7 -3
- package/dist/fleet-lease.js +31 -23
- package/dist/hooks/classic-stations.d.ts +11 -54
- package/dist/hooks/hook-llm.js +14 -8
- package/dist/hooks/hook-runner.d.ts +4 -4
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/admission.js +10 -7
- package/dist/http/approval-sweeps.js +4 -3
- package/dist/http/caller-trust.d.ts +2 -1
- package/dist/http/caller-trust.js +1 -3
- package/dist/http/checkpoint-ctx.d.ts +76 -16
- package/dist/http/checkpoint-ctx.js +45 -1
- package/dist/http/dispatch.js +24 -19
- package/dist/http/durable-run-leg.d.ts +5 -1
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/health-self-report.d.ts +2 -2
- package/dist/http/mcp-remote-text.d.ts +5 -0
- package/dist/http/mcp-remote-text.js +7 -0
- package/dist/http/principal-gate.d.ts +15 -10
- package/dist/http/principal-gate.js +48 -9
- package/dist/http/resume-ctx-read.d.ts +2 -16
- package/dist/http/resume-ctx-read.js +2 -3
- package/dist/http/resume-legs.d.ts +2 -20
- package/dist/http/resume-legs.js +52 -38
- package/dist/http/route-ctx.d.ts +8 -2
- package/dist/http/routes/approvals-assistant.d.ts +0 -2
- package/dist/http/routes/approvals-assistant.js +65 -16
- package/dist/http/routes/capabilities.js +11 -1
- package/dist/http/routes/mcp-reconnect.d.ts +1 -1
- package/dist/http/routes/mcp-reconnect.js +5 -7
- package/dist/http/routes/runs.js +4 -3
- package/dist/http/routes/session-sync.js +70 -12
- package/dist/http/routes/sessions.js +20 -15
- package/dist/http/routes/tasks.js +8 -6
- package/dist/http/server.d.ts +4 -0
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +6 -1
- package/dist/http/submit-ctx.js +1 -1
- package/dist/http/wire-types.d.ts +57 -30
- package/dist/identity-source.d.ts +65 -0
- package/dist/identity-source.js +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/memory-store-refusal.d.ts +3 -3
- package/dist/model-entry-refusal.d.ts +2 -0
- package/dist/model-entry-refusal.js +11 -0
- package/dist/model-route-gap.d.ts +49 -4
- package/dist/model-route-gap.js +34 -1
- package/dist/model-select.d.ts +6 -17
- package/dist/model-select.js +0 -3
- package/dist/namespaced-rule-name.d.ts +1 -1
- package/dist/object-store-buckets.d.ts +13 -0
- package/dist/object-store-buckets.js +3 -0
- package/dist/observability/fail-open.d.ts +19 -7
- package/dist/observability/fail-open.js +19 -7
- package/dist/observability/metrics.js +2 -2
- package/dist/observability/principal-context.d.ts +4 -1
- package/dist/observability/principal-context.js +6 -1
- package/dist/observability/run-terminal.js +1 -0
- package/dist/operator-ask.d.ts +2 -2
- package/dist/org-delivery-gate.js +1 -1
- package/dist/org-memory-admission.d.ts +2 -2
- package/dist/org-memory-admission.js +39 -2
- package/dist/parked-decide.d.ts +4 -3
- package/dist/parked-decide.js +29 -9
- package/dist/peer-directory.d.ts +2 -1
- package/dist/peer-directory.js +7 -5
- package/dist/permission-rule-list.d.ts +2 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
- package/dist/plugins/approval-ask-store-sql.js +3 -0
- package/dist/plugins/blob-backend.js +2 -1
- package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
- package/dist/plugins/checkpoint-index-columns.js +28 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +46 -12
- package/dist/plugins/checkpoint-store-sql.js +64 -35
- package/dist/plugins/file-history-store-sql.d.ts +6 -0
- package/dist/plugins/file-history-store-sql.js +37 -37
- package/dist/plugins/file-run-store.js +8 -2
- package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
- package/dist/plugins/k8s-exec-protocol.js +116 -28
- package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
- package/dist/plugins/k8s-pod-delete-failure.js +17 -0
- package/dist/plugins/k8s-pod-reaper.d.ts +112 -0
- package/dist/plugins/k8s-pod-reaper.js +122 -0
- package/dist/plugins/local-checkpoint-store.d.ts +3 -6
- package/dist/plugins/local-checkpoint-store.js +7 -6
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/one-time-migrations.d.ts +49 -37
- package/dist/plugins/one-time-migrations.js +87 -128
- package/dist/plugins/permission-rule-store-file.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
- package/dist/plugins/permission-rule-store-sql.js +16 -17
- package/dist/plugins/pg-pool.js +4 -0
- package/dist/plugins/pg-session-storage.js +2 -2
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +4 -0
- package/dist/plugins/remote-env-e2b.d.ts +3 -0
- package/dist/plugins/remote-env-e2b.js +1 -0
- package/dist/plugins/remote-env-file-error.d.ts +8 -1
- package/dist/plugins/remote-env-file-error.js +7 -1
- package/dist/plugins/remote-env-host.d.ts +7 -1
- package/dist/plugins/remote-env-host.js +7 -1
- package/dist/plugins/remote-env-k8s.d.ts +154 -14
- package/dist/plugins/remote-env-k8s.js +300 -21
- package/dist/plugins/remote-env-local-docker.d.ts +3 -0
- package/dist/plugins/remote-env-local-docker.js +1 -0
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/run-store-sql.js +10 -4
- package/dist/plugins/s3-answer.d.ts +15 -0
- package/dist/plugins/s3-answer.js +5 -0
- package/dist/plugins/session-ownership.d.ts +17 -3
- package/dist/plugins/session-ownership.js +19 -5
- package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
- package/dist/plugins/snapshot-bucket-presence.js +35 -7
- package/dist/plugins/sql-errors.d.ts +60 -0
- package/dist/plugins/sql-errors.js +64 -1
- package/dist/plugins/store-contracts.d.ts +10 -1
- package/dist/plugins/store-fault.d.ts +4 -0
- package/dist/plugins/store-fault.js +10 -0
- package/dist/plugins/tidb-pool.js +8 -1
- package/dist/plugins/tidb-session-store.js +2 -2
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/plugins/web-search.js +2 -2
- package/dist/request-declaration-refusal.d.ts +94 -0
- package/dist/request-declaration-refusal.js +78 -0
- package/dist/request-image-urls.d.ts +6 -0
- package/dist/request-image-urls.js +27 -0
- package/dist/request-key-closure.d.ts +5 -0
- package/dist/request-key-closure.js +5 -2
- package/dist/request-value-closure.d.ts +31 -2
- package/dist/request-value-closure.js +7 -1
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +2 -2
- package/dist/rules-consent.js +7 -4
- package/dist/run-local.d.ts +2 -2
- package/dist/runs.js +2 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/runtime-governance.d.ts +5 -5
- package/dist/sandbox-git-lease.d.ts +49 -0
- package/dist/sandbox-git-lease.js +60 -0
- package/dist/sandbox-git-plane.d.ts +84 -0
- package/dist/sandbox-git-plane.js +105 -0
- package/dist/sandbox-pkg-source.d.ts +28 -1
- package/dist/sandbox-pkg-source.js +1 -2
- package/dist/sealed-key.d.ts +19 -0
- package/dist/sealed-key.js +47 -4
- package/dist/security.d.ts +16 -4
- package/dist/security.js +8 -3
- package/dist/server-secret-env.d.ts +6 -0
- package/dist/server-secret-env.js +14 -0
- package/dist/session-error-wire.d.ts +14 -6
- package/dist/session-error-wire.js +2 -1
- package/dist/session-incarnation.d.ts +16 -5
- package/dist/session-incarnation.js +7 -4
- package/dist/session-policy-wire.d.ts +1 -1
- package/dist/session-policy-wire.js +2 -2
- package/dist/session-row.d.ts +3 -17
- package/dist/session-row.js +4 -0
- package/dist/session-sync.d.ts +15 -3
- package/dist/session-sync.js +5 -2
- package/dist/spec-fields.d.ts +43 -13
- package/dist/spec-fields.js +33 -12
- package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
- package/dist/stored-request-prompt-profile-m30.js +25 -0
- package/dist/task-a2a.d.ts +52 -81
- package/dist/task-a2a.js +219 -89
- package/dist/task-mcp.d.ts +7 -15
- package/dist/task-mcp.js +16 -51
- package/dist/task-settings.d.ts +2 -2
- package/dist/tool-approval.d.ts +35 -12
- package/dist/tool-approval.js +59 -39
- package/dist/tool-rule-names.d.ts +5 -0
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.js +37 -13
- package/dist/trace/ledger-sink.d.ts +4 -5
- package/dist/trace/ledger-sink.js +3 -7
- package/dist/trace/project.d.ts +37 -3
- package/dist/trace/project.js +20 -6
- package/dist/trace/redact.d.ts +40 -12
- package/dist/trace/redact.js +332 -128
- package/dist/trace/self-minted-wire.d.ts +641 -0
- package/dist/trace/self-minted-wire.js +201 -0
- package/dist/trace/wire-projection-faces.d.ts +5 -3
- package/dist/trace/wire-projection-faces.js +7 -3
- package/dist/url-credential-refusal.d.ts +52 -0
- package/dist/url-credential-refusal.js +14 -0
- package/dist/url-refusal.d.ts +13 -4
- package/dist/url-refusal.js +8 -2
- package/dist/web-admin-client/apply-effective.js +0 -1
- package/dist/web-admin-client/center-request.d.ts +20 -2
- package/dist/web-admin-client/center-request.js +40 -5
- package/dist/web-admin-client/http-client.js +6 -6
- package/dist/web-admin-client/types.d.ts +5 -5
- package/dist/workspace-space.d.ts +27 -0
- package/dist/workspace-space.js +26 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
- package/package.json +4 -3
- package/scripts/drain-check-local.mjs +2 -1
- package/scripts/drain-check.mysql.sql +3 -1
- package/scripts/drain-check.pg.sql +3 -1
- package/skills/schedule.md +1 -1
- package/wire-changes/server@7.104.0.json +134 -0
- package/wire-changes/server@7.105.0.json +53 -0
- package/wire-changes/server@7.106.0.json +475 -0
- package/wire-changes/server@7.107.0.json +64 -0
- package/wire-changes/server@7.108.0.json +153 -0
- package/wire-changes/server@7.109.0.json +46 -0
- package/wire-changes/server@7.110.0.json +90 -0
- package/wire-changes/server@7.111.0.json +86 -0
- package/wire-changes/server@7.112.0.json +60 -0
- package/wire-changes/server@7.113.0.json +26 -0
- package/wire-changes/server@7.114.0.json +26 -0
- package/wire-changes/server@7.115.0.json +97 -0
- package/wire-changes/server@7.116.0.json +218 -0
- package/wire-changes/server@7.116.1.json +26 -0
- package/wire-changes/server@7.117.0.json +65 -0
- package/wire-changes/server@7.118.0.json +129 -0
- package/wire-changes/server@7.119.0.json +65 -0
- package/wire-changes/server@7.120.0.json +89 -0
- package/wire-changes/server@7.121.0.json +219 -0
- package/wire-changes/server@7.122.0.json +54 -0
- package/wire-changes/server@7.123.0.json +26 -0
- package/wire-changes/server@7.124.0.json +336 -0
- package/wire-changes/server@7.124.1.json +26 -0
- package/wire-changes/server@7.125.0.json +111 -0
- package/wire-changes/server@7.126.0.json +285 -0
- package/wire-changes/server@7.127.0.json +77 -0
- package/wire-changes/server@7.127.1.json +26 -0
- package/wire-changes/server@7.127.2.json +53 -0
- package/wire-changes/server@7.128.0.json +26 -0
- package/wire-changes/server@7.129.0.json +106 -0
- package/wire-changes/server@7.129.1.json +26 -0
- package/wire-changes/server@7.130.0.json +26 -0
- package/wire-changes/server@7.131.0.json +118 -0
- package/wire-changes/server@7.132.0.json +226 -0
- package/wire-changes/server@7.133.0.json +1155 -0
- package/wire-changes/server@7.134.0.json +107 -0
- package/wire-changes/server@7.135.0.json +174 -0
- package/wire-changes/server@7.136.0.json +448 -0
- package/wire-changes/server@7.137.0.json +825 -0
- package/dist/url-credential.d.ts +0 -52
- package/dist/url-credential.js +0 -49
|
@@ -6,11 +6,14 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
6
6
|
import WebSocket from "ws";
|
|
7
7
|
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
|
|
8
8
|
import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
|
|
9
|
-
import {
|
|
9
|
+
import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
|
|
10
|
+
import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
|
|
11
|
+
import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_PREFIX_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotKeyPrefixProblem, snapshotNameOwnedBy } from "../workspace-space.js";
|
|
10
12
|
import { spaceCommitAlive } from "../workspace-space-commit.js";
|
|
11
13
|
import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
|
|
12
14
|
import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
|
|
13
15
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
16
|
+
import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failure.js";
|
|
14
17
|
import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
|
|
15
18
|
import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
|
|
16
19
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
@@ -37,9 +40,12 @@ const unsupported = (op) => ({
|
|
|
37
40
|
const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
|
|
38
41
|
const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
|
|
39
42
|
const WRITE_CHUNK_BYTES = 64 * 1024;
|
|
40
|
-
import { applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
43
|
+
import { applyPodSpecPatch, ADAPTER_VOLUME_NAMES, SANDBOX_POD_LABELS, SANDBOX_RUN_END_HOLD_ANNOTATION, SANDBOX_RUN_LABEL, SANDBOX_SECRET_SEAT_VERBS, isLabelValue } from "./k8s-exec-protocol.js";
|
|
41
44
|
export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
42
|
-
|
|
45
|
+
import { sandboxRunEndHoldMs } from "./k8s-pod-reaper.js";
|
|
46
|
+
import { BROKER_COMMAND, BROKER_CONTAINER_NAME, BROKER_LEASE_DIR, BROKER_LEASE_KEY, BROKER_PROBE_ARG, BROKER_VOLUME_NAME } from "../broker/constants.js";
|
|
47
|
+
import { buildSandboxLeaseFile, placeholderLeaseEntries, sandboxGitRouteEnv } from "../sandbox-git-lease.js";
|
|
48
|
+
const CACHE_VOLUME = ADAPTER_VOLUME_NAMES.cache;
|
|
43
49
|
function occupantKeyOf(occupant) {
|
|
44
50
|
if (occupant === undefined)
|
|
45
51
|
return { state: "unavailable", reason: "incarnation_unknown" };
|
|
@@ -166,12 +172,39 @@ export function errorMessageOf(e) {
|
|
|
166
172
|
}
|
|
167
173
|
export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
|
|
168
174
|
export const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
|
|
169
|
-
const
|
|
175
|
+
const SANDBOX_SECRET_NAME_PREFIX = "sbx-env-";
|
|
176
|
+
const SANDBOX_SECRET_LABEL = "sema.io/sandbox-secret";
|
|
177
|
+
const secretEnvKey = (name) => `env.${name}`;
|
|
178
|
+
const SANDBOX_LEASE_NAME_PREFIX = "sbx-lease-";
|
|
179
|
+
const SANDBOX_LEASE_LABEL = "sema.io/sandbox-lease";
|
|
180
|
+
function brokerContainer(image) {
|
|
181
|
+
return {
|
|
182
|
+
name: BROKER_CONTAINER_NAME,
|
|
183
|
+
image,
|
|
184
|
+
command: [...BROKER_COMMAND],
|
|
185
|
+
env: [{ name: "BUN_RUNTIME_TRANSPILER_CACHE_PATH", value: "0" }],
|
|
186
|
+
securityContext: {
|
|
187
|
+
runAsUser: 0,
|
|
188
|
+
runAsGroup: 0,
|
|
189
|
+
runAsNonRoot: false,
|
|
190
|
+
privileged: false,
|
|
191
|
+
allowPrivilegeEscalation: false,
|
|
192
|
+
readOnlyRootFilesystem: true,
|
|
193
|
+
capabilities: { drop: ["ALL"] },
|
|
194
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
195
|
+
procMount: "Default",
|
|
196
|
+
},
|
|
197
|
+
volumeMounts: [{ name: BROKER_VOLUME_NAME, mountPath: BROKER_LEASE_DIR, readOnly: true }],
|
|
198
|
+
readinessProbe: { exec: { command: [...BROKER_COMMAND, BROKER_PROBE_ARG] }, periodSeconds: 2, timeoutSeconds: 5 },
|
|
199
|
+
resources: { limits: { cpu: "250m", memory: "256Mi" } },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
170
202
|
export class RemoteK8sExecutionEnv {
|
|
171
203
|
capabilities;
|
|
172
204
|
cwd;
|
|
173
205
|
podName;
|
|
174
206
|
podIP;
|
|
207
|
+
leaseRef;
|
|
175
208
|
destroyed = false;
|
|
176
209
|
handleSnapshotId;
|
|
177
210
|
inFlight = new Set();
|
|
@@ -183,6 +216,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
183
216
|
}
|
|
184
217
|
homeSeat;
|
|
185
218
|
cdPath = "unknown";
|
|
219
|
+
withheldEnv = [];
|
|
186
220
|
cfg;
|
|
187
221
|
deps;
|
|
188
222
|
occupantIncarnation;
|
|
@@ -218,6 +252,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
218
252
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
219
253
|
...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
|
|
220
254
|
...(config.podEnv ? { podEnv: config.podEnv } : {}),
|
|
255
|
+
...(config.podSecretEnv && Object.keys(config.podSecretEnv).length > 0 ? { podSecretEnv: config.podSecretEnv } : {}),
|
|
256
|
+
...(config.runId !== undefined && config.runId !== "" && isLabelValue(config.runId) ? { runId: config.runId } : {}),
|
|
257
|
+
...(config.sandboxBroker !== undefined ? { sandboxBroker: config.sandboxBroker } : {}),
|
|
221
258
|
...(config.snapshotExclude && config.snapshotExclude.length > 0 ? { snapshotExclude: config.snapshotExclude } : {}),
|
|
222
259
|
};
|
|
223
260
|
this.cwd = this.cfg.mountPath;
|
|
@@ -241,6 +278,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
241
278
|
supportsDetach: true,
|
|
242
279
|
};
|
|
243
280
|
}
|
|
281
|
+
get sandboxLeaseRef() {
|
|
282
|
+
return this.leaseRef !== undefined && this.leaseRef.podName === this.podName ? this.leaseRef : undefined;
|
|
283
|
+
}
|
|
244
284
|
get lifetimeMs() {
|
|
245
285
|
return this.cfg.timeoutMs;
|
|
246
286
|
}
|
|
@@ -291,7 +331,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
291
331
|
headers: {
|
|
292
332
|
authorization: `Bearer ${token}`,
|
|
293
333
|
accept: "application/json",
|
|
294
|
-
...(payload ? { "content-type": "application/json", "content-length": Buffer.byteLength(payload) } : {}),
|
|
334
|
+
...(payload ? { "content-type": opts?.contentType ?? "application/json", "content-length": Buffer.byteLength(payload) } : {}),
|
|
295
335
|
},
|
|
296
336
|
...(isHttps ? { ca: ca ? [ca] : undefined, rejectUnauthorized: !this.cfg.insecureTls } : {}),
|
|
297
337
|
timeout: opts?.timeoutMs ?? this.cfg.rpcTimeoutMs,
|
|
@@ -507,7 +547,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
507
547
|
const claim = {
|
|
508
548
|
apiVersion: "v1",
|
|
509
549
|
kind: "PersistentVolumeClaim",
|
|
510
|
-
metadata: { name: cache.claim.name, labels: { [WORKSPACE_CACHE_SPACE_LABEL]: cache.claim.label, "managed-by": "
|
|
550
|
+
metadata: { name: cache.claim.name, labels: { [WORKSPACE_CACHE_SPACE_LABEL]: cache.claim.label, "managed-by": SANDBOX_POD_LABELS["managed-by"] } },
|
|
511
551
|
spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: cache.size } } },
|
|
512
552
|
};
|
|
513
553
|
const made = await this.request("POST", base, claim, { signal });
|
|
@@ -560,6 +600,38 @@ export class RemoteK8sExecutionEnv {
|
|
|
560
600
|
applyPodSpecPatch(probe, { labels: this.cfg.podSpecPatch.labels });
|
|
561
601
|
return sandboxEgressGaps({ nodes, policies, podLabels: probe.metadata.labels, ipv6: this.cfg.ipv6 });
|
|
562
602
|
}
|
|
603
|
+
async secretSeatReading(seat) {
|
|
604
|
+
const answers = await Promise.all(SANDBOX_SECRET_SEAT_VERBS[seat].map(async (verb) => {
|
|
605
|
+
try {
|
|
606
|
+
const r = await this.request("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", {
|
|
607
|
+
apiVersion: "authorization.k8s.io/v1",
|
|
608
|
+
kind: "SelfSubjectAccessReview",
|
|
609
|
+
spec: { resourceAttributes: { namespace: this.cfg.namespace, verb, resource: "secrets" } },
|
|
610
|
+
});
|
|
611
|
+
const allowed = r.json?.status?.allowed;
|
|
612
|
+
const answered = r.status >= 200 && r.status < 300;
|
|
613
|
+
return answered && typeof allowed === "boolean" ? { verb, allowed } : { verb, unasked: answered ? "answer without status.allowed" : `HTTP ${r.status}` };
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
return { verb, unasked: errorMessageOf(e) };
|
|
617
|
+
}
|
|
618
|
+
}));
|
|
619
|
+
const denied = answers.flatMap((a) => ("allowed" in a && !a.allowed ? [a.verb] : []));
|
|
620
|
+
const unasked = answers.flatMap((a) => ("unasked" in a ? [`${a.verb}: ${a.unasked}`] : []));
|
|
621
|
+
if (denied.length > 0)
|
|
622
|
+
return { state: "denied", verbs: denied };
|
|
623
|
+
if (unasked.length > 0)
|
|
624
|
+
return { state: "unverified", detail: unasked.join("; ") };
|
|
625
|
+
return { state: "granted" };
|
|
626
|
+
}
|
|
627
|
+
async listSandboxPods() {
|
|
628
|
+
const selector = encodeURIComponent(Object.entries(SANDBOX_POD_LABELS).map(([k, v]) => `${k}=${v}`).join(","));
|
|
629
|
+
const r = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods?labelSelector=${selector}`);
|
|
630
|
+
if (r.status < 200 || r.status >= 300)
|
|
631
|
+
throw new Error(`sandbox pod listing answered HTTP ${r.status}`);
|
|
632
|
+
const items = r.json?.items;
|
|
633
|
+
return (Array.isArray(items) ? items : []).flatMap((item) => sandboxPodListingOf(item) ?? []);
|
|
634
|
+
}
|
|
563
635
|
reclaimDeadlinePassed(what) {
|
|
564
636
|
return `reclaim deadline (${this.cfg.dataTimeoutMs} ms, the snapshot time bound, one per reclaim call) passed — ${what}`;
|
|
565
637
|
}
|
|
@@ -577,19 +649,36 @@ export class RemoteK8sExecutionEnv {
|
|
|
577
649
|
}
|
|
578
650
|
async createPod(signal, cache) {
|
|
579
651
|
const ns = this.cfg.namespace;
|
|
580
|
-
const
|
|
652
|
+
const secretEnv = this.cfg.podSecretEnv ?? {};
|
|
653
|
+
const secretName = Object.keys(secretEnv).length > 0 ? `${SANDBOX_SECRET_NAME_PREFIX}${randomUUID()}` : undefined;
|
|
654
|
+
const broker = this.cfg.sandboxBroker;
|
|
655
|
+
const leaseName = broker !== undefined ? `${SANDBOX_LEASE_NAME_PREFIX}${randomUUID()}` : undefined;
|
|
656
|
+
const placeholder = broker !== undefined ? buildSandboxLeaseFile(broker.origins, placeholderLeaseEntries(broker.origins)) : undefined;
|
|
657
|
+
if (placeholder !== undefined && !placeholder.ok) {
|
|
658
|
+
return this.holdRefusal(new RemoteExecutionError("connect_failed", `the member-credential broker cannot be set up for this pod: ${placeholder.problem}`));
|
|
659
|
+
}
|
|
660
|
+
const layered = { ...(cache ? workspaceCacheEnv(cache.mount) : {}), ...(this.cfg.podEnv ?? {}) };
|
|
661
|
+
const envEntries = [
|
|
662
|
+
...Object.entries({ ...layered, ...(broker !== undefined ? sandboxGitRouteEnv(broker.origins) : {}) })
|
|
663
|
+
.filter(([name]) => !Object.hasOwn(secretEnv, name))
|
|
664
|
+
.map(([name, value]) => ({ name, value: String(value) })),
|
|
665
|
+
...(secretName !== undefined ? Object.keys(secretEnv).map((name) => ({ name, valueFrom: { secretKeyRef: { name: secretName, key: secretEnvKey(name) } } })) : []),
|
|
666
|
+
];
|
|
667
|
+
const pkgSourceMarked = Boolean(this.cfg.podEnv?.SEMA_PKG_SOURCE || secretEnv.SEMA_PKG_SOURCE);
|
|
581
668
|
const sysctls = sandboxPodSysctls(this.cfg.ipv6);
|
|
582
669
|
const pod = {
|
|
583
670
|
apiVersion: "v1",
|
|
584
671
|
kind: "Pod",
|
|
585
672
|
metadata: {
|
|
586
673
|
generateName: "sbx-",
|
|
587
|
-
labels: { ...SANDBOX_POD_LABELS },
|
|
674
|
+
labels: { ...SANDBOX_POD_LABELS, ...(this.cfg.runId !== undefined ? { [SANDBOX_RUN_LABEL]: this.cfg.runId } : {}) },
|
|
675
|
+
...(this.cfg.runId !== undefined ? { annotations: { [SANDBOX_RUN_END_HOLD_ANNOTATION]: String(sandboxRunEndHoldMs(this.cfg.dataTimeoutMs)) } } : {}),
|
|
588
676
|
},
|
|
589
677
|
spec: {
|
|
590
678
|
...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
|
|
591
679
|
...(sysctls ? { securityContext: { sysctls: sysctls.map((x) => ({ ...x })) } } : {}),
|
|
592
680
|
restartPolicy: "Never",
|
|
681
|
+
...(broker !== undefined ? { shareProcessNamespace: false } : {}),
|
|
593
682
|
automountServiceAccountToken: false,
|
|
594
683
|
activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
|
|
595
684
|
terminationGracePeriodSeconds: 1,
|
|
@@ -597,18 +686,30 @@ export class RemoteK8sExecutionEnv {
|
|
|
597
686
|
{
|
|
598
687
|
name: "sandbox",
|
|
599
688
|
image: this.cfg.image,
|
|
600
|
-
command:
|
|
689
|
+
command: pkgSourceMarked
|
|
601
690
|
? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
|
|
602
691
|
: ["sh", "-c", "sleep infinity"],
|
|
603
|
-
...(
|
|
692
|
+
...(envEntries.length > 0 ? { env: envEntries } : {}),
|
|
604
693
|
...(cache ? { volumeMounts: [{ name: CACHE_VOLUME, mountPath: cache.mount }] } : {}),
|
|
605
694
|
resources: { limits: { cpu: this.cfg.cpu, memory: this.cfg.memory } },
|
|
606
695
|
},
|
|
696
|
+
...(broker !== undefined ? [brokerContainer(broker.image)] : []),
|
|
607
697
|
],
|
|
608
|
-
...(
|
|
698
|
+
...((volumes) => (volumes.length > 0 ? { volumes } : {}))([
|
|
699
|
+
...(cache ? [{ name: CACHE_VOLUME, persistentVolumeClaim: { claimName: cache.claim.name } }] : []),
|
|
700
|
+
...(leaseName !== undefined ? [{ name: BROKER_VOLUME_NAME, secret: { secretName: leaseName, items: [{ key: BROKER_LEASE_KEY, path: BROKER_LEASE_KEY }], defaultMode: 0o400 } }] : []),
|
|
701
|
+
]),
|
|
609
702
|
},
|
|
610
703
|
};
|
|
611
|
-
if (this.cfg.podSpecPatch)
|
|
704
|
+
if (this.cfg.podSpecPatch && broker !== undefined) {
|
|
705
|
+
try {
|
|
706
|
+
applyPodSpecPatch(pod, this.cfg.podSpecPatch, { broker: true });
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
return this.holdRefusal(new RemoteExecutionError("connect_failed", `${errorMessageOf(e)} — no pod is created (the member-credential broker never shares a pod with a patch that could reach it)`));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else if (this.cfg.podSpecPatch)
|
|
612
713
|
applyPodSpecPatch(pod, this.cfg.podSpecPatch);
|
|
613
714
|
let created;
|
|
614
715
|
try {
|
|
@@ -626,6 +727,20 @@ export class RemoteK8sExecutionEnv {
|
|
|
626
727
|
const name = created.json?.metadata?.name;
|
|
627
728
|
if (!name)
|
|
628
729
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "pod create returned no name") };
|
|
730
|
+
if (secretName !== undefined) {
|
|
731
|
+
const sealed = await this.createPodSecret(secretName, name, created.json?.metadata?.uid, secretEnv, signal);
|
|
732
|
+
if (!sealed.ok) {
|
|
733
|
+
await this.discardRefusedPod(name, "env_secret_unsealed");
|
|
734
|
+
return sealed;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (leaseName !== undefined && placeholder?.ok === true) {
|
|
738
|
+
const leased = await this.createLeaseSecret(leaseName, name, created.json?.metadata?.uid, placeholder.text, signal);
|
|
739
|
+
if (!leased.ok) {
|
|
740
|
+
await this.discardRefusedPod(name, "lease_secret_unsealed");
|
|
741
|
+
return leased;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
629
744
|
const deadline = Date.now() + this.cfg.readyTimeoutMs;
|
|
630
745
|
let podIP;
|
|
631
746
|
for (;;) {
|
|
@@ -643,7 +758,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
643
758
|
}
|
|
644
759
|
const status = got.json?.status;
|
|
645
760
|
const phase = status?.phase;
|
|
646
|
-
|
|
761
|
+
const brokerStatus = status?.containerStatuses?.find((c) => c.name === BROKER_CONTAINER_NAME);
|
|
762
|
+
if (phase === "Running" && (broker === undefined || brokerStatus?.ready === true)) {
|
|
647
763
|
podIP = status?.podIP;
|
|
648
764
|
break;
|
|
649
765
|
}
|
|
@@ -651,11 +767,16 @@ export class RemoteK8sExecutionEnv {
|
|
|
651
767
|
await this.deletePod(name).catch(() => { });
|
|
652
768
|
return this.refusePod(name, phase, status?.reason, status?.message);
|
|
653
769
|
}
|
|
654
|
-
const waiting = status?.containerStatuses?.
|
|
770
|
+
const waiting = status?.containerStatuses?.find((c) => c.name !== BROKER_CONTAINER_NAME)?.state?.waiting;
|
|
655
771
|
if (waiting?.reason === "ImagePullBackOff" || waiting?.reason === "ErrImagePull") {
|
|
656
772
|
await this.deletePod(name).catch(() => { });
|
|
657
773
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `image pull failed for ${this.cfg.image}: ${waiting.message ?? waiting.reason}`) };
|
|
658
774
|
}
|
|
775
|
+
const brokerWaiting = brokerStatus?.state?.waiting;
|
|
776
|
+
if (broker !== undefined && (brokerWaiting?.reason === "ImagePullBackOff" || brokerWaiting?.reason === "ErrImagePull")) {
|
|
777
|
+
await this.discardRefusedPod(name, "broker_image_pull_failed");
|
|
778
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `image pull failed for the member-credential broker image ${broker.image} (K8S_SANDBOX_BROKER_IMAGE): ${brokerWaiting.message ?? brokerWaiting.reason}`) };
|
|
779
|
+
}
|
|
659
780
|
if (Date.now() > deadline) {
|
|
660
781
|
if (cache) {
|
|
661
782
|
const bound = await this.cacheClaimBound(cache, signal);
|
|
@@ -669,6 +790,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
669
790
|
: { cacheMiss: "claim_unbound", detail: `the claim ${cache.claim.name} was not bound within ${this.cfg.dataTimeoutMs}ms (no default StorageClass, or its provisioner is down)` };
|
|
670
791
|
}
|
|
671
792
|
await this.deletePod(name).catch(() => { });
|
|
793
|
+
if (phase === "Running" && broker !== undefined) {
|
|
794
|
+
return { ok: false, error: new RemoteExecutionError("timeout", `the member-credential broker container was not ready within ${this.cfg.readyTimeoutMs}ms (it reads its lease file and answers its own readiness probe; image ${broker.image})`) };
|
|
795
|
+
}
|
|
672
796
|
return { ok: false, error: new RemoteExecutionError("timeout", `pod not Running within ${this.cfg.readyTimeoutMs}ms (phase=${phase ?? "?"})`) };
|
|
673
797
|
}
|
|
674
798
|
await new Promise((r) => setTimeout(r, 1000));
|
|
@@ -679,6 +803,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
679
803
|
}
|
|
680
804
|
this.podName = name;
|
|
681
805
|
this.podIP = podIP;
|
|
806
|
+
this.leaseRef = leaseName !== undefined && broker !== undefined ? { namespace: ns, podName: name, leaseSecretName: leaseName, origins: [...broker.origins] } : undefined;
|
|
682
807
|
const askHome = this.homeSeat.asks;
|
|
683
808
|
const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}${askHome ? ` && { ${LANE_HOME_PROBE_COMMAND} || true; }` : ""}`);
|
|
684
809
|
if (!mk.ok || mk.value.exitCode !== 0) {
|
|
@@ -701,23 +826,110 @@ export class RemoteK8sExecutionEnv {
|
|
|
701
826
|
this.logger?.warn("workspace_cache_fresh", { space: incarnationLogPrefix(cache.spaceKey), claim: cache.claim.name, size: cache.size });
|
|
702
827
|
return ok(this.workspaceHandle());
|
|
703
828
|
}
|
|
829
|
+
async createPodSecret(secretName, podName, podUid, values, signal) {
|
|
830
|
+
const encoded = Object.entries(values).map(([n, v]) => [secretEnvKey(n), Buffer.from(String(v), "utf8").toString("base64")]);
|
|
831
|
+
return this.createOwnedSecret({
|
|
832
|
+
name: secretName,
|
|
833
|
+
kind: "env",
|
|
834
|
+
label: SANDBOX_SECRET_LABEL,
|
|
835
|
+
immutable: true,
|
|
836
|
+
data: Object.fromEntries(encoded),
|
|
837
|
+
hidden: [...Object.values(values).map(String), ...encoded.map(([, b64]) => b64)],
|
|
838
|
+
what: "sandbox secret",
|
|
839
|
+
effect: "the pod is deleted (deployer-named sandbox env reaches a pod only through its own Secret, never as plaintext in the pod spec)",
|
|
840
|
+
}, podName, podUid, signal);
|
|
841
|
+
}
|
|
842
|
+
async createLeaseSecret(leaseName, podName, podUid, leaseText, signal) {
|
|
843
|
+
const b64 = Buffer.from(leaseText, "utf8").toString("base64");
|
|
844
|
+
return this.createOwnedSecret({
|
|
845
|
+
name: leaseName,
|
|
846
|
+
kind: "lease",
|
|
847
|
+
label: SANDBOX_LEASE_LABEL,
|
|
848
|
+
immutable: false,
|
|
849
|
+
data: { [BROKER_LEASE_KEY]: b64 },
|
|
850
|
+
hidden: [b64],
|
|
851
|
+
what: "sandbox lease secret",
|
|
852
|
+
effect: "the pod is deleted (a pod that carries the member-credential broker never runs without the lease its broker reads)",
|
|
853
|
+
}, podName, podUid, signal);
|
|
854
|
+
}
|
|
855
|
+
async createOwnedSecret(seat, podName, podUid, signal) {
|
|
856
|
+
const { effect } = seat;
|
|
857
|
+
if (typeof podUid !== "string" || podUid === "")
|
|
858
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `pod create returned no uid — the ${seat.what} cannot be bound to its pod; ${effect}`) };
|
|
859
|
+
const secret = {
|
|
860
|
+
apiVersion: "v1",
|
|
861
|
+
kind: "Secret",
|
|
862
|
+
metadata: {
|
|
863
|
+
name: seat.name,
|
|
864
|
+
labels: { "managed-by": SANDBOX_POD_LABELS["managed-by"], [seat.label]: podName },
|
|
865
|
+
ownerReferences: [{ apiVersion: "v1", kind: "Pod", name: podName, uid: podUid, blockOwnerDeletion: false }],
|
|
866
|
+
},
|
|
867
|
+
type: "Opaque",
|
|
868
|
+
...(seat.immutable ? { immutable: true } : {}),
|
|
869
|
+
data: seat.data,
|
|
870
|
+
};
|
|
871
|
+
const hidden = seat.hidden.filter((v) => v !== "");
|
|
872
|
+
const scrub = (text) => hidden.reduce((out, v) => out.split(v).join("<redacted>"), text);
|
|
873
|
+
let made;
|
|
874
|
+
try {
|
|
875
|
+
made = await this.request("POST", `/api/v1/namespaces/${this.cfg.namespace}/secrets`, secret, { signal });
|
|
876
|
+
}
|
|
877
|
+
catch (e) {
|
|
878
|
+
if (signal?.aborted)
|
|
879
|
+
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
880
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `${seat.what} create RPC failed: ${scrub(errorMessageOf(e)).slice(0, 300)}; ${effect}`) };
|
|
881
|
+
}
|
|
882
|
+
if (made.status === 200 || made.status === 201)
|
|
883
|
+
return { ok: true };
|
|
884
|
+
const detail = scrub(JSON.stringify(made.json) ?? "").slice(0, 300);
|
|
885
|
+
if (made.status === 401 || made.status === 403) {
|
|
886
|
+
return { ok: false, error: new RemoteExecutionError("auth_failed", `${seat.what} create denied (${made.status}): ${detail} — the sandbox Role needs secrets ${SANDBOX_SECRET_SEAT_VERBS[seat.kind].join(" / ")}; ${effect}`) };
|
|
887
|
+
}
|
|
888
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `${seat.what} create failed (${made.status}): ${detail}; ${effect}`) };
|
|
889
|
+
}
|
|
890
|
+
async writeLease(ref, entries) {
|
|
891
|
+
const fail = (code, message) => {
|
|
892
|
+
this.logger?.warn("sandbox_lease_write_failed", { namespace: ref.namespace, pod: ref.podName, secretName: ref.leaseSecretName, code, detail: message });
|
|
893
|
+
return { ok: false, code, message };
|
|
894
|
+
};
|
|
895
|
+
const built = buildSandboxLeaseFile(ref.origins, entries);
|
|
896
|
+
if (!built.ok)
|
|
897
|
+
return fail("lease_invalid", `the lease was not sent — the broker would refuse it: ${built.problem}`);
|
|
898
|
+
const b64 = Buffer.from(built.text, "utf8").toString("base64");
|
|
899
|
+
const hidden = [b64, ...entries.flatMap((e) => (e.state === "ready" ? [e.secret, Buffer.from(`${e.username}:${e.secret}`, "utf8").toString("base64")] : []))].filter((v) => v !== "");
|
|
900
|
+
const scrub = (text) => hidden.reduce((out, v) => out.split(v).join("<redacted>"), text);
|
|
901
|
+
let r;
|
|
902
|
+
try {
|
|
903
|
+
r = await this.request("PATCH", `/api/v1/namespaces/${ref.namespace}/secrets/${encodeURIComponent(ref.leaseSecretName)}`, { data: { [BROKER_LEASE_KEY]: b64 } }, { contentType: "application/merge-patch+json" });
|
|
904
|
+
}
|
|
905
|
+
catch (e) {
|
|
906
|
+
return fail("write_failed", `the lease write request failed: ${scrub(errorMessageOf(e)).slice(0, 300)}`);
|
|
907
|
+
}
|
|
908
|
+
if (r.status === 200 || r.status === 201)
|
|
909
|
+
return { ok: true };
|
|
910
|
+
const detail = scrub(JSON.stringify(r.json) ?? "").slice(0, 300);
|
|
911
|
+
if (r.status === 401 || r.status === 403)
|
|
912
|
+
return fail("auth_failed", `the lease write was denied (${r.status}): ${detail} — the sandbox Role needs secrets ${SANDBOX_SECRET_SEAT_VERBS.lease.join(" / ")}`);
|
|
913
|
+
if (r.status === 404)
|
|
914
|
+
return fail("gone", `the lease Secret ${ref.leaseSecretName} is gone (its pod ${ref.podName} was deleted)`);
|
|
915
|
+
return fail("write_failed", `the lease write failed (${r.status}): ${detail}`);
|
|
916
|
+
}
|
|
704
917
|
presignSnapshot(req) {
|
|
705
918
|
const s3 = this.cfg.s3Snapshot;
|
|
706
|
-
const prefix = s3.keyPrefix ?? "kata";
|
|
707
919
|
const [label, partition] = req.lane === "space" ? ["spaceIncarnation", req.space.incarnation] : ["sessionId", this.cfg.sessionId ?? "_"];
|
|
708
920
|
const snapshotId = "snapshotId" in req ? req.snapshotId : undefined;
|
|
709
921
|
const objectName = "object" in req ? req.object : undefined;
|
|
710
|
-
const safe = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
711
922
|
const segments = [
|
|
712
923
|
[label, partition],
|
|
713
924
|
...(snapshotId !== undefined ? [["snapshotId", snapshotId]] : []),
|
|
714
925
|
...(objectName !== undefined ? [["object", objectName]] : []),
|
|
715
|
-
["keyPrefix", prefix],
|
|
716
926
|
];
|
|
717
927
|
for (const [label, seg] of segments) {
|
|
718
|
-
if (!
|
|
928
|
+
if (!SNAPSHOT_KEY_SEGMENT.test(seg))
|
|
719
929
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `unsafe snapshot key segment (${label}): refusing to build a traversing object key`) };
|
|
720
930
|
}
|
|
931
|
+
if (snapshotKeyPrefixProblem(this.snapshotKeyPrefix()) !== undefined)
|
|
932
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", "unsafe snapshot key segment (keyPrefix): refusing to build a traversing object key") };
|
|
721
933
|
const common = {
|
|
722
934
|
endpoint: s3.endpoint,
|
|
723
935
|
bucket: s3.bucket,
|
|
@@ -732,8 +944,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
732
944
|
const object = "pointer" in req ? req.pointer : "object" in req ? req.object : `${req.snapshotId}.tar.gz`;
|
|
733
945
|
return { ok: true, value: presignS3Url({ ...common, key: `${base}${object}`, method: req.method }) };
|
|
734
946
|
}
|
|
947
|
+
snapshotKeyPrefix() {
|
|
948
|
+
return this.cfg.s3Snapshot?.keyPrefix ?? WORKSPACE_SNAPSHOT_PREFIX_DEFAULT;
|
|
949
|
+
}
|
|
735
950
|
snapshotBase(req) {
|
|
736
|
-
const prefix = this.
|
|
951
|
+
const prefix = this.snapshotKeyPrefix();
|
|
737
952
|
return req.lane === "space" ? `${prefix}/${req.space.incarnation}/space/` : `${prefix}/${this.cfg.sessionId ?? "_"}/`;
|
|
738
953
|
}
|
|
739
954
|
async reclaimSpaceSnapshots(spaceKey, deadline) {
|
|
@@ -825,12 +1040,25 @@ export class RemoteK8sExecutionEnv {
|
|
|
825
1040
|
catch (e) {
|
|
826
1041
|
return spaceReadFailed(what, `: ${this.redactUrl(errorMessageOf(e), url.value)}`, e);
|
|
827
1042
|
}
|
|
1043
|
+
if (isNoSuchBucketAnswer(res.status, body))
|
|
1044
|
+
return this.refuseMissingBucket(space);
|
|
828
1045
|
if (res.status === 404)
|
|
829
1046
|
return ok(undefined);
|
|
830
1047
|
if (!res.ok)
|
|
831
1048
|
return spaceReadFailed(what, ` (HTTP ${res.status})`);
|
|
832
1049
|
return ok(body.trim());
|
|
833
1050
|
}
|
|
1051
|
+
refuseMissingBucket(space) {
|
|
1052
|
+
const bucket = this.cfg.s3Snapshot.bucket;
|
|
1053
|
+
this.spaceLine("workspace_snapshot_bucket_missing", space, {
|
|
1054
|
+
bucket,
|
|
1055
|
+
code: SNAPSHOT_BUCKET_MISSING,
|
|
1056
|
+
effect: "the workspace snapshot bucket does not exist in the object store (the server never creates buckets) — this run is refused rather than started on a workspace that could be neither restored nor saved",
|
|
1057
|
+
});
|
|
1058
|
+
return this.holdRefusal(spaceReadFailed("pointer", ` (${SNAPSHOT_BUCKET_MISSING}): the workspace snapshot bucket "${bucket}" (WORKSPACE_SNAPSHOT_BUCKET; default ${WORKSPACE_SNAPSHOT_BUCKET_DEFAULT}) does not exist in the object store — ` +
|
|
1059
|
+
`refusing to start this run on a workspace that could be neither restored nor saved. Ways out: create the bucket, or set WORKSPACE_SNAPSHOT_BUCKET to one that exists, ` +
|
|
1060
|
+
"or set WORKSPACE_PERSIST=off for a deployment that keeps no workspace between runs").error);
|
|
1061
|
+
}
|
|
834
1062
|
async readLatest(space, signal) {
|
|
835
1063
|
const r = await this.readPointer(space, "latest", signal);
|
|
836
1064
|
return r.ok ? ok(r.value === undefined ? undefined : r.value) : r;
|
|
@@ -1045,7 +1273,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
1045
1273
|
});
|
|
1046
1274
|
}
|
|
1047
1275
|
async dropDanglingLatest(space, snapshotId) {
|
|
1048
|
-
const prefix = this.
|
|
1276
|
+
const prefix = this.snapshotKeyPrefix();
|
|
1049
1277
|
const dropped = await this.writePointer(space, "latest", null);
|
|
1050
1278
|
this.spaceLine("workspace_latest_dangling", space, {
|
|
1051
1279
|
key: `${prefix}/${space.incarnation}/space/${snapshotId}.tar.gz`,
|
|
@@ -1073,7 +1301,27 @@ export class RemoteK8sExecutionEnv {
|
|
|
1073
1301
|
return this.connect();
|
|
1074
1302
|
}
|
|
1075
1303
|
async deletePod(name) {
|
|
1076
|
-
await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${name}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
|
|
1304
|
+
const r = await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${encodeURIComponent(name)}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
|
|
1305
|
+
if (r.status === 404)
|
|
1306
|
+
return "gone";
|
|
1307
|
+
if (r.status < 200 || r.status >= 300)
|
|
1308
|
+
throw new PodDeleteRefused(r.status);
|
|
1309
|
+
return "deleted";
|
|
1310
|
+
}
|
|
1311
|
+
async discardRefusedPod(name, why) {
|
|
1312
|
+
try {
|
|
1313
|
+
await this.deletePod(name);
|
|
1314
|
+
}
|
|
1315
|
+
catch (e) {
|
|
1316
|
+
reportPodDeleteFailed({
|
|
1317
|
+
namespace: this.cfg.namespace,
|
|
1318
|
+
pod: name,
|
|
1319
|
+
why,
|
|
1320
|
+
error: e,
|
|
1321
|
+
effect: "the refused connect's pod, and the Secret it owns, stay in the sandbox namespace until the pod reaper reaches them (static data; the connect is refused either way)",
|
|
1322
|
+
logger: this.logger,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1077
1325
|
}
|
|
1078
1326
|
async destroy() {
|
|
1079
1327
|
await this.disposeBackgroundShells().catch(() => { });
|
|
@@ -1861,6 +2109,37 @@ export function createSpaceReclaimer(config, deps) {
|
|
|
1861
2109
|
export async function checkSandboxEgressPolicy(config, deps) {
|
|
1862
2110
|
return new RemoteK8sExecutionEnv(config, deps).egressPolicyGaps();
|
|
1863
2111
|
}
|
|
2112
|
+
export async function readSandboxSecretSeat(config, seat, deps) {
|
|
2113
|
+
return new RemoteK8sExecutionEnv(config, deps).secretSeatReading(seat);
|
|
2114
|
+
}
|
|
2115
|
+
export async function writeSandboxLease(config, ref, entries, deps) {
|
|
2116
|
+
return new RemoteK8sExecutionEnv(config, deps).writeLease(ref, entries);
|
|
2117
|
+
}
|
|
2118
|
+
export function createSandboxPodApi(config, deps) {
|
|
2119
|
+
let env;
|
|
2120
|
+
const control = () => (env ??= new RemoteK8sExecutionEnv(config, deps));
|
|
2121
|
+
return {
|
|
2122
|
+
namespace: config.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT,
|
|
2123
|
+
list: () => control().listSandboxPods(),
|
|
2124
|
+
delete: (name) => control().deletePod(name),
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function sandboxPodListingOf(item) {
|
|
2128
|
+
const o = item;
|
|
2129
|
+
const name = o?.metadata?.name;
|
|
2130
|
+
if (typeof name !== "string" || name === "")
|
|
2131
|
+
return undefined;
|
|
2132
|
+
const runId = o?.metadata?.labels?.[SANDBOX_RUN_LABEL];
|
|
2133
|
+
const runEndHold = o?.metadata?.annotations?.[SANDBOX_RUN_END_HOLD_ANNOTATION];
|
|
2134
|
+
const phase = o?.status?.phase;
|
|
2135
|
+
return {
|
|
2136
|
+
name,
|
|
2137
|
+
...(typeof runId === "string" && runId !== "" ? { runId } : {}),
|
|
2138
|
+
...(typeof runEndHold === "string" ? { runEndHold } : {}),
|
|
2139
|
+
...(typeof phase === "string" ? { phase } : {}),
|
|
2140
|
+
terminating: typeof o?.metadata?.deletionTimestamp === "string",
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
1864
2143
|
export function k8sExecutionEnvFactory(config, deps) {
|
|
1865
2144
|
return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId }, deps);
|
|
1866
2145
|
}
|
|
@@ -74,6 +74,9 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
74
74
|
/** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在容器里(镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
75
75
|
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
76
76
|
readonly cdPath: "unknown";
|
|
77
|
+
/** core 7.56.0 [ref]:shell 子进程继承的环境里收不到的条目 —— 本车道 shell 的环境在容器里**构建**(本进程的 env 从不注入,只有引擎逐次交的 `env`
|
|
78
|
+
* 这条显式授予),没有一条是从本进程继承的 ⇒ `[]`(core 契约:「`[]` when its shell environment is built rather than inherited」)。 */
|
|
79
|
+
readonly withheldEnv: readonly [];
|
|
77
80
|
private readonly cfg;
|
|
78
81
|
private readonly spawnFn;
|
|
79
82
|
/** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
|
|
@@ -95,6 +95,9 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
95
95
|
/** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在远端登录 shell 里(目标机的 profile / env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
96
96
|
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
97
97
|
readonly cdPath: "unknown";
|
|
98
|
+
/** core 7.56.0 [ref]:shell 子进程继承的环境里收不到的条目 —— 本车道 shell 的环境在目标机上**构建**(本进程的 env 从不注入,只有引擎逐次交的 `env`
|
|
99
|
+
* 这条显式授予),没有一条是从本进程继承的 ⇒ `[]`(core 契约:「`[]` when its shell environment is built rather than inherited」)。 */
|
|
100
|
+
readonly withheldEnv: readonly [];
|
|
98
101
|
private readonly cfg;
|
|
99
102
|
private readonly injectedConnectOnce?;
|
|
100
103
|
constructor(config: SshEnvConfig, deps?: SshEnvDeps);
|
|
@@ -10,10 +10,11 @@ import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR, readLegacyHolderDeathCode }
|
|
|
10
10
|
import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
|
|
11
11
|
import { mysqlDriver, pgDriver, dialectJsonEncoder, SQL_DB_NOW_MS, sqlDbNowMs } from "./sql-driver.js";
|
|
12
12
|
import { isDupKeyError } from "./sql-errors.js";
|
|
13
|
+
import { readRunIdentityCells, runIdentityCells } from "../identity-source.js";
|
|
13
14
|
import { decideSpaceCommitAbandon, decideSpaceCommitClaim, fenceSpaceCommitAtTerminal, spaceCommitStateOf } from "../workspace-space-commit.js";
|
|
14
15
|
function runRowCols(dialect) {
|
|
15
16
|
return (`task_id, session_id, owner, status, ${jsonColumnText(dialect, "result")}, error, error_code, job_id, source, ` +
|
|
16
|
-
"instance_id, cancel_requested, objective_preview, created_at, updated_at");
|
|
17
|
+
"identity_source, identity_scope, identity_principal_inc, instance_id, cancel_requested, objective_preview, created_at, updated_at");
|
|
17
18
|
}
|
|
18
19
|
const readPersistedTaskResultBody = createObjectColumnReader();
|
|
19
20
|
function decodeResultColumn(v, taskId) {
|
|
@@ -84,15 +85,19 @@ export class SqlRunStore {
|
|
|
84
85
|
try {
|
|
85
86
|
await conn.query(this.q("INSERT INTO task_active (session_id, task_id, since) VALUES (?,?,?)", "INSERT INTO task_active (session_id, task_id, since) VALUES ($1,$2,$3)"), [sessionId, taskId, now]);
|
|
86
87
|
stage = "row";
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"VALUES (
|
|
88
|
+
const cells = runIdentityCells(meta.identity ?? null);
|
|
89
|
+
await conn.query(this.q("INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, identity_source, identity_scope, identity_principal_inc, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
|
|
90
|
+
"VALUES (?,?,?,'running',?,?,?,?,?,?,?,?,?,?,?)", "INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, identity_source, identity_scope, identity_principal_inc, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
|
|
91
|
+
"VALUES ($1,$2,$3,'running',$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)"), [
|
|
90
92
|
taskId,
|
|
91
93
|
sessionId,
|
|
92
94
|
owner,
|
|
93
95
|
instanceId,
|
|
94
96
|
meta.jobId ?? null,
|
|
95
97
|
meta.source ?? null,
|
|
98
|
+
cells.identitySource,
|
|
99
|
+
cells.identityScope,
|
|
100
|
+
cells.identityPrincipalInc,
|
|
96
101
|
this.db.dialect === "tidb"
|
|
97
102
|
? meta.objectivePreview ?? null
|
|
98
103
|
: meta.objectivePreview == null
|
|
@@ -299,6 +304,7 @@ export class SqlRunStore {
|
|
|
299
304
|
errorCode: readLegacyHolderDeathCode({ status: String(r.status), error: r.error ?? null, errorCode: r.error_code ?? null }),
|
|
300
305
|
jobId: r.job_id ?? null,
|
|
301
306
|
source: r.source ?? null,
|
|
307
|
+
identity: readRunIdentityCells(r.identity_source, r.identity_scope, r.identity_principal_inc),
|
|
302
308
|
instanceId: r.instance_id ?? null,
|
|
303
309
|
cancelRequested: Boolean(r.cancel_requested),
|
|
304
310
|
objectivePreview: r.objective_preview ?? null,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How this server reads an S3-compatible store's ANSWER — the one place a "the bucket itself is not there" reading is spelled.
|
|
3
|
+
*
|
|
4
|
+
* A 404 from an object store means two different things: `NoSuchKey` (the bucket is there, this object is not — a space's first run,
|
|
5
|
+
* a pointer never written) and `NoSuchBucket` (the bucket was never created — a deployment error no run can fix by retrying). Reading
|
|
6
|
+
* the second as the first turns a missing bucket into a silently empty workspace on every run. The ONLY evidence of a missing bucket is
|
|
7
|
+
* a 404 whose body carries the S3 error code `NoSuchBucket`: a bare 404, a 404 from a proxy or a wrong path, a 403 (credentials that
|
|
8
|
+
* cannot list ≠ no bucket) and a 5xx prove nothing about the bucket. Every consumer asks this one predicate: the boot probe of a
|
|
9
|
+
* configured bucket (`MinioBlobBackend.probeBucket`) and the runtime read of the workspace `latest` pointer (`remote-env-k8s.ts`).
|
|
10
|
+
*/
|
|
11
|
+
/** The code a run's refusal / operator line carries when the store answered "no such bucket" (the `workspace_snapshot_bucket_missing` line's `code`). */
|
|
12
|
+
export declare const SNAPSHOT_BUCKET_MISSING = "snapshot_bucket_missing";
|
|
13
|
+
/** Does this answer prove the bucket is missing? `status` = the HTTP status, `body` = the answer's text (an S3 error document). */
|
|
14
|
+
export declare function isNoSuchBucketAnswer(status: number, body: string): boolean;
|
|
15
|
+
//# sourceMappingURL=s3-answer.d.ts.map
|