@phnx-labs/agents-cli 1.22.114 → 1.22.116
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/CHANGELOG.md +245 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +210 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +214 -148
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/feed.js +65 -14
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +158 -11
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +29 -11
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +93 -302
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +3 -9
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +6 -5
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +229 -37
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +141 -0
- package/dist/lib/feed/hub-server.js +586 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +72 -1
- package/dist/lib/hosts/remote-cmd.js +151 -10
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +27 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +220 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
package/dist/lib/session/tail.js
CHANGED
|
@@ -73,3 +73,222 @@ export function readSessionTailWithRaw(filePath, agent, maxBytes = DEFAULT_MAX_B
|
|
|
73
73
|
export function readSessionTail(filePath, agent, maxBytes = DEFAULT_MAX_BYTES, maxEvents = DEFAULT_MAX_EVENTS) {
|
|
74
74
|
return readSessionTailWithRaw(filePath, agent, maxBytes, maxEvents).events;
|
|
75
75
|
}
|
|
76
|
+
/** A session's original first turn is typically within the first few KiB —
|
|
77
|
+
* far smaller than the tail window, since it's exactly one line near the
|
|
78
|
+
* start of the file, not a rolling window of recent activity. */
|
|
79
|
+
const DEFAULT_HEAD_MAX_BYTES = 32 * 1024;
|
|
80
|
+
/**
|
|
81
|
+
* Read exactly the first `maxBytes` of the file and, if that chunk doesn't
|
|
82
|
+
* reach EOF, drop a trailing partial line so downstream per-line parsers only
|
|
83
|
+
* see whole lines. This is the plain, cheap path — correct and unchanged for
|
|
84
|
+
* the overwhelming majority of transcripts, where the opening JSONL record is
|
|
85
|
+
* a few hundred bytes. Returns '' on any error, an empty file, or a chunk that
|
|
86
|
+
* turned out to hold no complete line at all (the record is bigger than
|
|
87
|
+
* `maxBytes`) — callers needing to recover from THAT case use
|
|
88
|
+
* {@link readSessionHeadContentBounded}, not this.
|
|
89
|
+
*/
|
|
90
|
+
function readSessionHeadChunk(filePath, maxBytes) {
|
|
91
|
+
let fd;
|
|
92
|
+
try {
|
|
93
|
+
fd = fs.openSync(filePath, 'r');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const size = fs.fstatSync(fd).size;
|
|
100
|
+
if (size === 0)
|
|
101
|
+
return { content: '', sawEof: true };
|
|
102
|
+
const len = Math.min(size, maxBytes);
|
|
103
|
+
const buf = Buffer.alloc(len);
|
|
104
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
105
|
+
return { content: buf.toString('utf8'), sawEof: len >= size };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
fs.closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Read the FIRST `maxBytes` of a JSONL transcript as cleaned text — the mirror
|
|
116
|
+
* of {@link readSessionTailContent}, for recovering the session's ACTUAL
|
|
117
|
+
* original request boundedly when it isn't already indexed
|
|
118
|
+
* (`SessionMeta.firstUserMessage`). Returns '' on any error, an empty file, or
|
|
119
|
+
* a first record too large to fit `maxBytes` (bare — no elision fallback; see
|
|
120
|
+
* {@link readSessionHeadContentBounded} for that).
|
|
121
|
+
*/
|
|
122
|
+
export function readSessionHeadContent(filePath, maxBytes = DEFAULT_HEAD_MAX_BYTES) {
|
|
123
|
+
const chunk = readSessionHeadChunk(filePath, maxBytes);
|
|
124
|
+
if (!chunk)
|
|
125
|
+
return '';
|
|
126
|
+
let content = chunk.content;
|
|
127
|
+
if (!chunk.sawEof) {
|
|
128
|
+
// Didn't reach EOF: the last line in this chunk may be a partial write
|
|
129
|
+
// of a longer record. Keep only whole lines.
|
|
130
|
+
const lastNl = content.lastIndexOf('\n');
|
|
131
|
+
content = lastNl >= 0 ? content.slice(0, lastNl) : '';
|
|
132
|
+
}
|
|
133
|
+
return content;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Hard ceiling on how far {@link readSessionHeadContentBounded} will read
|
|
137
|
+
* looking for the opening record's closing newline. A JSONL record is one
|
|
138
|
+
* line, so a user turn that embeds a multi-megabyte image (base64, inline in
|
|
139
|
+
* the same `"data": "..."` string) can push that line's own terminator
|
|
140
|
+
* arbitrarily far past the plain {@link DEFAULT_HEAD_MAX_BYTES} chunk — this
|
|
141
|
+
* is the point past which the record is treated as unrecoverable (`partial`),
|
|
142
|
+
* never guessed at from a truncated fragment.
|
|
143
|
+
*/
|
|
144
|
+
const HEAD_ELISION_MAX_READ_BYTES = 4 * 1024 * 1024;
|
|
145
|
+
/** Keep at most this many UTF-16 units of any single JSON string value
|
|
146
|
+
* verbatim; the remainder is replaced with a short marker. Bounds the ELIDED
|
|
147
|
+
* OUTPUT size independent of how large the source string (an image's base64
|
|
148
|
+
* payload can be tens of megabytes) actually is, while staying far larger
|
|
149
|
+
* than any real first-turn text block ever needs to be. */
|
|
150
|
+
const HEAD_ELISION_STRING_KEEP_UNITS = 2048;
|
|
151
|
+
/** Wall-clock ceiling on the elision scan itself. The scan runs over data
|
|
152
|
+
* already bounded by {@link HEAD_ELISION_MAX_READ_BYTES} and does O(1) work
|
|
153
|
+
* per character, so this should never trip in practice — it exists as a
|
|
154
|
+
* defense-in-depth budget, not the primary bound. */
|
|
155
|
+
const HEAD_ELISION_MAX_MS = 200;
|
|
156
|
+
/** How many characters the elision scan advances between wall-clock checks —
|
|
157
|
+
* frequent enough that the time budget above is actually honored, infrequent
|
|
158
|
+
* enough that `Date.now()` isn't on the hot path of every character. */
|
|
159
|
+
const HEAD_ELISION_TIME_CHECK_MASK = 0x3ffff; // every ~262k chars
|
|
160
|
+
/**
|
|
161
|
+
* A single-pass, JSON-string-aware elision scan: copies `input` verbatim
|
|
162
|
+
* except inside a string literal whose content exceeds
|
|
163
|
+
* {@link HEAD_ELISION_STRING_KEEP_UNITS}, where it keeps a short prefix and
|
|
164
|
+
* splices in a `…[elided N chars]` marker for the rest — but still tracks
|
|
165
|
+
* escape/quote state through to that string's real closing quote, so
|
|
166
|
+
* surrounding JSON structure (a text block that follows an oversized image
|
|
167
|
+
* block in the same content array) stays syntactically valid. This is a
|
|
168
|
+
* generic JSON-string eliser, not an image-specific one: it does not care
|
|
169
|
+
* WHAT is inside the oversized string, only that it is too large to keep.
|
|
170
|
+
*
|
|
171
|
+
* `deadlineMs` is an absolute `Date.now()` value. `truncatedByBudget: true`
|
|
172
|
+
* means the scan did not finish (ran out of time) — critically, the output
|
|
173
|
+
* up to that point NEVER includes the unflushed tail of a string that had
|
|
174
|
+
* already crossed the elision threshold (the flush happens once, the moment
|
|
175
|
+
* the threshold is crossed, not at the closing quote), so a cutoff mid-scan
|
|
176
|
+
* can never smuggle megabytes of un-elided content into the result.
|
|
177
|
+
*/
|
|
178
|
+
function elideOversizedJsonStrings(input, deadlineMs) {
|
|
179
|
+
const outParts = [];
|
|
180
|
+
let spanStart = 0;
|
|
181
|
+
let inString = false;
|
|
182
|
+
let escaped = false;
|
|
183
|
+
let stringStart = 0;
|
|
184
|
+
let skipping = false; // current string already crossed the keep threshold
|
|
185
|
+
const n = input.length;
|
|
186
|
+
for (let i = 0; i < n; i++) {
|
|
187
|
+
if ((i & HEAD_ELISION_TIME_CHECK_MASK) === 0 && Date.now() > deadlineMs) {
|
|
188
|
+
if (!skipping)
|
|
189
|
+
outParts.push(input.slice(spanStart, i));
|
|
190
|
+
return { text: outParts.join(''), truncatedByBudget: true };
|
|
191
|
+
}
|
|
192
|
+
const ch = input.charCodeAt(i);
|
|
193
|
+
if (!inString) {
|
|
194
|
+
if (ch === 0x22 /* '"' */) {
|
|
195
|
+
inString = true;
|
|
196
|
+
stringStart = i + 1;
|
|
197
|
+
skipping = false;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (escaped) {
|
|
202
|
+
escaped = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === 0x5c /* '\\' */) {
|
|
206
|
+
escaped = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (ch === 0x22 /* '"' */) {
|
|
210
|
+
inString = false;
|
|
211
|
+
if (skipping) {
|
|
212
|
+
const elidedUnits = i - stringStart - HEAD_ELISION_STRING_KEEP_UNITS;
|
|
213
|
+
outParts.push(`…[elided ${elidedUnits} chars]`);
|
|
214
|
+
spanStart = i; // resume normal copying AT the closing quote
|
|
215
|
+
skipping = false; // the string is closed; the final flush must still emit the record's own tail ("}\n) even when this was the last string literal
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!skipping && (i - stringStart) === HEAD_ELISION_STRING_KEEP_UNITS) {
|
|
220
|
+
outParts.push(input.slice(spanStart, i)); // flush the kept prefix now
|
|
221
|
+
skipping = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!skipping)
|
|
225
|
+
outParts.push(input.slice(spanStart, n));
|
|
226
|
+
return { text: outParts.join(''), truncatedByBudget: false };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Recover the transcript's opening JSONL record even when it doesn't fit the
|
|
230
|
+
* plain {@link DEFAULT_HEAD_MAX_BYTES} chunk — the case a first turn carrying
|
|
231
|
+
* a multi-megabyte inline image produces, where the record's own closing
|
|
232
|
+
* newline sits past whatever small chunk was read, so the old bare head
|
|
233
|
+
* reader dropped the WHOLE record (and with it, the real original request)
|
|
234
|
+
* rather than the oversized value inside it.
|
|
235
|
+
*
|
|
236
|
+
* Only invoked when the cheap path already failed to find a complete line
|
|
237
|
+
* (bounded — see {@link readSessionHeadChunk}'s `sawEof`), so an ordinary
|
|
238
|
+
* transcript never pays this cost. Reads up to {@link HEAD_ELISION_MAX_READ_BYTES}
|
|
239
|
+
* from byte 0, runs it through {@link elideOversizedJsonStrings} (bounded by
|
|
240
|
+
* {@link HEAD_ELISION_MAX_MS}), and returns the first complete elided line —
|
|
241
|
+
* text before and after the oversized value is preserved verbatim (only the
|
|
242
|
+
* oversized value itself is shortened), UTF-8 decoding and escape handling
|
|
243
|
+
* both go through the exact same path an unelided record would. Returns ''
|
|
244
|
+
* (a partial result, not a guess) when the scan is cut off by either budget
|
|
245
|
+
* before a complete line was ever produced — never a followup/tail turn
|
|
246
|
+
* substituted for it.
|
|
247
|
+
*/
|
|
248
|
+
export function readSessionHeadContentBounded(filePath, maxReadBytes = HEAD_ELISION_MAX_READ_BYTES) {
|
|
249
|
+
const chunk = readSessionHeadChunk(filePath, maxReadBytes);
|
|
250
|
+
if (!chunk || !chunk.content)
|
|
251
|
+
return '';
|
|
252
|
+
const { text, truncatedByBudget } = elideOversizedJsonStrings(chunk.content, Date.now() + HEAD_ELISION_MAX_MS);
|
|
253
|
+
const firstNl = text.indexOf('\n');
|
|
254
|
+
if (firstNl >= 0)
|
|
255
|
+
return text.slice(0, firstNl);
|
|
256
|
+
// No line terminator found anywhere in the elided text: either the record
|
|
257
|
+
// (minus its oversized values) is STILL bigger than the read budget, or the
|
|
258
|
+
// elision scan itself hit its time budget first, or we reached real EOF
|
|
259
|
+
// with no trailing newline at all (a truncated/interrupted write).
|
|
260
|
+
if (truncatedByBudget)
|
|
261
|
+
return '';
|
|
262
|
+
return chunk.sawEof ? text : '';
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Read the FIRST `maxEvents` normalized events from a JSONL transcript's
|
|
266
|
+
* head — the session's actual opening turns, bounded and cheap (one small
|
|
267
|
+
* read from byte 0, the same shared Claude/Codex content parsers `readSessionTail`
|
|
268
|
+
* uses, zero duplicated parse logic). Only Claude and Codex are supported,
|
|
269
|
+
* matching {@link readSessionTailWithRaw}; other agents return no events.
|
|
270
|
+
*
|
|
271
|
+
* When the plain bounded chunk holds no complete first line at all — an
|
|
272
|
+
* opening user turn embedding a multi-megabyte inline image is the real case
|
|
273
|
+
* this covers — falls back to {@link readSessionHeadContentBounded}'s
|
|
274
|
+
* JSON-aware elision scan rather than returning nothing: the same content
|
|
275
|
+
* parsers then see a syntactically valid record with the oversized value
|
|
276
|
+
* shortened, so a text block before or after an elided image block is
|
|
277
|
+
* recovered exactly as if the image had never been oversized (the parser's
|
|
278
|
+
* own image-handling path, e.g. Claude's `normalizedAttachmentEvent`, still
|
|
279
|
+
* runs — it just sees a short placeholder `source.data` instead of the real
|
|
280
|
+
* payload, so an attachment's derived byte size is not meaningful when this
|
|
281
|
+
* fallback fired, only the surrounding TEXT is trustworthy).
|
|
282
|
+
*/
|
|
283
|
+
export function readSessionHead(filePath, agent, maxBytes = DEFAULT_HEAD_MAX_BYTES, maxEvents = DEFAULT_MAX_EVENTS) {
|
|
284
|
+
if (agent !== 'claude' && agent !== 'codex')
|
|
285
|
+
return [];
|
|
286
|
+
let content = readSessionHeadContent(filePath, maxBytes);
|
|
287
|
+
if (!content.trim())
|
|
288
|
+
content = readSessionHeadContentBounded(filePath);
|
|
289
|
+
if (!content.trim())
|
|
290
|
+
return [];
|
|
291
|
+
const events = agent === 'codex' ? parseCodexContent(content) : parseClaudeContent(content);
|
|
292
|
+
sanitizeEvents(events);
|
|
293
|
+
return events.length > maxEvents ? events.slice(0, maxEvents) : events;
|
|
294
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { installCli, resolveCliManifest } from './cli-resources.js';
|
|
2
|
+
import { getCachedToolSetup, refreshToolSetup } from './setup-tool-status.js';
|
|
3
|
+
const PACKAGES = {
|
|
4
|
+
browser: '@phnx-labs/browser-cli@0.1.5',
|
|
5
|
+
computer: '@phnx-labs/computer-cli@0.1.5',
|
|
6
|
+
term: '@phnx-labs/term-cli@0.1.0',
|
|
7
|
+
};
|
|
8
|
+
/** Use the host installer; setup must not install or start a tool from a read. */
|
|
9
|
+
export async function installSetupTool(tool) {
|
|
10
|
+
if (getCachedToolSetup().find((row) => row.tool === tool)?.installed)
|
|
11
|
+
return true;
|
|
12
|
+
const manifest = resolveCliManifest(tool) ?? {
|
|
13
|
+
name: tool,
|
|
14
|
+
check: { kind: 'which', cmd: tool },
|
|
15
|
+
install: [{ npm: PACKAGES[tool] }],
|
|
16
|
+
source: 'builtin',
|
|
17
|
+
path: '',
|
|
18
|
+
};
|
|
19
|
+
const result = installCli(manifest);
|
|
20
|
+
if (result.error)
|
|
21
|
+
console.error(result.error);
|
|
22
|
+
const rows = await refreshToolSetup(tool);
|
|
23
|
+
const installed = rows[0]?.installed === true;
|
|
24
|
+
if (!installed && !result.error)
|
|
25
|
+
console.error(`${tool} installation did not produce a standalone executable on PATH.`);
|
|
26
|
+
return result.installed && installed;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const SETUP_TOOLS: readonly ["browser", "computer", "secrets", "term"];
|
|
2
|
+
export type SetupTool = typeof SETUP_TOOLS[number];
|
|
3
|
+
export interface ToolSetupRow {
|
|
4
|
+
tool: SetupTool;
|
|
5
|
+
installed: boolean | null;
|
|
6
|
+
executable?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
readiness: 'ready' | 'needs-setup' | 'permission-required' | 'stopped' | 'unsupported' | 'unknown';
|
|
9
|
+
detail: string;
|
|
10
|
+
checkedAtMs: number | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ToolSetupOptions {
|
|
13
|
+
cacheDir?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function toolSetupCacheDir(options?: ToolSetupOptions): string;
|
|
16
|
+
/** Presence is metadata; health is the last explicit check, never a timer probe. */
|
|
17
|
+
export declare function getCachedToolSetup(options?: ToolSetupOptions): ToolSetupRow[];
|
|
18
|
+
export declare function toolReadiness(tool: SetupTool, status: unknown): Pick<ToolSetupRow, 'readiness' | 'detail'>;
|
|
19
|
+
/** A shared disk lock coalesces overlapping requests from separate CLI clients. */
|
|
20
|
+
export declare function refreshToolSetup(tool?: SetupTool | 'all', options?: ToolSetupOptions): Promise<ToolSetupRow[]>;
|
|
21
|
+
/** File notifications are shared by the daemon collector; no background probes. */
|
|
22
|
+
export declare function subscribeToolSetup(listener: (rows: ToolSetupRow[]) => void, options?: ToolSetupOptions): () => void;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { findInPath } from './agent-spec/agents.js';
|
|
4
|
+
import { getCacheDir, getDeviceMetaPath, getHelpersDir, getUserAgentsDir } from './state.js';
|
|
5
|
+
import { atomicWriteJsonSync, withFileLockAsync } from './fs-atomic.js';
|
|
6
|
+
import { probeCapture } from './probe.js';
|
|
7
|
+
import { invocation, isStandaloneComputer } from './computer-client.js';
|
|
8
|
+
import { getSocketPath as browserSocketPath } from './browser/ipc.js';
|
|
9
|
+
export const SETUP_TOOLS = ['browser', 'computer', 'secrets', 'term'];
|
|
10
|
+
export function toolSetupCacheDir(options = {}) {
|
|
11
|
+
return path.join(options.cacheDir ?? getCacheDir(), 'setup-tools');
|
|
12
|
+
}
|
|
13
|
+
function cachePath(tool, options) {
|
|
14
|
+
return path.join(toolSetupCacheDir(options), `${tool}.json`);
|
|
15
|
+
}
|
|
16
|
+
function setupInputs(tool) {
|
|
17
|
+
const inputs = [path.join(getUserAgentsDir(), 'agents.yaml'), getDeviceMetaPath()];
|
|
18
|
+
if (tool === 'browser')
|
|
19
|
+
inputs.push(browserSocketPath());
|
|
20
|
+
if (tool === 'computer')
|
|
21
|
+
inputs.push(process.env.COMPUTER_HELPER_SOCKET || path.join(getHelpersDir(), 'computer.sock'));
|
|
22
|
+
return inputs;
|
|
23
|
+
}
|
|
24
|
+
function inputStamp(tool) {
|
|
25
|
+
return setupInputs(tool).map((file) => {
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.statSync(file);
|
|
28
|
+
return `${file}:${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return `${file}:missing`;
|
|
32
|
+
}
|
|
33
|
+
}).join('|');
|
|
34
|
+
}
|
|
35
|
+
function binaryMetadata(tool) {
|
|
36
|
+
const empty = { tool, installed: false, readiness: 'needs-setup', detail: 'CLI is not installed.', checkedAtMs: null };
|
|
37
|
+
try {
|
|
38
|
+
const explicit = process.env[`${tool.toUpperCase()}_BIN`]?.trim();
|
|
39
|
+
const accept = (candidate) => {
|
|
40
|
+
if (tool === 'computer')
|
|
41
|
+
return isStandaloneComputer(candidate);
|
|
42
|
+
if (tool === 'browser') {
|
|
43
|
+
try {
|
|
44
|
+
return !fs.realpathSync(candidate).endsWith(path.join('dist', 'browser.js'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
let executable = explicit ? (accept(explicit) ? explicit : null) : findInPath(tool, { accept });
|
|
53
|
+
if (!executable)
|
|
54
|
+
return { row: empty, fingerprint: 'missing' };
|
|
55
|
+
if (/\.(cmd|ps1)$/i.test(executable)) {
|
|
56
|
+
const packageDir = path.join(path.dirname(executable), 'node_modules', '@phnx-labs', `${tool}-cli`);
|
|
57
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
58
|
+
const entry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[tool];
|
|
59
|
+
if (pkg.name !== `@phnx-labs/${tool}-cli` || typeof entry !== 'string')
|
|
60
|
+
throw new Error('unrecognized npm launcher');
|
|
61
|
+
const resolved = path.resolve(packageDir, entry);
|
|
62
|
+
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`))
|
|
63
|
+
throw new Error('invalid npm entrypoint');
|
|
64
|
+
executable = resolved;
|
|
65
|
+
}
|
|
66
|
+
fs.accessSync(executable, process.platform === 'win32' || /\.[cm]?js$/i.test(executable) ? fs.constants.R_OK : fs.constants.X_OK);
|
|
67
|
+
const real = fs.realpathSync(executable);
|
|
68
|
+
const stat = fs.statSync(real);
|
|
69
|
+
if (!stat.isFile())
|
|
70
|
+
throw new Error('not a file');
|
|
71
|
+
let version;
|
|
72
|
+
let dir = path.dirname(real);
|
|
73
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
74
|
+
try {
|
|
75
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
76
|
+
if (pkg.name === `@phnx-labs/${tool}-cli` && typeof pkg.version === 'string') {
|
|
77
|
+
version = pkg.version;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { /* Executables need not be npm packages. */ }
|
|
82
|
+
const parent = path.dirname(dir);
|
|
83
|
+
if (parent === dir)
|
|
84
|
+
break;
|
|
85
|
+
dir = parent;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
fingerprint: `${real}:${stat.size}:${stat.mtimeMs}:${version ?? ''}|${inputStamp(tool)}`,
|
|
89
|
+
row: { tool, installed: true, executable, version, readiness: 'unknown', detail: 'Installed. Health has not been checked.', checkedAtMs: null },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { fingerprint: 'unavailable', row: { ...empty, installed: null, readiness: 'unknown', detail: 'Executable could not be inspected.' } };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function readCache(tool, options) {
|
|
97
|
+
try {
|
|
98
|
+
const cached = JSON.parse(fs.readFileSync(cachePath(tool, options), 'utf8'));
|
|
99
|
+
if (cached.v === 1 && cached.row?.tool === tool && typeof cached.fingerprint === 'string')
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
|
102
|
+
catch { /* No health result yet. */ }
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** Presence is metadata; health is the last explicit check, never a timer probe. */
|
|
106
|
+
export function getCachedToolSetup(options = {}) {
|
|
107
|
+
return SETUP_TOOLS.map((tool) => {
|
|
108
|
+
const { row, fingerprint } = binaryMetadata(tool);
|
|
109
|
+
const cached = readCache(tool, options);
|
|
110
|
+
return cached?.fingerprint === fingerprint ? { ...cached.row, executable: row.executable, version: row.version } : row;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function toolReadiness(tool, status) {
|
|
114
|
+
if (!status || typeof status !== 'object')
|
|
115
|
+
return { readiness: 'unknown', detail: 'The CLI returned an unreadable health result.' };
|
|
116
|
+
const value = status;
|
|
117
|
+
if (tool === 'computer') {
|
|
118
|
+
if (value.installed === false)
|
|
119
|
+
return { readiness: 'needs-setup', detail: 'CLI installed; computer helper needs setup.' };
|
|
120
|
+
if (value.running === true && value.trusted === true)
|
|
121
|
+
return { readiness: 'ready', detail: 'Helper is running and Accessibility is granted. Screen Recording is checked when capturing.' };
|
|
122
|
+
if (value.running === true && value.trusted === false)
|
|
123
|
+
return { readiness: 'permission-required', detail: 'Grant Accessibility to Agents Computer in System Settings.' };
|
|
124
|
+
if (value.running === false)
|
|
125
|
+
return { readiness: 'stopped', detail: 'Computer helper is stopped or unreachable.' };
|
|
126
|
+
}
|
|
127
|
+
if (tool === 'browser') {
|
|
128
|
+
if (value.running === true)
|
|
129
|
+
return { readiness: 'ready', detail: 'Browser service is running.' };
|
|
130
|
+
if (value.running === false)
|
|
131
|
+
return { readiness: 'stopped', detail: 'Browser service is stopped.' };
|
|
132
|
+
}
|
|
133
|
+
return { readiness: 'unknown', detail: typeof value.error === 'string' && value.error.trim() ? value.error.slice(0, 500) : 'The CLI did not report a recognized health state.' };
|
|
134
|
+
}
|
|
135
|
+
async function checkTool(row) {
|
|
136
|
+
if (!row.installed || !row.executable)
|
|
137
|
+
return { ...row, checkedAtMs: Date.now() };
|
|
138
|
+
// The standalone has no non-interactive health JSON. Do not list bundles or
|
|
139
|
+
// unlock the broker merely to paint a settings row.
|
|
140
|
+
if (row.tool === 'secrets')
|
|
141
|
+
return { ...row, checkedAtMs: Date.now(), detail: 'Installed. Secret access is checked when used; this check does not unlock secrets.' };
|
|
142
|
+
// term is a headless PTY engine with no `status --json` health surface; it is
|
|
143
|
+
// spawned on demand by the OAuth device-code driver (fleet login / auth mint).
|
|
144
|
+
// Presence on PATH is the whole readiness signal — do not probe it.
|
|
145
|
+
if (row.tool === 'term')
|
|
146
|
+
return { ...row, readiness: 'ready', detail: 'Installed. Spawned on demand by fleet login and auth mint.', checkedAtMs: Date.now() };
|
|
147
|
+
try {
|
|
148
|
+
const { command, prefix } = invocation(row.executable);
|
|
149
|
+
const { stdout } = await probeCapture(command, [...prefix, 'status', '--json'], 8000, { acceptedExitCodes: [0, 1], maxOutputBytes: 256 * 1024 });
|
|
150
|
+
return { ...row, ...toolReadiness(row.tool, JSON.parse(stdout)), checkedAtMs: Date.now() };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { ...row, readiness: 'unknown', detail: 'Health check failed or timed out. Check again to retry.', checkedAtMs: Date.now() };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** A shared disk lock coalesces overlapping requests from separate CLI clients. */
|
|
157
|
+
export async function refreshToolSetup(tool = 'all', options = {}) {
|
|
158
|
+
const requestedAt = Date.now();
|
|
159
|
+
const dir = toolSetupCacheDir(options);
|
|
160
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
161
|
+
for (const selected of tool === 'all' ? SETUP_TOOLS : [tool]) {
|
|
162
|
+
const file = cachePath(selected, options);
|
|
163
|
+
await withFileLockAsync(file, async () => {
|
|
164
|
+
const metadata = binaryMetadata(selected);
|
|
165
|
+
const cached = readCache(selected, options);
|
|
166
|
+
if (cached?.fingerprint === metadata.fingerprint && (cached.row.checkedAtMs ?? 0) >= requestedAt)
|
|
167
|
+
return;
|
|
168
|
+
const row = await checkTool(metadata.row);
|
|
169
|
+
atomicWriteJsonSync(file, { v: 1, fingerprint: metadata.fingerprint, row });
|
|
170
|
+
}, { realpath: false, acquireTimeoutMs: 20_000 });
|
|
171
|
+
}
|
|
172
|
+
return getCachedToolSetup(options).filter((row) => tool === 'all' || row.tool === tool);
|
|
173
|
+
}
|
|
174
|
+
/** File notifications are shared by the daemon collector; no background probes. */
|
|
175
|
+
export function subscribeToolSetup(listener, options = {}) {
|
|
176
|
+
const dir = toolSetupCacheDir(options);
|
|
177
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
178
|
+
const paths = new Set([dir, ...(process.env.PATH ?? '').split(path.delimiter).filter(Boolean)]);
|
|
179
|
+
for (const tool of SETUP_TOOLS)
|
|
180
|
+
for (const input of setupInputs(tool))
|
|
181
|
+
paths.add(path.dirname(input));
|
|
182
|
+
for (const row of getCachedToolSetup(options)) {
|
|
183
|
+
if (row.executable) {
|
|
184
|
+
paths.add(path.dirname(row.executable));
|
|
185
|
+
try {
|
|
186
|
+
paths.add(path.dirname(fs.realpathSync(row.executable)));
|
|
187
|
+
}
|
|
188
|
+
catch { /* Removed since discovery. */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let previous = JSON.stringify(getCachedToolSetup(options));
|
|
192
|
+
let debounce;
|
|
193
|
+
const changed = () => {
|
|
194
|
+
if (debounce)
|
|
195
|
+
return;
|
|
196
|
+
debounce = setTimeout(() => {
|
|
197
|
+
debounce = undefined;
|
|
198
|
+
const rows = getCachedToolSetup(options);
|
|
199
|
+
const next = JSON.stringify(rows);
|
|
200
|
+
if (next !== previous) {
|
|
201
|
+
previous = next;
|
|
202
|
+
listener(rows);
|
|
203
|
+
}
|
|
204
|
+
}, 150);
|
|
205
|
+
debounce.unref();
|
|
206
|
+
};
|
|
207
|
+
const watchers = [];
|
|
208
|
+
for (const target of paths) {
|
|
209
|
+
try {
|
|
210
|
+
const watcher = fs.watch(target, { persistent: false }, changed);
|
|
211
|
+
watcher.on('error', changed);
|
|
212
|
+
watchers.push(watcher);
|
|
213
|
+
}
|
|
214
|
+
catch { /* Absent PATH entries have no executable to report. */ }
|
|
215
|
+
}
|
|
216
|
+
changed();
|
|
217
|
+
return () => { if (debounce)
|
|
218
|
+
clearTimeout(debounce); for (const watcher of watchers)
|
|
219
|
+
watcher.close(); };
|
|
220
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Best-effort runtime env for spawned agents. Never throws AND never prompts.
|
|
2
|
+
*
|
|
3
|
+
* Auto-injecting the share write token on every `agents run` is a background
|
|
4
|
+
* convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
|
|
5
|
+
* ID sheet (SEC-13: an agent launch never pops biometry on its own). The read is
|
|
6
|
+
* always `agentOnly`: it resolves the token only from the injected env or an
|
|
7
|
+
* already-held / no-ACL bundle, and silently returns undefined otherwise (the
|
|
8
|
+
* agent can still publish via its own `artifacts share` credentials). For
|
|
9
|
+
* zero-friction auto-share with no prompt: unlock once (`agents secrets unlock
|
|
10
|
+
* share`) or make it no-ACL (`agents secrets policy share never`). */
|
|
11
|
+
export declare function shareRuntimeEnv(): Record<string, string> | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Runtime share-token injection for spawned agents.
|
|
2
|
+
//
|
|
3
|
+
// The artifact share ENGINE moved to `@phnx-labs/artifacts-cli` (PHNX-3992);
|
|
4
|
+
// agents-cli no longer publishes artifacts itself. What stays here is the thin
|
|
5
|
+
// glue that lets a dispatched agent inherit the operator's BYO write token so
|
|
6
|
+
// its own `artifacts share` can publish headlessly: `run`/`teams`/`cloud`
|
|
7
|
+
// dispatch call `shareRuntimeEnv()` and forward the result into the agent env.
|
|
8
|
+
// artifacts-cli honors the same `SHARE_WRITE_TOKEN` variable, so the injection
|
|
9
|
+
// contract is unchanged by the extraction.
|
|
10
|
+
//
|
|
11
|
+
// The endpoint config still lives in `agents.yaml` under `share:` (Meta.share)
|
|
12
|
+
// so a previously-provisioned BYO endpoint keeps syncing fleet-wide; the raw
|
|
13
|
+
// write token lives in the keychain-backed `share` secrets bundle, never on disk.
|
|
14
|
+
import { readMeta } from './state.js';
|
|
15
|
+
import { bundleExistsSync as bundleExists, readAndResolveBundleEnvSync as readAndResolveBundleEnv, } from './secrets-client.js';
|
|
16
|
+
const SHARE_BUNDLE = 'share';
|
|
17
|
+
const SHARE_TOKEN_KEY = 'WRITE_TOKEN';
|
|
18
|
+
const SHARE_TOKEN_ENV_KEY = 'SHARE_WRITE_TOKEN';
|
|
19
|
+
/** Trim; empty / whitespace-only strings are absent. */
|
|
20
|
+
function nonempty(v) {
|
|
21
|
+
const t = v?.trim();
|
|
22
|
+
return t ? t : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Whether a BYO share endpoint is still configured in `agents.yaml`. */
|
|
25
|
+
function hasShareEndpoint() {
|
|
26
|
+
return !!nonempty(readMeta().share?.baseUrl);
|
|
27
|
+
}
|
|
28
|
+
/** The write token as injected ephemerally into fleet/cloud agents. */
|
|
29
|
+
function readWriteTokenEnv(env = process.env) {
|
|
30
|
+
const token = env[SHARE_TOKEN_ENV_KEY]?.trim();
|
|
31
|
+
return token ? token : null;
|
|
32
|
+
}
|
|
33
|
+
/** Best-effort runtime env for spawned agents. Never throws AND never prompts.
|
|
34
|
+
*
|
|
35
|
+
* Auto-injecting the share write token on every `agents run` is a background
|
|
36
|
+
* convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
|
|
37
|
+
* ID sheet (SEC-13: an agent launch never pops biometry on its own). The read is
|
|
38
|
+
* always `agentOnly`: it resolves the token only from the injected env or an
|
|
39
|
+
* already-held / no-ACL bundle, and silently returns undefined otherwise (the
|
|
40
|
+
* agent can still publish via its own `artifacts share` credentials). For
|
|
41
|
+
* zero-friction auto-share with no prompt: unlock once (`agents secrets unlock
|
|
42
|
+
* share`) or make it no-ACL (`agents secrets policy share never`). */
|
|
43
|
+
export function shareRuntimeEnv() {
|
|
44
|
+
if (!hasShareEndpoint())
|
|
45
|
+
return undefined;
|
|
46
|
+
const fromEnv = readWriteTokenEnv();
|
|
47
|
+
if (fromEnv)
|
|
48
|
+
return { [SHARE_TOKEN_ENV_KEY]: fromEnv };
|
|
49
|
+
try {
|
|
50
|
+
if (!bundleExists(SHARE_BUNDLE))
|
|
51
|
+
return undefined;
|
|
52
|
+
const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
|
|
53
|
+
caller: 'share',
|
|
54
|
+
keys: [SHARE_TOKEN_KEY],
|
|
55
|
+
agentOnly: true, // never raise a Touch ID sheet on an agent launch (SEC-13)
|
|
56
|
+
});
|
|
57
|
+
const token = env[SHARE_TOKEN_KEY];
|
|
58
|
+
return token ? { [SHARE_TOKEN_ENV_KEY]: token } : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -6,15 +6,30 @@ export type AccountProvisioning = 'portable' | 'per-device';
|
|
|
6
6
|
/**
|
|
7
7
|
* The exact command that logs a given agent in — for warn banners and nudges.
|
|
8
8
|
* Driven off the registry `cliCommand` with the per-agent subcommand overrides
|
|
9
|
-
* (verified against the real CLIs): codex/grok
|
|
10
|
-
*
|
|
11
|
-
* remaining agents (kimi, gemini, …) start
|
|
9
|
+
* (verified against the real CLIs): codex/grok/opencode run the finite login
|
|
10
|
+
* subcommand `HARNESS_AUTH` wires (`loginSubcommand`), claude logs in from
|
|
11
|
+
* inside its TUI via `/login`, and the remaining agents (kimi, gemini, …) start
|
|
12
|
+
* their device/oauth flow on launch.
|
|
12
13
|
*/
|
|
13
14
|
export declare function loginHint(agentId: AgentId): string;
|
|
15
|
+
/**
|
|
16
|
+
* Harnesses that log back in through a finite native subcommand runnable via
|
|
17
|
+
* `agents run <agent>@<version> -- <args>`. Claude logs in from inside its TUI
|
|
18
|
+
* (`/login`); cursor and the rest start their device/oauth flow on launch.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SUBCOMMAND_LOGIN_AGENTS: readonly AgentId[];
|
|
21
|
+
/**
|
|
22
|
+
* The finite native login subcommand (`login`, `login --device-auth`,
|
|
23
|
+
* `auth login`) for a {@link SUBCOMMAND_LOGIN_AGENTS} harness, read from the one
|
|
24
|
+
* `HARNESS_AUTH` row so every surface that spells it — the hint, the per-version
|
|
25
|
+
* fix, `agents doctor` — agrees. Null for every other harness.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loginSubcommand(agent: AgentId): string | null;
|
|
14
28
|
/**
|
|
15
29
|
* Exact action shown beside a non-live account. Every emitted command exists
|
|
16
30
|
* today — never a planned surface and never a hidden verb:
|
|
17
|
-
* - Per-device harnesses repair
|
|
31
|
+
* - Per-device harnesses (kimi/antigravity) repair on the box itself: run the
|
|
32
|
+
* harness there (`loginHint`) and complete its native login.
|
|
18
33
|
* - Named accounts re-auth through `agents accounts login <harness>#<name>`.
|
|
19
34
|
* A known account with no slot on a headed device is onboarded with
|
|
20
35
|
* `agents accounts add <harness> <name>`. A worker never runs an
|