@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,497 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSecretKey,
|
|
3
|
+
randomBytes,
|
|
4
|
+
type KeyObject,
|
|
5
|
+
} from "node:crypto";
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { env } from "./env";
|
|
18
|
+
import {
|
|
19
|
+
TASK_FORWARDER_SECRET_BYTES,
|
|
20
|
+
taskForwarderMac,
|
|
21
|
+
verifyTaskForwarderMac,
|
|
22
|
+
type TaskForwarderIdentity,
|
|
23
|
+
} from "./task-gateway-protocol";
|
|
24
|
+
import { assertSafeHostTaskId, isSafeHostTaskId } from "./task-identity";
|
|
25
|
+
|
|
26
|
+
const TASK_GATEWAY_STATE_VERSION = 1 as const;
|
|
27
|
+
const TASK_GATEWAY_MAX_ENV_BYTES = 512 * 1024;
|
|
28
|
+
const TASK_GATEWAY_ETAG = /^"[0-9a-f]{64}"$/;
|
|
29
|
+
|
|
30
|
+
interface PersistedTaskGatewayIdentity {
|
|
31
|
+
version: typeof TASK_GATEWAY_STATE_VERSION;
|
|
32
|
+
taskId: string;
|
|
33
|
+
generation: string;
|
|
34
|
+
secret: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface TaskGatewayRecord {
|
|
38
|
+
readonly taskId: string;
|
|
39
|
+
readonly generation: string;
|
|
40
|
+
readonly secret: KeyObject;
|
|
41
|
+
active: boolean;
|
|
42
|
+
/** Host lifecycle quarantine; unlike an unproved restart record, this
|
|
43
|
+
* generation may be restaged and rolled back by the same process. */
|
|
44
|
+
suspended: boolean;
|
|
45
|
+
resolveEnv: TaskGatewayEnvResolver | null;
|
|
46
|
+
snapshot: TaskGatewayEnvSnapshot | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TaskGatewayRegistration extends TaskForwarderIdentity {}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A complete, already-rendered loader response. The renderer owns the POSIX
|
|
53
|
+
* quoting and managed-key list. The gateway verifies its final sentinel and
|
|
54
|
+
* exact quoted lowercase-SHA-256 ETag, then sends these bytes unchanged.
|
|
55
|
+
*/
|
|
56
|
+
export interface TaskGatewayEnvSnapshot {
|
|
57
|
+
readonly body: string | Uint8Array;
|
|
58
|
+
readonly etag: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type TaskGatewayEnvResolver = () =>
|
|
62
|
+
| TaskGatewayEnvSnapshot
|
|
63
|
+
| null;
|
|
64
|
+
|
|
65
|
+
export interface ResolvedTaskGatewayEnv {
|
|
66
|
+
readonly body: Buffer;
|
|
67
|
+
readonly etag: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RegisterTaskGatewayOptions {
|
|
71
|
+
resolveEnv?: TaskGatewayEnvResolver;
|
|
72
|
+
snapshot?: TaskGatewayEnvSnapshot;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type RevocationListener = (identity: TaskGatewayRegistration) => void;
|
|
76
|
+
|
|
77
|
+
const records = new Map<string, TaskGatewayRecord>();
|
|
78
|
+
const revocationListeners = new Set<RevocationListener>();
|
|
79
|
+
|
|
80
|
+
function stateDir(): string {
|
|
81
|
+
return resolve(env.dataDir, "task-gateway");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function statePath(taskId: string): string {
|
|
85
|
+
return resolve(stateDir(), `${taskId}.json`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stateDirectoryIsPrivate(): boolean {
|
|
89
|
+
try {
|
|
90
|
+
const metadata = lstatSync(stateDir());
|
|
91
|
+
return (
|
|
92
|
+
metadata.isDirectory() &&
|
|
93
|
+
!metadata.isSymbolicLink() &&
|
|
94
|
+
(metadata.mode & 0o077) === 0 &&
|
|
95
|
+
(process.getuid === undefined || metadata.uid === process.getuid())
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseSecret(value: unknown): Buffer | null {
|
|
103
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const decoded = Buffer.from(value, "base64url");
|
|
107
|
+
return decoded.length === TASK_FORWARDER_SECRET_BYTES &&
|
|
108
|
+
decoded.toString("base64url") === value
|
|
109
|
+
? decoded
|
|
110
|
+
: null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function persistedRecord(taskId: string): TaskGatewayRecord | null {
|
|
114
|
+
assertSafeHostTaskId(taskId);
|
|
115
|
+
if (!stateDirectoryIsPrivate()) return null;
|
|
116
|
+
try {
|
|
117
|
+
const path = statePath(taskId);
|
|
118
|
+
const metadata = lstatSync(path);
|
|
119
|
+
// A task-controlled symlink must never redirect a credential read. Host
|
|
120
|
+
// state is private already; requiring no group/other bits makes that
|
|
121
|
+
// boundary explicit and catches an accidentally broad installation.
|
|
122
|
+
if (
|
|
123
|
+
!metadata.isFile() ||
|
|
124
|
+
metadata.isSymbolicLink() ||
|
|
125
|
+
(metadata.mode & 0o077) !== 0 ||
|
|
126
|
+
(process.getuid !== undefined && metadata.uid !== process.getuid())
|
|
127
|
+
) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<
|
|
131
|
+
PersistedTaskGatewayIdentity
|
|
132
|
+
>;
|
|
133
|
+
const secret = parseSecret(parsed.secret);
|
|
134
|
+
if (
|
|
135
|
+
parsed.version !== TASK_GATEWAY_STATE_VERSION ||
|
|
136
|
+
parsed.taskId !== taskId ||
|
|
137
|
+
typeof parsed.generation !== "string" ||
|
|
138
|
+
!/^[A-Za-z0-9_-]{16,64}$/.test(parsed.generation) ||
|
|
139
|
+
secret === null
|
|
140
|
+
) {
|
|
141
|
+
secret?.fill(0);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const key = createSecretKey(secret);
|
|
145
|
+
secret.fill(0);
|
|
146
|
+
return {
|
|
147
|
+
taskId,
|
|
148
|
+
generation: parsed.generation,
|
|
149
|
+
secret: key,
|
|
150
|
+
// Durable state proves a surviving forwarder's secret, but not that the
|
|
151
|
+
// task still exists. Lifecycle recovery must explicitly reactivate it.
|
|
152
|
+
active: false,
|
|
153
|
+
suspended: false,
|
|
154
|
+
resolveEnv: null,
|
|
155
|
+
snapshot: null,
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function currentRecord(taskId: string): TaskGatewayRecord | null {
|
|
163
|
+
const cached = records.get(taskId);
|
|
164
|
+
if (cached) return cached;
|
|
165
|
+
if (!isSafeHostTaskId(taskId)) return null;
|
|
166
|
+
const loaded = persistedRecord(taskId);
|
|
167
|
+
if (loaded) records.set(taskId, loaded);
|
|
168
|
+
return loaded;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function persistIdentity(
|
|
172
|
+
identity: TaskGatewayRegistration,
|
|
173
|
+
secret: Buffer,
|
|
174
|
+
): void {
|
|
175
|
+
mkdirSync(stateDir(), { recursive: true, mode: 0o700 });
|
|
176
|
+
if (!stateDirectoryIsPrivate()) {
|
|
177
|
+
// Tighten an older host-created directory, but never follow a symlink or
|
|
178
|
+
// adopt a directory owned by another account.
|
|
179
|
+
const metadata = lstatSync(stateDir());
|
|
180
|
+
if (
|
|
181
|
+
!metadata.isDirectory() ||
|
|
182
|
+
metadata.isSymbolicLink() ||
|
|
183
|
+
(process.getuid !== undefined && metadata.uid !== process.getuid())
|
|
184
|
+
) {
|
|
185
|
+
throw new Error("unsafe task gateway state directory");
|
|
186
|
+
}
|
|
187
|
+
chmodSync(stateDir(), 0o700);
|
|
188
|
+
}
|
|
189
|
+
const path = statePath(identity.taskId);
|
|
190
|
+
const temporary = resolve(
|
|
191
|
+
stateDir(),
|
|
192
|
+
`.${identity.taskId}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`,
|
|
193
|
+
);
|
|
194
|
+
const serialized = JSON.stringify({
|
|
195
|
+
version: TASK_GATEWAY_STATE_VERSION,
|
|
196
|
+
taskId: identity.taskId,
|
|
197
|
+
generation: identity.generation,
|
|
198
|
+
secret: secret.toString("base64url"),
|
|
199
|
+
} satisfies PersistedTaskGatewayIdentity);
|
|
200
|
+
try {
|
|
201
|
+
writeFileSync(temporary, serialized, { flag: "wx", mode: 0o600 });
|
|
202
|
+
renameSync(temporary, path);
|
|
203
|
+
} finally {
|
|
204
|
+
rmSync(temporary, { force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function notifyRevoked(record: TaskGatewayRecord): void {
|
|
209
|
+
const identity = {
|
|
210
|
+
taskId: record.taskId,
|
|
211
|
+
generation: record.generation,
|
|
212
|
+
};
|
|
213
|
+
for (const listener of revocationListeners) {
|
|
214
|
+
try {
|
|
215
|
+
listener(identity);
|
|
216
|
+
} catch {
|
|
217
|
+
// Revocation must continue even if one socket owner misbehaves.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assignEnv(
|
|
223
|
+
record: TaskGatewayRecord,
|
|
224
|
+
options: RegisterTaskGatewayOptions,
|
|
225
|
+
): void {
|
|
226
|
+
if (options.resolveEnv && options.snapshot) {
|
|
227
|
+
throw new Error("task gateway env must use a resolver or snapshot, not both");
|
|
228
|
+
}
|
|
229
|
+
record.resolveEnv = options.resolveEnv ?? null;
|
|
230
|
+
record.snapshot = options.snapshot ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Mint and durably register a fresh task generation. */
|
|
234
|
+
export function registerTaskGatewayTask(
|
|
235
|
+
taskId: string,
|
|
236
|
+
options: RegisterTaskGatewayOptions = {},
|
|
237
|
+
): TaskGatewayRegistration {
|
|
238
|
+
assertSafeHostTaskId(taskId);
|
|
239
|
+
if (options.resolveEnv && options.snapshot) {
|
|
240
|
+
throw new Error("task gateway env must use a resolver or snapshot, not both");
|
|
241
|
+
}
|
|
242
|
+
const previous = currentRecord(taskId);
|
|
243
|
+
const secret = randomBytes(TASK_FORWARDER_SECRET_BYTES);
|
|
244
|
+
const identity = {
|
|
245
|
+
taskId,
|
|
246
|
+
generation: randomBytes(18).toString("base64url"),
|
|
247
|
+
};
|
|
248
|
+
try {
|
|
249
|
+
persistIdentity(identity, secret);
|
|
250
|
+
const record: TaskGatewayRecord = {
|
|
251
|
+
...identity,
|
|
252
|
+
secret: createSecretKey(secret),
|
|
253
|
+
active: true,
|
|
254
|
+
suspended: false,
|
|
255
|
+
resolveEnv: null,
|
|
256
|
+
snapshot: null,
|
|
257
|
+
};
|
|
258
|
+
assignEnv(record, options);
|
|
259
|
+
records.set(taskId, record);
|
|
260
|
+
if (previous) notifyRevoked(previous);
|
|
261
|
+
return identity;
|
|
262
|
+
} finally {
|
|
263
|
+
secret.fill(0);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reattach in-memory env state to a generation that survived a host restart.
|
|
269
|
+
* Corrupt or absent state returns null; callers then rotate and restart the
|
|
270
|
+
* task forwarder instead of accepting an unproved identity.
|
|
271
|
+
*/
|
|
272
|
+
export function recoverTaskGatewayTask(
|
|
273
|
+
taskId: string,
|
|
274
|
+
options: RegisterTaskGatewayOptions = {},
|
|
275
|
+
): TaskGatewayRegistration | null {
|
|
276
|
+
assertSafeHostTaskId(taskId);
|
|
277
|
+
const record = currentRecord(taskId);
|
|
278
|
+
if (!record) return null;
|
|
279
|
+
assignEnv(record, options);
|
|
280
|
+
record.active = true;
|
|
281
|
+
record.suspended = false;
|
|
282
|
+
return { taskId, generation: record.generation };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A fresh fixed-size copy intended only for the forwarder's stdin. Host
|
|
287
|
+
* recovery may stage a replacement forwarder while the generation is
|
|
288
|
+
* suspended; authentication remains closed until reactivateTaskGatewayTask.
|
|
289
|
+
*/
|
|
290
|
+
export function taskGatewayForwarderSecretInput(
|
|
291
|
+
taskId: string,
|
|
292
|
+
generation: string,
|
|
293
|
+
): Uint8Array | null {
|
|
294
|
+
const record = currentRecord(taskId);
|
|
295
|
+
if (
|
|
296
|
+
!record ||
|
|
297
|
+
(!record.active && !record.suspended) ||
|
|
298
|
+
record.generation !== generation
|
|
299
|
+
) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const exported = record.secret.export();
|
|
303
|
+
const bytes = Buffer.isBuffer(exported) ? exported : Buffer.from(exported);
|
|
304
|
+
if (bytes.length !== TASK_FORWARDER_SECRET_BYTES) {
|
|
305
|
+
bytes.fill(0);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const copy = Uint8Array.from(bytes);
|
|
309
|
+
bytes.fill(0);
|
|
310
|
+
return copy;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function setTaskGatewayEnvResolver(
|
|
314
|
+
taskId: string,
|
|
315
|
+
generation: string,
|
|
316
|
+
resolveEnv: TaskGatewayEnvResolver,
|
|
317
|
+
): boolean {
|
|
318
|
+
const record = currentRecord(taskId);
|
|
319
|
+
if (!record || !record.active || record.generation !== generation) return false;
|
|
320
|
+
record.resolveEnv = resolveEnv;
|
|
321
|
+
record.snapshot = null;
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Snapshot convenience for runtimes whose env source already fans out updates. */
|
|
326
|
+
export function updateTaskGatewayEnv(
|
|
327
|
+
taskId: string,
|
|
328
|
+
generation: string,
|
|
329
|
+
snapshot: TaskGatewayEnvSnapshot,
|
|
330
|
+
): boolean {
|
|
331
|
+
const record = currentRecord(taskId);
|
|
332
|
+
if (!record || !record.active || record.generation !== generation) return false;
|
|
333
|
+
record.snapshot = snapshot;
|
|
334
|
+
record.resolveEnv = null;
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeEnvSnapshot(
|
|
339
|
+
snapshot: TaskGatewayEnvSnapshot | null,
|
|
340
|
+
): ResolvedTaskGatewayEnv | null {
|
|
341
|
+
if (snapshot === null || !TASK_GATEWAY_ETAG.test(snapshot.etag)) return null;
|
|
342
|
+
const body =
|
|
343
|
+
typeof snapshot.body === "string"
|
|
344
|
+
? Buffer.from(snapshot.body, "utf8")
|
|
345
|
+
: Buffer.from(snapshot.body);
|
|
346
|
+
if (
|
|
347
|
+
body.length > TASK_GATEWAY_MAX_ENV_BYTES ||
|
|
348
|
+
body.includes(0)
|
|
349
|
+
) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const text = body.toString("utf8");
|
|
353
|
+
const rawEtag = snapshot.etag.slice(1, -1);
|
|
354
|
+
if (
|
|
355
|
+
!Buffer.from(text, "utf8").equals(body) ||
|
|
356
|
+
!text.endsWith(`# uai-env-v1 end=${rawEtag}\n`)
|
|
357
|
+
) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
return { body, etag: snapshot.etag };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Resolve on every request so rotations/deletions are visible immediately. */
|
|
364
|
+
export function resolveTaskGatewayEnv(
|
|
365
|
+
identity: TaskGatewayRegistration,
|
|
366
|
+
): ResolvedTaskGatewayEnv | null {
|
|
367
|
+
const record = currentRecord(identity.taskId);
|
|
368
|
+
if (
|
|
369
|
+
!record ||
|
|
370
|
+
!record.active ||
|
|
371
|
+
record.generation !== identity.generation
|
|
372
|
+
) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
let snapshot: TaskGatewayEnvSnapshot | null;
|
|
376
|
+
try {
|
|
377
|
+
snapshot = record.resolveEnv ? record.resolveEnv() : record.snapshot;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return normalizeEnvSnapshot(snapshot);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function isTaskGatewayGenerationCurrent(
|
|
385
|
+
identity: TaskGatewayRegistration,
|
|
386
|
+
): boolean {
|
|
387
|
+
const record = currentRecord(identity.taskId);
|
|
388
|
+
return record?.active === true && record.generation === identity.generation;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Current non-secret identity, used by the fixed-task sidecar listener. */
|
|
392
|
+
export function currentTaskGatewayRegistration(
|
|
393
|
+
taskId: string,
|
|
394
|
+
): TaskGatewayRegistration | null {
|
|
395
|
+
const record = currentRecord(taskId);
|
|
396
|
+
return record?.active
|
|
397
|
+
? { taskId: record.taskId, generation: record.generation }
|
|
398
|
+
: null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Trusted recovery identity, available while public authentication is shut. */
|
|
402
|
+
export function taskGatewayRegistrationForRecovery(
|
|
403
|
+
taskId: string,
|
|
404
|
+
): TaskGatewayRegistration | null {
|
|
405
|
+
const record = currentRecord(taskId);
|
|
406
|
+
return record && (record.active || record.suspended)
|
|
407
|
+
? { taskId: record.taskId, generation: record.generation }
|
|
408
|
+
: null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Immediately close a live generation while retaining its durable identity.
|
|
413
|
+
* Runtime pause/recovery uses this before the first stop; a failed stop can
|
|
414
|
+
* reactivate the same generation without ever minting a second authority.
|
|
415
|
+
*/
|
|
416
|
+
export function suspendTaskGatewayTask(
|
|
417
|
+
taskId: string,
|
|
418
|
+
): TaskGatewayRegistration | null {
|
|
419
|
+
const record = currentRecord(taskId);
|
|
420
|
+
if (!record || !record.active) return null;
|
|
421
|
+
record.active = false;
|
|
422
|
+
record.suspended = true;
|
|
423
|
+
notifyRevoked(record);
|
|
424
|
+
return { taskId: record.taskId, generation: record.generation };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Same-process rollback for a failed pause/maintenance stop. */
|
|
428
|
+
export function reactivateTaskGatewayTask(
|
|
429
|
+
identity: TaskGatewayRegistration,
|
|
430
|
+
): boolean {
|
|
431
|
+
const record = currentRecord(identity.taskId);
|
|
432
|
+
if (
|
|
433
|
+
!record ||
|
|
434
|
+
!record.suspended ||
|
|
435
|
+
record.generation !== identity.generation
|
|
436
|
+
) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
record.active = true;
|
|
440
|
+
record.suspended = false;
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function verifyTaskGatewayAuthentication(
|
|
445
|
+
identity: TaskGatewayRegistration,
|
|
446
|
+
nonce: Buffer,
|
|
447
|
+
providedMac: Buffer,
|
|
448
|
+
): boolean {
|
|
449
|
+
const record = currentRecord(identity.taskId);
|
|
450
|
+
if (
|
|
451
|
+
!record ||
|
|
452
|
+
!record.active ||
|
|
453
|
+
record.generation !== identity.generation
|
|
454
|
+
) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
return verifyTaskForwarderMac(
|
|
459
|
+
taskForwarderMac(record.secret, identity, nonce),
|
|
460
|
+
providedMac,
|
|
461
|
+
);
|
|
462
|
+
} catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Generation-fenced teardown: a stale stop cannot revoke a replacement. */
|
|
468
|
+
export function removeTaskGatewayTask(
|
|
469
|
+
taskId: string,
|
|
470
|
+
generation?: string,
|
|
471
|
+
): boolean {
|
|
472
|
+
if (!isSafeHostTaskId(taskId)) return false;
|
|
473
|
+
const record = currentRecord(taskId);
|
|
474
|
+
if (!record || (generation !== undefined && record.generation !== generation)) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
// Revoke live authority first. If durable cleanup fails, this process stays
|
|
478
|
+
// closed and a restarted process loads the leftover generation inactive
|
|
479
|
+
// until explicit task recovery; surface the deletion error to the caller.
|
|
480
|
+
records.delete(taskId);
|
|
481
|
+
notifyRevoked(record);
|
|
482
|
+
rmSync(statePath(taskId), { force: true });
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function subscribeTaskGatewayRevocations(
|
|
487
|
+
listener: RevocationListener,
|
|
488
|
+
): () => void {
|
|
489
|
+
revocationListeners.add(listener);
|
|
490
|
+
return () => revocationListeners.delete(listener);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Test-only cache reset; temp data directories remain owned by each test. */
|
|
494
|
+
export function resetTaskGatewayStateForTests(): void {
|
|
495
|
+
records.clear();
|
|
496
|
+
revocationListeners.clear();
|
|
497
|
+
}
|
package/lib/transcript.ts
CHANGED
|
@@ -66,6 +66,7 @@ async function appendOneViaEnvironment(
|
|
|
66
66
|
): Promise<void> {
|
|
67
67
|
const path = `${environment.descriptor.workspacePath}/.uai/${TARGET_FILENAME[target]}`;
|
|
68
68
|
const proc = await environment.spawn({
|
|
69
|
+
projectEnvProfile: "maintenance",
|
|
69
70
|
argv: [
|
|
70
71
|
"/bin/sh",
|
|
71
72
|
"-c",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runuai/host",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.150",
|
|
4
4
|
"description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Uai Tech <team@runuai.com>",
|
|
@@ -56,6 +56,18 @@
|
|
|
56
56
|
"./protocol": {
|
|
57
57
|
"types": "./src/protocol.ts",
|
|
58
58
|
"default": "./src/protocol.ts"
|
|
59
|
+
},
|
|
60
|
+
"./project-env-policy": {
|
|
61
|
+
"types": "./src/project-env-policy.ts",
|
|
62
|
+
"default": "./src/project-env-policy.ts"
|
|
63
|
+
},
|
|
64
|
+
"./project-env-latest": {
|
|
65
|
+
"types": "./src/project-env-latest.ts",
|
|
66
|
+
"default": "./src/project-env-latest.ts"
|
|
67
|
+
},
|
|
68
|
+
"./project-env-wording": {
|
|
69
|
+
"types": "./src/project-env-wording.ts",
|
|
70
|
+
"default": "./src/project-env-wording.ts"
|
|
59
71
|
}
|
|
60
72
|
},
|
|
61
73
|
"scripts": {
|