@parall/agent-core 1.30.0 → 1.32.0
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/dist/bridge-workspace.js +12 -12
- package/dist/dispatch-adapter.d.ts +15 -8
- package/dist/dispatch-adapter.d.ts.map +1 -1
- package/dist/event-format.d.ts +1 -1
- package/dist/event-format.d.ts.map +1 -1
- package/dist/event-format.js +68 -25
- package/dist/gateway-base.d.ts +15 -13
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +662 -312
- package/dist/index.d.ts +15 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/internal/attachment-input.d.ts +3 -3
- package/dist/internal/attachment-input.d.ts.map +1 -1
- package/dist/internal/attachment-input.js +61 -58
- package/dist/logger.d.ts +1 -1
- package/dist/platform-config.d.ts +28 -2
- package/dist/platform-config.d.ts.map +1 -1
- package/dist/platform-config.js +42 -11
- package/dist/prompt-fragments.d.ts +1 -1
- package/dist/prompt-fragments.d.ts.map +1 -1
- package/dist/prompt-fragments.js +28 -10
- package/dist/provider-config.d.ts +20 -0
- package/dist/provider-config.d.ts.map +1 -0
- package/dist/provider-config.js +41 -0
- package/dist/routing.d.ts +5 -5
- package/dist/routing.js +6 -6
- package/dist/session-state.d.ts +16 -0
- package/dist/session-state.d.ts.map +1 -1
- package/dist/session-state.js +45 -0
- package/dist/skills/index.d.ts +5 -4
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +28 -21
- package/dist/skills/parall-clips.d.ts +2 -0
- package/dist/skills/parall-clips.d.ts.map +1 -0
- package/dist/skills/parall-clips.js +44 -0
- package/dist/telemetry.d.ts +27 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +205 -0
- package/dist/types.d.ts +18 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/src/bridge-workspace.ts +12 -12
- package/src/dispatch-adapter.ts +31 -8
- package/src/event-format.ts +80 -30
- package/src/gateway-base.ts +998 -442
- package/src/index.ts +23 -12
- package/src/internal/attachment-input.ts +127 -100
- package/src/logger.ts +1 -1
- package/src/platform-config.ts +61 -16
- package/src/prompt-fragments.ts +28 -10
- package/src/provider-config.ts +51 -0
- package/src/routing.ts +11 -11
- package/src/session-state.ts +62 -0
- package/src/skills/index.ts +34 -23
- package/src/skills/parall-clips.ts +44 -0
- package/src/telemetry.ts +252 -0
- package/src/types.ts +18 -2
package/src/index.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export * from
|
|
11
|
-
export
|
|
12
|
-
export
|
|
1
|
+
export * from './provider-config.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './session-state.js';
|
|
4
|
+
export * from './routing.js';
|
|
5
|
+
export * from './event-format.js';
|
|
6
|
+
export * from './prompt-fragments.js';
|
|
7
|
+
export * from './bridge-workspace.js';
|
|
8
|
+
export * from './dispatch-adapter.js';
|
|
9
|
+
export { createLogger, childLogger } from './logger.js';
|
|
10
|
+
export * from './gateway-base.js';
|
|
11
|
+
export * from './platform-config.js';
|
|
12
|
+
export { writeSkillFiles, buildSkillReferences, SKILLS } from './skills/index.js';
|
|
13
|
+
export type { SkillMeta } from './skills/index.js';
|
|
14
|
+
export {
|
|
15
|
+
initAgentTelemetry,
|
|
16
|
+
createOtelLogger,
|
|
17
|
+
runWithSessionKey,
|
|
18
|
+
startDispatchSpan,
|
|
19
|
+
endDispatchSpan,
|
|
20
|
+
recordDispatchMetric,
|
|
21
|
+
recordMissingReply,
|
|
22
|
+
} from './telemetry.js';
|
|
23
|
+
export type { TelemetryHandle } from './telemetry.js';
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import { constants, type Dirent, type Stats } from
|
|
3
|
-
import * as fsSync from
|
|
4
|
-
import * as fs from
|
|
5
|
-
import * as path from
|
|
6
|
-
import type { DispatchContext, GatewayLogger } from
|
|
7
|
-
import { formatBytes, renderLocalAttachmentSection } from
|
|
8
|
-
import type { LocalAttachmentResult, PreparedLocalImage } from
|
|
9
|
-
import type { ParallEvent } from
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { constants, type Dirent, type Stats } from 'node:fs';
|
|
3
|
+
import * as fsSync from 'node:fs';
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import type { DispatchContext, GatewayLogger } from '../dispatch-adapter.js';
|
|
7
|
+
import { formatBytes, renderLocalAttachmentSection } from '../prompt-fragments.js';
|
|
8
|
+
import type { LocalAttachmentResult, PreparedLocalImage } from '../prompt-fragments.js';
|
|
9
|
+
import type { ParallEvent } from '../types.js';
|
|
10
10
|
|
|
11
11
|
// Re-export so external consumers (codex-agent, claude-agent) keep importing
|
|
12
12
|
// via the internal/attachment-input subpath. The canonical definition lives
|
|
@@ -50,11 +50,11 @@ export const DEFAULT_ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
50
50
|
export const DEFAULT_MAINTENANCE_COOLDOWN_MS = 60 * 1000;
|
|
51
51
|
|
|
52
52
|
const SUPPORTED_IMAGE_MIME_TYPES = new Set([
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
'image/png',
|
|
54
|
+
'image/jpeg',
|
|
55
|
+
'image/jpg',
|
|
56
|
+
'image/webp',
|
|
57
|
+
'image/gif',
|
|
58
58
|
]);
|
|
59
59
|
|
|
60
60
|
// Process-global state — correct under the current "one gateway per agent"
|
|
@@ -72,11 +72,16 @@ export async function prepareLocalImageAttachments(
|
|
|
72
72
|
opts: LocalAttachmentOptions,
|
|
73
73
|
): Promise<LocalAttachmentResult> {
|
|
74
74
|
const attachments = event.attachments ?? [];
|
|
75
|
-
const imageAttachments = attachments.filter((att) =>
|
|
75
|
+
const imageAttachments = attachments.filter((att) =>
|
|
76
|
+
SUPPORTED_IMAGE_MIME_TYPES.has(att.mimeType.toLowerCase()),
|
|
77
|
+
);
|
|
76
78
|
if (imageAttachments.length === 0) return { images: [], notes: [] };
|
|
77
79
|
|
|
78
80
|
const maxTotalImageBytes = opts.maxTotalImageBytes ?? DEFAULT_MAX_TOTAL_IMAGE_BYTES;
|
|
79
|
-
const totalDeclaredBytes = imageAttachments.reduce(
|
|
81
|
+
const totalDeclaredBytes = imageAttachments.reduce(
|
|
82
|
+
(sum, att) => sum + Math.max(0, att.fileSize),
|
|
83
|
+
0,
|
|
84
|
+
);
|
|
80
85
|
if (totalDeclaredBytes > maxTotalImageBytes) {
|
|
81
86
|
return {
|
|
82
87
|
images: [],
|
|
@@ -107,15 +112,12 @@ export async function prepareLocalImageAttachments(
|
|
|
107
112
|
// (one pass per cooldown window) and run it off the dispatch path. The
|
|
108
113
|
// cache cap is best-effort, not a hard ceiling — cooldown drift is fine.
|
|
109
114
|
const maintenanceCooldownMs = opts.maintenanceCooldownMs ?? DEFAULT_MAINTENANCE_COOLDOWN_MS;
|
|
110
|
-
const maintenancePromise = scheduleAttachmentMaintenance(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
log: opts.log,
|
|
117
|
-
},
|
|
118
|
-
);
|
|
115
|
+
const maintenancePromise = scheduleAttachmentMaintenance(rootDir, {
|
|
116
|
+
ttlMs: opts.attachmentTtlMs ?? DEFAULT_ATTACHMENT_TTL_MS,
|
|
117
|
+
maxBytes: opts.maxCacheBytes ?? DEFAULT_ATTACHMENT_CACHE_MAX_BYTES,
|
|
118
|
+
cooldownMs: maintenanceCooldownMs,
|
|
119
|
+
log: opts.log,
|
|
120
|
+
});
|
|
119
121
|
if (maintenanceCooldownMs === 0 && maintenancePromise) {
|
|
120
122
|
// Sync-mode for tests that need deterministic prune ordering relative to
|
|
121
123
|
// active-dispatch tracking. Production paths leave this on the background.
|
|
@@ -150,7 +152,9 @@ export async function prepareLocalImageAttachments(
|
|
|
150
152
|
try {
|
|
151
153
|
await fetchFresh();
|
|
152
154
|
} catch (err) {
|
|
153
|
-
opts.log?.warn?.(
|
|
155
|
+
opts.log?.warn?.(
|
|
156
|
+
`agent-core: failed to prepare local image attachment ${att.id}: ${String(err)}`,
|
|
157
|
+
);
|
|
154
158
|
notes.push(`[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)}]`);
|
|
155
159
|
continue;
|
|
156
160
|
}
|
|
@@ -167,7 +171,9 @@ export async function prepareLocalImageAttachments(
|
|
|
167
171
|
try {
|
|
168
172
|
await fetchFresh();
|
|
169
173
|
} catch (err) {
|
|
170
|
-
opts.log?.warn?.(
|
|
174
|
+
opts.log?.warn?.(
|
|
175
|
+
`agent-core: failed to re-download corrupted cache for ${att.id}: ${String(err)}`,
|
|
176
|
+
);
|
|
171
177
|
notes.push(`[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)}]`);
|
|
172
178
|
continue;
|
|
173
179
|
}
|
|
@@ -176,7 +182,9 @@ export async function prepareLocalImageAttachments(
|
|
|
176
182
|
|
|
177
183
|
if (!mimeOk) {
|
|
178
184
|
await removeLocalFileIfInside(localPath, rootDir);
|
|
179
|
-
notes.push(
|
|
185
|
+
notes.push(
|
|
186
|
+
`[Image attachment unavailable locally: prll://${sanitizeMeta(att.id)} (downloaded content does not match ${sanitizeMeta(att.mimeType)})]`,
|
|
187
|
+
);
|
|
180
188
|
continue;
|
|
181
189
|
}
|
|
182
190
|
|
|
@@ -195,10 +203,7 @@ export async function prepareLocalImageAttachments(
|
|
|
195
203
|
}
|
|
196
204
|
}
|
|
197
205
|
|
|
198
|
-
export function appendLocalAttachmentRefs(
|
|
199
|
-
body: string,
|
|
200
|
-
result: LocalAttachmentResult,
|
|
201
|
-
): string {
|
|
206
|
+
export function appendLocalAttachmentRefs(body: string, result: LocalAttachmentResult): string {
|
|
202
207
|
const section = renderLocalAttachmentSection(result);
|
|
203
208
|
return section ? `${body}\n\n${section}` : body;
|
|
204
209
|
}
|
|
@@ -230,7 +235,7 @@ export function pinLocalAttachmentPaths(images: PreparedLocalImage[]): () => voi
|
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
function attachmentRootDir(workspaceDir: string): string {
|
|
233
|
-
return path.join(path.resolve(workspaceDir),
|
|
238
|
+
return path.join(path.resolve(workspaceDir), '.parall', 'attachments');
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
/**
|
|
@@ -241,17 +246,17 @@ function attachmentRootDir(workspaceDir: string): string {
|
|
|
241
246
|
*/
|
|
242
247
|
export function ensureLocalAttachmentGitExclude(workingDirectory: string): void {
|
|
243
248
|
try {
|
|
244
|
-
const rel = execSync(
|
|
249
|
+
const rel = execSync('git rev-parse --git-path info/exclude', {
|
|
245
250
|
cwd: workingDirectory,
|
|
246
|
-
encoding:
|
|
247
|
-
stdio: [
|
|
251
|
+
encoding: 'utf8',
|
|
252
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
248
253
|
}).trim();
|
|
249
254
|
const excludePath = path.isAbsolute(rel) ? rel : path.join(workingDirectory, rel);
|
|
250
255
|
fsSync.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
251
|
-
const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath,
|
|
252
|
-
if (existing.split(/\r?\n/).some((line) => line.trim() ===
|
|
253
|
-
const prefix = existing.length > 0 && !existing.endsWith(
|
|
254
|
-
fsSync.appendFileSync(excludePath, `${prefix}.parall/\n`,
|
|
256
|
+
const existing = fsSync.existsSync(excludePath) ? fsSync.readFileSync(excludePath, 'utf8') : '';
|
|
257
|
+
if (existing.split(/\r?\n/).some((line) => line.trim() === '.parall/')) return;
|
|
258
|
+
const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
259
|
+
fsSync.appendFileSync(excludePath, `${prefix}.parall/\n`, 'utf8');
|
|
255
260
|
} catch {
|
|
256
261
|
// Best-effort only. A missing exclude entry should not block dispatch.
|
|
257
262
|
}
|
|
@@ -308,7 +313,7 @@ function scheduleAttachmentMaintenance(
|
|
|
308
313
|
|
|
309
314
|
async function ensureAttachmentRootDir(workspaceDir: string): Promise<string> {
|
|
310
315
|
const workspaceRoot = path.resolve(workspaceDir);
|
|
311
|
-
const parallDir = path.join(workspaceRoot,
|
|
316
|
+
const parallDir = path.join(workspaceRoot, '.parall');
|
|
312
317
|
const rootDir = attachmentRootDir(workspaceRoot);
|
|
313
318
|
|
|
314
319
|
await fs.mkdir(workspaceRoot, { recursive: true });
|
|
@@ -334,17 +339,21 @@ async function ensurePathIsNotSymlink(filePath: string): Promise<void> {
|
|
|
334
339
|
throw new Error(`refusing to use symlinked attachment path ${filePath}`);
|
|
335
340
|
}
|
|
336
341
|
} catch (err) {
|
|
337
|
-
if ((err as NodeJS.ErrnoException)?.code ===
|
|
342
|
+
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return;
|
|
338
343
|
throw err;
|
|
339
344
|
}
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
function isPathInside(childPath: string, parentPath: string): boolean {
|
|
343
348
|
const rel = path.relative(parentPath, childPath);
|
|
344
|
-
return rel ===
|
|
349
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
345
350
|
}
|
|
346
351
|
|
|
347
|
-
async function existingUsableFile(
|
|
352
|
+
async function existingUsableFile(
|
|
353
|
+
filePath: string,
|
|
354
|
+
expectedSize: number,
|
|
355
|
+
rootDir: string,
|
|
356
|
+
): Promise<boolean> {
|
|
348
357
|
try {
|
|
349
358
|
const stat = await localFileStatInsideRoot(filePath, rootDir);
|
|
350
359
|
return stat.isFile() && stat.size > 0 && (expectedSize <= 0 || stat.size === expectedSize);
|
|
@@ -437,7 +446,11 @@ async function openLocalTempFileInsideRoot(filePath: string, rootDir: string) {
|
|
|
437
446
|
}
|
|
438
447
|
}
|
|
439
448
|
|
|
440
|
-
async function assertLocalFileIdentity(
|
|
449
|
+
async function assertLocalFileIdentity(
|
|
450
|
+
filePath: string,
|
|
451
|
+
rootDir: string,
|
|
452
|
+
expected: Stats,
|
|
453
|
+
): Promise<void> {
|
|
441
454
|
const stat = await localFileStatInsideRoot(filePath, rootDir);
|
|
442
455
|
if (!sameFileIdentity(stat, expected)) {
|
|
443
456
|
throw new Error(`attachment file changed during validation: ${filePath}`);
|
|
@@ -475,24 +488,26 @@ async function cleanupOldAttachmentFiles(
|
|
|
475
488
|
}
|
|
476
489
|
|
|
477
490
|
const cutoff = Date.now() - ttlMs;
|
|
478
|
-
await Promise.all(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
+
await Promise.all(
|
|
492
|
+
entries.map(async (entry) => {
|
|
493
|
+
if (!entry.isDirectory()) return;
|
|
494
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
495
|
+
try {
|
|
496
|
+
if (preserveDirs?.has(path.resolve(fullPath))) return;
|
|
497
|
+
// lstat (not stat) for consistency with the symlink-hardened download
|
|
498
|
+
// path: if a racing local writer swapped the dir entry for a symlink
|
|
499
|
+
// between readdir and now, we don't want fs.rm to follow it and delete
|
|
500
|
+
// outside the attachment root.
|
|
501
|
+
const stat = await fs.lstat(fullPath);
|
|
502
|
+
if (!stat.isDirectory()) return;
|
|
503
|
+
if (stat.mtimeMs < cutoff) {
|
|
504
|
+
await fs.rm(fullPath, { recursive: true, force: true });
|
|
505
|
+
}
|
|
506
|
+
} catch (err) {
|
|
507
|
+
log?.warn?.(`agent-core: failed to clean attachment temp dir ${fullPath}: ${String(err)}`);
|
|
491
508
|
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
}));
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
496
511
|
}
|
|
497
512
|
|
|
498
513
|
async function pruneAttachmentCache(
|
|
@@ -596,7 +611,7 @@ async function downloadAttachmentToFile(
|
|
|
596
611
|
} catch {
|
|
597
612
|
throw new Error(`invalid attachment URL`);
|
|
598
613
|
}
|
|
599
|
-
if (parsed.protocol !==
|
|
614
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
600
615
|
throw new Error(`refusing attachment URL with scheme ${parsed.protocol}`);
|
|
601
616
|
}
|
|
602
617
|
|
|
@@ -612,7 +627,7 @@ async function downloadAttachmentToFile(
|
|
|
612
627
|
throw new Error(`${res.status} ${res.statusText}`);
|
|
613
628
|
}
|
|
614
629
|
// Content-Length is only an early-exit hint; the stream byte counter below is authoritative.
|
|
615
|
-
const contentLength = parseContentLength(res.headers.get(
|
|
630
|
+
const contentLength = parseContentLength(res.headers.get('content-length'));
|
|
616
631
|
if (contentLength !== undefined && contentLength > maxBytes) {
|
|
617
632
|
throw new Error(`download would exceed ${formatBytes(maxBytes)} remaining turn limit`);
|
|
618
633
|
}
|
|
@@ -649,7 +664,7 @@ async function writeResponseToFileWithLimit(
|
|
|
649
664
|
rootDir: string,
|
|
650
665
|
): Promise<number> {
|
|
651
666
|
if (maxBytes <= 0) {
|
|
652
|
-
throw new Error(
|
|
667
|
+
throw new Error('download would exceed turn limit');
|
|
653
668
|
}
|
|
654
669
|
|
|
655
670
|
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -716,26 +731,30 @@ function localFileName(attachmentId: string, fileName: string, mimeType: string)
|
|
|
716
731
|
|
|
717
732
|
function extensionForMime(mimeType: string): string {
|
|
718
733
|
switch (mimeType.toLowerCase()) {
|
|
719
|
-
case
|
|
720
|
-
case
|
|
721
|
-
return
|
|
722
|
-
case
|
|
723
|
-
return
|
|
724
|
-
case
|
|
725
|
-
return
|
|
726
|
-
case
|
|
734
|
+
case 'image/jpeg':
|
|
735
|
+
case 'image/jpg':
|
|
736
|
+
return '.jpg';
|
|
737
|
+
case 'image/webp':
|
|
738
|
+
return '.webp';
|
|
739
|
+
case 'image/gif':
|
|
740
|
+
return '.gif';
|
|
741
|
+
case 'image/png':
|
|
727
742
|
default:
|
|
728
|
-
return
|
|
743
|
+
return '.png';
|
|
729
744
|
}
|
|
730
745
|
}
|
|
731
746
|
|
|
732
747
|
function sanitizePathSegment(value: string): string {
|
|
733
|
-
const safe = value.replace(/[^A-Za-z0-9._-]+/g,
|
|
734
|
-
if (!safe || /^\.+$/.test(safe)) return
|
|
748
|
+
const safe = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
749
|
+
if (!safe || /^\.+$/.test(safe)) return 'attachment';
|
|
735
750
|
return safe;
|
|
736
751
|
}
|
|
737
752
|
|
|
738
|
-
async function downloadedFileMatchesImageMime(
|
|
753
|
+
async function downloadedFileMatchesImageMime(
|
|
754
|
+
filePath: string,
|
|
755
|
+
mimeType: string,
|
|
756
|
+
rootDir: string,
|
|
757
|
+
): Promise<boolean> {
|
|
739
758
|
const file = await openLocalFileInsideRoot(filePath, rootDir);
|
|
740
759
|
try {
|
|
741
760
|
const buffer = Buffer.alloc(16);
|
|
@@ -748,38 +767,47 @@ async function downloadedFileMatchesImageMime(filePath: string, mimeType: string
|
|
|
748
767
|
|
|
749
768
|
function imageHeaderMatches(bytes: Buffer, mimeType: string): boolean {
|
|
750
769
|
switch (mimeType.toLowerCase()) {
|
|
751
|
-
case
|
|
752
|
-
return
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
770
|
+
case 'image/png':
|
|
771
|
+
return (
|
|
772
|
+
bytes.length >= 8 &&
|
|
773
|
+
bytes[0] === 0x89 &&
|
|
774
|
+
bytes[1] === 0x50 &&
|
|
775
|
+
bytes[2] === 0x4e &&
|
|
776
|
+
bytes[3] === 0x47 &&
|
|
777
|
+
bytes[4] === 0x0d &&
|
|
778
|
+
bytes[5] === 0x0a &&
|
|
779
|
+
bytes[6] === 0x1a &&
|
|
780
|
+
bytes[7] === 0x0a
|
|
781
|
+
);
|
|
782
|
+
case 'image/jpeg':
|
|
783
|
+
case 'image/jpg':
|
|
763
784
|
return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
764
|
-
case
|
|
765
|
-
return
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
785
|
+
case 'image/gif':
|
|
786
|
+
return (
|
|
787
|
+
bytes.length >= 6 &&
|
|
788
|
+
(bytes.subarray(0, 6).toString('ascii') === 'GIF87a' ||
|
|
789
|
+
bytes.subarray(0, 6).toString('ascii') === 'GIF89a')
|
|
790
|
+
);
|
|
791
|
+
case 'image/webp':
|
|
792
|
+
return (
|
|
793
|
+
bytes.length >= 12 &&
|
|
794
|
+
bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
|
|
795
|
+
bytes.subarray(8, 12).toString('ascii') === 'WEBP'
|
|
796
|
+
);
|
|
772
797
|
default:
|
|
773
798
|
return false;
|
|
774
799
|
}
|
|
775
800
|
}
|
|
776
801
|
|
|
777
802
|
function isAbortError(err: unknown): boolean {
|
|
778
|
-
return err instanceof DOMException && err.name ===
|
|
803
|
+
return err instanceof DOMException && err.name === 'AbortError';
|
|
779
804
|
}
|
|
780
805
|
|
|
781
806
|
function sanitizeMeta(value: string): string {
|
|
782
|
-
return value
|
|
807
|
+
return value
|
|
808
|
+
.replace(/[\r\n]+/g, ' ')
|
|
809
|
+
.replace(/[[\]|]/g, ' ')
|
|
810
|
+
.trim();
|
|
783
811
|
}
|
|
784
812
|
|
|
785
813
|
function parseContentLength(value: string | null): number | undefined {
|
|
@@ -788,4 +816,3 @@ function parseContentLength(value: string | null): number | undefined {
|
|
|
788
816
|
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
789
817
|
return n;
|
|
790
818
|
}
|
|
791
|
-
|
package/src/logger.ts
CHANGED
package/src/platform-config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
import type { ParallClient, PlatformConfigResponse } from
|
|
4
|
-
import type { GatewayLogger } from
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type { ParallClient, PlatformConfigResponse } from '@parall/sdk';
|
|
4
|
+
import type { GatewayLogger } from './dispatch-adapter.js';
|
|
5
5
|
|
|
6
6
|
export interface PlatformDefaults {
|
|
7
7
|
model: string | null;
|
|
@@ -19,8 +19,49 @@ export interface PlatformManagementProfile {
|
|
|
19
19
|
model_management?: string | null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function isPlatformManagedProfile(
|
|
23
|
-
|
|
22
|
+
export function isPlatformManagedProfile(
|
|
23
|
+
profile: PlatformManagementProfile | null | undefined,
|
|
24
|
+
): boolean {
|
|
25
|
+
return profile?.machine_id != null || profile?.model_management === 'platform';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the platform-delivered model should be treated as an operator
|
|
30
|
+
* OVERRIDE (beats env) rather than a FLOOR (loses to env). True only when the
|
|
31
|
+
* profile is platform-managed AND not explicitly self-managed: legacy hosted
|
|
32
|
+
* profiles (model_management=null, resolved to platform server-side) are
|
|
33
|
+
* overrides; local self agents — whose delivered model is a catalog floor —
|
|
34
|
+
* are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
|
|
35
|
+
* this derivation in one place avoids drift across the bridges' (re)config
|
|
36
|
+
* sites where the subtle null-hosted-vs-explicit-self distinction matters.
|
|
37
|
+
*/
|
|
38
|
+
export function isPlatformModelOverride(
|
|
39
|
+
profile: PlatformManagementProfile | null | undefined,
|
|
40
|
+
): boolean {
|
|
41
|
+
return isPlatformManagedProfile(profile) && profile?.model_management !== 'self';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the model a runtime should launch with. Precedence:
|
|
46
|
+
* 1. platform operator override (only when the agent is platform-managed)
|
|
47
|
+
* 2. env/runtime config (PRLL_CLAUDE_MODEL / PRLL_CODEX_MODEL)
|
|
48
|
+
* 3. server-delivered model floor (applies even when model_management=self)
|
|
49
|
+
*
|
|
50
|
+
* The floor (3) is what makes self-hosted + Parall-proxy agents work: the
|
|
51
|
+
* server now delivers `agents.defaults.model` whenever the route is the
|
|
52
|
+
* catalog-gated Parall proxy, so the CLI never falls back to its built-in
|
|
53
|
+
* default (which can outrun the catalog and get rejected). Returns null when
|
|
54
|
+
* no model is resolved, so the bridge omits `--model` and lets the CLI / own
|
|
55
|
+
* login pick — the correct behavior for runtime_auth / custom routes, where
|
|
56
|
+
* the server delivers no floor.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveRuntimeModel(
|
|
59
|
+
isOperatorOverride: boolean,
|
|
60
|
+
platformModel: string | null,
|
|
61
|
+
configModel: string | null | undefined,
|
|
62
|
+
): string | undefined {
|
|
63
|
+
const operatorOverride = isOperatorOverride ? platformModel : null;
|
|
64
|
+
return operatorOverride ?? configModel ?? platformModel ?? undefined;
|
|
24
65
|
}
|
|
25
66
|
|
|
26
67
|
interface PlatformModelDef {
|
|
@@ -34,7 +75,7 @@ interface CachedPlatformConfig {
|
|
|
34
75
|
fetchedAt: string;
|
|
35
76
|
}
|
|
36
77
|
|
|
37
|
-
const CACHE_FILENAME =
|
|
78
|
+
const CACHE_FILENAME = 'parall-platform-config.json';
|
|
38
79
|
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
39
80
|
|
|
40
81
|
function cachePath(stateDir: string): string {
|
|
@@ -43,7 +84,7 @@ function cachePath(stateDir: string): string {
|
|
|
43
84
|
|
|
44
85
|
function loadCache(stateDir: string): CachedPlatformConfig | null {
|
|
45
86
|
try {
|
|
46
|
-
const raw = fs.readFileSync(cachePath(stateDir),
|
|
87
|
+
const raw = fs.readFileSync(cachePath(stateDir), 'utf-8');
|
|
47
88
|
return JSON.parse(raw) as CachedPlatformConfig;
|
|
48
89
|
} catch {
|
|
49
90
|
return null;
|
|
@@ -59,7 +100,7 @@ function saveCache(stateDir: string, response: PlatformConfigResponse): void {
|
|
|
59
100
|
const filePath = cachePath(stateDir);
|
|
60
101
|
const tmpPath = `${filePath}.tmp`;
|
|
61
102
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
62
|
-
fs.writeFileSync(tmpPath, JSON.stringify(cached, null, 2),
|
|
103
|
+
fs.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), 'utf-8');
|
|
63
104
|
fs.renameSync(tmpPath, filePath);
|
|
64
105
|
}
|
|
65
106
|
|
|
@@ -69,7 +110,7 @@ function runtimeModelName(
|
|
|
69
110
|
config: Record<string, unknown>,
|
|
70
111
|
): string | null {
|
|
71
112
|
const runtime = runtimeType?.trim();
|
|
72
|
-
if (!runtime || runtime ===
|
|
113
|
+
if (!runtime || runtime === 'openclaw') return canonicalModel;
|
|
73
114
|
|
|
74
115
|
const models = (config.models ?? {}) as Record<string, unknown>;
|
|
75
116
|
const providers = (models.providers ?? {}) as Record<string, unknown>;
|
|
@@ -78,7 +119,7 @@ function runtimeModelName(
|
|
|
78
119
|
const match = catalog.find((model) => model.id === canonicalModel);
|
|
79
120
|
const runtimeNames = (match?.runtime_names ?? {}) as Record<string, unknown>;
|
|
80
121
|
const runtimeName = runtimeNames[runtime];
|
|
81
|
-
return typeof runtimeName ===
|
|
122
|
+
return typeof runtimeName === 'string' && runtimeName ? runtimeName : null;
|
|
82
123
|
}
|
|
83
124
|
|
|
84
125
|
function extractDefaults(config: Record<string, unknown>, runtimeType?: string): PlatformDefaults {
|
|
@@ -86,13 +127,13 @@ function extractDefaults(config: Record<string, unknown>, runtimeType?: string):
|
|
|
86
127
|
const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
|
|
87
128
|
|
|
88
129
|
let model: string | null = null;
|
|
89
|
-
if (typeof defaults.model ===
|
|
90
|
-
const canonicalModel = defaults.model.replace(/^parall\//,
|
|
130
|
+
if (typeof defaults.model === 'string' && defaults.model) {
|
|
131
|
+
const canonicalModel = defaults.model.replace(/^parall\//, '');
|
|
91
132
|
model = runtimeModelName(canonicalModel, runtimeType, config);
|
|
92
133
|
}
|
|
93
134
|
|
|
94
135
|
let thinkingEffort: string | null = null;
|
|
95
|
-
if (typeof defaults.thinking_effort ===
|
|
136
|
+
if (typeof defaults.thinking_effort === 'string' && defaults.thinking_effort) {
|
|
96
137
|
thinkingEffort = defaults.thinking_effort;
|
|
97
138
|
}
|
|
98
139
|
|
|
@@ -124,7 +165,9 @@ export function createPlatformConfigManager(opts: {
|
|
|
124
165
|
fresh = await client.getPlatformConfig(cachedVersion);
|
|
125
166
|
} catch (err) {
|
|
126
167
|
if (cached) {
|
|
127
|
-
log?.warn(
|
|
168
|
+
log?.warn(
|
|
169
|
+
`platform config fetch failed, using cached version ${cachedVersion}: ${String(err)}`,
|
|
170
|
+
);
|
|
128
171
|
return currentDefaults;
|
|
129
172
|
}
|
|
130
173
|
log?.warn(`platform config fetch failed and no cache available: ${String(err)}`);
|
|
@@ -136,7 +179,9 @@ export function createPlatformConfigManager(opts: {
|
|
|
136
179
|
}
|
|
137
180
|
|
|
138
181
|
if (fresh.schema_version !== undefined && fresh.schema_version > SUPPORTED_SCHEMA_VERSION) {
|
|
139
|
-
log?.warn(
|
|
182
|
+
log?.warn(
|
|
183
|
+
`platform config schema_version ${fresh.schema_version} > supported (${SUPPORTED_SCHEMA_VERSION}), keeping current`,
|
|
184
|
+
);
|
|
140
185
|
return currentDefaults;
|
|
141
186
|
}
|
|
142
187
|
|