@runuai/host 0.9.148 → 0.9.149

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.
Files changed (93) hide show
  1. package/db/migrations/0024_task_env_mode.sql +9 -0
  2. package/db/migrations/0025_project_env_authority.sql +147 -0
  3. package/db/migrations/0026_project_env_replica.sql +150 -0
  4. package/db/migrations/meta/_journal.json +21 -0
  5. package/db/schema.ts +312 -2
  6. package/images/standard/Dockerfile +44 -6
  7. package/images/standard/container/uai-init +85 -2
  8. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  9. package/lib/agent-cli.ts +18 -1
  10. package/lib/agent.ts +99 -34
  11. package/lib/agents/codex.ts +9 -1
  12. package/lib/agents/cursor.ts +15 -1
  13. package/lib/agents/grok.ts +8 -0
  14. package/lib/agents/kimi.ts +1 -0
  15. package/lib/agents/machine-durable.ts +2 -0
  16. package/lib/agents/opencode.ts +1 -0
  17. package/lib/agents/proc.ts +6 -1
  18. package/lib/agents/transport.ts +9 -5
  19. package/lib/browser-testing.ts +6 -0
  20. package/lib/codex-auth.ts +2 -0
  21. package/lib/credential-store.ts +141 -6
  22. package/lib/engine-accounts.ts +1 -0
  23. package/lib/env-file.ts +7 -0
  24. package/lib/env-schema.ts +841 -51
  25. package/lib/git-diff.ts +1 -0
  26. package/lib/git-identity.ts +2 -0
  27. package/lib/github-tokens.ts +1 -0
  28. package/lib/host-env.ts +98 -18
  29. package/lib/lease.ts +11 -3
  30. package/lib/machine-gateway-tunnel.ts +195 -17
  31. package/lib/machine-project-env-loader.ts +152 -0
  32. package/lib/managed-runtime.ts +270 -51
  33. package/lib/mcp-gateway.ts +777 -26
  34. package/lib/orchestrator.ts +391 -69
  35. package/lib/project-env-authority.ts +1319 -0
  36. package/lib/project-env-feature.ts +74 -0
  37. package/lib/project-env-loader.ts +237 -0
  38. package/lib/project-env-replica.ts +2094 -0
  39. package/lib/project-env-schema-capture.ts +342 -0
  40. package/lib/project-env-staging.ts +148 -0
  41. package/lib/project-env-task-policy.ts +176 -0
  42. package/lib/runtime-archive.ts +79 -16
  43. package/lib/runtime-authority.ts +44 -0
  44. package/lib/runtime-state.ts +175 -10
  45. package/lib/secrets-proxy/address.ts +72 -0
  46. package/lib/secrets-proxy/audit.ts +30 -0
  47. package/lib/secrets-proxy/ca-store.ts +200 -0
  48. package/lib/secrets-proxy/ca.ts +470 -0
  49. package/lib/secrets-proxy/container-steps.ts +138 -0
  50. package/lib/secrets-proxy/der.ts +273 -0
  51. package/lib/secrets-proxy/engine.ts +1633 -0
  52. package/lib/secrets-proxy/host.ts +93 -0
  53. package/lib/secrets-proxy/hostname.ts +60 -0
  54. package/lib/secrets-proxy/placeholders.ts +196 -0
  55. package/lib/secrets-proxy/policy.ts +141 -0
  56. package/lib/secrets-proxy/readiness.ts +148 -0
  57. package/lib/secrets-proxy/runtime.ts +582 -0
  58. package/lib/secrets-proxy/scrub.ts +559 -0
  59. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  60. package/lib/secrets-proxy/task-scripts.ts +262 -0
  61. package/lib/secrets-proxy/task-trust.ts +574 -0
  62. package/lib/secrets-proxy/trust-watch.ts +80 -0
  63. package/lib/shared-files-s3.ts +3 -0
  64. package/lib/sidecar-guard.ts +58 -0
  65. package/lib/sidecar-resume.ts +51 -3
  66. package/lib/standard-image.ts +50 -12
  67. package/lib/task-env-forwarder.ts +228 -0
  68. package/lib/task-env-source.ts +193 -0
  69. package/lib/task-environment/apple-container.ts +45 -11
  70. package/lib/task-environment/docker.ts +51 -12
  71. package/lib/task-environment/index.ts +6 -2
  72. package/lib/task-environment/machine-task-up.ts +258 -25
  73. package/lib/task-environment/machine.ts +80 -22
  74. package/lib/task-environment/registry.ts +3 -2
  75. package/lib/task-environment/self-identity.ts +20 -0
  76. package/lib/task-environment/self.ts +92 -16
  77. package/lib/task-environment/types.ts +63 -0
  78. package/lib/task-gateway-port.ts +22 -0
  79. package/lib/task-gateway-protocol.ts +190 -0
  80. package/lib/task-gateway-recovery.ts +44 -0
  81. package/lib/task-gateway-state.ts +497 -0
  82. package/lib/transcript.ts +1 -0
  83. package/package.json +13 -1
  84. package/scripts/agent/env-hook.sh +626 -0
  85. package/scripts/agent/task-up.sh +476 -27
  86. package/scripts/agent/uai-env-exec.sh +26 -0
  87. package/src/cli.ts +26 -3
  88. package/src/index.ts +273 -19
  89. package/src/main.ts +939 -48
  90. package/src/project-env-latest.ts +130 -0
  91. package/src/project-env-policy.ts +281 -0
  92. package/src/project-env-wording.ts +47 -0
  93. package/src/protocol.ts +2219 -15
