@cat-factory/local-server 0.15.0 → 0.17.0
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/dist/LocalContainerRunnerTransport.d.ts +97 -9
- package/dist/LocalContainerRunnerTransport.d.ts.map +1 -1
- package/dist/LocalContainerRunnerTransport.js +355 -93
- package/dist/LocalContainerRunnerTransport.js.map +1 -1
- package/dist/LocalProcessRunnerTransport.d.ts +63 -0
- package/dist/LocalProcessRunnerTransport.d.ts.map +1 -0
- package/dist/LocalProcessRunnerTransport.js +188 -0
- package/dist/LocalProcessRunnerTransport.js.map +1 -0
- package/dist/NativeRoutingRunnerTransport.d.ts +20 -0
- package/dist/NativeRoutingRunnerTransport.d.ts.map +1 -0
- package/dist/NativeRoutingRunnerTransport.js +50 -0
- package/dist/NativeRoutingRunnerTransport.js.map +1 -0
- package/dist/container.d.ts +13 -0
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +165 -22
- package/dist/container.js.map +1 -1
- package/dist/harnessHttp.d.ts +58 -0
- package/dist/harnessHttp.d.ts.map +1 -0
- package/dist/harnessHttp.js +95 -0
- package/dist/harnessHttp.js.map +1 -0
- package/dist/runtimes/appleContainerRuntime.d.ts +3 -0
- package/dist/runtimes/appleContainerRuntime.d.ts.map +1 -1
- package/dist/runtimes/appleContainerRuntime.js +8 -1
- package/dist/runtimes/appleContainerRuntime.js.map +1 -1
- package/dist/runtimes/containerRuntime.d.ts +30 -1
- package/dist/runtimes/containerRuntime.d.ts.map +1 -1
- package/dist/runtimes/containerRuntime.js +5 -0
- package/dist/runtimes/containerRuntime.js.map +1 -1
- package/dist/runtimes/dockerRuntime.d.ts +4 -0
- package/dist/runtimes/dockerRuntime.d.ts.map +1 -1
- package/dist/runtimes/dockerRuntime.js +21 -2
- package/dist/runtimes/dockerRuntime.js.map +1 -1
- package/dist/runtimes/index.d.ts.map +1 -1
- package/dist/runtimes/index.js +1 -0
- package/dist/runtimes/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -14
- package/dist/server.js.map +1 -1
- package/package.json +9 -8
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { RunnerDispatchKind, RunnerJobRef, RunnerJobView, RunnerTransport } from '@cat-factory/kernel';
|
|
3
|
+
export interface LocalProcessRunnerTransportOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Path to the executor-harness HTTP server entry (its `server.js`/`server.ts`). Spawned
|
|
6
|
+
* as `node <entry>`; with a `.ts` entry, Node's type-stripping (Node 24+) runs it.
|
|
7
|
+
*/
|
|
8
|
+
harnessEntry: string;
|
|
9
|
+
/** Node executable to spawn the harness with. Default `process.execPath`. */
|
|
10
|
+
nodePath?: string;
|
|
11
|
+
/** Extra args to pass to node before the entry (e.g. `--experimental-strip-types`). */
|
|
12
|
+
nodeArgs?: string[];
|
|
13
|
+
/** Shared secret injected as `HARNESS_SHARED_SECRET` + sent on every call. Default random. */
|
|
14
|
+
sharedSecret?: string;
|
|
15
|
+
/** Extra env for the harness process (e.g. GITHUB_ALLOWED_HOSTS). */
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
/** Injectable fetch — defaults to the global. */
|
|
18
|
+
fetchImpl?: typeof fetch;
|
|
19
|
+
/** Injectable spawn — defaults to node:child_process.spawn (overridable in tests). */
|
|
20
|
+
spawnImpl?: typeof spawn;
|
|
21
|
+
/** Injectable free-port picker — defaults to an ephemeral OS port (overridable in tests). */
|
|
22
|
+
pickPort?: () => Promise<number>;
|
|
23
|
+
/** How long to wait for the harness `/health` after spawn. Default 30s. */
|
|
24
|
+
readyTimeoutMs?: number;
|
|
25
|
+
/** Per-HTTP-call timeout. Default 30s. */
|
|
26
|
+
requestTimeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class LocalProcessRunnerTransport implements RunnerTransport {
|
|
29
|
+
private readonly harnessEntry;
|
|
30
|
+
private readonly nodePath;
|
|
31
|
+
private readonly nodeArgs;
|
|
32
|
+
private readonly sharedSecret;
|
|
33
|
+
private readonly extraEnv;
|
|
34
|
+
private readonly fetchImpl;
|
|
35
|
+
private readonly spawnImpl;
|
|
36
|
+
private readonly pickPort;
|
|
37
|
+
private readonly readyTimeoutMs;
|
|
38
|
+
private readonly requestTimeoutMs;
|
|
39
|
+
/** The single long-lived harness process, started lazily and reused across all runs. */
|
|
40
|
+
private proc;
|
|
41
|
+
private starting;
|
|
42
|
+
constructor(options: LocalProcessRunnerTransportOptions);
|
|
43
|
+
dispatch(ref: RunnerJobRef, spec: Record<string, unknown>, kind?: RunnerDispatchKind): Promise<void>;
|
|
44
|
+
poll(ref: RunnerJobRef): Promise<RunnerJobView>;
|
|
45
|
+
/**
|
|
46
|
+
* No per-run teardown: the harness host process is long-lived and reused across runs
|
|
47
|
+
* (the harness already removes each job's ephemeral workspace itself). Provided so the
|
|
48
|
+
* port contract is satisfied; kept idempotent.
|
|
49
|
+
*/
|
|
50
|
+
release(): Promise<void>;
|
|
51
|
+
/** Stop the harness process (for shutdown / tests). Idempotent. */
|
|
52
|
+
shutdown(): Promise<void>;
|
|
53
|
+
private ensureProcess;
|
|
54
|
+
private startProcess;
|
|
55
|
+
private waitForHealth;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a {@link LocalProcessRunnerTransport} from the environment. Requires
|
|
59
|
+
* `LOCAL_HARNESS_ENTRY` (the path to the executor-harness server entry to run as a host
|
|
60
|
+
* process). The native CLIs (`claude` / `codex`) must already be installed on the host.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createLocalProcessTransportFromEnv(env: NodeJS.ProcessEnv): LocalProcessRunnerTransport;
|
|
63
|
+
//# sourceMappingURL=LocalProcessRunnerTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalProcessRunnerTransport.d.ts","sourceRoot":"","sources":["../src/LocalProcessRunnerTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAG7D,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,qBAAqB,CAAA;AA0B5B,MAAM,WAAW,kCAAkC;IACjD;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,sFAAsF;IACtF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAeD,qBAAa,2BAA4B,YAAW,eAAe;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IAEzC,wFAAwF;IACxF,OAAO,CAAC,IAAI,CAAoE;IAChF,OAAO,CAAC,QAAQ,CAA6E;IAE7F,YAAY,OAAO,EAAE,kCAAkC,EAWtD;IAEK,QAAQ,CACZ,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,GAAE,kBAA4B,GACjC,OAAO,CAAC,IAAI,CAAC,CAYf;IAEK,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAapD;IAED;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAE7B;IAED,mEAAmE;IAC7D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAK9B;YAIa,aAAa;YAWb,YAAY;IAoC1B,OAAO,CAAC,aAAa;CAYtB;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,CAChD,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,2BAA2B,CAmB7B"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { EVICTION_ERROR, pollHarnessJob, postHarnessJob, waitForHarnessHealth, } from './harnessHttp.js';
|
|
5
|
+
// The NATIVE local runner backend (opt-in via `LOCAL_NATIVE_AGENTS`): instead of a Docker
|
|
6
|
+
// container per run, it runs the SAME executor-harness as a long-lived HOST PROCESS on
|
|
7
|
+
// 127.0.0.1 and drives it through the harness's existing HTTP API. So all the harness
|
|
8
|
+
// machinery — git clone/push/PR, structured-output, watchdogs, the JobRegistry, progress —
|
|
9
|
+
// is reused unchanged; the only difference from the container transport is WHERE the harness
|
|
10
|
+
// runs (a host `node` process vs a container) and that the agent uses the developer's OWN
|
|
11
|
+
// installed `claude` / `codex` CLI with its ambient login (the executor sets `ambientAuth`
|
|
12
|
+
// on the job, so no credential is leased). This bypasses Docker entirely.
|
|
13
|
+
//
|
|
14
|
+
// SECURITY: the agent runs as a plain host subprocess with the developer's full shell/file
|
|
15
|
+
// access and their personal subscription — no container sandbox, no spend metering, no
|
|
16
|
+
// model-locking. Acceptable ONLY because local mode is the developer's own machine; it is
|
|
17
|
+
// therefore opt-in (default off) and reachable only from `buildLocalContainer`.
|
|
18
|
+
/** The harness is always on loopback for the native host-process transport. */
|
|
19
|
+
const endpointFor = (port) => ({ host: '127.0.0.1', port });
|
|
20
|
+
/** An ephemeral free localhost port (best-effort; a tiny TOCTOU window is fine for dev). */
|
|
21
|
+
function ephemeralPort() {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const srv = createServer();
|
|
24
|
+
srv.on('error', reject);
|
|
25
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
26
|
+
const addr = srv.address();
|
|
27
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
28
|
+
srv.close(() => (port ? resolve(port) : reject(new Error('could not pick a free port'))));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export class LocalProcessRunnerTransport {
|
|
33
|
+
harnessEntry;
|
|
34
|
+
nodePath;
|
|
35
|
+
nodeArgs;
|
|
36
|
+
sharedSecret;
|
|
37
|
+
extraEnv;
|
|
38
|
+
fetchImpl;
|
|
39
|
+
spawnImpl;
|
|
40
|
+
pickPort;
|
|
41
|
+
readyTimeoutMs;
|
|
42
|
+
requestTimeoutMs;
|
|
43
|
+
/** The single long-lived harness process, started lazily and reused across all runs. */
|
|
44
|
+
proc;
|
|
45
|
+
starting;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.harnessEntry = options.harnessEntry;
|
|
48
|
+
this.nodePath = options.nodePath ?? process.execPath;
|
|
49
|
+
this.nodeArgs = options.nodeArgs ?? [];
|
|
50
|
+
this.sharedSecret = options.sharedSecret ?? randomBytes(24).toString('hex');
|
|
51
|
+
this.extraEnv = options.env ?? {};
|
|
52
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
53
|
+
this.spawnImpl = options.spawnImpl ?? spawn;
|
|
54
|
+
this.pickPort = options.pickPort ?? ephemeralPort;
|
|
55
|
+
this.readyTimeoutMs = options.readyTimeoutMs ?? 30_000;
|
|
56
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
57
|
+
}
|
|
58
|
+
async dispatch(ref, spec, kind = 'agent') {
|
|
59
|
+
const proc = await this.ensureProcess();
|
|
60
|
+
// The harness keys jobs by the per-step `ref.jobId` in the body; a re-dispatch
|
|
61
|
+
// (durable-driver replay) re-POSTs, which the JobRegistry treats as a re-attach.
|
|
62
|
+
await postHarnessJob({
|
|
63
|
+
fetchImpl: this.fetchImpl,
|
|
64
|
+
endpoint: endpointFor(proc.port),
|
|
65
|
+
secret: this.sharedSecret,
|
|
66
|
+
body: { ...spec, kind },
|
|
67
|
+
timeoutMs: this.requestTimeoutMs,
|
|
68
|
+
label: 'Native harness',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async poll(ref) {
|
|
72
|
+
const proc = this.proc;
|
|
73
|
+
// The process died (or was never started) → report an eviction so the run can recover.
|
|
74
|
+
if (!proc || proc.exited)
|
|
75
|
+
return { state: 'failed', error: EVICTION_ERROR };
|
|
76
|
+
return pollHarnessJob({
|
|
77
|
+
fetchImpl: this.fetchImpl,
|
|
78
|
+
endpoint: endpointFor(proc.port),
|
|
79
|
+
jobId: ref.jobId,
|
|
80
|
+
secret: this.sharedSecret,
|
|
81
|
+
timeoutMs: this.requestTimeoutMs,
|
|
82
|
+
label: 'Native harness',
|
|
83
|
+
isDead: () => proc.exited,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* No per-run teardown: the harness host process is long-lived and reused across runs
|
|
88
|
+
* (the harness already removes each job's ephemeral workspace itself). Provided so the
|
|
89
|
+
* port contract is satisfied; kept idempotent.
|
|
90
|
+
*/
|
|
91
|
+
async release() {
|
|
92
|
+
// intentionally a no-op
|
|
93
|
+
}
|
|
94
|
+
/** Stop the harness process (for shutdown / tests). Idempotent. */
|
|
95
|
+
async shutdown() {
|
|
96
|
+
const proc = this.proc;
|
|
97
|
+
this.proc = undefined;
|
|
98
|
+
this.starting = undefined;
|
|
99
|
+
if (proc && !proc.exited)
|
|
100
|
+
proc.child.kill();
|
|
101
|
+
}
|
|
102
|
+
// --- internals ----------------------------------------------------------
|
|
103
|
+
async ensureProcess() {
|
|
104
|
+
if (this.proc && !this.proc.exited)
|
|
105
|
+
return this.proc;
|
|
106
|
+
this.starting ??= this.startProcess();
|
|
107
|
+
try {
|
|
108
|
+
this.proc = await this.starting;
|
|
109
|
+
return this.proc;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.starting = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async startProcess() {
|
|
116
|
+
const port = await this.pickPort();
|
|
117
|
+
const child = this.spawnImpl(this.nodePath, [...this.nodeArgs, this.harnessEntry], {
|
|
118
|
+
env: {
|
|
119
|
+
...process.env,
|
|
120
|
+
...this.extraEnv,
|
|
121
|
+
PORT: String(port),
|
|
122
|
+
HARNESS_SHARED_SECRET: this.sharedSecret,
|
|
123
|
+
// The harness only auto-listens when NODE_ENV !== 'test'.
|
|
124
|
+
NODE_ENV: 'production',
|
|
125
|
+
},
|
|
126
|
+
stdio: 'ignore',
|
|
127
|
+
});
|
|
128
|
+
const handle = { child, port, exited: false };
|
|
129
|
+
// The harness child is long-lived and not detached, but Node does NOT auto-kill a
|
|
130
|
+
// child when the parent exits — without this, every dev restart orphans a `node
|
|
131
|
+
// <harness>` process still bound to its port (and possibly mid-run on the developer's
|
|
132
|
+
// live Claude/Codex login). `shutdown()` covers the graceful path; this `exit` hook is
|
|
133
|
+
// the backstop for SIGTERM/SIGINT/uncaught exits that reach `process.exit` directly.
|
|
134
|
+
const killOnParentExit = () => {
|
|
135
|
+
try {
|
|
136
|
+
child.kill();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// best-effort
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
process.once('exit', killOnParentExit);
|
|
143
|
+
child.on('exit', () => {
|
|
144
|
+
handle.exited = true;
|
|
145
|
+
process.removeListener('exit', killOnParentExit);
|
|
146
|
+
if (this.proc === handle)
|
|
147
|
+
this.proc = undefined;
|
|
148
|
+
});
|
|
149
|
+
await this.waitForHealth(port, handle);
|
|
150
|
+
return handle;
|
|
151
|
+
}
|
|
152
|
+
waitForHealth(port, handle) {
|
|
153
|
+
return waitForHarnessHealth({
|
|
154
|
+
fetchImpl: this.fetchImpl,
|
|
155
|
+
endpoint: endpointFor(port),
|
|
156
|
+
readyTimeoutMs: this.readyTimeoutMs,
|
|
157
|
+
requestTimeoutMs: this.requestTimeoutMs,
|
|
158
|
+
intervalMs: 200,
|
|
159
|
+
isDead: () => handle.exited,
|
|
160
|
+
deadError: 'the native harness process exited before becoming healthy',
|
|
161
|
+
timeoutError: `Timed out waiting for the native harness on 127.0.0.1:${port} to become healthy`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build a {@link LocalProcessRunnerTransport} from the environment. Requires
|
|
167
|
+
* `LOCAL_HARNESS_ENTRY` (the path to the executor-harness server entry to run as a host
|
|
168
|
+
* process). The native CLIs (`claude` / `codex`) must already be installed on the host.
|
|
169
|
+
*/
|
|
170
|
+
export function createLocalProcessTransportFromEnv(env) {
|
|
171
|
+
const harnessEntry = env.LOCAL_HARNESS_ENTRY?.trim();
|
|
172
|
+
if (!harnessEntry) {
|
|
173
|
+
throw new Error('LOCAL_HARNESS_ENTRY is required for native local mode (LOCAL_NATIVE_AGENTS): set it to ' +
|
|
174
|
+
'the executor-harness server entry path (its built server.js, or src/server.ts run via ' +
|
|
175
|
+
'Node type-stripping).');
|
|
176
|
+
}
|
|
177
|
+
const nodeArgs = env.LOCAL_HARNESS_NODE_ARGS?.trim()
|
|
178
|
+
? env.LOCAL_HARNESS_NODE_ARGS.trim().split(/\s+/)
|
|
179
|
+
: undefined;
|
|
180
|
+
const allowedHosts = env.GITHUB_ALLOWED_HOSTS?.trim();
|
|
181
|
+
return new LocalProcessRunnerTransport({
|
|
182
|
+
harnessEntry,
|
|
183
|
+
...(nodeArgs ? { nodeArgs } : {}),
|
|
184
|
+
sharedSecret: env.HARNESS_SHARED_SECRET?.trim() || undefined,
|
|
185
|
+
...(allowedHosts ? { env: { GITHUB_ALLOWED_HOSTS: allowedHosts } } : {}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=LocalProcessRunnerTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalProcessRunnerTransport.js","sourceRoot":"","sources":["../src/LocalProcessRunnerTransport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RunnerDispatchKind, RunnerDispatchOptions, RunnerJobRef, RunnerJobView, RunnerTransport } from '@cat-factory/kernel';
|
|
2
|
+
export declare class NativeRoutingRunnerTransport implements RunnerTransport {
|
|
3
|
+
/** Host-process transport for ambient (native CLI) steps — built lazily, cached. */
|
|
4
|
+
private readonly ambient;
|
|
5
|
+
/** Per-run container transport for everything else — built lazily, cached (the
|
|
6
|
+
* container transport resolves its pool config from the DB, so this may be async). */
|
|
7
|
+
private readonly managed;
|
|
8
|
+
/** ref → the transport that handled its dispatch, so poll/release hit the same backend. */
|
|
9
|
+
private readonly routed;
|
|
10
|
+
constructor(
|
|
11
|
+
/** Host-process transport for ambient (native CLI) steps — built lazily, cached. */
|
|
12
|
+
ambient: () => RunnerTransport | Promise<RunnerTransport>,
|
|
13
|
+
/** Per-run container transport for everything else — built lazily, cached (the
|
|
14
|
+
* container transport resolves its pool config from the DB, so this may be async). */
|
|
15
|
+
managed: () => RunnerTransport | Promise<RunnerTransport>);
|
|
16
|
+
dispatch(ref: RunnerJobRef, spec: Record<string, unknown>, kind?: RunnerDispatchKind, options?: RunnerDispatchOptions): Promise<void>;
|
|
17
|
+
poll(ref: RunnerJobRef): Promise<RunnerJobView>;
|
|
18
|
+
release(ref: RunnerJobRef): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=NativeRoutingRunnerTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeRoutingRunnerTransport.d.ts","sourceRoot":"","sources":["../src/NativeRoutingRunnerTransport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAqB5B,qBAAa,4BAA6B,YAAW,eAAe;IAKhE,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB;0FACsF;IACtF,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAE5D;IACE,oFAAoF;IACnE,OAAO,EAAE,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAC1E;0FACsF;IACrE,OAAO,EAAE,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,EACxE;IAEE,QAAQ,CACZ,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,GAAE,kBAA4B,EAClC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAIf;IAEK,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAMpD;IAEK,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9C;CACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// NATIVE-MODE transport router (local facade, `LOCAL_NATIVE_AGENTS`): native mode runs the
|
|
2
|
+
// developer's OWN `claude` / `codex` CLI as a host process (no sandbox), which is only
|
|
3
|
+
// appropriate for the steps that actually use that ambient login. The executor flags such a
|
|
4
|
+
// step with `ambientAuth: true` on its job body; everything else (a proxy/`pi` model, or a
|
|
5
|
+
// non-native vendor reusing the claude-code harness) MUST still run in a sandboxed per-run
|
|
6
|
+
// container, exactly as the README promises ("proxy-only models still need the container
|
|
7
|
+
// path"). Previously native mode sent EVERY dispatch to the host process, silently running
|
|
8
|
+
// proxy-model steps unsandboxed.
|
|
9
|
+
//
|
|
10
|
+
// So this routes per JOB (not per run — a single run legitimately mixes an ambient Claude
|
|
11
|
+
// step with a proxy step): `ambientAuth` jobs → the host-process transport; the rest → the
|
|
12
|
+
// container transport (built lazily, so a native deployment that only runs Claude/Codex
|
|
13
|
+
// never needs LOCAL_HARNESS_IMAGE; a proxy step without an image fails loudly there). The
|
|
14
|
+
// chosen transport is remembered per ref so poll/release reach the same backend.
|
|
15
|
+
const EVICTION_ERROR = 'Job not found (container evicted or crashed)';
|
|
16
|
+
const refKey = (ref) => `${ref.runId}:${ref.jobId}`;
|
|
17
|
+
export class NativeRoutingRunnerTransport {
|
|
18
|
+
ambient;
|
|
19
|
+
managed;
|
|
20
|
+
/** ref → the transport that handled its dispatch, so poll/release hit the same backend. */
|
|
21
|
+
routed = new Map();
|
|
22
|
+
constructor(
|
|
23
|
+
/** Host-process transport for ambient (native CLI) steps — built lazily, cached. */
|
|
24
|
+
ambient,
|
|
25
|
+
/** Per-run container transport for everything else — built lazily, cached (the
|
|
26
|
+
* container transport resolves its pool config from the DB, so this may be async). */
|
|
27
|
+
managed) {
|
|
28
|
+
this.ambient = ambient;
|
|
29
|
+
this.managed = managed;
|
|
30
|
+
}
|
|
31
|
+
async dispatch(ref, spec, kind = 'agent', options) {
|
|
32
|
+
const transport = await (spec.ambientAuth === true ? this.ambient() : this.managed());
|
|
33
|
+
this.routed.set(refKey(ref), transport);
|
|
34
|
+
await transport.dispatch(ref, spec, kind, options);
|
|
35
|
+
}
|
|
36
|
+
async poll(ref) {
|
|
37
|
+
// Unknown ref (a fresh process after a durable replay): report an eviction so the
|
|
38
|
+
// sweeper re-drives — the re-dispatch (idempotent) re-populates the routing map.
|
|
39
|
+
const transport = this.routed.get(refKey(ref));
|
|
40
|
+
if (!transport)
|
|
41
|
+
return { state: 'failed', error: EVICTION_ERROR };
|
|
42
|
+
return transport.poll(ref);
|
|
43
|
+
}
|
|
44
|
+
async release(ref) {
|
|
45
|
+
const transport = this.routed.get(refKey(ref));
|
|
46
|
+
this.routed.delete(refKey(ref));
|
|
47
|
+
await transport?.release?.(ref);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=NativeRoutingRunnerTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeRoutingRunnerTransport.js","sourceRoot":"","sources":["../src/NativeRoutingRunnerTransport.ts"],"names":[],"mappings":"AAQA,2FAA2F;AAC3F,uFAAuF;AACvF,4FAA4F;AAC5F,2FAA2F;AAC3F,2FAA2F;AAC3F,yFAAyF;AACzF,2FAA2F;AAC3F,iCAAiC;AACjC,EAAE;AACF,0FAA0F;AAC1F,2FAA2F;AAC3F,wFAAwF;AACxF,0FAA0F;AAC1F,iFAAiF;AAEjF,MAAM,cAAc,GAAG,8CAA8C,CAAA;AAErE,MAAM,MAAM,GAAG,CAAC,GAAiB,EAAU,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,EAAE,CAAA;AAEzE,MAAM,OAAO,4BAA4B;IAMpB,OAAO;IAGP,OAAO;IAR1B,2FAA2F;IAC1E,MAAM,GAAG,IAAI,GAAG,EAA2B,CAAA;IAE5D;IACE,oFAAoF;IACnE,OAAyD;IAC1E;0FACsF;IACrE,OAAyD;uBAHzD,OAAO;uBAGP,OAAO;IACvB,CAAC;IAEJ,KAAK,CAAC,QAAQ,CACZ,GAAiB,EACjB,IAA6B,EAC7B,IAAI,GAAuB,OAAO,EAClC,OAA+B;QAE/B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QACrF,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,CAAA;QACvC,MAAM,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IACpD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAiB;QAC1B,kFAAkF;QAClF,iFAAiF;QACjF,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC9C,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,CAAA;QACjE,OAAO,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAiB;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC9C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC/B,MAAM,SAAS,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAAA;IACjC,CAAC;CACF"}
|
package/dist/container.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { NodeContainerOptions } from '@cat-factory/node-server';
|
|
2
2
|
import type { ServerContainer } from '@cat-factory/server';
|
|
3
|
+
import type { HarnessKind } from '@cat-factory/kernel';
|
|
3
4
|
export declare function buildLocalContainer(options: NodeContainerOptions): ServerContainer;
|
|
5
|
+
/**
|
|
6
|
+
* Parse `LOCAL_NATIVE_AGENTS` into the set of subscription harnesses to run natively. The
|
|
7
|
+
* documented form is a comma-separated list of harness ids (`claude-code,codex`); `claude`
|
|
8
|
+
* is accepted as an alias for `claude-code`. Blank/unset OR an explicit off value
|
|
9
|
+
* (`false`/`0`/`off`/`no`/`none`/`disabled`) ⇒ off (`[]`) — so disabling native mode never
|
|
10
|
+
* accidentally enables it. An affirmative value naming no harness (`true`/`1`/`on`/…) ⇒ BOTH
|
|
11
|
+
* native harnesses. Only `claude-code` / `codex` are ever native; any other unrecognised
|
|
12
|
+
* token is ignored. A value with neither a recognised harness nor an affirmative keyword
|
|
13
|
+
* (e.g. a typo) ⇒ off, so an unintelligible setting fails safe rather than enabling an
|
|
14
|
+
* unsandboxed, unmetered mode.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseNativeHarnesses(raw: string | undefined): HarnessKind[];
|
|
4
17
|
//# sourceMappingURL=container.d.ts.map
|
package/dist/container.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAIpE,OAAO,KAAK,EAAqC,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAG7F,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,qBAAqB,CAAA;AAuCvE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,eAAe,CA2RlF;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,EAAE,CAc3E"}
|
package/dist/container.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
|
-
import { CryptoIdGenerator, DrizzleGitHubInstallationRepository, DrizzleRunnerPoolConnectionRepository, ProvisioningLogRecorder, SystemClock, buildNodeContainer, buildNodeResolveTransport, createDrizzleRepositories, loadNodeConfig, withProvisioningLog, } from '@cat-factory/node-server';
|
|
1
|
+
import { CryptoIdGenerator, DrizzleGitHubInstallationRepository, DrizzleLocalSettingsRepository, DrizzleRunnerPoolConnectionRepository, ProvisioningLogRecorder, SystemClock, buildNodeContainer, buildNodeResolveTransport, createDrizzleRepositories, loadNodeConfig, withProvisioningLog, } from '@cat-factory/node-server';
|
|
2
2
|
import { ConflictError } from '@cat-factory/kernel';
|
|
3
3
|
import { WorkspaceSettingsService } from '@cat-factory/orchestration';
|
|
4
|
+
import { logger } from '@cat-factory/server';
|
|
5
|
+
import { LocalSettingsService } from '@cat-factory/integrations';
|
|
6
|
+
import { NativeRoutingRunnerTransport } from './NativeRoutingRunnerTransport.js';
|
|
4
7
|
import { applyLocalDefaults } from './config.js';
|
|
5
8
|
import { createLocalGitHubClient, fetchPatAccount, githubPatCreationUrl } from './github.js';
|
|
6
9
|
import { AutoProvisioningInstallationRepository } from './installations.js';
|
|
7
10
|
import { createLocalContainerTransportFromEnv, } from './LocalContainerRunnerTransport.js';
|
|
11
|
+
import { createLocalProcessTransportFromEnv, } from './LocalProcessRunnerTransport.js';
|
|
8
12
|
import { createRuntimeAdapter } from './runtimes/index.js';
|
|
9
13
|
// The local-mode composition root. It is intentionally thin: the ENTIRE Drizzle/
|
|
10
14
|
// Postgres persistence, pg-boss durable execution, gateways and model provisioning
|
|
11
15
|
// come from `buildNodeContainer` unchanged. Local mode only swaps the differentiators
|
|
12
16
|
// behind the seams `buildNodeContainer` exposes:
|
|
13
17
|
// - the runner backend → host Docker by default (a per-run local container,
|
|
14
|
-
// LocalContainerRunnerTransport, Docker/Podman/OrbStack/Colima/Apple `container
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// LocalContainerRunnerTransport, Docker/Podman/OrbStack/Colima/Apple `container`,
|
|
19
|
+
// with the warm pool + per-repo checkout reuse configured from the DB local-mode
|
|
20
|
+
// settings), but PER WORKSPACE it can be delegated to the workspace's registered
|
|
21
|
+
// self-hosted runner pool (the `delegateAgentsToRunnerPool` setting) — the
|
|
22
|
+
// local-vs-external opt-in. The Tester's environment is the symmetric opt-in
|
|
23
|
+
// (`delegateTestEnvToProvider`, wired below as the tester fallback default), so a
|
|
24
|
+
// developer runs everything locally by default but can flip either concern to an
|
|
25
|
+
// external service from the UI;
|
|
26
|
+
// - optional NATIVE execution: run agents as a host process driving the developer's own
|
|
27
|
+
// installed `claude` / `codex` CLI (ambient login), bypassing Docker for the steps that
|
|
28
|
+
// use that login (`LOCAL_NATIVE_AGENTS`); everything else still runs in a container;
|
|
20
29
|
// - the push/clone token → a static GitHub PAT (`GITHUB_PAT`) instead of a GitHub
|
|
21
30
|
// App installation token.
|
|
22
31
|
// Repo resolution is unchanged: the executor still resolves a block's repo from the
|
|
@@ -34,9 +43,18 @@ export function buildLocalContainer(options) {
|
|
|
34
43
|
// ON: the Node loader only enables it for a configured GitHub App, but local mode reaches
|
|
35
44
|
// GitHub through the PAT-backed client, so the read/link endpoints (connection, available
|
|
36
45
|
// repos, "add from existing repo") should be served the same way.
|
|
46
|
+
// Native local execution (opt-in): run agents as a host process driving the developer's
|
|
47
|
+
// OWN installed `claude` / `codex` CLI (ambient login), bypassing Docker. The env is the
|
|
48
|
+
// ALLOW-LIST of subscription harnesses to run natively (`claude-code,codex`); parsed into
|
|
49
|
+
// a harness set so the executor flags `ambientAuth` ONLY for a listed harness whose vendor
|
|
50
|
+
// is that CLI's native vendor (Claude/Codex), and the personal-credential gate skips just
|
|
51
|
+
// those vendors. Default off — the container path is unchanged.
|
|
52
|
+
const nativeHarnesses = parseNativeHarnesses(env.LOCAL_NATIVE_AGENTS);
|
|
53
|
+
const nativeAgents = nativeHarnesses.length > 0;
|
|
37
54
|
const config = {
|
|
38
55
|
...base,
|
|
39
56
|
...(pat ? { github: { ...base.github, enabled: true } } : {}),
|
|
57
|
+
...(nativeAgents ? { nativeAmbientAuth: nativeHarnesses } : {}),
|
|
40
58
|
localMode: {
|
|
41
59
|
enabled: true,
|
|
42
60
|
...(pat ? {} : { githubPatSetupUrl: githubPatCreationUrl() }),
|
|
@@ -62,15 +80,92 @@ export function buildLocalContainer(options) {
|
|
|
62
80
|
workspaceSettingsRepository: repos.workspaceSettingsRepository,
|
|
63
81
|
workspaceRepository: repos.workspaceRepository,
|
|
64
82
|
});
|
|
65
|
-
// The
|
|
66
|
-
//
|
|
67
|
-
//
|
|
83
|
+
// The local container transport is constructed LAZILY on first dispatch, so the service
|
|
84
|
+
// still boots to serve the board (and inline kinds) when LOCAL_HARNESS_IMAGE is unset —
|
|
85
|
+
// only repo-operating container kinds then fail, loudly and with a clear message,
|
|
68
86
|
// mirroring how the Node facade treats a missing runner backend.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
//
|
|
88
|
+
// Native mode does NOT blanket-route every dispatch to the host process: a host process
|
|
89
|
+
// has no sandbox, so only the steps that actually use the developer's ambient CLI login
|
|
90
|
+
// (flagged `ambientAuth` by the executor) run there. Everything else — a proxy/`pi` model,
|
|
91
|
+
// or a non-native vendor reusing the claude-code harness — still runs in a per-run
|
|
92
|
+
// container (built lazily, so a Claude/Codex-only native deployment never needs an image;
|
|
93
|
+
// a proxy step without one fails loudly there). See NativeRoutingRunnerTransport.
|
|
94
|
+
// Local-mode operational settings (warm-pool sizing + per-repo checkout reuse) live in
|
|
95
|
+
// the DB as a per-deployment singleton, edited through the dedicated local-mode settings
|
|
96
|
+
// panel — they REPLACED the old LOCAL_POOL_* / HARNESS_* env vars. Built here so the
|
|
97
|
+
// serving transport resolves its pool config from it and the local-settings controller
|
|
98
|
+
// can read/write it. Requires the Drizzle db (always present for the local service).
|
|
99
|
+
// The serving container transport is built once, lazily, reading its pool + checkout
|
|
100
|
+
// config from the DB settings (not env). The promise is cached so every dispatch — and
|
|
101
|
+
// the native router's container leg — reuses the same instance (and its in-process pool).
|
|
102
|
+
// A settings edit is applied LIVE to this instance (see the service's `onChange` below),
|
|
103
|
+
// so the panel takes effect without a restart.
|
|
104
|
+
let containerTransport;
|
|
105
|
+
const localSettingsService = options.db
|
|
106
|
+
? new LocalSettingsService({
|
|
107
|
+
localSettingsRepository: new DrizzleLocalSettingsRepository(options.db),
|
|
108
|
+
clock: { now: () => Date.now() },
|
|
109
|
+
// Apply an edit to the already-built serving transport so the warm-pool + checkout
|
|
110
|
+
// config takes effect WITHOUT a restart. No-op until the transport is built (the
|
|
111
|
+
// next build reads the fresh settings anyway); a still-failing build is swallowed
|
|
112
|
+
// (a later dispatch surfaces the real problem with a clearer message).
|
|
113
|
+
onChange: async (settings) => {
|
|
114
|
+
if (!containerTransport)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
;
|
|
118
|
+
(await containerTransport).applySettings(settings);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// transport build is still failing — nothing to reconfigure yet
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
: undefined;
|
|
126
|
+
const buildServingTransport = async () => {
|
|
127
|
+
const settings = await localSettingsService?.resolve();
|
|
128
|
+
const transport = createLocalContainerTransportFromEnv(env, settings);
|
|
129
|
+
// Boot housekeeping on the SERVING instance: reap exited per-run containers, drain
|
|
130
|
+
// pool members orphaned by a previous process, and pre-warm to poolMinWarm. Best
|
|
131
|
+
// -effort — if the container runtime is down this throws, but a later dispatch then
|
|
132
|
+
// fails loudly with a clearer message, so swallow it here.
|
|
133
|
+
await transport.reapExited().catch((err) => {
|
|
134
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'local mode: could not reap / pre-warm job containers at startup');
|
|
135
|
+
});
|
|
136
|
+
return transport;
|
|
137
|
+
};
|
|
138
|
+
const resolveContainerTransport = () => {
|
|
139
|
+
if (!containerTransport) {
|
|
140
|
+
containerTransport = buildServingTransport();
|
|
141
|
+
// Don't let a transient build failure (e.g. a DB blip resolving the settings) poison
|
|
142
|
+
// every future dispatch: drop the cached promise on rejection so the next call retries.
|
|
143
|
+
containerTransport.catch(() => {
|
|
144
|
+
containerTransport = undefined;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return containerTransport;
|
|
73
148
|
};
|
|
149
|
+
// The local-agents resolver: in native mode the per-job router (only ambient-CLI steps go
|
|
150
|
+
// to the host process, the rest to a container), otherwise the warm-pool container
|
|
151
|
+
// transport directly.
|
|
152
|
+
let routed;
|
|
153
|
+
const localAgentsResolve = () => {
|
|
154
|
+
if (nativeAgents) {
|
|
155
|
+
if (!routed) {
|
|
156
|
+
let proc;
|
|
157
|
+
routed = new NativeRoutingRunnerTransport(() => (proc ??= createLocalProcessTransportFromEnv(env)), resolveContainerTransport);
|
|
158
|
+
}
|
|
159
|
+
return Promise.resolve(routed);
|
|
160
|
+
}
|
|
161
|
+
return resolveContainerTransport();
|
|
162
|
+
};
|
|
163
|
+
// Eagerly kick off the serving transport's boot housekeeping (reap + pre-warm) when an
|
|
164
|
+
// image is configured, so a warm pool is ready before the first run rather than warming
|
|
165
|
+
// on first dispatch. Skipped without an image (the board still boots; only container
|
|
166
|
+
// kinds fail, loudly). Fire-and-forget: dispatch reuses the same cached promise.
|
|
167
|
+
if (env.LOCAL_HARNESS_IMAGE?.trim())
|
|
168
|
+
void resolveContainerTransport().catch(() => { });
|
|
74
169
|
// The runner-pool resolver (the external opt-in target). In local mode `runners` is
|
|
75
170
|
// enabled (it keys off ENCRYPTION_KEY, which `applyLocalDefaults` always sets), so this
|
|
76
171
|
// is non-null and a workspace can register a pool via the API; a native adapter injected
|
|
@@ -87,11 +182,12 @@ export function buildLocalContainer(options) {
|
|
|
87
182
|
idGenerator,
|
|
88
183
|
clock,
|
|
89
184
|
});
|
|
90
|
-
const wrappedLocal = withProvisioningLog(
|
|
185
|
+
const wrappedLocal = withProvisioningLog(localAgentsResolve, recorder, 'container');
|
|
91
186
|
const wrappedPool = poolResolve ? withProvisioningLog(poolResolve, recorder, 'runner-pool') : null;
|
|
92
187
|
// The local-vs-external agents opt-in: dispatch to the registered runner pool when the
|
|
93
|
-
// workspace opts in (and one is wrapped), else to host Docker
|
|
94
|
-
// throw surfaces a clean "register a pool" message
|
|
188
|
+
// workspace opts in (and one is wrapped), else to host Docker (the warm-pool / native
|
|
189
|
+
// local backend). The pool branch's own throw surfaces a clean "register a pool" message
|
|
190
|
+
// when delegation is on but none exists.
|
|
95
191
|
const resolveTransport = async (workspaceId) => {
|
|
96
192
|
const delegate = !!workspaceId && (await wsSettings.get(workspaceId)).delegateAgentsToRunnerPool;
|
|
97
193
|
if (delegate && wrappedPool)
|
|
@@ -132,15 +228,21 @@ export function buildLocalContainer(options) {
|
|
|
132
228
|
// engine so it refuses a local-infra Tester run on an incapable runtime ("limited
|
|
133
229
|
// mode") instead of dispatching a job that can't stand its dependencies up. Building
|
|
134
230
|
// the adapter is pure (no IO), so this is cheap even though the transport stays lazy.
|
|
135
|
-
|
|
136
|
-
|
|
231
|
+
// Native mode runs agents on the host with no per-run Docker container; the Tester's
|
|
232
|
+
// local docker-compose infra (host compose with per-run project names) is a later phase,
|
|
233
|
+
// so it's reported unsupported for now (the engine steers to "limited mode"). The
|
|
234
|
+
// container path keeps the runtime's real Docker-in-Docker capability.
|
|
235
|
+
const localTestInfraSupported = nativeAgents
|
|
236
|
+
? false
|
|
237
|
+
: createRuntimeAdapter(env).capabilities.localDind;
|
|
238
|
+
const container = buildNodeContainer({
|
|
137
239
|
...options,
|
|
138
240
|
env,
|
|
139
241
|
config,
|
|
140
242
|
repos,
|
|
141
|
-
// The per-workspace chooser (host Docker vs the runner pool). Pre-wrapped
|
|
142
|
-
// correct provisioning-log subsystem per branch, so tell buildNodeContainer not
|
|
143
|
-
// re-wrap with a single subsystem tag.
|
|
243
|
+
// The per-workspace chooser (host Docker / native local vs the runner pool). Pre-wrapped
|
|
244
|
+
// with the correct provisioning-log subsystem per branch, so tell buildNodeContainer not
|
|
245
|
+
// to re-wrap with a single subsystem tag.
|
|
144
246
|
resolveTransport,
|
|
145
247
|
skipProvisioningLogWrap: true,
|
|
146
248
|
// Authenticate git with the developer's PAT when present. Absent → the executor
|
|
@@ -174,5 +276,46 @@ export function buildLocalContainer(options) {
|
|
|
174
276
|
localTestInfraSupported,
|
|
175
277
|
},
|
|
176
278
|
});
|
|
279
|
+
// Surface the local-mode settings service so the dedicated local-settings panel can
|
|
280
|
+
// read/write the warm-pool + checkout config (the controller 503s when this is absent,
|
|
281
|
+
// which is the case on every non-local facade).
|
|
282
|
+
return localSettingsService
|
|
283
|
+
? { ...container, localSettings: { service: localSettingsService } }
|
|
284
|
+
: container;
|
|
285
|
+
}
|
|
286
|
+
/** Values that explicitly DISABLE native mode (so `LOCAL_NATIVE_AGENTS=false` means off). */
|
|
287
|
+
const NATIVE_OFF_VALUES = new Set(['false', '0', 'off', 'no', 'none', 'disabled']);
|
|
288
|
+
/** Affirmative values that enable BOTH native harnesses without naming one. */
|
|
289
|
+
const NATIVE_ALL_VALUES = new Set(['true', '1', 'on', 'yes', 'all', 'both']);
|
|
290
|
+
/**
|
|
291
|
+
* Parse `LOCAL_NATIVE_AGENTS` into the set of subscription harnesses to run natively. The
|
|
292
|
+
* documented form is a comma-separated list of harness ids (`claude-code,codex`); `claude`
|
|
293
|
+
* is accepted as an alias for `claude-code`. Blank/unset OR an explicit off value
|
|
294
|
+
* (`false`/`0`/`off`/`no`/`none`/`disabled`) ⇒ off (`[]`) — so disabling native mode never
|
|
295
|
+
* accidentally enables it. An affirmative value naming no harness (`true`/`1`/`on`/…) ⇒ BOTH
|
|
296
|
+
* native harnesses. Only `claude-code` / `codex` are ever native; any other unrecognised
|
|
297
|
+
* token is ignored. A value with neither a recognised harness nor an affirmative keyword
|
|
298
|
+
* (e.g. a typo) ⇒ off, so an unintelligible setting fails safe rather than enabling an
|
|
299
|
+
* unsandboxed, unmetered mode.
|
|
300
|
+
*/
|
|
301
|
+
export function parseNativeHarnesses(raw) {
|
|
302
|
+
const trimmed = raw?.trim().toLowerCase();
|
|
303
|
+
if (!trimmed || NATIVE_OFF_VALUES.has(trimmed))
|
|
304
|
+
return [];
|
|
305
|
+
const out = new Set();
|
|
306
|
+
let affirmative = false;
|
|
307
|
+
for (const token of trimmed.split(',').map((s) => s.trim())) {
|
|
308
|
+
if (token === 'claude-code' || token === 'claude')
|
|
309
|
+
out.add('claude-code');
|
|
310
|
+
else if (token === 'codex')
|
|
311
|
+
out.add('codex');
|
|
312
|
+
else if (NATIVE_ALL_VALUES.has(token))
|
|
313
|
+
affirmative = true;
|
|
314
|
+
}
|
|
315
|
+
if (out.size > 0)
|
|
316
|
+
return [...out];
|
|
317
|
+
// No harness named: enable both ONLY for an explicit affirmative keyword; anything else
|
|
318
|
+
// unrecognised stays off (fail-safe — see the doc comment).
|
|
319
|
+
return affirmative ? ['claude-code', 'codex'] : [];
|
|
177
320
|
}
|
|
178
321
|
//# sourceMappingURL=container.js.map
|