@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Refresh ADR-153 project env in this process, then replace it with the task
|
|
3
|
+
# command. The mounted production name is /usr/local/bin/uai-env-exec.
|
|
4
|
+
|
|
5
|
+
if [ "$#" -eq 0 ]; then
|
|
6
|
+
printf '%s\n' 'usage: uai-env-exec COMMAND [ARG ...]' >&2
|
|
7
|
+
exit 64
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
if [ "${UAI_ENV_HOOK-0}" = "1" ]; then
|
|
11
|
+
__uai_hook=/usr/local/share/uai/env-hook.sh
|
|
12
|
+
if [ "${UAI_ENV_HOOK_TESTING-}" = "1" ] &&
|
|
13
|
+
[ -n "${UAI_ENV_TEST_HOOK_PATH-}" ]; then
|
|
14
|
+
__uai_hook=$UAI_ENV_TEST_HOOK_PATH
|
|
15
|
+
fi
|
|
16
|
+
if [ ! -r "$__uai_hook" ]; then
|
|
17
|
+
printf '%s\n' 'uai-env-exec: project environment hook is unavailable' >&2
|
|
18
|
+
exit 127
|
|
19
|
+
fi
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
. "$__uai_hook"
|
|
22
|
+
__uai_env_sync
|
|
23
|
+
unset __uai_hook
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
exec -- "$@"
|
package/src/cli.ts
CHANGED
|
@@ -53,13 +53,14 @@ import {
|
|
|
53
53
|
import {
|
|
54
54
|
abortManagedRuntimeInstall,
|
|
55
55
|
acknowledgeManagedRuntimeActivation,
|
|
56
|
+
acknowledgeManagedRuntimeActivationWithCapabilityFloor,
|
|
56
57
|
beginManagedRuntimeInstall,
|
|
57
58
|
completeManagedRuntimeInstall,
|
|
58
59
|
installManagedRuntimeArchive,
|
|
59
60
|
managedRuntimeInstallStartupState,
|
|
60
61
|
managedRuntimePaths,
|
|
61
62
|
nativeReleasePlatform,
|
|
62
|
-
|
|
63
|
+
reconcileManagedRuntimeActivationWithCapabilityFloor,
|
|
63
64
|
reconcileManagedRuntimeInstallLifecycle,
|
|
64
65
|
reconcileManagedRuntimeUninstall,
|
|
65
66
|
removeManagedRuntime,
|
|
@@ -76,6 +77,7 @@ import {
|
|
|
76
77
|
type ManagedServiceManagerState,
|
|
77
78
|
type ReconcileManagedRuntimeInstallLifecycleOptions,
|
|
78
79
|
} from "../lib/managed-runtime";
|
|
80
|
+
import { PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY } from "../lib/project-env-feature";
|
|
79
81
|
import { teardownAppleRuntimeStateForUninstall } from "../lib/apple-uninstall-teardown";
|
|
80
82
|
import {
|
|
81
83
|
readRuntimeProviderState,
|
|
@@ -269,7 +271,14 @@ async function cmdRun(rest: string[] = []): Promise<void> {
|
|
|
269
271
|
) {
|
|
270
272
|
const activationWasPending = existsSync(managedPaths.activationPending);
|
|
271
273
|
try {
|
|
272
|
-
|
|
274
|
+
const acknowledged = activationWasPending
|
|
275
|
+
? await acknowledgeManagedRuntimeActivationWithCapabilityFloor(
|
|
276
|
+
runningVersion,
|
|
277
|
+
PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY,
|
|
278
|
+
host.completeProjectEnvBindingsAfterManagedRuntimeFloor,
|
|
279
|
+
)
|
|
280
|
+
: await acknowledgeManagedRuntimeActivation(runningVersion);
|
|
281
|
+
if (!acknowledged) {
|
|
273
282
|
// Selection changed while the service module was loading. Its task
|
|
274
283
|
// admission remains closed by the marker; drain and let the supervisor
|
|
275
284
|
// start the newly selected stable command.
|
|
@@ -277,6 +286,7 @@ async function cmdRun(rest: string[] = []): Promise<void> {
|
|
|
277
286
|
return;
|
|
278
287
|
}
|
|
279
288
|
if (activationWasPending) {
|
|
289
|
+
host.advertiseProjectEnvBindingsAfterManagedRuntimeActivation();
|
|
280
290
|
console.log(
|
|
281
291
|
`[host-agent] managed runtime ${runningVersion} activated (pending activation acknowledged)`,
|
|
282
292
|
);
|
|
@@ -295,7 +305,20 @@ async function cmdRun(rest: string[] = []): Promise<void> {
|
|
|
295
305
|
new ManagedActivationWatcher({
|
|
296
306
|
restartRequired: async () => {
|
|
297
307
|
if (localManagedUpdateRunning) return false;
|
|
298
|
-
|
|
308
|
+
let capabilityCompleted = false;
|
|
309
|
+
const restartRequired =
|
|
310
|
+
await reconcileManagedRuntimeActivationWithCapabilityFloor(
|
|
311
|
+
runningVersion,
|
|
312
|
+
PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY,
|
|
313
|
+
() => {
|
|
314
|
+
host.completeProjectEnvBindingsAfterManagedRuntimeFloor();
|
|
315
|
+
capabilityCompleted = true;
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
if (capabilityCompleted) {
|
|
319
|
+
host.advertiseProjectEnvBindingsAfterManagedRuntimeActivation();
|
|
320
|
+
}
|
|
321
|
+
return restartRequired;
|
|
299
322
|
},
|
|
300
323
|
isIdle: host.isIdleForManagedHostUpdate,
|
|
301
324
|
onReady: host.requestManagedHostRestart,
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { readAttachment, writeAttachment } from "../lib/attachments";
|
|
32
32
|
import { readFileSync } from "node:fs";
|
|
33
33
|
import { resolve } from "node:path";
|
|
34
|
-
import { taskProjectWorktree } from "../lib/env";
|
|
34
|
+
import { env, taskProjectWorktree } from "../lib/env";
|
|
35
35
|
import { getDecryptedForProject } from "../lib/host-env";
|
|
36
36
|
import {
|
|
37
37
|
envSchemaNote,
|
|
@@ -46,20 +46,39 @@ import { buildTaskDiff } from "../lib/task-diff";
|
|
|
46
46
|
import {
|
|
47
47
|
deleteHostTask,
|
|
48
48
|
getHostTask,
|
|
49
|
+
hostTaskEnvMode,
|
|
49
50
|
recordHostEvent,
|
|
50
51
|
recordTaskDown,
|
|
51
52
|
recordTaskEnvironmentPrepared,
|
|
52
53
|
recordTaskError,
|
|
53
54
|
recordTaskOwner,
|
|
55
|
+
recordTaskProjectEnvSpec,
|
|
54
56
|
recordTaskStarting,
|
|
55
57
|
recordTaskUpResult,
|
|
58
|
+
taskProjectEnvSpec,
|
|
56
59
|
purgeHostTaskState,
|
|
57
60
|
upsertHostTask,
|
|
58
61
|
} from "../lib/runtime-state";
|
|
59
62
|
import {
|
|
60
63
|
clearTaskGatewayAcl,
|
|
61
64
|
clearTaskGatewayAclStrict,
|
|
65
|
+
waitForMcpGatewayReady,
|
|
62
66
|
} from "../lib/mcp-gateway";
|
|
67
|
+
import {
|
|
68
|
+
recoverTaskGatewayTask,
|
|
69
|
+
registerTaskGatewayTask,
|
|
70
|
+
reactivateTaskGatewayTask,
|
|
71
|
+
removeTaskGatewayTask,
|
|
72
|
+
suspendTaskGatewayTask,
|
|
73
|
+
taskGatewayForwarderSecretInput,
|
|
74
|
+
} from "../lib/task-gateway-state";
|
|
75
|
+
import { captureTaskEnvSchemas, forgetTaskEnvSchemaObservations } from "../lib/project-env-schema-capture";
|
|
76
|
+
import { forgetTaskEnvSnapshot, taskEnvResolver, taskEnvWithheldNoteNow } from "../lib/task-env-source";
|
|
77
|
+
import { activeSecretsProxy } from "../lib/secrets-proxy/host";
|
|
78
|
+
import { secretsProxyEnabled } from "../lib/secrets-proxy/runtime";
|
|
79
|
+
import { forgetTaskProxyReadiness } from "../lib/secrets-proxy/readiness";
|
|
80
|
+
import { admitFreshTaskView } from "../lib/secrets-proxy/sidecar-view";
|
|
81
|
+
import { invalidateTaskWrites, reconcileTaskProxyState } from "../lib/secrets-proxy/task-trust";
|
|
63
82
|
import { parsePreviewPortRuntimes } from "../lib/preview-ports";
|
|
64
83
|
import { dockerCli } from "../lib/docker-exec";
|
|
65
84
|
import {
|
|
@@ -186,6 +205,9 @@ export type {
|
|
|
186
205
|
} from "../lib/preview-ports";
|
|
187
206
|
import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
|
|
188
207
|
import { awaitSidecarReady, sidecarMode } from "../lib/sidecar";
|
|
208
|
+
import { guardSidecarMachine } from "../lib/sidecar-guard";
|
|
209
|
+
import { projectEnvBindingCapability } from "../lib/project-env-feature";
|
|
210
|
+
import { awaitProjectEnvReplicaReady } from "../lib/project-env-replica";
|
|
189
211
|
|
|
190
212
|
export const hostEvents: HostEventStream = {
|
|
191
213
|
subscribe(handler) {
|
|
@@ -297,6 +319,62 @@ export const hostCommands: HostCommands = {
|
|
|
297
319
|
if (readyAdmissionFailure) return readyAdmissionFailure;
|
|
298
320
|
const readyRuntimeFailure = runtimeUnavailable();
|
|
299
321
|
if (readyRuntimeFailure) return readyRuntimeFailure;
|
|
322
|
+
try {
|
|
323
|
+
await waitForMcpGatewayReady();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.warn(
|
|
326
|
+
`[task-gateway] refusing task ${input.task.id} before provisioning: ${
|
|
327
|
+
error instanceof Error ? error.message : String(error)
|
|
328
|
+
}`,
|
|
329
|
+
);
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
code: HostErrorCode.HostUnavailable,
|
|
333
|
+
message:
|
|
334
|
+
"The task environment gateway is not ready on this host. Retry the task; if it persists, check the host service log.",
|
|
335
|
+
retryable: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// A task machine's own guards first: its runner's data directory root's
|
|
339
|
+
// alone, and the metadata service closed to the task's user. Without
|
|
340
|
+
// them the task does not start: it could read the sidecar's host token.
|
|
341
|
+
if (sidecarMode()) {
|
|
342
|
+
const guard = guardSidecarMachine({ dataDir: env.dataDir });
|
|
343
|
+
if (!guard.ok) {
|
|
344
|
+
console.warn(`[sidecar] refusing task ${input.task.id}: ${guard.reason}`);
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
code: HostErrorCode.Internal,
|
|
348
|
+
message: `This task machine is not safe to run the task on: ${guard.reason}. Close the task and start it again on a new machine.`,
|
|
349
|
+
retryable: false,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// A bindings-capable task sidecar must activate and commit its exact
|
|
354
|
+
// replica before any task state, /env generation or process can exist.
|
|
355
|
+
// Otherwise the source's intentional pre-activation legacy fallback
|
|
356
|
+
// could expose stored plaintext during the first reconciliation.
|
|
357
|
+
if (sidecarMode()) {
|
|
358
|
+
try {
|
|
359
|
+
await awaitSidecarReady();
|
|
360
|
+
if (projectEnvBindingCapability() !== null) {
|
|
361
|
+
await awaitProjectEnvReplicaReady();
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.warn(
|
|
365
|
+
`[task-env] refusing task ${input.task.id} before provisioning: ${
|
|
366
|
+
error instanceof Error ? error.message : String(error)
|
|
367
|
+
}`,
|
|
368
|
+
);
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
code: HostErrorCode.HostUnavailable,
|
|
372
|
+
message:
|
|
373
|
+
"This task machine is still synchronizing its environment. Retry shortly.",
|
|
374
|
+
retryable: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
300
378
|
// ADR-118 follow-up: each apple-container task is its own VM, and each
|
|
301
379
|
// VM pins host vnodes for its life. Refuse to start ANOTHER one when the
|
|
302
380
|
// table is nearly full — a clear, retryable refusal now beats ENFILE
|
|
@@ -327,6 +405,19 @@ export const hostCommands: HostCommands = {
|
|
|
327
405
|
// that lost-response retry as the successful operation it already was:
|
|
328
406
|
// every cloud caller records a non-ok taskUp as an errored task.
|
|
329
407
|
const existingTask = getHostTask(input.task.id);
|
|
408
|
+
const incomingEnvSpec = taskProjectEnvSpec(input);
|
|
409
|
+
let existingGatewayReady = true;
|
|
410
|
+
if (existingTask && hostTaskEnvMode(existingTask) === "loader") {
|
|
411
|
+
// A lost-response retry is also the repair path after a host process
|
|
412
|
+
// restart: reattach the live resolver before any task process can be
|
|
413
|
+
// treated as reusable. Missing/corrupt durable identity forces normal
|
|
414
|
+
// recreation, which mints a generation and restarts its forwarder.
|
|
415
|
+
recordTaskProjectEnvSpec(input.task.id, input);
|
|
416
|
+
existingGatewayReady =
|
|
417
|
+
recoverTaskGatewayTask(input.task.id, {
|
|
418
|
+
resolveEnv: taskEnvResolver(input.task.id, incomingEnvSpec),
|
|
419
|
+
}) !== null;
|
|
420
|
+
}
|
|
330
421
|
let runtimeQuarantinedForRecreate = false;
|
|
331
422
|
let channelClosedForRecreate = false;
|
|
332
423
|
if (
|
|
@@ -370,6 +461,7 @@ export const hostCommands: HostCommands = {
|
|
|
370
461
|
if (environment === null) {
|
|
371
462
|
// The persisted locator no longer reconstructs (e.g. an engine
|
|
372
463
|
// identity change) — recreate through the normal path.
|
|
464
|
+
suspendTaskGatewayTask(input.task.id);
|
|
373
465
|
await orchestrator.closeChannel(input.task.id);
|
|
374
466
|
// Session drain can outlive the generation too: a superseded pass
|
|
375
467
|
// must not reach recordTaskStarting/provisioning below.
|
|
@@ -412,6 +504,7 @@ export const hostCommands: HostCommands = {
|
|
|
412
504
|
? input.projects
|
|
413
505
|
: []) {
|
|
414
506
|
const gitHealth = await environment.exec({
|
|
507
|
+
projectEnvProfile: "maintenance",
|
|
415
508
|
argv: [
|
|
416
509
|
"/usr/bin/env",
|
|
417
510
|
"-i",
|
|
@@ -450,7 +543,11 @@ export const hostCommands: HostCommands = {
|
|
|
450
543
|
break;
|
|
451
544
|
}
|
|
452
545
|
}
|
|
453
|
-
if (
|
|
546
|
+
if (
|
|
547
|
+
verdict.kind === "holds" &&
|
|
548
|
+
projectsHealthy &&
|
|
549
|
+
existingGatewayReady
|
|
550
|
+
) {
|
|
454
551
|
if (fastStale()) return staleRetry;
|
|
455
552
|
orchestrator.allowChannel(input.task.id);
|
|
456
553
|
return {
|
|
@@ -462,12 +559,14 @@ export const hostCommands: HostCommands = {
|
|
|
462
559
|
previewPorts: parsePreviewPortRuntimes(
|
|
463
560
|
existingTask.previewPorts,
|
|
464
561
|
),
|
|
562
|
+
envMode: hostTaskEnvMode(existingTask),
|
|
465
563
|
},
|
|
466
564
|
};
|
|
467
565
|
}
|
|
468
566
|
// Violated contract or broken project repo: drain, stop, and
|
|
469
567
|
// prove before task-up recreates the same stable name.
|
|
470
568
|
if (fastStale()) return staleRetry;
|
|
569
|
+
suspendTaskGatewayTask(input.task.id);
|
|
471
570
|
await orchestrator.closeChannel(input.task.id);
|
|
472
571
|
channelClosedForRecreate = true;
|
|
473
572
|
if (fastStale()) return staleRetry;
|
|
@@ -494,6 +593,7 @@ export const hostCommands: HostCommands = {
|
|
|
494
593
|
// recreate — and the rollback state must say stopped for the
|
|
495
594
|
// same reason as above.
|
|
496
595
|
if (fastStale()) return staleRetry;
|
|
596
|
+
suspendTaskGatewayTask(input.task.id);
|
|
497
597
|
await orchestrator.closeChannel(input.task.id);
|
|
498
598
|
// Fence AFTER the drain as well: provisioning must never start
|
|
499
599
|
// under a generation this pass no longer owns.
|
|
@@ -517,7 +617,7 @@ export const hostCommands: HostCommands = {
|
|
|
517
617
|
);
|
|
518
618
|
if (environment) {
|
|
519
619
|
const machineStatus = await environment.status();
|
|
520
|
-
if (machineStatus.state === "running") {
|
|
620
|
+
if (machineStatus.state === "running" && existingGatewayReady) {
|
|
521
621
|
orchestrator.allowChannel(input.task.id);
|
|
522
622
|
return {
|
|
523
623
|
ok: true,
|
|
@@ -525,6 +625,7 @@ export const hostCommands: HostCommands = {
|
|
|
525
625
|
composeProject: existingTask.composeProject ?? "",
|
|
526
626
|
worktreePath: existingTask.worktreePath ?? "",
|
|
527
627
|
codeServerPort: existingTask.codeServerPort ?? undefined,
|
|
628
|
+
envMode: hostTaskEnvMode(existingTask),
|
|
528
629
|
},
|
|
529
630
|
};
|
|
530
631
|
}
|
|
@@ -597,12 +698,14 @@ export const hostCommands: HostCommands = {
|
|
|
597
698
|
!runtimeContract.restarting &&
|
|
598
699
|
!runtimeContract.paused &&
|
|
599
700
|
runtimeContract.sharedMount === "read-only" &&
|
|
600
|
-
runtimeContract.taskEnvironment === "current"
|
|
701
|
+
runtimeContract.taskEnvironment === "current" &&
|
|
702
|
+
existingGatewayReady;
|
|
601
703
|
if (!reusable) {
|
|
602
704
|
// Drain/tombstone before touching the exact container. task-up's
|
|
603
705
|
// pre-materialization cleanup then removes this stopped immutable
|
|
604
706
|
// generation, so Compose must create the current RO/private-prefix
|
|
605
707
|
// contract rather than `docker start`ing it again.
|
|
708
|
+
suspendTaskGatewayTask(input.task.id);
|
|
606
709
|
await orchestrator.closeChannel(input.task.id);
|
|
607
710
|
channelClosedForRecreate = true;
|
|
608
711
|
const quarantined = await quarantineTaskRuntimeContainer({
|
|
@@ -740,6 +843,7 @@ export const hostCommands: HostCommands = {
|
|
|
740
843
|
previewPorts: parsePreviewPortRuntimes(
|
|
741
844
|
existingTask.previewPorts,
|
|
742
845
|
),
|
|
846
|
+
envMode: hostTaskEnvMode(existingTask),
|
|
743
847
|
},
|
|
744
848
|
};
|
|
745
849
|
}
|
|
@@ -749,6 +853,7 @@ export const hostCommands: HostCommands = {
|
|
|
749
853
|
// Drain and invalidate any stale in-memory channel before taskUp
|
|
750
854
|
// recreates the same stable compose/container names and repairs Git.
|
|
751
855
|
if (!channelClosedForRecreate) {
|
|
856
|
+
suspendTaskGatewayTask(input.task.id);
|
|
752
857
|
await orchestrator.closeChannel(input.task.id);
|
|
753
858
|
}
|
|
754
859
|
}
|
|
@@ -763,6 +868,8 @@ export const hostCommands: HostCommands = {
|
|
|
763
868
|
endedAt: existingTask.endedAt,
|
|
764
869
|
environmentProvider: existingTask.environmentProvider,
|
|
765
870
|
environmentLocator: existingTask.environmentLocator,
|
|
871
|
+
envMode: existingTask.envMode,
|
|
872
|
+
projectEnvSpec: existingTask.projectEnvSpec,
|
|
766
873
|
}
|
|
767
874
|
: {
|
|
768
875
|
statusMirror: existingTask.statusMirror,
|
|
@@ -771,6 +878,8 @@ export const hostCommands: HostCommands = {
|
|
|
771
878
|
endedAt: existingTask.endedAt,
|
|
772
879
|
environmentProvider: existingTask.environmentProvider,
|
|
773
880
|
environmentLocator: existingTask.environmentLocator,
|
|
881
|
+
envMode: existingTask.envMode,
|
|
882
|
+
projectEnvSpec: existingTask.projectEnvSpec,
|
|
774
883
|
}
|
|
775
884
|
: {
|
|
776
885
|
statusMirror: input.task.status,
|
|
@@ -778,6 +887,11 @@ export const hostCommands: HostCommands = {
|
|
|
778
887
|
startedAt: null,
|
|
779
888
|
endedAt: null,
|
|
780
889
|
};
|
|
890
|
+
// ADR-153 machines: a task with no row has never run on this machine,
|
|
891
|
+
// so everything it runs comes through its hosts view. Recorded before
|
|
892
|
+
// the row, and so before the task's first process; a task with a row
|
|
893
|
+
// (a start driven again, a start after a stop) stands as recorded.
|
|
894
|
+
if (sidecarMode() && !existingTask) admitFreshTaskView(input.task.id);
|
|
781
895
|
recordTaskStarting(input.task.id);
|
|
782
896
|
// Cross-process managed maintenance and task admission use a two-sided
|
|
783
897
|
// handshake: update/uninstall writes its durable marker before checking
|
|
@@ -806,6 +920,7 @@ export const hostCommands: HostCommands = {
|
|
|
806
920
|
.sort((a, b) => a.position - b.position)
|
|
807
921
|
.map((p) => p.id),
|
|
808
922
|
);
|
|
923
|
+
recordTaskProjectEnvSpec(input.task.id, input);
|
|
809
924
|
// ADR-048: persist the cloud-issued per-task cli secret (host-private) so the
|
|
810
925
|
// `uai` CLI's per-agent tokens can be minted now + after a host restart.
|
|
811
926
|
if (input.task.cliSecret) {
|
|
@@ -813,6 +928,7 @@ export const hostCommands: HostCommands = {
|
|
|
813
928
|
}
|
|
814
929
|
recordHostEvent(input.task.id, "task.created");
|
|
815
930
|
let environmentLocator: TaskEnvironmentLocator | undefined;
|
|
931
|
+
let registeredTaskGatewayGeneration: string | null = null;
|
|
816
932
|
const result = await wrapAgent(ctx, "taskUp", async () => {
|
|
817
933
|
// Self-heal the standard base image before task-up needs it. The
|
|
818
934
|
// boot-time build is best-effort + one-shot: a host that started before
|
|
@@ -848,9 +964,6 @@ export const hostCommands: HostCommands = {
|
|
|
848
964
|
"GitHub is connected on this host, but Uai could not prepare its credential for Git. Retry the task; if it persists, reconnect GitHub on this host.",
|
|
849
965
|
);
|
|
850
966
|
}
|
|
851
|
-
// ADR-133 sidecar: nothing to clone or spawn with until the lease
|
|
852
|
-
// has landed and the store is reconciled under it.
|
|
853
|
-
await awaitSidecarReady();
|
|
854
967
|
if (sidecarMode()) {
|
|
855
968
|
// ADR-133: project env lives under /uai/projects/<id>, readable by
|
|
856
969
|
// the lease only for parameters tagged with this org, and only the
|
|
@@ -870,6 +983,24 @@ export const hostCommands: HostCommands = {
|
|
|
870
983
|
);
|
|
871
984
|
}
|
|
872
985
|
}
|
|
986
|
+
// Mint the authenticated task generation before its environment
|
|
987
|
+
// starts. The declarations are pinned; values remain live DB reads.
|
|
988
|
+
const gatewayRegistration = registerTaskGatewayTask(input.task.id, {
|
|
989
|
+
resolveEnv: taskEnvResolver(input.task.id, incomingEnvSpec),
|
|
990
|
+
});
|
|
991
|
+
registeredTaskGatewayGeneration = gatewayRegistration.generation;
|
|
992
|
+
const gatewaySecret = taskGatewayForwarderSecretInput(
|
|
993
|
+
input.task.id,
|
|
994
|
+
gatewayRegistration.generation,
|
|
995
|
+
);
|
|
996
|
+
if (gatewaySecret === null) {
|
|
997
|
+
removeTaskGatewayTask(
|
|
998
|
+
input.task.id,
|
|
999
|
+
gatewayRegistration.generation,
|
|
1000
|
+
);
|
|
1001
|
+
registeredTaskGatewayGeneration = null;
|
|
1002
|
+
throw new Error("could not export the task gateway generation secret");
|
|
1003
|
+
}
|
|
873
1004
|
// Launch screen: the first stage boundary. A machine-backed task
|
|
874
1005
|
// spends the next minute launching an EC2 instance; a container
|
|
875
1006
|
// task goes straight to preparing its workspace.
|
|
@@ -878,22 +1009,68 @@ export const hostCommands: HostCommands = {
|
|
|
878
1009
|
machineTasksEnabled() ? "machine" : "workspace",
|
|
879
1010
|
);
|
|
880
1011
|
try {
|
|
1012
|
+
const taskGateway = {
|
|
1013
|
+
generation: gatewayRegistration.generation,
|
|
1014
|
+
secret: gatewaySecret,
|
|
1015
|
+
};
|
|
881
1016
|
const provisioned = gitCredential
|
|
882
1017
|
? await gitCredential.run(() =>
|
|
883
1018
|
provisionTaskEnvironment(
|
|
884
1019
|
input,
|
|
885
|
-
{
|
|
1020
|
+
{
|
|
1021
|
+
githubCredentialSocket: gitCredential.socketPath,
|
|
1022
|
+
taskGateway,
|
|
1023
|
+
},
|
|
886
1024
|
async (locator) => {
|
|
887
|
-
recordTaskEnvironmentPrepared(
|
|
1025
|
+
recordTaskEnvironmentPrepared(
|
|
1026
|
+
input.task.id,
|
|
1027
|
+
locator,
|
|
1028
|
+
"loader",
|
|
1029
|
+
);
|
|
888
1030
|
},
|
|
889
1031
|
),
|
|
890
1032
|
)
|
|
891
|
-
: await provisionTaskEnvironment(
|
|
892
|
-
|
|
893
|
-
|
|
1033
|
+
: await provisionTaskEnvironment(
|
|
1034
|
+
input,
|
|
1035
|
+
{ taskGateway },
|
|
1036
|
+
async (locator) => {
|
|
1037
|
+
recordTaskEnvironmentPrepared(
|
|
1038
|
+
input.task.id,
|
|
1039
|
+
locator,
|
|
1040
|
+
"loader",
|
|
1041
|
+
);
|
|
1042
|
+
},
|
|
1043
|
+
);
|
|
894
1044
|
environmentLocator = provisioned.handle.descriptor.locator;
|
|
895
|
-
|
|
1045
|
+
// ADR-153 §4: the task's own schema observations, read from the
|
|
1046
|
+
// bases task-up recorded before its container started (a machine
|
|
1047
|
+
// captures its own). Stored once; a resume reuses them.
|
|
1048
|
+
const envSchemas = isEnvironmentTransportProvider(getHostTask(input.task.id)?.environmentProvider)
|
|
1049
|
+
? undefined
|
|
1050
|
+
: captureTaskEnvSchemas(
|
|
1051
|
+
input.task.id,
|
|
1052
|
+
incomingEnvSpec.projects.map((project) => project.projectId),
|
|
1053
|
+
);
|
|
1054
|
+
// ADR-153: before any consumer is let in, bring the task to now:
|
|
1055
|
+
// a CA successor minted while it started, and the domains its
|
|
1056
|
+
// policy has now rather than those task-up was handed. A failure
|
|
1057
|
+
// leaves those keys withheld, and the trust watcher retries.
|
|
1058
|
+
const secretsProxy = activeSecretsProxy();
|
|
1059
|
+
if (secretsProxy !== null) {
|
|
1060
|
+
await reconcileTaskProxyState(provisioned.handle, secretsProxy.runtime).catch((error: unknown) => {
|
|
1061
|
+
console.warn(
|
|
1062
|
+
`[secrets-proxy] task ${input.task.id}: could not bring the task's trust and pins up to date: ${
|
|
1063
|
+
error instanceof Error ? error.message : String(error)
|
|
1064
|
+
}`,
|
|
1065
|
+
);
|
|
1066
|
+
secretsProxy.runtime.requestTrustDelivery();
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return envSchemas === undefined || envSchemas.length === 0
|
|
1070
|
+
? { ...provisioned.result, envMode: "loader" as const }
|
|
1071
|
+
: { ...provisioned.result, envMode: "loader" as const, envSchemas };
|
|
896
1072
|
} finally {
|
|
1073
|
+
gatewaySecret.fill(0);
|
|
897
1074
|
await gitCredential?.close().catch((error: unknown) => {
|
|
898
1075
|
// Cleanup must not replace the actual task-up result/error. The
|
|
899
1076
|
// credential is short-lived and close() also removes its private
|
|
@@ -904,13 +1081,58 @@ export const hostCommands: HostCommands = {
|
|
|
904
1081
|
});
|
|
905
1082
|
}
|
|
906
1083
|
});
|
|
1084
|
+
if (!result.ok && registeredTaskGatewayGeneration !== null) {
|
|
1085
|
+
forgetTaskEnvSnapshot(input.task.id);
|
|
1086
|
+
try {
|
|
1087
|
+
removeTaskGatewayTask(
|
|
1088
|
+
input.task.id,
|
|
1089
|
+
registeredTaskGatewayGeneration,
|
|
1090
|
+
);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
// The in-process authority is revoked before durable deletion is
|
|
1093
|
+
// attempted. Preserve the task-up error while retaining a cleanup
|
|
1094
|
+
// diagnostic; a leftover file loads inactive after restart.
|
|
1095
|
+
console.warn(
|
|
1096
|
+
`[task-gateway] task ${input.task.id}: failed to remove the rejected generation: ${
|
|
1097
|
+
error instanceof Error ? error.message : String(error)
|
|
1098
|
+
}`,
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
907
1102
|
if (result.ok) {
|
|
908
1103
|
orchestrator.allowChannel(input.task.id);
|
|
909
1104
|
if (environmentLocator === undefined) {
|
|
910
1105
|
throw new Error("task environment provision returned no locator");
|
|
911
1106
|
}
|
|
912
|
-
recordTaskUpResult(
|
|
1107
|
+
recordTaskUpResult(
|
|
1108
|
+
input.task.id,
|
|
1109
|
+
result.value,
|
|
1110
|
+
environmentLocator,
|
|
1111
|
+
"loader",
|
|
1112
|
+
);
|
|
913
1113
|
recordHostEvent(input.task.id, "task.started");
|
|
1114
|
+
// ADR-153: with the proxy off, every start says so, since the task's
|
|
1115
|
+
// proxied keys are withheld rather than delivered.
|
|
1116
|
+
if (activeSecretsProxy() === null) {
|
|
1117
|
+
orchestrator.emitSystemNote(
|
|
1118
|
+
input.task.id,
|
|
1119
|
+
secretsProxyEnabled()
|
|
1120
|
+
? "The secrets proxy could not start on this host, so this task's proxied env vars are withheld. See the host log."
|
|
1121
|
+
: "The secrets proxy is off on this host (UAI_SECRETS_PROXY=off), so this task's proxied env vars are withheld.",
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
// ADR-153 §4: one note naming the keys this task does not get, and
|
|
1125
|
+
// why, so nobody hunts for a value Uai withholds on purpose.
|
|
1126
|
+
try {
|
|
1127
|
+
const withheld = taskEnvWithheldNoteNow(input.task.id, incomingEnvSpec);
|
|
1128
|
+
if (withheld !== null) orchestrator.emitSystemNote(input.task.id, withheld);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.warn(
|
|
1131
|
+
`[project-env] task ${input.task.id}: withheld-keys note failed: ${
|
|
1132
|
+
error instanceof Error ? error.message : String(error)
|
|
1133
|
+
}`,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
914
1136
|
// Degraded start (uai-init failed twice): the task is up, but say so
|
|
915
1137
|
// in the feed — a silent half-start reads as a broken product.
|
|
916
1138
|
if (result.value.initWarning) {
|
|
@@ -1017,6 +1239,10 @@ export const hostCommands: HostCommands = {
|
|
|
1017
1239
|
}
|
|
1018
1240
|
// Tombstone + close before container destruction. A concurrent cloud
|
|
1019
1241
|
// ensure cannot reopen the channel while teardown is awaiting Docker.
|
|
1242
|
+
const suspendedTaskGateway = suspendTaskGatewayTask(input.taskId);
|
|
1243
|
+
// ADR-153: the suspended generation is no longer served; end the
|
|
1244
|
+
// proxy connections it already had, too.
|
|
1245
|
+
activeSecretsProxy()?.runtime.stopTask(input.taskId);
|
|
1020
1246
|
await orchestrator.closeChannel(input.taskId);
|
|
1021
1247
|
if (orphanGc) {
|
|
1022
1248
|
const inSlotRuntimeFailure = runtimeUnavailable();
|
|
@@ -1067,6 +1293,14 @@ export const hostCommands: HostCommands = {
|
|
|
1067
1293
|
clearTaskGatewayAclStrict(input.taskId);
|
|
1068
1294
|
removeTaskCliSecretStrict(input.taskId);
|
|
1069
1295
|
removeTaskIdentityStrict(input.taskId);
|
|
1296
|
+
removeTaskGatewayTask(input.taskId);
|
|
1297
|
+
// No longer served: the proxy forgets its placeholders, and no
|
|
1298
|
+
// write still queued for the task may land or record.
|
|
1299
|
+
activeSecretsProxy()?.runtime.dropTask(input.taskId);
|
|
1300
|
+
invalidateTaskWrites(input.taskId);
|
|
1301
|
+
forgetTaskProxyReadiness(input.taskId);
|
|
1302
|
+
forgetTaskEnvSnapshot(input.taskId);
|
|
1303
|
+
forgetTaskEnvSchemaObservations(input.taskId);
|
|
1070
1304
|
clearRefresh(input.taskId);
|
|
1071
1305
|
// Last, atomically. Do not record taskDown/task.ended first: both
|
|
1072
1306
|
// would recreate the very inventory evidence being pruned.
|
|
@@ -1081,10 +1315,25 @@ export const hostCommands: HostCommands = {
|
|
|
1081
1315
|
return failFromUnknown(error);
|
|
1082
1316
|
}
|
|
1083
1317
|
}
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1318
|
+
// The live generation was suspended before channel drain. Delete its
|
|
1319
|
+
// durable state only after destruction succeeds; on failure the task
|
|
1320
|
+
// is reopened below with the same on-disk MCP definitions.
|
|
1321
|
+
try {
|
|
1322
|
+
removeTaskGatewayTask(input.taskId);
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
// Destruction succeeded and the live authority was already revoked,
|
|
1325
|
+
// but keep the lifecycle retryable until the durable identity is
|
|
1326
|
+
// gone. Never reopen a channel onto the destroyed runtime.
|
|
1327
|
+
recordTaskError(input.taskId);
|
|
1328
|
+
return failFromUnknown(error);
|
|
1329
|
+
}
|
|
1330
|
+
// No longer served: the proxy forgets its placeholders, and no write
|
|
1331
|
+
// still queued for the task may land or record.
|
|
1332
|
+
activeSecretsProxy()?.runtime.dropTask(input.taskId);
|
|
1333
|
+
invalidateTaskWrites(input.taskId);
|
|
1334
|
+
forgetTaskProxyReadiness(input.taskId);
|
|
1335
|
+
forgetTaskEnvSnapshot(input.taskId);
|
|
1336
|
+
forgetTaskEnvSchemaObservations(input.taskId);
|
|
1088
1337
|
clearTaskGatewayAcl(input.taskId);
|
|
1089
1338
|
recordTaskDown(input.taskId, result.value.status);
|
|
1090
1339
|
recordHostEvent(input.taskId, "task.ended");
|
|
@@ -1092,7 +1341,12 @@ export const hostCommands: HostCommands = {
|
|
|
1092
1341
|
} else {
|
|
1093
1342
|
// Destruction failed; let a subsequent ensure restore the still-running
|
|
1094
1343
|
// task instead of leaving it permanently tombstoned.
|
|
1095
|
-
if (!orphanGc)
|
|
1344
|
+
if (!orphanGc) {
|
|
1345
|
+
if (suspendedTaskGateway) {
|
|
1346
|
+
reactivateTaskGatewayTask(suspendedTaskGateway);
|
|
1347
|
+
}
|
|
1348
|
+
orchestrator.allowChannel(input.taskId);
|
|
1349
|
+
}
|
|
1096
1350
|
}
|
|
1097
1351
|
return result;
|
|
1098
1352
|
});
|