@sema-agent/server 7.136.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 +173 -0
- package/USAGE.md +84 -16
- package/deploy/sema-up/README.md +14 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
- package/dist/approval-content-kind.d.ts +6 -0
- package/dist/approval-content-kind.js +7 -0
- 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.js +2 -2
- package/dist/boot/execution-env.d.ts +24 -1
- package/dist/boot/execution-env.js +69 -6
- 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 +16 -17
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +3 -1
- package/dist/boot/stage-05-execution-env.js +11 -2
- package/dist/boot/stage-06-runners.d.ts +3 -1
- package/dist/boot/stage-06-runners.js +5 -1
- package/dist/boot/stage-07-capability-layer.d.ts +4 -1
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +4 -1
- package/dist/boot/stage-08-reapers.js +2 -1
- package/dist/boot/stage-09-leader.d.ts +4 -1
- package/dist/boot/stage-10-http-server.d.ts +4 -1
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.d.ts +1 -1
- 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/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/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/center-credential-renewer.js +5 -1
- 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 +9 -6
- package/dist/config-types.d.ts +9 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +34 -8
- package/dist/deployment-governance.d.ts +2 -2
- package/dist/deployment-governance.js +7 -9
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/file-history-disclosure.d.ts +4 -2
- package/dist/file-history-disclosure.js +2 -0
- package/dist/hooks/classic-stations.d.ts +11 -54
- 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 +48 -7
- package/dist/http/checkpoint-ctx.js +40 -0
- package/dist/http/dispatch.js +15 -10
- 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-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.js +18 -4
- 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 +2 -1
- package/dist/http/routes/session-sync.js +42 -6
- package/dist/http/routes/sessions.js +2 -1
- 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/wire-types.d.ts +52 -27
- package/dist/identity-source.d.ts +65 -0
- package/dist/identity-source.js +45 -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-select.d.ts +6 -17
- package/dist/model-select.js +0 -3
- package/dist/namespaced-rule-name.d.ts +1 -1
- package/dist/observability/fail-open.d.ts +13 -1
- package/dist/observability/fail-open.js +13 -1
- package/dist/observability/metrics.js +1 -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.js +3 -2
- package/dist/peer-directory.js +2 -0
- package/dist/permission-rule-list.d.ts +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +19 -9
- package/dist/plugins/checkpoint-store-sql.js +6 -6
- package/dist/plugins/file-history-store-sql.js +7 -14
- 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 +3 -3
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/one-time-migrations.d.ts +28 -9
- package/dist/plugins/one-time-migrations.js +48 -5
- 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 +137 -10
- package/dist/plugins/remote-env-k8s.js +276 -14
- 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/session-ownership.d.ts +17 -3
- package/dist/plugins/session-ownership.js +19 -5
- package/dist/plugins/sql-errors.d.ts +23 -2
- package/dist/plugins/sql-errors.js +15 -0
- package/dist/plugins/store-contracts.d.ts +10 -1
- package/dist/plugins/store-fault.d.ts +2 -2
- package/dist/plugins/store-fault.js +4 -2
- package/dist/plugins/tidb-pool.js +7 -0
- package/dist/plugins/tidb-session-store.js +2 -2
- package/dist/request-key-closure.js +1 -1
- 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-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/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-policy-wire.d.ts +1 -1
- package/dist/session-policy-wire.js +2 -2
- package/dist/session-sync.d.ts +1 -1
- 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-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +2 -2
- package/dist/tool-approval.d.ts +12 -2
- package/dist/tool-approval.js +8 -3
- package/dist/tool-rule-names.d.ts +5 -0
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- 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 +2 -2
- 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 +18 -6
- package/dist/trace/redact.js +118 -53
- package/dist/trace/self-minted-wire.d.ts +377 -3
- package/dist/trace/self-minted-wire.js +122 -0
- package/dist/trace/wire-projection-faces.d.ts +5 -3
- package/dist/trace/wire-projection-faces.js +7 -3
- 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/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
- package/package.json +2 -2
- package/wire-changes/server@7.137.0.json +825 -0
|
@@ -13,6 +13,7 @@ import { spaceCommitAlive } from "../workspace-space-commit.js";
|
|
|
13
13
|
import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
|
|
14
14
|
import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
|
|
15
15
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
16
|
+
import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failure.js";
|
|
16
17
|
import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
|
|
17
18
|
import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
|
|
18
19
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
@@ -39,9 +40,12 @@ const unsupported = (op) => ({
|
|
|
39
40
|
const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
|
|
40
41
|
const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
|
|
41
42
|
const WRITE_CHUNK_BYTES = 64 * 1024;
|
|
42
|
-
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";
|
|
43
44
|
export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
44
|
-
|
|
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;
|
|
45
49
|
function occupantKeyOf(occupant) {
|
|
46
50
|
if (occupant === undefined)
|
|
47
51
|
return { state: "unavailable", reason: "incarnation_unknown" };
|
|
@@ -168,12 +172,39 @@ export function errorMessageOf(e) {
|
|
|
168
172
|
}
|
|
169
173
|
export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
|
|
170
174
|
export const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
|
|
171
|
-
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
|
+
}
|
|
172
202
|
export class RemoteK8sExecutionEnv {
|
|
173
203
|
capabilities;
|
|
174
204
|
cwd;
|
|
175
205
|
podName;
|
|
176
206
|
podIP;
|
|
207
|
+
leaseRef;
|
|
177
208
|
destroyed = false;
|
|
178
209
|
handleSnapshotId;
|
|
179
210
|
inFlight = new Set();
|
|
@@ -185,6 +216,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
185
216
|
}
|
|
186
217
|
homeSeat;
|
|
187
218
|
cdPath = "unknown";
|
|
219
|
+
withheldEnv = [];
|
|
188
220
|
cfg;
|
|
189
221
|
deps;
|
|
190
222
|
occupantIncarnation;
|
|
@@ -220,6 +252,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
220
252
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
221
253
|
...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
|
|
222
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 } : {}),
|
|
223
258
|
...(config.snapshotExclude && config.snapshotExclude.length > 0 ? { snapshotExclude: config.snapshotExclude } : {}),
|
|
224
259
|
};
|
|
225
260
|
this.cwd = this.cfg.mountPath;
|
|
@@ -243,6 +278,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
243
278
|
supportsDetach: true,
|
|
244
279
|
};
|
|
245
280
|
}
|
|
281
|
+
get sandboxLeaseRef() {
|
|
282
|
+
return this.leaseRef !== undefined && this.leaseRef.podName === this.podName ? this.leaseRef : undefined;
|
|
283
|
+
}
|
|
246
284
|
get lifetimeMs() {
|
|
247
285
|
return this.cfg.timeoutMs;
|
|
248
286
|
}
|
|
@@ -293,7 +331,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
293
331
|
headers: {
|
|
294
332
|
authorization: `Bearer ${token}`,
|
|
295
333
|
accept: "application/json",
|
|
296
|
-
...(payload ? { "content-type": "application/json", "content-length": Buffer.byteLength(payload) } : {}),
|
|
334
|
+
...(payload ? { "content-type": opts?.contentType ?? "application/json", "content-length": Buffer.byteLength(payload) } : {}),
|
|
297
335
|
},
|
|
298
336
|
...(isHttps ? { ca: ca ? [ca] : undefined, rejectUnauthorized: !this.cfg.insecureTls } : {}),
|
|
299
337
|
timeout: opts?.timeoutMs ?? this.cfg.rpcTimeoutMs,
|
|
@@ -509,7 +547,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
509
547
|
const claim = {
|
|
510
548
|
apiVersion: "v1",
|
|
511
549
|
kind: "PersistentVolumeClaim",
|
|
512
|
-
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"] } },
|
|
513
551
|
spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: cache.size } } },
|
|
514
552
|
};
|
|
515
553
|
const made = await this.request("POST", base, claim, { signal });
|
|
@@ -562,6 +600,38 @@ export class RemoteK8sExecutionEnv {
|
|
|
562
600
|
applyPodSpecPatch(probe, { labels: this.cfg.podSpecPatch.labels });
|
|
563
601
|
return sandboxEgressGaps({ nodes, policies, podLabels: probe.metadata.labels, ipv6: this.cfg.ipv6 });
|
|
564
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
|
+
}
|
|
565
635
|
reclaimDeadlinePassed(what) {
|
|
566
636
|
return `reclaim deadline (${this.cfg.dataTimeoutMs} ms, the snapshot time bound, one per reclaim call) passed — ${what}`;
|
|
567
637
|
}
|
|
@@ -579,19 +649,36 @@ export class RemoteK8sExecutionEnv {
|
|
|
579
649
|
}
|
|
580
650
|
async createPod(signal, cache) {
|
|
581
651
|
const ns = this.cfg.namespace;
|
|
582
|
-
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);
|
|
583
668
|
const sysctls = sandboxPodSysctls(this.cfg.ipv6);
|
|
584
669
|
const pod = {
|
|
585
670
|
apiVersion: "v1",
|
|
586
671
|
kind: "Pod",
|
|
587
672
|
metadata: {
|
|
588
673
|
generateName: "sbx-",
|
|
589
|
-
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)) } } : {}),
|
|
590
676
|
},
|
|
591
677
|
spec: {
|
|
592
678
|
...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
|
|
593
679
|
...(sysctls ? { securityContext: { sysctls: sysctls.map((x) => ({ ...x })) } } : {}),
|
|
594
680
|
restartPolicy: "Never",
|
|
681
|
+
...(broker !== undefined ? { shareProcessNamespace: false } : {}),
|
|
595
682
|
automountServiceAccountToken: false,
|
|
596
683
|
activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
|
|
597
684
|
terminationGracePeriodSeconds: 1,
|
|
@@ -599,18 +686,30 @@ export class RemoteK8sExecutionEnv {
|
|
|
599
686
|
{
|
|
600
687
|
name: "sandbox",
|
|
601
688
|
image: this.cfg.image,
|
|
602
|
-
command:
|
|
689
|
+
command: pkgSourceMarked
|
|
603
690
|
? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
|
|
604
691
|
: ["sh", "-c", "sleep infinity"],
|
|
605
|
-
...(
|
|
692
|
+
...(envEntries.length > 0 ? { env: envEntries } : {}),
|
|
606
693
|
...(cache ? { volumeMounts: [{ name: CACHE_VOLUME, mountPath: cache.mount }] } : {}),
|
|
607
694
|
resources: { limits: { cpu: this.cfg.cpu, memory: this.cfg.memory } },
|
|
608
695
|
},
|
|
696
|
+
...(broker !== undefined ? [brokerContainer(broker.image)] : []),
|
|
609
697
|
],
|
|
610
|
-
...(
|
|
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
|
+
]),
|
|
611
702
|
},
|
|
612
703
|
};
|
|
613
|
-
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)
|
|
614
713
|
applyPodSpecPatch(pod, this.cfg.podSpecPatch);
|
|
615
714
|
let created;
|
|
616
715
|
try {
|
|
@@ -628,6 +727,20 @@ export class RemoteK8sExecutionEnv {
|
|
|
628
727
|
const name = created.json?.metadata?.name;
|
|
629
728
|
if (!name)
|
|
630
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
|
+
}
|
|
631
744
|
const deadline = Date.now() + this.cfg.readyTimeoutMs;
|
|
632
745
|
let podIP;
|
|
633
746
|
for (;;) {
|
|
@@ -645,7 +758,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
645
758
|
}
|
|
646
759
|
const status = got.json?.status;
|
|
647
760
|
const phase = status?.phase;
|
|
648
|
-
|
|
761
|
+
const brokerStatus = status?.containerStatuses?.find((c) => c.name === BROKER_CONTAINER_NAME);
|
|
762
|
+
if (phase === "Running" && (broker === undefined || brokerStatus?.ready === true)) {
|
|
649
763
|
podIP = status?.podIP;
|
|
650
764
|
break;
|
|
651
765
|
}
|
|
@@ -653,11 +767,16 @@ export class RemoteK8sExecutionEnv {
|
|
|
653
767
|
await this.deletePod(name).catch(() => { });
|
|
654
768
|
return this.refusePod(name, phase, status?.reason, status?.message);
|
|
655
769
|
}
|
|
656
|
-
const waiting = status?.containerStatuses?.
|
|
770
|
+
const waiting = status?.containerStatuses?.find((c) => c.name !== BROKER_CONTAINER_NAME)?.state?.waiting;
|
|
657
771
|
if (waiting?.reason === "ImagePullBackOff" || waiting?.reason === "ErrImagePull") {
|
|
658
772
|
await this.deletePod(name).catch(() => { });
|
|
659
773
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `image pull failed for ${this.cfg.image}: ${waiting.message ?? waiting.reason}`) };
|
|
660
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
|
+
}
|
|
661
780
|
if (Date.now() > deadline) {
|
|
662
781
|
if (cache) {
|
|
663
782
|
const bound = await this.cacheClaimBound(cache, signal);
|
|
@@ -671,6 +790,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
671
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)` };
|
|
672
791
|
}
|
|
673
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
|
+
}
|
|
674
796
|
return { ok: false, error: new RemoteExecutionError("timeout", `pod not Running within ${this.cfg.readyTimeoutMs}ms (phase=${phase ?? "?"})`) };
|
|
675
797
|
}
|
|
676
798
|
await new Promise((r) => setTimeout(r, 1000));
|
|
@@ -681,6 +803,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
681
803
|
}
|
|
682
804
|
this.podName = name;
|
|
683
805
|
this.podIP = podIP;
|
|
806
|
+
this.leaseRef = leaseName !== undefined && broker !== undefined ? { namespace: ns, podName: name, leaseSecretName: leaseName, origins: [...broker.origins] } : undefined;
|
|
684
807
|
const askHome = this.homeSeat.asks;
|
|
685
808
|
const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}${askHome ? ` && { ${LANE_HOME_PROBE_COMMAND} || true; }` : ""}`);
|
|
686
809
|
if (!mk.ok || mk.value.exitCode !== 0) {
|
|
@@ -703,6 +826,94 @@ export class RemoteK8sExecutionEnv {
|
|
|
703
826
|
this.logger?.warn("workspace_cache_fresh", { space: incarnationLogPrefix(cache.spaceKey), claim: cache.claim.name, size: cache.size });
|
|
704
827
|
return ok(this.workspaceHandle());
|
|
705
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
|
+
}
|
|
706
917
|
presignSnapshot(req) {
|
|
707
918
|
const s3 = this.cfg.s3Snapshot;
|
|
708
919
|
const [label, partition] = req.lane === "space" ? ["spaceIncarnation", req.space.incarnation] : ["sessionId", this.cfg.sessionId ?? "_"];
|
|
@@ -1090,7 +1301,27 @@ export class RemoteK8sExecutionEnv {
|
|
|
1090
1301
|
return this.connect();
|
|
1091
1302
|
}
|
|
1092
1303
|
async deletePod(name) {
|
|
1093
|
-
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
|
+
}
|
|
1094
1325
|
}
|
|
1095
1326
|
async destroy() {
|
|
1096
1327
|
await this.disposeBackgroundShells().catch(() => { });
|
|
@@ -1878,6 +2109,37 @@ export function createSpaceReclaimer(config, deps) {
|
|
|
1878
2109
|
export async function checkSandboxEgressPolicy(config, deps) {
|
|
1879
2110
|
return new RemoteK8sExecutionEnv(config, deps).egressPolicyGaps();
|
|
1880
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
|
+
}
|
|
1881
2143
|
export function k8sExecutionEnvFactory(config, deps) {
|
|
1882
2144
|
return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId }, deps);
|
|
1883
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,
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「谁也不指的串」的描述句 —— 拒句里「给的是什么」那一格的串形(从不回显值:一个谁也不指的串可能就是一段被粘进来的凭据)。
|
|
3
|
+
* 本模块的属主拒与鉴权边界的主体拒(`security.ts` `assertPrincipalShape` 的 `principal_not_a_name`)读这同一句:判官同一只,描述也同一句。
|
|
4
|
+
*/
|
|
5
|
+
export declare const NOT_A_PRINCIPAL_NAME = "a string that names nobody (empty, blank, or holding a control, separator or invisible format character)";
|
|
6
|
+
/**
|
|
7
|
+
* 「这个值能不能作一只会话行的属主格」—— core 7.56.0 [ref] 起只有一只判官 `principalNameOf`:主体名(有可见内容、不含控制 / 分隔 / 非连接符格式字符,
|
|
8
|
+
* 原样保存不规整)或 `null`(匿名)。空串、空白串、带不可见字符的串与一切非串都不是属主(它们谁也不指 —— 读成匿名会放行每一位主体)。
|
|
9
|
+
*
|
|
10
|
+
* 边界同判官:HTTP 请求带来的主体在鉴权边界已经过同一只 `principalNameOf`(`security.ts` `principalShapeProblem` 的 `not_a_name`,7.137.0)⇒
|
|
11
|
+
* 经过边界的请求到不了这里的拒臂;这里是纵深(嵌入方直调店、别的写者),规则只有引擎那一只,不在本仓另写。
|
|
12
|
+
*/
|
|
13
|
+
export declare function isSessionOwnerValue(owner: unknown): owner is string | null;
|
|
1
14
|
/**
|
|
2
15
|
* fork 拷贝的属主 —— core `SessionStore.fork` 契约的唯一读法:
|
|
3
|
-
*
|
|
16
|
+
* 显式主体名 ⇒ 该名;无参 / 显式 `null` ⇒ 源会话记下的属主(`undefined` = 源会话没记,拷贝也不记)。
|
|
17
|
+
* 显式给了却不是主体名(空串 / 空白 / 带不可见字符,core 7.56.0 [ref])⇒ 响亮拒(`SessionError("storage")`,不留行)—— 与 `register` 首写同一只判官。
|
|
4
18
|
*/
|
|
5
19
|
export declare function forkOwnerOf(requested: string | null | undefined, sourceOwner: string | null | undefined): string | null | undefined;
|
|
6
20
|
/**
|
|
7
|
-
* `register` 首写的 owner 守卫 —— core `SessionStore.register` 契约的唯一读法(三只店同调):首写的 owner
|
|
8
|
-
* 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。
|
|
21
|
+
* `register` 首写的 owner 守卫 —— core `SessionStore.register` 契约的唯一读法(三只店同调):首写的 owner 既不是主体名也不是 `null` ⇒ 响亮拒(不留行);
|
|
22
|
+
* 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。判官 = {@link isSessionOwnerValue}(core 7.56.0 [ref] `principalNameOf`)。
|
|
9
23
|
*/
|
|
10
24
|
export declare function assertFirstOwnerWritable(sessionId: string, owner: unknown): asserts owner is string | null;
|
|
11
25
|
//# sourceMappingURL=session-ownership.d.ts.map
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import { SessionError } from "@sema-agent/core";
|
|
1
|
+
import { SessionError, principalNameOf } from "@sema-agent/core";
|
|
2
|
+
export const NOT_A_PRINCIPAL_NAME = "a string that names nobody (empty, blank, or holding a control, separator or invisible format character)";
|
|
3
|
+
function ownerValueFault(owner) {
|
|
4
|
+
if (typeof owner === "string")
|
|
5
|
+
return NOT_A_PRINCIPAL_NAME;
|
|
6
|
+
return owner === undefined ? "undefined" : typeof owner;
|
|
7
|
+
}
|
|
8
|
+
export function isSessionOwnerValue(owner) {
|
|
9
|
+
return owner === null || principalNameOf(owner) !== undefined;
|
|
10
|
+
}
|
|
2
11
|
export function forkOwnerOf(requested, sourceOwner) {
|
|
3
|
-
|
|
12
|
+
if (requested === undefined || requested === null)
|
|
13
|
+
return sourceOwner;
|
|
14
|
+
if (principalNameOf(requested) === undefined) {
|
|
15
|
+
throw new SessionError("storage", `fork: session owner must be a principal name or null, got ${ownerValueFault(requested)} — refusing, nothing written`);
|
|
16
|
+
}
|
|
17
|
+
return requested;
|
|
4
18
|
}
|
|
5
19
|
export function assertFirstOwnerWritable(sessionId, owner) {
|
|
6
|
-
if (
|
|
20
|
+
if (isSessionOwnerValue(owner))
|
|
7
21
|
return;
|
|
8
|
-
throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal
|
|
9
|
-
"a
|
|
22
|
+
throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal name or null (anonymous), got ${ownerValueFault(owner)} — ` +
|
|
23
|
+
"a value that names nobody would leave the session claimable by anyone or ownable by no one; refusing, nothing written");
|
|
10
24
|
}
|
|
11
25
|
//# sourceMappingURL=session-ownership.js.map
|