@peakflo/agent-harness 0.2.8 → 0.2.9-rc.2
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/README.md +67 -1
- package/dist/cli.js +29 -0
- package/dist/harness-discovery.d.ts +173 -0
- package/dist/harness-discovery.js +812 -0
- package/dist/harness.d.ts +98 -1
- package/dist/harness.js +300 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -2
- package/dist/node-floor.d.ts +46 -0
- package/dist/node-floor.js +76 -0
- package/dist/options.js +17 -1
- package/dist/store/conversation-store.js +2 -0
- package/dist/store/sqlite-store.d.ts +17 -0
- package/dist/store/sqlite-store.js +26 -0
- package/dist/types.d.ts +83 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/package.json +1 -1
package/dist/options.js
CHANGED
|
@@ -150,6 +150,17 @@ function parseBoolean(raw, fallback, name) {
|
|
|
150
150
|
return false;
|
|
151
151
|
throw new OptionsError(`Invalid ${name}: '${raw}'. Expected true or false.`);
|
|
152
152
|
}
|
|
153
|
+
/** An on/off variable: 1/true/on/yes or 0/false/off/no. A typo fails the start. */
|
|
154
|
+
function parseSwitch(raw, fallback, name) {
|
|
155
|
+
if (raw === undefined || raw.trim() === '')
|
|
156
|
+
return fallback;
|
|
157
|
+
const value = raw.trim().toLowerCase();
|
|
158
|
+
if (['1', 'true', 'on', 'yes'].includes(value))
|
|
159
|
+
return true;
|
|
160
|
+
if (['0', 'false', 'off', 'no'].includes(value))
|
|
161
|
+
return false;
|
|
162
|
+
throw new OptionsError(`Invalid ${name}: '${raw}'. Expected 1 or 0.`);
|
|
163
|
+
}
|
|
153
164
|
/**
|
|
154
165
|
* Flags win over environment variables, so a local developer can override a
|
|
155
166
|
* value that pf-bootstrap injected.
|
|
@@ -207,6 +218,11 @@ function parseOptions(argv, env) {
|
|
|
207
218
|
defaultWorkspaceRoot(),
|
|
208
219
|
runtime,
|
|
209
220
|
maxConcurrentSessions: parseConcurrency(readFlag(argv, 'max-sessions') ?? env.PF_MAX_CONCURRENT_SESSIONS, runtime),
|
|
210
|
-
labels: parseLabels(readFlag(argv, 'labels') ?? env.PF_RUNNER_LABELS)
|
|
221
|
+
labels: parseLabels(readFlag(argv, 'labels') ?? env.PF_RUNNER_LABELS),
|
|
222
|
+
// The flag wins over the environment, so one start can turn it off
|
|
223
|
+
// without editing a service file.
|
|
224
|
+
harnessDiscovery: readBoolFlag(argv, 'no-harness-discovery')
|
|
225
|
+
? false
|
|
226
|
+
: parseSwitch(env.PF_HARNESS_DISCOVERY, true, 'harness discovery flag (PF_HARNESS_DISCOVERY)')
|
|
211
227
|
};
|
|
212
228
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
28
|
exports.MemoryConversationStore = exports.SqliteConversationStore = void 0;
|
|
29
29
|
exports.openConversationStore = openConversationStore;
|
|
30
|
+
const sqlite_store_1 = require("./sqlite-store");
|
|
30
31
|
const SCHEMA = `
|
|
31
32
|
CREATE TABLE IF NOT EXISTS model_messages (
|
|
32
33
|
session_id TEXT NOT NULL,
|
|
@@ -292,6 +293,7 @@ function openConversationStore(path) {
|
|
|
292
293
|
};
|
|
293
294
|
}
|
|
294
295
|
try {
|
|
296
|
+
(0, sqlite_store_1.ensureStoreDirectory)(path);
|
|
295
297
|
const db = new sqlite.DatabaseSync(path);
|
|
296
298
|
return { store: new SqliteConversationStore(db), durable: true };
|
|
297
299
|
}
|
|
@@ -106,6 +106,23 @@ export interface OpenStoreResult {
|
|
|
106
106
|
/** Why durability was unavailable. Reported so a degraded fleet is visible. */
|
|
107
107
|
reason?: string;
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Make the directory a store file goes in, when it is not there yet.
|
|
111
|
+
*
|
|
112
|
+
* sqlite creates the FILE and never the directory: a `--workspace-root` that
|
|
113
|
+
* does not exist yet made every open fail with "unable to open database
|
|
114
|
+
* file", and the runner then ran in memory and reported itself degraded —
|
|
115
|
+
* for a path it was about to create anyway on the first session. Private
|
|
116
|
+
* mode, because the stores hold transcripts and the model's conversation.
|
|
117
|
+
*
|
|
118
|
+
* A directory that ALREADY EXISTS is left exactly as it is, its mode included:
|
|
119
|
+
* `mkdir` with `recursive` does not touch one, and changing the permissions
|
|
120
|
+
* of a directory somebody chose is not this function's call.
|
|
121
|
+
*
|
|
122
|
+
* Throws when the directory cannot be made; the caller turns that into the
|
|
123
|
+
* same visible memory fallback as any other open failure.
|
|
124
|
+
*/
|
|
125
|
+
export declare function ensureStoreDirectory(path: string): void;
|
|
109
126
|
/**
|
|
110
127
|
* Open the durable store, or say plainly why it could not be opened.
|
|
111
128
|
*
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
*/
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
34
|
exports.MemoryTranscriptStore = exports.SqliteTranscriptStore = void 0;
|
|
35
|
+
exports.ensureStoreDirectory = ensureStoreDirectory;
|
|
35
36
|
exports.openTranscriptStore = openTranscriptStore;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const node_path_1 = require("node:path");
|
|
36
39
|
const SCHEMA = `
|
|
37
40
|
CREATE TABLE IF NOT EXISTS transcript_parts (
|
|
38
41
|
session_id TEXT NOT NULL,
|
|
@@ -309,6 +312,28 @@ class MemoryTranscriptStore {
|
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
exports.MemoryTranscriptStore = MemoryTranscriptStore;
|
|
315
|
+
/**
|
|
316
|
+
* Make the directory a store file goes in, when it is not there yet.
|
|
317
|
+
*
|
|
318
|
+
* sqlite creates the FILE and never the directory: a `--workspace-root` that
|
|
319
|
+
* does not exist yet made every open fail with "unable to open database
|
|
320
|
+
* file", and the runner then ran in memory and reported itself degraded —
|
|
321
|
+
* for a path it was about to create anyway on the first session. Private
|
|
322
|
+
* mode, because the stores hold transcripts and the model's conversation.
|
|
323
|
+
*
|
|
324
|
+
* A directory that ALREADY EXISTS is left exactly as it is, its mode included:
|
|
325
|
+
* `mkdir` with `recursive` does not touch one, and changing the permissions
|
|
326
|
+
* of a directory somebody chose is not this function's call.
|
|
327
|
+
*
|
|
328
|
+
* Throws when the directory cannot be made; the caller turns that into the
|
|
329
|
+
* same visible memory fallback as any other open failure.
|
|
330
|
+
*/
|
|
331
|
+
function ensureStoreDirectory(path) {
|
|
332
|
+
// sqlite's in-memory and temporary databases have no directory.
|
|
333
|
+
if (path === ':memory:' || path === '')
|
|
334
|
+
return;
|
|
335
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
|
|
336
|
+
}
|
|
312
337
|
/**
|
|
313
338
|
* Open the durable store, or say plainly why it could not be opened.
|
|
314
339
|
*
|
|
@@ -333,6 +358,7 @@ function openTranscriptStore(path) {
|
|
|
333
358
|
};
|
|
334
359
|
}
|
|
335
360
|
try {
|
|
361
|
+
ensureStoreDirectory(path);
|
|
336
362
|
const db = new sqlite.DatabaseSync(path);
|
|
337
363
|
return { store: new SqliteTranscriptStore(db), durable: true };
|
|
338
364
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -88,6 +88,13 @@ export interface HarnessOptions {
|
|
|
88
88
|
maxConcurrentSessions: number;
|
|
89
89
|
/** Free-form routing labels, e.g. ['eu-only', 'has-vpn']. */
|
|
90
90
|
labels?: string[];
|
|
91
|
+
/**
|
|
92
|
+
* Look for the coding harnesses installed on this machine and report them.
|
|
93
|
+
* Defaults to true. `--no-harness-discovery` or `PF_HARNESS_DISCOVERY=0`
|
|
94
|
+
* turns it off: no harness CLI is started and no list is reported. A
|
|
95
|
+
* sandbox runner never runs discovery, whatever this says.
|
|
96
|
+
*/
|
|
97
|
+
harnessDiscovery?: boolean;
|
|
91
98
|
}
|
|
92
99
|
/**
|
|
93
100
|
* Every request body in this contract carries the protocol version, so the
|
|
@@ -223,6 +230,73 @@ export interface RunnerCapabilities {
|
|
|
223
230
|
* serves `knowledge_files_list` / `knowledge_file_download`.
|
|
224
231
|
*/
|
|
225
232
|
features?: string[];
|
|
233
|
+
/**
|
|
234
|
+
* Coding harnesses FOUND on this host, whether or not this runner can drive
|
|
235
|
+
* them. Reported by `harness-discovery.ts`.
|
|
236
|
+
*
|
|
237
|
+
* DETECTION IS NOT TURN SUPPORT. `backends` stays the only list the control
|
|
238
|
+
* plane routes on; an entry here with `runnable: false` is information for a
|
|
239
|
+
* person ("Codex is installed on this machine") and never a promise.
|
|
240
|
+
*
|
|
241
|
+
* OPTIONAL AND ADDITIVE, so no protocol bump: an older control plane stores
|
|
242
|
+
* the field and ignores it, and an older harness simply never sends it.
|
|
243
|
+
* Absent on a sandbox runner, which skips discovery altogether.
|
|
244
|
+
*/
|
|
245
|
+
harnesses?: RunnerHarness[];
|
|
246
|
+
/**
|
|
247
|
+
* A short tag for the content of `harnesses`. Sent on EVERY capability
|
|
248
|
+
* report of a runner that runs discovery, also when the list itself is left
|
|
249
|
+
* out because the control plane already has it (see
|
|
250
|
+
* `RunnerHeartbeatResponse.harnessesDigest`).
|
|
251
|
+
*/
|
|
252
|
+
harnessesDigest?: string;
|
|
253
|
+
}
|
|
254
|
+
/** Where a harness's model list came from. */
|
|
255
|
+
export type RunnerHarnessModelSource =
|
|
256
|
+
/** Asked the harness CLI (`cursor-agent models`, `opencode models`). */
|
|
257
|
+
'cli'
|
|
258
|
+
/** Read from the harness's own model cache file. */
|
|
259
|
+
| 'config'
|
|
260
|
+
/** A list compiled into this harness build, as 20x does. */
|
|
261
|
+
| 'static'
|
|
262
|
+
/** The runnable backend's own probe (`backends[].models`). */
|
|
263
|
+
| 'backend';
|
|
264
|
+
export interface RunnerHarnessModel {
|
|
265
|
+
id: string;
|
|
266
|
+
name: string;
|
|
267
|
+
/** The model the harness would use when none is named. */
|
|
268
|
+
default?: boolean;
|
|
269
|
+
}
|
|
270
|
+
/** One known coding harness, as found on this host. */
|
|
271
|
+
export interface RunnerHarness {
|
|
272
|
+
/** 'codex' | 'claude-code' | 'cursor' | 'pi' | 'opencode' — the 20x names. */
|
|
273
|
+
name: string;
|
|
274
|
+
/** Display name, e.g. 'Claude Code'. */
|
|
275
|
+
label: string;
|
|
276
|
+
installed: boolean;
|
|
277
|
+
version?: string | null;
|
|
278
|
+
/** Installed, answering, and signed in. */
|
|
279
|
+
usable: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* Why `usable` is false. ALWAYS one of a fixed set of sentences written in
|
|
282
|
+
* `harness-discovery.ts` — never text a CLI printed, so it cannot carry an
|
|
283
|
+
* account name or a token.
|
|
284
|
+
*/
|
|
285
|
+
reason?: string;
|
|
286
|
+
/**
|
|
287
|
+
* The check of this harness DID NOT FINISH (it timed out), so `usable:
|
|
288
|
+
* false` here is not a statement about the harness. The runner checks again
|
|
289
|
+
* shortly. Only ever sent with `installed: true, usable: false`. A reader
|
|
290
|
+
* that does not know this field shows the entry as not usable with its
|
|
291
|
+
* reason, which says the same thing in words.
|
|
292
|
+
*/
|
|
293
|
+
transient?: boolean;
|
|
294
|
+
/** This runner can run a turn with it: it is also listed in `backends`. */
|
|
295
|
+
runnable: boolean;
|
|
296
|
+
models?: RunnerHarnessModel[];
|
|
297
|
+
modelSource?: RunnerHarnessModelSource;
|
|
298
|
+
/** ISO time of the discovery pass that first found this state. */
|
|
299
|
+
detectedAt: string;
|
|
226
300
|
}
|
|
227
301
|
export interface RunnerBackend {
|
|
228
302
|
/** 'opencode' | 'claude-code' | 'codex' | 'acp' | ... — an open vocabulary. */
|
|
@@ -319,6 +393,15 @@ export interface RunnerHeartbeatResponse {
|
|
|
319
393
|
*/
|
|
320
394
|
action?: 'none' | 'drain' | 'restart';
|
|
321
395
|
heartbeatIntervalMs?: number;
|
|
396
|
+
/**
|
|
397
|
+
* The `capabilities.harnessesDigest` the control plane has stored after
|
|
398
|
+
* this heartbeat. When it equals the runner's own, the runner leaves the
|
|
399
|
+
* (large) `harnesses` list out of the next heartbeat. ABSENT from an older
|
|
400
|
+
* control plane, and the runner then sends the list every time, which is
|
|
401
|
+
* the only thing that is correct there: an older control plane replaces
|
|
402
|
+
* `capabilities` as a whole on each heartbeat.
|
|
403
|
+
*/
|
|
404
|
+
harnessesDigest?: string;
|
|
322
405
|
/**
|
|
323
406
|
* The outbound-only answer to "how does anything reach a running session?".
|
|
324
407
|
* The control plane cannot dial in, so an abort or a user's answer to a
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The version of THIS package, as installed. */
|
|
2
2
|
export declare function getHarnessVersion(): string;
|
|
3
|
+
/** `engines.node` of THIS package (e.g. `>=22.19`), or null when unreadable. */
|
|
4
|
+
export declare function getNodeFloor(): string | null;
|
|
3
5
|
/**
|
|
4
6
|
* The version of the SANDBOX TOOLS layer (gh, @peculiar/x509, ...) currently
|
|
5
7
|
* installed, or null when nothing was ever synced — a self-hosted runner or
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getHarnessVersion = getHarnessVersion;
|
|
4
|
+
exports.getNodeFloor = getNodeFloor;
|
|
4
5
|
exports.getInstalledSandboxToolsVersion = getInstalledSandboxToolsVersion;
|
|
5
6
|
exports.getAttemptedSandboxToolsVersion = getAttemptedSandboxToolsVersion;
|
|
6
7
|
exports.getPiVersion = getPiVersion;
|
|
@@ -23,6 +24,16 @@ function getHarnessVersion() {
|
|
|
23
24
|
}
|
|
24
25
|
return cachedHarnessVersion;
|
|
25
26
|
}
|
|
27
|
+
/** `engines.node` of THIS package (e.g. `>=22.19`), or null when unreadable. */
|
|
28
|
+
function getNodeFloor() {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf8'));
|
|
31
|
+
return typeof pkg.engines?.node === 'string' ? pkg.engines.node : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
26
37
|
/**
|
|
27
38
|
* The version of the SANDBOX TOOLS layer (gh, @peculiar/x509, ...) currently
|
|
28
39
|
* installed, or null when nothing was ever synced — a self-hosted runner or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peakflo/agent-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9-rc.2",
|
|
4
4
|
"description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|