@bridge4dev/runner 0.11.0 → 0.22.1
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/adapters/claude.d.ts +15 -7
- package/dist/adapters/claude.js +1024 -70
- package/dist/adapters/codex.d.ts +18 -3
- package/dist/adapters/codex.js +224 -65
- package/dist/adapters/questions.d.ts +42 -0
- package/dist/adapters/questions.js +86 -0
- package/dist/adapters/types.d.ts +200 -4
- package/dist/attachments.d.ts +8 -1
- package/dist/attachments.js +22 -4
- package/dist/auto-resume.d.ts +18 -0
- package/dist/auto-resume.js +104 -0
- package/dist/commit-message.d.ts +51 -0
- package/dist/commit-message.js +224 -0
- package/dist/config.d.ts +29 -6
- package/dist/config.js +15 -0
- package/dist/crash-note.d.ts +54 -0
- package/dist/crash-note.js +105 -0
- package/dist/git.d.ts +71 -0
- package/dist/git.js +207 -10
- package/dist/gitops.d.ts +489 -12
- package/dist/gitops.js +1717 -96
- package/dist/index.js +435 -32
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +34 -0
- package/dist/policy.d.ts +63 -0
- package/dist/policy.js +412 -10
- package/dist/protocol.d.ts +382 -60
- package/dist/protocol.js +104 -1
- package/dist/recipe-schema.d.ts +310 -0
- package/dist/recipe-schema.js +103 -0
- package/dist/recipe.d.ts +94 -0
- package/dist/recipe.js +238 -0
- package/dist/self-update.d.ts +7 -0
- package/dist/self-update.js +171 -23
- package/dist/service-unit.d.ts +79 -0
- package/dist/service-unit.js +211 -0
- package/dist/supervisor.d.ts +108 -1
- package/dist/supervisor.js +1010 -56
- package/dist/verify-queue.d.ts +17 -0
- package/dist/verify-queue.js +100 -0
- package/dist/verify.d.ts +203 -0
- package/dist/verify.js +788 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VerifyReport } from './verify.js';
|
|
2
|
+
export declare class VerifyReportQueue {
|
|
3
|
+
private readonly file;
|
|
4
|
+
private entries;
|
|
5
|
+
constructor(dir?: string);
|
|
6
|
+
private load;
|
|
7
|
+
private persist;
|
|
8
|
+
/** Record a verdict. Re-queuing the same run replaces it rather than doubling. */
|
|
9
|
+
add(report: VerifyReport, now?: number): void;
|
|
10
|
+
/** Verdicts still waiting for an ack, oldest first. */
|
|
11
|
+
pending(now?: number): VerifyReport[];
|
|
12
|
+
markAttempted(runId: string): void;
|
|
13
|
+
/** The API stored it — stop redelivering. */
|
|
14
|
+
ack(runId: string): boolean;
|
|
15
|
+
get size(): number;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=verify-queue.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { stateDir } from './paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* Verdicts that have not been acknowledged yet (session 14).
|
|
6
|
+
*
|
|
7
|
+
* A build runs for minutes; a relayed command lives for seconds. So the verdict
|
|
8
|
+
* does not travel as a command answer — it is its own frame, and this queue is
|
|
9
|
+
* what makes that frame safe to lose. Three things can eat it: a closed browser
|
|
10
|
+
* tab, a runner that reconnects mid-build, and an API deployed BEFORE this
|
|
11
|
+
* release, which drops a frame type it has never heard of without a word.
|
|
12
|
+
*
|
|
13
|
+
* The last one is the reason for the acknowledgement rather than fire-and-
|
|
14
|
+
* forget: on an older API the report is simply never acked, stays here, and is
|
|
15
|
+
* delivered when the API catches up. Risk 7 of the session plan, and the one
|
|
16
|
+
* that would rot unnoticed without a test.
|
|
17
|
+
*/
|
|
18
|
+
/** More than this and the oldest goes — a queue is not an archive. */
|
|
19
|
+
const MAX_QUEUED = 50;
|
|
20
|
+
/** A verdict nobody could deliver in a week is not going to be delivered. */
|
|
21
|
+
const MAX_AGE_MS = 7 * 24 * 3_600_000;
|
|
22
|
+
export class VerifyReportQueue {
|
|
23
|
+
file;
|
|
24
|
+
entries = [];
|
|
25
|
+
constructor(dir = path.join(stateDir(), 'verify')) {
|
|
26
|
+
this.file = path.join(dir, 'reports.ndjson');
|
|
27
|
+
this.load(dir);
|
|
28
|
+
}
|
|
29
|
+
load(dir) {
|
|
30
|
+
try {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
const raw = fs.readFileSync(this.file, 'utf8');
|
|
33
|
+
for (const line of raw.split('\n')) {
|
|
34
|
+
if (!line.trim())
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(line);
|
|
38
|
+
if (parsed?.report?.runId)
|
|
39
|
+
this.entries.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// A torn tail write after a crash — the rest of the file is still good.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// No queue yet, or an unreadable state dir. Neither is fatal.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
persist() {
|
|
51
|
+
try {
|
|
52
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 });
|
|
53
|
+
const body = this.entries.map((entry) => JSON.stringify(entry)).join('\n');
|
|
54
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
55
|
+
fs.writeFileSync(tmp, body ? `${body}\n` : '', { mode: 0o600 });
|
|
56
|
+
fs.renameSync(tmp, this.file);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Losing the file costs redelivery, never correctness: the API is the
|
|
60
|
+
// one that decides what a run's final status is.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Record a verdict. Re-queuing the same run replaces it rather than doubling. */
|
|
64
|
+
add(report, now = Date.now()) {
|
|
65
|
+
this.entries = this.entries.filter((entry) => entry.report.runId !== report.runId);
|
|
66
|
+
this.entries.push({ report, queuedAt: now, attempts: 0 });
|
|
67
|
+
if (this.entries.length > MAX_QUEUED) {
|
|
68
|
+
this.entries = this.entries.slice(this.entries.length - MAX_QUEUED);
|
|
69
|
+
}
|
|
70
|
+
this.persist();
|
|
71
|
+
}
|
|
72
|
+
/** Verdicts still waiting for an ack, oldest first. */
|
|
73
|
+
pending(now = Date.now()) {
|
|
74
|
+
const before = this.entries.length;
|
|
75
|
+
this.entries = this.entries.filter((entry) => now - entry.queuedAt < MAX_AGE_MS);
|
|
76
|
+
if (this.entries.length !== before)
|
|
77
|
+
this.persist();
|
|
78
|
+
return this.entries.map((entry) => entry.report);
|
|
79
|
+
}
|
|
80
|
+
markAttempted(runId) {
|
|
81
|
+
const entry = this.entries.find((item) => item.report.runId === runId);
|
|
82
|
+
if (!entry)
|
|
83
|
+
return;
|
|
84
|
+
entry.attempts += 1;
|
|
85
|
+
this.persist();
|
|
86
|
+
}
|
|
87
|
+
/** The API stored it — stop redelivering. */
|
|
88
|
+
ack(runId) {
|
|
89
|
+
const before = this.entries.length;
|
|
90
|
+
this.entries = this.entries.filter((entry) => entry.report.runId !== runId);
|
|
91
|
+
if (this.entries.length === before)
|
|
92
|
+
return false;
|
|
93
|
+
this.persist();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
get size() {
|
|
97
|
+
return this.entries.length;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=verify-queue.js.map
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { type ProjectRecipe, type RecipeStepName } from './recipe-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Running an approved project recipe (session 14).
|
|
4
|
+
*
|
|
5
|
+
* This is the first thing DevBridge executes on a user's machine that is not an
|
|
6
|
+
* agent, so every constraint is here and none of them is optional:
|
|
7
|
+
*
|
|
8
|
+
* - **Only an approved recipe.** The caller sends the fingerprint of the copy a
|
|
9
|
+
* human approved; a run whose recipe hashes differently is refused. An
|
|
10
|
+
* approval is an approval of specific commands.
|
|
11
|
+
* - **Layer 1 still applies.** Every command goes through the same denylist as
|
|
12
|
+
* the agent's Bash tool. An approved recipe does not ASK — there is nobody to
|
|
13
|
+
* ask during a twenty-minute build — but it cannot get past a refusal.
|
|
14
|
+
* - **We choose the working directory**, and the command cannot change it: it
|
|
15
|
+
* is passed as `cwd`, never interpolated.
|
|
16
|
+
* - **The environment is an allowlist.** The runner's own token lives in this
|
|
17
|
+
* process's environment and must not reach a build script.
|
|
18
|
+
* - **One run per machine.** A half-hour build next to three agent sessions on
|
|
19
|
+
* a small VPS is the resource story; one at a time is the answer.
|
|
20
|
+
* - **Free disk is checked before anything starts** — 2026-07-13 is the day
|
|
21
|
+
* this machine's root filled up and took sshd with it.
|
|
22
|
+
* - **The owner has a veto**: `[verify] enabled = false` in the runner's own
|
|
23
|
+
* root-owned config, and the capability is not announced at all.
|
|
24
|
+
*/
|
|
25
|
+
export type VerifyTarget = 'BASE' | 'SESSION' | 'PREVIEW';
|
|
26
|
+
export type VerifyStatus = 'RUNNING' | 'PASSED' | 'FAILED' | 'CANCELLED';
|
|
27
|
+
/** Free space a run refuses to start below. Builds are not small. */
|
|
28
|
+
export declare const VERIFY_MIN_FREE_BYTES: number;
|
|
29
|
+
export interface VerifyStepResult {
|
|
30
|
+
step: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
exitCode: number | null;
|
|
33
|
+
durationMs: number;
|
|
34
|
+
/** The step ran out of its own budget rather than failing. */
|
|
35
|
+
timedOut?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface VerifyHealthResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
/** HTTP status, or null when the request never completed. */
|
|
40
|
+
status: number | null;
|
|
41
|
+
/** The sha the running build reported, when `health.shaPath` said where. */
|
|
42
|
+
sha: string | null;
|
|
43
|
+
detail?: string;
|
|
44
|
+
}
|
|
45
|
+
/** The verdict, as it travels to the API and lands in `dev_verify_runs`. */
|
|
46
|
+
export interface VerifyReport {
|
|
47
|
+
runId: string;
|
|
48
|
+
target: VerifyTarget;
|
|
49
|
+
status: Exclude<VerifyStatus, 'RUNNING'>;
|
|
50
|
+
recipeSha: string;
|
|
51
|
+
branch: string | null;
|
|
52
|
+
commitSha: string | null;
|
|
53
|
+
dirty: boolean;
|
|
54
|
+
steps: string[];
|
|
55
|
+
completedSteps: string[];
|
|
56
|
+
failedStep: string | null;
|
|
57
|
+
exitCode: number | null;
|
|
58
|
+
durationMs: number;
|
|
59
|
+
logTail: string;
|
|
60
|
+
health: VerifyHealthResult | null;
|
|
61
|
+
previewUrl: string | null;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
finishedAt: string;
|
|
64
|
+
}
|
|
65
|
+
export interface VerifyStatusResult {
|
|
66
|
+
runId: string;
|
|
67
|
+
status: VerifyStatus;
|
|
68
|
+
target: VerifyTarget;
|
|
69
|
+
currentStep: string | null;
|
|
70
|
+
completedSteps: string[];
|
|
71
|
+
failedStep: string | null;
|
|
72
|
+
exitCode: number | null;
|
|
73
|
+
/** Byte offset the caller should ask from next time. */
|
|
74
|
+
offset: number;
|
|
75
|
+
chunk: string;
|
|
76
|
+
/** More log is already on disk than this answer carried. */
|
|
77
|
+
hasMore: boolean;
|
|
78
|
+
startedAt: string;
|
|
79
|
+
finishedAt: string | null;
|
|
80
|
+
health: VerifyHealthResult | null;
|
|
81
|
+
previewUrl: string | null;
|
|
82
|
+
}
|
|
83
|
+
export interface VerifyStartInput {
|
|
84
|
+
/** Issued by the API — it is the primary key of the row this run reports to. */
|
|
85
|
+
runId: string;
|
|
86
|
+
target: VerifyTarget;
|
|
87
|
+
/** The approved recipe, and the fingerprint it must still hash to. */
|
|
88
|
+
recipe: ProjectRecipe;
|
|
89
|
+
recipeSha: string;
|
|
90
|
+
/** Where the commands run. Ours to choose; a command cannot change it. */
|
|
91
|
+
cwd: string;
|
|
92
|
+
/** Which steps to run, in recipe order. `deploy` only when asked for. */
|
|
93
|
+
steps: RecipeStepName[];
|
|
94
|
+
branch: string | null;
|
|
95
|
+
commitSha: string | null;
|
|
96
|
+
dirty: boolean;
|
|
97
|
+
/** PREVIEW runs `preview.run` instead of the step list. */
|
|
98
|
+
preview?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* How many bytes of `buffer` end on a complete UTF-8 character.
|
|
102
|
+
*
|
|
103
|
+
* UTF-8 is self-synchronising: a lead byte announces its length, and every
|
|
104
|
+
* continuation byte is `10xxxxxx`. So the only work is to walk back from the
|
|
105
|
+
* end over at most three bytes and drop a sequence that is not finished yet.
|
|
106
|
+
*/
|
|
107
|
+
export declare function completeUtf8Length(buffer: Buffer): number;
|
|
108
|
+
/** Offset of the first byte that STARTS a character — for a slice cut anywhere. */
|
|
109
|
+
export declare function firstUtf8Start(buffer: Buffer): number;
|
|
110
|
+
/**
|
|
111
|
+
* Free bytes on the filesystem the build would write to.
|
|
112
|
+
*
|
|
113
|
+
* `null` when the platform cannot answer — which is treated as «go ahead»
|
|
114
|
+
* rather than «refuse», because refusing every run on a system without statfs
|
|
115
|
+
* would be a worse failure than the one this guards against.
|
|
116
|
+
*/
|
|
117
|
+
export declare function freeBytesFor(target: string): number | null;
|
|
118
|
+
export interface VerifyRunnerOptions {
|
|
119
|
+
/** `[verify] enabled = false` in the runner's own config is a hard veto. */
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
/** Called with each finished verdict, for delivery to the API. */
|
|
122
|
+
onReport: (report: VerifyReport) => void;
|
|
123
|
+
/** Injected in tests. */
|
|
124
|
+
now?: () => number;
|
|
125
|
+
}
|
|
126
|
+
export declare class VerifyRunner {
|
|
127
|
+
private readonly opts;
|
|
128
|
+
private active;
|
|
129
|
+
/** Finished runs stay readable until the next one starts. */
|
|
130
|
+
private last;
|
|
131
|
+
constructor(opts: VerifyRunnerOptions);
|
|
132
|
+
get enabled(): boolean;
|
|
133
|
+
get busy(): boolean;
|
|
134
|
+
/** The run currently holding the machine, if any — for a truthful refusal. */
|
|
135
|
+
get activeRunId(): string | null;
|
|
136
|
+
/** What kind of run is holding it — `preview_stop` only owns its own. */
|
|
137
|
+
get activeTarget(): VerifyTarget | null;
|
|
138
|
+
start(input: VerifyStartInput): {
|
|
139
|
+
started: boolean;
|
|
140
|
+
error?: string;
|
|
141
|
+
};
|
|
142
|
+
status(runId: string, offset?: number): VerifyStatusResult | null;
|
|
143
|
+
cancel(runId: string): {
|
|
144
|
+
cancelled: boolean;
|
|
145
|
+
error?: string;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Stop whatever is running because the daemon is going away.
|
|
149
|
+
*
|
|
150
|
+
* The run is NOT reported as passed or failed — it is simply abandoned, and
|
|
151
|
+
* the API turns its still-RUNNING row into `LOST`. A verdict nobody observed
|
|
152
|
+
* is not a verdict.
|
|
153
|
+
*/
|
|
154
|
+
shutdown(): void;
|
|
155
|
+
private now;
|
|
156
|
+
private find;
|
|
157
|
+
/**
|
|
158
|
+
* The commands this run will execute, each one already past layer 1.
|
|
159
|
+
*
|
|
160
|
+
* Resolved BEFORE anything starts, so a recipe whose `deploy` step is refused
|
|
161
|
+
* fails immediately instead of half-way through a release.
|
|
162
|
+
*/
|
|
163
|
+
private resolveCommands;
|
|
164
|
+
private execute;
|
|
165
|
+
private runStep;
|
|
166
|
+
/** SIGTERM to the whole group, SIGKILL to whatever is still there after. */
|
|
167
|
+
private killGroup;
|
|
168
|
+
/**
|
|
169
|
+
* Append to the log, masked, and stop at the cap.
|
|
170
|
+
*
|
|
171
|
+
* Masking happens per write rather than at the end, because the end may never
|
|
172
|
+
* come — a run the user cancels still leaves its log readable.
|
|
173
|
+
*/
|
|
174
|
+
private append;
|
|
175
|
+
private tail;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* One command, run once, with no log and no report — the preview `stop`.
|
|
179
|
+
*
|
|
180
|
+
* Same working directory and same environment allowlist as a step; it just has
|
|
181
|
+
* nowhere to report to, because stopping a preview is bookkeeping rather than a
|
|
182
|
+
* verdict. Layer 1 has already been consulted by the caller (this is the one
|
|
183
|
+
* command allowed to take a compose project down, and only its own).
|
|
184
|
+
*/
|
|
185
|
+
export declare function runOneOffCommand(command: string, cwd: string, timeoutSec?: number): Promise<{
|
|
186
|
+
ok: boolean;
|
|
187
|
+
exitCode: number | null;
|
|
188
|
+
output: string;
|
|
189
|
+
}>;
|
|
190
|
+
/**
|
|
191
|
+
* Ask the running application what it is.
|
|
192
|
+
*
|
|
193
|
+
* Two facts, and they are different: that it answered at all, and which build
|
|
194
|
+
* it says it is. Only the second can support «running the version we verified»,
|
|
195
|
+
* and a project that does not expose a sha simply never gets that sentence.
|
|
196
|
+
*/
|
|
197
|
+
export declare function probeHealth(health: {
|
|
198
|
+
url: string;
|
|
199
|
+
shaPath?: string;
|
|
200
|
+
}): Promise<VerifyHealthResult>;
|
|
201
|
+
/** `data.build.sha` → the string at that path, if it is a plausible sha. */
|
|
202
|
+
export declare function extractSha(body: unknown, dottedPath: string): string | null;
|
|
203
|
+
//# sourceMappingURL=verify.d.ts.map
|