@runuai/host 0.9.167 → 0.9.169
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/lib/agent-cli.ts +811 -21
- package/lib/agents/cursor.ts +16 -10
- package/lib/agents/durable-proc.ts +12 -5
- package/lib/agents/machine-durable.ts +12 -5
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/transport.ts +5 -0
- package/lib/assignment-evidence.ts +93 -0
- package/lib/cloud-credential-leases.ts +23 -6
- package/lib/cloud-credential-runtime.ts +10 -1
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/engine-accounts.ts +4 -6
- package/lib/host-env.ts +2 -27
- package/lib/orchestrator.ts +97 -16
- package/lib/project-env-loader.ts +5 -0
- package/lib/secrets-proxy/policy.ts +12 -3
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image.ts +31 -9
- package/lib/task-env-source.ts +2 -0
- package/lib/task-environment/types.ts +20 -15
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-token.ts +3 -0
- package/package.json +14 -2
- package/src/cloud-credentials.ts +41 -4
- package/src/engine-account-paths.ts +11 -0
- package/src/index.ts +66 -3
- package/src/main.ts +142 -0
- package/src/protocol.ts +204 -2
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/workspace-path-policy.ts +41 -0
|
@@ -393,6 +393,11 @@ export function assertWorkspaceScopedPath(
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/** A malformed process request, distinct from provider/runtime failures. */
|
|
397
|
+
export class TaskEnvironmentRequestValidationError extends Error {
|
|
398
|
+
override name = "TaskEnvironmentRequestValidationError";
|
|
399
|
+
}
|
|
400
|
+
|
|
396
401
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
397
402
|
|
|
398
403
|
/** Shared admission guard for provider implementations and fake providers. */
|
|
@@ -402,7 +407,7 @@ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
402
407
|
export function assertTaskEnvironmentExecRequest(request: TaskEnvironmentExecRequest): void {
|
|
403
408
|
const { privileged, ...unprivileged } = request;
|
|
404
409
|
if (privileged !== undefined && (privileged !== true || request.user !== "root")) {
|
|
405
|
-
throw new
|
|
410
|
+
throw new TaskEnvironmentRequestValidationError("a privileged task environment exec must run as root");
|
|
406
411
|
}
|
|
407
412
|
assertTaskEnvironmentProcessRequest(unprivileged);
|
|
408
413
|
}
|
|
@@ -412,7 +417,7 @@ export function assertTaskEnvironmentProcessRequest(
|
|
|
412
417
|
): void {
|
|
413
418
|
assertTaskEnvironmentSessionRequest(request);
|
|
414
419
|
if (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs <= 0) {
|
|
415
|
-
throw new
|
|
420
|
+
throw new TaskEnvironmentRequestValidationError("task environment timeout must be a positive integer");
|
|
416
421
|
}
|
|
417
422
|
}
|
|
418
423
|
|
|
@@ -420,7 +425,7 @@ export function assertTaskEnvironmentProcessRequest(
|
|
|
420
425
|
* step without the capability it asked for. */
|
|
421
426
|
export function refusePrivilegedExec(request: TaskEnvironmentExecRequest): void {
|
|
422
427
|
if (request.privileged === true) {
|
|
423
|
-
throw new
|
|
428
|
+
throw new TaskEnvironmentRequestValidationError("this task environment provider has no privileged exec");
|
|
424
429
|
}
|
|
425
430
|
}
|
|
426
431
|
|
|
@@ -434,13 +439,13 @@ export function assertTaskEnvironmentSessionRequest(
|
|
|
434
439
|
// Only a one-shot exec may be privileged (assertTaskEnvironmentExecRequest
|
|
435
440
|
// strips it after checking); anywhere else it is refused outright.
|
|
436
441
|
if ("privileged" in request) {
|
|
437
|
-
throw new
|
|
442
|
+
throw new TaskEnvironmentRequestValidationError("only a one-shot task environment exec may be privileged");
|
|
438
443
|
}
|
|
439
444
|
if (
|
|
440
445
|
request.projectEnvProfile !== "maintenance" &&
|
|
441
446
|
request.projectEnvProfile !== "task"
|
|
442
447
|
) {
|
|
443
|
-
throw new
|
|
448
|
+
throw new TaskEnvironmentRequestValidationError(
|
|
444
449
|
"task environment project env profile must be maintenance or task",
|
|
445
450
|
);
|
|
446
451
|
}
|
|
@@ -451,19 +456,19 @@ export function assertTaskEnvironmentSessionRequest(
|
|
|
451
456
|
(part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
|
|
452
457
|
)
|
|
453
458
|
) {
|
|
454
|
-
throw new
|
|
459
|
+
throw new TaskEnvironmentRequestValidationError("task environment argv must contain nonempty NUL-free strings");
|
|
455
460
|
}
|
|
456
461
|
if (
|
|
457
462
|
request.cwd !== undefined &&
|
|
458
|
-
(!request.cwd.startsWith("/") || request.cwd.includes("\0"))
|
|
463
|
+
(typeof request.cwd !== "string" || !request.cwd.startsWith("/") || request.cwd.includes("\0"))
|
|
459
464
|
) {
|
|
460
|
-
throw new
|
|
465
|
+
throw new TaskEnvironmentRequestValidationError("task environment cwd must be an absolute NUL-free path");
|
|
461
466
|
}
|
|
462
467
|
if (
|
|
463
468
|
request.user !== undefined &&
|
|
464
|
-
(request.user.length === 0 || request.user.includes("\0"))
|
|
469
|
+
(typeof request.user !== "string" || request.user.length === 0 || request.user.includes("\0"))
|
|
465
470
|
) {
|
|
466
|
-
throw new
|
|
471
|
+
throw new TaskEnvironmentRequestValidationError("task environment user must be a nonempty NUL-free value");
|
|
467
472
|
}
|
|
468
473
|
if (
|
|
469
474
|
request.anchor !== undefined &&
|
|
@@ -474,7 +479,7 @@ export function assertTaskEnvironmentSessionRequest(
|
|
|
474
479
|
(part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
|
|
475
480
|
))
|
|
476
481
|
) {
|
|
477
|
-
throw new
|
|
482
|
+
throw new TaskEnvironmentRequestValidationError("a task environment anchor must be a NUL-free argv for a task user's process");
|
|
478
483
|
}
|
|
479
484
|
if (
|
|
480
485
|
request.inheritEnv !== undefined &&
|
|
@@ -485,7 +490,7 @@ export function assertTaskEnvironmentSessionRequest(
|
|
|
485
490
|
(name) => typeof name !== "string" || !ENV_NAME.test(name),
|
|
486
491
|
))
|
|
487
492
|
) {
|
|
488
|
-
throw new
|
|
493
|
+
throw new TaskEnvironmentRequestValidationError("task environment inherited env names are invalid");
|
|
489
494
|
}
|
|
490
495
|
for (const [name, value] of Object.entries(request.env ?? {})) {
|
|
491
496
|
if (
|
|
@@ -493,21 +498,21 @@ export function assertTaskEnvironmentSessionRequest(
|
|
|
493
498
|
typeof value !== "string" ||
|
|
494
499
|
value.includes("\0")
|
|
495
500
|
) {
|
|
496
|
-
throw new
|
|
501
|
+
throw new TaskEnvironmentRequestValidationError("task environment env must contain valid NUL-free entries");
|
|
497
502
|
}
|
|
498
503
|
}
|
|
499
504
|
if (
|
|
500
505
|
!Number.isSafeInteger(request.maxOutputBytes) ||
|
|
501
506
|
request.maxOutputBytes <= 0
|
|
502
507
|
) {
|
|
503
|
-
throw new
|
|
508
|
+
throw new TaskEnvironmentRequestValidationError("task environment output limit must be a positive integer");
|
|
504
509
|
}
|
|
505
510
|
if (
|
|
506
511
|
"launchTimeoutMs" in request &&
|
|
507
512
|
(!Number.isSafeInteger(request.launchTimeoutMs) ||
|
|
508
513
|
request.launchTimeoutMs <= 0)
|
|
509
514
|
) {
|
|
510
|
-
throw new
|
|
515
|
+
throw new TaskEnvironmentRequestValidationError(
|
|
511
516
|
"task environment detached launch timeout must be a positive integer",
|
|
512
517
|
);
|
|
513
518
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-161 §2: the live-project commands wired to this runner's real state —
|
|
3
|
+
* the task's persisted environment, its run-epoch admission, its lifecycle
|
|
4
|
+
* slot, its lease grant and env replica, and its persisted env spec.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { activeCloudCredentials, credentialsMode } from "./cloud-credential-host";
|
|
8
|
+
import { getOrchestrator } from "./orchestrator";
|
|
9
|
+
import {
|
|
10
|
+
getHostTask,
|
|
11
|
+
hostTaskProjectEnvSpec,
|
|
12
|
+
readTaskRunEpochState,
|
|
13
|
+
taskProjectEnvSpec,
|
|
14
|
+
upsertHostTask,
|
|
15
|
+
type TaskProjectEnvSpec,
|
|
16
|
+
} from "./runtime-state";
|
|
17
|
+
import { taskUaiDir } from "./env";
|
|
18
|
+
import { captureTaskEnvSchemaFromClone, taskEnvSchemaObservation } from "./project-env-schema-capture";
|
|
19
|
+
import { reconstructPersistedTaskEnvironment } from "./task-environment";
|
|
20
|
+
import { forgetTaskEnvSnapshot, taskEnvResolver } from "./task-env-source";
|
|
21
|
+
import { currentTaskGatewayRegistration, setTaskGatewayEnvResolver } from "./task-gateway-state";
|
|
22
|
+
import { TaskProjectJournalStore, TaskProjectOps } from "./task-project-ops";
|
|
23
|
+
import type { TaskCommandProject } from "../src/protocol";
|
|
24
|
+
|
|
25
|
+
function runIsCurrent(taskId: string, runEpoch: number): boolean {
|
|
26
|
+
const task = getHostTask(taskId);
|
|
27
|
+
if (!task || task.statusMirror !== "running") return false;
|
|
28
|
+
const state = readTaskRunEpochState(taskId);
|
|
29
|
+
// A task started before run epochs carries none; the cloud never sends
|
|
30
|
+
// these commands to it (it has no `live-task-projects-v1` contract).
|
|
31
|
+
return state !== null && state.assignmentOpen && state.highestRunEpoch === runEpoch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Names the cloud's literal channel must never set live: the editor theme
|
|
35
|
+
* is the task's, chosen at start. */
|
|
36
|
+
const RESERVED_LITERAL_ENV = new Set(["UAI_EDITOR_THEME"]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One project (and its preview URL literals) added to a persisted env spec.
|
|
40
|
+
* Every other entry stays; a literal name the task already has keeps its
|
|
41
|
+
* value, as at start where the first project to declare one wins.
|
|
42
|
+
*/
|
|
43
|
+
export function specWithProject(
|
|
44
|
+
spec: TaskProjectEnvSpec,
|
|
45
|
+
project: TaskCommandProject,
|
|
46
|
+
previewEnv: Record<string, string> = {},
|
|
47
|
+
): TaskProjectEnvSpec {
|
|
48
|
+
const hasProject = spec.projects.some((entry) => entry.projectId === project.id);
|
|
49
|
+
const addedLiterals = Object.entries(previewEnv).filter(
|
|
50
|
+
([name]) => !RESERVED_LITERAL_ENV.has(name) && !(name in spec.previewEnv),
|
|
51
|
+
);
|
|
52
|
+
if (hasProject && addedLiterals.length === 0) return spec;
|
|
53
|
+
const [entry] = taskProjectEnvSpec({
|
|
54
|
+
projects: [project],
|
|
55
|
+
task: { previewEnv: {} } as Parameters<typeof taskProjectEnvSpec>[0]["task"],
|
|
56
|
+
}).projects;
|
|
57
|
+
return {
|
|
58
|
+
...spec,
|
|
59
|
+
projects: hasProject
|
|
60
|
+
? spec.projects
|
|
61
|
+
: [...spec.projects, entry!].sort(
|
|
62
|
+
(left, right) => left.position - right.position || left.projectId.localeCompare(right.projectId),
|
|
63
|
+
),
|
|
64
|
+
previewEnv: Object.fromEntries(
|
|
65
|
+
[...Object.entries(spec.previewEnv), ...addedLiterals].sort(([left], [right]) => left.localeCompare(right)),
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function liveProjectsSupported(taskId: string): boolean {
|
|
71
|
+
const task = getHostTask(taskId);
|
|
72
|
+
if (!task) return false;
|
|
73
|
+
try {
|
|
74
|
+
return hostTaskProjectEnvSpec(task) !== null && currentTaskGatewayRegistration(taskId) !== null;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Persist the extended spec, then ALWAYS re-point the live resolver at the
|
|
82
|
+
* persisted spec, so a repeat repairs a resolver a crash left behind. Throws
|
|
83
|
+
* when the environment can't take it; Activate then answers `incomplete`.
|
|
84
|
+
*/
|
|
85
|
+
function installProjectEnv(
|
|
86
|
+
taskId: string,
|
|
87
|
+
runEpoch: number,
|
|
88
|
+
project: TaskCommandProject,
|
|
89
|
+
previewEnv: Record<string, string>,
|
|
90
|
+
): void {
|
|
91
|
+
const task = getHostTask(taskId);
|
|
92
|
+
if (!task) throw new Error(`no such task: ${taskId}`);
|
|
93
|
+
const spec = hostTaskProjectEnvSpec(task);
|
|
94
|
+
if (spec === null) throw new Error("this task has no persisted env spec to extend");
|
|
95
|
+
const registration = currentTaskGatewayRegistration(taskId);
|
|
96
|
+
if (!registration) throw new Error("this task has no live env gateway");
|
|
97
|
+
const next = specWithProject(spec, project, previewEnv);
|
|
98
|
+
if (next !== spec) upsertHostTask(taskId, { projectEnvSpec: JSON.stringify(next) });
|
|
99
|
+
forgetTaskEnvSnapshot(taskId);
|
|
100
|
+
if (!setTaskGatewayEnvResolver(taskId, registration.generation, taskEnvResolver(taskId, next, runEpoch))) {
|
|
101
|
+
throw new Error("the task's env gateway changed while the project was installed");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let singleton: TaskProjectOps | null = null;
|
|
106
|
+
|
|
107
|
+
export function taskProjectOps(): TaskProjectOps {
|
|
108
|
+
singleton ??= new TaskProjectOps({
|
|
109
|
+
journal: new TaskProjectJournalStore(taskUaiDir),
|
|
110
|
+
environment: async (taskId) => {
|
|
111
|
+
const task = getHostTask(taskId);
|
|
112
|
+
if (!task || task.statusMirror !== "running") return null;
|
|
113
|
+
return reconstructPersistedTaskEnvironment(task);
|
|
114
|
+
},
|
|
115
|
+
runIsCurrent,
|
|
116
|
+
inLifecycleSlot: (taskId, fn) => getOrchestrator().runTaskLifecycle(taskId, fn),
|
|
117
|
+
credentials: () => (credentialsMode() === "active" ? "cloud" : "own"),
|
|
118
|
+
installedGrant: (taskId, runEpoch) => {
|
|
119
|
+
const composer = activeCloudCredentials();
|
|
120
|
+
if (!composer) return null;
|
|
121
|
+
const view = composer.registry.currentView({ taskId, runEpoch });
|
|
122
|
+
return view
|
|
123
|
+
? { runEpoch, manifestRevision: view.manifestRevision, manifestDigest: view.manifestDigest }
|
|
124
|
+
: null;
|
|
125
|
+
},
|
|
126
|
+
envServes: (taskId, runEpoch, projectId) => {
|
|
127
|
+
const composer = activeCloudCredentials();
|
|
128
|
+
// The runner's own store serves every project it holds values for.
|
|
129
|
+
if (!composer) return true;
|
|
130
|
+
const view = composer.registry.currentView({ taskId, runEpoch });
|
|
131
|
+
if (!view || !view.projectIds.includes(projectId)) return false;
|
|
132
|
+
return composer.envSnapshot({ taskId, runEpoch }, view.projectIds) !== null;
|
|
133
|
+
},
|
|
134
|
+
liveProjectsSupported,
|
|
135
|
+
captureEnvSchema: (taskId, projectId, git) => captureTaskEnvSchemaFromClone(taskId, projectId, git),
|
|
136
|
+
storedEnvSchema: (taskId, projectId) => taskEnvSchemaObservation(taskId, projectId),
|
|
137
|
+
servedProjectIds: (taskId, runEpoch) => {
|
|
138
|
+
const composer = activeCloudCredentials();
|
|
139
|
+
if (!composer) return null;
|
|
140
|
+
const view = composer.registry.currentView({ taskId, runEpoch });
|
|
141
|
+
if (!view) return null;
|
|
142
|
+
return composer.envSnapshot({ taskId, runEpoch }, view.projectIds) !== null ? [...view.projectIds] : null;
|
|
143
|
+
},
|
|
144
|
+
installProjectEnv,
|
|
145
|
+
});
|
|
146
|
+
return singleton;
|
|
147
|
+
}
|