@atolis-hq/wake 0.3.62 → 0.3.64
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.
|
@@ -1,34 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// Agent CLIs can emit arbitrarily large machine-readable transcripts.
|
|
3
|
-
//
|
|
4
|
-
// below the resident's heap limit and surface the existing output-limit failure.
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
// Agent CLIs can emit arbitrarily large machine-readable transcripts. Capture
|
|
3
|
+
// raw bytes ourselves so overflow never enters a third-party string buffer.
|
|
5
4
|
const maximumCapturedProcessOutputBytes = 1024 * 1024;
|
|
6
5
|
export function runProcess(command, args, cwd, signal, timeoutMs) {
|
|
7
|
-
const child =
|
|
6
|
+
const child = spawn(command, args, {
|
|
8
7
|
...(cwd === undefined ? {} : { cwd }),
|
|
9
8
|
shell: false,
|
|
10
|
-
|
|
11
|
-
stdout: 'pipe',
|
|
12
|
-
stderr: 'pipe',
|
|
13
|
-
maxBuffer: maximumCapturedProcessOutputBytes,
|
|
14
|
-
cancelSignal: signal,
|
|
15
|
-
...(timeoutMs === undefined ? {} : { timeout: timeoutMs }),
|
|
16
|
-
reject: false,
|
|
17
|
-
stripFinalNewline: false,
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
18
10
|
});
|
|
11
|
+
const result = captureProcessOutput(child, signal, timeoutMs);
|
|
19
12
|
return {
|
|
20
|
-
result
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
timedOut: result.timedOut,
|
|
25
|
-
...(result.isMaxBuffer
|
|
26
|
-
? {
|
|
27
|
-
failureKind: 'output-limit',
|
|
28
|
-
...(result.shortMessage === undefined ? {} : { failureMessage: result.shortMessage }),
|
|
29
|
-
}
|
|
30
|
-
: {}),
|
|
31
|
-
})),
|
|
32
|
-
cancel: async () => void child.kill(),
|
|
13
|
+
result,
|
|
14
|
+
cancel: async () => {
|
|
15
|
+
terminate(child);
|
|
16
|
+
},
|
|
33
17
|
};
|
|
34
18
|
}
|
|
19
|
+
function captureProcessOutput(child, signal, timeoutMs) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const stdout = [];
|
|
22
|
+
const stderr = [];
|
|
23
|
+
let capturedBytes = 0;
|
|
24
|
+
let timedOut = false;
|
|
25
|
+
let overflowed = false;
|
|
26
|
+
let error;
|
|
27
|
+
const terminateForOverflow = () => {
|
|
28
|
+
overflowed = true;
|
|
29
|
+
child.stdout?.destroy();
|
|
30
|
+
child.stderr?.destroy();
|
|
31
|
+
terminate(child);
|
|
32
|
+
};
|
|
33
|
+
const capture = (destination) => (chunk) => {
|
|
34
|
+
if (overflowed)
|
|
35
|
+
return;
|
|
36
|
+
const remaining = maximumCapturedProcessOutputBytes - capturedBytes;
|
|
37
|
+
if (remaining <= 0 || chunk.length > remaining) {
|
|
38
|
+
if (remaining > 0)
|
|
39
|
+
destination.push(chunk.subarray(0, remaining));
|
|
40
|
+
capturedBytes = maximumCapturedProcessOutputBytes;
|
|
41
|
+
terminateForOverflow();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
destination.push(chunk);
|
|
45
|
+
capturedBytes += chunk.length;
|
|
46
|
+
};
|
|
47
|
+
child.stdout?.on('data', capture(stdout));
|
|
48
|
+
child.stderr?.on('data', capture(stderr));
|
|
49
|
+
const timeout = timeoutMs === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: setTimeout(() => {
|
|
52
|
+
timedOut = true;
|
|
53
|
+
terminate(child);
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
const onAbort = () => terminate(child);
|
|
56
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
57
|
+
child.once('error', (caught) => {
|
|
58
|
+
error = caught;
|
|
59
|
+
});
|
|
60
|
+
child.once('close', (exitCode) => {
|
|
61
|
+
if (timeout !== undefined)
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
signal.removeEventListener('abort', onAbort);
|
|
64
|
+
resolve({
|
|
65
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
66
|
+
stderr: error?.message ?? Buffer.concat(stderr).toString('utf8'),
|
|
67
|
+
exitCode: exitCode ?? undefined,
|
|
68
|
+
timedOut,
|
|
69
|
+
...(overflowed
|
|
70
|
+
? {
|
|
71
|
+
failureKind: 'output-limit',
|
|
72
|
+
failureMessage: `Process output exceeded ${maximumCapturedProcessOutputBytes} bytes`,
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function terminate(child) {
|
|
80
|
+
if (!child.killed && child.exitCode === null)
|
|
81
|
+
child.kill();
|
|
82
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const defaultMaximumGitHubResponseBytes = 8 * 1024 * 1024;
|
|
2
|
+
export class GitHubResponseTooLargeError extends Error {
|
|
3
|
+
maximumBytes;
|
|
4
|
+
observedBytes;
|
|
5
|
+
constructor(maximumBytes, observedBytes) {
|
|
6
|
+
super(`GitHub response exceeded ${maximumBytes} bytes (observed ${observedBytes})`);
|
|
7
|
+
this.maximumBytes = maximumBytes;
|
|
8
|
+
this.observedBytes = observedBytes;
|
|
9
|
+
this.name = 'GitHubResponseTooLargeError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function createBoundedGitHubFetch(baseFetch = globalThis.fetch, maximumResponseBytes = defaultMaximumGitHubResponseBytes) {
|
|
13
|
+
return async (input, init) => {
|
|
14
|
+
const response = await baseFetch(input, init);
|
|
15
|
+
const declaredBytes = contentLength(response);
|
|
16
|
+
if (declaredBytes !== undefined && declaredBytes > maximumResponseBytes) {
|
|
17
|
+
await response.body?.cancel();
|
|
18
|
+
throw new GitHubResponseTooLargeError(maximumResponseBytes, declaredBytes);
|
|
19
|
+
}
|
|
20
|
+
return new Proxy(response, {
|
|
21
|
+
get(target, property) {
|
|
22
|
+
if (property === 'text')
|
|
23
|
+
return async () => decode(await readBoundedBody(target, maximumResponseBytes));
|
|
24
|
+
if (property === 'arrayBuffer')
|
|
25
|
+
return async () => (await readBoundedBody(target, maximumResponseBytes)).buffer;
|
|
26
|
+
if (property === 'json')
|
|
27
|
+
return async () => JSON.parse(decode(await readBoundedBody(target, maximumResponseBytes)));
|
|
28
|
+
const value = Reflect.get(target, property, target);
|
|
29
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function contentLength(response) {
|
|
35
|
+
const value = response.headers.get('content-length');
|
|
36
|
+
if (value === null)
|
|
37
|
+
return undefined;
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
40
|
+
}
|
|
41
|
+
async function readBoundedBody(response, maximumBytes) {
|
|
42
|
+
if (response.body === null)
|
|
43
|
+
return new Uint8Array();
|
|
44
|
+
const reader = response.body.getReader();
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let observedBytes = 0;
|
|
47
|
+
try {
|
|
48
|
+
while (true) {
|
|
49
|
+
const chunk = await reader.read();
|
|
50
|
+
if (chunk.done)
|
|
51
|
+
return concatChunks(chunks, observedBytes);
|
|
52
|
+
observedBytes += chunk.value.byteLength;
|
|
53
|
+
if (observedBytes > maximumBytes) {
|
|
54
|
+
await reader.cancel();
|
|
55
|
+
throw new GitHubResponseTooLargeError(maximumBytes, observedBytes);
|
|
56
|
+
}
|
|
57
|
+
chunks.push(chunk.value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
reader.releaseLock();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function decode(bytes) {
|
|
65
|
+
return new TextDecoder().decode(bytes);
|
|
66
|
+
}
|
|
67
|
+
function concatChunks(chunks, length) {
|
|
68
|
+
const body = new Uint8Array(length);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const chunk of chunks) {
|
|
71
|
+
body.set(chunk, offset);
|
|
72
|
+
offset += chunk.byteLength;
|
|
73
|
+
}
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Octokit } from '@octokit/rest';
|
|
2
2
|
import { MergeMethod, ProviderPermission, PullRequestState } from '../../../activities/index.js';
|
|
3
3
|
import { GitHubOutboundAction } from '../contracts/vocabulary.js';
|
|
4
|
+
import { createBoundedGitHubFetch } from './bounded-fetch.js';
|
|
4
5
|
import { branch, getCombinedStatusForRef, getIssueLabels, getPullRequest, listCheckRunsForRef, listIssueComments, listIssues, listPullRequestFiles, listPullRequests, listReviewComments, listReviews, } from './client-reads.js';
|
|
5
6
|
import { createEtagCache } from './etag-cache.js';
|
|
6
7
|
// Octokit's request-log plugin reports every non-2xx response through this
|
|
@@ -29,6 +30,7 @@ function logGitHubRequestFailure(message) {
|
|
|
29
30
|
export function createGitHubClient(token) {
|
|
30
31
|
const octokit = new Octokit({
|
|
31
32
|
auth: token,
|
|
33
|
+
request: { fetch: createBoundedGitHubFetch() },
|
|
32
34
|
log: { debug() { }, info() { }, warn() { }, error: logGitHubRequestFailure },
|
|
33
35
|
});
|
|
34
36
|
const cache = createEtagCache();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolis-hq/wake",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.64",
|
|
4
4
|
"description": "Local autonomous agent control plane for software development",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -73,7 +73,6 @@
|
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@octokit/rest": "^22.0.0",
|
|
75
75
|
"cron-parser": "^5.10.0",
|
|
76
|
-
"execa": "^10.0.1",
|
|
77
76
|
"handlebars": "^4.7.9",
|
|
78
77
|
"ulid": "^3.0.2",
|
|
79
78
|
"yaml": "^2.9.0",
|