@bbigbang/agent-node 0.1.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.
Files changed (47) hide show
  1. package/dist/agentHost.js +483 -0
  2. package/dist/appVersion.js +14 -0
  3. package/dist/assetCachePaths.js +35 -0
  4. package/dist/attachmentInput.js +588 -0
  5. package/dist/attachmentMaterializer.js +230 -0
  6. package/dist/bigbangCli.js +17 -0
  7. package/dist/bigbangMessageSendDetection.js +284 -0
  8. package/dist/builtinSkillRoots.js +54 -0
  9. package/dist/claudeConfig.js +32 -0
  10. package/dist/claudeDirectRuntime.js +1960 -0
  11. package/dist/claudeSessionControls.js +78 -0
  12. package/dist/claudeTranscriptFs.js +147 -0
  13. package/dist/codexAppServerClient.js +188 -0
  14. package/dist/codexAppServerEnv.js +14 -0
  15. package/dist/codexAppServerRpc.js +273 -0
  16. package/dist/codexAppServerRuntime.js +3495 -0
  17. package/dist/codexBuiltinPrompt.js +117 -0
  18. package/dist/codexConversationSummarizer.js +76 -0
  19. package/dist/codexTranscriptFs.js +145 -0
  20. package/dist/config.js +129 -0
  21. package/dist/connection.js +151 -0
  22. package/dist/dispatchQueueStore.js +39 -0
  23. package/dist/dreamEnv.js +1 -0
  24. package/dist/dreamMemoryFallback.js +118 -0
  25. package/dist/dreamToolPolicy.js +293 -0
  26. package/dist/droidMissionRunner.js +808 -0
  27. package/dist/executor.js +1078 -0
  28. package/dist/hostRuntime.js +1 -0
  29. package/dist/libraryAuthorityFs.js +74 -0
  30. package/dist/libraryMirror.js +183 -0
  31. package/dist/main.js +1659 -0
  32. package/dist/native-worker/native-worker.mjs +475 -0
  33. package/dist/nativeMissionAgentDispatch.js +463 -0
  34. package/dist/nativeMissionRunner.js +461 -0
  35. package/dist/nativeSkillMounts.js +204 -0
  36. package/dist/nativeWorkerHost.js +142 -0
  37. package/dist/nodeSink.js +142 -0
  38. package/dist/panelHttpFetch.js +334 -0
  39. package/dist/runtimeDrivers.js +62 -0
  40. package/dist/skillFs.js +229 -0
  41. package/dist/soloHost.js +165 -0
  42. package/dist/soloNodeSink.js +138 -0
  43. package/dist/terminalManager.js +254 -0
  44. package/dist/workspaceFs.js +1020 -0
  45. package/dist/workspaceGit.js +694 -0
  46. package/dist/workspaceInspect.js +22 -0
  47. package/package.json +49 -0
