@phnx-labs/agents-cli 1.22.107 → 1.22.109
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 +52 -0
- package/dist/commands/daemon.js +45 -40
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/exec.js +4 -2
- package/dist/commands/monitors.js +15 -2
- package/dist/commands/routines.js +46 -6
- package/dist/commands/sessions-backup-setup.d.ts +12 -0
- package/dist/commands/sessions-backup-setup.js +65 -0
- package/dist/commands/sessions-resume.d.ts +14 -1
- package/dist/commands/sessions-resume.js +56 -9
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/share.js +15 -2
- package/dist/commands/sync.js +30 -1
- package/dist/lib/accounts/slots.js +7 -0
- package/dist/lib/agent-spec/agents.d.ts +60 -8
- package/dist/lib/agent-spec/agents.js +118 -45
- package/dist/lib/daemon/daemon.d.ts +34 -1
- package/dist/lib/daemon/daemon.js +63 -9
- package/dist/lib/daemon/leaked-daemons.d.ts +60 -0
- package/dist/lib/daemon/leaked-daemons.js +180 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +32 -1
- package/dist/lib/hooks/install.js +70 -63
- package/dist/lib/models.js +76 -5
- package/dist/lib/session/cloud.js +3 -1
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +136 -2
- package/dist/lib/session/recovery.d.ts +9 -1
- package/dist/lib/session/recovery.js +14 -4
- package/dist/lib/session/tool-calls.d.ts +1 -1
- package/dist/lib/session/tool-calls.js +40 -5
- package/dist/lib/share/backend.d.ts +23 -0
- package/dist/lib/share/backend.js +24 -0
- package/dist/lib/share/provision.d.ts +12 -0
- package/dist/lib/share/provision.js +30 -0
- package/dist/lib/share/worker-template.js +273 -0
- package/dist/lib/terminal/engine.js +13 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import Database from '../sqlite.js';
|
|
13
13
|
import { isSyntheticUserMessage, extractSlashCommandName, extractSlashCommandFromToolInput, unwrapUserQuery } from './prompt.js';
|
|
14
14
|
import { structuredToolResult, commandsFromCodexExec } from './tool-calls.js';
|
|
15
|
+
import { isCloudSessionPath } from './cloud.js';
|
|
15
16
|
/**
|
|
16
17
|
* Largest session file we will load into memory. Above this we throw a clean
|
|
17
18
|
* error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
|
|
@@ -143,7 +144,11 @@ const TRANSCRIPT_PARSERS = {
|
|
|
143
144
|
codex: (filePath) => parseCodex(filePath),
|
|
144
145
|
gemini: (filePath) => parseGemini(filePath),
|
|
145
146
|
antigravity: (filePath) => parseAntigravity(filePath),
|
|
146
|
-
opencode
|
|
147
|
+
// Cloud-captured opencode sessions are normalized JSONL (produced by the
|
|
148
|
+
// factory at capture time), NOT the local `opencode.db#<session>` SQLite
|
|
149
|
+
// composite the offline parser reads — route by whether the path is in the
|
|
150
|
+
// cloud session cache (PHNX-3845).
|
|
151
|
+
opencode: (filePath) => isCloudSessionPath(filePath) ? parseOpencodeCloud(filePath) : parseOpenCode(filePath),
|
|
147
152
|
grok: (filePath) => parseGrok(filePath),
|
|
148
153
|
rush: (filePath) => parseRush(filePath),
|
|
149
154
|
openclaw: () => [], // OpenClaw sessions don't have parseable files yet
|
|
@@ -206,7 +211,7 @@ export function detectAgent(filePath) {
|
|
|
206
211
|
return 'muse';
|
|
207
212
|
}
|
|
208
213
|
// Cloud convention: cloud-sessions/<id>/session.<format>.jsonl
|
|
209
|
-
const cloudMatch = filePath.match(/session\.(claude|codex|rush)\.jsonl(?:$|[?#])/);
|
|
214
|
+
const cloudMatch = filePath.match(/session\.(claude|codex|rush|opencode)\.jsonl(?:$|[?#])/);
|
|
210
215
|
if (cloudMatch)
|
|
211
216
|
return cloudMatch[1];
|
|
212
217
|
if (filePath.includes('opencode.db'))
|
|
@@ -1832,6 +1837,135 @@ export function parseOpenCode(filePath) {
|
|
|
1832
1837
|
return events;
|
|
1833
1838
|
}
|
|
1834
1839
|
// ---------------------------------------------------------------------------
|
|
1840
|
+
// OpenCode parser — cloud (normalized JSONL)
|
|
1841
|
+
// ---------------------------------------------------------------------------
|
|
1842
|
+
//
|
|
1843
|
+
// OpenCode stores sessions in a SQLite DB, so the offline path reads a
|
|
1844
|
+
// `opencode.db#<session>` composite (parseOpenCode above). The CLOUD path is
|
|
1845
|
+
// different: the factory converts that DB into a flat, normalized JSONL
|
|
1846
|
+
// transcript at capture time (prix/factory opencode-capture.ts, PHNX-3845), so
|
|
1847
|
+
// the whole cloud pipeline stays SQLite-free downstream. This parser reads that
|
|
1848
|
+
// JSONL; it's routed from TRANSCRIPT_PARSERS only when the file lives in the
|
|
1849
|
+
// cloud session cache (isCloudSessionPath).
|
|
1850
|
+
//
|
|
1851
|
+
// Each line is one of:
|
|
1852
|
+
// transcript row:
|
|
1853
|
+
// { role: "user"|"assistant", part_type: "text"|"reasoning"|"tool",
|
|
1854
|
+
// part_data: <json string>, time_created: <ms number> }
|
|
1855
|
+
// todo snapshot (at most one, emitted last):
|
|
1856
|
+
// { part_type: "todo", todos: [{content,status}], time_created: <ms> }
|
|
1857
|
+
//
|
|
1858
|
+
// The per-part logic mirrors parseOpenCode's post-query switch (and prix/api's
|
|
1859
|
+
// server-side parseOpencode) so the event stream is identical to the offline
|
|
1860
|
+
// path. NOTE: the shell tool is named `bash` on real opencode (>=1.18.x), not
|
|
1861
|
+
// `shell` — map `command` for both so a shell step reads by its command line.
|
|
1862
|
+
export function parseOpencodeCloud(filePath) {
|
|
1863
|
+
const content = safeReadSessionFile(filePath);
|
|
1864
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
1865
|
+
const events = [];
|
|
1866
|
+
for (const line of lines) {
|
|
1867
|
+
let raw;
|
|
1868
|
+
try {
|
|
1869
|
+
raw = JSON.parse(line);
|
|
1870
|
+
}
|
|
1871
|
+
catch {
|
|
1872
|
+
/* malformed JSONL line, skip */
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const partType = typeof raw.part_type === 'string' ? raw.part_type : '';
|
|
1876
|
+
const timeMs = typeof raw.time_created === 'number'
|
|
1877
|
+
? raw.time_created
|
|
1878
|
+
: parseInt(String(raw.time_created), 10);
|
|
1879
|
+
const timestamp = Number.isFinite(timeMs)
|
|
1880
|
+
? new Date(timeMs).toISOString()
|
|
1881
|
+
: new Date().toISOString();
|
|
1882
|
+
// Todo snapshot: emit one `todo_write` tool_use so the shared enrichment
|
|
1883
|
+
// (`extractTodoProgressFromEvents`) derives `todos` the same way it does for
|
|
1884
|
+
// every other harness — the offline parser emits the identical event.
|
|
1885
|
+
if (partType === 'todo') {
|
|
1886
|
+
const todos = Array.isArray(raw.todos) ? raw.todos : [];
|
|
1887
|
+
if (todos.length) {
|
|
1888
|
+
events.push({
|
|
1889
|
+
type: 'tool_use',
|
|
1890
|
+
agent: 'opencode',
|
|
1891
|
+
timestamp,
|
|
1892
|
+
tool: 'todo_write',
|
|
1893
|
+
args: { todos },
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
let partData;
|
|
1899
|
+
try {
|
|
1900
|
+
partData = JSON.parse(typeof raw.part_data === 'string' ? raw.part_data : '');
|
|
1901
|
+
}
|
|
1902
|
+
catch {
|
|
1903
|
+
/* malformed part data, skip */
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
switch (partType) {
|
|
1907
|
+
case 'text': {
|
|
1908
|
+
const text = (partData.text || '').trim();
|
|
1909
|
+
if (text) {
|
|
1910
|
+
events.push({
|
|
1911
|
+
type: 'message',
|
|
1912
|
+
agent: 'opencode',
|
|
1913
|
+
timestamp,
|
|
1914
|
+
role: raw.role === 'user' ? 'user' : 'assistant',
|
|
1915
|
+
content: text,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
case 'reasoning': {
|
|
1921
|
+
const text = (partData.text || '').trim();
|
|
1922
|
+
if (text) {
|
|
1923
|
+
events.push({
|
|
1924
|
+
type: 'thinking',
|
|
1925
|
+
agent: 'opencode',
|
|
1926
|
+
timestamp,
|
|
1927
|
+
content: text,
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
break;
|
|
1931
|
+
}
|
|
1932
|
+
case 'tool': {
|
|
1933
|
+
const toolName = partData.tool || 'unknown';
|
|
1934
|
+
const state = partData.state || {};
|
|
1935
|
+
const input = state.input || {};
|
|
1936
|
+
const output = state.output || '';
|
|
1937
|
+
const callId = typeof partData.callID === 'string' ? partData.callID : undefined;
|
|
1938
|
+
events.push({
|
|
1939
|
+
type: 'tool_use',
|
|
1940
|
+
agent: 'opencode',
|
|
1941
|
+
timestamp,
|
|
1942
|
+
tool: toolName,
|
|
1943
|
+
callId,
|
|
1944
|
+
args: input,
|
|
1945
|
+
command: toolName === 'bash' || toolName === 'shell' ? input.command : undefined,
|
|
1946
|
+
path: input.filePath || input.path || undefined,
|
|
1947
|
+
});
|
|
1948
|
+
if (state.status === 'completed' || state.status === 'error') {
|
|
1949
|
+
const outputStr = typeof output === 'string' ? output : JSON.stringify(output);
|
|
1950
|
+
events.push({
|
|
1951
|
+
type: state.status === 'error' ? 'error' : 'tool_result',
|
|
1952
|
+
agent: 'opencode',
|
|
1953
|
+
timestamp,
|
|
1954
|
+
tool: toolName,
|
|
1955
|
+
callId,
|
|
1956
|
+
success: state.status === 'completed',
|
|
1957
|
+
output: outputStr,
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
// step-start / step-finish / patch / file never reach the JSONL (dropped
|
|
1963
|
+
// at capture time) — nothing to handle here.
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return events;
|
|
1967
|
+
}
|
|
1968
|
+
// ---------------------------------------------------------------------------
|
|
1835
1969
|
// Rush parser
|
|
1836
1970
|
//
|
|
1837
1971
|
// Rush messages.jsonl format is flat: one JSON object per line with
|
|
@@ -68,7 +68,15 @@ export declare function sessionMatchesAccount(session: Pick<SessionMeta, 'agent'
|
|
|
68
68
|
export declare function inspectNativeResumeSession(session: SessionMeta, versionHome: string): NativeResumeInspection;
|
|
69
69
|
/** Keep the native conversation where possible; replay remains a separate choice. */
|
|
70
70
|
export declare function resolveSessionRecoveryFromCandidates(session: SessionMeta, candidates: RotateCandidate[], supportsNative?: (agent: AgentId, version?: string) => boolean, nativeInspection?: NativeResumeInspection, options?: SessionRecoverySelection): SessionRecoveryTarget;
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Whether recovery has conversation content to replay for this session: a
|
|
73
|
+
* non-empty transcript file, or archived content in the index for a row this
|
|
74
|
+
* device owns. A mirror digest or live registry entry is not conversation
|
|
75
|
+
* content. The picker consults this before spending a terminal tab on a pick
|
|
76
|
+
* that {@link assertRecoverableTranscript} would refuse one hop later.
|
|
77
|
+
*/
|
|
78
|
+
export declare function sessionTranscriptReadable(session: SessionMeta): boolean;
|
|
79
|
+
/** Refuse recovery for a session with nothing to replay (PHNX-4080). */
|
|
72
80
|
export declare function assertRecoverableTranscript(session: SessionMeta): void;
|
|
73
81
|
/**
|
|
74
82
|
* Resolve recovery for a durable session, reading the live account pool.
|
|
@@ -365,16 +365,26 @@ export function resolveSessionRecoveryFromCandidates(session, candidates, suppor
|
|
|
365
365
|
reason: `${sourceReason(session, source, options.model ?? session.model)}; continuing with ${continueWith}`,
|
|
366
366
|
};
|
|
367
367
|
}
|
|
368
|
-
/**
|
|
369
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Whether recovery has conversation content to replay for this session: a
|
|
370
|
+
* non-empty transcript file, or archived content in the index for a row this
|
|
371
|
+
* device owns. A mirror digest or live registry entry is not conversation
|
|
372
|
+
* content. The picker consults this before spending a terminal tab on a pick
|
|
373
|
+
* that {@link assertRecoverableTranscript} would refuse one hop later.
|
|
374
|
+
*/
|
|
375
|
+
export function sessionTranscriptReadable(session) {
|
|
370
376
|
const file = splitSessionFilePath(session.filePath).container;
|
|
371
377
|
try {
|
|
372
378
|
if (file && fs.statSync(file).isFile() && fs.statSync(file).size > 0
|
|
373
379
|
&& (session.agent !== 'opencode' || parseOpenCode(session.filePath).length > 0))
|
|
374
|
-
return;
|
|
380
|
+
return true;
|
|
375
381
|
}
|
|
376
382
|
catch { /* The canonical scan already tried to repair this path. */ }
|
|
377
|
-
|
|
383
|
+
return !session.mirrorSyncedAt && !session.mirrorSource && Boolean(readSessionContent(session.id)?.trim());
|
|
384
|
+
}
|
|
385
|
+
/** Refuse recovery for a session with nothing to replay (PHNX-4080). */
|
|
386
|
+
export function assertRecoverableTranscript(session) {
|
|
387
|
+
if (sessionTranscriptReadable(session))
|
|
378
388
|
return;
|
|
379
389
|
throw new SessionRecoveryError(`Session ${session.shortId} has no readable transcript after checking its account homes and index. ` +
|
|
380
390
|
`No agent was started. Start a new conversation with: agents run ${session.agent}`);
|
|
@@ -10,7 +10,7 @@ export declare const TOOL_CHANGED_MAX_CALLS = 10000;
|
|
|
10
10
|
export declare const TOOL_INDEX_LIMIT_ORDINAL: number;
|
|
11
11
|
export declare const TOOL_TEXT_PROCESSING_MAX_BYTES: number;
|
|
12
12
|
export declare const TOOL_SHELL_PARSE_MAX_BYTES: number;
|
|
13
|
-
export declare const TOOL_INDEX_VERSION =
|
|
13
|
+
export declare const TOOL_INDEX_VERSION = 9;
|
|
14
14
|
export type ToolCallOutcome = 'ok' | 'error' | 'unknown';
|
|
15
15
|
export interface IndexedToolCall {
|
|
16
16
|
ordinal: number;
|
|
@@ -12,10 +12,12 @@ export const TOOL_CHANGED_MAX_CALLS = 10_000;
|
|
|
12
12
|
export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
|
|
13
13
|
export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
|
|
14
14
|
export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
|
|
15
|
-
// Bumped to
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
15
|
+
// Bumped to 9 for Codex tool-outcome classification (PHNX-3761): Codex results
|
|
16
|
+
// arrive as a string or input_text[] rather than an object, so every one used to
|
|
17
|
+
// store as 'unknown' and error rates read 0%. A re-index re-derives outcome for
|
|
18
|
+
// rows stored by an older extractor.
|
|
19
|
+
// (8 added the per-call end timestamp, PHNX-3437.)
|
|
20
|
+
export const TOOL_INDEX_VERSION = 9;
|
|
19
21
|
const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
|
|
20
22
|
const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
|
|
21
23
|
const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
|
|
@@ -258,9 +260,37 @@ function structuredString(source, name) {
|
|
|
258
260
|
const value = structuredField(source, name);
|
|
259
261
|
return typeof value === 'string' ? value : undefined;
|
|
260
262
|
}
|
|
263
|
+
// Codex tool results arrive as a plain string or an array of {type:'input_text',
|
|
264
|
+
// text} blocks (never a plain object), so the text lives across the blocks and
|
|
265
|
+
// the only outcome signal is the leading "Script completed"/"Script failed"
|
|
266
|
+
// marker the exec harness prefixes onto the first block.
|
|
267
|
+
function textSegments(value) {
|
|
268
|
+
if (typeof value === 'string')
|
|
269
|
+
return [value];
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
return value
|
|
272
|
+
.map((block) => block && typeof block === 'object' && typeof block.text === 'string'
|
|
273
|
+
? block.text
|
|
274
|
+
: '')
|
|
275
|
+
.filter((text) => text.length > 0);
|
|
276
|
+
}
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
function codexScriptOutcome(value) {
|
|
280
|
+
const lead = textSegments(value)[0]?.trimStart();
|
|
281
|
+
if (!lead)
|
|
282
|
+
return undefined;
|
|
283
|
+
if (lead.startsWith('Script failed'))
|
|
284
|
+
return 'error';
|
|
285
|
+
if (lead.startsWith('Script completed'))
|
|
286
|
+
return 'ok';
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
261
289
|
function resultText(value) {
|
|
262
290
|
if (typeof value === 'string')
|
|
263
291
|
return value;
|
|
292
|
+
if (Array.isArray(value))
|
|
293
|
+
return textSegments(value).join('\n');
|
|
264
294
|
const object = resultObject(value);
|
|
265
295
|
if (object) {
|
|
266
296
|
for (const key of ['output', 'stdout', 'content', 'text', 'message']) {
|
|
@@ -280,9 +310,14 @@ export function structuredToolResult(value) {
|
|
|
280
310
|
const structured = resultObject(value);
|
|
281
311
|
const success = structuredField(structured, 'success');
|
|
282
312
|
const isError = success === false || structuredField(structured, 'is_error') === true;
|
|
313
|
+
const outcome = isError
|
|
314
|
+
? 'error'
|
|
315
|
+
: success === true
|
|
316
|
+
? 'ok'
|
|
317
|
+
: codexScriptOutcome(value) ?? 'unknown';
|
|
283
318
|
return {
|
|
284
319
|
text: resultText(value),
|
|
285
|
-
outcome
|
|
320
|
+
outcome,
|
|
286
321
|
exitCode: structuredNumber(structured, 'exit_code'),
|
|
287
322
|
statusCode: structuredNumber(structured, 'status_code'),
|
|
288
323
|
errorCode: structuredString(structured, 'error_code'),
|
|
@@ -89,6 +89,29 @@ export declare function phoenixIdBaseForDeploy(opts?: {
|
|
|
89
89
|
baseUrl?: string;
|
|
90
90
|
domain?: string;
|
|
91
91
|
}): string | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Managed collaboration backend config to bind on a managed Worker deploy
|
|
94
|
+
* (PHNX-3835): `PRIX_ARTIFACT_COLLAB_BASE` (the Prix artifact-collaboration API
|
|
95
|
+
* base) and `ARTIFACT_COLLAB_SERVICE_TOKEN` (the service credential the Worker
|
|
96
|
+
* sends as `Authorization: Bearer`, NEVER surfaced to the browser). Both are read
|
|
97
|
+
* from the deploy environment.
|
|
98
|
+
*
|
|
99
|
+
* Deliberately dormant-by-default: the two secrets are applied ONLY when BOTH are
|
|
100
|
+
* present. Until the Prix API counterpart is live an operator leaves them unset,
|
|
101
|
+
* and the Worker fails `/__collab` closed (404) with the artifact page GET
|
|
102
|
+
* unaffected. A pure-BYO endpoint never gets them (it has no Phoenix identity).
|
|
103
|
+
* Unlike `phoenixIdBaseForDeploy` this never throws on absence — collaboration is
|
|
104
|
+
* additive, not required for a working managed deploy.
|
|
105
|
+
*/
|
|
106
|
+
export declare function collabConfigForDeploy(opts?: {
|
|
107
|
+
managed?: boolean;
|
|
108
|
+
}, cfg?: {
|
|
109
|
+
baseUrl?: string;
|
|
110
|
+
domain?: string;
|
|
111
|
+
}): {
|
|
112
|
+
collabBase?: string;
|
|
113
|
+
collabServiceToken?: string;
|
|
114
|
+
};
|
|
92
115
|
/**
|
|
93
116
|
* Pick the principal via the shared selection policy. Signed-in AND no explicit
|
|
94
117
|
* BYO override → managed; otherwise BYO.
|
|
@@ -84,6 +84,30 @@ export function phoenixIdBaseForDeploy(opts = {}, cfg) {
|
|
|
84
84
|
}
|
|
85
85
|
return base;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Managed collaboration backend config to bind on a managed Worker deploy
|
|
89
|
+
* (PHNX-3835): `PRIX_ARTIFACT_COLLAB_BASE` (the Prix artifact-collaboration API
|
|
90
|
+
* base) and `ARTIFACT_COLLAB_SERVICE_TOKEN` (the service credential the Worker
|
|
91
|
+
* sends as `Authorization: Bearer`, NEVER surfaced to the browser). Both are read
|
|
92
|
+
* from the deploy environment.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately dormant-by-default: the two secrets are applied ONLY when BOTH are
|
|
95
|
+
* present. Until the Prix API counterpart is live an operator leaves them unset,
|
|
96
|
+
* and the Worker fails `/__collab` closed (404) with the artifact page GET
|
|
97
|
+
* unaffected. A pure-BYO endpoint never gets them (it has no Phoenix identity).
|
|
98
|
+
* Unlike `phoenixIdBaseForDeploy` this never throws on absence — collaboration is
|
|
99
|
+
* additive, not required for a working managed deploy.
|
|
100
|
+
*/
|
|
101
|
+
export function collabConfigForDeploy(opts = {}, cfg) {
|
|
102
|
+
const managed = opts.managed === true || (cfg != null && isManagedShareEndpoint(cfg));
|
|
103
|
+
if (!managed)
|
|
104
|
+
return {};
|
|
105
|
+
const base = (process.env.PRIX_ARTIFACT_COLLAB_BASE || '').replace(/\/+$/, '').trim();
|
|
106
|
+
const token = (process.env.ARTIFACT_COLLAB_SERVICE_TOKEN || '').trim();
|
|
107
|
+
if (!base || !token)
|
|
108
|
+
return {};
|
|
109
|
+
return { collabBase: base, collabServiceToken: token };
|
|
110
|
+
}
|
|
87
111
|
/**
|
|
88
112
|
* The share surface's BYO-override signals: an explicit `--byo`, a
|
|
89
113
|
* caller-supplied static write token, or `AGENTS_SHARE_BACKEND=byo`. Detecting
|
|
@@ -64,6 +64,10 @@ export interface UpdateWorkerResult {
|
|
|
64
64
|
}
|
|
65
65
|
/** Cloudflare `secret_text` name the Worker reads as `env.PHOENIX_ID_BASE`. */
|
|
66
66
|
export declare const WORKER_PHOENIX_ID_BASE_SECRET = "PHOENIX_ID_BASE";
|
|
67
|
+
/** Cloudflare `secret_text` name the Worker reads as `env.PRIX_ARTIFACT_COLLAB_BASE`. */
|
|
68
|
+
export declare const WORKER_COLLAB_BASE_SECRET = "PRIX_ARTIFACT_COLLAB_BASE";
|
|
69
|
+
/** Cloudflare `secret_text` name the Worker reads as `env.ARTIFACT_COLLAB_SERVICE_TOKEN`. */
|
|
70
|
+
export declare const WORKER_COLLAB_SERVICE_TOKEN_SECRET = "ARTIFACT_COLLAB_SERVICE_TOKEN";
|
|
67
71
|
export type UpdateWorkerOpts = ProvisionOptions & {
|
|
68
72
|
force?: boolean;
|
|
69
73
|
/**
|
|
@@ -73,6 +77,14 @@ export type UpdateWorkerOpts = ProvisionOptions & {
|
|
|
73
77
|
* fails loud — do not pass a blank to "skip".
|
|
74
78
|
*/
|
|
75
79
|
phoenixIdBase?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Managed collaboration backend (PHNX-3835). BOTH must be present to enable
|
|
82
|
+
* `/__collab`; pass neither to leave the surface disabled (the Worker fails it
|
|
83
|
+
* closed). The service token rides Cloudflare's Secrets API, never rendered
|
|
84
|
+
* HTML or browser JS. Applied like `phoenixIdBase` — only when defined.
|
|
85
|
+
*/
|
|
86
|
+
collabBase?: string;
|
|
87
|
+
collabServiceToken?: string;
|
|
76
88
|
};
|
|
77
89
|
/**
|
|
78
90
|
* Re-deploy the Worker script against an ALREADY-provisioned endpoint (same
|
|
@@ -107,6 +107,10 @@ export function hashWorkerScript(script) {
|
|
|
107
107
|
}
|
|
108
108
|
/** Cloudflare `secret_text` name the Worker reads as `env.PHOENIX_ID_BASE`. */
|
|
109
109
|
export const WORKER_PHOENIX_ID_BASE_SECRET = 'PHOENIX_ID_BASE';
|
|
110
|
+
/** Cloudflare `secret_text` name the Worker reads as `env.PRIX_ARTIFACT_COLLAB_BASE`. */
|
|
111
|
+
export const WORKER_COLLAB_BASE_SECRET = 'PRIX_ARTIFACT_COLLAB_BASE';
|
|
112
|
+
/** Cloudflare `secret_text` name the Worker reads as `env.ARTIFACT_COLLAB_SERVICE_TOKEN`. */
|
|
113
|
+
export const WORKER_COLLAB_SERVICE_TOKEN_SECRET = 'ARTIFACT_COLLAB_SERVICE_TOKEN';
|
|
110
114
|
/**
|
|
111
115
|
* Re-deploy the Worker script against an ALREADY-provisioned endpoint (same
|
|
112
116
|
* account/worker/bucket `deployWorker` was first called with) and idempotently
|
|
@@ -145,6 +149,7 @@ export async function updateWorker(apiToken, accountId, workerName, bucketName,
|
|
|
145
149
|
if (opts.phoenixIdBase !== undefined) {
|
|
146
150
|
await applyPhoenixIdBaseSecret(apiToken, accountId, workerName, opts.phoenixIdBase, opts);
|
|
147
151
|
}
|
|
152
|
+
await applyCollabSecrets(apiToken, accountId, workerName, opts);
|
|
148
153
|
return { templateHash, skipped: true };
|
|
149
154
|
}
|
|
150
155
|
await deployWorker(apiToken, accountId, workerName, worker, bucketName, opts);
|
|
@@ -168,8 +173,33 @@ export async function updateWorker(apiToken, accountId, workerName, bucketName,
|
|
|
168
173
|
throw new Error(`Worker deployed but PHOENIX_ID_BASE failed to re-apply — Phoenix-bearer publishes will 401 until you re-run \`agents artifacts share update\`. (${detail})`);
|
|
169
174
|
}
|
|
170
175
|
}
|
|
176
|
+
// The script upload wiped bindings/secrets (see JSDoc above), so re-apply the
|
|
177
|
+
// collaboration secrets too — but only when the caller provided them. An
|
|
178
|
+
// ordinary deploy with collaboration still dormant passes neither and the
|
|
179
|
+
// Worker keeps failing `/__collab` closed.
|
|
180
|
+
try {
|
|
181
|
+
await applyCollabSecrets(apiToken, accountId, workerName, opts);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
185
|
+
throw new Error(`Worker deployed but the collaboration secrets failed to re-apply — \`/__collab\` stays disabled until you re-run \`agents artifacts share update\`. (${detail})`);
|
|
186
|
+
}
|
|
171
187
|
return { templateHash, skipped: false };
|
|
172
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Bind the managed collaboration secrets (PHNX-3835) — both or neither. A no-op
|
|
191
|
+
* when the caller passed neither (collaboration dormant), so a plain managed
|
|
192
|
+
* deploy never touches them; fails loud on a half-config (one without the other).
|
|
193
|
+
*/
|
|
194
|
+
async function applyCollabSecrets(apiToken, accountId, workerName, opts) {
|
|
195
|
+
if (opts.collabBase === undefined && opts.collabServiceToken === undefined)
|
|
196
|
+
return;
|
|
197
|
+
if (opts.collabBase === undefined || opts.collabServiceToken === undefined) {
|
|
198
|
+
throw new Error('Managed collaboration needs BOTH PRIX_ARTIFACT_COLLAB_BASE and ARTIFACT_COLLAB_SERVICE_TOKEN — set both or neither.');
|
|
199
|
+
}
|
|
200
|
+
await putWorkerSecret(apiToken, accountId, workerName, WORKER_COLLAB_BASE_SECRET, opts.collabBase, opts);
|
|
201
|
+
await putWorkerSecret(apiToken, accountId, workerName, WORKER_COLLAB_SERVICE_TOKEN_SECRET, opts.collabServiceToken, opts);
|
|
202
|
+
}
|
|
173
203
|
/** Add/update a secret_text binding using Cloudflare's Workers Secrets API. */
|
|
174
204
|
export async function putWorkerSecret(apiToken, accountId, workerName, name, text, opts = {}) {
|
|
175
205
|
const request = opts.request ?? defaultCloudflareRequester;
|