fix: render registry workspaces for harness

This commit is contained in:
2026-08-05 16:03:45 +02:00
parent ece9cfda50
commit bd798b1c96
21 changed files with 659 additions and 69 deletions
+31 -7
View File
@@ -2,7 +2,7 @@ import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams } from "node:ch
import type { AppConfig } from "../config.js";
import { RpcClient } from "../rpc/rpc-client.js";
import { SessionBridge } from "../bridge/session-bridge.js";
import type { ThtRunner } from "../tht/tht-runner.js";
import type { RuntimeConfigLease, ThtRunner } from "../tht/tht-runner.js";
import { buildPiChildEnv, canonicalPiProvider } from "./provider-credentials.js";
import { loadPiAuthProviders } from "./auth-providers.js";
import { secretValue } from "../config/secret-bundle.js";
@@ -14,6 +14,7 @@ export interface SessionRuntime {
bridge: SessionBridge;
child: ChildProcessWithoutNullStreams;
ownerKey?: string;
releaseRuntimeConfig?: () => void;
}
export interface RuntimeOptions {
@@ -24,6 +25,7 @@ export interface RuntimeOptions {
question?: string;
mode?: "new" | "resume";
principal?: PrincipalContext;
runtimeConfig?: RuntimeConfigLease;
}
/** Injectable child-process boundary; callbacks may ignore arguments in simpler tests. */
@@ -37,6 +39,7 @@ export class PiProcessManager {
private runtimes = new Map<string, SessionRuntime>();
private spawnFn: (
sessionId: string, author: string, provider: string | undefined, principal?: PrincipalContext,
runtimeConfigPath?: string,
) => ChildProcessWithoutNullStreams;
private loadAuthProviders: (agentDir: string) => ReadonlySet<string>;
@@ -47,16 +50,17 @@ export class PiProcessManager {
this.loadAuthProviders = opts?.authProviders
?? ((agentDir) => loadPiAuthProviders({ agentDir }));
if (opts?.spawnFn) {
this.spawnFn = (sessionId, author, provider, principal) =>
this.spawnPi(opts.spawnFn!, sessionId, author, provider, principal);
this.spawnFn = (sessionId, author, provider, principal, runtimeConfigPath) =>
this.spawnPi(opts.spawnFn!, sessionId, author, provider, principal, runtimeConfigPath);
} else {
this.spawnFn = (sessionId, author, provider, principal) =>
this.spawnPi(nodeSpawn, sessionId, author, provider, principal);
this.spawnFn = (sessionId, author, provider, principal, runtimeConfigPath) =>
this.spawnPi(nodeSpawn, sessionId, author, provider, principal, runtimeConfigPath);
}
}
private spawnPi(
spawnFn: SpawnFn, sessionId: string, author: string, provider: string | undefined, principal?: PrincipalContext,
spawnFn: SpawnFn, sessionId: string, author: string, provider: string | undefined,
principal?: PrincipalContext, runtimeConfigPath?: string,
): ChildProcessWithoutNullStreams {
// This is the final shared boundary for createFor(), spawnFor(), and resume(). Validate
// before auth-provider inspection, then make Pi consume the exact copied bytes rather than
@@ -94,6 +98,7 @@ export class PiProcessManager {
}
delete env.THT_DATA_ROOT;
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
if (runtimeConfigPath !== undefined) env.THT_CONFIG = runtimeConfigPath;
// pi 0.73 removed `--approve`: rpc mode is headless and its argv is intentionally minimal.
child = spawnFn(this.cfg.piBin, ["--mode", "rpc"], {
cwd: this.cfg.harnessDir,
@@ -132,15 +137,31 @@ export class PiProcessManager {
// SIGTERM to the in-flight Pi process and lose its pending gate.
const existing = this.runtimes.get(sessionId);
if (existing) {
o.runtimeConfig?.release();
throw new Error(`session runtime already active: ${sessionId}`);
}
if (o.principal) this.teardownForPrincipal(o.principal);
if (this.runtimes.size >= this.cfg.maxPiProcesses) {
o.runtimeConfig?.release();
throw new Error("max Pi processes reached");
}
const author = o.author ?? "dev@local";
const provider = canonicalPiProvider(o.provider ?? this.cfg.defaults.provider);
const child = this.spawnFn(sessionId, author, provider, o.principal);
let child: ChildProcessWithoutNullStreams;
try {
child = this.spawnFn(sessionId, author, provider, o.principal, o.runtimeConfig?.path);
} catch (error) {
o.runtimeConfig?.release();
throw error;
}
let runtimeConfigReleased = false;
const releaseRuntimeConfig = () => {
if (runtimeConfigReleased) return;
runtimeConfigReleased = true;
o.runtimeConfig?.release();
};
child.once("exit", releaseRuntimeConfig);
child.once("close", releaseRuntimeConfig);
let rt: SessionRuntime | undefined;
try {
const rpc = new RpcClient(child);
@@ -150,6 +171,7 @@ export class PiProcessManager {
bridge,
child,
ownerKey: o.principal ? `${o.principal.issuer}\0${o.principal.subject}` : undefined,
...(o.runtimeConfig ? { releaseRuntimeConfig } : {}),
};
rt = runtime;
bridge.beginTurn();
@@ -184,6 +206,7 @@ export class PiProcessManager {
return runtime;
} catch (error) {
if (rt && this.runtimes.get(sessionId) === rt) this.runtimes.delete(sessionId);
releaseRuntimeConfig();
try { child.kill(); } catch { /* preserve the initialization error */ }
throw error;
}
@@ -251,6 +274,7 @@ export class PiProcessManager {
// Delete before signalling the child so its asynchronous exit cannot be mistaken for a
// crash, and so a replacement installed by a later lifecycle operation is never targeted.
this.runtimes.delete(id);
expected.releaseRuntimeConfig?.();
expected.child.kill();
return true;
}