@peakflo/agent-harness 0.2.8 → 0.2.9-rc.1
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 +35 -0
- package/dist/cli.js +5 -0
- package/dist/harness-discovery.d.ts +142 -0
- package/dist/harness-discovery.js +727 -0
- package/dist/harness.d.ts +68 -1
- package/dist/harness.js +197 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -2
- package/dist/options.js +17 -1
- package/dist/types.d.ts +75 -0
- package/package.json +1 -1
package/dist/harness.d.ts
CHANGED
|
@@ -2,8 +2,25 @@ import type { CodingAgentAdapter } from './adapters';
|
|
|
2
2
|
import { SessionModelGateway } from './adapters/model-gateway';
|
|
3
3
|
import { ControlPlaneClient } from './control-plane';
|
|
4
4
|
import { CredentialStore } from './credential-store';
|
|
5
|
+
import { type DiscoverHarnessesOptions } from './harness-discovery';
|
|
5
6
|
import { ArtifactInspector, EventSink, Notifier, SecretProvider, Telemetry, ConversationStore, TranscriptStore, WorkspaceProvider } from './ports';
|
|
6
|
-
import { HarnessOptions, HealthReport } from './types';
|
|
7
|
+
import { HarnessOptions, HealthReport, RunnerHarness } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* How often the installed harnesses are looked for again.
|
|
10
|
+
*
|
|
11
|
+
* Five minutes: short enough that a harness installed or signed in to after
|
|
12
|
+
* start shows up without a restart or a re-enrol, and long enough that the
|
|
13
|
+
* heartbeat (30 s) does not start a handful of CLIs every time. Between passes
|
|
14
|
+
* every heartbeat sends the cached answer.
|
|
15
|
+
*/
|
|
16
|
+
export declare const HARNESS_DISCOVERY_REFRESH_MS: number;
|
|
17
|
+
/** Listed in `capabilities.features` when `capabilities.harnesses` is sent. */
|
|
18
|
+
export declare const HARNESS_DISCOVERY_FEATURE = "harnessDiscovery";
|
|
19
|
+
/**
|
|
20
|
+
* A short tag for WHAT a harness list says, ignoring WHEN it was found.
|
|
21
|
+
* Two passes that found the same thing give the same tag.
|
|
22
|
+
*/
|
|
23
|
+
export declare function harnessContentKey(harnesses: readonly RunnerHarness[]): string;
|
|
7
24
|
/**
|
|
8
25
|
* The control plane refused this harness's PROTOCOL or VERSION. FINAL.
|
|
9
26
|
*
|
|
@@ -90,6 +107,8 @@ export interface HarnessDeps {
|
|
|
90
107
|
approvalRenewIntervalMs?: number;
|
|
91
108
|
/** Injected in tests. */
|
|
92
109
|
clientFactory?: (url: string) => ControlPlaneClient;
|
|
110
|
+
/** Injected in tests; defaults to the real `discoverHarnesses`. */
|
|
111
|
+
discoverHarnesses?: (options: DiscoverHarnessesOptions) => Promise<RunnerHarness[]>;
|
|
93
112
|
}
|
|
94
113
|
/**
|
|
95
114
|
* THE HARNESS.
|
|
@@ -153,6 +172,23 @@ export declare class Harness {
|
|
|
153
172
|
private modelCatalog;
|
|
154
173
|
/** Epoch ms of the last successful (or attempted) model probe. */
|
|
155
174
|
private modelProbeAt;
|
|
175
|
+
/**
|
|
176
|
+
* The harnesses found on this machine, from the last discovery pass.
|
|
177
|
+
*
|
|
178
|
+
* Cached for the same reason the model catalogue is: a pass starts several
|
|
179
|
+
* CLIs, and the heartbeat must not. NULL on a sandbox runner, which never
|
|
180
|
+
* runs discovery, and until the first pass has finished.
|
|
181
|
+
*/
|
|
182
|
+
private harnessReport;
|
|
183
|
+
/** Epoch ms when the last discovery pass STARTED. Doubles as a cooldown. */
|
|
184
|
+
private harnessDiscoveryAt;
|
|
185
|
+
private harnessDiscoveryInFlight;
|
|
186
|
+
/**
|
|
187
|
+
* The `harnessesDigest` the control plane said it has stored, from its last
|
|
188
|
+
* heartbeat answer. Null until it says so, and for ever with an older
|
|
189
|
+
* control plane.
|
|
190
|
+
*/
|
|
191
|
+
private ackedHarnessesDigest;
|
|
156
192
|
/**
|
|
157
193
|
* Empty gateway for the model probe. The probe builds a FRESH adapter and
|
|
158
194
|
* must not read a live turn's credential; a null config is "no gateway",
|
|
@@ -272,6 +308,37 @@ export declare class Harness {
|
|
|
272
308
|
* an empty-but-meaningful list) when the catalogue is empty.
|
|
273
309
|
*/
|
|
274
310
|
private applyModelCatalog;
|
|
311
|
+
/**
|
|
312
|
+
* Look for the installed harnesses again and cache the answer.
|
|
313
|
+
*
|
|
314
|
+
* NEVER ON A SANDBOX. A cloud sandbox image ships exactly what the control
|
|
315
|
+
* plane put in it, so there is nothing to discover, and its capability
|
|
316
|
+
* report must stay what it was before this existed.
|
|
317
|
+
*
|
|
318
|
+
* Never rejects. One pass at a time: a second call while a pass is running
|
|
319
|
+
* returns the running one.
|
|
320
|
+
*/
|
|
321
|
+
private refreshHarnessDiscovery;
|
|
322
|
+
/**
|
|
323
|
+
* Attach the cached harness report to a capability report.
|
|
324
|
+
*
|
|
325
|
+
* `runnable` is decided HERE, against the backends of THIS report, and not
|
|
326
|
+
* taken from the cache: the backend list is re-probed on every heartbeat
|
|
327
|
+
* and the harness list only every few minutes, so a cached `runnable` could
|
|
328
|
+
* contradict `backends`. Pi's models are copied from its backend entry for
|
|
329
|
+
* the same reason — one source, so the two lists cannot disagree.
|
|
330
|
+
*/
|
|
331
|
+
private applyHarnessReport;
|
|
332
|
+
private harnessDiscoveryEnabled;
|
|
333
|
+
/**
|
|
334
|
+
* Whether to start a discovery pass on this heartbeat.
|
|
335
|
+
*
|
|
336
|
+
* NEVER WHILE A TURN IS RUNNING — the same rule as `modelsNeedRefresh`. A
|
|
337
|
+
* pass starts up to a dozen CLI processes; a turn in progress should not
|
|
338
|
+
* compete with them for the machine, and nothing is lost by waiting: the
|
|
339
|
+
* next idle heartbeat starts the pass.
|
|
340
|
+
*/
|
|
341
|
+
private harnessesNeedRefresh;
|
|
275
342
|
/**
|
|
276
343
|
* Whether the model catalogue needs a (rare) re-probe.
|
|
277
344
|
*
|
package/dist/harness.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Harness = exports.UpgradeRequestedError = exports.VersionRefusedError = void 0;
|
|
3
|
+
exports.Harness = exports.UpgradeRequestedError = exports.VersionRefusedError = exports.HARNESS_DISCOVERY_FEATURE = exports.HARNESS_DISCOVERY_REFRESH_MS = void 0;
|
|
4
|
+
exports.harnessContentKey = harnessContentKey;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
6
|
const node_path_1 = require("node:path");
|
|
5
7
|
const model_gateway_1 = require("./adapters/model-gateway");
|
|
6
8
|
const capabilities_1 = require("./capabilities");
|
|
@@ -9,6 +11,7 @@ const control_plane_1 = require("./control-plane");
|
|
|
9
11
|
const credential_store_1 = require("./credential-store");
|
|
10
12
|
const github_command_guards_1 = require("./github-command-guards");
|
|
11
13
|
const github_workspace_1 = require("./github-workspace");
|
|
14
|
+
const harness_discovery_1 = require("./harness-discovery");
|
|
12
15
|
const health_1 = require("./health");
|
|
13
16
|
const model_probe_1 = require("./model-probe");
|
|
14
17
|
const ports_default_1 = require("./ports-default");
|
|
@@ -29,6 +32,28 @@ const LEASE_MIN_EMPTY_POLL_MS = 250;
|
|
|
29
32
|
* process. Every six hours is a bounded refresh of a slowly changing catalog.
|
|
30
33
|
*/
|
|
31
34
|
const MODEL_PROBE_REFRESH_MS = 6 * 60 * 60 * 1000;
|
|
35
|
+
/**
|
|
36
|
+
* How often the installed harnesses are looked for again.
|
|
37
|
+
*
|
|
38
|
+
* Five minutes: short enough that a harness installed or signed in to after
|
|
39
|
+
* start shows up without a restart or a re-enrol, and long enough that the
|
|
40
|
+
* heartbeat (30 s) does not start a handful of CLIs every time. Between passes
|
|
41
|
+
* every heartbeat sends the cached answer.
|
|
42
|
+
*/
|
|
43
|
+
exports.HARNESS_DISCOVERY_REFRESH_MS = 5 * 60 * 1000;
|
|
44
|
+
/** Listed in `capabilities.features` when `capabilities.harnesses` is sent. */
|
|
45
|
+
exports.HARNESS_DISCOVERY_FEATURE = 'harnessDiscovery';
|
|
46
|
+
/**
|
|
47
|
+
* A short tag for WHAT a harness list says, ignoring WHEN it was found.
|
|
48
|
+
* Two passes that found the same thing give the same tag.
|
|
49
|
+
*/
|
|
50
|
+
function harnessContentKey(harnesses) {
|
|
51
|
+
const content = harnesses.map(({ detectedAt: _detectedAt, ...rest }) => rest);
|
|
52
|
+
return (0, node_crypto_1.createHash)('sha256')
|
|
53
|
+
.update(JSON.stringify(content))
|
|
54
|
+
.digest('hex')
|
|
55
|
+
.slice(0, 32);
|
|
56
|
+
}
|
|
32
57
|
/**
|
|
33
58
|
* The control plane refused this harness's PROTOCOL or VERSION. FINAL.
|
|
34
59
|
*
|
|
@@ -134,6 +159,23 @@ class Harness {
|
|
|
134
159
|
modelCatalog = null;
|
|
135
160
|
/** Epoch ms of the last successful (or attempted) model probe. */
|
|
136
161
|
modelProbeAt = 0;
|
|
162
|
+
/**
|
|
163
|
+
* The harnesses found on this machine, from the last discovery pass.
|
|
164
|
+
*
|
|
165
|
+
* Cached for the same reason the model catalogue is: a pass starts several
|
|
166
|
+
* CLIs, and the heartbeat must not. NULL on a sandbox runner, which never
|
|
167
|
+
* runs discovery, and until the first pass has finished.
|
|
168
|
+
*/
|
|
169
|
+
harnessReport = null;
|
|
170
|
+
/** Epoch ms when the last discovery pass STARTED. Doubles as a cooldown. */
|
|
171
|
+
harnessDiscoveryAt = 0;
|
|
172
|
+
harnessDiscoveryInFlight = null;
|
|
173
|
+
/**
|
|
174
|
+
* The `harnessesDigest` the control plane said it has stored, from its last
|
|
175
|
+
* heartbeat answer. Null until it says so, and for ever with an older
|
|
176
|
+
* control plane.
|
|
177
|
+
*/
|
|
178
|
+
ackedHarnessesDigest = null;
|
|
137
179
|
/**
|
|
138
180
|
* Empty gateway for the model probe. The probe builds a FRESH adapter and
|
|
139
181
|
* must not read a live turn's credential; a null config is "no gateway",
|
|
@@ -283,6 +325,13 @@ class Harness {
|
|
|
283
325
|
maxConcurrentSessions: this.options.maxConcurrentSessions,
|
|
284
326
|
labels: this.options.labels
|
|
285
327
|
});
|
|
328
|
+
/*
|
|
329
|
+
* STARTED, NEVER AWAITED. Registration must not wait for a third-party
|
|
330
|
+
* CLI: a pass can take as long as its slowest harness (20 s), and a runner
|
|
331
|
+
* that is slow to register looks like a runner that did not come back. The
|
|
332
|
+
* report is attached to the first heartbeat after the pass has finished.
|
|
333
|
+
*/
|
|
334
|
+
void this.refreshHarnessDiscovery();
|
|
286
335
|
// Install immediately before model probing can start a backend. The lease
|
|
287
336
|
// is process-wide and reference-counted, so concurrent Harness instances
|
|
288
337
|
// share one generic launcher instead of racing to rewrite global PATH.
|
|
@@ -298,6 +347,8 @@ class Harness {
|
|
|
298
347
|
*/
|
|
299
348
|
await this.refreshModelCatalog(true);
|
|
300
349
|
this.applyModelCatalog(this.capabilities);
|
|
350
|
+
// Attaches the report only if the pass has already finished.
|
|
351
|
+
this.applyHarnessReport(this.capabilities);
|
|
301
352
|
log('Harness starting', {
|
|
302
353
|
harnessVersion: (0, version_1.getHarnessVersion)(),
|
|
303
354
|
healthPort: this.options.healthPort,
|
|
@@ -517,6 +568,10 @@ class Harness {
|
|
|
517
568
|
if (response.heartbeatIntervalMs) {
|
|
518
569
|
this.heartbeatIntervalMs = response.heartbeatIntervalMs;
|
|
519
570
|
}
|
|
571
|
+
this.ackedHarnessesDigest =
|
|
572
|
+
typeof response.harnessesDigest === 'string'
|
|
573
|
+
? response.harnessesDigest
|
|
574
|
+
: null;
|
|
520
575
|
this.observeHarnessPin(response.harnessVersion);
|
|
521
576
|
this.observeSandboxToolsPin(response.sandboxToolsVersion);
|
|
522
577
|
this.applyCommandsSafely(response.sessionCommands ?? []);
|
|
@@ -723,6 +778,137 @@ class Harness {
|
|
|
723
778
|
}
|
|
724
779
|
}
|
|
725
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Look for the installed harnesses again and cache the answer.
|
|
783
|
+
*
|
|
784
|
+
* NEVER ON A SANDBOX. A cloud sandbox image ships exactly what the control
|
|
785
|
+
* plane put in it, so there is nothing to discover, and its capability
|
|
786
|
+
* report must stay what it was before this existed.
|
|
787
|
+
*
|
|
788
|
+
* Never rejects. One pass at a time: a second call while a pass is running
|
|
789
|
+
* returns the running one.
|
|
790
|
+
*/
|
|
791
|
+
refreshHarnessDiscovery() {
|
|
792
|
+
if (!this.harnessDiscoveryEnabled()) {
|
|
793
|
+
return Promise.resolve();
|
|
794
|
+
}
|
|
795
|
+
if (this.harnessDiscoveryInFlight) {
|
|
796
|
+
return this.harnessDiscoveryInFlight;
|
|
797
|
+
}
|
|
798
|
+
this.harnessDiscoveryAt = Date.now();
|
|
799
|
+
const discover = this.deps.discoverHarnesses ?? harness_discovery_1.discoverHarnesses;
|
|
800
|
+
const pass = (async () => {
|
|
801
|
+
try {
|
|
802
|
+
const report = await discover({
|
|
803
|
+
runnableBackends: (this.capabilities?.backends ?? []).map((backend) => backend.name)
|
|
804
|
+
});
|
|
805
|
+
// Same content as before: keep the OLD report, with its
|
|
806
|
+
// `detectedAt`. A pass that found nothing new must not look like a
|
|
807
|
+
// change, or the whole list would be sent again every five minutes.
|
|
808
|
+
if (!this.harnessReport ||
|
|
809
|
+
harnessContentKey(report) !== harnessContentKey(this.harnessReport)) {
|
|
810
|
+
this.harnessReport = report;
|
|
811
|
+
}
|
|
812
|
+
log('Harnesses discovered', {
|
|
813
|
+
installed: report
|
|
814
|
+
.filter((harness) => harness.installed)
|
|
815
|
+
.map((harness) => harness.name),
|
|
816
|
+
usable: report
|
|
817
|
+
.filter((harness) => harness.usable)
|
|
818
|
+
.map((harness) => harness.name)
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
// discoverHarnesses never rejects by construction. If a regression
|
|
823
|
+
// makes it reject, keep the last good answer instead of failing the
|
|
824
|
+
// start-up or the heartbeat.
|
|
825
|
+
}
|
|
826
|
+
finally {
|
|
827
|
+
this.harnessDiscoveryInFlight = null;
|
|
828
|
+
}
|
|
829
|
+
})();
|
|
830
|
+
this.harnessDiscoveryInFlight = pass;
|
|
831
|
+
return pass;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Attach the cached harness report to a capability report.
|
|
835
|
+
*
|
|
836
|
+
* `runnable` is decided HERE, against the backends of THIS report, and not
|
|
837
|
+
* taken from the cache: the backend list is re-probed on every heartbeat
|
|
838
|
+
* and the harness list only every few minutes, so a cached `runnable` could
|
|
839
|
+
* contradict `backends`. Pi's models are copied from its backend entry for
|
|
840
|
+
* the same reason — one source, so the two lists cannot disagree.
|
|
841
|
+
*/
|
|
842
|
+
applyHarnessReport(capabilities) {
|
|
843
|
+
if (!this.harnessReport) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const harnesses = this.harnessReport.map((harness) => {
|
|
847
|
+
const backend = capabilities.backends.find((candidate) => candidate.name === harness.name);
|
|
848
|
+
if (!backend) {
|
|
849
|
+
return { ...harness, runnable: false };
|
|
850
|
+
}
|
|
851
|
+
/*
|
|
852
|
+
* A HARNESS THIS RUNNER RUNS IS NEVER SHOWN WORSE THAN `backends` SAYS.
|
|
853
|
+
*
|
|
854
|
+
* The backend probe runs on every heartbeat and is what work is routed
|
|
855
|
+
* on. The cached discovery pass is older and looks in other places, so
|
|
856
|
+
* on its own it could say "not installed" or "version check timed out"
|
|
857
|
+
* for a Pi that is running turns right now. Where the two disagree, the
|
|
858
|
+
* backend probe is right.
|
|
859
|
+
*/
|
|
860
|
+
const entry = {
|
|
861
|
+
...harness,
|
|
862
|
+
installed: true,
|
|
863
|
+
usable: true,
|
|
864
|
+
runnable: true,
|
|
865
|
+
version: backend.version ?? harness.version ?? null
|
|
866
|
+
};
|
|
867
|
+
delete entry.reason;
|
|
868
|
+
if (!entry.models && backend.models && backend.models.length > 0) {
|
|
869
|
+
entry.models = backend.models.map(({ id, name }) => ({ id, name }));
|
|
870
|
+
entry.modelSource = 'backend';
|
|
871
|
+
}
|
|
872
|
+
return entry;
|
|
873
|
+
});
|
|
874
|
+
const digest = harnessContentKey(harnesses);
|
|
875
|
+
capabilities.harnessesDigest = digest;
|
|
876
|
+
/*
|
|
877
|
+
* THE LIST IS SENT ONLY WHEN THE CONTROL PLANE DOES NOT HAVE IT.
|
|
878
|
+
*
|
|
879
|
+
* It is about 20 KB on a machine with every harness, and it changes
|
|
880
|
+
* rarely. A control plane that understands the digest answers each
|
|
881
|
+
* heartbeat with the digest it has stored; while that equals ours, the
|
|
882
|
+
* list is left out and only the digest goes. An older control plane never
|
|
883
|
+
* answers with a digest, so it is sent the list every time — which it
|
|
884
|
+
* needs, because it replaces `capabilities` as a whole.
|
|
885
|
+
*/
|
|
886
|
+
if (this.ackedHarnessesDigest !== digest) {
|
|
887
|
+
capabilities.harnesses = harnesses;
|
|
888
|
+
}
|
|
889
|
+
capabilities.features = [
|
|
890
|
+
...(capabilities.features ?? []).filter((feature) => feature !== exports.HARNESS_DISCOVERY_FEATURE),
|
|
891
|
+
exports.HARNESS_DISCOVERY_FEATURE
|
|
892
|
+
];
|
|
893
|
+
}
|
|
894
|
+
harnessDiscoveryEnabled() {
|
|
895
|
+
return (this.options.runtime !== 'sandbox' &&
|
|
896
|
+
this.options.harnessDiscovery !== false);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Whether to start a discovery pass on this heartbeat.
|
|
900
|
+
*
|
|
901
|
+
* NEVER WHILE A TURN IS RUNNING — the same rule as `modelsNeedRefresh`. A
|
|
902
|
+
* pass starts up to a dozen CLI processes; a turn in progress should not
|
|
903
|
+
* compete with them for the machine, and nothing is lost by waiting: the
|
|
904
|
+
* next idle heartbeat starts the pass.
|
|
905
|
+
*/
|
|
906
|
+
harnessesNeedRefresh() {
|
|
907
|
+
if (!this.harnessDiscoveryEnabled() || this.active.size > 0) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
return Date.now() - this.harnessDiscoveryAt >= exports.HARNESS_DISCOVERY_REFRESH_MS;
|
|
911
|
+
}
|
|
726
912
|
/**
|
|
727
913
|
* Whether the model catalogue needs a (rare) re-probe.
|
|
728
914
|
*
|
|
@@ -765,7 +951,13 @@ class Harness {
|
|
|
765
951
|
labels: this.options.labels
|
|
766
952
|
});
|
|
767
953
|
this.applyModelCatalog(capabilities);
|
|
954
|
+
this.applyHarnessReport(capabilities);
|
|
768
955
|
this.capabilities = capabilities;
|
|
956
|
+
// Not awaited: the answer is attached to a LATER heartbeat. Only the
|
|
957
|
+
// cached report is ever sent, so a slow CLI cannot delay a heartbeat.
|
|
958
|
+
if (this.harnessesNeedRefresh()) {
|
|
959
|
+
void this.refreshHarnessDiscovery();
|
|
960
|
+
}
|
|
769
961
|
/*
|
|
770
962
|
* THE MODEL CATALOGUE IS NOT RE-PROBED HERE, EVER. Only the CLI probes
|
|
771
963
|
* run on a heartbeat; Pi discovery starts an RPC process, and doing that
|
|
@@ -791,6 +983,10 @@ class Harness {
|
|
|
791
983
|
if (response.heartbeatIntervalMs) {
|
|
792
984
|
this.heartbeatIntervalMs = response.heartbeatIntervalMs;
|
|
793
985
|
}
|
|
986
|
+
this.ackedHarnessesDigest =
|
|
987
|
+
typeof response.harnessesDigest === 'string'
|
|
988
|
+
? response.harnessesDigest
|
|
989
|
+
: null;
|
|
794
990
|
this.observeHarnessPin(response.harnessVersion);
|
|
795
991
|
this.observeSandboxToolsPin(response.sandboxToolsVersion);
|
|
796
992
|
this.applyCommandsSafely(response.sessionCommands ?? []);
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ export type { ProbeCapabilitiesOptions, ProbeResult } from './capabilities';
|
|
|
24
24
|
*/
|
|
25
25
|
export { flattenProviderModels, probeBackendModels, probeModelCatalog } from './model-probe';
|
|
26
26
|
export type { ModelCatalog, ModelProbeAdapterFactory, ProviderRecord } from './model-probe';
|
|
27
|
+
/**
|
|
28
|
+
* Harness discovery — which coding harnesses (Codex, Claude Code, Cursor, Pi,
|
|
29
|
+
* OpenCode) are installed on this machine, whether each is signed in, and the
|
|
30
|
+
* models each offers. Reported as `capabilities.harnesses`, apart from
|
|
31
|
+
* `capabilities.backends`: finding a harness does not mean this runner can
|
|
32
|
+
* run a turn with it.
|
|
33
|
+
*/
|
|
34
|
+
export { discoverHarnesses, KNOWN_HARNESSES, DISCOVERY_COMMAND_TIMEOUT_MS, DISCOVERY_HARNESS_TIMEOUT_MS } from './harness-discovery';
|
|
35
|
+
export type { DiscoverHarnessesOptions, DiscoveryDeps, DiscoveryExecResult } from './harness-discovery';
|
|
27
36
|
export type { ArtifactInspector, EventSink, HarnessPorts, InspectedArtifact, Notifier, SecretProvider, StoredPart, StoredPartInput, TaskMcpProvider, TaskMcpScope, Telemetry, TokenInjector, TranscriptStore, WorkspaceProvider, WorkspaceRequest } from './ports';
|
|
28
37
|
export { CompositeEventSink, DirectoryWorkspaceProvider, NoArtifactInspector, NoSecretProvider, NoopTelemetry, NullEventSink, ServerNotifier, SessionTokenInjector, StaticTaskMcpProvider } from './ports-default';
|
|
29
38
|
export type { DirectoryWorkspaceOptions } from './ports-default';
|
package/dist/index.js
CHANGED
|
@@ -23,8 +23,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
23
23
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.
|
|
27
|
-
exports.isProtocolSupported = exports.isPersistablePart = exports.isFinalStatus = exports.isEphemeralPartType = exports.ingestPartByteLength = exports.ROUTES = exports.PROTOCOL_VERSION_HEADER = exports.PROTOCOL_VERSION = exports.PERSISTED_PART_TYPES = exports.MISSED_HEARTBEATS_BEFORE_OFFLINE = void 0;
|
|
26
|
+
exports.HARNESS_UPGRADE_EXIT_CODE = exports.FINAL_HTTP_STATUSES = exports.EPHEMERAL_PART_TYPES = exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_LEASE_WAIT_SECONDS = exports.DEFAULT_LEASE_TTL_MS = exports.DEFAULT_HEARTBEAT_INTERVAL_MS = exports.DEFAULT_BASE_DELAY_MS = exports.CONTRACT_ERROR_CODES = exports.openTranscriptStore = exports.SqliteTranscriptStore = exports.MemoryTranscriptStore = exports.writeToChildStdin = exports.isBenignStreamError = exports.guardStream = exports.guardChildStreams = exports.toIngestPart = exports.projectParts = exports.SessionRunner = exports.McpAuthProxy = exports.GitHubWorkspaceProvider = exports.StaticTaskMcpProvider = exports.SessionTokenInjector = exports.ServerNotifier = exports.NullEventSink = exports.NoopTelemetry = exports.NoSecretProvider = exports.NoArtifactInspector = exports.DirectoryWorkspaceProvider = exports.CompositeEventSink = exports.DISCOVERY_HARNESS_TIMEOUT_MS = exports.DISCOVERY_COMMAND_TIMEOUT_MS = exports.KNOWN_HARNESSES = exports.discoverHarnesses = exports.probeModelCatalog = exports.probeBackendModels = exports.flattenProviderModels = exports.probeCapabilities = exports.probe = exports.getPiVersion = exports.getHarnessVersion = exports.parseOptions = exports.OptionsError = exports.HealthServer = exports.ProtocolRefusedError = exports.ControlPlaneError = exports.ControlPlaneClient = exports.VersionRefusedError = exports.UpgradeRequestedError = exports.Harness = void 0;
|
|
27
|
+
exports.isProtocolSupported = exports.isPersistablePart = exports.isFinalStatus = exports.isEphemeralPartType = exports.ingestPartByteLength = exports.ROUTES = exports.PROTOCOL_VERSION_HEADER = exports.PROTOCOL_VERSION = exports.PERSISTED_PART_TYPES = exports.MISSED_HEARTBEATS_BEFORE_OFFLINE = exports.MIN_SUPPORTED_PROTOCOL_VERSION = exports.MAX_LEASE_WAIT_SECONDS = exports.MAX_INGEST_PART_BYTES = exports.MAX_INGEST_PARTS = void 0;
|
|
28
28
|
/* ---- The process ---------------------------------------------------------- */
|
|
29
29
|
var harness_1 = require("./harness");
|
|
30
30
|
Object.defineProperty(exports, "Harness", { enumerable: true, get: function () { return harness_1.Harness; } });
|
|
@@ -55,6 +55,18 @@ var model_probe_1 = require("./model-probe");
|
|
|
55
55
|
Object.defineProperty(exports, "flattenProviderModels", { enumerable: true, get: function () { return model_probe_1.flattenProviderModels; } });
|
|
56
56
|
Object.defineProperty(exports, "probeBackendModels", { enumerable: true, get: function () { return model_probe_1.probeBackendModels; } });
|
|
57
57
|
Object.defineProperty(exports, "probeModelCatalog", { enumerable: true, get: function () { return model_probe_1.probeModelCatalog; } });
|
|
58
|
+
/**
|
|
59
|
+
* Harness discovery — which coding harnesses (Codex, Claude Code, Cursor, Pi,
|
|
60
|
+
* OpenCode) are installed on this machine, whether each is signed in, and the
|
|
61
|
+
* models each offers. Reported as `capabilities.harnesses`, apart from
|
|
62
|
+
* `capabilities.backends`: finding a harness does not mean this runner can
|
|
63
|
+
* run a turn with it.
|
|
64
|
+
*/
|
|
65
|
+
var harness_discovery_1 = require("./harness-discovery");
|
|
66
|
+
Object.defineProperty(exports, "discoverHarnesses", { enumerable: true, get: function () { return harness_discovery_1.discoverHarnesses; } });
|
|
67
|
+
Object.defineProperty(exports, "KNOWN_HARNESSES", { enumerable: true, get: function () { return harness_discovery_1.KNOWN_HARNESSES; } });
|
|
68
|
+
Object.defineProperty(exports, "DISCOVERY_COMMAND_TIMEOUT_MS", { enumerable: true, get: function () { return harness_discovery_1.DISCOVERY_COMMAND_TIMEOUT_MS; } });
|
|
69
|
+
Object.defineProperty(exports, "DISCOVERY_HARNESS_TIMEOUT_MS", { enumerable: true, get: function () { return harness_discovery_1.DISCOVERY_HARNESS_TIMEOUT_MS; } });
|
|
58
70
|
var ports_default_1 = require("./ports-default");
|
|
59
71
|
Object.defineProperty(exports, "CompositeEventSink", { enumerable: true, get: function () { return ports_default_1.CompositeEventSink; } });
|
|
60
72
|
Object.defineProperty(exports, "DirectoryWorkspaceProvider", { enumerable: true, get: function () { return ports_default_1.DirectoryWorkspaceProvider; } });
|
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
|
}
|
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,65 @@ 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
|
+
/** This runner can run a turn with it: it is also listed in `backends`. */
|
|
287
|
+
runnable: boolean;
|
|
288
|
+
models?: RunnerHarnessModel[];
|
|
289
|
+
modelSource?: RunnerHarnessModelSource;
|
|
290
|
+
/** ISO time of the discovery pass that first found this state. */
|
|
291
|
+
detectedAt: string;
|
|
226
292
|
}
|
|
227
293
|
export interface RunnerBackend {
|
|
228
294
|
/** 'opencode' | 'claude-code' | 'codex' | 'acp' | ... — an open vocabulary. */
|
|
@@ -319,6 +385,15 @@ export interface RunnerHeartbeatResponse {
|
|
|
319
385
|
*/
|
|
320
386
|
action?: 'none' | 'drain' | 'restart';
|
|
321
387
|
heartbeatIntervalMs?: number;
|
|
388
|
+
/**
|
|
389
|
+
* The `capabilities.harnessesDigest` the control plane has stored after
|
|
390
|
+
* this heartbeat. When it equals the runner's own, the runner leaves the
|
|
391
|
+
* (large) `harnesses` list out of the next heartbeat. ABSENT from an older
|
|
392
|
+
* control plane, and the runner then sends the list every time, which is
|
|
393
|
+
* the only thing that is correct there: an older control plane replaces
|
|
394
|
+
* `capabilities` as a whole on each heartbeat.
|
|
395
|
+
*/
|
|
396
|
+
harnessesDigest?: string;
|
|
322
397
|
/**
|
|
323
398
|
* The outbound-only answer to "how does anything reach a running session?".
|
|
324
399
|
* The control plane cannot dial in, so an abort or a user's answer to a
|
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.1",
|
|
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",
|