@runuai/host 0.9.170 → 0.9.172

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.
@@ -561,6 +561,7 @@ export class ClaudeSession implements AgentSession {
561
561
  executionProfile?: "communicator" | "astronaut";
562
562
  attachCompatibilityKey?: string;
563
563
  agentEnv?: Record<string, string>;
564
+ claudeSettings?: Record<string, unknown>;
564
565
  }) {
565
566
  this.agentId = args.agent.id;
566
567
 
@@ -575,6 +576,16 @@ export class ClaudeSession implements AgentSession {
575
576
  ? CLAUDE_ASTRONAUT_ARGS
576
577
  : CLAUDE_FULL_ACCESS_ARGS),
577
578
  ];
579
+ if (args.claudeSettings) {
580
+ // Empty helper settings and fallbackModel override inherited values.
581
+ // Preserve native settings sources, including project hooks/plugins.
582
+ // Connection env separately pins routing with PROVIDER_MANAGED_BY_HOST.
583
+ const settings = {
584
+ ...(args.executionProfile === "communicator" ? JSON.parse(CLAUDE_COMMUNICATOR_SETTINGS) as Record<string, unknown> : {}),
585
+ ...args.claudeSettings,
586
+ };
587
+ cliArgs.push("--settings", JSON.stringify(settings));
588
+ }
578
589
  if (process.env.UAI_CLAUDE_INCLUDE_PARTIAL_MESSAGES === "1") {
579
590
  cliArgs.push("--include-partial-messages");
580
591
  }
@@ -845,6 +856,7 @@ register({
845
856
  executionProfile,
846
857
  attachCompatibilityKey,
847
858
  agentEnv,
859
+ claudeSettings,
848
860
  }) =>
849
861
  new ClaudeSession({
850
862
  taskId,
@@ -854,5 +866,6 @@ register({
854
866
  executionProfile,
855
867
  attachCompatibilityKey,
856
868
  agentEnv,
869
+ claudeSettings,
857
870
  }),
858
871
  });
@@ -29,6 +29,7 @@
29
29
  * namespaces inside it anyway.
30
30
  */
31
31
 
32
+ import { connectionFailureReason, tomlString } from "../custom-connection-launch";
32
33
  import { existsSync } from "node:fs";
33
34
  import { homedir } from "node:os";
34
35
  import { join } from "node:path";
