@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.
- package/README.md +123 -13
- package/dist/adapters/acp-adapter.js +26 -38
- package/dist/adapters/claude-code-adapter.d.ts +4 -14
- package/dist/adapters/claude-code-adapter.js +86 -22
- package/dist/adapters/codex-adapter.js +17 -6
- package/dist/adapters/codex-app-server-adapter.js +9 -9
- package/dist/adapters/cursor-acp.d.ts +2 -3
- package/dist/adapters/cursor-acp.js +2 -3
- package/dist/adapters/pi-adapter.js +3 -0
- package/dist/adapters/vendor-environment.d.ts +12 -0
- package/dist/adapters/vendor-environment.js +186 -0
- package/dist/cli.js +55 -1
- package/dist/github-command-guards.js +5 -0
- package/dist/github-workspace.js +30 -3
- package/dist/harness.d.ts +4 -0
- package/dist/harness.js +32 -7
- package/dist/options.js +18 -2
- package/dist/runtime/permission-mode.d.ts +2 -3
- package/dist/runtime/permission-mode.js +2 -5
- package/dist/runtime/session-runner.d.ts +2 -0
- package/dist/runtime/session-runner.js +96 -16
- package/dist/runtime/task-files.d.ts +17 -0
- package/dist/runtime/task-files.js +290 -0
- package/dist/service.d.ts +57 -0
- package/dist/service.js +535 -0
- package/dist/types.d.ts +21 -0
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ const string_decoder_1 = require("string_decoder");
|
|
|
21
21
|
const util_1 = require("util");
|
|
22
22
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
23
23
|
const model_gateway_1 = require("./model-gateway");
|
|
24
|
+
const vendor_environment_1 = require("./vendor-environment");
|
|
24
25
|
const cold_start_steps_1 = require("../runtime/cold-start-steps");
|
|
25
26
|
const dev_server_guard_1 = require("../runtime/dev-server-guard");
|
|
26
27
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
@@ -66,6 +67,7 @@ function findPiNodeRuntime(platform, electronExecutable, pathExists = fs_1.exist
|
|
|
66
67
|
*/
|
|
67
68
|
exports.PI_MCP_SERVER_ALIASES = {
|
|
68
69
|
'[Workflo] Organisation Workspace': 'workflo',
|
|
70
|
+
'[Spaces] Organisation Workspace': 'workflo',
|
|
69
71
|
'[Workflo] MCP Dev Server': 'workflo-dev'
|
|
70
72
|
};
|
|
71
73
|
function slugifyPiName(value, maxLength) {
|
|
@@ -428,6 +430,7 @@ class PiAdapter {
|
|
|
428
430
|
// precedence over its config, which would undo `directTools: false` and
|
|
429
431
|
// recreate overlong provider-facing names.
|
|
430
432
|
delete env.MCP_DIRECT_TOOLS;
|
|
433
|
+
env.GIT_SSH_COMMAND = vendor_environment_1.DISABLED_GIT_SSH_COMMAND;
|
|
431
434
|
return env;
|
|
432
435
|
}
|
|
433
436
|
buildSkillFiles(config) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SessionConfig } from './coding-agent-adapter';
|
|
2
|
+
export declare function allowedVendorSecretKey(key: string): boolean;
|
|
3
|
+
/** Remove folders for vendor sessions that have not been used for 90 days. */
|
|
4
|
+
export declare function sweepVendorConfigDirs(now?: number): number;
|
|
5
|
+
export declare function vendorConfigDir(kind: 'claude' | 'codex' | 'cursor', config: SessionConfig, sourceDir: string, authFiles: readonly string[]): string;
|
|
6
|
+
/** Keep Claude API-key turns away from a subscription credential link. */
|
|
7
|
+
export declare function claudeVendorConfigDir(config: SessionConfig): string;
|
|
8
|
+
/** The isolated Codex home can use only the owner's file-based login. */
|
|
9
|
+
export declare function codexVendorConfigDir(config: SessionConfig, useApiKey: boolean): string;
|
|
10
|
+
export declare function isolateVendorHome(env: NodeJS.ProcessEnv, directory: string): void;
|
|
11
|
+
export declare const DISABLED_GIT_SSH_COMMAND: string;
|
|
12
|
+
export declare function vendorEnvironment(config: SessionConfig, auth?: Record<string, string | undefined>): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DISABLED_GIT_SSH_COMMAND = void 0;
|
|
4
|
+
exports.allowedVendorSecretKey = allowedVendorSecretKey;
|
|
5
|
+
exports.sweepVendorConfigDirs = sweepVendorConfigDirs;
|
|
6
|
+
exports.vendorConfigDir = vendorConfigDir;
|
|
7
|
+
exports.claudeVendorConfigDir = claudeVendorConfigDir;
|
|
8
|
+
exports.codexVendorConfigDir = codexVendorConfigDir;
|
|
9
|
+
exports.isolateVendorHome = isolateVendorHome;
|
|
10
|
+
exports.vendorEnvironment = vendorEnvironment;
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_os_1 = require("node:os");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
// A vendor CLI runs commands on behalf of the agent. Do not give it the
|
|
15
|
+
// runner's control-plane credentials or the owner's GitHub login.
|
|
16
|
+
const HOST_KEYS = [
|
|
17
|
+
'PATH',
|
|
18
|
+
'USER',
|
|
19
|
+
'TMPDIR',
|
|
20
|
+
'TMP',
|
|
21
|
+
'TEMP',
|
|
22
|
+
'SHELL',
|
|
23
|
+
'LANG',
|
|
24
|
+
'LC_ALL',
|
|
25
|
+
'LC_CTYPE',
|
|
26
|
+
'TERM',
|
|
27
|
+
'SYSTEMROOT',
|
|
28
|
+
'WINDIR',
|
|
29
|
+
'COMSPEC',
|
|
30
|
+
'PATHEXT'
|
|
31
|
+
];
|
|
32
|
+
const SESSION_KEYS = [
|
|
33
|
+
'HTTPS_PROXY',
|
|
34
|
+
'https_proxy',
|
|
35
|
+
'NO_PROXY',
|
|
36
|
+
'no_proxy',
|
|
37
|
+
'SSL_CERT_FILE',
|
|
38
|
+
'GIT_SSL_CAINFO',
|
|
39
|
+
'CURL_CA_BUNDLE',
|
|
40
|
+
'NODE_EXTRA_CA_CERTS',
|
|
41
|
+
'GH_TOKEN',
|
|
42
|
+
'GH_CONFIG_DIR'
|
|
43
|
+
];
|
|
44
|
+
const FORBIDDEN = /^(?:PF_|PEAKFLO_|GH_|GIT_|GITHUB_TOKEN$|SSH_|HOME$|USERPROFILE$|CODEX_HOME$|CLAUDE_CONFIG_DIR$|CLAUDE_SECURESTORAGE_CONFIG_DIR$|XDG_CONFIG_HOME$|APPDATA$|LOCALAPPDATA$|PATH$|HTTPS?_PROXY$|NO_PROXY$|SSL_CERT_FILE$|CURL_CA_BUNDLE$|NODE_EXTRA_CA_CERTS$|NODE_OPTIONS$|LD_PRELOAD$|DYLD_)/i;
|
|
45
|
+
function allowedVendorSecretKey(key) {
|
|
46
|
+
return !FORBIDDEN.test(key);
|
|
47
|
+
}
|
|
48
|
+
// Persist across harness restarts so native vendor sessions can resume.
|
|
49
|
+
// Each runner OS account owns this directory; it is not under the worktree.
|
|
50
|
+
const configRoot = process.env.NODE_ENV === 'test'
|
|
51
|
+
? (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'pf-vendor-config-test-'))
|
|
52
|
+
: (0, node_path_1.join)((0, node_os_1.homedir)(), '.pf-agent-harness', 'vendor-config');
|
|
53
|
+
if (process.env.NODE_ENV === 'test') {
|
|
54
|
+
process.once('exit', () => (0, node_fs_1.rmSync)(configRoot, { recursive: true, force: true }));
|
|
55
|
+
}
|
|
56
|
+
const VENDOR_CONFIG_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
|
|
57
|
+
/** Remove folders for vendor sessions that have not been used for 90 days. */
|
|
58
|
+
function sweepVendorConfigDirs(now = Date.now()) {
|
|
59
|
+
if (!(0, node_fs_1.existsSync)(configRoot) || !(0, node_fs_1.lstatSync)(configRoot).isDirectory())
|
|
60
|
+
return 0;
|
|
61
|
+
let removed = 0;
|
|
62
|
+
for (const kind of ['claude', 'codex', 'cursor']) {
|
|
63
|
+
const kindRoot = (0, node_path_1.join)(configRoot, kind);
|
|
64
|
+
if (!(0, node_fs_1.existsSync)(kindRoot) || !(0, node_fs_1.lstatSync)(kindRoot).isDirectory())
|
|
65
|
+
continue;
|
|
66
|
+
for (const entry of (0, node_fs_1.readdirSync)(kindRoot, { withFileTypes: true })) {
|
|
67
|
+
if (!entry.isDirectory())
|
|
68
|
+
continue;
|
|
69
|
+
const taskRoot = (0, node_path_1.join)(kindRoot, entry.name);
|
|
70
|
+
if ((0, node_fs_1.lstatSync)(taskRoot).mtimeMs >= now - VENDOR_CONFIG_MAX_AGE_MS)
|
|
71
|
+
continue;
|
|
72
|
+
(0, node_fs_1.rmSync)(taskRoot, { recursive: true, force: true });
|
|
73
|
+
removed++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
function vendorConfigDir(kind, config, sourceDir, authFiles) {
|
|
79
|
+
const directory = (0, node_path_1.join)(configRoot, kind, config.taskId.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
80
|
+
(0, node_fs_1.mkdirSync)(configRoot, { recursive: true, mode: 0o700 });
|
|
81
|
+
(0, node_fs_1.chmodSync)(configRoot, 0o700);
|
|
82
|
+
(0, node_fs_1.mkdirSync)(directory, { recursive: true, mode: 0o700 });
|
|
83
|
+
(0, node_fs_1.chmodSync)(directory, 0o700);
|
|
84
|
+
for (const name of authFiles) {
|
|
85
|
+
const source = (0, node_path_1.resolve)(sourceDir, name);
|
|
86
|
+
const target = (0, node_path_1.join)(directory, name);
|
|
87
|
+
if ((0, node_fs_1.existsSync)(source)) {
|
|
88
|
+
// Codex CLI 0.163.0 opens auth.json with truncate(true) and writes
|
|
89
|
+
// through this link. A copy would diverge when Codex refreshes login.
|
|
90
|
+
try {
|
|
91
|
+
if ((0, node_fs_1.lstatSync)(target).isSymbolicLink() &&
|
|
92
|
+
(0, node_fs_1.readlinkSync)(target) === source)
|
|
93
|
+
continue;
|
|
94
|
+
(0, node_fs_1.unlinkSync)(target);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== 'ENOENT')
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
(0, node_fs_1.symlinkSync)(source, target);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
(0, node_fs_1.utimesSync)(directory, new Date(), new Date());
|
|
104
|
+
return directory;
|
|
105
|
+
}
|
|
106
|
+
/** Keep Claude API-key turns away from a subscription credential link. */
|
|
107
|
+
function claudeVendorConfigDir(config) {
|
|
108
|
+
if (config.authMethod === 'api_key')
|
|
109
|
+
return vendorConfigDir('claude', { ...config, taskId: `${config.taskId}-api-key` }, '', []);
|
|
110
|
+
const ownerDir = (0, node_path_1.resolve)(process.env.CLAUDE_CONFIG_DIR || (0, node_path_1.join)((0, node_os_1.homedir)(), '.claude'));
|
|
111
|
+
return vendorConfigDir('claude', config, process.platform === 'linux' ? ownerDir : '', process.platform === 'linux' ? ['.credentials.json'] : []);
|
|
112
|
+
}
|
|
113
|
+
/** The isolated Codex home can use only the owner's file-based login. */
|
|
114
|
+
function codexVendorConfigDir(config, useApiKey) {
|
|
115
|
+
if (useApiKey)
|
|
116
|
+
return vendorConfigDir('codex', { ...config, taskId: `${config.taskId}-api-key` }, '', []);
|
|
117
|
+
const ownerDir = (0, node_path_1.resolve)(process.env.CODEX_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex'));
|
|
118
|
+
const configFile = (0, node_path_1.join)(ownerDir, 'config.toml');
|
|
119
|
+
if ((0, node_fs_1.existsSync)(configFile)) {
|
|
120
|
+
const contents = (0, node_fs_1.readFileSync)(configFile, 'utf8');
|
|
121
|
+
// cli_auth_credentials_store is a top-level TOML key. A key below a
|
|
122
|
+
// table heading is not the CLI auth store setting.
|
|
123
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (trimmed.startsWith('['))
|
|
126
|
+
break;
|
|
127
|
+
const assignment = trimmed.match(/^cli_auth_credentials_store\s*=\s*(.*)$/);
|
|
128
|
+
if (!assignment)
|
|
129
|
+
continue;
|
|
130
|
+
const mode = assignment[1].match(/^['"]([a-z]+)['"](?:\s*(?:#.*)?)?$/)?.[1];
|
|
131
|
+
if (mode !== 'file') {
|
|
132
|
+
throw new Error(`Codex subscription login uses cli_auth_credentials_store=${mode ?? 'unknown'}. ` +
|
|
133
|
+
'The runner supports file login only. Set cli_auth_credentials_store="file" and run codex login, or use an API key.');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(ownerDir, 'auth.json'))) {
|
|
138
|
+
throw new Error('Codex file login was not found. Run codex login with cli_auth_credentials_store="file", or use an API key. Keyring and auto login cannot be shared with an isolated CODEX_HOME.');
|
|
139
|
+
}
|
|
140
|
+
return vendorConfigDir('codex', config, ownerDir, ['auth.json']);
|
|
141
|
+
}
|
|
142
|
+
function isolateVendorHome(env, directory) {
|
|
143
|
+
env.HOME = directory;
|
|
144
|
+
env.USERPROFILE = directory;
|
|
145
|
+
env.XDG_CONFIG_HOME = directory;
|
|
146
|
+
env.APPDATA = directory;
|
|
147
|
+
env.LOCALAPPDATA = directory;
|
|
148
|
+
}
|
|
149
|
+
exports.DISABLED_GIT_SSH_COMMAND = `ssh -F ${process.platform === 'win32' ? 'NUL' : '/dev/null'} -o IdentitiesOnly=yes -o IdentityFile=${process.platform === 'win32' ? 'NUL' : '/dev/null'} -o IdentityAgent=none`;
|
|
150
|
+
function vendorEnvironment(config, auth = {}) {
|
|
151
|
+
const env = {};
|
|
152
|
+
for (const key of HOST_KEYS) {
|
|
153
|
+
if (process.env[key])
|
|
154
|
+
env[key] = process.env[key];
|
|
155
|
+
}
|
|
156
|
+
for (const key of SESSION_KEYS) {
|
|
157
|
+
if (config.environment?.[key])
|
|
158
|
+
env[key] = config.environment[key];
|
|
159
|
+
}
|
|
160
|
+
// A missing guard environment must never select the owner's gh login.
|
|
161
|
+
if (!env.GH_CONFIG_DIR) {
|
|
162
|
+
env.GH_CONFIG_DIR = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'pf-vendor-gh-'));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
(0, node_fs_1.mkdirSync)(env.GH_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
166
|
+
}
|
|
167
|
+
for (const [key, value] of Object.entries(config.secretEnvVars ?? {})) {
|
|
168
|
+
if (allowedVendorSecretKey(key))
|
|
169
|
+
env[key] = value;
|
|
170
|
+
}
|
|
171
|
+
for (const [key, value] of Object.entries(auth)) {
|
|
172
|
+
if (value && allowedVendorSecretKey(key))
|
|
173
|
+
env[key] = value;
|
|
174
|
+
}
|
|
175
|
+
env.GIT_CONFIG_GLOBAL = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
176
|
+
env.GIT_CONFIG_NOSYSTEM = '1';
|
|
177
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
178
|
+
env.GIT_SSH_COMMAND = exports.DISABLED_GIT_SSH_COMMAND;
|
|
179
|
+
// A non-repository turn can still create a local Git repository and commit.
|
|
180
|
+
// Keep the identity independent of the owner's global Git configuration.
|
|
181
|
+
env.GIT_AUTHOR_NAME = 'Peakflo Agent';
|
|
182
|
+
env.GIT_AUTHOR_EMAIL = 'agent@peakflo.bot';
|
|
183
|
+
env.GIT_COMMITTER_NAME = 'Peakflo Agent';
|
|
184
|
+
env.GIT_COMMITTER_EMAIL = 'agent@peakflo.bot';
|
|
185
|
+
return env;
|
|
186
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -40,12 +40,17 @@ const credential_store_1 = require("./credential-store");
|
|
|
40
40
|
const harness_1 = require("./harness");
|
|
41
41
|
const node_floor_1 = require("./node-floor");
|
|
42
42
|
const options_1 = require("./options");
|
|
43
|
+
const service_1 = require("./service");
|
|
43
44
|
const version_1 = require("./version");
|
|
44
45
|
const USAGE = `pf-agent-harness — headless Peakflo coding-agent harness
|
|
45
46
|
|
|
46
47
|
Usage:
|
|
47
48
|
pf-agent-harness start [options]
|
|
48
49
|
pf-agent-harness version
|
|
50
|
+
pf-agent-harness service install [start options]
|
|
51
|
+
pf-agent-harness service status
|
|
52
|
+
pf-agent-harness service repair
|
|
53
|
+
pf-agent-harness service uninstall
|
|
49
54
|
|
|
50
55
|
Options:
|
|
51
56
|
--control-plane <url> workflow-api base url (env PF_CONTROL_PLANE_URL)
|
|
@@ -76,6 +81,8 @@ Options:
|
|
|
76
81
|
--max-sessions <n> concurrent sessions (env PF_MAX_CONCURRENT_SESSIONS,
|
|
77
82
|
default 10 for desktop/self-hosted; sandbox is configured by control plane)
|
|
78
83
|
--labels <a,b> routing labels (env PF_RUNNER_LABELS)
|
|
84
|
+
--vendor-harnesses <list> allow codex,claude-code,cursor on this machine
|
|
85
|
+
(env PF_VENDOR_HARNESSES; default none)
|
|
79
86
|
--no-harness-discovery do not look for Codex, Claude Code, Cursor,
|
|
80
87
|
Pi and OpenCode on this machine, and report
|
|
81
88
|
none (env PF_HARNESS_DISCOVERY=0). On by
|
|
@@ -88,6 +95,19 @@ PF_ALLOW_UNSUPPORTED_NODE=1 starts anyway, at your own risk.
|
|
|
88
95
|
In a managed sandbox pf-bootstrap supplies these through the environment, so
|
|
89
96
|
the enrolment token never appears in /proc/<pid>/cmdline.
|
|
90
97
|
`;
|
|
98
|
+
const serviceStart = (0, service_1.isServiceStart)(process.argv.slice(2));
|
|
99
|
+
function stopPermanentServiceFailure(reason, linuxCode) {
|
|
100
|
+
try {
|
|
101
|
+
(0, service_1.recordServiceFailure)(reason);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* exit even if the disk is full */
|
|
105
|
+
}
|
|
106
|
+
process.stderr.write(`Service stopped: ${reason}.\n`);
|
|
107
|
+
// launchd can prevent a restart on a successful exit. systemd uses
|
|
108
|
+
// RestartPreventExitStatus for the two permanent error codes.
|
|
109
|
+
process.exit((0, service_1.permanentServiceExitCode)(process.platform, linuxCode));
|
|
110
|
+
}
|
|
91
111
|
/**
|
|
92
112
|
* Read `--flag value` from argv. A tiny duplicate of the reader in options.ts,
|
|
93
113
|
* because the login decision has to be made BEFORE `parseOptions` runs — that
|
|
@@ -122,6 +142,14 @@ async function main() {
|
|
|
122
142
|
})}\n`);
|
|
123
143
|
return;
|
|
124
144
|
}
|
|
145
|
+
if (command === 'service') {
|
|
146
|
+
await (0, service_1.runServiceCommand)(argv.slice(1), {
|
|
147
|
+
nodePath: process.execPath,
|
|
148
|
+
cliPath: __filename,
|
|
149
|
+
env: process.env
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
125
153
|
if (command !== 'start') {
|
|
126
154
|
process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
|
|
127
155
|
process.exitCode = 2;
|
|
@@ -141,6 +169,8 @@ async function main() {
|
|
|
141
169
|
env: process.env
|
|
142
170
|
});
|
|
143
171
|
if (nodeFloor.action === 'refuse') {
|
|
172
|
+
if (serviceStart)
|
|
173
|
+
throw new options_1.OptionsError(nodeFloor.message);
|
|
144
174
|
process.stderr.write(`${nodeFloor.message}\n`);
|
|
145
175
|
process.exit(1);
|
|
146
176
|
}
|
|
@@ -165,7 +195,7 @@ async function main() {
|
|
|
165
195
|
// `--token=x` counts as a token, hence readOptionValue rather than indexOf.
|
|
166
196
|
const hasToken = Boolean(readOptionValue(startArgs, 'token') ?? process.env.PF_ENROLMENT_TOKEN);
|
|
167
197
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
168
|
-
if (!hasToken && (wantsLogin || interactive)) {
|
|
198
|
+
if (!serviceStart && !hasToken && (wantsLogin || interactive)) {
|
|
169
199
|
const { enrolViaBrowser } = await Promise.resolve().then(() => __importStar(require('./enrol/browser-enrol')));
|
|
170
200
|
const token = await enrolViaBrowser({
|
|
171
201
|
controlPlaneUrl: readOptionValue(startArgs, 'control-plane') ??
|
|
@@ -182,6 +212,11 @@ async function main() {
|
|
|
182
212
|
process.env.PF_ENROLMENT_TOKEN = token;
|
|
183
213
|
}
|
|
184
214
|
const options = (0, options_1.parseOptions)(startArgs, process.env);
|
|
215
|
+
new service_1.ServiceManager({
|
|
216
|
+
nodePath: process.execPath,
|
|
217
|
+
cliPath: __filename,
|
|
218
|
+
env: process.env
|
|
219
|
+
}).assertNoDuplicateRunner(options);
|
|
185
220
|
const handedOffCredential = (0, credential_store_1.takeHandedOffCredential)(process.env);
|
|
186
221
|
const resolveAdapter = (0, adapter_resolver_1.createAdapterResolver)(options.runtime, options.workspaceRoot);
|
|
187
222
|
const harness = new harness_1.Harness(options, {
|
|
@@ -207,6 +242,8 @@ async function main() {
|
|
|
207
242
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
208
243
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
209
244
|
await harness.start();
|
|
245
|
+
if (serviceStart)
|
|
246
|
+
(0, service_1.clearServiceFailure)();
|
|
210
247
|
// BLOCK ON THE LEASE LOOP, and let it decide when the process is done.
|
|
211
248
|
//
|
|
212
249
|
// The Phase 0 skeleton relied on the health socket keeping the event loop
|
|
@@ -216,9 +253,13 @@ async function main() {
|
|
|
216
253
|
// build, so the exit code carries the reason.
|
|
217
254
|
try {
|
|
218
255
|
await harness.wait();
|
|
256
|
+
if (serviceStart && process.platform === 'darwin')
|
|
257
|
+
process.exit(75);
|
|
219
258
|
}
|
|
220
259
|
catch (error) {
|
|
221
260
|
if (error instanceof harness_1.UpgradeRequestedError) {
|
|
261
|
+
if (serviceStart)
|
|
262
|
+
stopPermanentServiceFailure('harness upgrade required', 78);
|
|
222
263
|
process.exit(contract_1.HARNESS_UPGRADE_EXIT_CODE);
|
|
223
264
|
}
|
|
224
265
|
throw error;
|
|
@@ -231,9 +272,22 @@ main().catch((error) => {
|
|
|
231
272
|
// ever. A sandbox that hangs is worse than one that dies: the control plane
|
|
232
273
|
// cannot recycle it, so the version gate would never take effect.
|
|
233
274
|
if (error instanceof options_1.OptionsError) {
|
|
275
|
+
if (serviceStart)
|
|
276
|
+
stopPermanentServiceFailure('invalid service options', 2);
|
|
234
277
|
process.stderr.write(`${error.message}\n\n${USAGE}`);
|
|
235
278
|
process.exit(2);
|
|
236
279
|
}
|
|
280
|
+
if (serviceStart && error instanceof harness_1.StoredCredentialUnavailableError) {
|
|
281
|
+
stopPermanentServiceFailure('runner credential unavailable', 78);
|
|
282
|
+
}
|
|
283
|
+
if (serviceStart && error instanceof harness_1.VersionRefusedError) {
|
|
284
|
+
stopPermanentServiceFailure('harness version or protocol refused', 78);
|
|
285
|
+
}
|
|
286
|
+
if (serviceStart) {
|
|
287
|
+
const reason = (0, service_1.permanentStartupFailure)(error);
|
|
288
|
+
if (reason)
|
|
289
|
+
stopPermanentServiceFailure(reason, 78);
|
|
290
|
+
}
|
|
237
291
|
const message = error instanceof Error ? error.stack : String(error);
|
|
238
292
|
process.stderr.write(`pf-agent-harness failed to start:\n${message}\n`);
|
|
239
293
|
process.exit(1);
|
|
@@ -7,6 +7,7 @@ exports.installPushGuard = installPushGuard;
|
|
|
7
7
|
const node_fs_1 = require("node:fs");
|
|
8
8
|
const promises_1 = require("node:fs/promises");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
|
+
const vendor_environment_1 = require("./adapters/vendor-environment");
|
|
10
11
|
exports.MERGE_REFUSAL = 'GitHub merge refused: agents may open pull requests but must not merge them.';
|
|
11
12
|
let guardPathState;
|
|
12
13
|
let guardPathQueue = Promise.resolve();
|
|
@@ -59,6 +60,9 @@ for (const key of Object.keys(gitEnv)) {
|
|
|
59
60
|
if (key.startsWith('GH_') && !['GH_TOKEN', 'GH_CONFIG_DIR'].includes(key)) delete gitEnv[key];
|
|
60
61
|
}
|
|
61
62
|
gitEnv.GIT_TERMINAL_PROMPT = '0';
|
|
63
|
+
gitEnv.GIT_CONFIG_GLOBAL = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
64
|
+
gitEnv.GIT_CONFIG_NOSYSTEM = '1';
|
|
65
|
+
gitEnv.GIT_SSH_COMMAND = ${JSON.stringify(vendor_environment_1.DISABLED_GIT_SSH_COMMAND)};
|
|
62
66
|
// The wrapper deliberately removes caller-controlled GIT_* overrides, but
|
|
63
67
|
// Git's libcurl requires GIT_SSL_CAINFO to trust the harness TLS proxy. Pin it
|
|
64
68
|
// to the harness-owned bundle instead of accepting the caller's value.
|
|
@@ -152,6 +156,7 @@ for (const key of Object.keys(cleanEnv)) {
|
|
|
152
156
|
if (key.startsWith('GH_') && !['GH_TOKEN', 'GH_CONFIG_DIR'].includes(key)) delete cleanEnv[key];
|
|
153
157
|
}
|
|
154
158
|
cleanEnv.GIT_TERMINAL_PROMPT = '0';
|
|
159
|
+
cleanEnv.GIT_SSH_COMMAND = ${JSON.stringify(vendor_environment_1.DISABLED_GIT_SSH_COMMAND)};
|
|
155
160
|
// Inspect all args, including endpoint=... and flags before the subcommand.
|
|
156
161
|
const decodedArgs = args.map(arg => {
|
|
157
162
|
try { return decodeURIComponent(arg); } catch { return arg; }
|
package/dist/github-workspace.js
CHANGED
|
@@ -8,6 +8,7 @@ const promises_1 = require("node:fs/promises");
|
|
|
8
8
|
const node_http_1 = require("node:http");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
10
|
const node_util_1 = require("node:util");
|
|
11
|
+
const vendor_environment_1 = require("./adapters/vendor-environment");
|
|
11
12
|
const github_command_guards_1 = require("./github-command-guards");
|
|
12
13
|
const github_proxy_1 = require("./github-proxy");
|
|
13
14
|
const github_proxy_certificates_1 = require("./github-proxy-certificates");
|
|
@@ -393,6 +394,31 @@ class GitHubWorkspaceProvider {
|
|
|
393
394
|
await this.runGit(['config', '--worktree', 'core.hooksPath', hooks], {
|
|
394
395
|
cwd: worktree
|
|
395
396
|
});
|
|
397
|
+
// A direct /usr/bin/git call also uses this worktree configuration.
|
|
398
|
+
// Rewrite GitHub SSH addresses before transport selection and disable
|
|
399
|
+
// owner SSH keys for any address that is not rewritten.
|
|
400
|
+
await this.runGit([
|
|
401
|
+
'config',
|
|
402
|
+
'--worktree',
|
|
403
|
+
'--unset-all',
|
|
404
|
+
'url.https://github.com/.insteadOf'
|
|
405
|
+
], {
|
|
406
|
+
cwd: worktree
|
|
407
|
+
}).catch(() => undefined);
|
|
408
|
+
for (const sshPrefix of ['git@github.com:', 'ssh://git@github.com/']) {
|
|
409
|
+
await this.runGit([
|
|
410
|
+
'config',
|
|
411
|
+
'--worktree',
|
|
412
|
+
'--add',
|
|
413
|
+
'url.https://github.com/.insteadOf',
|
|
414
|
+
sshPrefix
|
|
415
|
+
], {
|
|
416
|
+
cwd: worktree
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
await this.runGit(['config', '--worktree', 'core.sshCommand', vendor_environment_1.DISABLED_GIT_SSH_COMMAND], { cwd: worktree });
|
|
420
|
+
await this.runGit(['config', '--worktree', 'user.name', 'Peakflo Agent'], { cwd: worktree });
|
|
421
|
+
await this.runGit(['config', '--worktree', 'user.email', 'agent@peakflo.bot'], { cwd: worktree });
|
|
396
422
|
request.onRepositoryStatus?.({
|
|
397
423
|
fullName: repository.fullName,
|
|
398
424
|
status: 'checkedOut'
|
|
@@ -412,7 +438,7 @@ class GitHubWorkspaceProvider {
|
|
|
412
438
|
'# GitHub repository access',
|
|
413
439
|
'',
|
|
414
440
|
'Each repository is a child directory. Commit and push only the agent/<session-id> branch.',
|
|
415
|
-
'GitHub
|
|
441
|
+
'GitHub access uses the session proxy. Use gh and git only in this session worktree. GH_TOKEN is a session sentinel, not a GitHub token. Merges and pushes outside agent/<session-id> are refused.',
|
|
416
442
|
''
|
|
417
443
|
].join('\n');
|
|
418
444
|
await (0, promises_1.writeFile)((0, node_path_1.join)(sessionRoot, 'AGENTS.md'), instructions, {
|
|
@@ -443,7 +469,8 @@ class GitHubWorkspaceProvider {
|
|
|
443
469
|
CURL_CA_BUNDLE: this.caCertificatePath,
|
|
444
470
|
NODE_EXTRA_CA_CERTS: this.caCertificatePath,
|
|
445
471
|
GH_TOKEN: session.sentinel,
|
|
446
|
-
GH_CONFIG_DIR: (0, node_path_1.join)(this.sessionPath(sessionId), '.gh')
|
|
472
|
+
GH_CONFIG_DIR: (0, node_path_1.join)(this.sessionPath(sessionId), '.gh'),
|
|
473
|
+
GIT_SSH_COMMAND: vendor_environment_1.DISABLED_GIT_SSH_COMMAND
|
|
447
474
|
};
|
|
448
475
|
}
|
|
449
476
|
takeTurnNote(sessionId) {
|
|
@@ -454,7 +481,7 @@ class GitHubWorkspaceProvider {
|
|
|
454
481
|
this.restartNotedSessions.add(sessionId);
|
|
455
482
|
const restored = this.restoredWorktreeNotes.get(sessionId);
|
|
456
483
|
return [
|
|
457
|
-
'Harness restarted;
|
|
484
|
+
'Harness restarted; use gh and git only in this session worktree through the local proxy.',
|
|
458
485
|
restored
|
|
459
486
|
]
|
|
460
487
|
.filter(Boolean)
|
package/dist/harness.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export declare function harnessContentKey(harnesses: readonly RunnerHarness[]):
|
|
|
49
49
|
export declare class VersionRefusedError extends Error {
|
|
50
50
|
constructor(message: string);
|
|
51
51
|
}
|
|
52
|
+
/** A service cannot re-enrol without a new one-time token from a user. */
|
|
53
|
+
export declare class StoredCredentialUnavailableError extends Error {
|
|
54
|
+
constructor();
|
|
55
|
+
}
|
|
52
56
|
/** The sandbox supervisor, not the harness, owns package installation. */
|
|
53
57
|
export declare class UpgradeRequestedError extends Error {
|
|
54
58
|
readonly harnessVersion: string;
|
package/dist/harness.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Harness = exports.UpgradeRequestedError = exports.VersionRefusedError = exports.HARNESS_DISCOVERY_FEATURE = exports.HARNESS_RETRY_DELAYS_MS = exports.HARNESS_DISCOVERY_REFRESH_MS = void 0;
|
|
3
|
+
exports.Harness = exports.UpgradeRequestedError = exports.StoredCredentialUnavailableError = exports.VersionRefusedError = exports.HARNESS_DISCOVERY_FEATURE = exports.HARNESS_RETRY_DELAYS_MS = exports.HARNESS_DISCOVERY_REFRESH_MS = void 0;
|
|
4
4
|
exports.mergeHarnessReports = mergeHarnessReports;
|
|
5
5
|
exports.harnessContentKey = harnessContentKey;
|
|
6
6
|
const node_crypto_1 = require("node:crypto");
|
|
7
7
|
const node_path_1 = require("node:path");
|
|
8
8
|
const model_gateway_1 = require("./adapters/model-gateway");
|
|
9
|
+
const vendor_environment_1 = require("./adapters/vendor-environment");
|
|
9
10
|
const capabilities_1 = require("./capabilities");
|
|
10
11
|
const contract_1 = require("./contract");
|
|
11
12
|
const control_plane_1 = require("./control-plane");
|
|
@@ -111,6 +112,14 @@ class VersionRefusedError extends Error {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
exports.VersionRefusedError = VersionRefusedError;
|
|
115
|
+
/** A service cannot re-enrol without a new one-time token from a user. */
|
|
116
|
+
class StoredCredentialUnavailableError extends Error {
|
|
117
|
+
constructor() {
|
|
118
|
+
super('No usable stored runner credential. Start the runner in a terminal to sign in again.');
|
|
119
|
+
this.name = 'StoredCredentialUnavailableError';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.StoredCredentialUnavailableError = StoredCredentialUnavailableError;
|
|
114
123
|
/** The sandbox supervisor, not the harness, owns package installation. */
|
|
115
124
|
class UpgradeRequestedError extends Error {
|
|
116
125
|
harnessVersion;
|
|
@@ -365,6 +374,16 @@ class Harness {
|
|
|
365
374
|
// The health check comes up FIRST, so a probe sees 'starting' rather than a
|
|
366
375
|
// connection refused while registration is still retrying.
|
|
367
376
|
await this.health.start();
|
|
377
|
+
try {
|
|
378
|
+
const removed = (0, vendor_environment_1.sweepVendorConfigDirs)();
|
|
379
|
+
if (removed)
|
|
380
|
+
log('Old vendor config folders removed', { removed });
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
log('Vendor config cleanup failed', {
|
|
384
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
385
|
+
});
|
|
386
|
+
}
|
|
368
387
|
await this.proxy.start();
|
|
369
388
|
this.capabilities = await (0, capabilities_1.probeCapabilities)({
|
|
370
389
|
maxConcurrentSessions: this.options.maxConcurrentSessions,
|
|
@@ -373,10 +392,8 @@ class Harness {
|
|
|
373
392
|
// Install immediately before model probing can start a backend. The lease
|
|
374
393
|
// is process-wide and reference-counted, so concurrent Harness instances
|
|
375
394
|
// share one generic launcher instead of racing to rewrite global PATH.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
this.releaseGitHubGuard = guard.release;
|
|
379
|
-
}
|
|
395
|
+
const guard = await (0, github_command_guards_1.acquireGhGuardPath)(this.options.workspaceRoot);
|
|
396
|
+
this.releaseGitHubGuard = guard.release;
|
|
380
397
|
/*
|
|
381
398
|
* MODELS ARE PROBED ONCE, AT START-UP — not on every heartbeat, because
|
|
382
399
|
* enumerating providers starts a Pi discovery process. The catalogue is
|
|
@@ -534,8 +551,7 @@ class Harness {
|
|
|
534
551
|
// 2. ENROL. The ordinary first-boot path, and the fallback when no usable
|
|
535
552
|
// stored credential exists (absent, expired or rejected).
|
|
536
553
|
if (!this.options.enrolmentToken) {
|
|
537
|
-
throw new
|
|
538
|
-
'--token / PF_ENROLMENT_TOKEN for the first registration.');
|
|
554
|
+
throw new StoredCredentialUnavailableError();
|
|
539
555
|
}
|
|
540
556
|
// Back-off and the final-status short-circuit both live in the client, so
|
|
541
557
|
// every route obeys the same retry rule and no route can drift from it.
|
|
@@ -945,6 +961,7 @@ class Harness {
|
|
|
945
961
|
if (!this.harnessReport) {
|
|
946
962
|
return;
|
|
947
963
|
}
|
|
964
|
+
capabilities.vendorHarnesses = this.options.vendorHarnesses ?? [];
|
|
948
965
|
const blocked = this.addVendorBackends(capabilities);
|
|
949
966
|
const harnesses = this.harnessReport.map((harness) => {
|
|
950
967
|
const backend = capabilities.backends.find((candidate) => candidate.name === harness.name);
|
|
@@ -1030,6 +1047,14 @@ class Harness {
|
|
|
1030
1047
|
continue;
|
|
1031
1048
|
if (capabilities.backends.some((b) => b.name === harness.name))
|
|
1032
1049
|
continue;
|
|
1050
|
+
if (!this.options.vendorHarnesses?.includes(harness.name)) {
|
|
1051
|
+
blocked.set(harness.name, 'Not enabled on this machine. Start the runner with --vendor-harnesses ' +
|
|
1052
|
+
harness.name +
|
|
1053
|
+
' or set PF_VENDOR_HARNESSES=' +
|
|
1054
|
+
harness.name +
|
|
1055
|
+
'.');
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1033
1058
|
const reason = blocker(harness.name, this.options.runtime);
|
|
1034
1059
|
if (reason) {
|
|
1035
1060
|
blocked.set(harness.name, reason);
|
package/dist/options.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.OptionsError = void 0;
|
|
|
4
4
|
exports.parseOptions = parseOptions;
|
|
5
5
|
const node_os_1 = require("node:os");
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
|
+
const vendor_backends_1 = require("./vendor-backends");
|
|
7
8
|
const DEFAULT_HEALTH_PORT = 7777;
|
|
8
9
|
const DEFAULT_GITHUB_PROXY_PORT = 7780;
|
|
9
10
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -141,6 +142,20 @@ function parseLabels(raw) {
|
|
|
141
142
|
.filter(Boolean);
|
|
142
143
|
return labels.length > 0 ? labels : undefined;
|
|
143
144
|
}
|
|
145
|
+
function parseVendorHarnesses(raw) {
|
|
146
|
+
if (!raw?.trim())
|
|
147
|
+
return [];
|
|
148
|
+
const names = raw
|
|
149
|
+
.split(',')
|
|
150
|
+
.map((name) => name.trim())
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
for (const name of names) {
|
|
153
|
+
if (!(0, vendor_backends_1.isVendorBackend)(name)) {
|
|
154
|
+
throw new OptionsError(`Invalid vendor harness: '${name}'. Expected codex, claude-code or cursor.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...new Set(names)];
|
|
158
|
+
}
|
|
144
159
|
function parseBoolean(raw, fallback, name) {
|
|
145
160
|
if (raw === undefined || raw === '')
|
|
146
161
|
return fallback;
|
|
@@ -178,7 +193,7 @@ function parseOptions(argv, env) {
|
|
|
178
193
|
throw new OptionsError('Missing control plane url. Pass --control-plane <url> or set PF_CONTROL_PLANE_URL.');
|
|
179
194
|
}
|
|
180
195
|
const enrolmentToken = readFlag(argv, 'token') ?? env.PF_ENROLMENT_TOKEN;
|
|
181
|
-
if (!enrolmentToken) {
|
|
196
|
+
if (!enrolmentToken && !readBoolFlag(argv, 'stored-credential')) {
|
|
182
197
|
throw new OptionsError('Missing enrolment token. Pass --token <token> or set PF_ENROLMENT_TOKEN.');
|
|
183
198
|
}
|
|
184
199
|
let parsed;
|
|
@@ -205,7 +220,7 @@ function parseOptions(argv, env) {
|
|
|
205
220
|
const runtime = parseRuntime(readFlag(argv, 'runtime') ?? env.PF_RUNNER_RUNTIME);
|
|
206
221
|
return {
|
|
207
222
|
controlPlaneUrl,
|
|
208
|
-
enrolmentToken,
|
|
223
|
+
enrolmentToken: enrolmentToken ?? '',
|
|
209
224
|
healthPort: parsePort(readFlag(argv, 'health-port') ?? env.PF_HARNESS_HEALTH_PORT, DEFAULT_HEALTH_PORT),
|
|
210
225
|
githubProxyPort: parsePort(readFlag(argv, 'github-proxy-port') ?? env.PF_GITHUB_PROXY_PORT, DEFAULT_GITHUB_PROXY_PORT),
|
|
211
226
|
autoCommitDirtyWorktree: parseBoolean(readFlag(argv, 'worktree-wip-commit') ?? env.PF_WORKTREE_WIP_COMMIT, runtime === 'sandbox', 'worktree WIP commit flag'),
|
|
@@ -219,6 +234,7 @@ function parseOptions(argv, env) {
|
|
|
219
234
|
runtime,
|
|
220
235
|
maxConcurrentSessions: parseConcurrency(readFlag(argv, 'max-sessions') ?? env.PF_MAX_CONCURRENT_SESSIONS, runtime),
|
|
221
236
|
labels: parseLabels(readFlag(argv, 'labels') ?? env.PF_RUNNER_LABELS),
|
|
237
|
+
vendorHarnesses: parseVendorHarnesses(readFlag(argv, 'vendor-harnesses') ?? env.PF_VENDOR_HARNESSES),
|
|
222
238
|
// The flag wins over the environment, so one start can turn it off
|
|
223
239
|
// without editing a service file.
|
|
224
240
|
harnessDiscovery: readBoolFlag(argv, 'no-harness-discovery')
|
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
* adapter default. That is defect G1 — a Permission control that is recorded,
|
|
18
18
|
* served, rendered, and enforced nowhere.
|
|
19
19
|
*
|
|
20
|
-
* A stored mode keeps the user's choice. Missing
|
|
21
|
-
*
|
|
22
|
-
* `'ask'`; it may be a mode from a newer control plane.
|
|
20
|
+
* A stored mode keeps the user's choice. Missing and unrecognised modes
|
|
21
|
+
* fail closed onto 'ask'.
|
|
23
22
|
*
|
|
24
23
|
* ---------------------------------------------------------------------------
|
|
25
24
|
* THE TABLE, AND WHAT IS AND IS NOT FAITHFUL
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
* adapter default. That is defect G1 — a Permission control that is recorded,
|
|
19
19
|
* served, rendered, and enforced nowhere.
|
|
20
20
|
*
|
|
21
|
-
* A stored mode keeps the user's choice. Missing
|
|
22
|
-
*
|
|
23
|
-
* `'ask'`; it may be a mode from a newer control plane.
|
|
21
|
+
* A stored mode keeps the user's choice. Missing and unrecognised modes
|
|
22
|
+
* fail closed onto 'ask'.
|
|
24
23
|
*
|
|
25
24
|
* ---------------------------------------------------------------------------
|
|
26
25
|
* THE TABLE, AND WHAT IS AND IS NOT FAITHFUL
|
|
@@ -92,8 +91,6 @@ exports.claudeCodePermissionMode = claudeCodePermissionMode;
|
|
|
92
91
|
*/
|
|
93
92
|
function mapPermissionMode(mode) {
|
|
94
93
|
switch (mode) {
|
|
95
|
-
case null:
|
|
96
|
-
case undefined:
|
|
97
94
|
case 'bypassPermissions':
|
|
98
95
|
return {
|
|
99
96
|
permissionMode: 'allow',
|
|
@@ -25,6 +25,7 @@ import { LeaseResponse, LeasedTurnInput, ModelMessageInput, TranscriptPartInput,
|
|
|
25
25
|
import { KnowledgeToolHost, type KnowledgeControl } from './knowledge-tools';
|
|
26
26
|
import { McpAuthProxy } from './mcp-auth-proxy';
|
|
27
27
|
import { type FailedMcpCheck } from './mcp-preflight';
|
|
28
|
+
export declare function approvalTimeoutMs(value: string | undefined): number;
|
|
28
29
|
export interface SessionRunnerDeps {
|
|
29
30
|
/** Resolves a backend name from the lease to a working adapter. */
|
|
30
31
|
getAdapter(backend: string): CodingAgentAdapter;
|
|
@@ -347,6 +348,7 @@ export declare class SessionRunner {
|
|
|
347
348
|
private openApproval;
|
|
348
349
|
/** The transcript identity of an ordinary agent question, if one is open. */
|
|
349
350
|
private openQuestionPartId;
|
|
351
|
+
private openQuestionStartedAt;
|
|
350
352
|
/**
|
|
351
353
|
* Expiry of the model token THIS RUNNING BACKEND is holding, when the
|
|
352
354
|
* control plane reported one — see `LeaseResponse.modelTokenExpiresAt`.
|