@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,142 @@
1
+ import { spawn } from 'node:child_process';
2
+ const DEFAULT_TIMEOUT_MS = 300_000;
3
+ const DEFAULT_GRACE_PERIOD_MS = 5_000;
4
+ /**
5
+ * Manages the lifecycle of a single native mission worker subprocess:
6
+ * spawn, stdout/stderr streaming, timeout enforcement, and cancellation.
7
+ *
8
+ * On POSIX systems the worker is spawned detached so that it becomes the
9
+ * leader of a new process group. When a timeout or cancellation occurs the
10
+ * entire process group is signaled (SIGTERM, then SIGKILL after a grace
11
+ * period). This prevents orphan worker descendants from outliving the host.
12
+ */
13
+ export class NativeWorkerHost {
14
+ options;
15
+ child = null;
16
+ timeoutTimer = null;
17
+ graceTimer = null;
18
+ ended = false;
19
+ pendingKillReason = null;
20
+ cancelBeforeSpawn = false;
21
+ constructor(options) {
22
+ this.options = options;
23
+ }
24
+ get pid() {
25
+ return this.child?.pid ?? null;
26
+ }
27
+ spawn() {
28
+ const { command, args, cwd, env } = this.options;
29
+ // Detaching on POSIX lets us signal the whole process group, ensuring
30
+ // any grandchildren spawned by the worker are reaped along with it.
31
+ this.child = spawn(command, args, {
32
+ cwd,
33
+ env,
34
+ stdio: ['ignore', 'pipe', 'pipe'],
35
+ detached: process.platform !== 'win32',
36
+ });
37
+ this.child.stdout?.on('data', (chunk) => {
38
+ this.options.onStdout(String(chunk));
39
+ });
40
+ this.child.stderr?.on('data', (chunk) => {
41
+ this.options.onStderr(String(chunk));
42
+ });
43
+ this.child.on('error', (error) => {
44
+ this.end({
45
+ exitCode: null,
46
+ signal: null,
47
+ error: String(error.message ?? error),
48
+ });
49
+ });
50
+ this.child.on('close', (exitCode, signal) => {
51
+ this.end({
52
+ exitCode: exitCode ?? null,
53
+ signal: signal ?? null,
54
+ });
55
+ });
56
+ // A cancellation request arrived before the child was spawned. Honor it
57
+ // immediately now that the process exists and can be signalled.
58
+ if (this.cancelBeforeSpawn) {
59
+ this.kill('cancel');
60
+ }
61
+ const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
62
+ if (timeoutMs > 0) {
63
+ this.timeoutTimer = setTimeout(() => {
64
+ this.kill('timeout');
65
+ }, timeoutMs);
66
+ this.timeoutTimer.unref?.();
67
+ }
68
+ }
69
+ cancel() {
70
+ // If spawn() has not yet created the child, record the cancellation so
71
+ // the worker is killed as soon as it spawns. This closes a small race
72
+ // where core dispatches a feature and immediately cancels it before the
73
+ // node has finished creating the host.
74
+ if (!this.child) {
75
+ this.cancelBeforeSpawn = true;
76
+ return;
77
+ }
78
+ this.kill('cancel');
79
+ }
80
+ kill(reason) {
81
+ if (!this.child || this.ended)
82
+ return;
83
+ // A kill sequence is already in progress. Repeated cancel/timeout calls
84
+ // while SIGTERM is being honored should not queue additional signals.
85
+ if (this.pendingKillReason)
86
+ return;
87
+ this.pendingKillReason = reason;
88
+ this.signalProcess('SIGTERM');
89
+ const graceMs = this.options.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
90
+ this.graceTimer = setTimeout(() => {
91
+ if (!this.ended) {
92
+ this.signalProcess('SIGKILL');
93
+ }
94
+ }, graceMs);
95
+ this.graceTimer.unref?.();
96
+ }
97
+ signalProcess(signal) {
98
+ if (!this.child || this.child.pid == null || this.ended)
99
+ return;
100
+ try {
101
+ if (process.platform !== 'win32') {
102
+ // Negative PID sends the signal to the entire process group.
103
+ process.kill(-this.child.pid, signal);
104
+ }
105
+ else {
106
+ this.child.kill(signal);
107
+ }
108
+ }
109
+ catch (error) {
110
+ const errno = error;
111
+ // ESRCH means the process is already gone; treat that as a clean reap.
112
+ if (errno.code !== 'ESRCH' && !this.ended) {
113
+ this.end({
114
+ exitCode: null,
115
+ signal: signal,
116
+ error: `Failed to send ${signal}: ${String(errno.message ?? error)}`,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ end(result) {
122
+ if (this.ended)
123
+ return;
124
+ this.ended = true;
125
+ if (this.timeoutTimer) {
126
+ clearTimeout(this.timeoutTimer);
127
+ this.timeoutTimer = null;
128
+ }
129
+ if (this.graceTimer) {
130
+ clearTimeout(this.graceTimer);
131
+ this.graceTimer = null;
132
+ }
133
+ if (this.pendingKillReason && !result.error) {
134
+ const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
135
+ result.error =
136
+ this.pendingKillReason === 'timeout'
137
+ ? `Worker timed out after ${timeoutMs}ms`
138
+ : 'Worker was cancelled';
139
+ }
140
+ this.options.onExit(result);
141
+ }
142
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * OutboundSink that forwards agent output to core as run.event / permission.request messages.
3
+ */
4
+ export class NodeSink {
5
+ runId;
6
+ conversationId;
7
+ send;
8
+ hooks;
9
+ constructor(runId, conversationId, send, hooks) {
10
+ this.runId = runId;
11
+ this.conversationId = conversationId;
12
+ this.send = send;
13
+ this.hooks = hooks;
14
+ }
15
+ async sendAgentText(text) {
16
+ this.emitEvent({ type: 'content.delta', text });
17
+ }
18
+ async sendActivityText(text) {
19
+ this.emitEvent({ type: 'activity.delta', text });
20
+ }
21
+ async sendText(text) {
22
+ this.emitEvent({ type: 'content.delta', text });
23
+ }
24
+ async sendThinkingText(text) {
25
+ this.emitEvent({ type: 'thinking.delta', text });
26
+ }
27
+ async requestPermission(req) {
28
+ this.hooks?.onPermissionRequest?.();
29
+ this.send({
30
+ type: 'permission.request',
31
+ runId: this.runId,
32
+ conversationId: this.conversationId,
33
+ requestId: req.requestId,
34
+ toolName: req.toolName ?? req.toolTitle,
35
+ toolArgs: req.toolArgs ?? null,
36
+ toolKind: req.toolKind,
37
+ ...(req.approvalKind ? { approvalKind: req.approvalKind } : {}),
38
+ ...(req.title ? { title: req.title } : {}),
39
+ ...(req.description ? { description: req.description } : {}),
40
+ ...(req.input !== undefined ? { input: req.input } : {}),
41
+ ...(req.actions?.length ? { actions: req.actions } : {}),
42
+ });
43
+ }
44
+ async sendUi(event) {
45
+ if (event.kind === 'tool') {
46
+ const toolEvent = event;
47
+ if (event.stage === 'complete') {
48
+ const normalizedStatus = event.status === 'cancelled'
49
+ ? 'cancelled'
50
+ : event.status === 'error' || event.status === 'failed' || event.status === 'declined'
51
+ ? 'failed'
52
+ : 'completed';
53
+ const isError = normalizedStatus === 'failed';
54
+ this.emitEvent({
55
+ type: 'tool.result',
56
+ toolCallId: event.toolCallId ?? '',
57
+ output: toolEvent.output ?? event.detail ?? event.status ?? 'done',
58
+ detail: event.detail,
59
+ error: isError,
60
+ status: normalizedStatus,
61
+ metadata: event.metadata,
62
+ });
63
+ }
64
+ else {
65
+ const normalizedStatus = event.status === 'cancelled'
66
+ ? 'cancelled'
67
+ : event.status === 'error' || event.status === 'failed' || event.status === 'declined'
68
+ ? 'failed'
69
+ : event.status === 'completed'
70
+ ? 'completed'
71
+ : 'running';
72
+ this.emitEvent({
73
+ type: 'tool.call',
74
+ toolCallId: event.toolCallId ?? '',
75
+ name: event.title,
76
+ input: toolEvent.input ?? event.detail ?? null,
77
+ detail: event.detail,
78
+ status: normalizedStatus,
79
+ metadata: event.metadata,
80
+ });
81
+ }
82
+ }
83
+ if (event.kind === 'usage') {
84
+ this.emitEvent({
85
+ type: 'run.usage',
86
+ inputTokens: event.inputTokens,
87
+ cachedInputTokens: event.cachedInputTokens,
88
+ outputTokens: event.outputTokens,
89
+ reasoningOutputTokens: event.reasoningOutputTokens,
90
+ totalTokens: event.totalTokens,
91
+ currentInputTokens: event.currentInputTokens,
92
+ currentCachedInputTokens: event.currentCachedInputTokens,
93
+ modelContextWindow: event.modelContextWindow,
94
+ createdAt: Date.now(),
95
+ metadata: event.metadata,
96
+ });
97
+ }
98
+ if (event.kind === 'compact') {
99
+ this.emitEvent({
100
+ type: 'runtime.compact',
101
+ threadId: event.threadId,
102
+ turnId: event.turnId,
103
+ itemId: event.itemId,
104
+ source: event.source,
105
+ eventKey: event.eventKey,
106
+ createdAt: Date.now(),
107
+ });
108
+ }
109
+ if (event.kind === 'plan_phase') {
110
+ this.emitEvent({
111
+ type: 'plan.phase',
112
+ phase: event.phase,
113
+ createdAt: Date.now(),
114
+ });
115
+ }
116
+ if (event.kind === 'plan' || event.kind === 'task') {
117
+ const createdAt = Date.now();
118
+ this.emitEvent({
119
+ type: event.kind === 'plan' ? 'plan.update' : 'task.update',
120
+ title: event.title,
121
+ detail: event.detail,
122
+ createdAt,
123
+ });
124
+ if (event.silent)
125
+ return;
126
+ const text = event.detail
127
+ ? `\n[${event.kind}] ${event.title}\n${event.detail}\n`
128
+ : `\n[${event.kind}] ${event.title}\n`;
129
+ this.emitEvent({ type: 'content.delta', text });
130
+ }
131
+ }
132
+ async breakTextStream() { }
133
+ async flush() { }
134
+ emitEvent(event) {
135
+ this.send({
136
+ type: 'run.event',
137
+ runId: this.runId,
138
+ conversationId: this.conversationId,
139
+ event,
140
+ });
141
+ }
142
+ }
@@ -0,0 +1,334 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { validatePanelApiJsonlUrl } from '@bbigbang/protocol';
3
+ import { WorkspaceFsError } from './workspaceFs.js';
4
+ const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
5
+ const MAX_ALLOWED_BYTES = 5 * 1024 * 1024;
6
+ const REQUEST_TIMEOUT_MS = 5_000;
7
+ export async function fetchLoopbackHttpText(url, options = {}) {
8
+ const parsed = parseAllowedPanelHttpUrl(url, {
9
+ allowedOrigins: options.allowedOrigins ?? [],
10
+ allowedUrlPrefixes: options.allowedUrlPrefixes ?? [],
11
+ });
12
+ const maxBytes = normalizeMaxBytes(options.maxBytes);
13
+ const range = normalizeRange(options.rangeStart, options.rangeLength);
14
+ const controller = new AbortController();
15
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
16
+ try {
17
+ const response = await fetch(parsed.toString(), {
18
+ method: 'GET',
19
+ redirect: 'error',
20
+ signal: controller.signal,
21
+ headers: {
22
+ Accept: 'application/x-ndjson, application/jsonl, application/json, text/plain;q=0.9',
23
+ ...(options.authHeaders ?? {}),
24
+ ...(typeof options.ifNoneMatch === 'string' && options.ifNoneMatch.trim()
25
+ ? { 'If-None-Match': options.ifNoneMatch.trim() }
26
+ : {}),
27
+ ...(typeof options.ifModifiedSince === 'string' && options.ifModifiedSince.trim()
28
+ ? { 'If-Modified-Since': options.ifModifiedSince.trim() }
29
+ : {}),
30
+ ...(typeof options.ifRange === 'string' && options.ifRange.trim()
31
+ ? { 'If-Range': options.ifRange.trim() }
32
+ : {}),
33
+ ...(range ? { Range: `bytes=${range.start}-${range.end}` } : {}),
34
+ },
35
+ });
36
+ const etag = response.headers.get('etag');
37
+ const lastModified = response.headers.get('last-modified');
38
+ if (response.status === 304) {
39
+ return {
40
+ url: parsed.toString(),
41
+ content: '',
42
+ status: response.status,
43
+ contentType: response.headers.get('content-type'),
44
+ size: 0,
45
+ notModified: true,
46
+ etag,
47
+ lastModified,
48
+ };
49
+ }
50
+ if (!response.ok) {
51
+ throw new WorkspaceFsError('io_error', `HTTP endpoint returned ${response.status}.`);
52
+ }
53
+ const rangeInfo = response.status === 206
54
+ ? parseContentRange(response.headers.get('content-range'))
55
+ : null;
56
+ const contentType = response.headers.get('content-type');
57
+ if (!isAllowedTextContentType(contentType)) {
58
+ throw new WorkspaceFsError('binary_file', 'HTTP endpoint must return JSON or text content.');
59
+ }
60
+ const declaredLength = Number(response.headers.get('content-length') ?? NaN);
61
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
62
+ throw new WorkspaceFsError('file_too_large', `HTTP response exceeds limit (${maxBytes} bytes).`);
63
+ }
64
+ const bytes = await readResponseBytes(response, maxBytes);
65
+ return {
66
+ url: parsed.toString(),
67
+ content: new TextDecoder('utf-8', { fatal: false }).decode(bytes),
68
+ status: response.status,
69
+ contentType,
70
+ size: bytes.byteLength,
71
+ ...(rangeInfo
72
+ ? {
73
+ partialContent: true,
74
+ rangeStart: rangeInfo.start,
75
+ rangeEnd: rangeInfo.end,
76
+ totalSize: rangeInfo.totalSize,
77
+ hasMore: rangeInfo.totalSize == null ? bytes.byteLength >= maxBytes : rangeInfo.end + 1 < rangeInfo.totalSize,
78
+ }
79
+ : range
80
+ ? { partialContent: false }
81
+ : {}),
82
+ etag,
83
+ lastModified,
84
+ };
85
+ }
86
+ catch (error) {
87
+ if (error instanceof WorkspaceFsError)
88
+ throw error;
89
+ if (error?.name === 'AbortError') {
90
+ throw new WorkspaceFsError('io_error', 'HTTP endpoint request timed out.');
91
+ }
92
+ throw new WorkspaceFsError('io_error', String(error?.message ?? error));
93
+ }
94
+ finally {
95
+ clearTimeout(timeout);
96
+ }
97
+ }
98
+ export async function proxyWorkspaceServiceHttp(url, options = {}) {
99
+ const parsed = await parseWorkspaceServiceUrl(url, options.lookupHostname);
100
+ const method = normalizeWorkspaceServiceMethod(options.method);
101
+ const maxBytes = normalizeMaxBytes(options.maxBytes);
102
+ const headers = normalizeWorkspaceServiceHeaders(options.headers);
103
+ const controller = new AbortController();
104
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
105
+ try {
106
+ const response = await fetch(parsed.toString(), {
107
+ method,
108
+ redirect: 'error',
109
+ signal: controller.signal,
110
+ headers: {
111
+ Accept: 'application/json, text/plain;q=0.9',
112
+ ...headers,
113
+ },
114
+ ...(method === 'GET' || method === 'DELETE' ? {} : { body: options.body ?? '' }),
115
+ });
116
+ const contentType = response.headers.get('content-type');
117
+ if (!isAllowedTextContentType(contentType)) {
118
+ throw new WorkspaceFsError('binary_file', 'Workspace service response must be JSON or text content.');
119
+ }
120
+ const declaredLength = Number(response.headers.get('content-length') ?? NaN);
121
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
122
+ throw new WorkspaceFsError('file_too_large', `Workspace service response exceeds limit (${maxBytes} bytes).`);
123
+ }
124
+ const bytes = await readResponseBytes(response, maxBytes);
125
+ return {
126
+ url: parsed.toString(),
127
+ method,
128
+ status: response.status,
129
+ body: new TextDecoder('utf-8', { fatal: false }).decode(bytes),
130
+ contentType,
131
+ size: bytes.byteLength,
132
+ };
133
+ }
134
+ catch (error) {
135
+ if (error instanceof WorkspaceFsError)
136
+ throw error;
137
+ if (error?.name === 'AbortError') {
138
+ throw new WorkspaceFsError('io_error', 'Workspace service proxy request timed out.');
139
+ }
140
+ throw new WorkspaceFsError('io_error', String(error?.message ?? error));
141
+ }
142
+ finally {
143
+ clearTimeout(timeout);
144
+ }
145
+ }
146
+ function parseAllowedPanelHttpUrl(rawUrl, options) {
147
+ const validation = validatePanelApiJsonlUrl(rawUrl, options);
148
+ if (!validation.ok) {
149
+ const code = validation.reason.includes('valid URL') || validation.reason.includes('control characters')
150
+ ? 'invalid_request'
151
+ : 'path_outside_workspace';
152
+ throw new WorkspaceFsError(code, formatHttpEndpointValidationReason(validation.reason));
153
+ }
154
+ return new URL(validation.url);
155
+ }
156
+ async function parseWorkspaceServiceUrl(rawUrl, lookupHostname = resolveHostnameAddresses) {
157
+ let parsed;
158
+ try {
159
+ parsed = new URL(rawUrl);
160
+ }
161
+ catch {
162
+ throw new WorkspaceFsError('invalid_request', 'Workspace service URL must be a valid URL.');
163
+ }
164
+ if (parsed.username || parsed.password) {
165
+ throw new WorkspaceFsError('path_outside_workspace', 'Workspace service URL must not include credentials.');
166
+ }
167
+ if (parsed.protocol !== 'http:') {
168
+ throw new WorkspaceFsError('path_outside_workspace', 'Workspace service proxy only supports http loopback URLs.');
169
+ }
170
+ const hostname = parsed.hostname.toLowerCase();
171
+ if (hostname === 'localhost') {
172
+ const addresses = await lookupHostname(hostname);
173
+ if (addresses.length === 0 || addresses.some((address) => !isLoopbackAddress(address))) {
174
+ throw new WorkspaceFsError('path_outside_workspace', 'Workspace service localhost must resolve only to loopback addresses.');
175
+ }
176
+ if (!parsed.port) {
177
+ throw new WorkspaceFsError('invalid_request', 'Workspace service URL must include an explicit port.');
178
+ }
179
+ return parsed;
180
+ }
181
+ if (!isLoopbackAddress(hostname)) {
182
+ throw new WorkspaceFsError('path_outside_workspace', 'Workspace service URL must target localhost, 127.0.0.1, or ::1.');
183
+ }
184
+ if (!parsed.port) {
185
+ throw new WorkspaceFsError('invalid_request', 'Workspace service URL must include an explicit port.');
186
+ }
187
+ return parsed;
188
+ }
189
+ async function resolveHostnameAddresses(hostname) {
190
+ const entries = await lookup(hostname, { all: true, verbatim: true });
191
+ return entries.map((entry) => entry.address);
192
+ }
193
+ function isLoopbackAddress(hostname) {
194
+ const normalized = hostname.replace(/^\[/u, '').replace(/\]$/u, '').toLowerCase();
195
+ return normalized === '::1' || normalized.startsWith('127.');
196
+ }
197
+ function normalizeWorkspaceServiceMethod(method) {
198
+ if (method === undefined)
199
+ return 'GET';
200
+ if (method === 'GET' || method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
201
+ return method;
202
+ }
203
+ throw new WorkspaceFsError('invalid_request', 'Workspace service proxy method is not supported.');
204
+ }
205
+ function normalizeWorkspaceServiceHeaders(headers) {
206
+ if (!headers)
207
+ return {};
208
+ const normalized = {};
209
+ for (const [rawName, rawValue] of Object.entries(headers)) {
210
+ const name = rawName.trim().toLowerCase();
211
+ if (!name || /[\r\n:]/u.test(name)) {
212
+ throw new WorkspaceFsError('invalid_request', 'Workspace service proxy header name is invalid.');
213
+ }
214
+ if (isForbiddenWorkspaceServiceHeader(name)) {
215
+ throw new WorkspaceFsError('invalid_request', `Workspace service proxy cannot forward header "${rawName}".`);
216
+ }
217
+ if (typeof rawValue !== 'string' || /[\r\n]/u.test(rawValue)) {
218
+ throw new WorkspaceFsError('invalid_request', `Workspace service proxy header "${rawName}" value is invalid.`);
219
+ }
220
+ normalized[rawName] = rawValue;
221
+ }
222
+ return normalized;
223
+ }
224
+ function isForbiddenWorkspaceServiceHeader(name) {
225
+ return name === 'authorization'
226
+ || name === 'cookie'
227
+ || name === 'forwarded'
228
+ || name === 'host'
229
+ || name === 'connection'
230
+ || name === 'content-length'
231
+ || name === 'transfer-encoding'
232
+ || name === 'x-real-ip'
233
+ || name === 'x-forwarded-for'
234
+ || name === 'x-forwarded-host'
235
+ || name === 'x-forwarded-port'
236
+ || name === 'x-forwarded-proto'
237
+ || name === 'x-forwarded-protocol'
238
+ || name === 'x-forwarded-server'
239
+ || name === 'x-forwarded-ssl'
240
+ || name === 'proxy-authorization'
241
+ || name === 'proxy-authenticate'
242
+ || name.startsWith('proxy-')
243
+ || name.startsWith('x-forwarded-')
244
+ || name.startsWith('sec-');
245
+ }
246
+ function formatHttpEndpointValidationReason(reason) {
247
+ return reason
248
+ .replace(/^api_jsonl url/u, 'HTTP endpoint URL')
249
+ .replace(/^api_jsonl endpoints/u, 'HTTP endpoint')
250
+ .replace(/^api_jsonl only supports/u, 'HTTP endpoint only supports');
251
+ }
252
+ function normalizeMaxBytes(value) {
253
+ if (value === undefined)
254
+ return DEFAULT_MAX_BYTES;
255
+ if (!Number.isInteger(value) || value < 1 || value > MAX_ALLOWED_BYTES) {
256
+ throw new WorkspaceFsError('invalid_request', `maxBytes must be between 1 and ${MAX_ALLOWED_BYTES}.`);
257
+ }
258
+ return value;
259
+ }
260
+ function normalizeRange(rangeStart, rangeLength) {
261
+ if (rangeStart === undefined && rangeLength === undefined)
262
+ return null;
263
+ if (typeof rangeStart !== 'number'
264
+ || typeof rangeLength !== 'number'
265
+ || !Number.isInteger(rangeStart)
266
+ || !Number.isInteger(rangeLength)
267
+ || rangeStart < 0
268
+ || rangeLength <= 0
269
+ || rangeLength > MAX_ALLOWED_BYTES) {
270
+ throw new WorkspaceFsError('invalid_request', `rangeStart/rangeLength must describe a byte range with length 1-${MAX_ALLOWED_BYTES}.`);
271
+ }
272
+ return {
273
+ start: rangeStart,
274
+ end: rangeStart + rangeLength - 1,
275
+ };
276
+ }
277
+ function parseContentRange(raw) {
278
+ if (!raw)
279
+ return null;
280
+ const match = /^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/iu.exec(raw.trim());
281
+ if (!match)
282
+ return null;
283
+ const start = Number(match[1]);
284
+ const end = Number(match[2]);
285
+ const totalSize = match[3] === '*' ? null : Number(match[3]);
286
+ if (!Number.isSafeInteger(start)
287
+ || !Number.isSafeInteger(end)
288
+ || end < start
289
+ || (totalSize !== null && (!Number.isSafeInteger(totalSize) || totalSize < end + 1))) {
290
+ return null;
291
+ }
292
+ return { start, end, totalSize };
293
+ }
294
+ function isAllowedTextContentType(contentType) {
295
+ if (!contentType)
296
+ return true;
297
+ const normalized = contentType.toLowerCase();
298
+ return normalized.includes('json')
299
+ || normalized.startsWith('text/')
300
+ || normalized.includes('x-ndjson');
301
+ }
302
+ async function readResponseBytes(response, maxBytes) {
303
+ if (!response.body) {
304
+ return new Uint8Array(await response.arrayBuffer());
305
+ }
306
+ const reader = response.body.getReader();
307
+ const chunks = [];
308
+ let total = 0;
309
+ while (true) {
310
+ const { done, value } = await reader.read();
311
+ if (done)
312
+ break;
313
+ if (!value)
314
+ continue;
315
+ total += value.byteLength;
316
+ if (total > maxBytes) {
317
+ try {
318
+ await reader.cancel();
319
+ }
320
+ catch {
321
+ // ignore cancellation errors after we have already decided to fail
322
+ }
323
+ throw new WorkspaceFsError('file_too_large', `HTTP response exceeds limit (${maxBytes} bytes).`);
324
+ }
325
+ chunks.push(value);
326
+ }
327
+ const merged = new Uint8Array(total);
328
+ let offset = 0;
329
+ for (const chunk of chunks) {
330
+ merged.set(chunk, offset);
331
+ offset += chunk.byteLength;
332
+ }
333
+ return merged;
334
+ }
@@ -0,0 +1,62 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { listRuntimeDrivers, } from '@bbigbang/protocol';
3
+ const CODEX_APP_SERVER_PROBE_TIMEOUT_MS = 10_000;
4
+ export function resolveAvailableRuntimeDrivers(params = {}) {
5
+ const runtimeDrivers = [];
6
+ const unavailableRuntimeDrivers = [];
7
+ for (const driver of listRuntimeDrivers()) {
8
+ const reason = getDriverUnavailableReason(driver, params);
9
+ if (reason) {
10
+ unavailableRuntimeDrivers.push({
11
+ agentType: driver.agentType,
12
+ reason,
13
+ });
14
+ continue;
15
+ }
16
+ runtimeDrivers.push(driver);
17
+ }
18
+ return {
19
+ runtimeDrivers,
20
+ unavailableRuntimeDrivers,
21
+ };
22
+ }
23
+ function getDriverUnavailableReason(driver, params) {
24
+ if (driver.agentType !== 'codex_app_server') {
25
+ return null;
26
+ }
27
+ const probe = probeCodexAppServer({
28
+ command: driver.command,
29
+ args: driver.args,
30
+ spawnSyncImpl: params.spawnSyncImpl,
31
+ env: params.env,
32
+ timeoutMs: params.timeoutMs,
33
+ });
34
+ return probe.available ? null : (probe.reason ?? 'launch probe failed.');
35
+ }
36
+ function probeCodexAppServer(params) {
37
+ const spawnSyncImpl = params.spawnSyncImpl ?? spawnSync;
38
+ const result = spawnSyncImpl(params.command, [...params.args, '--help'], {
39
+ env: params.env ?? process.env,
40
+ stdio: 'ignore',
41
+ timeout: params.timeoutMs ?? CODEX_APP_SERVER_PROBE_TIMEOUT_MS,
42
+ });
43
+ if (result.error) {
44
+ return {
45
+ available: false,
46
+ reason: result.error.message || 'launch probe failed.',
47
+ };
48
+ }
49
+ if (typeof result.status === 'number' && result.status !== 0) {
50
+ return {
51
+ available: false,
52
+ reason: `probe exited with status ${result.status}.`,
53
+ };
54
+ }
55
+ if (result.signal) {
56
+ return {
57
+ available: false,
58
+ reason: `probe exited via signal ${result.signal}.`,
59
+ };
60
+ }
61
+ return { available: true };
62
+ }