@runuai/host 0.9.152 → 0.9.154
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/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/db/schema.ts +10 -0
- package/lib/agent.ts +4 -0
- package/lib/browser-testing.ts +8 -3
- package/lib/cloud-credential-leases.ts +24 -4
- package/lib/cloud-credential-runtime.ts +322 -17
- package/lib/credential-cleanup-inventory.ts +2 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +140 -32
- package/lib/credential-consumers/mcp.ts +6 -1
- package/lib/mcp-gateway.ts +576 -122
- package/lib/orchestrator.ts +91 -9
- package/lib/runtime-state.ts +88 -0
- package/lib/shared-files-s3.ts +743 -3
- package/lib/sidecar-resume.ts +9 -0
- package/lib/task-credential-cleanup.ts +36 -5
- package/lib/task-environment/machine-task-up.ts +35 -19
- package/lib/task-files-machine-retirement.ts +88 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-person-credentials.ts +76 -0
- package/package.json +1 -1
- package/src/cloud-credentials.ts +10 -0
- package/src/index.ts +40 -7
- package/src/main.ts +49 -2
package/lib/shared-files-s3.ts
CHANGED
|
@@ -13,10 +13,20 @@
|
|
|
13
13
|
* caveat of hostless-implementation.md decision 4; EFS is the swap if they
|
|
14
14
|
* bite, and this module is the only place that would change.
|
|
15
15
|
*/
|
|
16
|
+
import type { TaskLeaseFilesAccess } from "./cloud-credential-leases";
|
|
16
17
|
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
17
18
|
import { LEASE_AWS_CONFIG_FILE, currentLease } from "./lease";
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
18
20
|
|
|
19
21
|
export const MOUNT_S3_BIN = "/usr/bin/mount-s3";
|
|
22
|
+
/** Files v1 mounts only below this root-owned machine path. The task sees it
|
|
23
|
+
* through an atomic `/workspace/files` symlink. Root mount/umount operations
|
|
24
|
+
* therefore never resolve a component the task can replace. */
|
|
25
|
+
export const TASK_FILES_V1_MOUNT_ROOT = "/mnt/uai-files";
|
|
26
|
+
/** Boot-local, root-only latch armed before the first v1 Mountpoint process
|
|
27
|
+
* can start. Once armed, only retiring/rebooting the one-task machine can
|
|
28
|
+
* prove that no private namespace or detached descriptor retains Files. */
|
|
29
|
+
export const TASK_FILES_V1_MOUNT_ATTEMPTED = "/run/uai/files-mount-attempted";
|
|
20
30
|
/** The task user the mounts are owned by. Its ids are looked up on the
|
|
21
31
|
* machine, never assumed: the Debian cloud image the payload AMI is built
|
|
22
32
|
* from ships an `admin` user at uid 1000, so `node` lands on 1001 there
|
|
@@ -32,10 +42,29 @@ export interface TaskUserIds {
|
|
|
32
42
|
gid: number;
|
|
33
43
|
}
|
|
34
44
|
|
|
45
|
+
export interface SharedFilesV1Options {
|
|
46
|
+
/**
|
|
47
|
+
* The exact run's last-moment admission fence. It rides on every call into
|
|
48
|
+
* the task environment, including the marker write, so a withdrawn run
|
|
49
|
+
* cannot start its next mount step after an awaited provider lookup.
|
|
50
|
+
*/
|
|
51
|
+
admit?: () => void;
|
|
52
|
+
/** Stage the mounts behind the root-only fixed directory without exposing
|
|
53
|
+
* them in the workspace. The Files consumer publishes synchronously from
|
|
54
|
+
* its exact gate-open callback. */
|
|
55
|
+
publish?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ResolveTaskUserIdsOptions extends SharedFilesV1Options {
|
|
59
|
+
/** A v1 admission refusal is an error, never a reason to use fallback ids. */
|
|
60
|
+
failOnExecError?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
/** `id -u`/`id -g` of the task user on this machine, through the root exec. */
|
|
36
64
|
export async function resolveTaskUserIds(
|
|
37
65
|
handle: Pick<TaskEnvironmentHandle, "exec">,
|
|
38
66
|
user: string = SHARED_FILES_USER,
|
|
67
|
+
options: ResolveTaskUserIdsOptions = {},
|
|
39
68
|
): Promise<TaskUserIds> {
|
|
40
69
|
const fallback = { uid: SHARED_FILES_FALLBACK_UID, gid: SHARED_FILES_FALLBACK_GID };
|
|
41
70
|
try {
|
|
@@ -45,16 +74,28 @@ export async function resolveTaskUserIds(
|
|
|
45
74
|
user: "root",
|
|
46
75
|
timeoutMs: 10_000,
|
|
47
76
|
maxOutputBytes: 1024,
|
|
77
|
+
...(options.admit ? { admit: options.admit } : {}),
|
|
48
78
|
});
|
|
49
|
-
if (result.exitCode !== 0)
|
|
79
|
+
if (result.exitCode !== 0) {
|
|
80
|
+
if (options.failOnExecError) {
|
|
81
|
+
throw new Error(`could not resolve the task user's ids (exit ${result.exitCode})`);
|
|
82
|
+
}
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
50
85
|
const [uid, gid] = Buffer.from(result.stdout)
|
|
51
86
|
.toString("utf8")
|
|
52
87
|
.trim()
|
|
53
88
|
.split(/\s+/)
|
|
54
89
|
.map((part) => Number.parseInt(part, 10));
|
|
55
|
-
if (!Number.isInteger(uid) || !Number.isInteger(gid) || uid! < 0 || gid! < 0)
|
|
90
|
+
if (!Number.isInteger(uid) || !Number.isInteger(gid) || uid! < 0 || gid! < 0) {
|
|
91
|
+
if (options.failOnExecError) {
|
|
92
|
+
throw new Error("could not resolve the task user's ids");
|
|
93
|
+
}
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
56
96
|
return { uid: uid!, gid: gid! };
|
|
57
|
-
} catch {
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (options.failOnExecError) throw error;
|
|
58
99
|
return fallback;
|
|
59
100
|
}
|
|
60
101
|
}
|
|
@@ -217,6 +258,200 @@ async function mountSharedFilesFromLeaseNow(
|
|
|
217
258
|
return warnings.length > 0 ? warnings.join("\n") : null;
|
|
218
259
|
}
|
|
219
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Mount shared files from the exact task/run's v1 Files session.
|
|
263
|
+
*
|
|
264
|
+
* Unlike the transitional lease path above, this function never consults
|
|
265
|
+
* `currentLease`, never turns a requested mount failure into a warning, and
|
|
266
|
+
* does not publish the workspace marker until every requested prefix is up.
|
|
267
|
+
* The Files consumer owns writing the session's credential_process files;
|
|
268
|
+
* mount-s3 learns only their fixed config path here.
|
|
269
|
+
*/
|
|
270
|
+
export async function mountSharedFilesFromAccess(
|
|
271
|
+
handle: Pick<TaskEnvironmentHandle<unknown>, "exec">,
|
|
272
|
+
workspacePath: string,
|
|
273
|
+
mode: SharedFilesMode,
|
|
274
|
+
access: TaskLeaseFilesAccess,
|
|
275
|
+
options: SharedFilesV1Options = {},
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
if (workspacePath !== "/workspace") {
|
|
278
|
+
throw new Error("shared files: task-machine workspace is not canonical");
|
|
279
|
+
}
|
|
280
|
+
return serializedByWorkspace(TASK_FILES_V1_MOUNT_ROOT, () =>
|
|
281
|
+
mountSharedFilesFromAccessNow(handle, workspacePath, mode, access, options),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function mountSharedFilesFromAccessNow(
|
|
286
|
+
handle: Pick<TaskEnvironmentHandle<unknown>, "exec">,
|
|
287
|
+
workspacePath: string,
|
|
288
|
+
mode: SharedFilesMode,
|
|
289
|
+
access: TaskLeaseFilesAccess,
|
|
290
|
+
options: SharedFilesV1Options,
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
if (mode === "off") return;
|
|
293
|
+
const owner = await resolveTaskUserIds(handle, SHARED_FILES_USER, {
|
|
294
|
+
...options,
|
|
295
|
+
failOnExecError: true,
|
|
296
|
+
});
|
|
297
|
+
// The legacy planner takes a workspace and appends `files/{org,me}`.
|
|
298
|
+
// Files v1's argument is already the fixed mount root, so rewrite only the
|
|
299
|
+
// resulting directory: `/workspace/files` exposes these exact children.
|
|
300
|
+
const plans = sharedFilesMountPlans(TASK_FILES_V1_MOUNT_ROOT, mode, access).map(
|
|
301
|
+
(plan) => ({
|
|
302
|
+
...plan,
|
|
303
|
+
directory: `${TASK_FILES_V1_MOUNT_ROOT}/${plan.directory.endsWith("/org") ? "org" : "me"}`,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
const scopes = plans.map((plan) => plan.directory.slice(`${TASK_FILES_V1_MOUNT_ROOT}/`.length));
|
|
307
|
+
const prepared = await handle.exec({
|
|
308
|
+
projectEnvProfile: "maintenance",
|
|
309
|
+
argv: [
|
|
310
|
+
"/usr/bin/python3",
|
|
311
|
+
"-I",
|
|
312
|
+
"-c",
|
|
313
|
+
TASK_FILES_V1_PREPARE_HELPER,
|
|
314
|
+
TASK_FILES_V1_MOUNT_ROOT,
|
|
315
|
+
scopes.join(","),
|
|
316
|
+
],
|
|
317
|
+
user: "root",
|
|
318
|
+
env: {},
|
|
319
|
+
timeoutMs: 10_000,
|
|
320
|
+
maxOutputBytes: 4 * 1024,
|
|
321
|
+
...(options.admit ? { admit: options.admit } : {}),
|
|
322
|
+
});
|
|
323
|
+
if (prepared.exitCode !== 0) {
|
|
324
|
+
throw new Error(`shared files: could not prepare the fixed mount root (exit ${prepared.exitCode})`);
|
|
325
|
+
}
|
|
326
|
+
for (const plan of plans) {
|
|
327
|
+
const result = await handle.exec({
|
|
328
|
+
projectEnvProfile: "maintenance",
|
|
329
|
+
argv: [
|
|
330
|
+
"/usr/bin/env",
|
|
331
|
+
"-i",
|
|
332
|
+
"PATH=/usr/bin:/bin",
|
|
333
|
+
`AWS_CONFIG_FILE=${LEASE_AWS_CONFIG_FILE}`,
|
|
334
|
+
"AWS_EC2_METADATA_DISABLED=true",
|
|
335
|
+
"/bin/sh",
|
|
336
|
+
"-c",
|
|
337
|
+
'if /usr/bin/mountpoint -q "$1" 2>/dev/null; then exit 40; fi; shift; exec "$@"',
|
|
338
|
+
"mount-fixed",
|
|
339
|
+
plan.directory,
|
|
340
|
+
...mountS3Argv(plan, owner),
|
|
341
|
+
],
|
|
342
|
+
user: "root",
|
|
343
|
+
env: {},
|
|
344
|
+
timeoutMs: 60_000,
|
|
345
|
+
maxOutputBytes: 64 * 1024,
|
|
346
|
+
...(options.admit ? { admit: options.admit } : {}),
|
|
347
|
+
});
|
|
348
|
+
if (result.exitCode !== 0) {
|
|
349
|
+
const detail = Buffer.from(result.stderr).toString("utf8").trim();
|
|
350
|
+
throw new Error(
|
|
351
|
+
`shared files: could not mount ${plan.directory}: ${detail || `exit ${result.exitCode}`}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (options.publish === false) return;
|
|
357
|
+
const linked = await handle.exec({
|
|
358
|
+
projectEnvProfile: "maintenance",
|
|
359
|
+
argv: [
|
|
360
|
+
"/usr/bin/python3",
|
|
361
|
+
"-I",
|
|
362
|
+
"-c",
|
|
363
|
+
TASK_FILES_V1_LINK_HELPER,
|
|
364
|
+
workspacePath,
|
|
365
|
+
TASK_FILES_V1_MOUNT_ROOT,
|
|
366
|
+
mode,
|
|
367
|
+
scopes.join(","),
|
|
368
|
+
],
|
|
369
|
+
user: "root",
|
|
370
|
+
env: {},
|
|
371
|
+
timeoutMs: 10_000,
|
|
372
|
+
maxOutputBytes: 4 * 1024,
|
|
373
|
+
...(options.admit ? { admit: options.admit } : {}),
|
|
374
|
+
});
|
|
375
|
+
if (linked.exitCode !== 0) {
|
|
376
|
+
throw new Error(`shared files: could not publish the fixed mount root (exit ${linked.exitCode})`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Publish already-staged v1 mounts from the synchronous registry gate-open
|
|
382
|
+
* callback. Files v1 exists only on a root sidecar/self runtime, so this
|
|
383
|
+
* local fixed-interpreter operation is the final boundary before any other
|
|
384
|
+
* consumer gate opens. */
|
|
385
|
+
export function publishSharedFilesV1Sync(
|
|
386
|
+
workspacePath: string,
|
|
387
|
+
mode: SharedFilesMode,
|
|
388
|
+
scopes: readonly ("org" | "me")[],
|
|
389
|
+
): void {
|
|
390
|
+
if (mode === "off") return;
|
|
391
|
+
if (workspacePath !== "/workspace") {
|
|
392
|
+
throw new Error("shared files: task-machine workspace is not canonical");
|
|
393
|
+
}
|
|
394
|
+
const result = spawnSync(
|
|
395
|
+
"/usr/bin/python3",
|
|
396
|
+
[
|
|
397
|
+
"-I",
|
|
398
|
+
"-c",
|
|
399
|
+
TASK_FILES_V1_LINK_HELPER,
|
|
400
|
+
workspacePath,
|
|
401
|
+
TASK_FILES_V1_MOUNT_ROOT,
|
|
402
|
+
mode,
|
|
403
|
+
scopes.join(","),
|
|
404
|
+
],
|
|
405
|
+
{
|
|
406
|
+
cwd: "/",
|
|
407
|
+
env: {},
|
|
408
|
+
encoding: "utf8",
|
|
409
|
+
timeout: 10_000,
|
|
410
|
+
maxBuffer: 4 * 1024,
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
if (result.error || result.status !== 0 || result.signal !== null) {
|
|
414
|
+
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
415
|
+
throw new Error(
|
|
416
|
+
`shared files: could not publish the fixed mount root${
|
|
417
|
+
stderr ? `: ${stderr}` : ""
|
|
418
|
+
}`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Synchronous half of gate close: make the fixed root non-traversable and
|
|
424
|
+
* remove the task-visible link/marker before any asynchronous cleanup. Open
|
|
425
|
+
* file descriptors still require process quiescence + ordinary unmount. */
|
|
426
|
+
export function hideSharedFilesV1Sync(workspacePath: string): void {
|
|
427
|
+
if (workspacePath !== "/workspace") {
|
|
428
|
+
throw new Error("shared files: task-machine workspace is not canonical");
|
|
429
|
+
}
|
|
430
|
+
const result = spawnSync(
|
|
431
|
+
"/usr/bin/python3",
|
|
432
|
+
[
|
|
433
|
+
"-I",
|
|
434
|
+
"-c",
|
|
435
|
+
TASK_FILES_V1_HIDE_HELPER,
|
|
436
|
+
workspacePath,
|
|
437
|
+
TASK_FILES_V1_MOUNT_ROOT,
|
|
438
|
+
],
|
|
439
|
+
{
|
|
440
|
+
cwd: "/",
|
|
441
|
+
env: {},
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
timeout: 10_000,
|
|
444
|
+
maxBuffer: 4 * 1024,
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
if (result.error || result.status !== 0 || result.signal !== null) {
|
|
448
|
+
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
449
|
+
throw new Error(
|
|
450
|
+
`shared files: could not hide the fixed mount root${stderr ? `: ${stderr}` : ""}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
220
455
|
/** Unmount both directories; tolerant of never-mounted paths. */
|
|
221
456
|
export async function unmountSharedFiles(
|
|
222
457
|
handle: TaskEnvironmentHandle<unknown>,
|
|
@@ -234,3 +469,508 @@ export async function unmountSharedFiles(
|
|
|
234
469
|
.catch(() => {});
|
|
235
470
|
}
|
|
236
471
|
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Strict v1 cleanup. Both mountpoints are attempted even when one fails, and
|
|
475
|
+
* success proves that neither remains mounted. It shares the mount queue, so
|
|
476
|
+
* cleanup cannot overtake a mount/marker pass for the same workspace.
|
|
477
|
+
*/
|
|
478
|
+
export async function unmountSharedFilesStrict(
|
|
479
|
+
handle: Pick<TaskEnvironmentHandle<unknown>, "exec">,
|
|
480
|
+
workspacePath: string,
|
|
481
|
+
options: SharedFilesV1Options = {},
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
if (workspacePath !== "/workspace") {
|
|
484
|
+
throw new Error("shared files: task-machine workspace is not canonical");
|
|
485
|
+
}
|
|
486
|
+
return serializedByWorkspace(TASK_FILES_V1_MOUNT_ROOT, async () => {
|
|
487
|
+
const result = await handle.exec({
|
|
488
|
+
argv: [
|
|
489
|
+
"/usr/bin/python3",
|
|
490
|
+
"-I",
|
|
491
|
+
"-c",
|
|
492
|
+
TASK_FILES_V1_UNMOUNT_HELPER,
|
|
493
|
+
workspacePath,
|
|
494
|
+
TASK_FILES_V1_MOUNT_ROOT,
|
|
495
|
+
],
|
|
496
|
+
projectEnvProfile: "maintenance",
|
|
497
|
+
user: "root",
|
|
498
|
+
env: {},
|
|
499
|
+
timeoutMs: 30_000,
|
|
500
|
+
maxOutputBytes: 4 * 1024,
|
|
501
|
+
...(options.admit ? { admit: options.admit } : {}),
|
|
502
|
+
});
|
|
503
|
+
if (result.exitCode !== 0) {
|
|
504
|
+
const detail = Buffer.from(result.stderr).toString("utf8").trim();
|
|
505
|
+
throw new Error(
|
|
506
|
+
`shared files: could not prove the fixed mounts absent: ${detail || `exit ${result.exitCode}`}`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Create only the fixed, root-controlled mount directories. Neither the
|
|
513
|
+
* workspace nor any task-owned path is resolved here. */
|
|
514
|
+
export const TASK_FILES_V1_PREPARE_HELPER = String.raw`
|
|
515
|
+
import os
|
|
516
|
+
import stat
|
|
517
|
+
import sys
|
|
518
|
+
|
|
519
|
+
root, encoded = sys.argv[1:3]
|
|
520
|
+
if root != "/mnt/uai-files":
|
|
521
|
+
raise RuntimeError("unexpected Files mount root")
|
|
522
|
+
scopes = [part for part in encoded.split(",") if part]
|
|
523
|
+
if any(part not in ("org", "me") for part in scopes) or "me" not in scopes:
|
|
524
|
+
raise RuntimeError("invalid Files mount scope")
|
|
525
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
526
|
+
|
|
527
|
+
def arm_mount_attempt():
|
|
528
|
+
runtime = os.open("/run/uai", flags)
|
|
529
|
+
try:
|
|
530
|
+
runtime_info = os.fstat(runtime)
|
|
531
|
+
if runtime_info.st_uid != 0:
|
|
532
|
+
raise RuntimeError("the Files runtime directory is not root-owned")
|
|
533
|
+
try:
|
|
534
|
+
attempted = os.open(
|
|
535
|
+
"files-mount-attempted",
|
|
536
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC,
|
|
537
|
+
0o600,
|
|
538
|
+
dir_fd=runtime,
|
|
539
|
+
)
|
|
540
|
+
except FileExistsError:
|
|
541
|
+
raise RuntimeError("a Files mount was already attempted in this boot; retire this task machine")
|
|
542
|
+
try:
|
|
543
|
+
marker_info = os.fstat(attempted)
|
|
544
|
+
if marker_info.st_uid != 0 or not stat.S_ISREG(marker_info.st_mode) or stat.S_IMODE(marker_info.st_mode) != 0o600:
|
|
545
|
+
raise RuntimeError("the Files mount-attempt marker is unsafe")
|
|
546
|
+
os.write(attempted, b"1\n")
|
|
547
|
+
os.fsync(attempted)
|
|
548
|
+
finally:
|
|
549
|
+
os.close(attempted)
|
|
550
|
+
finally:
|
|
551
|
+
os.close(runtime)
|
|
552
|
+
|
|
553
|
+
def decode_mount_path(value):
|
|
554
|
+
result = bytearray()
|
|
555
|
+
index = 0
|
|
556
|
+
while index < len(value):
|
|
557
|
+
if value[index:index + 1] == b"\\" and index + 3 < len(value) and all(48 <= part <= 55 for part in value[index + 1:index + 4]):
|
|
558
|
+
result.append(int(value[index + 1:index + 4], 8))
|
|
559
|
+
index += 4
|
|
560
|
+
else:
|
|
561
|
+
result.append(value[index])
|
|
562
|
+
index += 1
|
|
563
|
+
return os.fsdecode(bytes(result))
|
|
564
|
+
|
|
565
|
+
with open("/proc/self/mountinfo", "rb") as stream:
|
|
566
|
+
for line in stream:
|
|
567
|
+
before = line.rstrip(b"\n").split(b" - ", 1)[0].split(b" ")
|
|
568
|
+
path = decode_mount_path(before[4])
|
|
569
|
+
if path == root or path.startswith(root + "/"):
|
|
570
|
+
arm_mount_attempt()
|
|
571
|
+
raise RuntimeError("the Files mount root is not empty")
|
|
572
|
+
|
|
573
|
+
parent_path, root_name = os.path.split(root)
|
|
574
|
+
if not parent_path or not root_name:
|
|
575
|
+
raise RuntimeError("invalid Files mount root")
|
|
576
|
+
parent = os.open(parent_path, flags)
|
|
577
|
+
try:
|
|
578
|
+
try:
|
|
579
|
+
os.mkdir(root_name, 0o755, dir_fd=parent)
|
|
580
|
+
except FileExistsError:
|
|
581
|
+
pass
|
|
582
|
+
directory = os.open(root_name, flags, dir_fd=parent)
|
|
583
|
+
finally:
|
|
584
|
+
os.close(parent)
|
|
585
|
+
try:
|
|
586
|
+
info = os.fstat(directory)
|
|
587
|
+
if info.st_uid != 0:
|
|
588
|
+
raise RuntimeError("Files mount root is not root-owned")
|
|
589
|
+
# Staged mounts are deliberately unreachable to the task until the exact
|
|
590
|
+
# registry open publishes them.
|
|
591
|
+
os.fchmod(directory, 0o700)
|
|
592
|
+
for scope in scopes:
|
|
593
|
+
try:
|
|
594
|
+
os.mkdir(scope, 0o755, dir_fd=directory)
|
|
595
|
+
except FileExistsError:
|
|
596
|
+
pass
|
|
597
|
+
child = os.open(scope, flags, dir_fd=directory)
|
|
598
|
+
try:
|
|
599
|
+
if os.fstat(child).st_uid != 0:
|
|
600
|
+
raise RuntimeError("Files mountpoint is not root-owned")
|
|
601
|
+
os.fchmod(child, 0o755)
|
|
602
|
+
finally:
|
|
603
|
+
os.close(child)
|
|
604
|
+
for unused in set(("org", "me")) - set(scopes):
|
|
605
|
+
try:
|
|
606
|
+
os.rmdir(unused, dir_fd=directory)
|
|
607
|
+
except FileNotFoundError:
|
|
608
|
+
pass
|
|
609
|
+
finally:
|
|
610
|
+
os.close(directory)
|
|
611
|
+
|
|
612
|
+
# Arm the one-way boot latch only after the fixed root is ready, but before
|
|
613
|
+
# the caller can spawn mount-s3. A lost helper response is deliberately
|
|
614
|
+
# terminal: the caller cannot distinguish it from a mount launch that raced
|
|
615
|
+
# ahead in another attempt.
|
|
616
|
+
arm_mount_attempt()
|
|
617
|
+
`;
|
|
618
|
+
|
|
619
|
+
/** Publish the fixed root without ever asking root to follow the task's
|
|
620
|
+
* `files` entry. A stray task entry is atomically moved aside, never read or
|
|
621
|
+
* recursively deleted. */
|
|
622
|
+
export const TASK_FILES_V1_LINK_HELPER = String.raw`
|
|
623
|
+
import os
|
|
624
|
+
import stat
|
|
625
|
+
import sys
|
|
626
|
+
|
|
627
|
+
workspace, target, mode, encoded = sys.argv[1:5]
|
|
628
|
+
if workspace != "/workspace" or target != "/mnt/uai-files" or mode not in ("ro", "rw"):
|
|
629
|
+
raise RuntimeError("invalid Files link")
|
|
630
|
+
scopes = [part for part in encoded.split(",") if part]
|
|
631
|
+
if any(part not in ("org", "me") for part in scopes) or "me" not in scopes or len(set(scopes)) != len(scopes):
|
|
632
|
+
raise RuntimeError("invalid Files scopes")
|
|
633
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
634
|
+
|
|
635
|
+
def decode_mount_path(value):
|
|
636
|
+
result = bytearray()
|
|
637
|
+
index = 0
|
|
638
|
+
while index < len(value):
|
|
639
|
+
if value[index:index + 1] == b"\\" and index + 3 < len(value) and all(48 <= part <= 55 for part in value[index + 1:index + 4]):
|
|
640
|
+
result.append(int(value[index + 1:index + 4], 8))
|
|
641
|
+
index += 4
|
|
642
|
+
else:
|
|
643
|
+
result.append(value[index])
|
|
644
|
+
index += 1
|
|
645
|
+
return os.fsdecode(bytes(result))
|
|
646
|
+
|
|
647
|
+
expected = {(target + "/" + scope, "fuse", "mountpoint-s3") for scope in scopes}
|
|
648
|
+
|
|
649
|
+
def exact_topology():
|
|
650
|
+
observed = []
|
|
651
|
+
with open("/proc/self/mountinfo", "rb") as stream:
|
|
652
|
+
for line in stream:
|
|
653
|
+
before, after = line.rstrip(b"\n").split(b" - ", 1)
|
|
654
|
+
fields = before.split(b" ")
|
|
655
|
+
tail = after.split(b" ")
|
|
656
|
+
path = decode_mount_path(fields[4])
|
|
657
|
+
if path == target or path.startswith(target + "/"):
|
|
658
|
+
observed.append((path, tail[0].decode("ascii"), tail[1].decode("ascii")))
|
|
659
|
+
return len(observed) == len(expected) and set(observed) == expected
|
|
660
|
+
|
|
661
|
+
if not exact_topology():
|
|
662
|
+
raise RuntimeError("the staged Files mount topology is not exact")
|
|
663
|
+
|
|
664
|
+
mount_root = os.open(target, flags)
|
|
665
|
+
directory = os.open(workspace, flags)
|
|
666
|
+
try:
|
|
667
|
+
workspace_owner = os.fstat(directory)
|
|
668
|
+
# The files entry belongs to the task until this exact publication. Preserve any
|
|
669
|
+
# stray entry by moving it aside in the same directory; never traverse or
|
|
670
|
+
# recursively remove it. The bounded loop also prevents a task racing the
|
|
671
|
+
# name from holding the root helper forever.
|
|
672
|
+
for attempt in range(64):
|
|
673
|
+
try:
|
|
674
|
+
info = os.stat("files", dir_fd=directory, follow_symlinks=False)
|
|
675
|
+
except FileNotFoundError:
|
|
676
|
+
info = None
|
|
677
|
+
if info is not None and stat.S_ISLNK(info.st_mode):
|
|
678
|
+
try:
|
|
679
|
+
if os.readlink("files", dir_fd=directory) == target:
|
|
680
|
+
break
|
|
681
|
+
except OSError:
|
|
682
|
+
pass
|
|
683
|
+
if info is not None:
|
|
684
|
+
quarantine = ".uai-files-stray-" + os.urandom(16).hex()
|
|
685
|
+
try:
|
|
686
|
+
os.mkdir(quarantine, 0o700, dir_fd=directory)
|
|
687
|
+
except FileExistsError:
|
|
688
|
+
continue
|
|
689
|
+
quarantine_fd = os.open(quarantine, flags, dir_fd=directory)
|
|
690
|
+
try:
|
|
691
|
+
try:
|
|
692
|
+
os.rename("files", "entry", src_dir_fd=directory, dst_dir_fd=quarantine_fd)
|
|
693
|
+
except FileNotFoundError:
|
|
694
|
+
os.close(quarantine_fd)
|
|
695
|
+
quarantine_fd = None
|
|
696
|
+
os.rmdir(quarantine, dir_fd=directory)
|
|
697
|
+
continue
|
|
698
|
+
os.fchown(quarantine_fd, workspace_owner.st_uid, workspace_owner.st_gid)
|
|
699
|
+
os.fchmod(quarantine_fd, 0o700)
|
|
700
|
+
finally:
|
|
701
|
+
if quarantine_fd is not None:
|
|
702
|
+
os.close(quarantine_fd)
|
|
703
|
+
try:
|
|
704
|
+
os.symlink(target, "files", dir_fd=directory)
|
|
705
|
+
break
|
|
706
|
+
except FileExistsError:
|
|
707
|
+
continue
|
|
708
|
+
else:
|
|
709
|
+
raise RuntimeError("workspace Files link could not be published")
|
|
710
|
+
if os.readlink("files", dir_fd=directory) != target:
|
|
711
|
+
raise RuntimeError("workspace Files link was replaced")
|
|
712
|
+
try:
|
|
713
|
+
os.mkdir(".uai", 0o700, dir_fd=directory)
|
|
714
|
+
os.chown(".uai", workspace_owner.st_uid, workspace_owner.st_gid, dir_fd=directory, follow_symlinks=False)
|
|
715
|
+
except FileExistsError:
|
|
716
|
+
pass
|
|
717
|
+
# The marker is advisory task metadata, not part of the visibility proof.
|
|
718
|
+
# A task-controlled non-directory .uai, a wrong owner, or a non-regular
|
|
719
|
+
# marker must not turn a successfully hidden root into machine retirement.
|
|
720
|
+
try:
|
|
721
|
+
uai = os.open(".uai", flags, dir_fd=directory)
|
|
722
|
+
except OSError:
|
|
723
|
+
uai = None
|
|
724
|
+
if uai is not None:
|
|
725
|
+
try:
|
|
726
|
+
uai_owner = os.fstat(uai)
|
|
727
|
+
if (uai_owner.st_uid, uai_owner.st_gid) == (workspace_owner.st_uid, workspace_owner.st_gid):
|
|
728
|
+
try:
|
|
729
|
+
descriptor = os.open(
|
|
730
|
+
"files-mounted",
|
|
731
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC,
|
|
732
|
+
0o600,
|
|
733
|
+
dir_fd=uai,
|
|
734
|
+
)
|
|
735
|
+
except OSError:
|
|
736
|
+
descriptor = None
|
|
737
|
+
if descriptor is not None:
|
|
738
|
+
try:
|
|
739
|
+
marker_info = os.fstat(descriptor)
|
|
740
|
+
if stat.S_ISREG(marker_info.st_mode):
|
|
741
|
+
os.ftruncate(descriptor, 0)
|
|
742
|
+
os.write(descriptor, mode.encode("ascii"))
|
|
743
|
+
os.fchmod(descriptor, 0o600)
|
|
744
|
+
os.fchown(descriptor, uai_owner.st_uid, uai_owner.st_gid)
|
|
745
|
+
finally:
|
|
746
|
+
os.close(descriptor)
|
|
747
|
+
finally:
|
|
748
|
+
os.close(uai)
|
|
749
|
+
# The workspace mutations above are task-controlled and may take time.
|
|
750
|
+
# Prove every expected mount again at the final visibility boundary.
|
|
751
|
+
if not exact_topology():
|
|
752
|
+
raise RuntimeError("the staged Files mount topology changed before publish")
|
|
753
|
+
# Last: only now can the task traverse the fixed root, after the link and
|
|
754
|
+
# any safe advisory marker work are complete.
|
|
755
|
+
os.fchmod(mount_root, 0o755)
|
|
756
|
+
finally:
|
|
757
|
+
os.close(directory)
|
|
758
|
+
os.close(mount_root)
|
|
759
|
+
`;
|
|
760
|
+
|
|
761
|
+
/** Path visibility only. Physical mount retirement deliberately remains in
|
|
762
|
+
* the asynchronous strict helper, which quiesces exact task-user holders. */
|
|
763
|
+
export const TASK_FILES_V1_HIDE_HELPER = String.raw`
|
|
764
|
+
import os
|
|
765
|
+
import stat
|
|
766
|
+
import sys
|
|
767
|
+
|
|
768
|
+
workspace, root = sys.argv[1:3]
|
|
769
|
+
if workspace != "/workspace" or root != "/mnt/uai-files":
|
|
770
|
+
raise RuntimeError("invalid Files hide root")
|
|
771
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
772
|
+
try:
|
|
773
|
+
mount_root = os.open(root, flags)
|
|
774
|
+
except FileNotFoundError:
|
|
775
|
+
mount_root = None
|
|
776
|
+
if mount_root is not None:
|
|
777
|
+
try:
|
|
778
|
+
os.fchmod(mount_root, 0o700)
|
|
779
|
+
finally:
|
|
780
|
+
os.close(mount_root)
|
|
781
|
+
try:
|
|
782
|
+
directory = os.open(workspace, flags)
|
|
783
|
+
except FileNotFoundError:
|
|
784
|
+
directory = None
|
|
785
|
+
if directory is not None:
|
|
786
|
+
try:
|
|
787
|
+
try:
|
|
788
|
+
info = os.stat("files", dir_fd=directory, follow_symlinks=False)
|
|
789
|
+
except OSError:
|
|
790
|
+
info = None
|
|
791
|
+
if info is not None and stat.S_ISLNK(info.st_mode):
|
|
792
|
+
try:
|
|
793
|
+
if os.readlink("files", dir_fd=directory) == root:
|
|
794
|
+
os.unlink("files", dir_fd=directory)
|
|
795
|
+
except OSError:
|
|
796
|
+
pass
|
|
797
|
+
try:
|
|
798
|
+
uai = os.open(".uai", flags, dir_fd=directory)
|
|
799
|
+
except OSError:
|
|
800
|
+
uai = None
|
|
801
|
+
if uai is not None:
|
|
802
|
+
try:
|
|
803
|
+
try:
|
|
804
|
+
os.unlink("files-mounted", dir_fd=uai)
|
|
805
|
+
except OSError:
|
|
806
|
+
pass
|
|
807
|
+
finally:
|
|
808
|
+
os.close(uai)
|
|
809
|
+
finally:
|
|
810
|
+
os.close(directory)
|
|
811
|
+
`;
|
|
812
|
+
|
|
813
|
+
/** Detach only the fixed root-owned mounts. The workspace operation is an
|
|
814
|
+
* exact symlink unlink through its opened parent; no root unmount ever sees
|
|
815
|
+
* task-controlled path components. */
|
|
816
|
+
export const TASK_FILES_V1_UNMOUNT_HELPER = String.raw`
|
|
817
|
+
import os
|
|
818
|
+
import stat
|
|
819
|
+
import subprocess
|
|
820
|
+
import sys
|
|
821
|
+
|
|
822
|
+
workspace, root = sys.argv[1:3]
|
|
823
|
+
if workspace != "/workspace" or root != "/mnt/uai-files":
|
|
824
|
+
raise RuntimeError("invalid Files cleanup root")
|
|
825
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
826
|
+
# Hide the staged root before any awaited unmount. Open handles are dealt with
|
|
827
|
+
# by the ordinary unmount proof below; no lazy detach is accepted as cleanup.
|
|
828
|
+
try:
|
|
829
|
+
mount_root = os.open(root, flags)
|
|
830
|
+
except FileNotFoundError:
|
|
831
|
+
mount_root = None
|
|
832
|
+
if mount_root is not None:
|
|
833
|
+
try:
|
|
834
|
+
os.fchmod(mount_root, 0o700)
|
|
835
|
+
finally:
|
|
836
|
+
os.close(mount_root)
|
|
837
|
+
try:
|
|
838
|
+
directory = os.open(workspace, flags)
|
|
839
|
+
except FileNotFoundError:
|
|
840
|
+
directory = None
|
|
841
|
+
if directory is not None:
|
|
842
|
+
try:
|
|
843
|
+
try:
|
|
844
|
+
info = os.stat("files", dir_fd=directory, follow_symlinks=False)
|
|
845
|
+
except OSError:
|
|
846
|
+
info = None
|
|
847
|
+
if info is not None and stat.S_ISLNK(info.st_mode):
|
|
848
|
+
try:
|
|
849
|
+
if os.readlink("files", dir_fd=directory) == root:
|
|
850
|
+
os.unlink("files", dir_fd=directory)
|
|
851
|
+
except OSError:
|
|
852
|
+
pass
|
|
853
|
+
try:
|
|
854
|
+
uai = os.open(".uai", flags, dir_fd=directory)
|
|
855
|
+
except OSError:
|
|
856
|
+
uai = None
|
|
857
|
+
if uai is not None:
|
|
858
|
+
try:
|
|
859
|
+
try:
|
|
860
|
+
os.unlink("files-mounted", dir_fd=uai)
|
|
861
|
+
except OSError:
|
|
862
|
+
pass
|
|
863
|
+
finally:
|
|
864
|
+
os.close(uai)
|
|
865
|
+
finally:
|
|
866
|
+
os.close(directory)
|
|
867
|
+
|
|
868
|
+
def decode_mount_path(value):
|
|
869
|
+
result = bytearray()
|
|
870
|
+
index = 0
|
|
871
|
+
while index < len(value):
|
|
872
|
+
if value[index:index + 1] == b"\\" and index + 3 < len(value) and all(48 <= part <= 55 for part in value[index + 1:index + 4]):
|
|
873
|
+
result.append(int(value[index + 1:index + 4], 8))
|
|
874
|
+
index += 4
|
|
875
|
+
else:
|
|
876
|
+
result.append(value[index])
|
|
877
|
+
index += 1
|
|
878
|
+
return os.fsdecode(bytes(result))
|
|
879
|
+
|
|
880
|
+
def mounts(path="/proc/self/mountinfo"):
|
|
881
|
+
found = []
|
|
882
|
+
with open(path, "rb") as stream:
|
|
883
|
+
for line in stream:
|
|
884
|
+
before, after = line.rstrip(b"\n").split(b" - ", 1)
|
|
885
|
+
fields = before.split(b" ")
|
|
886
|
+
tail = after.split(b" ")
|
|
887
|
+
found.append((
|
|
888
|
+
int(fields[0]),
|
|
889
|
+
decode_mount_path(fields[4]),
|
|
890
|
+
tail[0].decode("ascii"),
|
|
891
|
+
tail[1].decode("ascii"),
|
|
892
|
+
tuple(int(part) for part in fields[2].split(b":")),
|
|
893
|
+
))
|
|
894
|
+
return found
|
|
895
|
+
|
|
896
|
+
targets = {root + "/org", root + "/me"}
|
|
897
|
+
legacy = {workspace.rstrip("/") + "/files/org", workspace.rstrip("/") + "/files/me"}
|
|
898
|
+
|
|
899
|
+
def mount_attempted(create=False):
|
|
900
|
+
runtime = os.open("/run/uai", flags)
|
|
901
|
+
try:
|
|
902
|
+
runtime_info = os.fstat(runtime)
|
|
903
|
+
if runtime_info.st_uid != 0:
|
|
904
|
+
raise RuntimeError("the Files runtime directory is not root-owned")
|
|
905
|
+
try:
|
|
906
|
+
info = os.stat("files-mount-attempted", dir_fd=runtime, follow_symlinks=False)
|
|
907
|
+
except FileNotFoundError:
|
|
908
|
+
if not create:
|
|
909
|
+
return False
|
|
910
|
+
try:
|
|
911
|
+
descriptor = os.open(
|
|
912
|
+
"files-mount-attempted",
|
|
913
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC,
|
|
914
|
+
0o600,
|
|
915
|
+
dir_fd=runtime,
|
|
916
|
+
)
|
|
917
|
+
except FileExistsError:
|
|
918
|
+
# Another strict-cleanup attempt armed the same one-way latch.
|
|
919
|
+
# Revalidate its directory entry and continue best-effort
|
|
920
|
+
# unmounting rather than losing the retirement witness.
|
|
921
|
+
info = os.stat("files-mount-attempted", dir_fd=runtime, follow_symlinks=False)
|
|
922
|
+
else:
|
|
923
|
+
try:
|
|
924
|
+
info = os.fstat(descriptor)
|
|
925
|
+
os.write(descriptor, b"1\n")
|
|
926
|
+
os.fsync(descriptor)
|
|
927
|
+
finally:
|
|
928
|
+
os.close(descriptor)
|
|
929
|
+
if info.st_uid != 0 or not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o600:
|
|
930
|
+
raise RuntimeError("the Files mount-attempt marker is unsafe")
|
|
931
|
+
return True
|
|
932
|
+
finally:
|
|
933
|
+
os.close(runtime)
|
|
934
|
+
|
|
935
|
+
initial = mounts()
|
|
936
|
+
under_root = [(path, fs_type, source) for _mid, path, fs_type, source, _device in initial if path == root or path.startswith(root + "/")]
|
|
937
|
+
counts = {path: sum(1 for observed, _fs_type, _source in under_root if observed == path) for path in targets}
|
|
938
|
+
unexpected = [
|
|
939
|
+
path for path, fs_type, source in under_root
|
|
940
|
+
if path not in targets or fs_type != "fuse" or source != "mountpoint-s3"
|
|
941
|
+
]
|
|
942
|
+
unexpected += [path for path, count in counts.items() if count > 1]
|
|
943
|
+
unexpected += [
|
|
944
|
+
path for _mid, path, fs_type, source, _device in initial
|
|
945
|
+
if path in legacy or (fs_type == "fuse" and source == "mountpoint-s3" and path not in targets)
|
|
946
|
+
]
|
|
947
|
+
attempted = mount_attempted()
|
|
948
|
+
if unexpected or any(counts.values()):
|
|
949
|
+
# An older/lost attempt may have reached mount-s3 before its marker was
|
|
950
|
+
# observed. Latch retirement before changing the local mount topology.
|
|
951
|
+
attempted = mount_attempted(create=True)
|
|
952
|
+
if unexpected:
|
|
953
|
+
raise RuntimeError("legacy or unexpected Files mount remains; retire this task machine")
|
|
954
|
+
|
|
955
|
+
failures = []
|
|
956
|
+
for path in sorted(targets):
|
|
957
|
+
records = [record for record in mounts() if record[1] == path]
|
|
958
|
+
if not records:
|
|
959
|
+
continue
|
|
960
|
+
if len(records) != 1 or records[0][2:4] != ("fuse", "mountpoint-s3"):
|
|
961
|
+
failures.append(path + ":identity-changed")
|
|
962
|
+
continue
|
|
963
|
+
ended = subprocess.run(["/usr/bin/umount", path], env={}, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
964
|
+
if ended.returncode != 0:
|
|
965
|
+
failures.append(path + ":unmount")
|
|
966
|
+
if any(mount_path == path for _mid, mount_path, _fs_type, _source, _device in mounts()):
|
|
967
|
+
failures.append(path + ":still-mounted")
|
|
968
|
+
if attempted:
|
|
969
|
+
# A /proc scan cannot prove every private mount namespace or detached fd
|
|
970
|
+
# gone: a task can fork during the scan, retain a namespace fd, or become
|
|
971
|
+
# root through the task image's sudo. The boot-local latch makes runtime
|
|
972
|
+
# destruction the sole success proof once mount-s3 could have started.
|
|
973
|
+
failures.append("task-machine-retirement-required")
|
|
974
|
+
if failures:
|
|
975
|
+
raise RuntimeError(";".join(failures))
|
|
976
|
+
`;
|