@@ -573,6 +574,7 @@ export class CodexSession implements AgentSession {
573
574
  private readonly handlers = new Set<AgentEventHandler>();
574
575
  private readonly systemPreamble: string;
575
576
  private readonly taskId: string;
577
+ private readonly customConnection: boolean;
576
578
  /** ADR-078: a thread id persisted by a PRIOR host process, to resume. */
577
579
  private readonly resumeThreadId: string | null;
578
580
  private closed = false;
@@ -624,6 +626,7 @@ export class CodexSession implements AgentSession {
624
626
  this.agentId = args.agent.id;
625
627
  this.systemPreamble = args.systemPreamble;
626
628
  this.taskId = args.taskId;
629
+ this.customConnection = args.agentEnv?.UAI_CUSTOM_CONNECTION === "1";
627
630
  this.attachCompatibilityKey = args.attachCompatibilityKey;
628
631
  // ADR-078: read BEFORE createAgentTransport (whose session-row upsert
629
632
  // preserves this column). Non-null → a host restart; resume the thread.
@@ -639,10 +642,10 @@ export class CodexSession implements AgentSession {
639
642
  // `app-server` subcommand. Without them codex uses its configured defaults.
640
643
  const codexArgs: string[] = [];
641
644
  if (args.agent.model) {
642
- codexArgs.push("-c", `model=${args.agent.model}`);
645
+ codexArgs.push("-c", `model=${tomlString(args.agent.model)}`);
643
646
  }
644
647
  if (args.agent.effort) {
645
- codexArgs.push("-c", `model_reasoning_effort=${args.agent.effort}`);
648
+ codexArgs.push("-c", `model_reasoning_effort=${tomlString(args.agent.effort)}`);
646
649
  }
647
650
  for (const override of args.cliConfigOverrides ?? []) {
648
651
  codexArgs.push("-c", override);
@@ -759,7 +762,8 @@ export class CodexSession implements AgentSession {
759
762
  }
760
763
  } else if (fired === "turn/interrupt" && isObj(msg.error)) {
761
764
  const code = typeof msg.error.code === "number" ? msg.error.code : undefined;
762
- const message = `Couldn't stop the message: ${this.cliLabel}${code === undefined ? "" : ` (code ${code})`}: ${str(msg.error.message, "Unknown error. Please try again.")}`;
765
+ let message = `Couldn't stop the message: ${this.cliLabel}${code === undefined ? "" : ` (code ${code})`}: ${str(msg.error.message, "Unknown error. Please try again.")}`;
766
+ if (this.customConnection) message = connectionFailureReason(message);
763
767
  this.reportError(message, code);
764
768
  this.emit({ type: "notice", message: agentErrorMessage(message) });
765
769
  }
@@ -1076,8 +1080,8 @@ export class CodexSession implements AgentSession {
1076
1080
  continue;
1077
1081
  }
1078
1082
  console.warn(
1079
- `[codex] thread/resume ${threadId} failed (${
1080
- err instanceof Error ? err.message : String(err)
1083
+ `[codex] thread/resume ${this.customConnection ? "connection" : threadId} failed (${
1084
+ this.customConnection ? connectionFailureReason(err) : err instanceof Error ? err.message : String(err)
1081
1085
  }) — starting a fresh thread`,
1082
1086
  );
1083
1087
  return false;
@@ -1196,6 +1200,7 @@ export class CodexSession implements AgentSession {
1196
1200
  }
1197
1201
 
1198
1202
  private reportError(message: string, rpcCode?: number, capture = true): void {
1203
+ if (this.customConnection) message = connectionFailureReason(message);
1199
1204
  reportAgentError(message, { taskId: this.taskId, agentId: this.agentId, engine: this.kind, rpcCode }, { capture });
1200
1205
  }
1201
1206
 
@@ -1204,6 +1209,7 @@ export class CodexSession implements AgentSession {
1204
1209
  private failTurn(message: string, code?: number, usage?: AgentUsage): void {
1205
1210
  if (this.failed || this.closed) return;
1206
1211
  this.failed = true;
1212
+ if (this.customConnection) message = connectionFailureReason(message);
1207
1213
  this.reportError(message, code);
1208
1214
  const aborted = this.outstandingTurns > 0;
1209
1215
  const settles = this.outstandingTurns + [...this.turnStarts.values()]
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
 
12
+ import { isOwnedInternalConnectionRuntime } from "../internal-connections";
13
+ import { spawn } from "node:child_process";
12
14
  import { and, eq } from "drizzle-orm";
13
15
 
14
16
  import { getDb, schema } from "../db";
@@ -118,3 +120,78 @@ export async function stopPersistedAgentSession(
118
120
  .run();
119
121
  return row.sessionDir;
120
122
  }
123
+
124
+ /** Remove internal runner generations only after the caller has proven every
125
+ * covered anchored process stopped. The persisted compatibility key survives
126
+ * restart and binds this deletion to the run and credential being cleaned. */
127
+ export async function purgeInternalAgentSessionState(
128
+ request: { taskId: string; runEpoch: number; credentialIds?: readonly string[] },
129
+ environments: readonly TaskEnvironmentHandle<unknown>[],
130
+ removeDirectory: (root: string, path: string) => Promise<void> = removeInternalSessionDirectory,
131
+ ): Promise<void> {
132
+ const db = getDb();
133
+ const rows = db.select().from(schema.hostAgentSessions).where(eq(schema.hostAgentSessions.taskId, request.taskId)).all();
134
+ for (const row of rows) {
135
+ let key: unknown;
136
+ try { key = JSON.parse(row.attachCompatibilityKey ?? "null"); } catch { continue; }
137
+ if (!Array.isArray(key) || key[1] !== "lease-v1" || !Number.isSafeInteger(key[2]) || key[2] > request.runEpoch || key.at(-1) !== "custom-internal" ||
138
+ (request.credentialIds && !request.credentialIds.includes(key[3] as string))) continue;
139
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(row.agentId)) throw new Error("unsupported");
140
+ if (!environments.length) throw new Error("unsupported");
141
+ // Owned-runtime workspaces are host mounts; they persist even after the
142
+ // container disappears. Delete through host directory FDs, not exec into
143
+ // a container which may already be stopped/absent.
144
+ if (environments.some(e => e.descriptor.taskId !== request.taskId || !isOwnedInternalConnectionRuntime(e.descriptor.locator.provider))) throw new Error("unsupported");
145
+ const root = taskWorkspaceDir(request.taskId);
146
+ await removeDirectory(root, `${root}/.uai/sessions/${row.agentId}`);
147
+ db.delete(schema.hostAgentSessions).where(and(eq(schema.hostAgentSessions.taskId, row.taskId), eq(schema.hostAgentSessions.agentId, row.agentId), eq(schema.hostAgentSessions.sessionDir, row.sessionDir))).run();
148
+ }
149
+ }
150
+
151
+ /** FD-relative tree removal. A task can
152
+ * replace entries concurrently; links are never followed, and a race fails
153
+ * closed for the cleanup coordinator to retry. No provider text is read. */
154
+ export const INTERNAL_SESSION_UNLINK_HELPER = String.raw`
155
+ import os, stat, sys
156
+ root, absolute = sys.argv[1:3]
157
+ prefix = root.rstrip("/") + "/"
158
+ if not absolute.startswith(prefix): raise RuntimeError()
159
+ parts = absolute[len(prefix):].split("/")
160
+ if not parts or any(p in ("", ".", "..") for p in parts): raise RuntimeError()
161
+ flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
162
+ fds = []
163
+ def remove(parent, name):
164
+ try: info = os.stat(name, dir_fd=parent, follow_symlinks=False)
165
+ except FileNotFoundError: return
166
+ if not stat.S_ISDIR(info.st_mode): raise RuntimeError()
167
+ child = os.open(name, flags, dir_fd=parent)
168
+ try:
169
+ for entry in os.listdir(child):
170
+ info = os.stat(entry, dir_fd=child, follow_symlinks=False)
171
+ if stat.S_ISDIR(info.st_mode): remove(child, entry)
172
+ else: os.unlink(entry, dir_fd=child)
173
+ finally: os.close(child)
174
+ os.rmdir(name, dir_fd=parent)
175
+ try:
176
+ current = os.open("/", flags); fds.append(current)
177
+ for p in root.split("/") + parts[:-1]:
178
+ if not p: continue
179
+ current = os.open(p, flags, dir_fd=current); fds.append(current)
180
+ remove(current, parts[-1])
181
+ try: os.stat(parts[-1], dir_fd=current, follow_symlinks=False)
182
+ except FileNotFoundError: sys.exit(0)
183
+ raise RuntimeError()
184
+ except FileNotFoundError: sys.exit(0)
185
+ finally:
186
+ for fd in reversed(fds): os.close(fd)
187
+ `;
188
+ function removeInternalSessionDirectory(root: string, path: string): Promise<void> {
189
+ return new Promise((resolve, reject) => {
190
+ const proc = spawn("/usr/bin/python3", ["-I", "-", root, path], { stdio: ["pipe", "ignore", "ignore"], env: { PATH: "/usr/bin:/bin" } });
191
+ const timer = setTimeout(() => { proc.kill("SIGKILL"); reject(new Error("unsupported")); }, 10_000);
192
+ proc.on("error", () => { clearTimeout(timer); reject(new Error("unsupported")); });
193
+ proc.on("exit", code => { clearTimeout(timer); if (code === 0) resolve(); else reject(new Error("unsupported")); });
194
+ proc.stdin.on("error", () => {});
195
+ proc.stdin.end(INTERNAL_SESSION_UNLINK_HELPER);
196
+ });
197
+ }
@@ -201,6 +201,8 @@ function claimCompatibleAttachRow(
201
201
  }
202
202
 
203
203
  export function createAgentTransport(opts: AgentTransportOptions): LineTransport {
204
+ // Connection stderr/provider errors may contain arbitrary credentials.
205
+ if (opts.explicitEnv?.UAI_CUSTOM_CONNECTION === "1") opts = { ...opts, debugLabel: undefined };
204
206
  // ADR-121: machine-backed sessions ride the environment's own transport —
205
207
  // the container-runtime preflight is a docker/apple concern a machine task
206
208
  // must not trip over, and the host-FS durable flow below polls files a
@@ -58,6 +58,7 @@ export const RosterAgent = z.object({
58
58
  .max(128)
59
59
  .regex(/^[A-Za-z0-9_.:-]+$/)
60
60
  .optional(),
61
+ accountBinding: z.enum(["required", "preferred"]).optional(),
61
62
  defaultPrompt: z.string().optional(), // persona
62
63
  initialPrompt: z.string().optional(), // first-turn instructions, this task only
63
64
  skills: z.array(RosterSkill).optional(), // ADR-046 reference material
@@ -243,5 +244,7 @@ export interface AgentSessionFactory {
243
244
  * `-c key=value`), e.g. the Uai Managed Engine's model-provider block
244
245
  * that points codex at the inference gateway. Never from the roster. */
245
246
  cliConfigOverrides?: string[];
247
+ /** Host-generated per-launch Claude settings; never from roster input. */
248
+ claudeSettings?: Record<string, unknown>;
246
249
  }): Promise<AgentSession>;
247
250
  }
package/lib/agents/uai.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  * the request's `model`, and the gateway maps slug → upstream id.
16
16
  */
17
17
 
18
+ import { codexProviderOverrides, endpointAuthEnv } from "../custom-connection-launch";
18
19
  import { factoryFor, register, supportsExecutionProfile } from "./registry";
19
20
  import type { AgentSession, AgentSessionFactory } from "./types";
20
21
 
@@ -30,23 +31,7 @@ export function uaiGatewayEnv(
30
31
  url: string,
31
32
  taskToken: string,
32
33
  ): Record<string, string> {
33
- if (provider === "openai") {
34
- return {
35
- OPENAI_BASE_URL: url,
36
- OPENAI_API_KEY: taskToken,
37
- // Read by the `uai` model provider's `env_key` (see uaiCodexOverrides).
38
- UAI_INFERENCE_TOKEN: taskToken,
39
- };
40
- }
41
- return {
42
- ANTHROPIC_BASE_URL: url,
43
- ANTHROPIC_AUTH_TOKEN: taskToken,
44
- // Explicit blanks WIN over the claude adapter's passEnv inheritance
45
- // (same mechanism its communicator profile uses to scrub) — the host's
46
- // real credential must never ride a gateway-billed session.
47
- ANTHROPIC_API_KEY: "",
48
- CLAUDE_CODE_OAUTH_TOKEN: "",
49
- };
34
+ return endpointAuthEnv(provider, url, taskToken, "bearer", "UAI_INFERENCE_TOKEN");
50
35
  }
51
36
 
52
37
  /**
@@ -63,14 +48,7 @@ export function uaiGatewayEnv(
63
48
  */
64
49
  export function uaiCodexOverrides(url: string): string[] {
65
50
  const base = `${url.replace(/\/+$/, "")}/v1`;
66
- return [
67
- "model_provider=uai",
68
- 'model_providers.uai.name="Uai Managed Engine"',
69
- `model_providers.uai.base_url="${base}"`,
70
- 'model_providers.uai.env_key="UAI_INFERENCE_TOKEN"',
71
- 'model_providers.uai.wire_api="responses"',
72
- "model_providers.uai.requires_openai_auth=false",
73
- ];
51
+ return codexProviderOverrides(base, { provider: "uai", name: "Uai Managed Engine", envKey: "UAI_INFERENCE_TOKEN" });
74
52
  }
75
53
 
76
54
  const create: AgentSessionFactory["create"] = async (args): Promise<AgentSession> => {
@@ -34,7 +34,7 @@ export class AssignmentEvidenceJournal {
34
34
  const dir = openSync(dirname(this.path), "r");
35
35
  try { fsyncSync(dir); } finally { closeSync(dir); }
36
36
  }
37
- begin(input: { sessionId: string; engine: string; runEpoch: number; runtimeVersion: string | null; recipientGeneration: string | null; assignment?: AssignmentDelivery | null; replay?: boolean; leadRunId?: string | null }): (AssignmentTurnEvidence & { assignment: AssignmentDelivery | null }) | null {
37
+ begin(input: { actualAccount?: { credentialId: string; credentialRevision: string; connection: boolean } | null; sessionId: string; engine: string; runEpoch: number; runtimeVersion: string | null; recipientGeneration: string | null; assignment?: AssignmentDelivery | null; replay?: boolean; leadRunId?: string | null }): (AssignmentTurnEvidence & { assignment: AssignmentDelivery | null }) | null {
38
38
  const state = this.read(input.sessionId);
39
39
  if (state.current && (state.current.runEpoch !== input.runEpoch || state.current.recipientGeneration !== input.recipientGeneration)) {
40
40
  state.current = null;
@@ -59,6 +59,7 @@ export class AssignmentEvidenceJournal {
59
59
  ...(input.leadRunId ? { leadRunId: input.leadRunId } : {}),
60
60
  correlation: input.assignment || input.leadRunId ? "explicit" : current ? "followup" : "unknown",
61
61
  acceptedAt: Date.now(), engine: input.engine, runtimeVersion: input.runtimeVersion,
62
+ ...(input.actualAccount !== undefined ? { credentialId: input.actualAccount?.credentialId ?? null, credentialRevision: input.actualAccount?.credentialRevision ?? null, connection: input.actualAccount?.connection ?? null } : {}),
62
63
  };
63
64
  // Concurrent prompts aren't assumed to execute FIFO. Without a provider
64
65
  // turn/request join, every overlapping turn is explicitly unallocated.
@@ -26,6 +26,7 @@ import {
26
26
  type CaptureResultWire,
27
27
  type CredentialLocation,
28
28
  } from "../src/cloud-credentials";
29
+ import { parseCustomConnectionMaterial, type CustomConnectionMaterial } from "../src/custom-connections";
29
30
  import { ImmutableParameterAbortedError, storeFailureKind, type CredentialStore } from "./credential-store";
30
31
  import { laneFor } from "./token-lanes/registry";
31
32
  import {
@@ -48,7 +49,8 @@ export interface BrokerEngineValue {
48
49
 
49
50
  export type EngineCaptureMaterial =
50
51
  | { authKind: "env"; env: Record<string, string> }
51
- | { authKind: "config-dir"; files: CredentialFiles };
52
+ | { authKind: "config-dir"; files: CredentialFiles }
53
+ | { authKind: "custom-connection"; connection: CustomConnectionMaterial };
52
54
 
53
55
  export interface BuiltCapture {
54
56
  lease: string;
@@ -87,6 +89,17 @@ export function buildEngineCapture(
87
89
  return { error: "This capture is not an AI account." };
88
90
  }
89
91
  const kind = location.engineKind!;
92
+ if (material.authKind === "custom-connection") {
93
+ const connection = parseCustomConnectionMaterial(material.connection);
94
+ if (!connection || connection.engine !== kind || flow.brokerParameter !== null) {
95
+ return { error: "unsupported" };
96
+ }
97
+ return {
98
+ lease: serializeLeasedValue({ v: 1, part: "lease", location, class: "engine", authKind: "custom-connection", connection }),
99
+ broker: null,
100
+ expiresAt: connection.expiresAt ?? null,
101
+ };
102
+ }
90
103
  if (material.authKind === "env") {
91
104
  if (!onlyKeys(material.env, ENGINE_ENV_CREDENTIAL_KEYS[kind] ?? [])) {
92
105
  return { error: "This key is not one the engine reads." };
@@ -1,3 +1,4 @@
1
+ import type { CustomConnectionCombination } from "../src/custom-connections";
1
2
  import { parseTaskSecretMaterial, type TaskSecretManifestEntry, type TaskSecretMaterial } from "../src/task-secrets";
2
3
  /**
3
4
  * ADR-154 task-scoped credential leases.
@@ -137,6 +138,8 @@ export interface TaskStaticCredential<T extends LeasedValue> {
137
138
 
138
139
  /** A task-facing view. It contains material, but never STS credentials. */
139
140
  export interface TaskCredentialView {
141
+ connectionsEnabled?: readonly CustomConnectionCombination[];
142
+ connectionsUnsupported?: readonly string[];
140
143
  taskSecrets?: readonly { manifest: Readonly<TaskSecretManifestEntry>; material: Readonly<TaskSecretMaterial> | null }[];
141
144
  assignment: Readonly<TaskAssignment>;
142
145
  leaseId: string;
@@ -293,7 +296,7 @@ type CredentialDescriptor = {
293
296
  parameter: string | null;
294
297
  version: number | null;
295
298
  sessionId: string | null;
296
- authKind: "env" | "config-dir" | "oauth" | "token" | "none" | null;
299
+ authKind: "custom-connection" | "env" | "config-dir" | "oauth" | "token" | "none" | null;
297
300
  location: CredentialLocation | null;
298
301
  taskSecret?: { entry: TaskSecretManifestEntry; orgId: string; taskId: string };
299
302
  };
@@ -552,7 +555,8 @@ function freezeMaterial<T extends LeasedValue | TaskSecretMaterial>(material: T)
552
555
  Object.freeze(material.location);
553
556
  if (material.class === "engine") {
554
557
  if (material.authKind === "env") Object.freeze(material.env);
555
- else Object.freeze(material.files);
558
+ else if (material.authKind === "config-dir") Object.freeze(material.files);
559
+ else Object.freeze(material.connection);
556
560
  } else if (material.class === "mcp" && material.authKind === "token") {
557
561
  Object.freeze(material.header);
558
562
  }
@@ -581,7 +585,7 @@ function freezeView(
581
585
  throw new CredentialLeaseError("engine material is incomplete");
582
586
  }
583
587
  return Object.freeze({
584
- manifest: Object.freeze({ ...manifest }),
588
+ manifest: Object.freeze({ ...manifest, ...(manifest.connection ? { connection: Object.freeze({ ...manifest.connection, models: Object.freeze([...manifest.connection.models]) as unknown as string[] }) } : {}) }),
585
589
  availability: record.availability,
586
590
  material: record.material as Readonly<EngineLeasedValue> | null,
587
591
  observedVersion: record.observedVersion,
@@ -633,6 +637,8 @@ function freezeView(
633
637
  manifestRevision: lease.manifestRevision,
634
638
  manifestDigest: lease.manifestDigest,
635
639
  expiresAt,
640
+ ...(lease.manifest.connectionsEnabled ? { connectionsEnabled: Object.freeze([...lease.manifest.connectionsEnabled]) } : {}),
641
+ ...(lease.manifest.connectionsUnsupported ? { connectionsUnsupported: Object.freeze([...lease.manifest.connectionsUnsupported]) } : {}),
636
642
  projectIds: Object.freeze(lease.manifest.projects.map((project) => project.projectId)),
637
643
  ...(lease.manifest.taskSecrets?.length ? { taskSecrets: Object.freeze(lease.manifest.taskSecrets.map(manifest => {
638
644
  const record = requireRecord(manifest.credentialId);
@@ -1,3 +1,4 @@
1
+ import { isOwnedInternalConnectionRuntime } from "./internal-connections";
1
2
  import type { TaskSecretApplied } from "../src/task-secrets";
2
3
  import { openTaskSecrets, closeTaskSecrets } from "./credential-consumers/task-secrets";
3
4
  /**
@@ -149,6 +150,9 @@ export class AuthoritativeTaskFilesFailure extends Error {
149
150
 
150
151
  export interface CloudCredentialRuntimeDeps {
151
152
  runnerKind: "owned" | "task-machine";
153
+ /** Actual persisted provider, or the authoritative plan for a new runtime.
154
+ * Missing/unknown refuses internal credentials. Rechecked before opening. */
155
+ internalRuntimeProvider?(assignment: Readonly<TaskAssignment>): string | null | undefined;
152
156
  /** To the cloud, over the authenticated bridge. False: not sent now. */
153
157
  send(frame: LeaseRequestV1Frame | CredentialRefreshFrame | ProjectEnvReplicaV1AckFrame | TaskSecretApplied): boolean;
154
158
  /** Every engine of the run (and older) proven stopped in every runtime it
@@ -1071,6 +1075,8 @@ export class CloudCredentialRuntime {
1071
1075
 
1072
1076
  #openGate(assignment: Readonly<TaskAssignment>, view: TaskCredentialView): void {
1073
1077
  if (this.#shutDown) throw new CredentialLeaseUnavailableError("the runner is shutting down");
1078
+ if (view.engines.some(e => e.manifest.connection?.internal === true) &&
1079
+ (this.#deps.runnerKind !== "owned" || !isOwnedInternalConnectionRuntime(this.#deps.internalRuntimeProvider?.(assignment)))) throw new CredentialLeaseUnavailableError("unsupported");
1074
1080
  openTaskSecrets(assignment, view);
1075
1081
  openEngineGate(assignment, view);
1076
1082
  openMcpGate(assignment, view);
@@ -1261,6 +1267,8 @@ export class CloudCredentialRuntime {
1261
1267
  }
1262
1268
 
1263
1269
  #assertInstallCurrent(request: CredentialInstallRequest): void {
1270
+ if (request.next.engines.some(e => e.manifest.connection?.internal === true) &&
1271
+ (this.#deps.runnerKind !== "owned" || !isOwnedInternalConnectionRuntime(this.#deps.internalRuntimeProvider?.(request.assignment)))) throw new CredentialLeaseUnavailableError("unsupported");
1264
1272
  if (this.#shutDown) throw new CredentialLeaseUnavailableError("the runner is shutting down");
1265
1273
  if (!request.isCurrent()) {
1266
1274
  throw new CredentialLeaseUnavailableError("the grant was withdrawn while it was being installed");
@@ -0,0 +1,25 @@
1
+ /** Exact, session-scoped defense for diagnostics that reach host telemetry. */
2
+ const active = new Map<string, number>();
3
+
4
+ export function registerConnectionSecret(key: string): () => void {
5
+ if (!key) return () => {};
6
+ active.set(key, (active.get(key) ?? 0) + 1);
7
+ let released = false;
8
+ return () => {
9
+ if (released) return;
10
+ released = true;
11
+ const remaining = (active.get(key) ?? 1) - 1;
12
+ if (remaining) active.set(key, remaining);
13
+ else active.delete(key);
14
+ };
15
+ }
16
+
17
+ export function redactActiveConnectionSecrets(text: string): string {
18
+ // Empty replacement cannot itself contain another active secret.
19
+ let previous: string;
20
+ do {
21
+ previous = text;
22
+ for (const key of active.keys()) text = text.split(key).join("");
23
+ } while (text !== previous);
24
+ return text;
25
+ }
@@ -146,6 +146,7 @@ export function forgetCredentialCleanupPending(
146
146
 
147
147
  /** An engine account's file, or the intermediate its writer stages
148
148
  * (`.uai-tmp`) before renaming it into place. */
149
+ const CONNECTION_AUTH_PATH = /^\/home\/node\/\.uai-connection\/[a-f0-9]{64}(?:\/(?:auth\.json|config\.toml|aws-config|aws-credentials))?$/;
149
150
  const ENGINE_AUTH_PATH = /^\/home\/node\/(?:\.codex(?:-acct-[A-Za-z0-9_-]+)?(?:-agent-[A-Za-z0-9_-]+)?|\.grok(?:-acct-[A-Za-z0-9_-]+)?|\.local\/share\/opencode(?:-acct-[A-Za-z0-9_-]+)?)\/auth\.json(?:\.uai-tmp)?$/;
150
151
  /** A file a writer stages (`.uai-tmp`) before renaming it into place. */
151
152
  function andStaged(...paths: string[]): string[] {
@@ -188,7 +189,7 @@ export function assertCredentialCleanupPath(
188
189
  throw new Error("credential cleanup path is invalid");
189
190
  }
190
191
  const allowed =
191
- (lane === "engine-auth" && ENGINE_AUTH_PATH.test(value)) ||
192
+ (lane === "engine-auth" && (ENGINE_AUTH_PATH.test(value) || CONNECTION_AUTH_PATH.test(value))) ||
192
193
  (lane === "github" && GITHUB_PATHS.has(value)) ||
193
194
  (lane === "ssh" && SSH_PATHS.has(value)) ||
194
195
  (lane === "files-session" && FILES_SESSION_PATHS.has(value));
@@ -1,3 +1,4 @@
1
+ import { validConnectionModels, type CustomConnectionCombination, type CustomConnectionLease, type CustomConnectionMaterial } from "../../src/custom-connections";
1
2
  /**
2
3
  * ADR-154 §6, §8, §10 on the runner: a task's AI accounts, from its lease.
3
4
  *
@@ -89,7 +90,9 @@ export interface LeasedEngineAccount {
89
90
  engineKind: string;
90
91
  scope: CredentialScope;
91
92
  label: string;
92
- authKind: "env" | "config-dir";
93
+ authKind: "env" | "config-dir" | "custom-connection";
94
+ connection?: CustomConnectionLease;
95
+ connectionMaterial?: CustomConnectionMaterial;
93
96
  /** The lease parameter's version this material was read at. */
94
97
  version: number;
95
98
  /** Env-token accounts: the variables the engine's spawn takes. */
@@ -109,14 +112,17 @@ export interface UnavailableEngineAccount {
109
112
  engineKind: string;
110
113
  scope: CredentialScope;
111
114
  label: string;
112
- authKind: "env" | "config-dir";
115
+ authKind: "env" | "config-dir" | "custom-connection";
113
116
  home: string | null;
117
+ connection?: CustomConnectionLease;
114
118
  unavailable: true;
115
119
  }
116
120
 
117
121
  export type ServedEngineAccount = LeasedEngineAccount | UnavailableEngineAccount;
118
122
 
119
123
  export interface TaskEngineLease {
124
+ connectionsEnabled?: readonly CustomConnectionCombination[];
125
+ connectionsUnsupported?: readonly string[];
120
126
  assignment: TaskAssignment;
121
127
  /** False while the registry's gate is closed: spawn nothing. */
122
128
  open: boolean;
@@ -142,6 +148,8 @@ interface WrittenHome {
142
148
  }
143
149
 
144
150
  interface TaskEngineState {
151
+ connectionsEnabled: readonly CustomConnectionCombination[];
152
+ connectionsUnsupported: readonly string[];
145
153
  assignment: TaskAssignment;
146
154
  /** The accounts of the view the gate opened on; null while it is closed. */
147
155
  served: ServedEngineAccount[] | null;
@@ -200,6 +208,7 @@ function accountOf(credential: TaskEngineCredential): ServedEngineAccount {
200
208
  label: manifest.label,
201
209
  authKind: manifest.authKind,
202
210
  home,
211
+ ...(manifest.connection ? { connection: manifest.connection } : {}),
203
212
  };
204
213
  if (
205
214
  credential.availability !== "available" ||
@@ -210,6 +219,13 @@ function accountOf(credential: TaskEngineCredential): ServedEngineAccount {
210
219
  ) {
211
220
  return { ...identity, unavailable: true };
212
221
  }
222
+ if (material.authKind === "custom-connection") {
223
+ const c = manifest.connection;
224
+ const m = material.connection;
225
+ const matches = c && (c.internal === true) === (m.internal === true) && c.providerType === m.providerType && c.authMode === m.authMode && c.host === new URL(m.baseUrl).host && c.region === m.region && c.transport === m.transport && (c.expiresAt ?? null) === (m.expiresAt ?? null) && m.engine === manifest.engineKind && validConnectionModels(c.models, c.defaultModel, c.backgroundModel, m.engine, m.apiKey);
226
+ if (!matches) return { ...identity, unavailable: true };
227
+ return { ...identity, version: credential.observedVersion, env: null, files: null, expiresAt: m.expiresAt ?? null, connectionMaterial: m };
228
+ }
213
229
  return material.authKind === "env"
214
230
  ? { ...identity, version: credential.observedVersion, env: { ...material.env }, files: null, expiresAt: null }
215
231
  : {
@@ -235,7 +251,8 @@ function sameAccount(a: ServedEngineAccount, b: ServedEngineAccount): boolean {
235
251
  a.credentialId === b.credentialId &&
236
252
  a.revision === b.revision &&
237
253
  a.engineKind === b.engineKind &&
238
- a.authKind === b.authKind
254
+ a.authKind === b.authKind &&
255
+ a.connection?.settingsHash === b.connection?.settingsHash
239
256
  );
240
257
  }
241
258
 
@@ -261,6 +278,8 @@ function stateFor(assignment: TaskAssignment): TaskEngineState {
261
278
  if (current) current.served = null;
262
279
  const fresh: TaskEngineState = {
263
280
  assignment: { taskId: assignment.taskId, runEpoch: assignment.runEpoch },
281
+ connectionsEnabled: [],
282
+ connectionsUnsupported: [],
264
283
  served: null,
265
284
  queue: Promise.resolve(),
266
285
  writing: 0,
@@ -312,6 +331,8 @@ export async function installEngineCredentials(request: CredentialInstallRequest
312
331
  // the runner's own store.
313
332
  const state = stateFor(request.assignment);
314
333
  const next = accountsOf(request.next);
334
+ state.connectionsEnabled = request.next.connectionsEnabled ?? [];
335
+ state.connectionsUnsupported = request.next.connectionsUnsupported ?? [];
315
336
  const floors = next.filter(isLeasedEngineAccount).flatMap((account) => {
316
337
  const served = state.exposed.get(exposureKey(account));
317
338
  return served !== undefined && account.version > served
@@ -350,6 +371,8 @@ export async function installEngineCredentials(request: CredentialInstallRequest
350
371
  export function openEngineGate(assignment: TaskAssignment, view: TaskCredentialView): void {
351
372
  if (olderRun(assignment)) return;
352
373
  const state = stateFor(assignment);
374
+ state.connectionsEnabled = view.connectionsEnabled ?? [];
375
+ state.connectionsUnsupported = view.connectionsUnsupported ?? [];
353
376
  state.served = accountsOf(view);
354
377
  expose(state, state.served);
355
378
  notify(assignment.taskId);
@@ -444,7 +467,7 @@ export function engineVersionHeldBack(
444
467
  export function leasedEnginesFor(taskId: string): TaskEngineLease | undefined {
445
468
  const state = tasks.get(taskId);
446
469
  if (!state) return undefined;
447
- return { assignment: { ...state.assignment }, open: state.served !== null, accounts: [...(state.served ?? [])] };
470
+ return { assignment: { ...state.assignment }, open: state.served !== null, accounts: [...(state.served ?? [])], connectionsEnabled: state.connectionsEnabled, ...(state.connectionsUnsupported.length ? { connectionsUnsupported: state.connectionsUnsupported } : {}) };
448
471
  }
449
472
 
450
473
  /** The live, open record for this exact run, or a failure: never "none". */
@@ -483,7 +506,7 @@ export function waitForEngineGate(
483
506
  return new CredentialLeaseUnavailableError("the task moved on to another run");
484
507
  }
485
508
  if (!sameAssignment(state.assignment, assignment) || !state.served) return "closed";
486
- return { assignment: { ...state.assignment }, open: true, accounts: [...state.served] };
509
+ return { assignment: { ...state.assignment }, open: true, accounts: [...state.served], connectionsEnabled: state.connectionsEnabled, ...(state.connectionsUnsupported.length ? { connectionsUnsupported: state.connectionsUnsupported } : {}) };
487
510
  };
488
511
  const now = check();
489
512
  if (now instanceof Error) return Promise.reject(now);
@@ -533,6 +556,7 @@ function homeOf(account: LeasedEngineAccount, home: string | undefined): string
533
556
  * a home derived from it). Never the files' content.
534
557
  */
535
558
  export function engineSpawnEnv(account: LeasedEngineAccount, home?: string): Record<string, string> {
559
+ if (account.authKind === "custom-connection") throw new Error("a connection requires its launch builder");
536
560
  if (account.authKind === "env") return { ...(account.env ?? {}) };
537
561
  const selector = ENGINE_ACCOUNT_HOMES[account.engineKind];
538
562
  if (!selector) throw new Error("the account's engine reads no file");