@peakflo/agent-harness 0.2.9 → 0.2.10-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.
@@ -0,0 +1,535 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServiceManager = void 0;
4
+ exports.permanentServiceExitCode = permanentServiceExitCode;
5
+ exports.permanentStartupFailure = permanentStartupFailure;
6
+ exports.isServiceStart = isServiceStart;
7
+ exports.serviceStartArgs = serviceStartArgs;
8
+ exports.renderLaunchAgent = renderLaunchAgent;
9
+ exports.renderSystemdUnit = renderSystemdUnit;
10
+ exports.recordServiceFailure = recordServiceFailure;
11
+ exports.clearServiceFailure = clearServiceFailure;
12
+ exports.runServiceCommand = runServiceCommand;
13
+ const node_child_process_1 = require("node:child_process");
14
+ const node_fs_1 = require("node:fs");
15
+ const node_net_1 = require("node:net");
16
+ const node_os_1 = require("node:os");
17
+ const node_path_1 = require("node:path");
18
+ const credential_store_1 = require("./credential-store");
19
+ const node_floor_1 = require("./node-floor");
20
+ const options_1 = require("./options");
21
+ const vendor_backends_1 = require("./vendor-backends");
22
+ const version_1 = require("./version");
23
+ const LABEL = 'ai.peakflo.agent-harness';
24
+ const UNIT = 'pf-agent-harness.service';
25
+ const VALUE_FLAGS = new Set([
26
+ '--control-plane',
27
+ '--app-url',
28
+ '--label',
29
+ '--health-port',
30
+ '--github-proxy-port',
31
+ '--worktree-wip-commit',
32
+ '--heartbeat-interval-ms',
33
+ '--workspace-root',
34
+ '--runtime',
35
+ '--max-sessions',
36
+ '--labels',
37
+ '--vendor-harnesses'
38
+ ]);
39
+ const SWITCH_FLAGS = new Set(['--no-harness-discovery']);
40
+ const LAST_ERROR_FILE = 'service-last-error';
41
+ function permanentServiceExitCode(platform, linuxCode) {
42
+ return platform === 'darwin' ? 0 : linuxCode;
43
+ }
44
+ /** Only failures that a restart cannot repair belong in this list. */
45
+ function permanentStartupFailure(error) {
46
+ const failure = error;
47
+ const code = failure?.code;
48
+ if (code === 'EADDRINUSE')
49
+ return 'local port already in use';
50
+ if (code === 'EACCES' && failure?.syscall === 'listen')
51
+ return 'local port access denied';
52
+ if (code === 'ERR_INVALID_URL')
53
+ return 'invalid service URL';
54
+ return null;
55
+ }
56
+ /** Existing service files use --stored-credential; new ones also use --service. */
57
+ function isServiceStart(argv) {
58
+ return (argv[0] === 'start' &&
59
+ (argv.slice(1).includes('--service') ||
60
+ argv.slice(1).includes('--stored-credential')));
61
+ }
62
+ function portOccupied(port) {
63
+ return new Promise((resolve, reject) => {
64
+ const server = (0, node_net_1.createServer)();
65
+ server.once('error', (error) => {
66
+ if (error.code === 'EADDRINUSE')
67
+ resolve(true);
68
+ else
69
+ reject(error);
70
+ });
71
+ server.listen(port, '127.0.0.1', () => server.close(() => resolve(false)));
72
+ });
73
+ }
74
+ function safe(value) {
75
+ if (/[\r\n\0]/.test(value))
76
+ throw new Error('Service values cannot contain line breaks or NUL.');
77
+ return value;
78
+ }
79
+ function xml(value) {
80
+ return safe(value)
81
+ .replace(/&/g, '&')
82
+ .replace(/</g, '&lt;')
83
+ .replace(/>/g, '&gt;')
84
+ .replace(/"/g, '&quot;')
85
+ .replace(/'/g, '&apos;');
86
+ }
87
+ function systemd(value) {
88
+ return `"${safe(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '$$$$').replace(/%/g, '%%')}"`;
89
+ }
90
+ function systemdEnvironment(value) {
91
+ // systemd does not expand $ in Environment= values.
92
+ return `"${safe(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
93
+ }
94
+ function argValue(args, flag) {
95
+ const at = args.indexOf(flag);
96
+ return at >= 0 ? args[at + 1] : undefined;
97
+ }
98
+ /** Parse only documented start options. A token can never enter service state. */
99
+ function serviceStartArgs(input, env, home) {
100
+ const values = new Map();
101
+ const switches = new Set();
102
+ for (let index = 0; index < input.length; index++) {
103
+ const raw = input[index];
104
+ const equals = raw.indexOf('=');
105
+ const flag = equals >= 0 ? raw.slice(0, equals) : raw;
106
+ if (SWITCH_FLAGS.has(flag) && equals < 0) {
107
+ switches.add(flag);
108
+ continue;
109
+ }
110
+ if (!VALUE_FLAGS.has(flag)) {
111
+ throw new Error(`Unsupported service start option: ${flag}`);
112
+ }
113
+ const value = equals >= 0 ? raw.slice(equals + 1) : input[++index];
114
+ if (!value || value.startsWith('--'))
115
+ throw new Error(`Missing value for ${flag}.`);
116
+ values.set(flag, safe(value));
117
+ }
118
+ const controlPlane = values.get('--control-plane') ?? env.PF_CONTROL_PLANE_URL;
119
+ if (!controlPlane)
120
+ throw new Error('Set --control-plane for service install.');
121
+ const validateUrl = (value, name) => {
122
+ let url;
123
+ try {
124
+ url = new URL(value);
125
+ }
126
+ catch {
127
+ throw new Error(`The service ${name} URL is invalid.`);
128
+ }
129
+ if (url.username ||
130
+ url.password ||
131
+ url.search ||
132
+ url.hash ||
133
+ (url.protocol !== 'https:' &&
134
+ !(url.protocol === 'http:' &&
135
+ ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) {
136
+ throw new Error(`The service ${name} URL must use HTTPS (or HTTP on loopback) and have no credentials, query, or fragment.`);
137
+ }
138
+ };
139
+ validateUrl(controlPlane, 'control plane');
140
+ values.set('--control-plane', safe(controlPlane));
141
+ const appUrl = values.get('--app-url');
142
+ if (appUrl)
143
+ validateUrl(appUrl, 'app');
144
+ const runtime = values.get('--runtime') ?? env.PF_RUNNER_RUNTIME ?? 'desktop';
145
+ if (runtime !== 'desktop' && runtime !== 'self-hosted') {
146
+ throw new Error('A user service needs desktop or self-hosted runtime.');
147
+ }
148
+ values.set('--runtime', runtime);
149
+ const root = values.get('--workspace-root') ??
150
+ env.PF_WORKSPACE_ROOT ??
151
+ (0, node_path_1.join)(home, '.pf-agent-harness', 'workspaces');
152
+ values.set('--workspace-root', (0, node_path_1.resolve)(root));
153
+ if (env.PF_VENDOR_HARNESSES && !values.has('--vendor-harnesses')) {
154
+ values.set('--vendor-harnesses', safe(env.PF_VENDOR_HARNESSES));
155
+ }
156
+ const vendorNames = values.get('--vendor-harnesses');
157
+ if (vendorNames !== undefined) {
158
+ const names = vendorNames
159
+ .split(',')
160
+ .map((name) => name.trim())
161
+ .filter(Boolean);
162
+ if (names.length === 0 || names.some((name) => !(0, vendor_backends_1.isVendorBackend)(name))) {
163
+ throw new Error('Invalid vendor harness list. Use codex, claude-code, or cursor.');
164
+ }
165
+ values.set('--vendor-harnesses', [...new Set(names)].join(','));
166
+ }
167
+ // Keep a stable, explicit order so install is repeatable.
168
+ const result = ['--service', '--stored-credential'];
169
+ for (const flag of VALUE_FLAGS) {
170
+ const value = values.get(flag);
171
+ if (value !== undefined)
172
+ result.push(flag, value);
173
+ }
174
+ for (const flag of SWITCH_FLAGS)
175
+ if (switches.has(flag))
176
+ result.push(flag);
177
+ return result;
178
+ }
179
+ function renderLaunchAgent(config, logDir) {
180
+ const args = [config.nodePath, config.cliPath, 'start', ...config.startArgs];
181
+ const array = args
182
+ .map((arg) => ` <string>${xml(arg)}</string>`)
183
+ .join('\n');
184
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${LABEL}</string>\n <key>ProgramArguments</key><array>\n${array}\n </array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n <key>ThrottleInterval</key><integer>30</integer>\n <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(config.path)}</string></dict>\n <key>StandardOutPath</key><string>${xml((0, node_path_1.join)(logDir, 'stdout.log'))}</string>\n <key>StandardErrorPath</key><string>${xml((0, node_path_1.join)(logDir, 'stderr.log'))}</string>\n</dict>\n</plist>\n`;
185
+ }
186
+ function renderSystemdUnit(config) {
187
+ const command = [
188
+ config.nodePath,
189
+ config.cliPath,
190
+ 'start',
191
+ ...config.startArgs
192
+ ]
193
+ .map(systemd)
194
+ .join(' ');
195
+ return `[Unit]\nDescription=Peakflo agent harness\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${command}\nEnvironment=${systemdEnvironment(`PATH=${config.path}`)}\nRestart=always\nRestartSec=30s\nRestartPreventExitStatus=2 78\n\n[Install]\nWantedBy=default.target\n`;
196
+ }
197
+ function defaultRun(file, args) {
198
+ return (0, node_child_process_1.execFileSync)(file, args, {
199
+ encoding: 'utf8',
200
+ stdio: ['ignore', 'pipe', 'pipe']
201
+ });
202
+ }
203
+ function sleep(ms) {
204
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
205
+ }
206
+ function writePrivate(file, contents) {
207
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(file), { recursive: true, mode: 0o700 });
208
+ const temp = `${file}.${process.pid}.tmp`;
209
+ try {
210
+ (0, node_fs_1.writeFileSync)(temp, contents, { encoding: 'utf8', mode: 0o600 });
211
+ (0, node_fs_1.chmodSync)(temp, 0o600);
212
+ (0, node_fs_1.renameSync)(temp, file);
213
+ }
214
+ finally {
215
+ (0, node_fs_1.rmSync)(temp, { force: true });
216
+ }
217
+ }
218
+ function recordServiceFailure(reason, home = (0, node_os_1.homedir)()) {
219
+ writePrivate((0, node_path_1.join)(home, '.pf-agent-harness', LAST_ERROR_FILE), `${safe(reason)}\n`);
220
+ }
221
+ function clearServiceFailure(home = (0, node_os_1.homedir)()) {
222
+ (0, node_fs_1.rmSync)((0, node_path_1.join)(home, '.pf-agent-harness', LAST_ERROR_FILE), { force: true });
223
+ }
224
+ class ServiceManager {
225
+ context;
226
+ home;
227
+ platform;
228
+ run;
229
+ output;
230
+ stateFile;
231
+ serviceFile;
232
+ logDir;
233
+ constructor(context) {
234
+ this.context = context;
235
+ this.home = context.home ?? (0, node_os_1.homedir)();
236
+ this.platform = context.platform ?? process.platform;
237
+ this.run = context.run ?? defaultRun;
238
+ this.output =
239
+ context.output ?? ((message) => process.stdout.write(`${message}\n`));
240
+ this.stateFile = (0, node_path_1.join)(this.home, '.pf-agent-harness', 'service.json');
241
+ this.logDir = (0, node_path_1.join)(this.home, '.pf-agent-harness', 'logs');
242
+ this.serviceFile =
243
+ this.platform === 'darwin'
244
+ ? (0, node_path_1.join)(this.home, 'Library', 'LaunchAgents', `${LABEL}.plist`)
245
+ : (0, node_path_1.join)(this.home, '.config', 'systemd', 'user', UNIT);
246
+ }
247
+ supported() {
248
+ if (this.platform !== 'darwin' && this.platform !== 'linux') {
249
+ throw new Error('Background service is not supported on Windows yet.');
250
+ }
251
+ }
252
+ readConfig() {
253
+ const config = JSON.parse((0, node_fs_1.readFileSync)(this.stateFile, 'utf8'));
254
+ if (!Array.isArray(config.startArgs) ||
255
+ !config.startArgs.includes('--stored-credential') ||
256
+ !(0, node_path_1.isAbsolute)(config.nodePath) ||
257
+ !(0, node_path_1.isAbsolute)(config.cliPath) ||
258
+ typeof config.path !== 'string') {
259
+ throw new Error('Service settings are invalid. Run service install again.');
260
+ }
261
+ config.startArgs = serviceStartArgs(config.startArgs.filter((arg) => arg !== '--stored-credential' && arg !== '--service'), {}, this.home);
262
+ safe(config.nodePath);
263
+ safe(config.cliPath);
264
+ safe(config.path);
265
+ return config;
266
+ }
267
+ checkCredential(args) {
268
+ const root = argValue(args, '--workspace-root');
269
+ const stored = new credential_store_1.FileCredentialStore(root ?? '').load();
270
+ if (!stored ||
271
+ (stored.credentialExpiresAt &&
272
+ Date.parse(stored.credentialExpiresAt) <= Date.now())) {
273
+ throw new Error('No usable saved runner credential. Start the runner once in a terminal, then install the service.');
274
+ }
275
+ }
276
+ writeAndStart(config) {
277
+ if (!(0, node_fs_1.existsSync)(config.nodePath) || !(0, node_fs_1.existsSync)(config.cliPath)) {
278
+ throw new Error('Node.js or the harness CLI is missing. Reinstall the package and run service repair.');
279
+ }
280
+ (0, node_fs_1.mkdirSync)(this.logDir, { recursive: true, mode: 0o700 });
281
+ writePrivate(this.stateFile, `${JSON.stringify(config, null, 2)}\n`);
282
+ if (this.platform === 'darwin') {
283
+ writePrivate(this.serviceFile, renderLaunchAgent(config, this.logDir));
284
+ const domain = `gui/${this.context.uid ?? process.getuid?.() ?? (0, node_os_1.userInfo)().uid}`;
285
+ try {
286
+ this.run('launchctl', ['bootout', `${domain}/${LABEL}`]);
287
+ }
288
+ catch {
289
+ /* absent */
290
+ }
291
+ // launchd can hold the old label for several seconds after bootout.
292
+ const delays = [250, 500, 1000, 2000, 4000, 2000];
293
+ for (let attempt = 0; attempt <= delays.length; attempt++) {
294
+ try {
295
+ this.run('launchctl', ['bootstrap', domain, this.serviceFile]);
296
+ break;
297
+ }
298
+ catch (error) {
299
+ if (attempt === delays.length) {
300
+ throw new Error('launchd could not start the agent after 10 seconds. Run pf-agent-harness service repair.', { cause: error });
301
+ }
302
+ (this.context.sleep ?? sleep)(delays[attempt]);
303
+ }
304
+ }
305
+ }
306
+ else {
307
+ writePrivate(this.serviceFile, renderSystemdUnit(config));
308
+ this.run('systemctl', ['--user', 'daemon-reload']);
309
+ this.run('systemctl', ['--user', 'enable', UNIT]);
310
+ try {
311
+ this.run('systemctl', ['--user', 'reset-failed', UNIT]);
312
+ }
313
+ catch {
314
+ // A new unit may have no failed state to clear. Restart gives the
315
+ // operator the actionable error if systemd cannot start it.
316
+ }
317
+ this.run('systemctl', ['--user', 'restart', UNIT]);
318
+ }
319
+ }
320
+ async install(input) {
321
+ this.supported();
322
+ this.checkNodeVersion();
323
+ const startArgs = serviceStartArgs(input, this.context.env, this.home);
324
+ this.checkCredential(startArgs);
325
+ const { healthPort } = (0, options_1.parseOptions)(startArgs, {});
326
+ let occupied;
327
+ try {
328
+ occupied = await (this.context.portOccupied ?? portOccupied)(healthPort);
329
+ }
330
+ catch (error) {
331
+ throw new Error(`Health port ${healthPort} could not be checked. Check access to the local port before service install.`, { cause: error });
332
+ }
333
+ if (occupied) {
334
+ let installedServiceOwnsPort = false;
335
+ try {
336
+ const prior = this.readConfig();
337
+ const priorPort = (0, options_1.parseOptions)(prior.startArgs, {}).healthPort;
338
+ if ((0, node_fs_1.existsSync)(this.serviceFile) && priorPort === healthPort) {
339
+ const info = this.platform === 'darwin'
340
+ ? this.run('launchctl', [
341
+ 'print',
342
+ `gui/${this.context.uid ?? process.getuid?.() ?? (0, node_os_1.userInfo)().uid}/${LABEL}`
343
+ ])
344
+ : this.run('systemctl', [
345
+ '--user',
346
+ 'show',
347
+ UNIT,
348
+ '--property=ActiveState',
349
+ '--property=MainPID'
350
+ ]);
351
+ installedServiceOwnsPort =
352
+ this.platform === 'darwin'
353
+ ? /\bstate = running\b/.test(info) &&
354
+ /\bpid = [1-9]\d*/.test(info)
355
+ : /^ActiveState=active$/m.test(info) &&
356
+ /^MainPID=[1-9]\d*/m.test(info);
357
+ }
358
+ }
359
+ catch {
360
+ /* The port is still in use, so refuse unless ownership is clear. */
361
+ }
362
+ if (!installedServiceOwnsPort) {
363
+ throw new Error(`Health port ${healthPort} is in use. Stop the manual runner before service install.`);
364
+ }
365
+ }
366
+ const config = {
367
+ startArgs,
368
+ nodePath: this.context.nodePath,
369
+ cliPath: this.context.cliPath,
370
+ path: this.context.env.PATH ?? ''
371
+ };
372
+ this.writeAndStart(config);
373
+ this.output(`Service installed. Logs: ${this.logDir}`);
374
+ if (this.context.cliPath.includes('/_npx/') ||
375
+ this.context.cliPath.includes('/.npm/_npx/')) {
376
+ this.output('Warning: this harness is in the npm npx cache. Install it globally, then run service repair.');
377
+ }
378
+ if (this.context.nodePath.includes('/.nvm/versions/node/')) {
379
+ this.output('Warning: this Node.js path can change after an nvm upgrade. Run service repair after upgrades.');
380
+ }
381
+ this.output('After a Node.js or harness upgrade, run: pf-agent-harness service repair');
382
+ if (this.platform === 'linux')
383
+ this.output('To run after logout, ask your system administrator to run: loginctl enable-linger $USER');
384
+ }
385
+ checkNodeVersion() {
386
+ const floor = (0, node_floor_1.checkNodeFloor)({
387
+ current: process.versions.node,
388
+ floor: (0, version_1.getNodeFloor)(),
389
+ runtime: 'desktop',
390
+ env: this.context.env
391
+ });
392
+ if (floor.action === 'refuse')
393
+ throw new Error(floor.message);
394
+ }
395
+ repair() {
396
+ this.supported();
397
+ this.checkNodeVersion();
398
+ const prior = this.readConfig();
399
+ this.checkCredential(prior.startArgs);
400
+ this.writeAndStart({
401
+ ...prior,
402
+ nodePath: this.context.nodePath,
403
+ cliPath: this.context.cliPath,
404
+ path: this.context.env.PATH ?? prior.path
405
+ });
406
+ this.output('Service repaired and restarted.');
407
+ }
408
+ assertNoDuplicateRunner(options) {
409
+ if (!(0, node_fs_1.existsSync)(this.serviceFile) || !(0, node_fs_1.existsSync)(this.stateFile))
410
+ return;
411
+ let config;
412
+ let info;
413
+ try {
414
+ config = this.readConfig();
415
+ info =
416
+ this.platform === 'darwin'
417
+ ? this.run('launchctl', [
418
+ 'print',
419
+ `gui/${this.context.uid ?? process.getuid?.() ?? (0, node_os_1.userInfo)().uid}/${LABEL}`
420
+ ])
421
+ : this.run('systemctl', [
422
+ '--user',
423
+ 'show',
424
+ UNIT,
425
+ '--property=ActiveState',
426
+ '--property=MainPID'
427
+ ]);
428
+ }
429
+ catch {
430
+ return;
431
+ }
432
+ const pid = Number(this.platform === 'darwin'
433
+ ? info.match(/\bpid = (\d+)/)?.[1]
434
+ : info.match(/^MainPID=(\d+)$/m)?.[1]);
435
+ if (!pid || pid === process.pid)
436
+ return;
437
+ const running = this.platform === 'darwin'
438
+ ? /\bstate = running\b/.test(info)
439
+ : /^ActiveState=active$/m.test(info);
440
+ if (!running)
441
+ return;
442
+ const serviceRoot = argValue(config.startArgs, '--workspace-root');
443
+ const servicePort = Number(argValue(config.startArgs, '--health-port') ?? '7777');
444
+ if ((serviceRoot &&
445
+ (0, node_path_1.resolve)(serviceRoot) === (0, node_path_1.resolve)(options.workspaceRoot)) ||
446
+ servicePort === options.healthPort) {
447
+ throw new Error('The background service is already running with this workspace or health port. Stop it before a manual start.');
448
+ }
449
+ }
450
+ status() {
451
+ this.supported();
452
+ if (!(0, node_fs_1.existsSync)(this.serviceFile)) {
453
+ this.output('Status: stopped. Service is not installed.');
454
+ return;
455
+ }
456
+ let savedError = '';
457
+ try {
458
+ savedError = (0, node_fs_1.readFileSync)((0, node_path_1.join)(this.home, '.pf-agent-harness', LAST_ERROR_FILE), 'utf8').trim();
459
+ }
460
+ catch {
461
+ /* no saved failure */
462
+ }
463
+ try {
464
+ if (this.platform === 'darwin') {
465
+ const domain = `gui/${this.context.uid ?? process.getuid?.() ?? (0, node_os_1.userInfo)().uid}`;
466
+ const info = this.run('launchctl', ['print', `${domain}/${LABEL}`]);
467
+ const running = /\bstate = running\b/.test(info);
468
+ const exit = info.match(/last exit code = (\d+)/)?.[1];
469
+ this.output(`Status: ${running ? 'running' : 'stopped'}. Last error: ${!running && savedError ? savedError : exit && exit !== '0' ? `exit ${exit}` : 'none'}.`);
470
+ }
471
+ else {
472
+ const info = this.run('systemctl', [
473
+ '--user',
474
+ 'show',
475
+ UNIT,
476
+ '--property=ActiveState',
477
+ '--property=Result',
478
+ '--property=ExecMainStatus'
479
+ ]);
480
+ const state = info.match(/^ActiveState=(.*)$/m)?.[1];
481
+ const result = info.match(/^Result=(.*)$/m)?.[1];
482
+ const code = info.match(/^ExecMainStatus=(.*)$/m)?.[1];
483
+ this.output(`Status: ${state === 'active' ? 'running' : 'stopped'}. Last error: ${state !== 'active' && savedError ? savedError : result && result !== 'success' ? `${result}${code && code !== '0' ? ` (exit ${code})` : ''}` : 'none'}.`);
484
+ }
485
+ }
486
+ catch {
487
+ this.output(`Status: stopped. Last error: ${savedError || 'service manager could not read status'}.`);
488
+ }
489
+ }
490
+ uninstall() {
491
+ this.supported();
492
+ if (this.platform === 'darwin') {
493
+ const domain = `gui/${this.context.uid ?? process.getuid?.() ?? (0, node_os_1.userInfo)().uid}`;
494
+ try {
495
+ this.run('launchctl', ['bootout', `${domain}/${LABEL}`]);
496
+ }
497
+ catch {
498
+ /* absent */
499
+ }
500
+ }
501
+ else if ((0, node_fs_1.existsSync)(this.serviceFile)) {
502
+ this.run('systemctl', ['--user', 'disable', '--now', UNIT]);
503
+ }
504
+ (0, node_fs_1.rmSync)(this.serviceFile, { force: true });
505
+ (0, node_fs_1.rmSync)(this.stateFile, { force: true });
506
+ clearServiceFailure(this.home);
507
+ if (this.platform === 'linux')
508
+ this.run('systemctl', ['--user', 'daemon-reload']);
509
+ this.output('Service removed. The saved runner credential remains in the workspace.');
510
+ }
511
+ }
512
+ exports.ServiceManager = ServiceManager;
513
+ async function runServiceCommand(args, context) {
514
+ const manager = new ServiceManager(context);
515
+ switch (args[0]) {
516
+ case 'help':
517
+ case '--help':
518
+ (context.output ?? ((message) => process.stdout.write(`${message}\n`)))('Use service install [start options], service status, service repair, or service uninstall.');
519
+ break;
520
+ case 'install':
521
+ await manager.install(args.slice(1));
522
+ break;
523
+ case 'status':
524
+ manager.status();
525
+ break;
526
+ case 'repair':
527
+ manager.repair();
528
+ break;
529
+ case 'uninstall':
530
+ manager.uninstall();
531
+ break;
532
+ default:
533
+ throw new Error('Use service install, status, repair, or uninstall.');
534
+ }
535
+ }
package/dist/types.d.ts CHANGED
@@ -95,6 +95,8 @@ export interface HarnessOptions {
95
95
  * sandbox runner never runs discovery, whatever this says.
96
96
  */
97
97
  harnessDiscovery?: boolean;
98
+ /** Vendor backends the machine owner explicitly enabled. Empty by default. */
99
+ vendorHarnesses?: string[];
98
100
  }
99
101
  /**
100
102
  * Every request body in this contract carries the protocol version, so the
@@ -203,6 +205,8 @@ export interface RunnerCapabilities {
203
205
  hostname: string;
204
206
  /** Coding-agent backends found on this host, with the version detected. */
205
207
  backends: RunnerBackend[];
208
+ /** Explicit local consent. An old runner without this field cannot take vendor turns. */
209
+ vendorHarnesses?: string[];
206
210
  /**
207
211
  * Flat union of every backend's reported models, deduplicated by id.
208
212
  *
@@ -557,6 +561,7 @@ export interface LeasedSession {
557
561
  * Space, and for a control plane that predates the capability.
558
562
  */
559
563
  spaceFiles?: LeasedSpaceFiles;
564
+ taskFiles?: LeasedTaskFiles;
560
565
  }
561
566
  export interface LeasedAgent {
562
567
  agentId: string;
@@ -646,6 +651,22 @@ export interface LeasedAttachment {
646
651
  url: string;
647
652
  sizeBytes?: number;
648
653
  }
654
+ export interface LeasedTaskFiles {
655
+ taskId: string;
656
+ /** API endpoint; authenticated by the lease's MCP token, never a signed URL. */
657
+ baseUrl: string;
658
+ files: Array<{
659
+ index: number;
660
+ relativePath: string;
661
+ mimeType: string;
662
+ sizeBytes: number;
663
+ revision: string;
664
+ }>;
665
+ skipped: Array<{
666
+ name: string;
667
+ reason: string;
668
+ }>;
669
+ }
649
670
  /**
650
671
  * The Space files manifest (P9). Present only when the runner advertised the
651
672
  * `spaceFiles` capability. The control plane decides WHICH files; the harness
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peakflo/agent-harness",
3
- "version": "0.2.9",
3
+ "version": "0.2.10-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",