@runuai/host 0.9.118 → 0.9.120
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/0022_task_project_ids.sql +5 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/db/schema.ts +3 -0
- package/lib/host-env.ts +46 -0
- package/lib/orchestrator.ts +37 -6
- package/lib/runtime-state.ts +16 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Machine tasks (ADR-121) have no compose env, so per-project env values
|
|
2
|
+
-- (ADR-015) must ride each agent session's env instead — and the host only
|
|
3
|
+
-- knew a task's project SLUGS while env is keyed by project id. Lead project
|
|
4
|
+
-- first. Older rows stay '[]' (Docker tasks never read it).
|
|
5
|
+
ALTER TABLE `uai_host_tasks` ADD `project_ids` text NOT NULL DEFAULT '[]';
|
|
@@ -155,6 +155,13 @@
|
|
|
155
155
|
"when": 1788900000003,
|
|
156
156
|
"tag": "0021_project_env_store_ref",
|
|
157
157
|
"breakpoints": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"idx": 22,
|
|
161
|
+
"version": "6",
|
|
162
|
+
"when": 1788900000004,
|
|
163
|
+
"tag": "0022_task_project_ids",
|
|
164
|
+
"breakpoints": true
|
|
158
165
|
}
|
|
159
166
|
]
|
|
160
167
|
}
|
package/db/schema.ts
CHANGED
|
@@ -37,6 +37,9 @@ export const hostTasks = sqliteTable(
|
|
|
37
37
|
ownerEmail: text("owner_email"),
|
|
38
38
|
ownerName: text("owner_name"),
|
|
39
39
|
projectSlugs: text("project_slugs").notNull().default("[]"),
|
|
40
|
+
// Lead project first. Machine tasks resolve per-project env through these
|
|
41
|
+
// ids at every agent spawn (Docker tasks take env from compose instead).
|
|
42
|
+
projectIds: text("project_ids").notNull().default("[]"),
|
|
40
43
|
// ADR-131 D3: which engine-account scope this task resolves credentials
|
|
41
44
|
// from ("org:<orgId>" on a shared host; null = "host"). Persisted so a
|
|
42
45
|
// host restart re-provisions the same scope before the cloud re-sends it.
|
package/lib/host-env.ts
CHANGED
|
@@ -209,3 +209,49 @@ export function getDecryptedForProject(projectId: string): Record<string, string
|
|
|
209
209
|
}
|
|
210
210
|
return out;
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Names a project may declare but a task must never receive from project env —
|
|
215
|
+
* the same list task-up.sh's `is_reserved_task_env_name` enforces for
|
|
216
|
+
* container tasks: git/gh plumbing, loader and shell hooks, toolchain config,
|
|
217
|
+
* the host's own `UAI_*` namespace, engine credentials, and the browser path.
|
|
218
|
+
*/
|
|
219
|
+
const RESERVED_TASK_ENV_EXACT = new Set([
|
|
220
|
+
"GH_TOKEN", "GITHUB_TOKEN", "GH_CONFIG_DIR", "GH_HOST",
|
|
221
|
+
"GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY",
|
|
222
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_INDEX_FILE", "GIT_EXEC_PATH",
|
|
223
|
+
"GIT_ASKPASS", "SSH_ASKPASS", "GIT_SSH", "GIT_SSH_COMMAND",
|
|
224
|
+
"HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME",
|
|
225
|
+
"PATH", "LD_PRELOAD", "LD_AUDIT", "LD_LIBRARY_PATH", "NODE_OPTIONS",
|
|
226
|
+
"NODE_PATH", "SHELLOPTS", "BASHOPTS", "PS4", "BASH_ENV", "ENV",
|
|
227
|
+
"CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN",
|
|
228
|
+
"XAI_API_KEY", "PLAYWRIGHT_BROWSERS_PATH",
|
|
229
|
+
]);
|
|
230
|
+
const RESERVED_TASK_ENV_NPM = /^npm_config_(prefix|cache|logs[_-]dir|umask)$/i;
|
|
231
|
+
|
|
232
|
+
export function isReservedTaskEnvName(name: string): boolean {
|
|
233
|
+
if (RESERVED_TASK_ENV_EXACT.has(name)) return true;
|
|
234
|
+
if (name.startsWith("GIT_CONFIG") || name.startsWith("ASDF_")) return true;
|
|
235
|
+
if (name.startsWith("COREPACK_") || name.startsWith("UAI_")) return true;
|
|
236
|
+
return RESERVED_TASK_ENV_NPM.test(name);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Project env for a task that has no container env to carry it (ADR-121
|
|
241
|
+
* machine tasks): every project's decrypted values, lead project winning a
|
|
242
|
+
* key collision, minus the reserved names above — exactly what task-up hands
|
|
243
|
+
* Compose for a Docker task. Live 2026-09-09: a machine task on Uai Cloud
|
|
244
|
+
* had the Infisical identity in the store and the host, and the agent never
|
|
245
|
+
* saw it, because only the compose path injected project env.
|
|
246
|
+
*/
|
|
247
|
+
export function projectEnvForTask(projectIds: readonly string[]): Record<string, string> {
|
|
248
|
+
const merged: Record<string, string> = {};
|
|
249
|
+
for (const projectId of [...projectIds].reverse()) {
|
|
250
|
+
Object.assign(merged, getDecryptedForProject(projectId));
|
|
251
|
+
}
|
|
252
|
+
const out: Record<string, string> = {};
|
|
253
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
254
|
+
if (!isReservedTaskEnvName(key)) out[key] = value;
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
package/lib/orchestrator.ts
CHANGED
|
@@ -43,7 +43,8 @@ import {
|
|
|
43
43
|
} from "./agents/types";
|
|
44
44
|
import { ACTIVE_STATUSES, isActive, isOnDashboard } from "./task-status";
|
|
45
45
|
import { isSafeHostTaskId } from "./task-identity";
|
|
46
|
-
import {
|
|
46
|
+
import { projectEnvForTask } from "./host-env";
|
|
47
|
+
import { getHostTask, hostTaskProjectIds, upsertHostTask } from "./runtime-state";
|
|
47
48
|
import { clearRefresh, reconcileTaskGitAuth } from "./github-tokens";
|
|
48
49
|
import { ensureTaskSshIdentity, setupTaskGitIdentity } from "./git-identity";
|
|
49
50
|
import { dockerCli } from "./docker-exec";
|
|
@@ -1505,6 +1506,7 @@ export class Orchestrator {
|
|
|
1505
1506
|
cliSecret,
|
|
1506
1507
|
this.isPromptLevelSecretary(channel, agent.id),
|
|
1507
1508
|
),
|
|
1509
|
+
this.machineProjectEnv(task),
|
|
1508
1510
|
),
|
|
1509
1511
|
},
|
|
1510
1512
|
);
|
|
@@ -1575,17 +1577,35 @@ export class Orchestrator {
|
|
|
1575
1577
|
channel: Channel,
|
|
1576
1578
|
agent: RosterAgent,
|
|
1577
1579
|
base: Record<string, string>,
|
|
1580
|
+
/** Lowest precedence: project env for tasks whose environment has no
|
|
1581
|
+
* container env to carry it. The account's env and the uai base win. */
|
|
1582
|
+
floor: Record<string, string> = {},
|
|
1578
1583
|
): Record<string, string> {
|
|
1579
1584
|
const account = pickEngineAccount(agent.kind, {
|
|
1580
1585
|
scope: channel.engineAccountScope,
|
|
1581
1586
|
});
|
|
1582
1587
|
if (!account) {
|
|
1583
1588
|
channel.accountByAgent.delete(agent.id);
|
|
1584
|
-
return base;
|
|
1589
|
+
return { ...floor, ...base };
|
|
1585
1590
|
}
|
|
1586
1591
|
channel.accountByAgent.set(agent.id, account.id);
|
|
1587
1592
|
noteEngineAccountUsed(account.id);
|
|
1588
|
-
return { ...account.execEnv, ...base };
|
|
1593
|
+
return { ...floor, ...account.execEnv, ...base };
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* ADR-015 project env for a machine task (ADR-121): Docker tasks receive it
|
|
1598
|
+
* from the compose env task-up renders, but a machine has no container, so
|
|
1599
|
+
* it rides every agent session's env instead. Read at each spawn, so a
|
|
1600
|
+
* respawn picks up values set since the task started. Empty for container
|
|
1601
|
+
* tasks — their env already carries it and must not be doubled into argv.
|
|
1602
|
+
*/
|
|
1603
|
+
private machineProjectEnv(task: {
|
|
1604
|
+
environmentProvider: string | null;
|
|
1605
|
+
projectIds?: string | null;
|
|
1606
|
+
}): Record<string, string> {
|
|
1607
|
+
if (task.environmentProvider !== "machine") return {};
|
|
1608
|
+
return projectEnvForTask(hostTaskProjectIds(task));
|
|
1589
1609
|
}
|
|
1590
1610
|
|
|
1591
1611
|
/** Install a session with an event handler scoped to this exact generation. */
|
|
@@ -1939,7 +1959,13 @@ export class Orchestrator {
|
|
|
1939
1959
|
return false;
|
|
1940
1960
|
}
|
|
1941
1961
|
|
|
1942
|
-
|
|
1962
|
+
// Live 2026-09-10 (Mac Studio, host 0.9.119): a reconcile pass had
|
|
1963
|
+
// already spawned the whole roster, bindSession had emitted `ready`, and
|
|
1964
|
+
// this unconditional `agents` then overwrote it — the page stayed
|
|
1965
|
+
// covered for good. Only announce the wake-up when there is one to do.
|
|
1966
|
+
const allBound = (): boolean =>
|
|
1967
|
+
initialRoster.every((agent) => channel.sessions.has(agent.id));
|
|
1968
|
+
if (!allBound()) this.emitTaskStage(channel.taskId, "agents");
|
|
1943
1969
|
for (const agent of initialRoster) {
|
|
1944
1970
|
if (!this.isActiveChannel(channel)) return false;
|
|
1945
1971
|
// A prior partial start or an independently guarded recovery may already
|
|
@@ -1972,6 +1998,7 @@ export class Orchestrator {
|
|
|
1972
1998
|
cliSecret,
|
|
1973
1999
|
this.isPromptLevelSecretary(channel, agent.id),
|
|
1974
2000
|
),
|
|
2001
|
+
this.machineProjectEnv(task),
|
|
1975
2002
|
),
|
|
1976
2003
|
},
|
|
1977
2004
|
);
|
|
@@ -1997,8 +2024,12 @@ export class Orchestrator {
|
|
|
1997
2024
|
// so it both duplicated the opening turn AND re-injected it on a host
|
|
1998
2025
|
// restart, making agents redo the first turn mid-task. The persona/mission
|
|
1999
2026
|
// an agent needs live in the always-on system preamble, so nothing is lost.
|
|
2000
|
-
// `ready`
|
|
2001
|
-
//
|
|
2027
|
+
// `ready` is emitted by bindSession as the last roster agent binds (or
|
|
2028
|
+
// above, when one could not). Re-assert it here for the roster that was
|
|
2029
|
+
// already bound when this loop began — every bind then predates this
|
|
2030
|
+
// pass and could have been overwritten by an `agents` from a start
|
|
2031
|
+
// that lost the race.
|
|
2032
|
+
if (allBound()) this.emitTaskStage(channel.taskId, "ready");
|
|
2002
2033
|
return true;
|
|
2003
2034
|
}
|
|
2004
2035
|
|
package/lib/runtime-state.ts
CHANGED
|
@@ -232,16 +232,32 @@ export function recordTaskOwner(
|
|
|
232
232
|
ownerName: string | null,
|
|
233
233
|
projectSlugs: string[],
|
|
234
234
|
engineAccountScope: string | null = null,
|
|
235
|
+
/** Lead project first — the order project env merges in. */
|
|
236
|
+
projectIds: string[] = [],
|
|
235
237
|
): void {
|
|
236
238
|
upsertHostTask(taskId, {
|
|
237
239
|
ownerUserId,
|
|
238
240
|
ownerEmail,
|
|
239
241
|
ownerName,
|
|
240
242
|
projectSlugs: JSON.stringify(projectSlugs),
|
|
243
|
+
projectIds: JSON.stringify(projectIds),
|
|
241
244
|
engineAccountScope,
|
|
242
245
|
});
|
|
243
246
|
}
|
|
244
247
|
|
|
248
|
+
/** The task's project ids, lead project first; `[]` for rows that predate
|
|
249
|
+
* the column or carry an unreadable value. */
|
|
250
|
+
export function hostTaskProjectIds(task: { projectIds?: string | null }): string[] {
|
|
251
|
+
try {
|
|
252
|
+
const parsed: unknown = JSON.parse(task.projectIds ?? "[]");
|
|
253
|
+
return Array.isArray(parsed)
|
|
254
|
+
? parsed.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
255
|
+
: [];
|
|
256
|
+
} catch {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
245
261
|
/** Append a task lifecycle event to the host event log (local `/api/events`). */
|
|
246
262
|
export function recordHostEvent(
|
|
247
263
|
taskId: string,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -776,6 +776,10 @@ export const hostCommands: HostCommands = {
|
|
|
776
776
|
input.projects.map((p) => p.slug),
|
|
777
777
|
// ADR-131 D3: persisted so recovery re-provisions the same scope.
|
|
778
778
|
input.task.engineAccountScope ?? null,
|
|
779
|
+
// Lead project first (position 0 wins env collisions, as in task-up).
|
|
780
|
+
[...input.projects]
|
|
781
|
+
.sort((a, b) => a.position - b.position)
|
|
782
|
+
.map((p) => p.id),
|
|
779
783
|
);
|
|
780
784
|
// ADR-048: persist the cloud-issued per-task cli secret (host-private) so the
|
|
781
785
|
// `uai` CLI's per-agent tokens can be minted now + after a host restart.
|