@runuai/host 0.9.147 → 0.9.149
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/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +137 -13
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +17 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +125 -58
- package/lib/agents/types.ts +3 -3
- 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 +1681 -138
- 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/lib/ulid.ts +10 -1
- 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 +941 -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 +2239 -15
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-only release switch for ADR-153 project-env bindings.
|
|
3
|
+
*
|
|
4
|
+
* Capability advertisement and task admission move together: once a sidecar
|
|
5
|
+
* advertises the protocol, its task cannot expose a legacy env view before the
|
|
6
|
+
* replica's first complete reconciliation. Advertisement also waits for the
|
|
7
|
+
* durable managed-runtime floor, so no acknowledged generation can roll back
|
|
8
|
+
* to code that serves the old value-only store.
|
|
9
|
+
*/
|
|
10
|
+
import { PROJECT_ENV_BINDINGS_PROTOCOL_FEATURE } from "../src/protocol";
|
|
11
|
+
import { PROJECT_ENV_POLICY_EPOCHS } from "../src/project-env-policy";
|
|
12
|
+
import { MANAGED_RUNTIME_CAPABILITY_VERSION } from "./runtime-archive";
|
|
13
|
+
|
|
14
|
+
const PROJECT_ENV_BINDINGS_ENABLED = true;
|
|
15
|
+
export const PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY =
|
|
16
|
+
MANAGED_RUNTIME_CAPABILITY_VERSION;
|
|
17
|
+
|
|
18
|
+
let capabilityFloorClaim: Promise<void> | null = null;
|
|
19
|
+
let capabilityFloorClaimFailed = false;
|
|
20
|
+
let capabilityReady = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claim the irreversible rollback floor before the first capability frame.
|
|
24
|
+
* The rejected promise is deliberately retained: one process gets one claim
|
|
25
|
+
* attempt and stays fail-closed until restart if durable persistence fails.
|
|
26
|
+
*/
|
|
27
|
+
export async function initializeProjectEnvBindingCapability(
|
|
28
|
+
claimFloor: (minimumCapability: number) => Promise<void>,
|
|
29
|
+
): Promise<boolean> {
|
|
30
|
+
if (!PROJECT_ENV_BINDINGS_ENABLED) return false;
|
|
31
|
+
capabilityFloorClaim ??= Promise.resolve()
|
|
32
|
+
.then(() => claimFloor(PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY))
|
|
33
|
+
.then(() => {
|
|
34
|
+
capabilityReady = true;
|
|
35
|
+
})
|
|
36
|
+
.catch((error: unknown) => {
|
|
37
|
+
capabilityFloorClaimFailed = true;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
await capabilityFloorClaim;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Complete the in-process half of the first-upgrade transaction. The managed
|
|
46
|
+
* runtime calls this synchronously under its operation lock after floor 4 is
|
|
47
|
+
* durable and before it removes activationPending, so task admission cannot
|
|
48
|
+
* observe a value-only feature state between those two commits.
|
|
49
|
+
*/
|
|
50
|
+
export function completeProjectEnvBindingCapabilityAfterRuntimeActivation(): void {
|
|
51
|
+
if (!PROJECT_ENV_BINDINGS_ENABLED || capabilityReady) return;
|
|
52
|
+
if (capabilityFloorClaim === null || !capabilityFloorClaimFailed) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"project env capability has no failed activation claim to complete",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
capabilityFloorClaim = Promise.resolve();
|
|
58
|
+
capabilityFloorClaimFailed = false;
|
|
59
|
+
capabilityReady = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One nullable bundle makes the feature id and its required policy epochs an
|
|
63
|
+
* atomic advertisement, and is also TaskUp's release admission switch. */
|
|
64
|
+
export function projectEnvBindingCapability(): {
|
|
65
|
+
feature: string;
|
|
66
|
+
policyEpochs: number[];
|
|
67
|
+
} | null {
|
|
68
|
+
return PROJECT_ENV_BINDINGS_ENABLED && capabilityReady
|
|
69
|
+
? {
|
|
70
|
+
feature: PROJECT_ENV_BINDINGS_PROTOCOL_FEATURE,
|
|
71
|
+
policyEpochs: [...PROJECT_ENV_POLICY_EPOCHS],
|
|
72
|
+
}
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-153 project-env resolution and shell-document rendering.
|
|
3
|
+
*
|
|
4
|
+
* This module is the one value-bearing seam between the encrypted host store
|
|
5
|
+
* and a task's authenticated loopback gateway. It keeps one rendered snapshot
|
|
6
|
+
* in memory and invalidates it synchronously with project-value mutations, so
|
|
7
|
+
* a rotation or deletion is visible to the next command without a restart.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getDecryptedForProject,
|
|
14
|
+
isReservedTaskEnvName,
|
|
15
|
+
projectEnvRevision,
|
|
16
|
+
} from "./host-env";
|
|
17
|
+
import type { TaskEnvView } from "./project-env-task-policy";
|
|
18
|
+
import type { TaskProjectEnvSpec } from "./runtime-state";
|
|
19
|
+
import type {
|
|
20
|
+
TaskGatewayEnvResolver,
|
|
21
|
+
TaskGatewayEnvSnapshot,
|
|
22
|
+
} from "./task-gateway-state";
|
|
23
|
+
|
|
24
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25
|
+
|
|
26
|
+
export interface ResolvedTaskProjectEnv {
|
|
27
|
+
/** Only currently-delivered names. A deleted name disappears on refresh. */
|
|
28
|
+
readonly values: Readonly<Record<string, string>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Where a task's project values come from (ADR-153 §6, "Reconciling a
|
|
33
|
+
* runner"): one snapshot's view of the store; nothing at all, while the
|
|
34
|
+
* store is unreconciled or a read of it failed; or, only for a build whose
|
|
35
|
+
* store predates records, the stored values as they are. `none` never falls
|
|
36
|
+
* back to the stored values.
|
|
37
|
+
*/
|
|
38
|
+
export type TaskEnvSource =
|
|
39
|
+
| { kind: "view"; view: TaskEnvView }
|
|
40
|
+
| { kind: "none" }
|
|
41
|
+
| { kind: "stored" };
|
|
42
|
+
|
|
43
|
+
const STORED: TaskEnvSource = { kind: "stored" };
|
|
44
|
+
|
|
45
|
+
function compareAscii(left: string, right: string): number {
|
|
46
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function previewNameAllowed(name: string): boolean {
|
|
50
|
+
// UAI_EDITOR_THEME is a cloud-owned non-secret task literal consumed once
|
|
51
|
+
// while seeding the editor. Every other reserved name remains host-owned.
|
|
52
|
+
return name === "UAI_EDITOR_THEME" || !isReservedTaskEnvName(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve lead-project precedence before applying that row's declaration.
|
|
57
|
+
* With the task's view (ADR-153 §4), that view is the only source of project
|
|
58
|
+
* values: each delivered key's value as of its decision's head, from one
|
|
59
|
+
* snapshot of the store, never a separate read of the store here. The view
|
|
60
|
+
* already applied ownership and policy to the live records, so the names
|
|
61
|
+
* frozen into the task at launch do not filter it: a key added since reaches
|
|
62
|
+
* the next command, as a changed one does. With `none`, there are no project
|
|
63
|
+
* values at all.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveTaskProjectEnv(
|
|
66
|
+
spec: TaskProjectEnvSpec,
|
|
67
|
+
source: TaskEnvSource = STORED,
|
|
68
|
+
): ResolvedTaskProjectEnv {
|
|
69
|
+
const view = source.kind === "view" ? source.view : null;
|
|
70
|
+
const selected: Record<string, string> = Object.create(null) as Record<
|
|
71
|
+
string,
|
|
72
|
+
string
|
|
73
|
+
>;
|
|
74
|
+
const claimed = new Set<string>();
|
|
75
|
+
const projects = [...spec.projects].sort(
|
|
76
|
+
(left, right) =>
|
|
77
|
+
left.position - right.position ||
|
|
78
|
+
compareAscii(left.projectId, right.projectId),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (view !== null) {
|
|
82
|
+
for (const policy of view.policies) {
|
|
83
|
+
if (policy.kind !== "deliver") continue;
|
|
84
|
+
const value = Object.hasOwn(view.values, policy.key) ? view.values[policy.key] : undefined;
|
|
85
|
+
const project = projects.find((candidate) => candidate.projectId === policy.projectId);
|
|
86
|
+
if (
|
|
87
|
+
value === undefined ||
|
|
88
|
+
project === undefined ||
|
|
89
|
+
!ENV_NAME.test(policy.key) ||
|
|
90
|
+
isReservedTaskEnvName(policy.key) ||
|
|
91
|
+
value.includes("\0")
|
|
92
|
+
) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
selected[policy.key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const project of source.kind === "stored" ? projects : []) {
|
|
100
|
+
const stored = getDecryptedForProject(project.projectId);
|
|
101
|
+
const declared = new Set(project.declaredNames);
|
|
102
|
+
for (const [name, value] of Object.entries(stored)) {
|
|
103
|
+
if (
|
|
104
|
+
claimed.has(name) ||
|
|
105
|
+
!ENV_NAME.test(name) ||
|
|
106
|
+
isReservedTaskEnvName(name)
|
|
107
|
+
) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// The first stored row owns this key even when that project's schema
|
|
111
|
+
// does not declare it. Falling through would pair a later project's
|
|
112
|
+
// declaration with a different project's value, violating ADR-153's
|
|
113
|
+
// one-authority-unit rule.
|
|
114
|
+
claimed.add(name);
|
|
115
|
+
if (!declared.has(name)) continue;
|
|
116
|
+
if (typeof value !== "string" || value.includes("\0")) continue;
|
|
117
|
+
selected[name] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Preview/theme literals are task-scoped and intentionally win a collision
|
|
122
|
+
// with project env, matching the pre-loader Compose contract.
|
|
123
|
+
for (const [name, value] of Object.entries(spec.previewEnv)) {
|
|
124
|
+
if (
|
|
125
|
+
ENV_NAME.test(name) &&
|
|
126
|
+
previewNameAllowed(name) &&
|
|
127
|
+
!value.includes("\0")
|
|
128
|
+
) {
|
|
129
|
+
selected[name] = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { values: selected };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** POSIX single-quote syntax, valid in dash, bash and zsh. */
|
|
136
|
+
export function quoteTaskEnvValue(value: string): string {
|
|
137
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render the complete document validated by env-hook.sh. The digest covers
|
|
142
|
+
* the value-bearing prefix; its raw hex is repeated only in the terminal
|
|
143
|
+
* sentinel, while HTTP receives the quoted strong ETag.
|
|
144
|
+
*/
|
|
145
|
+
export function renderTaskProjectEnv(
|
|
146
|
+
resolved: ResolvedTaskProjectEnv,
|
|
147
|
+
): TaskGatewayEnvSnapshot {
|
|
148
|
+
const names = Object.keys(resolved.values).sort(compareAscii);
|
|
149
|
+
let prefix = `# uai-env-v1 keys=${names.join(" ")}\n`;
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
prefix += `export ${name}=${quoteTaskEnvValue(resolved.values[name] ?? "")}\n`;
|
|
152
|
+
}
|
|
153
|
+
const revision = createHash("sha256").update(prefix, "utf8").digest("hex");
|
|
154
|
+
return {
|
|
155
|
+
body: `${prefix}# uai-env-v1 end=${revision}\n`,
|
|
156
|
+
etag: `"${revision}"`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolver closure installed on a durable task-gateway generation. The
|
|
162
|
+
* rendered snapshot stays in memory while every project used by the task has
|
|
163
|
+
* the same value revision. A set, delete, or credential-store reconcile bumps
|
|
164
|
+
* that project's revision synchronously, so the next request recomputes and
|
|
165
|
+
* still observes rotations and removals immediately.
|
|
166
|
+
*
|
|
167
|
+
* `uaiEnv` is the runner's own env for the task (the secrets proxy's trust
|
|
168
|
+
* variables while it runs), read per request: reserved names only, so it
|
|
169
|
+
* never collides with a project value, and its change re-renders too.
|
|
170
|
+
*
|
|
171
|
+
* `proxiedEnv` is the task's proxy-mode keys (ADR-153): each such name is
|
|
172
|
+
* withheld from the project values, and carries its placeholder only while
|
|
173
|
+
* the key is usable. Also read per request, and part of the cache key, so a
|
|
174
|
+
* change of readiness alone (pins in, pins failed) re-renders.
|
|
175
|
+
*/
|
|
176
|
+
export function taskProjectEnvResolver(
|
|
177
|
+
spec: TaskProjectEnvSpec,
|
|
178
|
+
uaiEnv: () => Readonly<Record<string, string>> = () => ({}),
|
|
179
|
+
proxiedEnv: () => {
|
|
180
|
+
readonly withheld: readonly string[];
|
|
181
|
+
readonly placeholders: Readonly<Record<string, string>>;
|
|
182
|
+
} = () => ({ withheld: [], placeholders: {} }),
|
|
183
|
+
taskSource: () => TaskEnvSource = () => STORED,
|
|
184
|
+
): TaskGatewayEnvResolver {
|
|
185
|
+
const projectIds = [...new Set(spec.projects.map((project) => project.projectId))];
|
|
186
|
+
let revisions: number[] | undefined;
|
|
187
|
+
let runnerKey: string | undefined;
|
|
188
|
+
let snapshot: TaskGatewayEnvSnapshot | undefined;
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
const currentRevisions = projectIds.map(projectEnvRevision);
|
|
192
|
+
const currentUai = uaiEnv();
|
|
193
|
+
const currentProxied = proxiedEnv();
|
|
194
|
+
const currentSource = taskSource();
|
|
195
|
+
const currentRunnerKey = JSON.stringify([
|
|
196
|
+
Object.entries(currentUai).sort(([a], [b]) => compareAscii(a, b)),
|
|
197
|
+
[...currentProxied.withheld].sort(compareAscii),
|
|
198
|
+
Object.entries(currentProxied.placeholders).sort(([a], [b]) => compareAscii(a, b)),
|
|
199
|
+
// The source, and a view's decisions and heads, never its values: a
|
|
200
|
+
// head that moves re-renders.
|
|
201
|
+
currentSource.kind === "view" ? [currentSource.kind, currentSource.view.policies] : [currentSource.kind],
|
|
202
|
+
]);
|
|
203
|
+
if (
|
|
204
|
+
snapshot &&
|
|
205
|
+
runnerKey === currentRunnerKey &&
|
|
206
|
+
revisions?.length === currentRevisions.length &&
|
|
207
|
+
revisions.every((revision, index) => revision === currentRevisions[index])
|
|
208
|
+
) {
|
|
209
|
+
return snapshot;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const resolved = resolveTaskProjectEnv(spec, currentSource);
|
|
213
|
+
const values: Record<string, string> = Object.create(null) as Record<string, string>;
|
|
214
|
+
Object.assign(values, resolved.values);
|
|
215
|
+
const withheld = new Set([
|
|
216
|
+
...currentProxied.withheld,
|
|
217
|
+
...(currentSource.kind === "view" ? currentSource.view.notDelivered : []),
|
|
218
|
+
]);
|
|
219
|
+
for (const name of withheld) delete values[name];
|
|
220
|
+
// With no project env at all, no placeholder stands in for one either.
|
|
221
|
+
const placeholders = currentSource.kind === "none" ? {} : currentProxied.placeholders;
|
|
222
|
+
for (const [name, placeholder] of Object.entries(placeholders)) {
|
|
223
|
+
// Only in place of a withheld proxy-mode value, never a new name.
|
|
224
|
+
if (withheld.has(name) && ENV_NAME.test(name) && !isReservedTaskEnvName(name) && !placeholder.includes("\0")) {
|
|
225
|
+
values[name] = placeholder;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const [name, value] of Object.entries(currentUai)) {
|
|
229
|
+
// Only reserved names: Uai's own variables, never a project's.
|
|
230
|
+
if (ENV_NAME.test(name) && isReservedTaskEnvName(name) && !value.includes("\0")) values[name] = value;
|
|
231
|
+
}
|
|
232
|
+
snapshot = renderTaskProjectEnv({ values });
|
|
233
|
+
revisions = currentRevisions;
|
|
234
|
+
runnerKey = currentRunnerKey;
|
|
235
|
+
return snapshot;
|
|
236
|
+
};
|
|
237
|
+
}
|