@gakim-digital/dexter-bridge 0.5.21 → 0.11.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/README.md +116 -35
- package/package.json +19 -5
- package/src/agent.js +1351 -331
- package/src/agentOutput.js +209 -0
- package/src/api.js +48 -1
- package/src/cli.js +267 -39
- package/src/config.js +30 -7
- package/src/framerAgentTools.js +1108 -0
- package/src/harnessMcpServer.js +240 -0
- package/src/harnessTools.js +548 -0
- package/src/logger.js +1 -1
- package/src/nativeSkills.js +295 -0
- package/src/outcomeWorkspace.js +351 -0
- package/src/protocol.js +239 -0
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1050 -156
- package/src/providers/codexStructuredOutput.js +243 -16
- package/src/providers/directByok.js +197 -0
- package/src/providers/index.js +33 -7
- package/src/providers/openCode.js +607 -0
- package/src/runtimeProfiles.js +284 -0
- package/src/providers/claudeAgentSdk.js +0 -507
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import {
|
|
4
|
+
applyOutcomeWorkspaceUpdates,
|
|
5
|
+
collectOutcomeWorkspaceChanges,
|
|
6
|
+
markOutcomeWorkspaceSynchronized,
|
|
7
|
+
} from './outcomeWorkspace.js';
|
|
8
|
+
|
|
9
|
+
export const HARNESS_TOOL_NAMES = [
|
|
10
|
+
'progress_update',
|
|
11
|
+
'workspace_inspect',
|
|
12
|
+
'workspace_sync',
|
|
13
|
+
'shell_run',
|
|
14
|
+
'preview_control',
|
|
15
|
+
'browser_control',
|
|
16
|
+
'data_inspect',
|
|
17
|
+
'verification_run',
|
|
18
|
+
];
|
|
19
|
+
const EMPTY_SCHEMA = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {},
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const HARNESS_TOOL_DEFINITIONS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'progress_update',
|
|
28
|
+
description:
|
|
29
|
+
'Tell the user what you are doing and why it matters in one or two natural first-person sentences. Call this before the first inspection or edit and again at every meaningful product phase change, but never delay the product work to produce it. Interpret the request in your own words; never quote or truncate it, expose tool or file names, or begin with "Finished:".',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
message: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
minLength: 1,
|
|
36
|
+
maxLength: 500,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['message'],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'workspace_inspect',
|
|
45
|
+
description: 'Inspect the current isolated project workspace.',
|
|
46
|
+
inputSchema: EMPTY_SCHEMA,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'workspace_sync',
|
|
50
|
+
description:
|
|
51
|
+
'Synchronize the current isolated workspace to the protected InstaWebAI build workspace. Call this before previewing or verifying.',
|
|
52
|
+
inputSchema: EMPTY_SCHEMA,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'shell_run',
|
|
56
|
+
description:
|
|
57
|
+
'Run a project command in InstaWebAI’s isolated build workspace. Use this for package installation, code generation, tests, builds, and other project commands. Edit source with native file tools; do not put source, patches, or heredocs in this command.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
command: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
minLength: 1,
|
|
64
|
+
maxLength: 8_192,
|
|
65
|
+
},
|
|
66
|
+
cwd: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
pattern: '^(?:\\.|[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*)$',
|
|
69
|
+
maxLength: 500,
|
|
70
|
+
default: '.',
|
|
71
|
+
},
|
|
72
|
+
timeoutMs: {
|
|
73
|
+
type: 'integer',
|
|
74
|
+
minimum: 1_000,
|
|
75
|
+
maximum: 600_000,
|
|
76
|
+
default: 300_000,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ['command'],
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'preview_control',
|
|
85
|
+
description: 'Start, restart, or inspect the server-managed development preview for the current workspace.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
action: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
enum: ['start', 'restart', 'status'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ['action'],
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'browser_control',
|
|
100
|
+
description:
|
|
101
|
+
'Use an isolated browser against the current development preview. Prefer batch for consecutive interactions so the final semantic snapshot is returned in one tool call.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
action: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
enum: ['open', 'snapshot', 'click', 'fill', 'select', 'press', 'batch', 'close'],
|
|
108
|
+
},
|
|
109
|
+
path: { type: 'string', maxLength: 1_000 },
|
|
110
|
+
selector: { type: 'string', maxLength: 1_000 },
|
|
111
|
+
value: { type: 'string', maxLength: 10_000 },
|
|
112
|
+
key: { type: 'string', maxLength: 80 },
|
|
113
|
+
actions: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
minItems: 1,
|
|
116
|
+
maxItems: 25,
|
|
117
|
+
items: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
action: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
enum: ['open', 'snapshot', 'click', 'fill', 'select', 'press'],
|
|
123
|
+
},
|
|
124
|
+
path: { type: 'string', maxLength: 1_000 },
|
|
125
|
+
selector: { type: 'string', maxLength: 1_000 },
|
|
126
|
+
value: { type: 'string', maxLength: 10_000 },
|
|
127
|
+
key: { type: 'string', maxLength: 80 },
|
|
128
|
+
},
|
|
129
|
+
required: ['action'],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ['action'],
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'data_inspect',
|
|
140
|
+
description:
|
|
141
|
+
'Inspect the application data contract or list redacted development records. This tool is read-only and never exposes database credentials or sensitive field values.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
action: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
enum: ['overview', 'list_records'],
|
|
148
|
+
},
|
|
149
|
+
entityId: { type: 'string', maxLength: 180 },
|
|
150
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
|
151
|
+
offset: { type: 'integer', minimum: 0, maximum: 100000, default: 0 },
|
|
152
|
+
search: { type: 'string', maxLength: 500, default: '' },
|
|
153
|
+
},
|
|
154
|
+
required: ['action'],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'verification_run',
|
|
160
|
+
description:
|
|
161
|
+
'Synchronize the workspace, then run InstaWebAI deterministic and browser verification. Use fast for focused workflow checks and visual for broader browser and presentation review.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
level: { type: 'string', enum: ['fast', 'visual'] },
|
|
166
|
+
workflowIds: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
items: { type: 'string', maxLength: 180 },
|
|
169
|
+
maxItems: 20,
|
|
170
|
+
default: [],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
required: ['level'],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const TOOL_BY_NAME = new Map(HARNESS_TOOL_DEFINITIONS.map((definition) => [definition.name, definition]));
|
|
180
|
+
const REMOTE_TOOLS = new Set([
|
|
181
|
+
'progress_update',
|
|
182
|
+
'workspace_inspect',
|
|
183
|
+
'workspace_sync',
|
|
184
|
+
'shell_run',
|
|
185
|
+
'preview_control',
|
|
186
|
+
'browser_control',
|
|
187
|
+
'data_inspect',
|
|
188
|
+
'verification_run',
|
|
189
|
+
]);
|
|
190
|
+
const AUTO_SYNC_TOOLS = new Set(['shell_run', 'preview_control', 'browser_control', 'verification_run']);
|
|
191
|
+
const FATAL_INFRASTRUCTURE_ERROR_CODES = new Set([
|
|
192
|
+
'APP_BUILD_WORKER_UNAVAILABLE',
|
|
193
|
+
'APP_BUILD_WORKER_TIMEOUT',
|
|
194
|
+
'APP_BUILD_WORKER_SANDBOX_UNAVAILABLE',
|
|
195
|
+
'APP_DATABASE_CONFIGURATION_INVALID',
|
|
196
|
+
'APP_DATABASE_PROVISIONER_UNAVAILABLE',
|
|
197
|
+
'APP_DATABASE_PROVISIONING_FAILED',
|
|
198
|
+
'APP_DATABASE_RECEIPT_REQUIRED',
|
|
199
|
+
'APP_DATABASE_RECEIPT_INVALID',
|
|
200
|
+
'APP_PREVIEW_DATABASE_MIGRATION_FAILED',
|
|
201
|
+
'APP_PREVIEW_DEPENDENCY_PLATFORM_MISMATCH',
|
|
202
|
+
'APP_PREVIEW_INFRASTRUCTURE_FAILED',
|
|
203
|
+
'APP_PREVIEW_PORT_EXHAUSTED',
|
|
204
|
+
'APP_PREVIEW_PORT_RANGE_INVALID',
|
|
205
|
+
'APP_PREVIEW_START_TIMEOUT',
|
|
206
|
+
'APP_RUNTIME_ORCHESTRATOR_FAILED',
|
|
207
|
+
'APP_RUNTIME_ORCHESTRATOR_TIMEOUT',
|
|
208
|
+
'APP_VERIFICATION_INFRASTRUCTURE_UNAVAILABLE',
|
|
209
|
+
'APP_VISUAL_CRITIC_UNAVAILABLE',
|
|
210
|
+
'APP_VISUAL_REVIEW_UNAVAILABLE',
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
function isRecord(value) {
|
|
214
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function toolError(code, message) {
|
|
218
|
+
return Object.assign(new Error(message), { code });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function toolActivityMessage(name, args, phase) {
|
|
222
|
+
const active = phase === 'started' || phase === 'running';
|
|
223
|
+
const failed = phase === 'failed';
|
|
224
|
+
if (name === 'shell_run') {
|
|
225
|
+
const command = String(args?.command || '')
|
|
226
|
+
.replace(/\s+/g, ' ')
|
|
227
|
+
.trim();
|
|
228
|
+
if (/^(?:npm|pnpm|yarn|bun)\s+(?:install|add|remove|uninstall|ci)\b/i.test(command)) {
|
|
229
|
+
return failed
|
|
230
|
+
? 'Could not install project dependencies'
|
|
231
|
+
: active
|
|
232
|
+
? 'Installing project dependencies'
|
|
233
|
+
: 'Finished installing project dependencies';
|
|
234
|
+
}
|
|
235
|
+
if (/\b(?:build|check|lint|test|typecheck|validate)\b/i.test(command)) {
|
|
236
|
+
return `${failed ? 'Could not finish' : active ? 'Running' : 'Finished'} ${command.slice(0, 180)}`;
|
|
237
|
+
}
|
|
238
|
+
return failed
|
|
239
|
+
? 'The project command failed'
|
|
240
|
+
: active
|
|
241
|
+
? 'Running a project command'
|
|
242
|
+
: 'Finished the project command';
|
|
243
|
+
}
|
|
244
|
+
if (name === 'preview_control') {
|
|
245
|
+
return failed ? 'The live preview could not start' : active ? 'Starting the live preview' : 'Live preview is ready';
|
|
246
|
+
}
|
|
247
|
+
if (name === 'browser_control') {
|
|
248
|
+
return failed
|
|
249
|
+
? 'The browser check failed'
|
|
250
|
+
: active
|
|
251
|
+
? 'Testing the app in the browser'
|
|
252
|
+
: 'Finished checking the app in the browser';
|
|
253
|
+
}
|
|
254
|
+
if (name === 'data_inspect') {
|
|
255
|
+
return failed
|
|
256
|
+
? 'Could not inspect application data'
|
|
257
|
+
: active
|
|
258
|
+
? 'Inspecting application data'
|
|
259
|
+
: 'Finished inspecting application data';
|
|
260
|
+
}
|
|
261
|
+
if (name === 'workspace_inspect') {
|
|
262
|
+
return failed
|
|
263
|
+
? 'Could not read the current project state'
|
|
264
|
+
: active
|
|
265
|
+
? 'Reading the current project state'
|
|
266
|
+
: 'Finished reading the project state';
|
|
267
|
+
}
|
|
268
|
+
if (name === 'workspace_sync') {
|
|
269
|
+
return failed ? 'Could not save source changes' : active ? 'Saving source changes' : 'Saved source changes';
|
|
270
|
+
}
|
|
271
|
+
if (name === 'verification_run') {
|
|
272
|
+
return failed
|
|
273
|
+
? 'The application check failed'
|
|
274
|
+
: active
|
|
275
|
+
? 'Checking the application'
|
|
276
|
+
: 'Finished checking the application';
|
|
277
|
+
}
|
|
278
|
+
return failed ? `${name} failed` : active ? `Using ${name}` : `Finished ${name}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function isFatalHarnessInfrastructureError(error) {
|
|
282
|
+
return Boolean(error?.code && FATAL_INFRASTRUCTURE_ERROR_CODES.has(error.code));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function validateArguments(name, value) {
|
|
286
|
+
if (!TOOL_BY_NAME.has(name)) {
|
|
287
|
+
throw toolError('APP_HARNESS_TOOL_UNKNOWN', `Unknown harness tool: ${name}`);
|
|
288
|
+
}
|
|
289
|
+
if (!isRecord(value)) {
|
|
290
|
+
throw toolError('APP_HARNESS_TOOL_INPUT_INVALID', 'Harness tool arguments must be an object.');
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizeRemoteResult(response, name, callId) {
|
|
296
|
+
const toolResult = response?.toolResult;
|
|
297
|
+
if (!toolResult || toolResult.callId !== callId || toolResult.name !== name) {
|
|
298
|
+
throw toolError('APP_HARNESS_TOOL_RESULT_MISSING', `InstaWebAI did not return a valid result for ${name}.`);
|
|
299
|
+
}
|
|
300
|
+
if (!toolResult.ok) {
|
|
301
|
+
throw toolError(
|
|
302
|
+
toolResult.error?.code || 'APP_HARNESS_TOOL_FAILED',
|
|
303
|
+
toolResult.error?.message || `${name} failed.`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return toolResult.result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createHarnessToolRuntime({ assignment, materialized, send, trace, onActivity } = {}) {
|
|
310
|
+
const requestedToolNames = Array.isArray(assignment?.toolProtocol?.tools)
|
|
311
|
+
? assignment.toolProtocol.tools
|
|
312
|
+
: HARNESS_TOOL_NAMES;
|
|
313
|
+
const allowedToolNames = new Set(requestedToolNames.filter((name) => TOOL_BY_NAME.has(name)));
|
|
314
|
+
const definitions = HARNESS_TOOL_DEFINITIONS.filter((definition) => allowedToolNames.has(definition.name));
|
|
315
|
+
let server = null;
|
|
316
|
+
let gateway = null;
|
|
317
|
+
let latestSourceHash = null;
|
|
318
|
+
let closed = false;
|
|
319
|
+
let callSequence = 0;
|
|
320
|
+
let serial = Promise.resolve();
|
|
321
|
+
let fatalInfrastructureError = null;
|
|
322
|
+
let resolveFatalInfrastructure;
|
|
323
|
+
const fatalInfrastructure = new Promise((resolve) => {
|
|
324
|
+
resolveFatalInfrastructure = resolve;
|
|
325
|
+
});
|
|
326
|
+
const initialOriginals = new Map(materialized.originals);
|
|
327
|
+
|
|
328
|
+
async function remoteCall(name, args) {
|
|
329
|
+
const callId = `harness_tool_${Date.now()}_${++callSequence}_${crypto.randomUUID()}`;
|
|
330
|
+
const reportActivity = (phase) => {
|
|
331
|
+
if (name === 'progress_update') return;
|
|
332
|
+
try {
|
|
333
|
+
void Promise.resolve(
|
|
334
|
+
onActivity?.({
|
|
335
|
+
kind: 'tool_active',
|
|
336
|
+
name,
|
|
337
|
+
phase,
|
|
338
|
+
message: toolActivityMessage(name, args, phase),
|
|
339
|
+
occurredAt: new Date().toISOString(),
|
|
340
|
+
}),
|
|
341
|
+
).catch(() => undefined);
|
|
342
|
+
} catch {
|
|
343
|
+
// Observability must never interrupt a tool call.
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
reportActivity('started');
|
|
347
|
+
const heartbeat = setInterval(() => reportActivity('running'), 15_000);
|
|
348
|
+
heartbeat.unref?.();
|
|
349
|
+
try {
|
|
350
|
+
const response = await send('tool_call', {
|
|
351
|
+
callId,
|
|
352
|
+
name,
|
|
353
|
+
arguments: args,
|
|
354
|
+
});
|
|
355
|
+
const result = normalizeRemoteResult(response, name, callId);
|
|
356
|
+
reportActivity('completed');
|
|
357
|
+
return result;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
reportActivity('failed');
|
|
360
|
+
if (!fatalInfrastructureError && isFatalHarnessInfrastructureError(error)) {
|
|
361
|
+
fatalInfrastructureError = error;
|
|
362
|
+
resolveFatalInfrastructure(error);
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
} finally {
|
|
366
|
+
clearInterval(heartbeat);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function synchronize() {
|
|
371
|
+
if (materialized.direct) {
|
|
372
|
+
const result = await remoteCall('workspace_inspect', {});
|
|
373
|
+
latestSourceHash =
|
|
374
|
+
typeof result?.sourceHash === 'string' && result.sourceHash ? result.sourceHash : latestSourceHash;
|
|
375
|
+
return {
|
|
376
|
+
...result,
|
|
377
|
+
synchronized: true,
|
|
378
|
+
changed: false,
|
|
379
|
+
changedFiles: [],
|
|
380
|
+
synchronizedFiles: [],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const files = collectOutcomeWorkspaceChanges(assignment, materialized);
|
|
384
|
+
collectOutcomeWorkspaceChanges(assignment, {
|
|
385
|
+
...materialized,
|
|
386
|
+
originals: initialOriginals,
|
|
387
|
+
});
|
|
388
|
+
const result = await remoteCall('workspace_sync', { files });
|
|
389
|
+
markOutcomeWorkspaceSynchronized(materialized, files);
|
|
390
|
+
latestSourceHash =
|
|
391
|
+
typeof result?.sourceHash === 'string' && result.sourceHash ? result.sourceHash : latestSourceHash;
|
|
392
|
+
return {
|
|
393
|
+
...result,
|
|
394
|
+
synchronizedFiles: files.map((file) => file.path),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function invokeUnsafe(name, rawArguments = {}) {
|
|
399
|
+
if (closed) {
|
|
400
|
+
throw toolError('APP_HARNESS_TOOL_RUNTIME_CLOSED', 'The harness tool runtime is closed.');
|
|
401
|
+
}
|
|
402
|
+
if (!allowedToolNames.has(name)) {
|
|
403
|
+
throw toolError('APP_HARNESS_TOOL_UNKNOWN', `Unavailable harness tool: ${name}`);
|
|
404
|
+
}
|
|
405
|
+
const args = validateArguments(name, rawArguments);
|
|
406
|
+
if (name === 'workspace_sync') return synchronize();
|
|
407
|
+
if (
|
|
408
|
+
!materialized.direct &&
|
|
409
|
+
allowedToolNames.has('workspace_sync') &&
|
|
410
|
+
AUTO_SYNC_TOOLS.has(name) &&
|
|
411
|
+
(latestSourceHash === null || collectOutcomeWorkspaceChanges(assignment, materialized).length > 0)
|
|
412
|
+
) {
|
|
413
|
+
await synchronize();
|
|
414
|
+
}
|
|
415
|
+
if (REMOTE_TOOLS.has(name)) {
|
|
416
|
+
const result = await remoteCall(name, args);
|
|
417
|
+
if (name === 'shell_run') {
|
|
418
|
+
if (!materialized.direct) {
|
|
419
|
+
applyOutcomeWorkspaceUpdates(materialized, result?.workspaceUpdates);
|
|
420
|
+
}
|
|
421
|
+
latestSourceHash =
|
|
422
|
+
typeof result?.sourceHash === 'string' && result.sourceHash ? result.sourceHash : latestSourceHash;
|
|
423
|
+
const { workspaceUpdates = [], ...publicResult } = result || {};
|
|
424
|
+
return {
|
|
425
|
+
...publicResult,
|
|
426
|
+
synchronizedFiles: workspaceUpdates.map((file) => file.path),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
throw toolError('APP_HARNESS_TOOL_UNKNOWN', `Unknown harness tool: ${name}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function invoke(name, args = {}) {
|
|
435
|
+
const operation = serial.then(() => invokeUnsafe(name, args));
|
|
436
|
+
serial = operation.catch(() => undefined);
|
|
437
|
+
return operation;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function startGateway() {
|
|
441
|
+
if (gateway) return gateway;
|
|
442
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
443
|
+
server = http.createServer((request, response) => {
|
|
444
|
+
const fail = (status, code, message) => {
|
|
445
|
+
response.writeHead(status, { 'content-type': 'application/json' });
|
|
446
|
+
response.end(JSON.stringify({ ok: false, error: { code, message } }));
|
|
447
|
+
};
|
|
448
|
+
if (request.method !== 'POST' || request.url !== '/tool') {
|
|
449
|
+
fail(404, 'APP_HARNESS_GATEWAY_NOT_FOUND', 'Harness tool endpoint not found.');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (request.headers.authorization !== `Bearer ${token}`) {
|
|
453
|
+
fail(401, 'APP_HARNESS_GATEWAY_UNAUTHORIZED', 'Invalid harness tool token.');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
let body = '';
|
|
457
|
+
request.on('data', (chunk) => {
|
|
458
|
+
body += chunk.toString('utf8');
|
|
459
|
+
if (body.length > 64 * 1024) request.destroy();
|
|
460
|
+
});
|
|
461
|
+
request.on('end', async () => {
|
|
462
|
+
try {
|
|
463
|
+
const payload = JSON.parse(body || '{}');
|
|
464
|
+
const result = await invoke(String(payload.name || ''), payload.arguments || {});
|
|
465
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
466
|
+
response.end(JSON.stringify({ ok: true, result }));
|
|
467
|
+
} catch (error) {
|
|
468
|
+
trace?.warn?.('harness_tool_failed', {
|
|
469
|
+
name: (() => {
|
|
470
|
+
try {
|
|
471
|
+
return JSON.parse(body || '{}')?.name || null;
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
})(),
|
|
476
|
+
code: error?.code,
|
|
477
|
+
message: error?.message,
|
|
478
|
+
});
|
|
479
|
+
fail(
|
|
480
|
+
Number(error?.statusCode) || 400,
|
|
481
|
+
error?.code || 'APP_HARNESS_TOOL_FAILED',
|
|
482
|
+
error?.message || 'Harness tool failed.',
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
await new Promise((resolve, reject) => {
|
|
488
|
+
server.once('error', reject);
|
|
489
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
490
|
+
});
|
|
491
|
+
const address = server.address();
|
|
492
|
+
if (!address || typeof address !== 'object') {
|
|
493
|
+
throw new Error('Harness tool gateway did not bind to a local port.');
|
|
494
|
+
}
|
|
495
|
+
gateway = {
|
|
496
|
+
url: `http://127.0.0.1:${address.port}/tool`,
|
|
497
|
+
token,
|
|
498
|
+
};
|
|
499
|
+
return gateway;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function close({ waitForPending = true } = {}) {
|
|
503
|
+
closed = true;
|
|
504
|
+
if (waitForPending) await serial.catch(() => undefined);
|
|
505
|
+
if (!server) return;
|
|
506
|
+
if (!waitForPending) server.closeAllConnections?.();
|
|
507
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
508
|
+
server = null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
definitions,
|
|
513
|
+
toolNames: definitions.map((definition) => definition.name),
|
|
514
|
+
invoke,
|
|
515
|
+
startGateway,
|
|
516
|
+
synchronize,
|
|
517
|
+
inspect: () => remoteCall('workspace_inspect', {}),
|
|
518
|
+
sourceHash: () => latestSourceHash,
|
|
519
|
+
hasPendingChanges: () =>
|
|
520
|
+
!materialized.direct && collectOutcomeWorkspaceChanges(assignment, materialized).length > 0,
|
|
521
|
+
directWorkspace: materialized.direct === true,
|
|
522
|
+
fatalInfrastructureError: () => fatalInfrastructureError,
|
|
523
|
+
waitForFatalInfrastructure: () => fatalInfrastructure,
|
|
524
|
+
close,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function codexDynamicToolSpecs(definitions = HARNESS_TOOL_DEFINITIONS) {
|
|
529
|
+
return definitions.map((tool) => ({
|
|
530
|
+
type: 'function',
|
|
531
|
+
name: tool.name,
|
|
532
|
+
description: tool.description,
|
|
533
|
+
inputSchema: tool.inputSchema,
|
|
534
|
+
deferLoading: false,
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function codexDynamicToolResult(result, success = true) {
|
|
539
|
+
return {
|
|
540
|
+
success,
|
|
541
|
+
contentItems: [
|
|
542
|
+
{
|
|
543
|
+
type: 'inputText',
|
|
544
|
+
text: JSON.stringify(result),
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
};
|
|
548
|
+
}
|
package/src/logger.js
CHANGED
|
@@ -7,7 +7,7 @@ const SECRET_PATTERNS = [
|
|
|
7
7
|
/\b(dcpp|dcpd)_[A-Za-z0-9_-]+/g,
|
|
8
8
|
/(Authorization:\s*Bearer\s+)[A-Za-z0-9._-]+/gi,
|
|
9
9
|
/("?(?:apiKey|deviceToken|pairingToken|pairingCode|authorization|secret|password|token)"?\s*[:=]\s*")([^"]+)/gi,
|
|
10
|
-
/((?:api[-_]?key|token|secret|password)=)[^&\s]+/gi,
|
|
10
|
+
/((?:api[-_]?key|token|secret|password|state)=)[^&\s]+/gi,
|
|
11
11
|
];
|
|
12
12
|
const MAX_LOG_BYTES = Math.max(256_000, Number(process.env.DEXTER_BRIDGE_MAX_LOG_BYTES) || 5_000_000);
|
|
13
13
|
const MAX_ROTATED_LOGS = Math.max(1, Number(process.env.DEXTER_BRIDGE_MAX_ROTATED_LOGS) || 4);
|