@@ -0,0 +1,193 @@
1
+ /**
2
+ * ADR-153 §4 and §6: where a task's project env comes from, for its /env and
3
+ * its proxy alike, on an owned runner and on a task machine's sidecar.
4
+ *
5
+ * - The store is the runner's own records on an owned runner, and on a task
6
+ * machine the sidecar's replica of the cloud's.
7
+ * - A store never activated gives tasks the stored values, as before
8
+ * records existed.
9
+ * - Once activated, a task's env is the view of one snapshot of the store,
10
+ * decided with the task's own schema observations: each key's value as of
11
+ * its head, a placeholder the proxy swaps, or nothing. While the store's
12
+ * gate is closed, or a read of it fails, the task gets no project env at
13
+ * all, never the stored values.
14
+ *
15
+ * A task's read of the store is kept until anything it depends on changes:
16
+ * one of its projects' values or records, a binding or the store's gate
17
+ * (each bumps a process-local revision, and only this process writes the
18
+ * store). Its observations are stored once and never change. So a change
19
+ * reaches the next command and the next request, while a command that
20
+ * changes nothing reads nothing.
21
+ */
22
+
23
+ import { projectEnvAuthorityRevision, projectEnvRevision } from "./host-env";
24
+ import { readProjectEnvAuthoritySnapshot } from "./project-env-authority";
25
+ import { readProjectEnvReplicaSnapshot, readProjectEnvReplicaStatus } from "./project-env-replica";
26
+ import { taskProjectEnvResolver, type TaskEnvSource } from "./project-env-loader";
27
+ import { captureTaskEnvSchemas, taskEnvSchemaObservation } from "./project-env-schema-capture";
28
+ import {
29
+ sameTaskEnvHead,
30
+ taskEnvView,
31
+ taskEnvWithheldNote,
32
+ type TaskEnvHead,
33
+ type TaskEnvSnapshot,
34
+ } from "./project-env-task-policy";
35
+ import { getHostTask, hostTaskProjectEnvSpec, type TaskProjectEnvSpec } from "./runtime-state";
36
+ import { sidecarMode } from "./sidecar";
37
+ import { taskProxiedEnv } from "./secrets-proxy/policy";
38
+ import { taskTrustEnv } from "./secrets-proxy/readiness";
39
+ import type { TaskGatewayEnvResolver } from "./task-gateway-state";
40
+
41
+ /** The task's projects, lead first, as its launch pinned them. */
42
+ function projectIdsOf(spec: TaskProjectEnvSpec): string[] {
43
+ const ordered = [...spec.projects].sort(
44
+ (left, right) => left.position - right.position || (left.projectId < right.projectId ? -1 : left.projectId > right.projectId ? 1 : 0),
45
+ );
46
+ return [...new Set(ordered.map((project) => project.projectId))];
47
+ }
48
+
49
+ /**
50
+ * One snapshot of the task's projects: `legacy` on a store that has never
51
+ * been activated, null while its gate is closed or it cannot be read, and
52
+ * otherwise the records, values and observations the task's decisions come
53
+ * from.
54
+ */
55
+ export function readTaskEnvSnapshot(taskId: string, spec: TaskProjectEnvSpec): TaskEnvSnapshot | "legacy" | null {
56
+ const projectIds = projectIdsOf(spec);
57
+ const key = JSON.stringify([projectEnvAuthorityRevision(), projectIds.map((projectId) => [projectId, projectEnvRevision(projectId)])]);
58
+ const hit = snapshots.get(taskId);
59
+ if (hit?.key === key) return hit.snapshot;
60
+ const snapshot = freshTaskEnvSnapshot(taskId, projectIds);
61
+ // Kept only when nothing in it can change without a revision moving: a
62
+ // read that failed, or one with an observation not stored yet (task-up
63
+ // may still be about to record its base), is read again next time.
64
+ if (snapshot === "legacy" || (snapshot !== null && projectIds.every((projectId) => taskEnvSchemaObservation(taskId, projectId) !== null))) {
65
+ snapshots.delete(taskId);
66
+ if (snapshots.size >= MAX_SNAPSHOTS) snapshots.delete(snapshots.keys().next().value!);
67
+ snapshots.set(taskId, { key, snapshot });
68
+ }
69
+ return snapshot;
70
+ }
71
+
72
+ /** Tasks' last reads, most recent last. Values stay in memory only here and
73
+ * in the rendered /env documents, as before. */
74
+ const snapshots = new Map<string, { key: string; snapshot: TaskEnvSnapshot | "legacy" }>();
75
+ const MAX_SNAPSHOTS = 256;
76
+
77
+ /** Forget a task's read, when its gateway generation goes. */
78
+ export function forgetTaskEnvSnapshot(taskId: string): void {
79
+ snapshots.delete(taskId);
80
+ }
81
+
82
+ function freshTaskEnvSnapshot(taskId: string, projectIds: readonly string[]): TaskEnvSnapshot | "legacy" | null {
83
+ return sidecarMode() ? replicaTaskEnvSnapshot(taskId, projectIds) : ownedTaskEnvSnapshot(taskId, projectIds);
84
+ }
85
+
86
+ /** An owned runner is the authority for its own records. */
87
+ function ownedTaskEnvSnapshot(taskId: string, projectIds: readonly string[]): TaskEnvSnapshot | "legacy" | null {
88
+ const read = readProjectEnvAuthoritySnapshot(projectIds);
89
+ if (read === null) return null;
90
+ if (read.mode === "legacy") return "legacy";
91
+ return snapshotOf(taskId, projectIds, (projectId) => read.projects.find((project) => project.projectId === projectId)?.entries ?? []);
92
+ }
93
+
94
+ /**
95
+ * On a task machine the cloud is the authority, and the sidecar keeps a
96
+ * replica of its task's records (ADR-153 §4, "A machine's replica"): once it
97
+ * has been activated, the task's view comes from it, and a key whose value
98
+ * the sidecar has not installed gets none. Before that, the values the
99
+ * sidecar reads from the store by name, as before.
100
+ */
101
+ function replicaTaskEnvSnapshot(taskId: string, projectIds: readonly string[]): TaskEnvSnapshot | "legacy" | null {
102
+ let activated: boolean;
103
+ try {
104
+ activated = readProjectEnvReplicaStatus().activated;
105
+ } catch {
106
+ return null;
107
+ }
108
+ if (!activated) return "legacy";
109
+ const read = readProjectEnvReplicaSnapshot(projectIds);
110
+ if (read === null) return null;
111
+ return snapshotOf(taskId, projectIds, (projectId) =>
112
+ (read.projects.find((project) => project.projectId === projectId)?.entries ?? []).map((entry) => ({
113
+ key: entry.state.key,
114
+ state: entry.state,
115
+ value: entry.materialization.kind === "installed" ? entry.value : null,
116
+ })),
117
+ );
118
+ }
119
+
120
+ interface StoreEntry {
121
+ key: string;
122
+ /** The record; null on a store that predates records. */
123
+ state: { head: TaskEnvHead; binding: unknown; policyEpoch: unknown } | null;
124
+ /** Null where the store holds no value for the record. */
125
+ value: string | null;
126
+ }
127
+
128
+ /** The task's snapshot of one read of the store, with its observations. */
129
+ function snapshotOf(taskId: string, projectIds: readonly string[], entriesOf: (projectId: string) => readonly StoreEntry[]): TaskEnvSnapshot {
130
+ const projects = projectIds.map((projectId) => {
131
+ const entries = entriesOf(projectId);
132
+ return {
133
+ projectId,
134
+ // Captured here on the task's first read, from the base task-up
135
+ // recorded before the container started; with none recorded yet, a
136
+ // schema it could not read, so an unbound key is withheld.
137
+ observation: taskEnvSchemaObservation(taskId, projectId) ?? captureTaskEnvSchemas(taskId, [projectId], { final: false })[0],
138
+ records: entries.flatMap((entry) =>
139
+ entry.state === null
140
+ ? []
141
+ : [{ key: entry.key, head: entry.state.head, binding: entry.state.binding, policyEpoch: entry.state.policyEpoch }],
142
+ ),
143
+ entries,
144
+ };
145
+ });
146
+ return {
147
+ projects: projects.map(({ projectId, observation, records }) => ({ projectId, observation, records })),
148
+ valueAt: (projectId: string, key: string, head: TaskEnvHead): string | null => {
149
+ const entry = projects.find((project) => project.projectId === projectId)?.entries.find((candidate) => candidate.key === key);
150
+ return entry?.state != null && sameTaskEnvHead(entry.state.head, head) ? entry.value : null;
151
+ },
152
+ };
153
+ }
154
+
155
+ /** Where the task's /env takes its project values from right now. */
156
+ export function taskEnvSource(taskId: string, spec: TaskProjectEnvSpec): TaskEnvSource {
157
+ const snapshot = readTaskEnvSnapshot(taskId, spec);
158
+ if (snapshot === "legacy") return { kind: "stored" };
159
+ if (snapshot === null) return { kind: "none" };
160
+ return { kind: "view", view: taskEnvView(snapshot) };
161
+ }
162
+
163
+ /** The task's snapshot for its proxy, by task id alone: what its persisted
164
+ * launch spec names. Nothing is proxied from a store never activated. */
165
+ export function proxyTaskEnvSnapshot(taskId: string): TaskEnvSnapshot | null {
166
+ const task = getHostTask(taskId);
167
+ let spec: TaskProjectEnvSpec | null = null;
168
+ try {
169
+ spec = task === null ? null : hostTaskProjectEnvSpec(task);
170
+ } catch {
171
+ return null;
172
+ }
173
+ if (spec === null) return null;
174
+ const snapshot = readTaskEnvSnapshot(taskId, spec);
175
+ return snapshot === "legacy" ? null : snapshot;
176
+ }
177
+
178
+ /** The task's note about the keys it does not get, from the store as it is
179
+ * now; null when it gets every one, or the store gives no view. */
180
+ export function taskEnvWithheldNoteNow(taskId: string, spec: TaskProjectEnvSpec): string | null {
181
+ const source = taskEnvSource(taskId, spec);
182
+ return source.kind === "view" ? taskEnvWithheldNote(source.view.policies) : null;
183
+ }
184
+
185
+ /** The resolver a loader task's gateway generation renders /env with. */
186
+ export function taskEnvResolver(taskId: string, spec: TaskProjectEnvSpec): TaskGatewayEnvResolver {
187
+ return taskProjectEnvResolver(
188
+ spec,
189
+ () => taskTrustEnv(taskId),
190
+ () => taskProxiedEnv(taskId),
191
+ () => taskEnvSource(taskId, spec),
192
+ );
193
+ }
@@ -27,12 +27,15 @@ import {
27
27
  hostWorkspaceWrite,
28
28
  } from "./workspace-files";
