@runuai/host 0.9.149 → 0.9.151
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/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/meta/_journal.json +14 -0
- package/db/schema.ts +188 -0
- package/lib/agent-cli.ts +11 -13
- package/lib/agent.ts +177 -7
- package/lib/agents/durable-proc.ts +56 -44
- package/lib/agents/registry.ts +19 -3
- package/lib/agents/session-state.ts +23 -10
- package/lib/agents/transport.ts +101 -69
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +4 -3
- package/lib/attachments.ts +19 -17
- package/lib/capture-flow.ts +319 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2544 -0
- package/lib/cloud-credential-runtime.ts +1035 -0
- package/lib/codex-auth.ts +21 -1
- package/lib/container-runtime.ts +76 -0
- package/lib/credential-cleanup-inventory.ts +405 -0
- package/lib/credential-consumers/engine-anchor.ts +1876 -0
- package/lib/credential-consumers/engines.ts +773 -0
- package/lib/credential-consumers/github.ts +594 -0
- package/lib/credential-consumers/mcp.ts +384 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-store-pickup.ts +15 -2
- package/lib/credential-store.ts +361 -33
- package/lib/credentials-activation-authority.ts +546 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/db.ts +11 -0
- package/lib/docker-exec.ts +3 -1
- package/lib/engine-accounts.ts +42 -32
- package/lib/engine-connect.ts +47 -2
- package/lib/engine-credential-env.ts +50 -0
- package/lib/engine-login.ts +83 -1
- package/lib/env-file.ts +62 -8
- package/lib/env.ts +5 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +219 -177
- package/lib/git-identity.ts +59 -14
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +56 -21
- package/lib/host-env.ts +17 -4
- package/lib/launch-mode.ts +25 -0
- package/lib/legacy-runtime-retirement.ts +388 -0
- package/lib/machine-exec.ts +14 -3
- package/lib/machine-provider-aws.ts +50 -8
- package/lib/machine-provider-local.ts +4 -0
- package/lib/machine-provider.ts +4 -0
- package/lib/managed-operation-drain.ts +8 -0
- package/lib/managed-runtime.ts +371 -7
- package/lib/mcp-capture.ts +860 -0
- package/lib/mcp-connections.ts +8 -1
- package/lib/mcp-gateway.ts +168 -10
- package/lib/obs.ts +2 -6
- package/lib/orchestrator.ts +1847 -236
- package/lib/person-capture.ts +327 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +50 -3
- package/lib/runtime-archive.ts +68 -1
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +429 -13
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/task-trust.ts +34 -3
- package/lib/sidecar-resume.ts +18 -5
- package/lib/skills.ts +47 -8
- package/lib/standard-image.ts +157 -103
- package/lib/stopped-task-status-queue.ts +68 -10
- package/lib/task-credential-cleanup.ts +525 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +66 -163
- package/lib/task-env-source.ts +68 -14
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +69 -28
- package/lib/task-environment/docker.ts +101 -20
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +459 -96
- package/lib/task-environment/machine.ts +101 -11
- package/lib/task-environment/self.ts +108 -50
- package/lib/task-environment/types.ts +89 -5
- package/lib/task-environment/workspace-files.ts +623 -25
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-gateway-recovery.ts +4 -1
- package/lib/task-gateway-state.ts +15 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +56 -2
- package/lib/token-broker-local.ts +28 -4
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +368 -28
- package/lib/token-lanes/openai-oauth.ts +7 -3
- package/lib/token-lanes/xai-oauth.ts +7 -3
- package/lib/transcript.ts +9 -5
- package/package.json +6 -2
- package/scripts/agent/task-up.sh +242 -100
- package/src/cloud-credentials.ts +2399 -0
- package/src/event-outbox.ts +8 -6
- package/src/index.ts +1386 -875
- package/src/main.ts +1062 -140
- package/src/outbound-frame.ts +31 -0
- package/src/project-env-policy.ts +125 -10
- package/src/protocol.ts +699 -23
- package/src/ui/server.ts +48 -10
package/lib/task-diff.ts
CHANGED
|
@@ -1,188 +1,91 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { join, posix } from "node:path";
|
|
4
|
-
|
|
5
1
|
import type { TaskDiffInput, TaskDiffResult } from "../src/protocol";
|
|
6
|
-
import { taskWorkspaceDir } from "./env";
|
|
7
2
|
import {
|
|
3
|
+
countDiffLines,
|
|
4
|
+
jsonEscapedBytes,
|
|
8
5
|
parseGitDiff,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
parseTaskDiffOutput,
|
|
7
|
+
taskDiffExecFailure,
|
|
8
|
+
taskDiffRequest,
|
|
9
|
+
TASK_DIFF_LINE_RESULT_BYTES,
|
|
10
|
+
TASK_DIFF_MAX_LINES,
|
|
11
|
+
TASK_DIFF_OUTPUT_BYTES,
|
|
12
|
+
TASK_DIFF_REPO_RESULT_BYTES,
|
|
13
|
+
TASK_DIFF_RESULT_BYTES,
|
|
14
14
|
} from "./git-diff";
|
|
15
15
|
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
16
16
|
|
|
17
|
-
// Cap how many untracked files we render as new-file patches. Each is a
|
|
18
|
-
// separate in-environment Git execution, so a pathological untracked dir that
|
|
19
|
-
// slips past .gitignore shouldn't turn the diff into thousands of execs.
|
|
20
|
-
const MAX_UNTRACKED = 200;
|
|
21
|
-
|
|
22
17
|
export type TaskDiffEnvironment = Pick<
|
|
23
18
|
TaskEnvironmentHandle,
|
|
24
19
|
"descriptor" | "exec"
|
|
25
20
|
>;
|
|
26
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A task's diff, as one run of TASK_DIFF_SCRIPT in `environment`: the task's
|
|
24
|
+
* own runtime while it runs, a throwaway sandbox once it has stopped
|
|
25
|
+
* (task-diff-sandbox.ts). Never Git on the host: what a task configured in
|
|
26
|
+
* its repositories would run there as the runner's user (#cb3j0z).
|
|
27
|
+
*
|
|
28
|
+
* Each project's diff is the whole branch against its base, including
|
|
29
|
+
* uncommitted work: the merge-base (where the branch diverged from the
|
|
30
|
+
* remote's default branch) against the working tree, then each untracked
|
|
31
|
+
* file as a new-file patch. A project the run could not diff, or whose
|
|
32
|
+
* output the bound cut short, carries an error and no files: a partial
|
|
33
|
+
* patch is never taken for the whole. The whole diff renders at most
|
|
34
|
+
* TASK_DIFF_MAX_LINES lines, and its result serializes to at most
|
|
35
|
+
* TASK_DIFF_RESULT_BYTES (its patches JSON-escaped, and each line's
|
|
36
|
+
* object): a project that would pass what is left of either is an error,
|
|
37
|
+
* never parsed.
|
|
38
|
+
*/
|
|
27
39
|
export async function buildTaskDiff(
|
|
28
40
|
input: TaskDiffInput,
|
|
29
|
-
environment: TaskDiffEnvironment
|
|
41
|
+
environment: TaskDiffEnvironment,
|
|
30
42
|
): Promise<TaskDiffResult> {
|
|
31
|
-
if (
|
|
32
|
-
environment !== null &&
|
|
33
|
-
environment.descriptor.taskId !== input.taskId
|
|
34
|
-
) {
|
|
43
|
+
if (environment.descriptor.taskId !== input.taskId) {
|
|
35
44
|
throw new Error("task diff environment belongs to a different task");
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
if (input.projects.length === 0) return { repos: [] };
|
|
47
|
+
const slugs = input.projects.map((project) => project.slug);
|
|
48
|
+
const result = await environment.exec(taskDiffRequest(environment.descriptor.workspacePath, slugs));
|
|
49
|
+
const failure = taskDiffExecFailure(result);
|
|
50
|
+
const output = failure === null ? parseTaskDiffOutput(Buffer.from(result.stdout).toString("utf8")) : [];
|
|
51
|
+
const bounded = failure === null && result.stdout.length >= TASK_DIFF_OUTPUT_BYTES;
|
|
52
|
+
let linesLeft = TASK_DIFF_MAX_LINES;
|
|
53
|
+
// Each project's own entry, and the result's wrapper, come out first: the
|
|
54
|
+
// bound is on the whole result as it is sent.
|
|
55
|
+
let bytesLeft = Math.max(0, TASK_DIFF_RESULT_BYTES - 64 - input.projects.length * TASK_DIFF_REPO_RESULT_BYTES);
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const base = await resolveBase(environment, cwd, environmentCwd);
|
|
52
|
-
// Diff the whole branch against base, including UNCOMMITTED work: compare
|
|
53
|
-
// the merge-base (where the branch diverged) to the WORKING TREE, not HEAD.
|
|
54
|
-
// The merge-base keeps base's own later commits from showing up reversed if
|
|
55
|
-
// it advanced; falling back to `base` itself if merge-base can't resolve.
|
|
56
|
-
const mergeBase =
|
|
57
|
-
(await runGitText(environment, cwd, environmentCwd, [
|
|
58
|
-
"merge-base",
|
|
59
|
-
base,
|
|
60
|
-
"HEAD",
|
|
61
|
-
])) ?? base;
|
|
62
|
-
try {
|
|
63
|
-
// Tracked changes (committed + staged + unstaged): a commit-ish vs the
|
|
64
|
-
// working tree, so omitting `..HEAD` is the point.
|
|
65
|
-
let text = environment
|
|
66
|
-
? await runGitDiffInEnvironment(
|
|
67
|
-
environment,
|
|
68
|
-
environmentCwd!,
|
|
69
|
-
mergeBase,
|
|
70
|
-
)
|
|
71
|
-
: await runGitDiff(cwd, mergeBase);
|
|
72
|
-
|
|
73
|
-
// Untracked files: `git diff <commit>` skips them, so list (honouring
|
|
74
|
-
// .gitignore) and append each as a /dev/null new-file patch.
|
|
75
|
-
const untracked = await listUntracked(
|
|
76
|
-
environment,
|
|
77
|
-
cwd,
|
|
78
|
-
environmentCwd,
|
|
79
|
-
);
|
|
80
|
-
for (const file of untracked) {
|
|
81
|
-
const patch = environment
|
|
82
|
-
? await runGitDiffUntrackedInEnvironment(
|
|
83
|
-
environment,
|
|
84
|
-
environmentCwd!,
|
|
85
|
-
file,
|
|
86
|
-
)
|
|
87
|
-
: await runGitDiffUntracked(cwd, file);
|
|
88
|
-
if (patch.trim().length === 0) continue;
|
|
89
|
-
if (text.length > 0 && !text.endsWith("\n")) text += "\n";
|
|
90
|
-
text += patch;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
out.push({
|
|
94
|
-
id: project.id,
|
|
95
|
-
name: project.slug,
|
|
96
|
-
mountPath: project.slug,
|
|
97
|
-
defaultBranch: stripOriginPrefix(base),
|
|
98
|
-
files: parseGitDiff(text),
|
|
99
|
-
});
|
|
100
|
-
} catch (err) {
|
|
101
|
-
out.push({
|
|
57
|
+
return {
|
|
58
|
+
repos: input.projects.map((project, index) => {
|
|
59
|
+
const found = output[index]?.slug === project.slug ? output[index] : undefined;
|
|
60
|
+
const entry = {
|
|
102
61
|
id: project.id,
|
|
103
62
|
name: project.slug,
|
|
104
63
|
mountPath: project.slug,
|
|
105
|
-
defaultBranch: stripOriginPrefix(base),
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
environment: TaskDiffEnvironment | null,
|
|
129
|
-
hostCwd: string,
|
|
130
|
-
environmentCwd: string | null,
|
|
131
|
-
): Promise<string> {
|
|
132
|
-
const head = await runGitText(
|
|
133
|
-
environment,
|
|
134
|
-
hostCwd,
|
|
135
|
-
environmentCwd,
|
|
136
|
-
["rev-parse", "--abbrev-ref", "origin/HEAD"],
|
|
137
|
-
);
|
|
138
|
-
if (head) return head;
|
|
139
|
-
|
|
140
|
-
return "origin/main";
|
|
64
|
+
defaultBranch: stripOriginPrefix(found?.base ?? "origin/main"),
|
|
65
|
+
};
|
|
66
|
+
if (found && found.error === undefined && found.tracked && found.untracked) {
|
|
67
|
+
const patches = found.patches ?? "";
|
|
68
|
+
const lines = countDiffLines(patches);
|
|
69
|
+
if (lines > linesLeft) {
|
|
70
|
+
return { ...entry, files: [], error: `the diff has more than ${TASK_DIFF_MAX_LINES} lines` };
|
|
71
|
+
}
|
|
72
|
+
const bytes = jsonEscapedBytes(patches) + lines * TASK_DIFF_LINE_RESULT_BYTES;
|
|
73
|
+
if (bytes > bytesLeft) {
|
|
74
|
+
return { ...entry, files: [], error: "the diff is too large to send" };
|
|
75
|
+
}
|
|
76
|
+
linesLeft -= lines;
|
|
77
|
+
bytesLeft -= bytes;
|
|
78
|
+
return { ...entry, files: parseGitDiff(patches) };
|
|
79
|
+
}
|
|
80
|
+
const error =
|
|
81
|
+
failure ??
|
|
82
|
+
found?.error ??
|
|
83
|
+
(bounded ? "the diff exceeded its output bound" : "the diff did not finish");
|
|
84
|
+
return { ...entry, files: [], error };
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
141
87
|
}
|
|
142
88
|
|
|
143
89
|
function stripOriginPrefix(ref: string): string {
|
|
144
90
|
return ref.startsWith("origin/") ? ref.slice("origin/".length) : ref;
|
|
145
91
|
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* List untracked files in the worktree, honouring .gitignore
|
|
149
|
-
* (`--exclude-standard`), capped at {@link MAX_UNTRACKED}. Empty on error.
|
|
150
|
-
*/
|
|
151
|
-
async function listUntracked(
|
|
152
|
-
environment: TaskDiffEnvironment | null,
|
|
153
|
-
hostCwd: string,
|
|
154
|
-
environmentCwd: string | null,
|
|
155
|
-
): Promise<string[]> {
|
|
156
|
-
const raw = await runGitText(environment, hostCwd, environmentCwd, [
|
|
157
|
-
"ls-files",
|
|
158
|
-
"--others",
|
|
159
|
-
"--exclude-standard",
|
|
160
|
-
]);
|
|
161
|
-
if (!raw) return [];
|
|
162
|
-
return raw
|
|
163
|
-
.split("\n")
|
|
164
|
-
.filter((line) => line.length > 0)
|
|
165
|
-
.slice(0, MAX_UNTRACKED);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Run a read-only Git command (host or in-environment) and return trimmed
|
|
170
|
-
* stdout, or null on any failure. Used for base-branch resolution where a
|
|
171
|
-
* missing upstream is expected, not exceptional.
|
|
172
|
-
*/
|
|
173
|
-
async function runGitText(
|
|
174
|
-
environment: TaskDiffEnvironment | null,
|
|
175
|
-
hostCwd: string,
|
|
176
|
-
environmentCwd: string | null,
|
|
177
|
-
args: string[],
|
|
178
|
-
): Promise<string | null> {
|
|
179
|
-
if (environment) {
|
|
180
|
-
return tryRunGitTextInEnvironment(environment, environmentCwd!, args);
|
|
181
|
-
}
|
|
182
|
-
const result = spawnSync("git", ["-C", hostCwd, ...args], {
|
|
183
|
-
encoding: "utf8",
|
|
184
|
-
});
|
|
185
|
-
if (result.status !== 0) return null;
|
|
186
|
-
const text = (result.stdout ?? "").trim();
|
|
187
|
-
return text.length > 0 ? text : null;
|
|
188
|
-
}
|
package/lib/task-env-source.ts
CHANGED
|
@@ -18,9 +18,18 @@
|
|
|
18
18
|
* store). Its observations are stored once and never change. So a change
|
|
19
19
|
* reaches the next command and the next request, while a command that
|
|
20
20
|
* changes nothing reads nothing.
|
|
21
|
+
*
|
|
22
|
+
* ADR-154: once the runner begins leaving its own credentials, neither
|
|
23
|
+
* store is read. On cloud credentials a task's env is what the composer
|
|
24
|
+
* serves for exactly the run the reading generation serves (/env's gateway
|
|
25
|
+
* generation, the proxy's live one) while that run is the task's open one,
|
|
26
|
+
* read afresh every time (its gate closes the moment the run's credentials
|
|
27
|
+
* do, and no copy of it is kept here). Until the runner is active, a task
|
|
28
|
+
* gets no project env at all.
|
|
21
29
|
*/
|
|
22
30
|
|
|
23
|
-
import {
|
|
31
|
+
import { activeCloudCredentials, onLeavingLegacy, onOwnCredentials } from "./cloud-credential-host";
|
|
32
|
+
import { noteProjectEnvViewChange, projectEnvAuthorityRevision, projectEnvRevision } from "./host-env";
|
|
24
33
|
import { readProjectEnvAuthoritySnapshot } from "./project-env-authority";
|
|
25
34
|
import { readProjectEnvReplicaSnapshot, readProjectEnvReplicaStatus } from "./project-env-replica";
|
|
26
35
|
import { taskProjectEnvResolver, type TaskEnvSource } from "./project-env-loader";
|
|
@@ -32,11 +41,11 @@ import {
|
|
|
32
41
|
type TaskEnvHead,
|
|
33
42
|
type TaskEnvSnapshot,
|
|
34
43
|
} from "./project-env-task-policy";
|
|
35
|
-
import { getHostTask, hostTaskProjectEnvSpec, type TaskProjectEnvSpec } from "./runtime-state";
|
|
44
|
+
import { getHostTask, hostTaskProjectEnvSpec, readTaskRunEpochState, type TaskProjectEnvSpec } from "./runtime-state";
|
|
36
45
|
import { sidecarMode } from "./sidecar";
|
|
37
46
|
import { taskProxiedEnv } from "./secrets-proxy/policy";
|
|
38
47
|
import { taskTrustEnv } from "./secrets-proxy/readiness";
|
|
39
|
-
import type
|
|
48
|
+
import { currentTaskGatewayRun, type TaskGatewayEnvResolver } from "./task-gateway-state";
|
|
40
49
|
|
|
41
50
|
/** The task's projects, lead first, as its launch pinned them. */
|
|
42
51
|
function projectIdsOf(spec: TaskProjectEnvSpec): string[] {
|
|
@@ -52,8 +61,13 @@ function projectIdsOf(spec: TaskProjectEnvSpec): string[] {
|
|
|
52
61
|
* otherwise the records, values and observations the task's decisions come
|
|
53
62
|
* from.
|
|
54
63
|
*/
|
|
55
|
-
export function readTaskEnvSnapshot(
|
|
64
|
+
export function readTaskEnvSnapshot(
|
|
65
|
+
taskId: string,
|
|
66
|
+
spec: TaskProjectEnvSpec,
|
|
67
|
+
runEpoch: number | null,
|
|
68
|
+
): TaskEnvSnapshot | "legacy" | null {
|
|
56
69
|
const projectIds = projectIdsOf(spec);
|
|
70
|
+
if (!onOwnCredentials()) return cloudTaskEnvSnapshot(taskId, projectIds, runEpoch);
|
|
57
71
|
const key = JSON.stringify([projectEnvAuthorityRevision(), projectIds.map((projectId) => [projectId, projectEnvRevision(projectId)])]);
|
|
58
72
|
const hit = snapshots.get(taskId);
|
|
59
73
|
if (hit?.key === key) return hit.snapshot;
|
|
@@ -79,6 +93,33 @@ export function forgetTaskEnvSnapshot(taskId: string): void {
|
|
|
79
93
|
snapshots.delete(taskId);
|
|
80
94
|
}
|
|
81
95
|
|
|
96
|
+
/**
|
|
97
|
+
* ADR-154: the task's env as the composer serves it for `runEpoch`, the
|
|
98
|
+
* reading generation's run, with the task's own observations; null unless
|
|
99
|
+
* the runner is active and that run is the task's admitted, open one (a
|
|
100
|
+
* generation from an older runtime gets nothing while a newer run starts),
|
|
101
|
+
* or while the composer serves nothing for it.
|
|
102
|
+
*/
|
|
103
|
+
function cloudTaskEnvSnapshot(
|
|
104
|
+
taskId: string,
|
|
105
|
+
projectIds: readonly string[],
|
|
106
|
+
runEpoch: number | null,
|
|
107
|
+
): TaskEnvSnapshot | null {
|
|
108
|
+
const composer = activeCloudCredentials();
|
|
109
|
+
if (composer === null || runEpoch === null) return null;
|
|
110
|
+
const run = readTaskRunEpochState(taskId);
|
|
111
|
+
if (run === null || !run.assignmentOpen || run.highestRunEpoch !== runEpoch) return null;
|
|
112
|
+
const served = composer.envSnapshot({ taskId, runEpoch }, projectIds);
|
|
113
|
+
if (served === null) return null;
|
|
114
|
+
return snapshotOf(taskId, projectIds, (projectId) =>
|
|
115
|
+
(served.projects.find((project) => project.projectId === projectId)?.entries ?? []).map((entry) => ({
|
|
116
|
+
key: entry.state.key,
|
|
117
|
+
state: entry.state,
|
|
118
|
+
value: entry.materialization.kind === "installed" ? entry.value : null,
|
|
119
|
+
})),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
82
123
|
function freshTaskEnvSnapshot(taskId: string, projectIds: readonly string[]): TaskEnvSnapshot | "legacy" | null {
|
|
83
124
|
return sidecarMode() ? replicaTaskEnvSnapshot(taskId, projectIds) : ownedTaskEnvSnapshot(taskId, projectIds);
|
|
84
125
|
}
|
|
@@ -152,16 +193,18 @@ function snapshotOf(taskId: string, projectIds: readonly string[], entriesOf: (p
|
|
|
152
193
|
};
|
|
153
194
|
}
|
|
154
195
|
|
|
155
|
-
/** Where the task's /env takes its project values from right now
|
|
156
|
-
|
|
157
|
-
|
|
196
|
+
/** Where the task's /env takes its project values from right now, for a
|
|
197
|
+
* generation serving `runEpoch`. */
|
|
198
|
+
export function taskEnvSource(taskId: string, spec: TaskProjectEnvSpec, runEpoch: number | null): TaskEnvSource {
|
|
199
|
+
const snapshot = readTaskEnvSnapshot(taskId, spec, runEpoch);
|
|
158
200
|
if (snapshot === "legacy") return { kind: "stored" };
|
|
159
201
|
if (snapshot === null) return { kind: "none" };
|
|
160
202
|
return { kind: "view", view: taskEnvView(snapshot) };
|
|
161
203
|
}
|
|
162
204
|
|
|
163
205
|
/** The task's snapshot for its proxy, by task id alone: what its persisted
|
|
164
|
-
* launch spec names
|
|
206
|
+
* launch spec names, for the run its live gateway generation serves.
|
|
207
|
+
* Nothing is proxied from a store never activated. */
|
|
165
208
|
export function proxyTaskEnvSnapshot(taskId: string): TaskEnvSnapshot | null {
|
|
166
209
|
const task = getHostTask(taskId);
|
|
167
210
|
let spec: TaskProjectEnvSpec | null = null;
|
|
@@ -171,23 +214,34 @@ export function proxyTaskEnvSnapshot(taskId: string): TaskEnvSnapshot | null {
|
|
|
171
214
|
return null;
|
|
172
215
|
}
|
|
173
216
|
if (spec === null) return null;
|
|
174
|
-
const snapshot = readTaskEnvSnapshot(taskId, spec);
|
|
217
|
+
const snapshot = readTaskEnvSnapshot(taskId, spec, currentTaskGatewayRun(taskId));
|
|
175
218
|
return snapshot === "legacy" ? null : snapshot;
|
|
176
219
|
}
|
|
177
220
|
|
|
178
221
|
/** The task's note about the keys it does not get, from the store as it is
|
|
179
222
|
* 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);
|
|
223
|
+
export function taskEnvWithheldNoteNow(taskId: string, spec: TaskProjectEnvSpec, runEpoch: number | null): string | null {
|
|
224
|
+
const source = taskEnvSource(taskId, spec, runEpoch);
|
|
182
225
|
return source.kind === "view" ? taskEnvWithheldNote(source.view.policies) : null;
|
|
183
226
|
}
|
|
184
227
|
|
|
185
|
-
/** The resolver a loader task's gateway generation renders /env with
|
|
186
|
-
|
|
228
|
+
/** The resolver a loader task's gateway generation renders /env with, for
|
|
229
|
+
* the run that generation serves (null: one from before run epochs). */
|
|
230
|
+
export function taskEnvResolver(taskId: string, spec: TaskProjectEnvSpec, runEpoch: number | null): TaskGatewayEnvResolver {
|
|
187
231
|
return taskProjectEnvResolver(
|
|
188
232
|
spec,
|
|
189
233
|
() => taskTrustEnv(taskId),
|
|
190
234
|
() => taskProxiedEnv(taskId),
|
|
191
|
-
() => taskEnvSource(taskId, spec),
|
|
235
|
+
() => taskEnvSource(taskId, spec, runEpoch),
|
|
192
236
|
);
|
|
193
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* ADR-154: the moment the runner begins leaving its own credentials, every
|
|
241
|
+
* read above stops taking their values (each checks `onOwnCredentials()`),
|
|
242
|
+
* and whoever follows the env (the proxy's trust delivery, which unpins
|
|
243
|
+
* what is no longer proxied) looks again. Registered once, at boot.
|
|
244
|
+
*/
|
|
245
|
+
export function followLeavingLegacy(): () => void {
|
|
246
|
+
return onLeavingLegacy(() => noteProjectEnvViewChange());
|
|
247
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-154 §6, for providers whose runtime is a container or a machine they
|
|
3
|
+
* exec into (Docker, Apple, a machine over ssh): an anchored request runs
|
|
4
|
+
* as root, the engine supervisor first, which drops to the task user the
|
|
5
|
+
* request would have run as before it starts the request's own argv.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Who a request with no user runs as in a task runtime, as the engines
|
|
9
|
+
* always have (their homes are under /home/node). */
|
|
10
|
+
export const ANCHORED_TASK_USER = "node";
|
|
11
|
+
|
|
12
|
+
/** `request`, run as root under its anchor; unchanged without one. */
|
|
13
|
+
export function rootAnchoredRequest<
|
|
14
|
+
T extends {
|
|
15
|
+
argv: readonly [string, ...string[]];
|
|
16
|
+
user?: string;
|
|
17
|
+
anchor?: { readonly prefix: readonly [string, ...string[]] };
|
|
18
|
+
},
|
|
19
|
+
>(request: T): T {
|
|
20
|
+
const { anchor } = request;
|
|
21
|
+
if (!anchor) return request;
|
|
22
|
+
if (request.user === "root") {
|
|
23
|
+
throw new Error("an anchored process runs as a task user, never as root");
|
|
24
|
+
}
|
|
25
|
+
const { anchor: _anchor, ...rest } = request;
|
|
26
|
+
return {
|
|
27
|
+
...rest,
|
|
28
|
+
user: "root",
|
|
29
|
+
argv: [...anchor.prefix, request.user ?? ANCHORED_TASK_USER, "--", ...request.argv],
|
|
30
|
+
} as unknown as T;
|
|
31
|
+
}
|
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
} from "./workspace-files";
|
|
29
29
|
import { assertSafeHostTaskId } from "../task-identity";
|
|
30
30
|
import { applyProjectEnvProfile } from "../runtime-authority";
|
|
31
|
+
import { rootAnchoredRequest } from "./anchor";
|
|
31
32
|
import {
|
|
32
33
|
capturedCliExec,
|
|
34
|
+
cliExecEnv,
|
|
33
35
|
streamingCliExec,
|
|
34
36
|
} from "./docker";
|
|
35
37
|
import {
|
|
@@ -37,6 +39,8 @@ import {
|
|
|
37
39
|
assertTaskEnvironmentProcessRequest,
|
|
38
40
|
refusePrivilegedExec,
|
|
39
41
|
assertTaskEnvironmentSessionRequest,
|
|
42
|
+
type TaskEnvironmentCallOptions,
|
|
43
|
+
type TaskEnvironmentReadOptions,
|
|
40
44
|
type TaskEnvironmentCopyRequest,
|
|
41
45
|
type TaskEnvironmentDescriptor,
|
|
42
46
|
type TaskEnvironmentDetachedSessionRequest,
|
|
@@ -54,6 +58,7 @@ import {
|
|
|
54
58
|
type TaskEnvironmentSpawnRequest,
|
|
55
59
|
type TaskEnvironmentStatus,
|
|
56
60
|
} from "./types";
|
|
61
|
+
import type { TaskWriteEnvironment } from "../credential-consumers/task-files";
|
|
57
62
|
|
|
58
63
|
export const APPLE_TASK_ENVIRONMENT_PROVIDER = "apple-container";
|
|
59
64
|
const APPLE_LOCATOR_SCHEMA_VERSION = 1;
|
|
@@ -88,7 +93,13 @@ export interface AppleCliResult {
|
|
|
88
93
|
|
|
89
94
|
export type AppleCliRunner = (
|
|
90
95
|
args: readonly string[],
|
|
91
|
-
options: {
|
|
96
|
+
options: {
|
|
97
|
+
timeoutMs: number;
|
|
98
|
+
maxOutputBytes: number;
|
|
99
|
+
/** The CLI child's own environment, when an exec names a credential
|
|
100
|
+
* bare (cliExecEnv). Absent, it inherits the runner's. */
|
|
101
|
+
env?: NodeJS.ProcessEnv;
|
|
102
|
+
},
|
|
92
103
|
) => Promise<AppleCliResult>;
|
|
93
104
|
|
|
94
105
|
export interface AppleContainerTaskEnvironmentDeps<
|
|
@@ -137,6 +148,7 @@ export interface AppleContainerProvisioningDeps<TInput, TCredentials, TResult> {
|
|
|
137
148
|
input: TInput,
|
|
138
149
|
credentials?: TCredentials,
|
|
139
150
|
envMode?: ProjectEnvMode,
|
|
151
|
+
runtimeEnvironment?: () => TaskWriteEnvironment,
|
|
140
152
|
) => Promise<TResult>;
|
|
141
153
|
inputTaskId: (input: TInput) => string;
|
|
142
154
|
/** The host worktree path this task will occupy, derivable before task-up
|
|
@@ -193,6 +205,9 @@ export function createAppleContainerTaskEnvironmentProvider<
|
|
|
193
205
|
request.input,
|
|
194
206
|
request.credentials,
|
|
195
207
|
request.envMode ?? "loader",
|
|
208
|
+
// ADR-154 §12: the container as task-up has created it, for the
|
|
209
|
+
// runtime-ready writes before uai-init.
|
|
210
|
+
() => new AppleContainerTaskEnvironmentHandle(preparedLocator, resolved, request.envMode ?? "loader"),
|
|
196
211
|
);
|
|
197
212
|
const prepared = parseAppleTaskEnvironmentLocator(preparedLocator);
|
|
198
213
|
// task-up composes its worktree path in shell from the operator's
|
|
@@ -271,6 +286,8 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
271
286
|
{
|
|
272
287
|
readonly descriptor: TaskEnvironmentDescriptor;
|
|
273
288
|
readonly durableIdentity: string;
|
|
289
|
+
/** ADR-154 §6: anchored requests start under the engine supervisor. */
|
|
290
|
+
readonly anchors = true;
|
|
274
291
|
readonly #value: AppleTaskEnvironmentLocatorValue;
|
|
275
292
|
|
|
276
293
|
constructor(
|
|
@@ -288,16 +305,19 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
288
305
|
};
|
|
289
306
|
}
|
|
290
307
|
|
|
291
|
-
async readWorkspaceFile(filePath: string): Promise<Uint8Array | null> {
|
|
308
|
+
async readWorkspaceFile(filePath: string, options?: TaskEnvironmentReadOptions): Promise<Uint8Array | null> {
|
|
309
|
+
options?.admit?.();
|
|
292
310
|
return hostWorkspaceRead(
|
|
293
311
|
this.descriptor,
|
|
294
312
|
path.join(this.#value.hostWorktreePath, "workspace"),
|
|
295
313
|
filePath,
|
|
314
|
+
options,
|
|
296
315
|
);
|
|
297
316
|
}
|
|
298
317
|
|
|
299
|
-
async writeWorkspaceFile(filePath: string, bytes: Uint8Array): Promise<void> {
|
|
300
|
-
|
|
318
|
+
async writeWorkspaceFile(filePath: string, bytes: Uint8Array, options?: TaskEnvironmentCallOptions): Promise<void> {
|
|
319
|
+
options?.admit?.();
|
|
320
|
+
await hostWorkspaceWrite(
|
|
301
321
|
this.descriptor,
|
|
302
322
|
path.join(this.#value.hostWorktreePath, "workspace"),
|
|
303
323
|
filePath,
|
|
@@ -309,16 +329,22 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
309
329
|
assertTaskEnvironmentExecRequest(request);
|
|
310
330
|
refusePrivilegedExec(request);
|
|
311
331
|
const profiled = applyProjectEnvProfile(
|
|
312
|
-
request,
|
|
332
|
+
rootAnchoredRequest(request),
|
|
313
333
|
this.descriptor.envMode ?? "baked",
|
|
314
334
|
"/usr/bin/zsh",
|
|
315
335
|
);
|
|
316
336
|
request.admit?.();
|
|
337
|
+
const invocation = appleExecArgs(
|
|
338
|
+
this.#value.containerName,
|
|
339
|
+
profiled,
|
|
340
|
+
"interactive",
|
|
341
|
+
);
|
|
317
342
|
return capturedCliExec(
|
|
318
343
|
this.deps.spawn,
|
|
319
344
|
this.deps.containerCliPath(),
|
|
320
|
-
|
|
345
|
+
invocation.args,
|
|
321
346
|
profiled,
|
|
347
|
+
invocation.env,
|
|
322
348
|
);
|
|
323
349
|
}
|
|
324
350
|
|
|
@@ -327,15 +353,22 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
327
353
|
): Promise<TaskEnvironmentProcess> {
|
|
328
354
|
assertTaskEnvironmentProcessRequest(request);
|
|
329
355
|
const profiled = applyProjectEnvProfile(
|
|
330
|
-
request,
|
|
356
|
+
rootAnchoredRequest(request),
|
|
331
357
|
this.descriptor.envMode ?? "baked",
|
|
332
358
|
"/usr/bin/zsh",
|
|
333
359
|
);
|
|
360
|
+
const invocation = appleExecArgs(
|
|
361
|
+
this.#value.containerName,
|
|
362
|
+
profiled,
|
|
363
|
+
"interactive",
|
|
364
|
+
);
|
|
365
|
+
request.admit?.();
|
|
334
366
|
return streamingCliExec(
|
|
335
367
|
this.deps.spawn,
|
|
336
368
|
this.deps.containerCliPath(),
|
|
337
|
-
|
|
369
|
+
invocation.args,
|
|
338
370
|
profiled,
|
|
371
|
+
invocation.env,
|
|
339
372
|
);
|
|
340
373
|
}
|
|
341
374
|
|
|
@@ -344,15 +377,22 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
344
377
|
): Promise<TaskEnvironmentProcess> {
|
|
345
378
|
assertTaskEnvironmentSessionRequest(request);
|
|
346
379
|
const profiled = applyProjectEnvProfile(
|
|
347
|
-
request,
|
|
380
|
+
rootAnchoredRequest(request),
|
|
348
381
|
this.descriptor.envMode ?? "baked",
|
|
349
382
|
"/usr/bin/zsh",
|
|
350
383
|
);
|
|
384
|
+
const invocation = appleExecArgs(
|
|
385
|
+
this.#value.containerName,
|
|
386
|
+
profiled,
|
|
387
|
+
"interactive",
|
|
388
|
+
);
|
|
389
|
+
request.admit?.();
|
|
351
390
|
return streamingCliExec(
|
|
352
391
|
this.deps.spawn,
|
|
353
392
|
this.deps.containerCliPath(),
|
|
354
|
-
|
|
393
|
+
invocation.args,
|
|
355
394
|
profiled,
|
|
395
|
+
invocation.env,
|
|
356
396
|
);
|
|
357
397
|
}
|
|
358
398
|
|
|
@@ -361,17 +401,21 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
361
401
|
): Promise<TaskEnvironmentExecResult> {
|
|
362
402
|
assertTaskEnvironmentSessionRequest(request);
|
|
363
403
|
const profiled = applyProjectEnvProfile(
|
|
364
|
-
request,
|
|
404
|
+
rootAnchoredRequest(request),
|
|
365
405
|
this.descriptor.envMode ?? "baked",
|
|
366
406
|
"/usr/bin/zsh",
|
|
367
407
|
);
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
maxOutputBytes: request.maxOutputBytes,
|
|
373
|
-
},
|
|
408
|
+
const invocation = appleExecArgs(
|
|
409
|
+
this.#value.containerName,
|
|
410
|
+
profiled,
|
|
411
|
+
"detached",
|
|
374
412
|
);
|
|
413
|
+
request.admit?.();
|
|
414
|
+
const result = await this.deps.run(invocation.args, {
|
|
415
|
+
timeoutMs: request.launchTimeoutMs,
|
|
416
|
+
maxOutputBytes: request.maxOutputBytes,
|
|
417
|
+
...(invocation.env === undefined ? {} : { env: invocation.env }),
|
|
418
|
+
});
|
|
375
419
|
return {
|
|
376
420
|
exitCode: result.status,
|
|
377
421
|
signal: null,
|
|
@@ -396,6 +440,7 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
396
440
|
`${this.#value.containerName}:${request.source}`,
|
|
397
441
|
request.destination,
|
|
398
442
|
];
|
|
443
|
+
request.admit?.();
|
|
399
444
|
const result = await this.deps.run(args, {
|
|
400
445
|
timeoutMs: 120_000,
|
|
401
446
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -598,6 +643,8 @@ class AppleContainerTaskEnvironmentHandle
|
|
|
598
643
|
}
|
|
599
644
|
}
|
|
600
645
|
|
|
646
|
+
/** The argv of one `container exec`, and the environment the CLI must run
|
|
647
|
+
* with to resolve the credentials it names (cliExecEnv). */
|
|
601
648
|
function appleExecArgs(
|
|
602
649
|
containerName: string,
|
|
603
650
|
request:
|
|
@@ -606,20 +653,13 @@ function appleExecArgs(
|
|
|
606
653
|
| TaskEnvironmentSessionRequest
|
|
607
654
|
| TaskEnvironmentDetachedSessionRequest,
|
|
608
655
|
mode: "interactive" | "detached",
|
|
609
|
-
): string[] {
|
|
656
|
+
): { args: string[]; env: NodeJS.ProcessEnv | undefined } {
|
|
610
657
|
const args = ["exec", mode === "detached" ? "-d" : "-i"];
|
|
611
658
|
if (request.cwd) args.push("--workdir", request.cwd);
|
|
612
659
|
if (request.user) args.push("--user", request.user);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
for (const [name, value] of Object.entries(request.env ?? {}).sort(
|
|
617
|
-
([left], [right]) => left.localeCompare(right),
|
|
618
|
-
)) {
|
|
619
|
-
args.push("--env", `${name}=${value}`);
|
|
620
|
-
}
|
|
621
|
-
args.push(containerName, ...request.argv);
|
|
622
|
-
return args;
|
|
660
|
+
const execEnv = cliExecEnv(request);
|
|
661
|
+
args.push(...execEnv.args, containerName, ...request.argv);
|
|
662
|
+
return { args, env: execEnv.env };
|
|
623
663
|
}
|
|
624
664
|
|
|
625
665
|
export function appleCliRunner(
|
|
@@ -633,6 +673,7 @@ function defaultRunner(containerCliPath: () => string): AppleCliRunner {
|
|
|
633
673
|
new Promise((resolve) => {
|
|
634
674
|
const child = nodeSpawn(containerCliPath(), [...args], {
|
|
635
675
|
stdio: ["ignore", "pipe", "pipe"],
|
|
676
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
636
677
|
});
|
|
637
678
|
const stdout: Buffer[] = [];
|
|
638
679
|
const stderr: Buffer[] = [];
|