@bridge4dev/runner 0.26.0 → 0.29.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/dist/adapters/claude.js +136 -8
- package/dist/adapters/codex.js +96 -2
- package/dist/adapters/types.d.ts +60 -1
- package/dist/agent-auth.d.ts +29 -0
- package/dist/agent-auth.js +136 -0
- package/dist/attachments.d.ts +27 -0
- package/dist/attachments.js +150 -8
- package/dist/auth-relay.d.ts +29 -1
- package/dist/auth-relay.js +228 -13
- package/dist/checkpoints.d.ts +175 -0
- package/dist/checkpoints.js +816 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +17 -0
- package/dist/environment.d.ts +15 -0
- package/dist/environment.js +25 -1
- package/dist/index.js +176 -10
- package/dist/journal.d.ts +34 -1
- package/dist/journal.js +51 -2
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +12 -0
- package/dist/policy.js +15 -0
- package/dist/protocol.d.ts +10 -10
- package/dist/recipe-schema.d.ts +1 -1
- package/dist/self-update.d.ts +43 -2
- package/dist/self-update.js +137 -43
- package/dist/supervisor.d.ts +86 -0
- package/dist/supervisor.js +555 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/paths.js
CHANGED
|
@@ -54,6 +54,18 @@ export function knownWorkspacesPath() {
|
|
|
54
54
|
export function previewsDir() {
|
|
55
55
|
return path.join(stateDir(), 'previews');
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Ticket #126: git object stores holding the sessions' restore points.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately under `devbridge-runner/`, and deliberately NOT under
|
|
61
|
+
* `worktrees/` or `previews/` — those two are excluded from the runner-tree
|
|
62
|
+
* rule in `SECRET_PATH_PATTERNS` (the agent has to be able to read its own
|
|
63
|
+
* workspace), so anything that must stay unreadable to the agent belongs
|
|
64
|
+
* exactly here.
|
|
65
|
+
*/
|
|
66
|
+
export function checkpointsDir() {
|
|
67
|
+
return path.join(stateDir(), 'checkpoints');
|
|
68
|
+
}
|
|
57
69
|
/**
|
|
58
70
|
* Per-session MCP config files (ticket #119).
|
|
59
71
|
*
|
package/dist/policy.js
CHANGED
|
@@ -97,6 +97,16 @@ const SECRET_PATH_PATTERNS = [
|
|
|
97
97
|
// its own workspace — caught by the existing suite the moment it was tried.
|
|
98
98
|
/(^|\/)devbridge-mcp\.[^/]*\.json$/,
|
|
99
99
|
/(^|\/)devbridge-runner(\/(?!worktrees(\/|$)|previews(\/|$))|$)/,
|
|
100
|
+
// Ticket #126: the object store holding this session's restore points — a
|
|
101
|
+
// full copy of the working tree as it was at every turn.
|
|
102
|
+
//
|
|
103
|
+
// The runner-directory rule above already covers it on an ordinary install.
|
|
104
|
+
// This one is for the case that rule cannot see: under a
|
|
105
|
+
// `DEVBRIDGE_RUNNER_HOME` override the path carries no `devbridge-runner`
|
|
106
|
+
// segment at all, which is exactly the gap the MCP-config entry was added
|
|
107
|
+
// for. Matching the distinctive `checkpoints/<hash>.git` shape closes it
|
|
108
|
+
// wherever the state directory ends up.
|
|
109
|
+
/(^|\/)checkpoints\/[0-9a-f]{8,}\.git(\/|$)/,
|
|
100
110
|
/(^|\/)(shadow|passwd|sudoers)$/,
|
|
101
111
|
/(^|\/)\.netrc$/,
|
|
102
112
|
/(^|\/)\.git-credentials$/,
|
|
@@ -440,6 +450,11 @@ const SECRET_COMMAND_PATTERNS = [
|
|
|
440
450
|
// never contains the suffix.
|
|
441
451
|
/devbridge-mcp/,
|
|
442
452
|
/devbridge-runner\/(?!worktrees|previews)/,
|
|
453
|
+
// Ticket #126 — the command form of the restore-point entry above. Written
|
|
454
|
+
// as its own rule for the same reason gotcha #110 exists: a path in one
|
|
455
|
+
// secret table and not the other is theatre, and `git --git-dir=…` /
|
|
456
|
+
// `cat …/checkpoints/<hash>.git/…` walks past a Read-tool guard entirely.
|
|
457
|
+
/checkpoints\/[0-9a-f]{8,}\.git/,
|
|
443
458
|
/devbridge-runner(?=\s|$|['"])/,
|
|
444
459
|
/\/etc\/(shadow|passwd|sudoers)/,
|
|
445
460
|
/\.git-credentials/,
|
package/dist/protocol.d.ts
CHANGED
|
@@ -126,8 +126,8 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
126
126
|
model: string | null;
|
|
127
127
|
effort: string | null;
|
|
128
128
|
prompt: string;
|
|
129
|
-
epoch: number;
|
|
130
129
|
providerSessionId: string | null;
|
|
130
|
+
epoch: number;
|
|
131
131
|
lastSeq: number;
|
|
132
132
|
costUsd: number;
|
|
133
133
|
activeMsBase: number;
|
|
@@ -179,11 +179,11 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
179
179
|
id: string;
|
|
180
180
|
title: string;
|
|
181
181
|
}[];
|
|
182
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
183
182
|
mcp?: {
|
|
184
183
|
url: string;
|
|
185
184
|
token: string;
|
|
186
185
|
} | undefined;
|
|
186
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
187
187
|
model?: string | null | undefined;
|
|
188
188
|
effort?: string | null | undefined;
|
|
189
189
|
epoch?: number | undefined;
|
|
@@ -328,8 +328,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
328
328
|
model: string | null;
|
|
329
329
|
effort: string | null;
|
|
330
330
|
prompt: string;
|
|
331
|
-
epoch: number;
|
|
332
331
|
providerSessionId: string | null;
|
|
332
|
+
epoch: number;
|
|
333
333
|
lastSeq: number;
|
|
334
334
|
costUsd: number;
|
|
335
335
|
activeMsBase: number;
|
|
@@ -381,11 +381,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
381
381
|
id: string;
|
|
382
382
|
title: string;
|
|
383
383
|
}[];
|
|
384
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
385
384
|
mcp?: {
|
|
386
385
|
url: string;
|
|
387
386
|
token: string;
|
|
388
387
|
} | undefined;
|
|
388
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
389
389
|
model?: string | null | undefined;
|
|
390
390
|
effort?: string | null | undefined;
|
|
391
391
|
epoch?: number | undefined;
|
|
@@ -410,8 +410,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
410
410
|
model: string | null;
|
|
411
411
|
effort: string | null;
|
|
412
412
|
prompt: string;
|
|
413
|
-
epoch: number;
|
|
414
413
|
providerSessionId: string | null;
|
|
414
|
+
epoch: number;
|
|
415
415
|
lastSeq: number;
|
|
416
416
|
costUsd: number;
|
|
417
417
|
activeMsBase: number;
|
|
@@ -469,11 +469,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
469
469
|
id: string;
|
|
470
470
|
title: string;
|
|
471
471
|
}[];
|
|
472
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
473
472
|
mcp?: {
|
|
474
473
|
url: string;
|
|
475
474
|
token: string;
|
|
476
475
|
} | undefined;
|
|
476
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
477
477
|
model?: string | null | undefined;
|
|
478
478
|
effort?: string | null | undefined;
|
|
479
479
|
epoch?: number | undefined;
|
|
@@ -642,8 +642,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
642
642
|
model: string | null;
|
|
643
643
|
effort: string | null;
|
|
644
644
|
prompt: string;
|
|
645
|
-
epoch: number;
|
|
646
645
|
providerSessionId: string | null;
|
|
646
|
+
epoch: number;
|
|
647
647
|
lastSeq: number;
|
|
648
648
|
costUsd: number;
|
|
649
649
|
activeMsBase: number;
|
|
@@ -695,11 +695,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
695
695
|
id: string;
|
|
696
696
|
title: string;
|
|
697
697
|
}[];
|
|
698
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
699
698
|
mcp?: {
|
|
700
699
|
url: string;
|
|
701
700
|
token: string;
|
|
702
701
|
} | undefined;
|
|
702
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
703
703
|
model?: string | null | undefined;
|
|
704
704
|
effort?: string | null | undefined;
|
|
705
705
|
epoch?: number | undefined;
|
|
@@ -722,8 +722,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
722
722
|
model: string | null;
|
|
723
723
|
effort: string | null;
|
|
724
724
|
prompt: string;
|
|
725
|
-
epoch: number;
|
|
726
725
|
providerSessionId: string | null;
|
|
726
|
+
epoch: number;
|
|
727
727
|
lastSeq: number;
|
|
728
728
|
costUsd: number;
|
|
729
729
|
activeMsBase: number;
|
|
@@ -778,11 +778,11 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
778
778
|
id: string;
|
|
779
779
|
title: string;
|
|
780
780
|
}[];
|
|
781
|
-
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
782
781
|
mcp?: {
|
|
783
782
|
url: string;
|
|
784
783
|
token: string;
|
|
785
784
|
} | undefined;
|
|
785
|
+
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
786
786
|
model?: string | null | undefined;
|
|
787
787
|
effort?: string | null | undefined;
|
|
788
788
|
epoch?: number | undefined;
|
package/dist/recipe-schema.d.ts
CHANGED
|
@@ -253,6 +253,7 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
253
253
|
shaPath?: string | undefined;
|
|
254
254
|
} | undefined;
|
|
255
255
|
}, {
|
|
256
|
+
version?: 1 | undefined;
|
|
256
257
|
preview?: {
|
|
257
258
|
run: string;
|
|
258
259
|
url?: string | undefined;
|
|
@@ -260,7 +261,6 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
260
261
|
timeoutSec?: number | undefined;
|
|
261
262
|
stop?: string | undefined;
|
|
262
263
|
} | undefined;
|
|
263
|
-
version?: 1 | undefined;
|
|
264
264
|
notes?: string | undefined;
|
|
265
265
|
steps?: {
|
|
266
266
|
verify?: {
|
package/dist/self-update.d.ts
CHANGED
|
@@ -62,6 +62,28 @@ export interface SelfUpdateOptions {
|
|
|
62
62
|
* has one above `packages/runner`.
|
|
63
63
|
*/
|
|
64
64
|
export declare function resolveInstalledPackageDir(entry?: string): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* The npm prefix the RUNNING package was installed into.
|
|
67
|
+
*
|
|
68
|
+
* This is the whole fix for the dedicated-user layout. `npm install -g` obeys
|
|
69
|
+
* npm's *configured* prefix, and `--prefix` is a per-invocation flag that npm
|
|
70
|
+
* persists nowhere — so the install the instructions prescribed for a dedicated
|
|
71
|
+
* user (`npm install -g --prefix ~/.local …`) produced a runner in
|
|
72
|
+
* `~/.local/lib/node_modules` while every later `npm install -g` aimed at
|
|
73
|
+
* `/usr`. Measured on a live server: `npm prefix -g` → `/usr`,
|
|
74
|
+
* `installIsWritable` → true (both checked directories really are the user's),
|
|
75
|
+
* and then npm dies with EACCES *inside* the update — the exact
|
|
76
|
+
* «the permissions to access this file as the current user» the owner pasted.
|
|
77
|
+
*
|
|
78
|
+
* Deriving the prefix from where we physically are is the truthful answer: it
|
|
79
|
+
* cannot disagree with reality, and it costs no subprocess. Returns null for a
|
|
80
|
+
* layout that is not a global npm install (a source checkout, a vendored copy),
|
|
81
|
+
* where the caller should fall back to `npm prefix -g`.
|
|
82
|
+
*
|
|
83
|
+
* `<prefix>/lib/node_modules/@scope/pkg` → `<prefix>`
|
|
84
|
+
* `<prefix>/lib/node_modules/pkg` → `<prefix>`
|
|
85
|
+
*/
|
|
86
|
+
export declare function installPrefixFor(packageDir?: string | null): string | null;
|
|
65
87
|
/**
|
|
66
88
|
* Can this user actually replace the installed package?
|
|
67
89
|
*
|
|
@@ -72,8 +94,11 @@ export declare function resolveInstalledPackageDir(entry?: string): string | nul
|
|
|
72
94
|
* current user» — a dashboard button that cannot work, and an error in npm's
|
|
73
95
|
* words rather than ours.
|
|
74
96
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
97
|
+
* Three directories matter, because npm rewrites all three: the package itself,
|
|
98
|
+
* the `node_modules` above it, and `<prefix>/bin`, where the command symlink
|
|
99
|
+
* lives. `bin` was missing here and is not hypothetical — a prefix whose
|
|
100
|
+
* `lib/node_modules` was handed over with chown while `bin` stayed root-owned
|
|
101
|
+
* passes the first two and still fails.
|
|
77
102
|
*/
|
|
78
103
|
export declare function installIsWritable(packageDir?: string | null): boolean;
|
|
79
104
|
/**
|
|
@@ -92,5 +117,21 @@ export declare function isSupervisedProcess(env?: NodeJS.ProcessEnv): boolean;
|
|
|
92
117
|
* never be talked into fetching its own replacement over http.
|
|
93
118
|
*/
|
|
94
119
|
export declare function isTrustedTarballUrl(tarballUrl: string, apiUrl: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The command a person can paste on the server to update this exact machine.
|
|
122
|
+
*
|
|
123
|
+
* Built by the runner rather than by the dashboard because only the runner
|
|
124
|
+
* knows the three things that make it correct: which prefix it lives in, which
|
|
125
|
+
* user it runs as, and that user's uid for `XDG_RUNTIME_DIR`. The dashboard
|
|
126
|
+
* used to assemble a generic `npm install -g <url> && systemctl --user restart`
|
|
127
|
+
* and then patch it with string surgery — which produced a command that could
|
|
128
|
+
* not work on precisely the machines that needed it.
|
|
129
|
+
*/
|
|
130
|
+
export declare function manualUpdateCommand(tarballUrl: string, options?: {
|
|
131
|
+
prefix?: string | null;
|
|
132
|
+
user?: string;
|
|
133
|
+
uid?: number;
|
|
134
|
+
packageDir?: string | null;
|
|
135
|
+
}): string;
|
|
95
136
|
export declare function selfUpdate(options: SelfUpdateOptions): Promise<SelfUpdateOutcome>;
|
|
96
137
|
//# sourceMappingURL=self-update.d.ts.map
|
package/dist/self-update.js
CHANGED
|
@@ -55,6 +55,51 @@ export function resolveInstalledPackageDir(entry = process.argv[1] ?? '') {
|
|
|
55
55
|
}
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The npm prefix the RUNNING package was installed into.
|
|
60
|
+
*
|
|
61
|
+
* This is the whole fix for the dedicated-user layout. `npm install -g` obeys
|
|
62
|
+
* npm's *configured* prefix, and `--prefix` is a per-invocation flag that npm
|
|
63
|
+
* persists nowhere — so the install the instructions prescribed for a dedicated
|
|
64
|
+
* user (`npm install -g --prefix ~/.local …`) produced a runner in
|
|
65
|
+
* `~/.local/lib/node_modules` while every later `npm install -g` aimed at
|
|
66
|
+
* `/usr`. Measured on a live server: `npm prefix -g` → `/usr`,
|
|
67
|
+
* `installIsWritable` → true (both checked directories really are the user's),
|
|
68
|
+
* and then npm dies with EACCES *inside* the update — the exact
|
|
69
|
+
* «the permissions to access this file as the current user» the owner pasted.
|
|
70
|
+
*
|
|
71
|
+
* Deriving the prefix from where we physically are is the truthful answer: it
|
|
72
|
+
* cannot disagree with reality, and it costs no subprocess. Returns null for a
|
|
73
|
+
* layout that is not a global npm install (a source checkout, a vendored copy),
|
|
74
|
+
* where the caller should fall back to `npm prefix -g`.
|
|
75
|
+
*
|
|
76
|
+
* `<prefix>/lib/node_modules/@scope/pkg` → `<prefix>`
|
|
77
|
+
* `<prefix>/lib/node_modules/pkg` → `<prefix>`
|
|
78
|
+
*/
|
|
79
|
+
export function installPrefixFor(packageDir = resolveInstalledPackageDir()) {
|
|
80
|
+
if (!packageDir)
|
|
81
|
+
return null;
|
|
82
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
83
|
+
const at = packageDir.lastIndexOf(marker);
|
|
84
|
+
if (at < 0)
|
|
85
|
+
return null;
|
|
86
|
+
const nodeModules = packageDir.slice(0, at + marker.length - 1);
|
|
87
|
+
const lib = path.dirname(nodeModules);
|
|
88
|
+
// Anything else is not the layout `npm install -g --prefix` produces, and
|
|
89
|
+
// guessing a prefix that npm would not use is worse than not guessing.
|
|
90
|
+
if (path.basename(lib) !== 'lib')
|
|
91
|
+
return null;
|
|
92
|
+
return path.dirname(lib);
|
|
93
|
+
}
|
|
94
|
+
function isWritable(target) {
|
|
95
|
+
try {
|
|
96
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
58
103
|
/**
|
|
59
104
|
* Can this user actually replace the installed package?
|
|
60
105
|
*
|
|
@@ -65,24 +110,29 @@ export function resolveInstalledPackageDir(entry = process.argv[1] ?? '') {
|
|
|
65
110
|
* current user» — a dashboard button that cannot work, and an error in npm's
|
|
66
111
|
* words rather than ours.
|
|
67
112
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
113
|
+
* Three directories matter, because npm rewrites all three: the package itself,
|
|
114
|
+
* the `node_modules` above it, and `<prefix>/bin`, where the command symlink
|
|
115
|
+
* lives. `bin` was missing here and is not hypothetical — a prefix whose
|
|
116
|
+
* `lib/node_modules` was handed over with chown while `bin` stayed root-owned
|
|
117
|
+
* passes the first two and still fails.
|
|
70
118
|
*/
|
|
71
119
|
export function installIsWritable(packageDir = resolveInstalledPackageDir()) {
|
|
72
120
|
if (!packageDir)
|
|
73
121
|
return false;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
fs.accessSync(target, fs.constants.W_OK);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
122
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
123
|
+
const at = packageDir.lastIndexOf(marker);
|
|
83
124
|
// `<prefix>/lib/node_modules/@scope/pkg` → `<prefix>/lib/node_modules`
|
|
84
|
-
const nodeModules =
|
|
85
|
-
|
|
125
|
+
const nodeModules = at >= 0 ? packageDir.slice(0, at + marker.length - 1) : path.dirname(packageDir);
|
|
126
|
+
if (!isWritable(packageDir) || !isWritable(nodeModules))
|
|
127
|
+
return false;
|
|
128
|
+
const prefix = installPrefixFor(packageDir);
|
|
129
|
+
// No `bin` yet is fine — npm creates it. An existing one we cannot write is not.
|
|
130
|
+
if (prefix) {
|
|
131
|
+
const bin = path.join(prefix, 'bin');
|
|
132
|
+
if (fs.existsSync(bin) && !isWritable(bin))
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
86
136
|
}
|
|
87
137
|
/**
|
|
88
138
|
* Is something going to restart us?
|
|
@@ -140,17 +190,23 @@ const COMMAND_NAME = 'devbridge-runner';
|
|
|
140
190
|
* command — the stable thing, referenced by a unit file we do not control —
|
|
141
191
|
* points at the new one. So we follow the command.
|
|
142
192
|
*/
|
|
143
|
-
async function commandOwner(exec) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
193
|
+
async function commandOwner(exec, installPrefix = null) {
|
|
194
|
+
// Where we actually live outranks what npm is configured to think. Under the
|
|
195
|
+
// dedicated-user layout `npm prefix -g` answers `/usr` — a directory that on
|
|
196
|
+
// such a host holds no runner at all — so asking npm first returned null and
|
|
197
|
+
// took the EEXIST rename-recovery and the unit repair down with it.
|
|
198
|
+
let prefix = installPrefix;
|
|
199
|
+
if (!prefix) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await exec('npm', ['prefix', '-g'], {
|
|
202
|
+
timeout: VERIFY_TIMEOUT_MS,
|
|
203
|
+
env: npmEnv(),
|
|
204
|
+
});
|
|
205
|
+
prefix = result.stdout.trim();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
154
210
|
}
|
|
155
211
|
if (!prefix)
|
|
156
212
|
return null;
|
|
@@ -167,13 +223,55 @@ async function commandOwner(exec) {
|
|
|
167
223
|
return null;
|
|
168
224
|
}
|
|
169
225
|
}
|
|
170
|
-
function installArgs(source) {
|
|
226
|
+
function installArgs(source, prefix) {
|
|
171
227
|
// `--ignore-scripts` matches the documented install: this package and its whole
|
|
172
228
|
// tree have no install/postinstall scripts, so nothing legitimate is skipped —
|
|
173
229
|
// and an update pulled over the network gets no chance to run anything at
|
|
174
230
|
// install time. `--loglevel=error` because npm's ERESOLVE warning about zod is
|
|
175
231
|
// expected, harmless and long enough to bury the line that matters.
|
|
176
|
-
|
|
232
|
+
//
|
|
233
|
+
// `--prefix` names the prefix the runner is ALREADY installed in. Without it
|
|
234
|
+
// npm uses its configured global prefix, which on a dedicated-user install is
|
|
235
|
+
// a directory the daemon cannot write — and, on the rarer host where it can,
|
|
236
|
+
// npm cheerfully installs a SECOND copy somewhere the service does not exec,
|
|
237
|
+
// reports success, and the runner restarts on the old version forever.
|
|
238
|
+
return [
|
|
239
|
+
'install',
|
|
240
|
+
'-g',
|
|
241
|
+
'--ignore-scripts',
|
|
242
|
+
'--loglevel=error',
|
|
243
|
+
...(prefix ? ['--prefix', prefix] : []),
|
|
244
|
+
source,
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The command a person can paste on the server to update this exact machine.
|
|
249
|
+
*
|
|
250
|
+
* Built by the runner rather than by the dashboard because only the runner
|
|
251
|
+
* knows the three things that make it correct: which prefix it lives in, which
|
|
252
|
+
* user it runs as, and that user's uid for `XDG_RUNTIME_DIR`. The dashboard
|
|
253
|
+
* used to assemble a generic `npm install -g <url> && systemctl --user restart`
|
|
254
|
+
* and then patch it with string surgery — which produced a command that could
|
|
255
|
+
* not work on precisely the machines that needed it.
|
|
256
|
+
*/
|
|
257
|
+
export function manualUpdateCommand(tarballUrl, options = {}) {
|
|
258
|
+
const packageDir = options.packageDir === undefined ? resolveInstalledPackageDir() : options.packageDir;
|
|
259
|
+
const prefix = options.prefix === undefined ? installPrefixFor(packageDir) : options.prefix;
|
|
260
|
+
const user = options.user ?? os.userInfo().username;
|
|
261
|
+
const uid = options.uid ?? (typeof process.getuid === 'function' ? process.getuid() : -1);
|
|
262
|
+
const install = ['npm install -g --ignore-scripts --loglevel=error']
|
|
263
|
+
.concat(prefix ? [`--prefix ${prefix}`] : [])
|
|
264
|
+
.concat([tarballUrl])
|
|
265
|
+
.join(' ');
|
|
266
|
+
const restart = 'systemctl --user restart devbridge-runner';
|
|
267
|
+
// Writable by us → one line, run as ourselves. Not writable → the install
|
|
268
|
+
// half needs root and the restart half needs the daemon's own user, and
|
|
269
|
+
// «run it as root» alone is how people ended up with a new package and an
|
|
270
|
+
// old process still running.
|
|
271
|
+
if (installIsWritable(packageDir))
|
|
272
|
+
return `${install} && ${restart}`;
|
|
273
|
+
const runtime = uid >= 0 ? `/run/user/${uid}` : `/run/user/$(id -u ${user})`;
|
|
274
|
+
return `sudo ${install} && sudo -iu ${user} env XDG_RUNTIME_DIR=${runtime} ${restart}`;
|
|
177
275
|
}
|
|
178
276
|
/**
|
|
179
277
|
* Install a global package, clearing the way if a DIFFERENTLY-NAMED build of this
|
|
@@ -186,25 +284,22 @@ function installArgs(source) {
|
|
|
186
284
|
* one owner instead of two, which is also the only state the NEXT update can
|
|
187
285
|
* work from.
|
|
188
286
|
*/
|
|
189
|
-
async function installGlobal(exec, source) {
|
|
287
|
+
async function installGlobal(exec, source, prefix) {
|
|
190
288
|
try {
|
|
191
|
-
await exec('npm', installArgs(source), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
289
|
+
await exec('npm', installArgs(source, prefix), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
192
290
|
return;
|
|
193
291
|
}
|
|
194
292
|
catch (error) {
|
|
195
293
|
if (!/EEXIST/i.test(describe(error)))
|
|
196
294
|
throw error;
|
|
197
|
-
const owner = await commandOwner(exec);
|
|
295
|
+
const owner = await commandOwner(exec, prefix);
|
|
198
296
|
if (!owner)
|
|
199
297
|
throw error;
|
|
200
298
|
log.warn('self-update: the command belongs to another package — retiring it', {
|
|
201
299
|
package: owner.name,
|
|
202
300
|
});
|
|
203
|
-
await exec('npm', ['uninstall', '-g', '--loglevel=error', owner.name], {
|
|
204
|
-
|
|
205
|
-
env: npmEnv(),
|
|
206
|
-
});
|
|
207
|
-
await exec('npm', installArgs(source), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
301
|
+
await exec('npm', ['uninstall', '-g', '--loglevel=error', ...(prefix ? ['--prefix', prefix] : []), owner.name], { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
302
|
+
await exec('npm', installArgs(source, prefix), { timeout: NPM_TIMEOUT_MS, env: npmEnv() });
|
|
208
303
|
}
|
|
209
304
|
}
|
|
210
305
|
/**
|
|
@@ -256,13 +351,12 @@ export async function selfUpdate(options) {
|
|
|
256
351
|
// runner's own user.
|
|
257
352
|
if (!installIsWritable(packageDir)) {
|
|
258
353
|
const user = os.userInfo().username;
|
|
259
|
-
const uid = typeof process.getuid === 'function' ? process.getuid() : -1;
|
|
260
354
|
return fail(`The runner package in ${packageDir} belongs to another user, and this daemon runs as ${user}, ` +
|
|
261
|
-
'so it cannot replace itself.
|
|
262
|
-
|
|
263
|
-
'then restart the service as the runner’s own user:\n' +
|
|
264
|
-
` sudo -iu ${user} env XDG_RUNTIME_DIR=/run/user/${uid >= 0 ? uid : '$(id -u ' + user + ')'} systemctl --user restart devbridge-runner`);
|
|
355
|
+
'so it cannot replace itself. Run this on the server instead:\n ' +
|
|
356
|
+
manualUpdateCommand(options.tarballUrl, { packageDir }));
|
|
265
357
|
}
|
|
358
|
+
// Derived once and threaded through every npm call below.
|
|
359
|
+
const prefix = installPrefixFor(packageDir);
|
|
266
360
|
// Pack the current version FIRST: without a rollback artefact there is no
|
|
267
361
|
// honest way back if the new build turns out to be broken.
|
|
268
362
|
const rollbackDir = path.join(stateDir(), 'rollback');
|
|
@@ -287,7 +381,7 @@ export async function selfUpdate(options) {
|
|
|
287
381
|
return fail('Could not prepare a rollback copy of the current version — update aborted');
|
|
288
382
|
}
|
|
289
383
|
try {
|
|
290
|
-
await installGlobal(exec, options.tarballUrl);
|
|
384
|
+
await installGlobal(exec, options.tarballUrl, prefix);
|
|
291
385
|
}
|
|
292
386
|
catch (error) {
|
|
293
387
|
return fail(`Install failed: ${describe(error)}`, { rollbackTarball });
|
|
@@ -295,7 +389,7 @@ export async function selfUpdate(options) {
|
|
|
295
389
|
// Where the new build actually landed. After a rename `packageDir` is the
|
|
296
390
|
// directory we just retired, so its manifest would report the OLD version —
|
|
297
391
|
// and the smoke test below would run code that no longer exists.
|
|
298
|
-
const installed = await commandOwner(exec);
|
|
392
|
+
const installed = await commandOwner(exec, prefix);
|
|
299
393
|
const newPackageDir = installed?.dir ?? packageDir;
|
|
300
394
|
const toVersion = readVersion(newPackageDir) ?? undefined;
|
|
301
395
|
// The real test: does the newly installed build start? `--version` loads the
|
|
@@ -318,12 +412,12 @@ export async function selfUpdate(options) {
|
|
|
318
412
|
// Through the same door as the install above: if the failed update renamed
|
|
319
413
|
// the package, the command now belongs to the new name and putting the old
|
|
320
414
|
// one back hits the very same EEXIST.
|
|
321
|
-
await installGlobal(exec, rollbackTarball);
|
|
415
|
+
await installGlobal(exec, rollbackTarball, prefix);
|
|
322
416
|
return fail(`The new version did not start (${detail}). The previous version was restored and the runner keeps working.`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
323
417
|
}
|
|
324
418
|
catch (rollbackError) {
|
|
325
419
|
return fail(`The new version did not start (${detail}) and the rollback failed too (${describe(rollbackError)}). ` +
|
|
326
|
-
`Restore it on the server with: npm install -g ${rollbackTarball}`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
420
|
+
`Restore it on the server with: npm install -g${prefix ? ` --prefix ${prefix}` : ''} ${rollbackTarball}`, { rollbackTarball, ...(toVersion ? { toVersion } : {}) });
|
|
327
421
|
}
|
|
328
422
|
}
|
|
329
423
|
// The service unit may be pinned to a file inside the directory this update
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface SupervisorOptions {
|
|
|
39
39
|
verifyEnabled?: boolean;
|
|
40
40
|
/** Test seam for the one-shot commit-message run. */
|
|
41
41
|
proposeCommitMessage?: typeof proposeCommitMessage;
|
|
42
|
+
/**
|
|
43
|
+
* `[checkpoints] enabled` from the runner's own config (ticket #126), by the
|
|
44
|
+
* same rule as `[verify] enabled`: restore points are copies of the working
|
|
45
|
+
* tree kept on this machine, so the machine's owner has the last word on
|
|
46
|
+
* whether they are taken at all. `false` means the capability is not
|
|
47
|
+
* announced and no rewind can be started.
|
|
48
|
+
*/
|
|
49
|
+
checkpointsEnabled?: boolean;
|
|
42
50
|
}
|
|
43
51
|
export declare class Supervisor {
|
|
44
52
|
private readonly ws;
|
|
@@ -61,6 +69,17 @@ export declare class Supervisor {
|
|
|
61
69
|
private readonly authRelay;
|
|
62
70
|
/** Serialises repo-mutating git commands per workspace repo (QA-99 MAJOR-1). */
|
|
63
71
|
private readonly repoLocks;
|
|
72
|
+
/**
|
|
73
|
+
* Repo keys with work in flight right now (ticket #126).
|
|
74
|
+
*
|
|
75
|
+
* A checkpoint deliberately does NOT take the repo lock — it only reads the
|
|
76
|
+
* worktree, and queueing a user's message behind a 150-second push would be
|
|
77
|
+
* a plain regression. What it does instead is decline to run while a
|
|
78
|
+
* mutating command holds the repo: a snapshot taken halfway through an apply
|
|
79
|
+
* would be a state that never existed, and offering to restore it is worse
|
|
80
|
+
* than having no restore point for that one message.
|
|
81
|
+
*/
|
|
82
|
+
private readonly repoLockDepth;
|
|
64
83
|
/** An update is installing right now — a second one would fight it. */
|
|
65
84
|
private selfUpdateInFlight;
|
|
66
85
|
/** Session 14: one project-recipe run per machine, and its verdict queue. */
|
|
@@ -198,6 +217,14 @@ export declare class Supervisor {
|
|
|
198
217
|
* slot.
|
|
199
218
|
*/
|
|
200
219
|
private isParkable;
|
|
220
|
+
/**
|
|
221
|
+
* Stop the agent process but keep the session resumable.
|
|
222
|
+
*
|
|
223
|
+
* `quiet` is for the callers that say it better themselves: a conversation
|
|
224
|
+
* rewind parks the session too, and announcing «the runner switched to
|
|
225
|
+
* another session» there is simply untrue — nothing switched, the user
|
|
226
|
+
* rewound (ticket #126).
|
|
227
|
+
*/
|
|
201
228
|
private park;
|
|
202
229
|
private forwardEvent;
|
|
203
230
|
/**
|
|
@@ -210,6 +237,16 @@ export declare class Supervisor {
|
|
|
210
237
|
*/
|
|
211
238
|
private onQuestionAnswer;
|
|
212
239
|
private onUserMessage;
|
|
240
|
+
/**
|
|
241
|
+
* Queue a message that no agent can take yet, and say so in the feed
|
|
242
|
+
* (ticket #125).
|
|
243
|
+
*
|
|
244
|
+
* Every queueing decision goes through here so the browser's idea of "this
|
|
245
|
+
* one can still be taken back" cannot drift from the runner's. Records with
|
|
246
|
+
* no seq — orphans, and anything written by a runner older than 0.28.0 —
|
|
247
|
+
* stay silent: they have no name the browser could quote back.
|
|
248
|
+
*/
|
|
249
|
+
private queueMessage;
|
|
213
250
|
/**
|
|
214
251
|
* Run delivery work for one session, strictly after whatever is already
|
|
215
252
|
* queued for it. Order is the whole point: two messages typed seconds apart
|
|
@@ -235,6 +272,39 @@ export declare class Supervisor {
|
|
|
235
272
|
* the message is retired from disk only once an agent has it.
|
|
236
273
|
*/
|
|
237
274
|
private deliverMessage;
|
|
275
|
+
/**
|
|
276
|
+
* Nothing could take the message: put it back where it came from.
|
|
277
|
+
*
|
|
278
|
+
* Records that were already held keep their identity (and their queued
|
|
279
|
+
* announcement); a first-time refusal mints one and announces it.
|
|
280
|
+
*/
|
|
281
|
+
private requeue;
|
|
282
|
+
/**
|
|
283
|
+
* Where this session's conversation stands, live process or not (ticket #126).
|
|
284
|
+
*
|
|
285
|
+
* Reads the running agent when there is one and remembers what it said;
|
|
286
|
+
* falls back to that memory when there is not. The provider session id is
|
|
287
|
+
* checked on the way out rather than on the way in: an anchor minted before
|
|
288
|
+
* a fork is not wrong, it simply belongs to a conversation this session no
|
|
289
|
+
* longer has.
|
|
290
|
+
*/
|
|
291
|
+
private currentAnchor;
|
|
292
|
+
/**
|
|
293
|
+
* Take a restore point in front of the work that is about to start
|
|
294
|
+
* (ticket #126).
|
|
295
|
+
*
|
|
296
|
+
* Called from the delivery chain — which is already asynchronous because of
|
|
297
|
+
* attachments — rather than from `deliverMessage`, which is synchronous and
|
|
298
|
+
* has five exits. Never throws and never blocks delivery: a message must
|
|
299
|
+
* reach the agent whether or not a restore point could be taken.
|
|
300
|
+
*
|
|
301
|
+
* Three reasons it declines, and each of them is a state in which a snapshot
|
|
302
|
+
* would be a lie rather than a restore point:
|
|
303
|
+
* - the machine's owner switched checkpoints off;
|
|
304
|
+
* - the agent is mid-turn, so the tree is being written to as we read it;
|
|
305
|
+
* - a repo-mutating command holds the repository.
|
|
306
|
+
*/
|
|
307
|
+
private captureCheckpoint;
|
|
238
308
|
/**
|
|
239
309
|
* Deliver messages that raced session start (already journaled).
|
|
240
310
|
*
|
|
@@ -263,15 +333,31 @@ export declare class Supervisor {
|
|
|
263
333
|
* mark the other session failed (QA-99 MAJOR-1).
|
|
264
334
|
*/
|
|
265
335
|
private withRepoLock;
|
|
336
|
+
/** Is a repo-mutating command queued or running for this path right now? */
|
|
337
|
+
private isRepoLocked;
|
|
266
338
|
/**
|
|
267
339
|
* Same lock, keyed by the shared repository rather than by whichever path the
|
|
268
340
|
* caller happened to have. A worktree commit and a workspace squash-merge
|
|
269
341
|
* touch one repo, so they must take one key (see `repoKeyFor`).
|
|
270
342
|
*/
|
|
271
343
|
private withRepoLockFor;
|
|
344
|
+
/**
|
|
345
|
+
* The live session this command names, or the sentence to refuse it with.
|
|
346
|
+
*
|
|
347
|
+
* A string return is deliberately unmistakable for a session: every caller
|
|
348
|
+
* has to branch on the type, so "I did not check" cannot compile.
|
|
349
|
+
*/
|
|
350
|
+
private requireSession;
|
|
272
351
|
/** A session actively mid-turn in this worktree — git writes must wait. */
|
|
273
352
|
private isWorktreeBusy;
|
|
274
353
|
private static readonly EVENT_PAYLOAD_CAP;
|
|
354
|
+
/**
|
|
355
|
+
* Journal an event, put it on the wire, and return it.
|
|
356
|
+
*
|
|
357
|
+
* The return value matters since ticket #125: the seq assigned here is the
|
|
358
|
+
* only name the browser and the runner share for one message, so the caller
|
|
359
|
+
* that echoes a user bubble has to be able to read it back.
|
|
360
|
+
*/
|
|
275
361
|
private sendEvent;
|
|
276
362
|
private reportStatus;
|
|
277
363
|
/** Graceful daemon shutdown: kill agents, keep sessions resumable server-side. */
|