29
29
  import { assertSafeHostTaskId } from "../task-identity";
30
+ import { applyProjectEnvProfile } from "../runtime-authority";
30
31
  import {
31
32
  capturedCliExec,
32
33
  streamingCliExec,
33
34
  } from "./docker";
34
35
  import {
36
+ assertTaskEnvironmentExecRequest,
35
37
  assertTaskEnvironmentProcessRequest,
38
+ refusePrivilegedExec,
36
39
  assertTaskEnvironmentSessionRequest,
37
40
  type TaskEnvironmentCopyRequest,
38
41
  type TaskEnvironmentDescriptor,
@@ -43,6 +46,7 @@ import {
43
46
  type TaskEnvironmentLocator,
44
47
  type TaskEnvironmentPort,
45
48
  type TaskEnvironmentProcess,
49
+ type ProjectEnvMode,
46
50
  type TaskEnvironmentProvider,
47
51
  type TaskEnvironmentRecoveryContext,
48
52
  type TaskEnvironmentRecoveryResult,
@@ -129,7 +133,11 @@ export type AppleTaskEnvironmentRecoveryDriver = (
129
133
  export interface AppleContainerProvisioningDeps<TInput, TCredentials, TResult> {
130
134
  /** Runs task-up with the selection environment already published by the
131
135
  * runtime pin (UAI_TASK_RUNTIME / UAI_CONTAINER_CLI). */
132
- taskUp: (input: TInput, credentials?: TCredentials) => Promise<TResult>;
136
+ taskUp: (
137
+ input: TInput,
138
+ credentials?: TCredentials,
139
+ envMode?: ProjectEnvMode,
140
+ ) => Promise<TResult>;
133
141
  inputTaskId: (input: TInput) => string;
134
142
  /** The host worktree path this task will occupy, derivable before task-up
135
143
  * runs so a crash mid-provision still leaves a recoverable locator. */
@@ -184,6 +192,7 @@ export function createAppleContainerTaskEnvironmentProvider<
184
192
  const result = await provisioning.taskUp(
185
193
  request.input,
186
194
  request.credentials,
195
+ request.envMode ?? "loader",
187
196
  );
188
197
  const prepared = parseAppleTaskEnvironmentLocator(preparedLocator);
189
198
  // task-up composes its worktree path in shell from the operator's
@@ -202,11 +211,12 @@ export function createAppleContainerTaskEnvironmentProvider<
202
211
  handle: new AppleContainerTaskEnvironmentHandle(
203
212
  preparedLocator,
204
213
  resolved,
214
+ request.envMode ?? "loader",
205
215
  ),
206
216
  result,
207
217
  };
208
218
  },
209
- async reconstruct(locator) {
219
+ async reconstruct(locator, envMode = "baked") {
210
220
  const persisted = parseAppleTaskEnvironmentLocator(locator);
211
221
  const current = validateAppleMachineIdentity(resolved.machineIdentity());
212
222
  if (
@@ -217,7 +227,7 @@ export function createAppleContainerTaskEnvironmentProvider<
217
227
  "task environment belongs to a different container backend",
218
228
  );
219
229
  }
220
- return new AppleContainerTaskEnvironmentHandle(locator, resolved);
230
+ return new AppleContainerTaskEnvironmentHandle(locator, resolved, envMode);
221
231
  },
222
232
  };
223
233
  }