@@ -0,0 +1,475 @@
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Native mission worker script.
6
+ *
7
+ * Receives feature context via environment variables (`NATIVE_WORKER_CONTEXT`)
8
+ * or a JSON file (`NATIVE_WORKER_CONTEXT_FILE`), calls an OpenAI-compatible
9
+ * LLM API directly with built-in `fetch`, performs workspace file operations,
10
+ * and writes a structured JSON output file for the runner to consume.
11
+ *
12
+ * Hardening features:
13
+ * - Output schema versioning (`schemaVersion`)
14
+ * - Retryable vs terminal error classification
15
+ * - Structured workspace file operation capture (`fileOperations`)
16
+ * - Explicit model/provider/env visibility in output and logs
17
+ */
18
+
19
+ const SCHEMA_VERSION = '1.0.0';
20
+ const STDOUT_LIMIT = 8_000;
21
+ const STDERR_LIMIT = 8_000;
22
+
23
+ // Environment variables that influence worker behavior. Values are surfaced in
24
+ // output.runtimeConfig.env so the exact runtime configuration is auditable;
25
+ // secrets are redacted to avoid leaking credentials.
26
+ const VISIBLE_ENV_VARS = [
27
+ 'OPENAI_API_BASE',
28
+ 'NATIVE_WORKER_TIMEOUT_MS',
29
+ 'NATIVE_WORKER_GRACE_PERIOD_MS',
30
+ ];
31
+
32
+ function main() {
33
+ run()
34
+ .then(({ exitCode }) => {
35
+ process.exit(exitCode);
36
+ })
37
+ .catch((error) => {
38
+ console.error('[native-worker] unhandled error:', error);
39
+ process.exit(1);
40
+ });
41
+ }
42
+
43
+ async function run() {
44
+ let rawContext;
45
+ try {
46
+ rawContext = readContext();
47
+ } catch (error) {
48
+ console.error('[native-worker]', error);
49
+ return { exitCode: 1 };
50
+ }
51
+
52
+ let context;
53
+ try {
54
+ context = JSON.parse(rawContext);
55
+ validateContext(context);
56
+ context.featureId = sanitizeFeatureId(context.featureId);
57
+ } catch (error) {
58
+ console.error('[native-worker]', error);
59
+ return { exitCode: 1 };
60
+ }
61
+
62
+ let workspaceRoot;
63
+ let missionDir;
64
+ try {
65
+ workspaceRoot = resolveSafePath(context.workspaceRoot, null);
66
+ missionDir = resolveSafePath(context.missionDir, workspaceRoot);
67
+ } catch (error) {
68
+ const message = String(error?.message ?? error);
69
+ console.error('[native-worker]', message);
70
+ return { exitCode: 1 };
71
+ }
72
+
73
+ const fileOps = new FileOperationTracker(workspaceRoot);
74
+ const contextFile = process.env.NATIVE_WORKER_CONTEXT_FILE;
75
+ if (contextFile && !process.env.NATIVE_WORKER_CONTEXT) {
76
+ fileOps.record(contextFile, 'read');
77
+ }
78
+
79
+ const runtimeConfig = buildRuntimeConfig(context);
80
+ logRuntimeConfig(runtimeConfig);
81
+
82
+ const stdoutLines = [
83
+ `Native worker started for ${context.featureId}`,
84
+ `Schema version: ${SCHEMA_VERSION}`,
85
+ `Model: ${runtimeConfig.model}`,
86
+ `Provider base URL: ${runtimeConfig.providerBaseUrl}`,
87
+ `Timeout: ${runtimeConfig.workerTimeoutMs}ms`,
88
+ `Grace period: ${runtimeConfig.workerGracePeriodMs}ms`,
89
+ ];
90
+ const stderrLines = [];
91
+
92
+ let llmResult;
93
+ try {
94
+ llmResult = await callLlm(context, runtimeConfig);
95
+ } catch (error) {
96
+ const classification = classifyLlmError(error);
97
+ const message = String(error?.message ?? error);
98
+ stderrLines.push(message);
99
+ writeFailure(context, missionDir, fileOps, runtimeConfig, stdoutLines.join('\n'), stderrLines.join('\n'), message, classification);
100
+ return { exitCode: 1 };
101
+ }
102
+
103
+ // Workspace file operation: write a durable marker inside the workspace root.
104
+ const markerRelative = path.join('.bigbang', 'native-worker-runs', context.featureId, 'marker.md');
105
+ const markerPath = path.join(workspaceRoot, markerRelative);
106
+ try {
107
+ ensureInsideWorkspace(markerPath, workspaceRoot);
108
+ fileOps.record(path.dirname(markerPath), 'mkdir');
109
+ mkdirSync(path.dirname(markerPath), { recursive: true });
110
+ const existedBefore = existsSync(markerPath);
111
+ const content = buildMarkerContent(context, llmResult, runtimeConfig);
112
+ fileOps.record(markerPath, existedBefore ? 'modify' : 'create', { size: Buffer.byteLength(content, 'utf8') });
113
+ writeFileSync(markerPath, content, 'utf8');
114
+ } catch (error) {
115
+ const classification = classifyWorkspaceError(error);
116
+ const message = String(error?.message ?? error);
117
+ stderrLines.push(message);
118
+ writeFailure(context, missionDir, fileOps, runtimeConfig, stdoutLines.join('\n'), stderrLines.join('\n'), message, classification);
119
+ return { exitCode: 1 };
120
+ }
121
+
122
+ const implementedFiles = [markerPath];
123
+
124
+ // Workspace file operation: touch a small run receipt in the mission dir so
125
+ // that "touched" operations appear in fileOperations as well.
126
+ const receiptPath = path.join(missionDir, 'native-output', `${context.featureId}-receipt.txt`);
127
+ try {
128
+ ensureInsideWorkspace(receiptPath, workspaceRoot);
129
+ fileOps.record(receiptPath, 'touch');
130
+ writeFileSync(receiptPath, `${context.featureId} ${Date.now()}\n`, 'utf8');
131
+ } catch (error) {
132
+ // Receipt is non-critical; log but do not fail the feature.
133
+ stderrLines.push(String(error?.message ?? error));
134
+ }
135
+
136
+ const handoff = {
137
+ salientSummary: `Native worker completed feature ${context.featureId}.`,
138
+ verification: {
139
+ markerPath,
140
+ markerRelative,
141
+ llmUsed: llmResult.used,
142
+ model: llmResult.model,
143
+ },
144
+ discoveredIssues: [],
145
+ };
146
+
147
+ if (llmResult.used && typeof llmResult.response === 'string' && llmResult.response.length > 0) {
148
+ handoff.llmResponsePreview = llmResult.response.slice(0, 2_000);
149
+ }
150
+
151
+ writeOutput(context, missionDir, {
152
+ schemaVersion: SCHEMA_VERSION,
153
+ featureId: context.featureId,
154
+ status: 'completed',
155
+ implementedFiles,
156
+ fileOperations: fileOps.toArray(),
157
+ runtimeConfig,
158
+ handoff,
159
+ stdout: truncateLines(stdoutLines.join('\n'), STDOUT_LIMIT),
160
+ stderr: truncateLines(stderrLines.join('\n'), STDERR_LIMIT),
161
+ exitCode: 0,
162
+ });
163
+
164
+ return { exitCode: 0 };
165
+ }
166
+
167
+ function readContext() {
168
+ const envRaw = process.env.NATIVE_WORKER_CONTEXT;
169
+ if (envRaw) {
170
+ return envRaw;
171
+ }
172
+
173
+ const contextFile = process.env.NATIVE_WORKER_CONTEXT_FILE;
174
+ if (contextFile) {
175
+ return readFileSync(contextFile, 'utf8');
176
+ }
177
+
178
+ throw new Error('NATIVE_WORKER_CONTEXT or NATIVE_WORKER_CONTEXT_FILE is required.');
179
+ }
180
+
181
+ function validateContext(context) {
182
+ if (!context || typeof context !== 'object') {
183
+ throw new Error('Feature context must be an object.');
184
+ }
185
+ const requiredStringFields = ['featureId', 'featureDescription', 'milestone', 'workspaceRoot', 'missionDir'];
186
+ for (const field of requiredStringFields) {
187
+ const value = context[field];
188
+ if (typeof value !== 'string' || value.trim() === '') {
189
+ throw new Error(`Feature context is missing required field: ${field}`);
190
+ }
191
+ }
192
+ }
193
+
194
+ const SAFE_FEATURE_ID_RE = /^[a-zA-Z0-9_-]+$/;
195
+
196
+ function sanitizeFeatureId(featureId) {
197
+ const trimmed = featureId.trim();
198
+ if (!SAFE_FEATURE_ID_RE.test(trimmed)) {
199
+ throw new Error(`featureId contains unsafe characters: ${featureId}`);
200
+ }
201
+ return trimmed;
202
+ }
203
+
204
+ function resolveSafePath(value, base) {
205
+ const resolved = path.resolve(value);
206
+ if (!existsSync(resolved)) {
207
+ throw new Error(`Path does not exist: ${value}`);
208
+ }
209
+ const real = realpathSync(resolved);
210
+ if (base) {
211
+ const realBase = realpathSync(base);
212
+ const relative = path.relative(realBase, real);
213
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
214
+ throw new Error(`Path escapes allowed workspace: ${value}`);
215
+ }
216
+ }
217
+ return real;
218
+ }
219
+
220
+ function ensureInsideWorkspace(targetPath, workspaceRoot) {
221
+ const realBase = realpathSync(workspaceRoot);
222
+ const resolved = path.resolve(targetPath);
223
+ let ancestor = resolved;
224
+ while (!existsSync(ancestor)) {
225
+ const parent = path.dirname(ancestor);
226
+ if (parent === ancestor) break;
227
+ ancestor = parent;
228
+ }
229
+ const realAncestor = realpathSync(ancestor);
230
+ const relative = path.relative(realBase, realAncestor);
231
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
232
+ throw new Error(`Target path escapes workspace: ${targetPath}`);
233
+ }
234
+ }
235
+
236
+ function resolveProviderBaseUrl() {
237
+ const baseUrl = process.env.OPENAI_API_BASE?.replace(/\/$/, '') ?? 'https://api.openai.com/v1';
238
+ // Redact any embedded credentials from the logged URL.
239
+ try {
240
+ const url = new URL(baseUrl);
241
+ url.username = '';
242
+ url.password = '';
243
+ return url.toString().replace(/\/$/, '');
244
+ } catch {
245
+ return baseUrl;
246
+ }
247
+ }
248
+
249
+ function buildRuntimeConfig(context) {
250
+ const model = context.workerModel ?? context.orchestratorModel ?? 'gpt-4o-mini';
251
+ const providerBaseUrl = resolveProviderBaseUrl();
252
+ return {
253
+ model,
254
+ modelMode: context.modelMode ?? 'platform_default',
255
+ orchestratorModel: context.orchestratorModel ?? null,
256
+ workerModel: context.workerModel ?? null,
257
+ validatorModel: context.validatorModel ?? null,
258
+ providerBaseUrl,
259
+ apiKeyConfigured: Boolean(process.env.OPENAI_API_KEY),
260
+ workerTimeoutMs: Number(process.env.NATIVE_WORKER_TIMEOUT_MS ?? 300_000),
261
+ workerGracePeriodMs: Number(process.env.NATIVE_WORKER_GRACE_PERIOD_MS ?? 5_000),
262
+ platform: process.platform,
263
+ nodeVersion: process.version,
264
+ env: buildVisibleEnv(),
265
+ };
266
+ }
267
+
268
+ function buildVisibleEnv() {
269
+ const env = {};
270
+ for (const key of VISIBLE_ENV_VARS) {
271
+ const value = process.env[key];
272
+ if (value !== undefined) {
273
+ env[key] = key.toLowerCase().includes('key') ? '[redacted]' : value;
274
+ }
275
+ }
276
+ return env;
277
+ }
278
+
279
+ function logRuntimeConfig(runtimeConfig) {
280
+ console.log('[native-worker] runtime config:', JSON.stringify({
281
+ ...runtimeConfig,
282
+ apiKeyConfigured: runtimeConfig.apiKeyConfigured ? 'yes' : 'no',
283
+ }));
284
+ }
285
+
286
+ async function callLlm(context, runtimeConfig) {
287
+ const apiKey = process.env.OPENAI_API_KEY;
288
+ if (!apiKey) {
289
+ const error = new Error('OPENAI_API_KEY is not configured in the worker environment.');
290
+ error.code = 'ENOKEY';
291
+ throw error;
292
+ }
293
+
294
+ const model = runtimeConfig.model;
295
+ const baseUrl = runtimeConfig.providerBaseUrl;
296
+
297
+ let response;
298
+ try {
299
+ response = await fetch(`${baseUrl}/chat/completions`, {
300
+ method: 'POST',
301
+ headers: {
302
+ 'Content-Type': 'application/json',
303
+ Authorization: `Bearer ${apiKey}`,
304
+ },
305
+ body: JSON.stringify({
306
+ model,
307
+ messages: [
308
+ { role: 'system', content: 'You are a helpful software engineering assistant.' },
309
+ { role: 'user', content: context.prompt },
310
+ ],
311
+ }),
312
+ });
313
+ } catch (error) {
314
+ // Network-level failures (timeout, DNS, connection reset) are retryable.
315
+ throw Object.assign(new Error(`LLM network error: ${String(error?.message ?? error)}`), { llmErrorKind: 'network' });
316
+ }
317
+
318
+ if (!response.ok) {
319
+ const status = response.status;
320
+ const statusText = response.statusText;
321
+ let bodyText = '';
322
+ try {
323
+ bodyText = await response.text();
324
+ } catch {
325
+ // Ignore body read failures.
326
+ }
327
+ const error = new Error(`LLM request failed: ${status} ${statusText}${bodyText ? ` - ${bodyText.slice(0, 500)}` : ''}`);
328
+ error.status = status;
329
+ error.llmErrorKind = 'http';
330
+ throw error;
331
+ }
332
+
333
+ const data = await response.json();
334
+ const choices = Array.isArray(data.choices) ? data.choices : [];
335
+ const first = choices[0];
336
+ const message = first?.message ?? {};
337
+ const content = typeof message.content === 'string' ? message.content : '';
338
+
339
+ if (choices.length === 0 && data.error) {
340
+ const upstreamError = new Error(`LLM upstream error: ${JSON.stringify(data.error).slice(0, 500)}`);
341
+ upstreamError.llmErrorKind = 'upstream';
342
+ throw upstreamError;
343
+ }
344
+
345
+ return { used: true, model, response: content };
346
+ }
347
+
348
+ function classifyLlmError(error) {
349
+ if (error?.code === 'ENOKEY') {
350
+ return { kind: 'terminal', category: 'auth_failure' };
351
+ }
352
+
353
+ const status = typeof error?.status === 'number' ? error.status : undefined;
354
+ const message = String(error?.message ?? error ?? '').toLowerCase();
355
+ const kind = error?.llmErrorKind;
356
+
357
+ if (status === 401) {
358
+ return { kind: 'terminal', category: 'auth_failure' };
359
+ }
360
+ if (status === 403) {
361
+ return { kind: 'terminal', category: 'permission_denied' };
362
+ }
363
+ if (status === 429) {
364
+ return { kind: 'retryable', category: 'rate_limit' };
365
+ }
366
+ if (status === 400 || status === 404) {
367
+ return { kind: 'terminal', category: 'invalid_model' };
368
+ }
369
+ if (kind === 'network' || message.includes('timeout') || message.includes('abort') || message.includes('econnreset') || message.includes('etimedout')) {
370
+ return { kind: 'retryable', category: 'network_timeout' };
371
+ }
372
+ if (status >= 500 || kind === 'upstream') {
373
+ return { kind: 'retryable', category: 'llm_error' };
374
+ }
375
+ return { kind: 'retryable', category: 'llm_error' };
376
+ }
377
+
378
+ function classifyWorkspaceError(error) {
379
+ const code = error?.code;
380
+ const message = String(error?.message ?? error ?? '').toLowerCase();
381
+ if (code === 'EACCES' || code === 'EPERM' || message.includes('permission denied')) {
382
+ return { kind: 'terminal', category: 'permission_denied' };
383
+ }
384
+ if (code === 'EEXIST' || code === 'ENOTDIR' || message.includes('not a directory') || message.includes('file already exists')) {
385
+ return { kind: 'terminal', category: 'workspace_error' };
386
+ }
387
+ if (code === 'ENOSPC' || message.includes('no space')) {
388
+ return { kind: 'terminal', category: 'workspace_error' };
389
+ }
390
+ if (message.includes('escapes workspace')) {
391
+ return { kind: 'terminal', category: 'permission_denied' };
392
+ }
393
+ return { kind: 'retryable', category: 'workspace_error' };
394
+ }
395
+
396
+ class FileOperationTracker {
397
+ constructor(workspaceRoot) {
398
+ this.workspaceRoot = workspaceRoot;
399
+ this.operations = [];
400
+ }
401
+
402
+ record(absolutePath, operation, extra = {}) {
403
+ const relativePath = path.relative(this.workspaceRoot, absolutePath) || '.';
404
+ this.operations.push({
405
+ path: absolutePath,
406
+ relativePath,
407
+ operation,
408
+ ...extra,
409
+ timestamp: Date.now(),
410
+ });
411
+ }
412
+
413
+ toArray() {
414
+ return this.operations;
415
+ }
416
+ }
417
+
418
+ function outputPath(context, missionDir) {
419
+ return path.join(missionDir, 'native-output', `${context.featureId}.json`);
420
+ }
421
+
422
+ function writeOutput(context, missionDir, output) {
423
+ const outPath = outputPath(context, missionDir);
424
+ mkdirSync(path.dirname(outPath), { recursive: true });
425
+ writeFileSync(outPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
426
+ }
427
+
428
+ function writeFailure(context, missionDir, fileOps, runtimeConfig, stdout, stderr, error, classification) {
429
+ try {
430
+ writeOutput(context, missionDir, {
431
+ schemaVersion: SCHEMA_VERSION,
432
+ featureId: context.featureId,
433
+ status: 'failed',
434
+ implementedFiles: [],
435
+ fileOperations: fileOps.toArray(),
436
+ runtimeConfig,
437
+ errorClassification: classification,
438
+ handoff: {
439
+ salientSummary: 'Native worker failed.',
440
+ discoveredIssues: [error],
441
+ },
442
+ stdout: truncateLines(stdout, STDOUT_LIMIT),
443
+ stderr: truncateLines(stderr, STDERR_LIMIT),
444
+ exitCode: 1,
445
+ });
446
+ } catch (writeError) {
447
+ console.error('[native-worker] failed to write failure output:', writeError);
448
+ }
449
+ }
450
+
451
+ function buildMarkerContent(context, llmResult, runtimeConfig) {
452
+ const lines = [
453
+ '# Native worker run',
454
+ '',
455
+ `Feature: ${context.featureId}`,
456
+ `Milestone: ${context.milestone}`,
457
+ `Description: ${context.featureDescription}`,
458
+ ];
459
+ if (context.title) {
460
+ lines.push(`Title: ${context.title}`);
461
+ }
462
+ if (llmResult.used) {
463
+ lines.push(`Model: ${llmResult.model}`);
464
+ }
465
+ lines.push(`Provider base URL: ${runtimeConfig.providerBaseUrl}`);
466
+ lines.push('', `Generated at: ${new Date().toISOString()}`);
467
+ return lines.join('\n') + '\n';
468
+ }
469
+
470
+ function truncateLines(value, limit) {
471
+ if (value.length <= limit) return value;
472
+ return `${value.slice(0, limit)}\n[truncated]`;
473
+ }
474
+
475
+ main();