@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
package/README.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @peakflo/agent-harness
|
|
2
2
|
|
|
3
|
+
Task agent turns copy file fields from their Workflo task into the runner's
|
|
4
|
+
`attachments/` directory before the model receives the turn. Browser recording
|
|
5
|
+
files use `attachments/recording/<taskId>/`. The lease lists file metadata;
|
|
6
|
+
the harness fetches bytes from workflow-api with its existing session token.
|
|
7
|
+
The model receives workspace-relative paths, not storage URLs. A local sync
|
|
8
|
+
state avoids downloading unchanged files again on subsequent turns, and any
|
|
9
|
+
file that cannot be copied is named in the turn prompt.
|
|
10
|
+
New UI uploads carry tenant and uploader in their storage key. Existing UI
|
|
11
|
+
and Notion attachments follow the same direct file-download access rule when
|
|
12
|
+
they are present in the task's file fields. Recording attachments remain
|
|
13
|
+
restricted to their own task. The older `uploads/<timestamp>-<name>` keys do
|
|
14
|
+
not encode a tenant; cross-tenant reads of those legacy keys remain a known
|
|
15
|
+
limit of the direct file route.
|
|
16
|
+
|
|
3
17
|
Run a Peakflo coding agent on your machine.
|
|
4
18
|
|
|
5
19
|
## Quick start
|
|
@@ -38,6 +52,68 @@ Optional flags include `--health-port <port>`, `--runtime <desktop|self-hosted>`
|
|
|
38
52
|
`--workspace-root <path>`, and `--github-proxy-port <port>`. Run
|
|
39
53
|
`npx @peakflo/agent-harness@rc --help` for the full list.
|
|
40
54
|
|
|
55
|
+
## Run after you close the terminal
|
|
56
|
+
|
|
57
|
+
The background service supports macOS launchd user agents and Linux systemd
|
|
58
|
+
user units. Windows background service setup is not supported yet.
|
|
59
|
+
|
|
60
|
+
First, run `start` once in a terminal and finish sign-in. The runner saves its
|
|
61
|
+
runner credential in the workspace with file mode 0600. Stop it with Ctrl-C.
|
|
62
|
+
Install the package in a stable location before you install the service. For
|
|
63
|
+
example:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm install -g @peakflo/agent-harness@latest
|
|
67
|
+
pf-agent-harness service install --control-plane https://api.peakflo.ai
|
|
68
|
+
pf-agent-harness service status
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For a self-hosted host, add `--runtime self-hosted`. If you used a custom
|
|
72
|
+
`--workspace-root` for the first start, pass that same path to `service install`.
|
|
73
|
+
Other `start` options can also follow `service install`. To enable vendor
|
|
74
|
+
harnesses on this machine, pass `--vendor-harnesses codex,claude-code,cursor` or set
|
|
75
|
+
`PF_VENDOR_HARNESSES` when you install. The service saves that choice as a
|
|
76
|
+
start option. It does not save a token or copy the current environment.
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pf-agent-harness service repair
|
|
80
|
+
pf-agent-harness service uninstall
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`install` checks that the health port is free before it starts a new service.
|
|
84
|
+
Stop a manual runner before you install. `install` updates an existing service.
|
|
85
|
+
`repair` writes the service file again with the current Node.js and harness
|
|
86
|
+
paths, then restarts it. `uninstall` stops and removes the service and its
|
|
87
|
+
settings. It keeps the runner credential so you
|
|
88
|
+
can use the runner again. `status` shows running or stopped and the last exit
|
|
89
|
+
error that the service manager reports.
|
|
90
|
+
|
|
91
|
+
On macOS, the service file is in `~/Library/LaunchAgents`, and logs are in
|
|
92
|
+
`~/.pf-agent-harness/logs`. On Linux, the unit is in
|
|
93
|
+
`~/.config/systemd/user`; read its logs with
|
|
94
|
+
`journalctl --user -u pf-agent-harness.service`. A Linux user service can stop
|
|
95
|
+
when you log out. If it must run after logout, ask your system administrator to
|
|
96
|
+
enable linger with `loginctl enable-linger $USER`. The installer does not run
|
|
97
|
+
that command.
|
|
98
|
+
|
|
99
|
+
On macOS, rotate the two log files in `~/.pf-agent-harness/logs` with your
|
|
100
|
+
normal user log rotation tool. The service does not cap them. Stop the service
|
|
101
|
+
before you remove or truncate them. After an npm or Node.js upgrade, run
|
|
102
|
+
`pf-agent-harness service repair` so the service uses the new paths. If you
|
|
103
|
+
installed the harness from npm's temporary `npx` cache or use an `nvm` Node.js
|
|
104
|
+
path, `service install` shows a warning about this path.
|
|
105
|
+
|
|
106
|
+
New service files start with `--service --stored-credential`. Older service
|
|
107
|
+
files with only `--stored-credential` still work after an upgrade. Run
|
|
108
|
+
`service repair` to update the file. The runner reads its saved credential
|
|
109
|
+
from the workspace. If that credential is missing, expired, or
|
|
110
|
+
rejected, start the runner once in a terminal to sign in again. The service
|
|
111
|
+
file and the service settings contain no enrolment token or runner credential.
|
|
112
|
+
It stops after an invalid option, a refused harness version, an unusable
|
|
113
|
+
credential, or a local port conflict. Linux waits 30 seconds before it retries
|
|
114
|
+
other failures. `service status` shows the last cause. A manual `start` refuses to
|
|
115
|
+
run beside a live service with the same workspace or health port.
|
|
116
|
+
|
|
41
117
|
## Harnesses found on your machine
|
|
42
118
|
|
|
43
119
|
On a desktop or self-hosted machine the runner looks for Codex (`codex`),
|
|
@@ -46,15 +122,26 @@ Claude Code (`claude`), Cursor (`cursor-agent`), Pi (`pi`) and OpenCode
|
|
|
46
122
|
whether it is signed in, and the models it offers. Peakflo shows this list in
|
|
47
123
|
the agent editor and in the chat details.
|
|
48
124
|
|
|
49
|
-
**Which ones the runner can run.** Pi always.
|
|
50
|
-
|
|
51
|
-
installed and signed in on that machine
|
|
52
|
-
do when you run the tool yourself. An agent in
|
|
53
|
-
harness and one of its models. OpenCode is
|
|
54
|
-
cloud sandbox runs Pi only.
|
|
55
|
-
|
|
56
|
-
- These turns use
|
|
57
|
-
|
|
125
|
+
**Which ones the runner can run.** Pi always. After onboarding 0g adds personal
|
|
126
|
+
tokens, a personal desktop or self-hosted runner can also run Codex, Claude Code
|
|
127
|
+
and Cursor while each tool is installed and signed in on that machine. The
|
|
128
|
+
runner uses that sign-in, as you do when you run the tool yourself. An agent in
|
|
129
|
+
Peakflo can then be given that harness and one of its models. OpenCode is
|
|
130
|
+
listed and cannot be run yet. A cloud sandbox runs Pi only.
|
|
131
|
+
|
|
132
|
+
- These turns use a configured vendor account. Codex links only `auth.json`
|
|
133
|
+
into a private runner folder, so login refresh stays in sync. Codex keyring
|
|
134
|
+
and auto login are not supported in that folder; select file login or an API
|
|
135
|
+
key. Claude Code uses a private config folder. On macOS it keeps access to
|
|
136
|
+
the host login keychain. On Linux it links the owner's credential file into
|
|
137
|
+
that folder and uses the owner file as the secure storage path for refresh.
|
|
138
|
+
API-key turns use a separate folder with no subscription login link. Claude
|
|
139
|
+
Code 2.1.144 or newer is required for this login path.
|
|
140
|
+
Cursor keeps the host home folder on macOS for its login keychain. Its
|
|
141
|
+
other config roots are private, but Cursor can still read owner settings
|
|
142
|
+
under the home folder. On Linux, the private home folder cannot find a
|
|
143
|
+
Cursor subscription login; use an API key for Cursor turns there.
|
|
144
|
+
These turns do not go through the Peakflo model gateway.
|
|
58
145
|
- When you sign out of a harness, or remove it, the runner stops taking work
|
|
59
146
|
for it after its next check.
|
|
60
147
|
- What the runner brings and what it expects, the same split as the 20x app:
|
|
@@ -94,6 +181,17 @@ or the content of credential files. Model names that look like an email
|
|
|
94
181
|
address or a key are dropped; that check is a safety net, not a guarantee
|
|
95
182
|
about what a third-party program prints.
|
|
96
183
|
|
|
184
|
+
**Enabling vendor turns.** Discovery only reports Codex, Claude Code and
|
|
185
|
+
Cursor. It does not let the runner take turns with them. To enable one or more,
|
|
186
|
+
start the runner with `--vendor-harnesses codex,claude-code,cursor`, or set
|
|
187
|
+
`PF_VENDOR_HARNESSES` to a comma-separated list. The default is empty. The
|
|
188
|
+
control plane also requires a personal runner whose owner is the turn speaker.
|
|
189
|
+
Admin-minted tokens always create shared runners, which cannot take vendor turns
|
|
190
|
+
even when they have an owner and this flag. Vendor turns are unavailable until
|
|
191
|
+
member-minted personal tokens ship in onboarding 0g. After that, each person
|
|
192
|
+
must register their own personal runner with a token they minted. An existing
|
|
193
|
+
shared runner must be replaced and any agent pins must use the new runner ID.
|
|
194
|
+
|
|
97
195
|
**Turning it off.** Start with `--no-harness-discovery`, or set
|
|
98
196
|
`PF_HARNESS_DISCOVERY=0`. The runner then starts none of these programs and
|
|
99
197
|
reports no list. Running turns with Pi is not affected.
|
|
@@ -112,10 +210,22 @@ cannot be honoured exactly, the runner uses the stricter behaviour.
|
|
|
112
210
|
| Don't ask for permission | Runs without asking | Runs without asking | Runs without asking; each request Cursor raises is approved once |
|
|
113
211
|
|
|
114
212
|
Cursor is read-only in the asking modes because it asks only about commands
|
|
115
|
-
that are not on
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
213
|
+
that are not on its own allow-list. Cursor has no supported switch that
|
|
214
|
+
proves all plugins and keychain state are isolated. If Cursor
|
|
215
|
+
will not accept the read-only mode, the turn is refused.
|
|
216
|
+
|
|
217
|
+
Every vendor turn gets only a selected child environment. Repository turns
|
|
218
|
+
get a session GitHub sentinel and local proxy. Other turns get an empty `gh`
|
|
219
|
+
login folder. Git uses no owner global configuration. Claude Code loads no
|
|
220
|
+
user or project settings. Codex receives a private `CODEX_HOME` with login
|
|
221
|
+
data only; owner MCP servers from `config.toml` are not copied. The default
|
|
222
|
+
approval wait is 30 minutes. Set `CODING_AGENT_APPROVAL_TIMEOUT_MINUTES` on
|
|
223
|
+
the runner to change it. An unanswered approval is denied and ends the turn.
|
|
224
|
+
An unanswered agent question also closes after this limit. On desktop and
|
|
225
|
+
self-hosted runners, the vendor CLI uses the same OS account as the owner.
|
|
226
|
+
These environment and config rules stop default credential lookups. They are
|
|
227
|
+
not a hard security boundary outside a sandbox: a process with that account's
|
|
228
|
+
filesystem and keychain access can still read owner data directly.
|
|
119
229
|
|
|
120
230
|
The runner never answers a request with "always allow", and does not offer
|
|
121
231
|
that answer to the person approving: Codex and Cursor would save it as a
|
|
@@ -28,11 +28,10 @@ exports.AcpAdapter = exports.ACP_SESSION_START_TIMEOUT_MS = void 0;
|
|
|
28
28
|
const child_process_1 = require("child_process");
|
|
29
29
|
const crypto_1 = require("crypto");
|
|
30
30
|
const child_stream_guards_1 = require("../runtime/child-stream-guards");
|
|
31
|
-
const fs_1 = require("fs");
|
|
32
|
-
const os_1 = require("os");
|
|
33
31
|
const path_1 = require("path");
|
|
34
32
|
const util_1 = require("util");
|
|
35
33
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
34
|
+
const vendor_environment_1 = require("./vendor-environment");
|
|
36
35
|
const cursor_acp_1 = require("./cursor-acp");
|
|
37
36
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
38
37
|
/**
|
|
@@ -183,7 +182,8 @@ class AcpAdapter {
|
|
|
183
182
|
env.CODEX_API_KEY = key;
|
|
184
183
|
}
|
|
185
184
|
env.NO_BROWSER = '1';
|
|
186
|
-
env.CODEX_HOME = (0,
|
|
185
|
+
env.CODEX_HOME = (0, vendor_environment_1.codexVendorConfigDir)(config, true);
|
|
186
|
+
(0, vendor_environment_1.isolateVendorHome)(env, env.CODEX_HOME);
|
|
187
187
|
console.log(`[AcpAdapter/codex] Auth: API key (authMethod=${config.authMethod ?? 'legacy'}, isolated CODEX_HOME)`);
|
|
188
188
|
return true;
|
|
189
189
|
}
|
|
@@ -191,19 +191,22 @@ class AcpAdapter {
|
|
|
191
191
|
// hijack auth away from the ChatGPT subscription in the default CODEX_HOME.
|
|
192
192
|
delete env.OPENAI_API_KEY;
|
|
193
193
|
delete env.CODEX_API_KEY;
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!env.CODEX_HOME) {
|
|
199
|
-
env.CODEX_HOME = (0, path_1.join)((0, os_1.homedir)(), '.codex');
|
|
200
|
-
}
|
|
194
|
+
// Copy only the subscription login. Do not load the owner's config.toml,
|
|
195
|
+
// which can name MCP servers and rules outside this turn's lease.
|
|
196
|
+
env.CODEX_HOME = (0, vendor_environment_1.codexVendorConfigDir)(config, false);
|
|
197
|
+
(0, vendor_environment_1.isolateVendorHome)(env, env.CODEX_HOME);
|
|
201
198
|
console.log(`[AcpAdapter/codex] Auth: ChatGPT subscription / Codex CLI login (authMethod=${config.authMethod ?? 'legacy'}, CODEX_HOME=${env.CODEX_HOME})`);
|
|
202
199
|
return false;
|
|
203
200
|
}
|
|
204
201
|
configureCursorAuthEnv(env, config) {
|
|
205
202
|
if (this.agentType !== 'cursor')
|
|
206
203
|
return;
|
|
204
|
+
// Keep configuration roots private, but preserve the macOS login keychain.
|
|
205
|
+
// Cursor has no complete switch for owner settings under ~/.cursor.
|
|
206
|
+
(0, vendor_environment_1.isolateVendorHome)(env, (0, vendor_environment_1.vendorConfigDir)('cursor', config, '', []));
|
|
207
|
+
if (process.platform === 'darwin' && process.env.HOME) {
|
|
208
|
+
env.HOME = process.env.HOME;
|
|
209
|
+
}
|
|
207
210
|
const explicitApiKey = config.apiKeys?.cursor;
|
|
208
211
|
const useApiKey = config.authMethod === 'api_key'
|
|
209
212
|
|| (config.authMethod !== 'subscription' && !!explicitApiKey);
|
|
@@ -276,19 +279,12 @@ class AcpAdapter {
|
|
|
276
279
|
const sessionId = config.taskId;
|
|
277
280
|
console.log(`[AcpAdapter/${this.agentType}] Creating session ${sessionId}`);
|
|
278
281
|
// Prepare environment with API keys
|
|
279
|
-
const env = {
|
|
280
|
-
...
|
|
282
|
+
const env = (0, vendor_environment_1.vendorEnvironment)(config, {
|
|
283
|
+
...(this.agentType === 'codex'
|
|
284
|
+
? { OPENAI_API_KEY: process.env.OPENAI_API_KEY, CODEX_API_KEY: process.env.CODEX_API_KEY }
|
|
285
|
+
: { CURSOR_API_KEY: process.env.CURSOR_API_KEY }),
|
|
281
286
|
...this.agentConfig.env
|
|
282
|
-
};
|
|
283
|
-
if (config.apiKeys?.anthropic) {
|
|
284
|
-
env.ANTHROPIC_API_KEY = config.apiKeys.anthropic;
|
|
285
|
-
}
|
|
286
|
-
// Inject secret env vars directly into process environment
|
|
287
|
-
if (config.secretEnvVars && Object.keys(config.secretEnvVars).length > 0) {
|
|
288
|
-
for (const [key, value] of Object.entries(config.secretEnvVars)) {
|
|
289
|
-
env[key] = value;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
287
|
+
});
|
|
292
288
|
// Decide Codex auth (subscription vs API key) LAST so it is authoritative.
|
|
293
289
|
// It must run after secret-env injection: a user secret named OPENAI_API_KEY /
|
|
294
290
|
// CODEX_API_KEY would otherwise be re-added to env after subscription mode
|
|
@@ -437,19 +433,12 @@ class AcpAdapter {
|
|
|
437
433
|
anthropicKeyLength: config.apiKeys?.anthropic?.length
|
|
438
434
|
});
|
|
439
435
|
// Prepare environment with API keys
|
|
440
|
-
const env = {
|
|
441
|
-
...
|
|
436
|
+
const env = (0, vendor_environment_1.vendorEnvironment)(config, {
|
|
437
|
+
...(this.agentType === 'codex'
|
|
438
|
+
? { OPENAI_API_KEY: process.env.OPENAI_API_KEY, CODEX_API_KEY: process.env.CODEX_API_KEY }
|
|
439
|
+
: { CURSOR_API_KEY: process.env.CURSOR_API_KEY }),
|
|
442
440
|
...this.agentConfig.env
|
|
443
|
-
};
|
|
444
|
-
if (config.apiKeys?.anthropic) {
|
|
445
|
-
env.ANTHROPIC_API_KEY = config.apiKeys.anthropic;
|
|
446
|
-
}
|
|
447
|
-
// Inject secret env vars directly into process environment
|
|
448
|
-
if (config.secretEnvVars && Object.keys(config.secretEnvVars).length > 0) {
|
|
449
|
-
for (const [key, value] of Object.entries(config.secretEnvVars)) {
|
|
450
|
-
env[key] = value;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
441
|
+
});
|
|
453
442
|
// Decide Codex auth (subscription vs API key) LAST so it is authoritative —
|
|
454
443
|
// see createSession for why this must run after secret-env injection.
|
|
455
444
|
const codexUseApiKey = this.configureCodexAuthEnv(env, config);
|
|
@@ -1258,10 +1247,9 @@ class AcpAdapter {
|
|
|
1258
1247
|
? oneTimeOptions
|
|
1259
1248
|
: allApprovalOptions;
|
|
1260
1249
|
if (session.config.permissionMode === 'allow') {
|
|
1261
|
-
// Cursor saves "allow always" as a standing rule
|
|
1262
|
-
//
|
|
1263
|
-
//
|
|
1264
|
-
// to ask. Every request is approved once instead.
|
|
1250
|
+
// Cursor saves "allow always" as a standing rule (seen: one bypass
|
|
1251
|
+
// turn added Shell(echo) and Shell(cat)). That rule could affect later
|
|
1252
|
+
// turns. Every request is approved once instead.
|
|
1265
1253
|
if (this.agentType === 'cursor') {
|
|
1266
1254
|
const once = approvalOptions.find((option) => option.optionId === 'allow-once')
|
|
1267
1255
|
|| approvalOptions.find((option) => option.kind === 'allow_once');
|
|
@@ -8,15 +8,9 @@
|
|
|
8
8
|
* - File-based session persistence (~/.claude/sessions/)
|
|
9
9
|
* - Different message format (SDKMessage vs OpenCode messages)
|
|
10
10
|
*/
|
|
11
|
-
/**
|
|
12
|
-
* Extracted verbatim from 20x `src/main/adapters/claude-code-adapter.ts`.
|
|
13
|
-
* CHANGED: nothing. The only import it has is `./coding-agent-adapter`, which
|
|
14
|
-
* sits in the same directory here too. `@anthropic-ai/claude-agent-sdk` is an
|
|
15
|
-
* optional dependency (dynamically imported) and is declared ambiently in
|
|
16
|
-
* `./optional-agent-sdks.d.ts` so `tsc` succeeds when it is not installed.
|
|
17
|
-
* Source formatting (no semicolons) preserved.
|
|
18
|
-
*/
|
|
19
11
|
import type { CodingAgentAdapter, SessionConfig, SessionStatus, SessionMessage, MessagePart } from './coding-agent-adapter';
|
|
12
|
+
export declare function assertClaudeIsolatedConfigVersion(version: string): void;
|
|
13
|
+
export declare function verifyClaudeIsolatedConfigVersion(path: string): Promise<void>;
|
|
20
14
|
export declare enum ClaudeSystemSubtype {
|
|
21
15
|
INIT = "init",
|
|
22
16
|
TASK_STARTED = "task_started",
|
|
@@ -35,6 +29,7 @@ export declare class ClaudeCodeAdapter implements CodingAgentAdapter {
|
|
|
35
29
|
private sessions;
|
|
36
30
|
private sdkLoading;
|
|
37
31
|
private claudeExecutablePath;
|
|
32
|
+
private verifiedClaudeExecutablePath;
|
|
38
33
|
/**
|
|
39
34
|
* Callback set by agent-manager to trigger an immediate poll cycle
|
|
40
35
|
* when new stream data is buffered. Eliminates the up-to-2-second
|
|
@@ -51,12 +46,7 @@ export declare class ClaudeCodeAdapter implements CodingAgentAdapter {
|
|
|
51
46
|
* (no shell: true). Resolve claude.cmd → the underlying cli.js path.
|
|
52
47
|
*/
|
|
53
48
|
private resolveWindowsCmdToJs;
|
|
54
|
-
/**
|
|
55
|
-
* Build environment variables for Claude process
|
|
56
|
-
* Removes CLAUDECODE to prevent nested session errors.
|
|
57
|
-
* When secrets are configured, sets SHELL to the secret-shell.sh wrapper
|
|
58
|
-
* so every bash command fetches secrets from the broker transparently.
|
|
59
|
-
*/
|
|
49
|
+
/** Build an isolated Claude process environment with the host keychain login. */
|
|
60
50
|
private buildClaudeEnvironment;
|
|
61
51
|
/**
|
|
62
52
|
* Build PreToolUse hooks for secret injection.
|
|
@@ -44,11 +44,50 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
})();
|
|
45
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
46
|
exports.ClaudeCodeAdapter = exports.ClaudeSystemSubtype = void 0;
|
|
47
|
+
exports.assertClaudeIsolatedConfigVersion = assertClaudeIsolatedConfigVersion;
|
|
48
|
+
exports.verifyClaudeIsolatedConfigVersion = verifyClaudeIsolatedConfigVersion;
|
|
49
|
+
/**
|
|
50
|
+
* Extracted verbatim from 20x `src/main/adapters/claude-code-adapter.ts`.
|
|
51
|
+
* CHANGED: nothing. The only import it has is `./coding-agent-adapter`, which
|
|
52
|
+
* sits in the same directory here too. `@anthropic-ai/claude-agent-sdk` is an
|
|
53
|
+
* optional dependency (dynamically imported) and is declared ambiently in
|
|
54
|
+
* `./optional-agent-sdks.d.ts` so `tsc` succeeds when it is not installed.
|
|
55
|
+
* Source formatting (no semicolons) preserved.
|
|
56
|
+
*/
|
|
57
|
+
const node_os_1 = require("node:os");
|
|
58
|
+
const node_path_1 = require("node:path");
|
|
47
59
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
48
60
|
const permission_mode_1 = require("../runtime/permission-mode");
|
|
61
|
+
const vendor_environment_1 = require("./vendor-environment");
|
|
49
62
|
let ClaudeAgentSDK = null;
|
|
50
63
|
/** Module-level cache for the resolved Claude executable path (persists across adapter instances) */
|
|
51
64
|
let resolvedClaudeExecutablePath = null;
|
|
65
|
+
const MINIMUM_ISOLATED_CONFIG_VERSION = [2, 1, 144];
|
|
66
|
+
function assertClaudeIsolatedConfigVersion(version) {
|
|
67
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:\s|$)/);
|
|
68
|
+
const actual = match?.slice(1, 4).map(Number);
|
|
69
|
+
const firstDifference = actual?.findIndex((part, index) => part !== MINIMUM_ISOLATED_CONFIG_VERSION[index]);
|
|
70
|
+
const tooOld = actual && firstDifference !== undefined && firstDifference >= 0 &&
|
|
71
|
+
actual[firstDifference] < MINIMUM_ISOLATED_CONFIG_VERSION[firstDifference];
|
|
72
|
+
if (!actual || tooOld) {
|
|
73
|
+
throw new Error('Claude Code CLI is too old or its version cannot be checked for isolated config. Upgrade Claude Code to 2.1.144 or newer.');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function verifyClaudeIsolatedConfigVersion(path) {
|
|
77
|
+
const { execFile } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
78
|
+
const { promisify } = await Promise.resolve().then(() => __importStar(require('node:util')));
|
|
79
|
+
const command = path.endsWith('.js') ? process.execPath : path;
|
|
80
|
+
const args = path.endsWith('.js') ? [path, '--version'] : ['--version'];
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await promisify(execFile)(command, args, { timeout: 5_000 });
|
|
83
|
+
assertClaudeIsolatedConfigVersion(stdout.trim());
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof Error && error.message.includes('Upgrade Claude Code'))
|
|
87
|
+
throw error;
|
|
88
|
+
throw new Error('Claude Code CLI version could not be checked for isolated config. Upgrade Claude Code to 2.1.144 or newer.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
52
91
|
/** Maximum number of messages to keep in the buffer per session */
|
|
53
92
|
const MAX_MESSAGE_BUFFER_SIZE = 500;
|
|
54
93
|
var ClaudeSystemSubtype;
|
|
@@ -84,6 +123,7 @@ class ClaudeCodeAdapter {
|
|
|
84
123
|
sessions = new Map();
|
|
85
124
|
sdkLoading = null;
|
|
86
125
|
claudeExecutablePath = null;
|
|
126
|
+
verifiedClaudeExecutablePath = null;
|
|
87
127
|
/**
|
|
88
128
|
* Callback set by agent-manager to trigger an immediate poll cycle
|
|
89
129
|
* when new stream data is buffered. Eliminates the up-to-2-second
|
|
@@ -215,16 +255,35 @@ class ClaudeCodeAdapter {
|
|
|
215
255
|
// Fall back to original .cmd path
|
|
216
256
|
return cmdPath;
|
|
217
257
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
/** Build an isolated Claude process environment with the host keychain login. */
|
|
259
|
+
buildClaudeEnvironment(config) {
|
|
260
|
+
const useApiKey = config.authMethod === 'api_key';
|
|
261
|
+
const env = (0, vendor_environment_1.vendorEnvironment)(config, {
|
|
262
|
+
ANTHROPIC_API_KEY: useApiKey ? config.apiKeys?.anthropic ?? process.env.ANTHROPIC_API_KEY : undefined,
|
|
263
|
+
CLAUDE_CODE_OAUTH_TOKEN: useApiKey ? undefined : process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
264
|
+
});
|
|
265
|
+
if (useApiKey)
|
|
266
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
267
|
+
else
|
|
268
|
+
delete env.ANTHROPIC_API_KEY;
|
|
269
|
+
const ownerClaudeDir = (0, node_path_1.resolve)(process.env.CLAUDE_CONFIG_DIR || (0, node_path_1.join)((0, node_os_1.homedir)(), '.claude'));
|
|
270
|
+
env.CLAUDE_CONFIG_DIR = (0, vendor_environment_1.claudeVendorConfigDir)(config);
|
|
271
|
+
// On Linux, read and refresh the owner's credential file at its source.
|
|
272
|
+
// The private config also has a link for CLI paths that use that root.
|
|
273
|
+
// On macOS, an empty value selects the owner's original Keychain item.
|
|
274
|
+
env.CLAUDE_SECURESTORAGE_CONFIG_DIR = useApiKey
|
|
275
|
+
? env.CLAUDE_CONFIG_DIR
|
|
276
|
+
: process.platform === 'linux' ? ownerClaudeDir : '';
|
|
277
|
+
// macOS needs the real HOME to find the login keychain. Linux can keep
|
|
278
|
+
// HOME private because the credential file is linked into this config.
|
|
279
|
+
if (useApiKey || process.platform === 'linux')
|
|
280
|
+
(0, vendor_environment_1.isolateVendorHome)(env, env.CLAUDE_CONFIG_DIR);
|
|
281
|
+
else {
|
|
282
|
+
if (process.env.HOME)
|
|
283
|
+
env.HOME = process.env.HOME;
|
|
284
|
+
if (process.env.USERPROFILE)
|
|
285
|
+
env.USERPROFILE = process.env.USERPROFILE;
|
|
286
|
+
}
|
|
228
287
|
return env;
|
|
229
288
|
}
|
|
230
289
|
/**
|
|
@@ -234,7 +293,7 @@ class ClaudeCodeAdapter {
|
|
|
234
293
|
* the output appear in conversation context.
|
|
235
294
|
*/
|
|
236
295
|
buildSecretHooks(config) {
|
|
237
|
-
const secretEnvVars = config.secretEnvVars;
|
|
296
|
+
const secretEnvVars = Object.fromEntries(Object.entries(config.secretEnvVars ?? {}).filter(([key]) => (0, vendor_environment_1.allowedVendorSecretKey)(key)));
|
|
238
297
|
if (!secretEnvVars || Object.keys(secretEnvVars).length === 0) {
|
|
239
298
|
return undefined;
|
|
240
299
|
}
|
|
@@ -339,12 +398,12 @@ class ClaudeCodeAdapter {
|
|
|
339
398
|
* Cleans the session file by removing messages with empty text blocks
|
|
340
399
|
* This prevents API errors when resuming sessions
|
|
341
400
|
*/
|
|
342
|
-
async cleanSessionFile(sessionId,
|
|
401
|
+
async cleanSessionFile(sessionId, config) {
|
|
343
402
|
try {
|
|
344
403
|
const { readFileSync, writeFileSync, existsSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
345
404
|
const { join } = await Promise.resolve().then(() => __importStar(require('path')));
|
|
346
|
-
const
|
|
347
|
-
const claudeDir = join(
|
|
405
|
+
const workspaceDir = config.workspaceDir;
|
|
406
|
+
const claudeDir = join((0, vendor_environment_1.claudeVendorConfigDir)(config), 'projects');
|
|
348
407
|
// Claude Code CLI encodes workspace paths by replacing all non-alphanumeric/non-hyphen chars with '-'
|
|
349
408
|
const encodedWorkspace = workspaceDir.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
350
409
|
const sessionFile = join(claudeDir, encodedWorkspace, `${sessionId}.jsonl`);
|
|
@@ -400,20 +459,20 @@ class ClaudeCodeAdapter {
|
|
|
400
459
|
if (!workspaceDir)
|
|
401
460
|
return [];
|
|
402
461
|
try {
|
|
403
|
-
return await this.loadSessionHistory(sessionId,
|
|
462
|
+
return await this.loadSessionHistory(sessionId, config);
|
|
404
463
|
}
|
|
405
464
|
catch {
|
|
406
465
|
return [];
|
|
407
466
|
}
|
|
408
467
|
}
|
|
409
|
-
async loadSessionHistory(sessionId,
|
|
468
|
+
async loadSessionHistory(sessionId, config) {
|
|
410
469
|
try {
|
|
411
470
|
const { readFileSync, existsSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
412
471
|
const { join } = await Promise.resolve().then(() => __importStar(require('path')));
|
|
413
|
-
const
|
|
414
|
-
// Session files are stored
|
|
472
|
+
const workspaceDir = config.workspaceDir;
|
|
473
|
+
// Session files are stored under the same runner config used by the CLI.
|
|
415
474
|
// Claude Code CLI encodes workspace paths by replacing all non-alphanumeric/non-hyphen chars with '-'
|
|
416
|
-
const claudeDir = join(
|
|
475
|
+
const claudeDir = join((0, vendor_environment_1.claudeVendorConfigDir)(config), 'projects');
|
|
417
476
|
const encodedWorkspace = workspaceDir.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
418
477
|
const sessionFile = join(claudeDir, encodedWorkspace, `${sessionId}.jsonl`);
|
|
419
478
|
console.log(`[ClaudeCodeAdapter] Loading session history from: ${sessionFile}`);
|
|
@@ -577,7 +636,7 @@ class ClaudeCodeAdapter {
|
|
|
577
636
|
throw new Error('INCOMPATIBLE_SESSION_ID: This session was created with a different coding agent and cannot be resumed with Claude Code.');
|
|
578
637
|
}
|
|
579
638
|
// Clean session file to remove empty text blocks before resuming
|
|
580
|
-
await this.cleanSessionFile(sessionId, config
|
|
639
|
+
await this.cleanSessionFile(sessionId, config);
|
|
581
640
|
// Create session state (idle until user sends a message)
|
|
582
641
|
const session = {
|
|
583
642
|
sessionId,
|
|
@@ -600,7 +659,7 @@ class ClaudeCodeAdapter {
|
|
|
600
659
|
// The query will be started in sendPrompt with resume option
|
|
601
660
|
console.log(`[ClaudeCodeAdapter] Session resumed: ${sessionId} (waiting for user prompt)`);
|
|
602
661
|
// Load conversation history from session file
|
|
603
|
-
const messages = await this.loadSessionHistory(sessionId, config
|
|
662
|
+
const messages = await this.loadSessionHistory(sessionId, config);
|
|
604
663
|
console.log(`[ClaudeCodeAdapter] Session loaded with ${messages.length} messages`);
|
|
605
664
|
return messages;
|
|
606
665
|
}
|
|
@@ -653,6 +712,10 @@ class ClaudeCodeAdapter {
|
|
|
653
712
|
}
|
|
654
713
|
// Find Claude executable
|
|
655
714
|
const claudePath = await this.findClaudeExecutable();
|
|
715
|
+
if (this.verifiedClaudeExecutablePath !== claudePath) {
|
|
716
|
+
await verifyClaudeIsolatedConfigVersion(claudePath);
|
|
717
|
+
this.verifiedClaudeExecutablePath = claudePath;
|
|
718
|
+
}
|
|
656
719
|
// Create new abort controller
|
|
657
720
|
const abortController = new AbortController();
|
|
658
721
|
session.abortController = abortController;
|
|
@@ -663,7 +726,8 @@ class ClaudeCodeAdapter {
|
|
|
663
726
|
const options = {
|
|
664
727
|
cwd: config.workspaceDir,
|
|
665
728
|
pathToClaudeCodeExecutable: claudePath,
|
|
666
|
-
env: this.buildClaudeEnvironment(),
|
|
729
|
+
env: this.buildClaudeEnvironment(config),
|
|
730
|
+
settingSources: [],
|
|
667
731
|
mcpServers: config.mcpServers,
|
|
668
732
|
model: config.model,
|
|
669
733
|
effort,
|
|
@@ -53,6 +53,7 @@ exports.CodexAdapter = exports.DEFAULT_CODEX_MODEL = void 0;
|
|
|
53
53
|
const child_process_1 = require("child_process");
|
|
54
54
|
const fs_1 = require("fs");
|
|
55
55
|
const child_stream_guards_1 = require("../runtime/child-stream-guards");
|
|
56
|
+
const vendor_environment_1 = require("./vendor-environment");
|
|
56
57
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
57
58
|
exports.DEFAULT_CODEX_MODEL = 'gpt-5.6-sol';
|
|
58
59
|
class CodexAdapter {
|
|
@@ -132,10 +133,20 @@ class CodexAdapter {
|
|
|
132
133
|
/**
|
|
133
134
|
* Build environment variables for Codex process
|
|
134
135
|
*/
|
|
135
|
-
buildCodexEnvironment() {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
buildCodexEnvironment(config) {
|
|
137
|
+
const useApiKey = config.authMethod === 'api_key' ||
|
|
138
|
+
(config.authMethod !== 'subscription' && !!config.apiKeys?.openai);
|
|
139
|
+
const env = (0, vendor_environment_1.vendorEnvironment)(config, {
|
|
140
|
+
OPENAI_API_KEY: config.apiKeys?.openai ?? process.env.OPENAI_API_KEY,
|
|
141
|
+
CODEX_API_KEY: config.apiKeys?.openai ?? process.env.CODEX_API_KEY
|
|
142
|
+
});
|
|
143
|
+
if (!useApiKey) {
|
|
144
|
+
delete env.OPENAI_API_KEY;
|
|
145
|
+
delete env.CODEX_API_KEY;
|
|
146
|
+
}
|
|
147
|
+
env.CODEX_HOME = (0, vendor_environment_1.codexVendorConfigDir)(config, useApiKey);
|
|
148
|
+
(0, vendor_environment_1.isolateVendorHome)(env, env.CODEX_HOME);
|
|
149
|
+
if (useApiKey && !env.OPENAI_API_KEY && !env.CODEX_API_KEY) {
|
|
139
150
|
console.warn('[CodexAdapter] No OPENAI_API_KEY or CODEX_API_KEY found in environment');
|
|
140
151
|
}
|
|
141
152
|
return env;
|
|
@@ -162,7 +173,7 @@ class CodexAdapter {
|
|
|
162
173
|
const codexProcess = (0, child_process_1.spawn)(this.codexExecutablePath, this.buildCodexArgs(config), {
|
|
163
174
|
cwd: config.workspaceDir,
|
|
164
175
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
165
|
-
env: this.buildCodexEnvironment(),
|
|
176
|
+
env: this.buildCodexEnvironment(config),
|
|
166
177
|
...(needsShell ? { shell: true } : {}),
|
|
167
178
|
});
|
|
168
179
|
// Every pipe needs an error listener before the first write. Codex can exit
|
|
@@ -235,7 +246,7 @@ class CodexAdapter {
|
|
|
235
246
|
const codexProcess = (0, child_process_1.spawn)(this.codexExecutablePath, this.buildCodexArgs(config), {
|
|
236
247
|
cwd: config.workspaceDir,
|
|
237
248
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
238
|
-
env: this.buildCodexEnvironment(),
|
|
249
|
+
env: this.buildCodexEnvironment(config),
|
|
239
250
|
...(needsShellResume ? { shell: true } : {}),
|
|
240
251
|
});
|
|
241
252
|
// Every pipe needs an error listener before the first write. Codex can exit
|
|
@@ -54,11 +54,11 @@ exports.codexApprovalPolicy = codexApprovalPolicy;
|
|
|
54
54
|
const child_process_1 = require("child_process");
|
|
55
55
|
const fs_1 = require("fs");
|
|
56
56
|
const child_stream_guards_1 = require("../runtime/child-stream-guards");
|
|
57
|
-
const os_1 = require("os");
|
|
58
57
|
const path_1 = require("path");
|
|
59
58
|
const util_1 = require("util");
|
|
60
59
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
61
60
|
const codex_approval_text_1 = require("./codex-approval-text");
|
|
61
|
+
const vendor_environment_1 = require("./vendor-environment");
|
|
62
62
|
const DEFAULT_CODEX_APP_SERVER_MODEL = 'gpt-5.5';
|
|
63
63
|
/**
|
|
64
64
|
* `thread/items/list` returns each item inside an envelope:
|
|
@@ -642,10 +642,10 @@ class CodexAppServerAdapter {
|
|
|
642
642
|
return await execFileAsync(command, args, options);
|
|
643
643
|
}
|
|
644
644
|
buildEnvironment(config) {
|
|
645
|
-
const env =
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
645
|
+
const env = (0, vendor_environment_1.vendorEnvironment)(config, {
|
|
646
|
+
OPENAI_API_KEY: config.apiKeys?.openai ?? process.env.OPENAI_API_KEY,
|
|
647
|
+
CODEX_API_KEY: config.apiKeys?.openai ?? process.env.CODEX_API_KEY
|
|
648
|
+
});
|
|
649
649
|
const explicitApiKey = config.apiKeys?.openai;
|
|
650
650
|
const useApiKey = config.authMethod === 'api_key' ? true
|
|
651
651
|
: config.authMethod === 'subscription' ? false
|
|
@@ -657,7 +657,8 @@ class CodexAppServerAdapter {
|
|
|
657
657
|
env.CODEX_API_KEY = key;
|
|
658
658
|
}
|
|
659
659
|
env.NO_BROWSER = '1';
|
|
660
|
-
env.CODEX_HOME = (0,
|
|
660
|
+
env.CODEX_HOME = (0, vendor_environment_1.codexVendorConfigDir)(config, true);
|
|
661
|
+
(0, vendor_environment_1.isolateVendorHome)(env, env.CODEX_HOME);
|
|
661
662
|
return {
|
|
662
663
|
env,
|
|
663
664
|
usesApiKey: true,
|
|
@@ -667,9 +668,8 @@ class CodexAppServerAdapter {
|
|
|
667
668
|
if (!useApiKey) {
|
|
668
669
|
delete env.OPENAI_API_KEY;
|
|
669
670
|
delete env.CODEX_API_KEY;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
671
|
+
env.CODEX_HOME = (0, vendor_environment_1.codexVendorConfigDir)(config, false);
|
|
672
|
+
(0, vendor_environment_1.isolateVendorHome)(env, env.CODEX_HOME);
|
|
673
673
|
}
|
|
674
674
|
return {
|
|
675
675
|
env,
|
|
@@ -40,9 +40,8 @@ export type CursorMode = 'agent' | 'plan' | 'ask';
|
|
|
40
40
|
* The Cursor mode a turn must run in.
|
|
41
41
|
*
|
|
42
42
|
* FAIL CLOSED. In its `agent` mode Cursor asks only about commands that are
|
|
43
|
-
* not on
|
|
44
|
-
*
|
|
45
|
-
* that list, so it cannot know which actions Cursor will run unasked, and a
|
|
43
|
+
* not on its own allow-list (seen: "Not in allowlist: cat, echo"). The runner
|
|
44
|
+
* cannot know which actions Cursor will run unasked, and a
|
|
46
45
|
* permission mode that promises a question before EACH action cannot be
|
|
47
46
|
* promised for Cursor. Every asking mode therefore runs
|
|
48
47
|
* Cursor in one of its own read-only modes — stricter than what was asked for,
|
|
@@ -112,9 +112,8 @@ function resolveCursorModelValue(model, sessionResult) {
|
|
|
112
112
|
* The Cursor mode a turn must run in.
|
|
113
113
|
*
|
|
114
114
|
* FAIL CLOSED. In its `agent` mode Cursor asks only about commands that are
|
|
115
|
-
* not on
|
|
116
|
-
*
|
|
117
|
-
* that list, so it cannot know which actions Cursor will run unasked, and a
|
|
115
|
+
* not on its own allow-list (seen: "Not in allowlist: cat, echo"). The runner
|
|
116
|
+
* cannot know which actions Cursor will run unasked, and a
|
|
118
117
|
* permission mode that promises a question before EACH action cannot be
|
|
119
118
|
* promised for Cursor. Every asking mode therefore runs
|
|
120
119
|
* Cursor in one of its own read-only modes — stricter than what was asked for,
|