@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.
- package/lib/agents/claude.ts +13 -0
- package/lib/agents/codex.ts +11 -5
- package/lib/agents/session-state.ts +77 -0
- package/lib/agents/transport.ts +2 -0
- package/lib/agents/types.ts +3 -0
- package/lib/agents/uai.ts +3 -25
- package/lib/assignment-evidence.ts +2 -1
- package/lib/capture-flow.ts +14 -1
- package/lib/cloud-credential-leases.ts +9 -3
- package/lib/cloud-credential-runtime.ts +8 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/credential-cleanup-inventory.ts +2 -1
- package/lib/credential-consumers/engines.ts +29 -5
- package/lib/credentials-activation-authority.ts +158 -10
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +299 -0
- package/lib/custom-connection-runtime.ts +70 -0
- package/lib/engine-accounts.ts +1 -1
- package/lib/engine-credential-env.ts +2 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/legacy-runtime-retirement.ts +43 -8
- package/lib/obs.ts +2 -1
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +308 -26
- package/lib/secrets-proxy/address.ts +23 -0
- package/lib/task-credential-cleanup.ts +13 -3
- package/package.json +1 -1
- package/runner/runner.mjs +231 -3
- package/src/capture-wire.ts +102 -0
- package/src/cloud-credentials.ts +36 -372
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +207 -0
- package/src/main.ts +116 -3
- package/src/protocol.ts +111 -0
package/lib/agents/claude.ts
CHANGED
|
@@ -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
|
});
|
package/lib/agents/codex.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/lib/agents/transport.ts
CHANGED
|
@@ -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
|
package/lib/agents/types.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/lib/capture-flow.ts
CHANGED
|
@@ -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");
|