@runuai/host 0.9.148 → 0.9.150
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/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/codex.ts +9 -1
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +2 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/transport.ts +9 -5
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +408 -81
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +939 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2219 -15
|
@@ -23,9 +23,12 @@ import type {
|
|
|
23
23
|
MachineProvider,
|
|
24
24
|
} from "../machine-provider";
|
|
25
25
|
import { machineEnvironmentSshArgs } from "../machine-exec";
|
|
26
|
+
import { applyProjectEnvProfile } from "../runtime-authority";
|
|
26
27
|
import { capturedCliExec, streamingCliExec } from "./docker";
|
|
27
28
|
import {
|
|
29
|
+
assertTaskEnvironmentExecRequest,
|
|
28
30
|
assertTaskEnvironmentProcessRequest,
|
|
31
|
+
refusePrivilegedExec,
|
|
29
32
|
assertTaskEnvironmentSessionRequest,
|
|
30
33
|
assertWorkspaceScopedPath,
|
|
31
34
|
type TaskEnvironmentCopyRequest,
|
|
@@ -37,6 +40,7 @@ import {
|
|
|
37
40
|
type TaskEnvironmentLocator,
|
|
38
41
|
type TaskEnvironmentPort,
|
|
39
42
|
type TaskEnvironmentProcess,
|
|
43
|
+
type ProjectEnvMode,
|
|
40
44
|
type TaskEnvironmentProvider,
|
|
41
45
|
type TaskEnvironmentProvisionRequest,
|
|
42
46
|
type TaskEnvironmentProvisioned,
|
|
@@ -185,6 +189,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
185
189
|
constructor(
|
|
186
190
|
locator: TaskEnvironmentLocator,
|
|
187
191
|
deps: MachineTaskEnvironmentDeps,
|
|
192
|
+
envMode: ProjectEnvMode,
|
|
188
193
|
) {
|
|
189
194
|
this.#value = parseMachineTaskEnvironmentLocator(locator);
|
|
190
195
|
this.#deps = {
|
|
@@ -197,6 +202,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
197
202
|
this.descriptor = {
|
|
198
203
|
taskId: this.#value.taskId,
|
|
199
204
|
locator,
|
|
205
|
+
envMode,
|
|
200
206
|
workspacePath: this.#value.workspacePath,
|
|
201
207
|
};
|
|
202
208
|
}
|
|
@@ -242,6 +248,24 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
242
248
|
return info;
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
#maintenanceRequest<
|
|
252
|
+
T extends {
|
|
253
|
+
argv: readonly [string, ...string[]];
|
|
254
|
+
cwd?: string;
|
|
255
|
+
user?: string;
|
|
256
|
+
inheritEnv?: readonly string[];
|
|
257
|
+
env?: Readonly<Record<string, string>>;
|
|
258
|
+
},
|
|
259
|
+
>(
|
|
260
|
+
request: T,
|
|
261
|
+
): T & { projectEnvProfile: "maintenance" } {
|
|
262
|
+
return applyProjectEnvProfile(
|
|
263
|
+
{ ...request, projectEnvProfile: "maintenance" as const },
|
|
264
|
+
this.descriptor.envMode ?? "baked",
|
|
265
|
+
"/bin/bash",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
245
269
|
async readWorkspaceFile(path: string): Promise<Uint8Array | null> {
|
|
246
270
|
assertWorkspaceScopedPath(this.descriptor, path);
|
|
247
271
|
const target = await this.#target();
|
|
@@ -250,7 +274,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
250
274
|
"ssh",
|
|
251
275
|
machineEnvironmentSshArgs(
|
|
252
276
|
target,
|
|
253
|
-
{
|
|
277
|
+
this.#maintenanceRequest({
|
|
254
278
|
argv: [
|
|
255
279
|
"/bin/sh",
|
|
256
280
|
"-c",
|
|
@@ -260,11 +284,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
260
284
|
"read",
|
|
261
285
|
path,
|
|
262
286
|
],
|
|
263
|
-
},
|
|
287
|
+
}),
|
|
264
288
|
"interactive",
|
|
265
289
|
),
|
|
266
290
|
{
|
|
267
291
|
argv: ["/bin/cat"],
|
|
292
|
+
projectEnvProfile: "maintenance",
|
|
268
293
|
timeoutMs: 60_000,
|
|
269
294
|
maxOutputBytes: 64 * 1024 * 1024,
|
|
270
295
|
},
|
|
@@ -286,7 +311,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
286
311
|
"ssh",
|
|
287
312
|
machineEnvironmentSshArgs(
|
|
288
313
|
target,
|
|
289
|
-
{
|
|
314
|
+
this.#maintenanceRequest({
|
|
290
315
|
argv: [
|
|
291
316
|
"/bin/sh",
|
|
292
317
|
"-c",
|
|
@@ -294,11 +319,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
294
319
|
"write",
|
|
295
320
|
path,
|
|
296
321
|
],
|
|
297
|
-
},
|
|
322
|
+
}),
|
|
298
323
|
"interactive",
|
|
299
324
|
),
|
|
300
325
|
{
|
|
301
326
|
argv: ["/bin/sh"],
|
|
327
|
+
projectEnvProfile: "maintenance",
|
|
302
328
|
stdin: bytes,
|
|
303
329
|
timeoutMs: 120_000,
|
|
304
330
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -314,13 +340,20 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
314
340
|
async exec(
|
|
315
341
|
request: TaskEnvironmentExecRequest,
|
|
316
342
|
): Promise<TaskEnvironmentExecResult> {
|
|
317
|
-
|
|
343
|
+
assertTaskEnvironmentExecRequest(request);
|
|
344
|
+
refusePrivilegedExec(request);
|
|
345
|
+
const profiled = applyProjectEnvProfile(
|
|
346
|
+
request,
|
|
347
|
+
this.descriptor.envMode ?? "baked",
|
|
348
|
+
"/bin/bash",
|
|
349
|
+
);
|
|
318
350
|
const target = await this.#target();
|
|
351
|
+
request.admit?.();
|
|
319
352
|
return capturedCliExec(
|
|
320
353
|
this.#deps.spawn,
|
|
321
354
|
"ssh",
|
|
322
|
-
machineEnvironmentSshArgs(target,
|
|
323
|
-
|
|
355
|
+
machineEnvironmentSshArgs(target, profiled, "interactive"),
|
|
356
|
+
profiled,
|
|
324
357
|
);
|
|
325
358
|
}
|
|
326
359
|
|
|
@@ -328,12 +361,17 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
328
361
|
request: TaskEnvironmentSpawnRequest,
|
|
329
362
|
): Promise<TaskEnvironmentProcess> {
|
|
330
363
|
assertTaskEnvironmentProcessRequest(request);
|
|
364
|
+
const profiled = applyProjectEnvProfile(
|
|
365
|
+
request,
|
|
366
|
+
this.descriptor.envMode ?? "baked",
|
|
367
|
+
"/bin/bash",
|
|
368
|
+
);
|
|
331
369
|
const target = await this.#target();
|
|
332
370
|
return streamingCliExec(
|
|
333
371
|
this.#deps.spawn,
|
|
334
372
|
"ssh",
|
|
335
|
-
machineEnvironmentSshArgs(target,
|
|
336
|
-
|
|
373
|
+
machineEnvironmentSshArgs(target, profiled, "interactive"),
|
|
374
|
+
profiled,
|
|
337
375
|
);
|
|
338
376
|
}
|
|
339
377
|
|
|
@@ -343,10 +381,10 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
343
381
|
assertTaskEnvironmentSessionRequest(request);
|
|
344
382
|
// Sessions default to the workspace the way container images default to
|
|
345
383
|
// WORKDIR /workspace — an agent CLI must start in the task's world.
|
|
346
|
-
const sessionRequest = {
|
|
384
|
+
const sessionRequest = applyProjectEnvProfile({
|
|
347
385
|
...request,
|
|
348
386
|
cwd: request.cwd ?? this.#value.workspacePath,
|
|
349
|
-
};
|
|
387
|
+
}, this.descriptor.envMode ?? "baked", "/bin/bash");
|
|
350
388
|
const target = await this.#target();
|
|
351
389
|
return streamingCliExec(
|
|
352
390
|
this.#deps.spawn,
|
|
@@ -360,10 +398,10 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
360
398
|
request: TaskEnvironmentDetachedSessionRequest,
|
|
361
399
|
): Promise<TaskEnvironmentExecResult> {
|
|
362
400
|
assertTaskEnvironmentSessionRequest(request);
|
|
363
|
-
const sessionRequest = {
|
|
401
|
+
const sessionRequest = applyProjectEnvProfile({
|
|
364
402
|
...request,
|
|
365
403
|
cwd: request.cwd ?? this.#value.workspacePath,
|
|
366
|
-
};
|
|
404
|
+
}, this.descriptor.envMode ?? "baked", "/bin/bash");
|
|
367
405
|
const target = await this.#target();
|
|
368
406
|
return capturedCliExec(
|
|
369
407
|
this.#deps.spawn,
|
|
@@ -402,7 +440,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
402
440
|
"ssh",
|
|
403
441
|
machineEnvironmentSshArgs(
|
|
404
442
|
target,
|
|
405
|
-
{
|
|
443
|
+
this.#maintenanceRequest({
|
|
406
444
|
argv: [
|
|
407
445
|
"/bin/sh",
|
|
408
446
|
"-c",
|
|
@@ -410,11 +448,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
410
448
|
"copy",
|
|
411
449
|
request.destination,
|
|
412
450
|
],
|
|
413
|
-
},
|
|
451
|
+
}),
|
|
414
452
|
"interactive",
|
|
415
453
|
),
|
|
416
454
|
{
|
|
417
455
|
argv: ["/bin/sh"],
|
|
456
|
+
projectEnvProfile: "maintenance",
|
|
418
457
|
stdin: tarball.stdout,
|
|
419
458
|
timeoutMs: 300_000,
|
|
420
459
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -433,7 +472,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
433
472
|
"ssh",
|
|
434
473
|
machineEnvironmentSshArgs(
|
|
435
474
|
target,
|
|
436
|
-
{
|
|
475
|
+
this.#maintenanceRequest({
|
|
437
476
|
argv: [
|
|
438
477
|
"/bin/sh",
|
|
439
478
|
"-c",
|
|
@@ -441,11 +480,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
441
480
|
"copy",
|
|
442
481
|
request.destination,
|
|
443
482
|
],
|
|
444
|
-
},
|
|
483
|
+
}),
|
|
445
484
|
"interactive",
|
|
446
485
|
),
|
|
447
486
|
{
|
|
448
487
|
argv: ["/bin/sh"],
|
|
488
|
+
projectEnvProfile: "maintenance",
|
|
449
489
|
stdin: bytes,
|
|
450
490
|
timeoutMs: 120_000,
|
|
451
491
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -463,11 +503,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
463
503
|
"ssh",
|
|
464
504
|
machineEnvironmentSshArgs(
|
|
465
505
|
target,
|
|
466
|
-
{ argv: ["/bin/cat", request.source] },
|
|
506
|
+
this.#maintenanceRequest({ argv: ["/bin/cat", request.source] }),
|
|
467
507
|
"interactive",
|
|
468
508
|
),
|
|
469
509
|
{
|
|
470
510
|
argv: ["/bin/cat"],
|
|
511
|
+
projectEnvProfile: "maintenance",
|
|
471
512
|
timeoutMs: 120_000,
|
|
472
513
|
maxOutputBytes: 64 * 1024 * 1024,
|
|
473
514
|
},
|
|
@@ -572,11 +613,14 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
572
613
|
"ssh",
|
|
573
614
|
machineEnvironmentSshArgs(
|
|
574
615
|
target,
|
|
575
|
-
|
|
616
|
+
this.#maintenanceRequest({
|
|
617
|
+
argv: ["/usr/bin/test", "-d", this.#value.workspacePath],
|
|
618
|
+
}),
|
|
576
619
|
"interactive",
|
|
577
620
|
),
|
|
578
621
|
{
|
|
579
622
|
argv: ["/usr/bin/test"],
|
|
623
|
+
projectEnvProfile: "maintenance",
|
|
580
624
|
timeoutMs: 15_000,
|
|
581
625
|
maxOutputBytes: 4 * 1024,
|
|
582
626
|
},
|
|
@@ -618,6 +662,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
618
662
|
if (!context.isCurrent()) {
|
|
619
663
|
return { outcome: "deferred", detail: "runtime generation superseded" };
|
|
620
664
|
}
|
|
665
|
+
await context.prepareConsumers?.();
|
|
621
666
|
context.allowConsumers?.();
|
|
622
667
|
await context.ensureConsumersStarted?.();
|
|
623
668
|
if (!context.isCurrent()) {
|
|
@@ -633,9 +678,14 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
633
678
|
const result = await capturedCliExec(
|
|
634
679
|
this.#deps.spawn,
|
|
635
680
|
"ssh",
|
|
636
|
-
machineEnvironmentSshArgs(
|
|
681
|
+
machineEnvironmentSshArgs(
|
|
682
|
+
target,
|
|
683
|
+
this.#maintenanceRequest({ argv: ["/bin/true"] }),
|
|
684
|
+
"interactive",
|
|
685
|
+
),
|
|
637
686
|
{
|
|
638
687
|
argv: ["/bin/true"],
|
|
688
|
+
projectEnvProfile: "maintenance",
|
|
639
689
|
timeoutMs: 15_000,
|
|
640
690
|
maxOutputBytes: 4 * 1024,
|
|
641
691
|
},
|
|
@@ -745,7 +795,11 @@ export function createMachineTaskEnvironmentProvider(
|
|
|
745
795
|
}
|
|
746
796
|
// running/pending: fall through to the ssh-readiness proof below.
|
|
747
797
|
|
|
748
|
-
const handle = new MachineTaskEnvironmentHandle(
|
|
798
|
+
const handle = new MachineTaskEnvironmentHandle(
|
|
799
|
+
locator,
|
|
800
|
+
deps,
|
|
801
|
+
request.envMode ?? "loader",
|
|
802
|
+
);
|
|
749
803
|
await waitForSsh(handle, deps.readyTimeoutMs ?? 120_000);
|
|
750
804
|
|
|
751
805
|
// Workspace + optional in-env clone (ADR-121: the clone happens in the
|
|
@@ -762,6 +816,7 @@ export function createMachineTaskEnvironmentProvider(
|
|
|
762
816
|
workspacePath,
|
|
763
817
|
],
|
|
764
818
|
user: "root",
|
|
819
|
+
projectEnvProfile: "maintenance",
|
|
765
820
|
timeoutMs: 30_000,
|
|
766
821
|
maxOutputBytes: 64 * 1024,
|
|
767
822
|
});
|
|
@@ -780,6 +835,7 @@ export function createMachineTaskEnvironmentProvider(
|
|
|
780
835
|
];
|
|
781
836
|
const clone = await handle.exec({
|
|
782
837
|
argv: cloneArgv,
|
|
838
|
+
projectEnvProfile: "maintenance",
|
|
783
839
|
timeoutMs: 10 * 60_000,
|
|
784
840
|
maxOutputBytes: 4 * 1024 * 1024,
|
|
785
841
|
});
|
|
@@ -794,8 +850,9 @@ export function createMachineTaskEnvironmentProvider(
|
|
|
794
850
|
|
|
795
851
|
async reconstruct(
|
|
796
852
|
locator: TaskEnvironmentLocator,
|
|
853
|
+
envMode: ProjectEnvMode = "baked",
|
|
797
854
|
): Promise<TaskEnvironmentHandle> {
|
|
798
|
-
return new MachineTaskEnvironmentHandle(locator, deps);
|
|
855
|
+
return new MachineTaskEnvironmentHandle(locator, deps, envMode);
|
|
799
856
|
},
|
|
800
857
|
};
|
|
801
858
|
}
|
|
@@ -810,6 +867,7 @@ async function waitForSsh(
|
|
|
810
867
|
try {
|
|
811
868
|
const probe = await handle.exec({
|
|
812
869
|
argv: ["/bin/true"],
|
|
870
|
+
projectEnvProfile: "maintenance",
|
|
813
871
|
timeoutMs: 10_000,
|
|
814
872
|
maxOutputBytes: 4 * 1024,
|
|
815
873
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ProjectEnvMode,
|
|
2
3
|
TaskEnvironmentLocator,
|
|
3
4
|
TaskEnvironmentProvider,
|
|
4
5
|
} from "./types";
|
|
@@ -48,8 +49,8 @@ export class TaskEnvironmentRegistry<
|
|
|
48
49
|
return provider;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
reconstruct(locator: TaskEnvironmentLocator) {
|
|
52
|
-
return this.provider(locator.provider).reconstruct(locator);
|
|
52
|
+
reconstruct(locator: TaskEnvironmentLocator, envMode: ProjectEnvMode = "baked") {
|
|
53
|
+
return this.provider(locator.provider).reconstruct(locator, envMode);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
kinds(): string[] {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `self` provider's fixed names: a Uai Cloud machine's own task, run by
|
|
3
|
+
* the sidecar on that machine (ADR-133). A module with no imports, so the
|
|
4
|
+
* secrets proxy can name them without loading the provider, which reaches it
|
|
5
|
+
* back through the agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const SELF_TASK_ENVIRONMENT_PROVIDER = "self";
|
|
9
|
+
|
|
10
|
+
/** The task user every agent, editor, and shell runs as. */
|
|
11
|
+
export const SELF_TASK_USER = "node";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A task process's own view of the hosts on its machine (ADR-153): the
|
|
15
|
+
* machine's /etc/hosts plus the secrets proxy's pins, which the root
|
|
16
|
+
* sidecar's own processes must never resolve. Every process the sidecar
|
|
17
|
+
* starts as the task user gets it bind-mounted over /etc/hosts, in a mount
|
|
18
|
+
* namespace of its own that still follows the machine's mounts.
|
|
19
|
+
*/
|
|
20
|
+
export const TASK_HOSTS_VIEW_PATH = "/run/uai/task-hosts";
|
|
@@ -19,10 +19,16 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
19
19
|
import { readFile } from "node:fs/promises";
|
|
20
20
|
|
|
21
21
|
import { buildRemoteCommand } from "../machine-exec";
|
|
22
|
+
import {
|
|
23
|
+
applyProjectEnvProfile,
|
|
24
|
+
PROJECT_ENV_PROFILE_ENV_NAMES,
|
|
25
|
+
} from "../runtime-authority";
|
|
22
26
|
import { capturedCliExec, streamingCliExec } from "./docker";
|
|
23
27
|
import type { MachineTaskInput } from "./machine";
|
|
24
28
|
import {
|
|
29
|
+
assertTaskEnvironmentExecRequest,
|
|
25
30
|
assertTaskEnvironmentProcessRequest,
|
|
31
|
+
refusePrivilegedExec,
|
|
26
32
|
assertTaskEnvironmentSessionRequest,
|
|
27
33
|
assertWorkspaceScopedPath,
|
|
28
34
|
type TaskEnvironmentCopyRequest,
|
|
@@ -34,6 +40,7 @@ import {
|
|
|
34
40
|
type TaskEnvironmentLocator,
|
|
35
41
|
type TaskEnvironmentPort,
|
|
36
42
|
type TaskEnvironmentProcess,
|
|
43
|
+
type ProjectEnvMode,
|
|
37
44
|
type TaskEnvironmentProvider,
|
|
38
45
|
type TaskEnvironmentProvisionRequest,
|
|
39
46
|
type TaskEnvironmentProvisioned,
|
|
@@ -44,11 +51,29 @@ import {
|
|
|
44
51
|
type TaskEnvironmentStatus,
|
|
45
52
|
TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
|
|
46
53
|
} from "./types";
|
|
54
|
+
import { SELF_TASK_ENVIRONMENT_PROVIDER, SELF_TASK_USER, TASK_HOSTS_VIEW_PATH } from "./self-identity";
|
|
47
55
|
|
|
48
|
-
export
|
|
56
|
+
export { SELF_TASK_ENVIRONMENT_PROVIDER, SELF_TASK_USER, TASK_HOSTS_VIEW_PATH };
|
|
49
57
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Binds the view, then runs the rest of its argv. With no view yet (nothing
|
|
60
|
+
* pinned so far: a start whose CA failed, the proxy off), it first makes one,
|
|
61
|
+
* a copy of the machine's file, so every task process has the view bound
|
|
62
|
+
* from its start and a later pin, rewritten into it in place, reaches them
|
|
63
|
+
* all. Made whole under another name and linked in, so it never shows half
|
|
64
|
+
* written and never replaces one made meanwhile. A view that cannot be
|
|
65
|
+
* bound fails the start: the task never runs with the proxy half in place.
|
|
66
|
+
*/
|
|
67
|
+
const TASK_VIEW_SCRIPT = [
|
|
68
|
+
"PATH=/usr/sbin:/usr/bin:/sbin:/bin; view=$1; shift",
|
|
69
|
+
'if [ ! -f "$view" ]; then',
|
|
70
|
+
' umask 022; dir=${view%/*}; mkdir -p "$dir"',
|
|
71
|
+
' tmp=$(mktemp "$dir/.task-hosts.XXXXXX") || exit 126',
|
|
72
|
+
' cat /etc/hosts > "$tmp" && chmod 0644 "$tmp" && ln "$tmp" "$view" 2>/dev/null; rm -f "$tmp"',
|
|
73
|
+
"fi",
|
|
74
|
+
'mount --bind "$view" /etc/hosts || exit 126',
|
|
75
|
+
'exec "$@"',
|
|
76
|
+
].join("\n");
|
|
52
77
|
|
|
53
78
|
const SAFE_TASK_ID = /^[a-z0-9]{1,32}$/i;
|
|
54
79
|
|
|
@@ -163,23 +188,43 @@ export function selfCommand(
|
|
|
163
188
|
privileged: boolean,
|
|
164
189
|
): { command: string; args: string[] } {
|
|
165
190
|
const line = buildRemoteCommand({ ...request, detached: mode === "detached" });
|
|
191
|
+
const outerProfileEnv = PROJECT_ENV_PROFILE_ENV_NAMES.flatMap((name) => {
|
|
192
|
+
const value = request.env?.[name];
|
|
193
|
+
return value === undefined ? [] : [`${name}=${value}`];
|
|
194
|
+
});
|
|
195
|
+
const shellCommand =
|
|
196
|
+
outerProfileEnv.length === 0
|
|
197
|
+
? { command: "/bin/sh", args: ["-lc", line] }
|
|
198
|
+
: {
|
|
199
|
+
command: "/usr/bin/env",
|
|
200
|
+
args: [...outerProfileEnv, "/bin/sh", "-lc", line],
|
|
201
|
+
};
|
|
166
202
|
const user = request.user ?? SELF_TASK_USER;
|
|
167
203
|
if (privileged && user !== "root") {
|
|
168
204
|
return {
|
|
169
|
-
command: "/usr/bin/
|
|
205
|
+
command: "/usr/bin/unshare",
|
|
170
206
|
args: [
|
|
207
|
+
"--mount",
|
|
208
|
+
"--propagation",
|
|
209
|
+
"slave",
|
|
210
|
+
"--",
|
|
211
|
+
"/bin/sh",
|
|
212
|
+
"-c",
|
|
213
|
+
TASK_VIEW_SCRIPT,
|
|
214
|
+
"uai-task-view",
|
|
215
|
+
TASK_HOSTS_VIEW_PATH,
|
|
216
|
+
"/usr/bin/setpriv",
|
|
171
217
|
`--reuid=${user}`,
|
|
172
218
|
`--regid=${user}`,
|
|
173
219
|
"--init-groups",
|
|
174
220
|
"--reset-env",
|
|
175
221
|
"--",
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
line,
|
|
222
|
+
shellCommand.command,
|
|
223
|
+
...shellCommand.args,
|
|
179
224
|
],
|
|
180
225
|
};
|
|
181
226
|
}
|
|
182
|
-
return
|
|
227
|
+
return shellCommand;
|
|
183
228
|
}
|
|
184
229
|
|
|
185
230
|
class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
@@ -190,7 +235,11 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
190
235
|
readonly #privileged: () => boolean;
|
|
191
236
|
readonly #etcEnvironment: () => Readonly<Record<string, string>>;
|
|
192
237
|
|
|
193
|
-
constructor(
|
|
238
|
+
constructor(
|
|
239
|
+
locator: TaskEnvironmentLocator,
|
|
240
|
+
deps: SelfTaskEnvironmentDeps,
|
|
241
|
+
envMode: ProjectEnvMode,
|
|
242
|
+
) {
|
|
194
243
|
this.#value = parseSelfTaskEnvironmentLocator(locator);
|
|
195
244
|
this.#spawn = deps.spawn ?? nodeSpawn;
|
|
196
245
|
this.#privileged =
|
|
@@ -200,6 +249,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
200
249
|
this.descriptor = {
|
|
201
250
|
taskId: this.#value.taskId,
|
|
202
251
|
locator,
|
|
252
|
+
envMode,
|
|
203
253
|
workspacePath: this.#value.workspacePath,
|
|
204
254
|
};
|
|
205
255
|
}
|
|
@@ -220,6 +270,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
220
270
|
// agents touch (a root-owned file inside the workspace is a trap).
|
|
221
271
|
const result = await this.exec({
|
|
222
272
|
argv: ["/bin/sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1"', "write", path],
|
|
273
|
+
projectEnvProfile: "maintenance",
|
|
223
274
|
stdin: bytes,
|
|
224
275
|
timeoutMs: 120_000,
|
|
225
276
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -233,16 +284,30 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
233
284
|
|
|
234
285
|
/** The request with the machine-wide environment beneath its own: what an
|
|
235
286
|
* ssh session on a machine task gets from pam_env, applied here by hand. */
|
|
236
|
-
#withMachineEnv<
|
|
287
|
+
#withMachineEnv<
|
|
288
|
+
T extends {
|
|
289
|
+
env?: Readonly<Record<string, string>>;
|
|
290
|
+
projectEnvProfile: "maintenance" | "task";
|
|
291
|
+
},
|
|
292
|
+
>(request: T): T {
|
|
237
293
|
const base = this.#etcEnvironment();
|
|
238
|
-
|
|
239
|
-
|
|
294
|
+
const withMachineEnv =
|
|
295
|
+
Object.keys(base).length === 0
|
|
296
|
+
? request
|
|
297
|
+
: { ...request, env: { ...base, ...request.env } };
|
|
298
|
+
return applyProjectEnvProfile(
|
|
299
|
+
withMachineEnv,
|
|
300
|
+
this.descriptor.envMode ?? "baked",
|
|
301
|
+
"/bin/bash",
|
|
302
|
+
);
|
|
240
303
|
}
|
|
241
304
|
|
|
242
305
|
async exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
|
|
243
|
-
|
|
306
|
+
assertTaskEnvironmentExecRequest(request);
|
|
307
|
+
refusePrivilegedExec(request);
|
|
244
308
|
const scoped = this.#withMachineEnv(request);
|
|
245
309
|
const { command, args } = selfCommand(scoped, "interactive", this.#privileged());
|
|
310
|
+
request.admit?.();
|
|
246
311
|
return capturedCliExec(this.#spawn, command, args, scoped);
|
|
247
312
|
}
|
|
248
313
|
|
|
@@ -306,6 +371,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
306
371
|
const result = await this.exec({
|
|
307
372
|
argv: ["/bin/sh", "-c", script, "copy", request.source, request.destination],
|
|
308
373
|
user: "root",
|
|
374
|
+
projectEnvProfile: "maintenance",
|
|
309
375
|
timeoutMs: 5 * 60_000,
|
|
310
376
|
maxOutputBytes: 64 * 1024,
|
|
311
377
|
});
|
|
@@ -341,6 +407,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
|
|
|
341
407
|
if (!context.isCurrent()) {
|
|
342
408
|
return { outcome: "deferred", detail: "runtime generation superseded" };
|
|
343
409
|
}
|
|
410
|
+
await context.prepareConsumers?.();
|
|
344
411
|
context.allowConsumers?.();
|
|
345
412
|
await context.ensureConsumersStarted?.();
|
|
346
413
|
if (!context.isCurrent()) {
|
|
@@ -378,10 +445,15 @@ export function createSelfTaskEnvironmentProvider(
|
|
|
378
445
|
const workspacePath = input.workspacePath ?? "/workspace";
|
|
379
446
|
const locator = selfTaskEnvironmentLocator({ taskId, workspacePath });
|
|
380
447
|
await request.onPrepared?.(locator);
|
|
381
|
-
const handle = new SelfTaskEnvironmentHandle(
|
|
448
|
+
const handle = new SelfTaskEnvironmentHandle(
|
|
449
|
+
locator,
|
|
450
|
+
deps,
|
|
451
|
+
request.envMode ?? "loader",
|
|
452
|
+
);
|
|
382
453
|
const prepare = await handle.exec({
|
|
383
454
|
argv: ["/bin/sh", "-c", 'mkdir -p "$1" && chown node:node "$1"', "prepare", workspacePath],
|
|
384
455
|
user: "root",
|
|
456
|
+
projectEnvProfile: "maintenance",
|
|
385
457
|
timeoutMs: 30_000,
|
|
386
458
|
maxOutputBytes: 64 * 1024,
|
|
387
459
|
});
|
|
@@ -399,6 +471,7 @@ export function createSelfTaskEnvironmentProvider(
|
|
|
399
471
|
input.repo.url,
|
|
400
472
|
workspacePath,
|
|
401
473
|
],
|
|
474
|
+
projectEnvProfile: "maintenance",
|
|
402
475
|
timeoutMs: 10 * 60_000,
|
|
403
476
|
maxOutputBytes: 4 * 1024 * 1024,
|
|
404
477
|
});
|
|
@@ -411,8 +484,11 @@ export function createSelfTaskEnvironmentProvider(
|
|
|
411
484
|
return { handle, result: handle.descriptor };
|
|
412
485
|
},
|
|
413
486
|
|
|
414
|
-
async reconstruct(
|
|
415
|
-
|
|
487
|
+
async reconstruct(
|
|
488
|
+
locator: TaskEnvironmentLocator,
|
|
489
|
+
envMode: ProjectEnvMode = "baked",
|
|
490
|
+
): Promise<TaskEnvironmentHandle> {
|
|
491
|
+
return new SelfTaskEnvironmentHandle(locator, deps, envMode);
|
|
416
492
|
},
|
|
417
493
|
};
|
|
418
494
|
}
|