@@ -266,12 +276,14 @@ class AppleContainerTaskEnvironmentHandle
266
276
  constructor(
267
277
  locator: TaskEnvironmentLocator,
268
278
  private readonly deps: ResolvedAppleContainerDeps,
279
+ envMode: ProjectEnvMode,
269
280
  ) {
270
281
  this.#value = parseAppleTaskEnvironmentLocator(locator);
271
282
  this.durableIdentity = this.#value.containerName;
272
283
  this.descriptor = {
273
284
  taskId: this.#value.taskId,
274
285
  locator,
286
+ envMode,
275
287
  workspacePath: "/workspace",
276
288
  };
277
289
  }
@@ -294,12 +306,19 @@ class AppleContainerTaskEnvironmentHandle
294
306
  }
295
307
 
296
308
  exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
297
- assertTaskEnvironmentProcessRequest(request);
309
+ assertTaskEnvironmentExecRequest(request);
310
+ refusePrivilegedExec(request);
311
+ const profiled = applyProjectEnvProfile(
312
+ request,
313
+ this.descriptor.envMode ?? "baked",
314
+ "/usr/bin/zsh",
315
+ );
316
+ request.admit?.();
298
317
  return capturedCliExec(
299
318
  this.deps.spawn,
300
319
  this.deps.containerCliPath(),
301
- appleExecArgs(this.#value.containerName, request, "interactive"),
302
- request,
320
+ appleExecArgs(this.#value.containerName, profiled, "interactive"),
321
+ profiled,
303
322
  );
304
323
  }
305
324
 
@@ -307,11 +326,16 @@ class AppleContainerTaskEnvironmentHandle
307
326
  request: TaskEnvironmentSpawnRequest,
308
327
  ): Promise<TaskEnvironmentProcess> {
309
328
  assertTaskEnvironmentProcessRequest(request);
329
+ const profiled = applyProjectEnvProfile(
330
+ request,
331
+ this.descriptor.envMode ?? "baked",
332
+ "/usr/bin/zsh",
333
+ );
310
334
  return streamingCliExec(
311
335
  this.deps.spawn,
312
336
  this.deps.containerCliPath(),
313
- appleExecArgs(this.#value.containerName, request, "interactive"),
314
- request,
337
+ appleExecArgs(this.#value.containerName, profiled, "interactive"),
338
+ profiled,
315
339
  );
316
340
  }
317
341
 
@@ -319,11 +343,16 @@ class AppleContainerTaskEnvironmentHandle
319
343
  request: TaskEnvironmentSessionRequest,
320
344
  ): Promise<TaskEnvironmentProcess> {
321
345
  assertTaskEnvironmentSessionRequest(request);
346
+ const profiled = applyProjectEnvProfile(
347
+ request,
348
+ this.descriptor.envMode ?? "baked",
349
+ "/usr/bin/zsh",
350
+ );
322
351
  return streamingCliExec(
323
352
  this.deps.spawn,
324
353
  this.deps.containerCliPath(),
325
- appleExecArgs(this.#value.containerName, request, "interactive"),
326
- request,
354
+ appleExecArgs(this.#value.containerName, profiled, "interactive"),
355
+ profiled,
327
356
  );
328
357
  }
329
358
 
@@ -331,8 +360,13 @@ class AppleContainerTaskEnvironmentHandle
331
360
  request: TaskEnvironmentDetachedSessionRequest,
332
361
  ): Promise<TaskEnvironmentExecResult> {
333
362
  assertTaskEnvironmentSessionRequest(request);
363
+ const profiled = applyProjectEnvProfile(
364
+ request,
365
+ this.descriptor.envMode ?? "baked",
366
+ "/usr/bin/zsh",
367
+ );
334
368
  const result = await this.deps.run(
335
- appleExecArgs(this.#value.containerName, request, "detached"),
369
+ appleExecArgs(this.#value.containerName, profiled, "detached"),
336
370
  {
337
371
  timeoutMs: request.launchTimeoutMs,
338
372
  maxOutputBytes: request.maxOutputBytes,
@@ -22,7 +22,9 @@ import {
22
22
  import { taskDir } from "../env";
23
23
  import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
24
24
  import { assertSafeHostTaskId } from "../task-identity";
25
+ import { applyProjectEnvProfile } from "../runtime-authority";
25
26
  import {
27
+ assertTaskEnvironmentExecRequest,
26
28
  assertTaskEnvironmentProcessRequest,
27
29
  assertTaskEnvironmentSessionRequest,
28
30
  type TaskEnvironmentCopyRequest,
@@ -35,6 +37,7 @@ import {
35
37
  type TaskEnvironmentPort,
36
38
  type TaskEnvironmentProcess,
37
39
  type TaskEnvironmentProcessExit,
40
+ type ProjectEnvMode,
38
41
  type TaskEnvironmentProvider,
39
42
  type TaskEnvironmentRecoveryContext,
40
43
  type TaskEnvironmentRecoveryResult,
@@ -73,6 +76,7 @@ export interface DockerTaskEnvironmentDeps {
73
76
  taskUp?: (
74
77
  input: TaskLaunchInput,
75
78
  credentials?: TaskUpCredentials,
79
+ envMode?: ProjectEnvMode,
76
80
  ) => Promise<TaskUpResult>;
77
81
  taskEnvironmentDown?: (taskId: string) => Promise<TaskDownResult>;
78
82
  taskStatus?: (taskId: string) => Promise<TaskStatusResult>;
@@ -123,7 +127,11 @@ export function createDockerTaskEnvironmentProvider(
123
127
  machine,
124
128
  );
125
129
  await request.onPrepared?.(preparedLocator);
126
- const result = await resolved.taskUp(request.input, request.credentials);
130
+ const result = await resolved.taskUp(
131
+ request.input,
132
+ request.credentials,
133
+ request.envMode ?? "loader",
134
+ );
127
135
  const prepared = parseDockerTaskEnvironmentLocator(preparedLocator);
128
136
  // task-up composes its worktree path in shell from the operator's
129
137
  // UAI_WORKSPACE_ROOT verbatim, so a benign trailing slash would fail a
@@ -145,11 +153,15 @@ export function createDockerTaskEnvironmentProvider(
145
153
  machine,
146
154
  );
147
155
  return {
148
- handle: new DockerTaskEnvironmentHandle(locator, resolved),
156
+ handle: new DockerTaskEnvironmentHandle(
157
+ locator,
158
+ resolved,
159
+ request.envMode ?? "loader",
160
+ ),
149
161
  result,
150
162
  };
151
163
  },
152
- async reconstruct(locator) {
164
+ async reconstruct(locator, envMode = "baked") {
153
165
  const persisted = parseDockerTaskEnvironmentLocator(locator);
154
166
  const current = validateMachineIdentity(resolved.machineIdentity());
155
167
  if (
@@ -161,7 +173,7 @@ export function createDockerTaskEnvironmentProvider(
161
173
  "task environment belongs to a different container backend",
162
174
  );
163
175
  }
164
- return new DockerTaskEnvironmentHandle(locator, resolved);
176
+ return new DockerTaskEnvironmentHandle(locator, resolved, envMode);
165
177
  },
166
178
  };
167
179
  }
@@ -256,12 +268,14 @@ class DockerTaskEnvironmentHandle
256
268
  DockerTaskEnvironmentDeps,
257
269
  "recover" | "recoveryDriver" | "taskStatus"
258
270
  >,
271
+ envMode: ProjectEnvMode,
259
272
  ) {
260
273
  this.#value = parseDockerTaskEnvironmentLocator(locator);
261
274
  this.durableIdentity = this.#value.containerName;
262
275
  this.descriptor = {
263
276
  taskId: this.#value.taskId,
264
277
  locator,
278
+ envMode,
265
279
  workspacePath: "/workspace",
266
280
  ...(this.#value.codeServerPort === undefined
267
281
  ? {}
@@ -293,12 +307,19 @@ class DockerTaskEnvironmentHandle
293
307
  }
294
308
 
295
309
  exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
296
- assertTaskEnvironmentProcessRequest(request);
310
+ assertTaskEnvironmentExecRequest(request);
311
+ const { privileged, ...unprivileged } = request;
312
+ const profiled = applyProjectEnvProfile(
313
+ unprivileged,
314
+ this.descriptor.envMode ?? "baked",
315
+ "/usr/bin/zsh",
316
+ );
317
+ request.admit?.();
297
318
  return capturedCliExec(
298
319
  this.deps.spawn,
299
320
  "docker",
300
- dockerExecArgs(this.#value.containerName, request),
301
- request,
321
+ dockerExecArgs(this.#value.containerName, profiled, "interactive", privileged === true),
322
+ profiled,
302
323
  );
303
324
  }
304
325
 
@@ -306,11 +327,16 @@ class DockerTaskEnvironmentHandle
306
327
  request: TaskEnvironmentSpawnRequest,
307
328
  ): Promise<TaskEnvironmentProcess> {
308
329
  assertTaskEnvironmentProcessRequest(request);
330
+ const profiled = applyProjectEnvProfile(
331
+ request,
332
+ this.descriptor.envMode ?? "baked",
333
+ "/usr/bin/zsh",
334
+ );
309
335
  return streamingCliExec(
310
336
  this.deps.spawn,
311
337
  "docker",
312
- dockerExecArgs(this.#value.containerName, request),
313
- request,
338
+ dockerExecArgs(this.#value.containerName, profiled),
339
+ profiled,
314
340
  );
315
341
  }
316
342
 
@@ -318,11 +344,16 @@ class DockerTaskEnvironmentHandle
318
344
  request: TaskEnvironmentSessionRequest,
319
345
  ): Promise<TaskEnvironmentProcess> {
320
346
  assertTaskEnvironmentSessionRequest(request);
347
+ const profiled = applyProjectEnvProfile(
348
+ request,
349
+ this.descriptor.envMode ?? "baked",
350
+ "/usr/bin/zsh",
351
+ );
321
352
  return streamingCliExec(
322
353
  this.deps.spawn,
323
354
  "docker",
324
- dockerExecArgs(this.#value.containerName, request),
325
- request,
355
+ dockerExecArgs(this.#value.containerName, profiled),
356
+ profiled,
326
357
  );
327
358
  }
328
359
 
@@ -330,8 +361,13 @@ class DockerTaskEnvironmentHandle
330
361
  request: TaskEnvironmentDetachedSessionRequest,
331
362
  ): Promise<TaskEnvironmentExecResult> {
332
363
  assertTaskEnvironmentSessionRequest(request);
364
+ const profiled = applyProjectEnvProfile(
365
+ request,
366
+ this.descriptor.envMode ?? "baked",
367
+ "/usr/bin/zsh",
368
+ );
333
369
  const result = await this.deps.docker(
334
- dockerExecArgs(this.#value.containerName, request, "detached"),
370
+ dockerExecArgs(this.#value.containerName, profiled, "detached"),
335
371
  {
336
372
  timeoutMs: request.launchTimeoutMs,
337
373
  maxOutputBytes: request.maxOutputBytes,
@@ -606,8 +642,11 @@ function dockerExecArgs(
606
642
  | TaskEnvironmentSessionRequest
607
643
  | TaskEnvironmentDetachedSessionRequest,
608
644
  mode: "interactive" | "detached" = "interactive",
645
+ /** Passed only by exec(), after assertTaskEnvironmentExecRequest. */
646
+ privileged = false,
609
647
  ): string[] {
610
648
  const args = ["exec", mode === "detached" ? "-d" : "-i"];
649
+ if (privileged) args.push("--privileged");
611
650
  if (request.cwd) args.push("--workdir", request.cwd);
612
651
  if (request.user) args.push("--user", request.user);
613
652
  for (const name of request.inheritEnv ?? []) {
@@ -33,6 +33,7 @@ import { createMachineHostTaskEnvironmentProvider } from "./machine-task-up";
33
33
  import { TaskEnvironmentRegistry } from "./registry";
34
34
  import {
35
35
  parseTaskEnvironmentLocator,
36
+ type ProjectEnvMode,
36
37
  type TaskEnvironmentHandle,
37
38
  type TaskEnvironmentLocator,
38
39
  type TaskEnvironmentProvisioned,
@@ -47,6 +48,7 @@ export {
47
48
  import { sidecarMode } from "../sidecar";
48
49
  import { mountSharedFilesFromLease } from "../shared-files-s3";
49
50
  import { createSelfTaskEnvironmentProvider } from "./self";
51
+ import { hostTaskEnvMode } from "../runtime-state";
50
52
 
51
53
  const registry = new TaskEnvironmentRegistry<
52
54
  TaskLaunchInput,
@@ -215,6 +217,7 @@ export function provisionTaskEnvironment(
215
217
  : dockerProvider;
216
218
  return provider.provision({
217
219
  taskId: input.task.id,
220
+ envMode: "loader",
218
221
  input,
219
222
  credentials,
220
223
  onPrepared,
@@ -223,8 +226,9 @@ export function provisionTaskEnvironment(
223
226
 
224
227
  export function reconstructTaskEnvironment(
225
228
  locator: TaskEnvironmentLocator,
229
+ envMode: ProjectEnvMode,
226
230
  ): Promise<TaskEnvironmentHandle<TaskDownResult>> {
227
- return registry.reconstruct(locator);
231
+ return registry.reconstruct(locator, envMode);
228
232
  }
229
233
 
230
234
  export async function reconstructPersistedTaskEnvironment(
@@ -240,7 +244,7 @@ export async function reconstructPersistedTaskEnvironment(
240
244
  : requireMachineIdentity(),
241
245
  );
242
246
  if (locator === null) return null;
243
- const handle = await registry.reconstruct(locator);
247
+ const handle = await registry.reconstruct(locator, hostTaskEnvMode(task));
244
248
  if (handle.descriptor.taskId !== task.taskId) {
245
249
  throw new Error("task environment locator belongs to a different task");
246
250
  }