@peakflo/agent-harness 0.0.0-stage → 0.2.2-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/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/adapters/acp-adapter.d.ts +177 -0
- package/dist/adapters/acp-adapter.js +1732 -0
- package/dist/adapters/claude-code-adapter.d.ts +171 -0
- package/dist/adapters/claude-code-adapter.js +1918 -0
- package/dist/adapters/codex-adapter.d.ts +67 -0
- package/dist/adapters/codex-adapter.js +681 -0
- package/dist/adapters/codex-app-server-adapter.d.ts +105 -0
- package/dist/adapters/codex-app-server-adapter.js +1309 -0
- package/dist/adapters/coding-agent-adapter.d.ts +289 -0
- package/dist/adapters/coding-agent-adapter.js +31 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +35 -0
- package/dist/adapters/model-gateway.d.ts +84 -0
- package/dist/adapters/model-gateway.js +270 -0
- package/dist/adapters/opencode-adapter.d.ts +403 -0
- package/dist/adapters/opencode-adapter.js +2710 -0
- package/dist/adapters/opencode-config.d.ts +41 -0
- package/dist/adapters/opencode-config.js +147 -0
- package/dist/adapters/opencode-server-policy.d.ts +82 -0
- package/dist/adapters/opencode-server-policy.js +148 -0
- package/dist/adapters/pi-adapter.d.ts +168 -0
- package/dist/adapters/pi-adapter.js +1468 -0
- package/dist/capabilities.d.ts +58 -0
- package/dist/capabilities.js +161 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +231 -0
- package/dist/contract.d.ts +273 -0
- package/dist/contract.js +357 -0
- package/dist/control-plane.d.ts +178 -0
- package/dist/control-plane.js +431 -0
- package/dist/credential-store.d.ts +110 -0
- package/dist/credential-store.js +181 -0
- package/dist/enrol/browser-enrol.d.ts +85 -0
- package/dist/enrol/browser-enrol.js +387 -0
- package/dist/github-command-guards.d.ts +20 -0
- package/dist/github-command-guards.js +330 -0
- package/dist/github-proxy-certificates.d.ts +25 -0
- package/dist/github-proxy-certificates.js +145 -0
- package/dist/github-proxy.d.ts +37 -0
- package/dist/github-proxy.js +476 -0
- package/dist/github-workspace.d.ts +55 -0
- package/dist/github-workspace.js +537 -0
- package/dist/harness.d.ts +333 -0
- package/dist/harness.js +1414 -0
- package/dist/health.d.ts +45 -0
- package/dist/health.js +116 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +123 -0
- package/dist/model-probe.d.ts +96 -0
- package/dist/model-probe.js +195 -0
- package/dist/options.d.ts +15 -0
- package/dist/options.js +212 -0
- package/dist/ports-default.d.ts +212 -0
- package/dist/ports-default.js +333 -0
- package/dist/ports.d.ts +436 -0
- package/dist/ports.js +27 -0
- package/dist/runtime/attachments.d.ts +92 -0
- package/dist/runtime/attachments.js +377 -0
- package/dist/runtime/child-stream-guards.d.ts +45 -0
- package/dist/runtime/child-stream-guards.js +111 -0
- package/dist/runtime/conversation.d.ts +87 -0
- package/dist/runtime/conversation.js +156 -0
- package/dist/runtime/lease-renewal.d.ts +169 -0
- package/dist/runtime/lease-renewal.js +433 -0
- package/dist/runtime/mcp-auth-proxy.d.ts +82 -0
- package/dist/runtime/mcp-auth-proxy.js +300 -0
- package/dist/runtime/permission-mode.d.ts +116 -0
- package/dist/runtime/permission-mode.js +146 -0
- package/dist/runtime/projection.d.ts +71 -0
- package/dist/runtime/projection.js +211 -0
- package/dist/runtime/session-runner.d.ts +520 -0
- package/dist/runtime/session-runner.js +2122 -0
- package/dist/shared/reasoning-effort.d.ts +11 -0
- package/dist/shared/reasoning-effort.js +20 -0
- package/dist/store/conversation-store.d.ts +100 -0
- package/dist/store/conversation-store.js +305 -0
- package/dist/store/sqlite-store.d.ts +120 -0
- package/dist/store/sqlite-store.js +346 -0
- package/dist/types.d.ts +952 -0
- package/dist/types.js +55 -0
- package/dist/version.d.ts +32 -0
- package/dist/version.js +77 -0
- package/package.json +53 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CAPABILITY PROBE.
|
|
3
|
+
*
|
|
4
|
+
* Ported from 20x `src/main/agent-installer/detect.js`, which has been probing
|
|
5
|
+
* these same binaries in production since long before this package existed.
|
|
6
|
+
* The detection logic — PATH repair, `--version`, the version-extracting
|
|
7
|
+
* regular expression, the swallow-everything failure mode — is reproduced
|
|
8
|
+
* rather than reinvented, because every line of it encodes a platform quirk
|
|
9
|
+
* somebody already hit.
|
|
10
|
+
*
|
|
11
|
+
* WHAT CHANGED IN THE PORT, and why:
|
|
12
|
+
*
|
|
13
|
+
* 1. It reports the WIRE shape (`RunnerCapabilities`) rather than the desktop's
|
|
14
|
+
* installer-shaped record. The control plane routes work on this, so it must
|
|
15
|
+
* speak the contract's vocabulary, not the installer's.
|
|
16
|
+
*
|
|
17
|
+
* 2. Probes run with a hard timeout and never reject. A capability probe that
|
|
18
|
+
* can fail is a runner that can fail to register, and "docker is missing" is
|
|
19
|
+
* not a reason to refuse to run an agent that does not need docker.
|
|
20
|
+
*
|
|
21
|
+
* 3. The result is NOT cached across calls. A host is upgraded in place — a
|
|
22
|
+
* backend can be installed and docker can appear without the runner ever
|
|
23
|
+
* restarting — which is exactly why the contract refreshes capabilities on
|
|
24
|
+
* every heartbeat rather than trusting the registration snapshot.
|
|
25
|
+
*/
|
|
26
|
+
import { RunnerCapabilities } from './types';
|
|
27
|
+
export interface ProbeResult {
|
|
28
|
+
installed: boolean;
|
|
29
|
+
version: string | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run one command and pull a version out of its stdout. NEVER REJECTS.
|
|
33
|
+
*
|
|
34
|
+
* `shell: true` on Windows is load-bearing and not a style choice — without it
|
|
35
|
+
* `.cmd`/`.bat` wrappers, which is how npm installs binaries on Windows, do not
|
|
36
|
+
* resolve at all and every backend reports as missing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function probe(command: string, args?: string[]): Promise<ProbeResult>;
|
|
39
|
+
export interface ProbeCapabilitiesOptions {
|
|
40
|
+
/**
|
|
41
|
+
* How many sessions this host will run at once.
|
|
42
|
+
*
|
|
43
|
+
* DEFAULTS TO 1, and that default is a security position rather than a
|
|
44
|
+
* performance one: cross-session isolation on a shared customer VM is the
|
|
45
|
+
* customer's duty, and 1 is the only value that needs no such assumption.
|
|
46
|
+
*/
|
|
47
|
+
maxConcurrentSessions?: number;
|
|
48
|
+
/** Free-form routing labels, e.g. ['eu-only', 'has-vpn']. */
|
|
49
|
+
labels?: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Everything the control plane needs to decide whether to send work here.
|
|
53
|
+
*
|
|
54
|
+
* All probes run CONCURRENTLY. Serially this would cost six process spawns at
|
|
55
|
+
* up to 10s each on a host where nothing is installed, and it runs on every
|
|
56
|
+
* heartbeat.
|
|
57
|
+
*/
|
|
58
|
+
export declare function probeCapabilities(options?: ProbeCapabilitiesOptions): Promise<RunnerCapabilities>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* THE CAPABILITY PROBE.
|
|
4
|
+
*
|
|
5
|
+
* Ported from 20x `src/main/agent-installer/detect.js`, which has been probing
|
|
6
|
+
* these same binaries in production since long before this package existed.
|
|
7
|
+
* The detection logic — PATH repair, `--version`, the version-extracting
|
|
8
|
+
* regular expression, the swallow-everything failure mode — is reproduced
|
|
9
|
+
* rather than reinvented, because every line of it encodes a platform quirk
|
|
10
|
+
* somebody already hit.
|
|
11
|
+
*
|
|
12
|
+
* WHAT CHANGED IN THE PORT, and why:
|
|
13
|
+
*
|
|
14
|
+
* 1. It reports the WIRE shape (`RunnerCapabilities`) rather than the desktop's
|
|
15
|
+
* installer-shaped record. The control plane routes work on this, so it must
|
|
16
|
+
* speak the contract's vocabulary, not the installer's.
|
|
17
|
+
*
|
|
18
|
+
* 2. Probes run with a hard timeout and never reject. A capability probe that
|
|
19
|
+
* can fail is a runner that can fail to register, and "docker is missing" is
|
|
20
|
+
* not a reason to refuse to run an agent that does not need docker.
|
|
21
|
+
*
|
|
22
|
+
* 3. The result is NOT cached across calls. A host is upgraded in place — a
|
|
23
|
+
* backend can be installed and docker can appear without the runner ever
|
|
24
|
+
* restarting — which is exactly why the contract refreshes capabilities on
|
|
25
|
+
* every heartbeat rather than trusting the registration snapshot.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.probe = probe;
|
|
29
|
+
exports.probeCapabilities = probeCapabilities;
|
|
30
|
+
const node_child_process_1 = require("node:child_process");
|
|
31
|
+
const node_fs_1 = require("node:fs");
|
|
32
|
+
const node_os_1 = require("node:os");
|
|
33
|
+
const node_path_1 = require("node:path");
|
|
34
|
+
const version_1 = require("./version");
|
|
35
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
36
|
+
/**
|
|
37
|
+
* The coding-agent backends this harness knows how to drive, and the command
|
|
38
|
+
* that reports each one's version.
|
|
39
|
+
*
|
|
40
|
+
* The NAMES are the contract's open backend vocabulary and must match what the
|
|
41
|
+
* control plane puts in `LeasedAgent.backend`. The COMMANDS are what 20x's
|
|
42
|
+
* installer already probes.
|
|
43
|
+
*/
|
|
44
|
+
const BACKEND_PROBES = [
|
|
45
|
+
{ name: 'pi', command: 'pi' }
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Put the well-known install directories on PATH before probing.
|
|
49
|
+
*
|
|
50
|
+
* Straight from detect.js. Without it a backend installed by its own installer
|
|
51
|
+
* is invisible until the process restarts — on the desktop that meant "restart
|
|
52
|
+
* the app after installing", and on a runner it would mean a host that silently
|
|
53
|
+
* reports a backend it actually has.
|
|
54
|
+
*/
|
|
55
|
+
function ensureAgentPaths() {
|
|
56
|
+
const home = (0, node_os_1.homedir)();
|
|
57
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
58
|
+
const segments = (process.env.PATH || '').split(sep);
|
|
59
|
+
const knownDirs = [
|
|
60
|
+
(0, node_path_1.join)(home, '.local', 'bin') // Linux binary installs
|
|
61
|
+
];
|
|
62
|
+
for (const dir of knownDirs) {
|
|
63
|
+
if ((0, node_fs_1.existsSync)(dir) &&
|
|
64
|
+
!segments.includes(dir) &&
|
|
65
|
+
!segments.includes(`${dir}/`)) {
|
|
66
|
+
process.env.PATH = `${dir}${sep}${process.env.PATH}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run one command and pull a version out of its stdout. NEVER REJECTS.
|
|
72
|
+
*
|
|
73
|
+
* `shell: true` on Windows is load-bearing and not a style choice — without it
|
|
74
|
+
* `.cmd`/`.bat` wrappers, which is how npm installs binaries on Windows, do not
|
|
75
|
+
* resolve at all and every backend reports as missing.
|
|
76
|
+
*/
|
|
77
|
+
function probe(command, args = ['--version']) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
let settled = false;
|
|
80
|
+
const finish = (result) => {
|
|
81
|
+
if (!settled) {
|
|
82
|
+
settled = true;
|
|
83
|
+
resolve(result);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
(0, node_child_process_1.execFile)(command, args, {
|
|
88
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
89
|
+
shell: process.platform === 'win32',
|
|
90
|
+
windowsHide: true
|
|
91
|
+
}, (error, stdout) => {
|
|
92
|
+
if (error) {
|
|
93
|
+
finish({ installed: false, version: null });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const raw = String(stdout).trim();
|
|
97
|
+
// Same expression as detect.js: pick the first version-like token, and
|
|
98
|
+
// fall back to the first line when a tool prints something exotic.
|
|
99
|
+
const match = raw.match(/(\d+\.\d+[\w.-]*)/);
|
|
100
|
+
finish({
|
|
101
|
+
installed: true,
|
|
102
|
+
version: match ? match[1] : raw.split('\n')[0] || null
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// execFile can throw synchronously (EACCES on a non-executable path).
|
|
108
|
+
finish({ installed: false, version: null });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `os.hostname()` throws on some locked-down containers. A host that cannot
|
|
114
|
+
* name itself is still a perfectly good runner, so this must not be fatal.
|
|
115
|
+
*/
|
|
116
|
+
function safeHostname() {
|
|
117
|
+
try {
|
|
118
|
+
return (0, node_os_1.hostname)() || 'unknown';
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return 'unknown';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Everything the control plane needs to decide whether to send work here.
|
|
126
|
+
*
|
|
127
|
+
* All probes run CONCURRENTLY. Serially this would cost six process spawns at
|
|
128
|
+
* up to 10s each on a host where nothing is installed, and it runs on every
|
|
129
|
+
* heartbeat.
|
|
130
|
+
*/
|
|
131
|
+
async function probeCapabilities(options = {}) {
|
|
132
|
+
ensureAgentPaths();
|
|
133
|
+
const [git, docker, ...backendResults] = await Promise.all([
|
|
134
|
+
probe('git'),
|
|
135
|
+
probe('docker'),
|
|
136
|
+
...BACKEND_PROBES.map((b) => probe(b.command))
|
|
137
|
+
]);
|
|
138
|
+
const backends = BACKEND_PROBES.map((b, index) => ({
|
|
139
|
+
name: b.name,
|
|
140
|
+
result: backendResults[index]
|
|
141
|
+
}))
|
|
142
|
+
// Only INSTALLED backends are reported. A backend listed with a null
|
|
143
|
+
// version would be indistinguishable from one that is present but whose
|
|
144
|
+
// version could not be read, and the control plane routes on this list.
|
|
145
|
+
.filter((entry) => entry.result?.installed)
|
|
146
|
+
.map((entry) => ({ name: entry.name, version: entry.result.version }));
|
|
147
|
+
const sandboxToolsVersion = (0, version_1.getInstalledSandboxToolsVersion)();
|
|
148
|
+
return {
|
|
149
|
+
os: process.platform,
|
|
150
|
+
arch: process.arch,
|
|
151
|
+
harnessVersion: (0, version_1.getHarnessVersion)(),
|
|
152
|
+
...(sandboxToolsVersion ? { sandboxToolsVersion } : {}),
|
|
153
|
+
nodeVersion: process.version,
|
|
154
|
+
hostname: safeHostname(),
|
|
155
|
+
backends,
|
|
156
|
+
git: git.installed,
|
|
157
|
+
docker: docker.installed,
|
|
158
|
+
maxConcurrentSessions: options.maxConcurrentSessions ?? 1,
|
|
159
|
+
labels: options.labels
|
|
160
|
+
};
|
|
161
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const adapters_1 = require("./adapters");
|
|
38
|
+
const contract_1 = require("./contract");
|
|
39
|
+
const credential_store_1 = require("./credential-store");
|
|
40
|
+
const harness_1 = require("./harness");
|
|
41
|
+
const options_1 = require("./options");
|
|
42
|
+
const version_1 = require("./version");
|
|
43
|
+
const USAGE = `pf-agent-harness — headless Peakflo coding-agent harness
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
pf-agent-harness start [options]
|
|
47
|
+
pf-agent-harness version
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--control-plane <url> workflow-api base url (env PF_CONTROL_PLANE_URL)
|
|
51
|
+
--token <token> enrolment token (env PF_ENROLMENT_TOKEN)
|
|
52
|
+
Omit it at a terminal and the runner opens your
|
|
53
|
+
browser to sign in, then asks which organisation
|
|
54
|
+
to join — the same shape 20x uses.
|
|
55
|
+
--login force the browser sign-in even without a TTY
|
|
56
|
+
--app-url <url> where the web app lives (env PF_APP_URL);
|
|
57
|
+
derived from --control-plane when omitted
|
|
58
|
+
--label <name> runner name shown in the UI (env PF_RUNNER_LABEL,
|
|
59
|
+
default this machine's hostname)
|
|
60
|
+
--health-port <port> loopback health port (env PF_HARNESS_HEALTH_PORT, default 7777)
|
|
61
|
+
--github-proxy-port <port> loopback GitHub proxy (env PF_GITHUB_PROXY_PORT, default 7780)
|
|
62
|
+
--worktree-wip-commit <bool> save dirty work in a WIP commit after each turn
|
|
63
|
+
(env PF_WORKTREE_WIP_COMMIT; default true in
|
|
64
|
+
sandbox, false otherwise)
|
|
65
|
+
--heartbeat-interval-ms <ms> heartbeat cadence, 1000-3600000
|
|
66
|
+
(env PF_HARNESS_HEARTBEAT_INTERVAL_MS,
|
|
67
|
+
default 30000)
|
|
68
|
+
--degraded started from the warm fallback copy
|
|
69
|
+
--degraded-reason <text> why the fallback was used
|
|
70
|
+
--workspace-root <path> session workspaces (env PF_WORKSPACE_ROOT;
|
|
71
|
+
defaults under ~/.pf-agent-harness, or
|
|
72
|
+
/var/pf/workspaces in a managed sandbox)
|
|
73
|
+
--runtime <mode> sandbox | self-hosted | desktop
|
|
74
|
+
(env PF_RUNNER_RUNTIME, default desktop)
|
|
75
|
+
--max-sessions <n> concurrent sessions (env PF_MAX_CONCURRENT_SESSIONS,
|
|
76
|
+
default 10 for desktop/self-hosted; sandbox is configured by control plane)
|
|
77
|
+
--labels <a,b> routing labels (env PF_RUNNER_LABELS)
|
|
78
|
+
|
|
79
|
+
In a managed sandbox pf-bootstrap supplies these through the environment, so
|
|
80
|
+
the enrolment token never appears in /proc/<pid>/cmdline.
|
|
81
|
+
`;
|
|
82
|
+
/**
|
|
83
|
+
* Which adapter drives which backend.
|
|
84
|
+
*
|
|
85
|
+
* PI ONLY IN THIS BUILD, even though the legacy adapters remain exported for
|
|
86
|
+
* extracted and are exported from the package. The others are wired by a HOST
|
|
87
|
+
* that has verified their prerequisites: Claude Code needs
|
|
88
|
+
* `@anthropic-ai/claude-agent-sdk` (an optional dependency, absent from the
|
|
89
|
+
* sandbox image), and Codex and ACP need binaries the image does not ship.
|
|
90
|
+
* Advertising a backend the host cannot actually run would make the control
|
|
91
|
+
* plane route work to a runner that must then fail it.
|
|
92
|
+
*/
|
|
93
|
+
function resolveAdapter(backend, ports) {
|
|
94
|
+
if (backend === 'pi') {
|
|
95
|
+
// The adapter reads the per-turn gateway holder directly. If the lease has
|
|
96
|
+
// no model token SessionRunner refuses before this constructor is reached.
|
|
97
|
+
return new adapters_1.PiAdapter(ports.modelGateway);
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Backend "${backend}" is not wired in this build. ` +
|
|
100
|
+
'Only "pi" is available; the other adapters are exported for a ' +
|
|
101
|
+
'host to wire once it has verified their prerequisites.');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read `--flag value` from argv. A tiny duplicate of the reader in options.ts,
|
|
105
|
+
* because the login decision has to be made BEFORE `parseOptions` runs — that
|
|
106
|
+
* is the very call that throws when the token is missing.
|
|
107
|
+
*/
|
|
108
|
+
function readOptionValue(argv, flag) {
|
|
109
|
+
// `--flag=value` FIRST. Without this, `--token=abc` matched no `--token`
|
|
110
|
+
// argument, so a runner started with a token at a terminal fell through to
|
|
111
|
+
// the browser sign-in — asking a human to authorise something they had
|
|
112
|
+
// already authorised, and burning the token they supplied.
|
|
113
|
+
const inline = argv.find((arg) => arg.startsWith(`--${flag}=`));
|
|
114
|
+
if (inline)
|
|
115
|
+
return inline.slice(`--${flag}=`.length) || undefined;
|
|
116
|
+
const index = argv.indexOf(`--${flag}`);
|
|
117
|
+
if (index === -1)
|
|
118
|
+
return undefined;
|
|
119
|
+
const value = argv[index + 1];
|
|
120
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
121
|
+
}
|
|
122
|
+
async function main() {
|
|
123
|
+
const argv = process.argv.slice(2);
|
|
124
|
+
const command = argv[0];
|
|
125
|
+
if (!command || command === 'help' || command === '--help') {
|
|
126
|
+
process.stdout.write(USAGE);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (command === 'version' || command === '--version') {
|
|
130
|
+
process.stdout.write(`${JSON.stringify({
|
|
131
|
+
harnessVersion: (0, version_1.getHarnessVersion)(),
|
|
132
|
+
nodeVersion: process.version,
|
|
133
|
+
piVersion: (0, version_1.getPiVersion)()
|
|
134
|
+
})}\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (command !== 'start') {
|
|
138
|
+
process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
|
|
139
|
+
process.exitCode = 2;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
/*
|
|
143
|
+
* NO TOKEN? OFFER TO SIGN IN, BUT ONLY WHERE THAT CAN POSSIBLY WORK.
|
|
144
|
+
*
|
|
145
|
+
* The interactive flow needs a human at a terminal and a browser on this
|
|
146
|
+
* machine. A managed sandbox has neither, and pf-bootstrap always supplies
|
|
147
|
+
* the token through the environment — so a sandbox missing its token must
|
|
148
|
+
* FAIL FAST with the original error rather than sit for ten minutes waiting
|
|
149
|
+
* for a browser that will never open. That would turn a loud, recyclable
|
|
150
|
+
* start-up failure into a silent hang, which is the worse of the two.
|
|
151
|
+
*
|
|
152
|
+
* Hence: only when stdin and stdout are both a TTY, or when `--login` asks
|
|
153
|
+
* for it explicitly.
|
|
154
|
+
*/
|
|
155
|
+
const startArgs = argv.slice(1);
|
|
156
|
+
const wantsLogin = startArgs.includes('--login');
|
|
157
|
+
// `--token=x` counts as a token, hence readOptionValue rather than indexOf.
|
|
158
|
+
const hasToken = Boolean(readOptionValue(startArgs, 'token') ?? process.env.PF_ENROLMENT_TOKEN);
|
|
159
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
160
|
+
if (!hasToken && (wantsLogin || interactive)) {
|
|
161
|
+
const { enrolViaBrowser } = await Promise.resolve().then(() => __importStar(require('./enrol/browser-enrol')));
|
|
162
|
+
const token = await enrolViaBrowser({
|
|
163
|
+
controlPlaneUrl: readOptionValue(startArgs, 'control-plane') ??
|
|
164
|
+
process.env.PF_CONTROL_PLANE_URL ??
|
|
165
|
+
'',
|
|
166
|
+
runtime: readOptionValue(startArgs, 'runtime') ??
|
|
167
|
+
process.env.PF_RUNNER_RUNTIME ??
|
|
168
|
+
'desktop',
|
|
169
|
+
appUrl: readOptionValue(startArgs, 'app-url') ?? process.env.PF_APP_URL,
|
|
170
|
+
label: readOptionValue(startArgs, 'label') ?? process.env.PF_RUNNER_LABEL
|
|
171
|
+
});
|
|
172
|
+
// Handed on through the environment rather than argv, for the same reason
|
|
173
|
+
// pf-bootstrap does: argv is world-readable via /proc/<pid>/cmdline.
|
|
174
|
+
process.env.PF_ENROLMENT_TOKEN = token;
|
|
175
|
+
}
|
|
176
|
+
const options = (0, options_1.parseOptions)(startArgs, process.env);
|
|
177
|
+
const handedOffCredential = (0, credential_store_1.takeHandedOffCredential)(process.env);
|
|
178
|
+
const harness = new harness_1.Harness(options, {
|
|
179
|
+
getAdapter: (backend, ports) => resolveAdapter(backend, ports),
|
|
180
|
+
...(handedOffCredential
|
|
181
|
+
? { credentialStore: new credential_store_1.MemoryCredentialStore(handedOffCredential) }
|
|
182
|
+
: {}),
|
|
183
|
+
onUpgradeRequested: (request) => {
|
|
184
|
+
if (typeof process.send === 'function') {
|
|
185
|
+
process.send({ type: 'pf-harness-upgrade', ...request });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
let shuttingDown = false;
|
|
190
|
+
const shutdown = (signal) => {
|
|
191
|
+
if (shuttingDown) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
shuttingDown = true;
|
|
195
|
+
process.stderr.write(`Received ${signal}, shutting down\n`);
|
|
196
|
+
void harness.stop().then(() => process.exit(0));
|
|
197
|
+
};
|
|
198
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
199
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
200
|
+
await harness.start();
|
|
201
|
+
// BLOCK ON THE LEASE LOOP, and let it decide when the process is done.
|
|
202
|
+
//
|
|
203
|
+
// The Phase 0 skeleton relied on the health socket keeping the event loop
|
|
204
|
+
// alive, which meant nothing could ever end the process deliberately — a
|
|
205
|
+
// refused protocol version would have hung instead of exiting. `wait()`
|
|
206
|
+
// resolves on a clean drain and rejects when the control plane refused this
|
|
207
|
+
// build, so the exit code carries the reason.
|
|
208
|
+
try {
|
|
209
|
+
await harness.wait();
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error instanceof harness_1.UpgradeRequestedError) {
|
|
213
|
+
process.exit(contract_1.HARNESS_UPGRADE_EXIT_CODE);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
main().catch((error) => {
|
|
219
|
+
// process.exit, NOT process.exitCode. `exitCode` only takes effect when the
|
|
220
|
+
// event loop drains on its own, and a start-up failure can leave a listening
|
|
221
|
+
// socket or a pending timer behind, which would keep the process alive for
|
|
222
|
+
// ever. A sandbox that hangs is worse than one that dies: the control plane
|
|
223
|
+
// cannot recycle it, so the version gate would never take effect.
|
|
224
|
+
if (error instanceof options_1.OptionsError) {
|
|
225
|
+
process.stderr.write(`${error.message}\n\n${USAGE}`);
|
|
226
|
+
process.exit(2);
|
|
227
|
+
}
|
|
228
|
+
const message = error instanceof Error ? error.stack : String(error);
|
|
229
|
+
process.stderr.write(`pf-agent-harness failed to start:\n${message}\n`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|