@runuai/host 0.9.118 → 0.9.119

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.
@@ -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
+ }
@@ -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 { getHostTask, upsertHostTask } from "./runtime-state";
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. */
@@ -1972,6 +1992,7 @@ export class Orchestrator {
1972
1992
  cliSecret,
1973
1993
  this.isPromptLevelSecretary(channel, agent.id),
1974
1994
  ),
1995
+ this.machineProjectEnv(task),
1975
1996
  ),
1976
1997
  },
1977
1998
  );
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.118",
3
+ "version": "0.9.119",
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>",
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.