@bridge4dev/runner 0.45.1 → 0.47.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/README.md +44 -2
- package/dist/adapters/claude.js +8 -4
- package/dist/adapters/codex.d.ts +10 -0
- package/dist/adapters/codex.js +294 -10
- package/dist/adapters/types.d.ts +16 -0
- package/dist/adapters/types.js +16 -0
- package/dist/agent-auto-update.d.ts +75 -0
- package/dist/agent-auto-update.js +134 -0
- package/dist/agent-binary.d.ts +48 -0
- package/dist/agent-binary.js +55 -0
- package/dist/agent-cleanup.d.ts +78 -0
- package/dist/agent-cleanup.js +184 -0
- package/dist/agent-install.d.ts +140 -0
- package/dist/agent-install.js +475 -0
- package/dist/agent-registry.d.ts +223 -0
- package/dist/agent-registry.js +131 -0
- package/dist/agent-versions.d.ts +93 -0
- package/dist/agent-versions.js +157 -0
- package/dist/auth-relay.d.ts +9 -1
- package/dist/auth-relay.js +3 -1
- package/dist/commit-message.js +5 -0
- package/dist/config.d.ts +44 -4
- package/dist/config.js +43 -0
- package/dist/file-publish.d.ts +27 -0
- package/dist/file-publish.js +82 -0
- package/dist/fsview.d.ts +8 -0
- package/dist/fsview.js +8 -1
- package/dist/index.js +61 -12
- package/dist/protocol.d.ts +194 -28
- package/dist/protocol.js +34 -2
- package/dist/recipe-schema.d.ts +12 -12
- package/dist/self-update.d.ts +14 -0
- package/dist/self-update.js +45 -13
- package/dist/supervisor.d.ts +244 -2
- package/dist/supervisor.js +742 -35
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -15,12 +15,12 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
15
15
|
name: z.ZodString;
|
|
16
16
|
token: z.ZodString;
|
|
17
17
|
}, "strip", z.ZodTypeAny, {
|
|
18
|
-
id: string;
|
|
19
18
|
name: string;
|
|
19
|
+
id: string;
|
|
20
20
|
token: string;
|
|
21
21
|
}, {
|
|
22
|
-
id: string;
|
|
23
22
|
name: string;
|
|
23
|
+
id: string;
|
|
24
24
|
token: string;
|
|
25
25
|
}>;
|
|
26
26
|
mcp: z.ZodOptional<z.ZodObject<{
|
|
@@ -83,14 +83,36 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
83
83
|
}, {
|
|
84
84
|
enabled?: boolean | undefined;
|
|
85
85
|
}>>;
|
|
86
|
+
/**
|
|
87
|
+
* #371: the machine owner's veto over installing agent CLIs.
|
|
88
|
+
*
|
|
89
|
+
* `agent_install` downloads and runs a vendor's software on this server on a
|
|
90
|
+
* word from the dashboard. The payload is not the server's to choose — the
|
|
91
|
+
* package and the installer URL are compiled into this runner — but «which
|
|
92
|
+
* version of Codex runs here» is still a decision about somebody else's
|
|
93
|
+
* machine, and its owner gets the last word on whether we may make it at all.
|
|
94
|
+
*
|
|
95
|
+
* Same shape and same rule as `[verify]` and `[checkpoints]`:
|
|
96
|
+
* `install_enabled = false` means the capability is not announced and
|
|
97
|
+
* `agent_install` is not offered as a command, so the dashboard draws no
|
|
98
|
+
* button rather than a button that answers «refused». The server cannot
|
|
99
|
+
* switch it back on: it is read from a root-owned file, here.
|
|
100
|
+
*/
|
|
101
|
+
agents: z.ZodOptional<z.ZodObject<{
|
|
102
|
+
install_enabled: z.ZodDefault<z.ZodBoolean>;
|
|
103
|
+
}, "strip", z.ZodTypeAny, {
|
|
104
|
+
install_enabled: boolean;
|
|
105
|
+
}, {
|
|
106
|
+
install_enabled?: boolean | undefined;
|
|
107
|
+
}>>;
|
|
86
108
|
}, "strip", z.ZodTypeAny, {
|
|
87
109
|
api: {
|
|
88
110
|
url: string;
|
|
89
111
|
ws_url: string;
|
|
90
112
|
};
|
|
91
113
|
server: {
|
|
92
|
-
id: string;
|
|
93
114
|
name: string;
|
|
115
|
+
id: string;
|
|
94
116
|
token: string;
|
|
95
117
|
};
|
|
96
118
|
checkpoints?: {
|
|
@@ -109,14 +131,17 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
109
131
|
verify?: {
|
|
110
132
|
enabled: boolean;
|
|
111
133
|
} | undefined;
|
|
134
|
+
agents?: {
|
|
135
|
+
install_enabled: boolean;
|
|
136
|
+
} | undefined;
|
|
112
137
|
}, {
|
|
113
138
|
api: {
|
|
114
139
|
url: string;
|
|
115
140
|
ws_url: string;
|
|
116
141
|
};
|
|
117
142
|
server: {
|
|
118
|
-
id: string;
|
|
119
143
|
name: string;
|
|
144
|
+
id: string;
|
|
120
145
|
token: string;
|
|
121
146
|
};
|
|
122
147
|
checkpoints?: {
|
|
@@ -135,10 +160,25 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
135
160
|
verify?: {
|
|
136
161
|
enabled?: boolean | undefined;
|
|
137
162
|
} | undefined;
|
|
163
|
+
agents?: {
|
|
164
|
+
install_enabled?: boolean | undefined;
|
|
165
|
+
} | undefined;
|
|
138
166
|
}>;
|
|
139
167
|
export type RunnerConfig = z.infer<typeof ConfigSchema>;
|
|
140
168
|
export declare function loadConfig(): RunnerConfig | null;
|
|
141
169
|
export declare function requireConfig(): RunnerConfig;
|
|
170
|
+
/**
|
|
171
|
+
* The config a re-pairing should write: the new server's identity, everything
|
|
172
|
+
* else as the machine's owner left it.
|
|
173
|
+
*
|
|
174
|
+
* Lives here, beside the schema, rather than inline in `cmdPair` — because the
|
|
175
|
+
* failure mode is a section that is simply not mentioned. `saveConfig` re-parses
|
|
176
|
+
* and rewrites the whole file, so a key forgotten in this merge is not «left
|
|
177
|
+
* alone», it is **deleted from disk**: re-pairing a machine would silently lift
|
|
178
|
+
* the owner's veto over installing agents, or move Codex back to a shared
|
|
179
|
+
* credential store. Every optional section of `ConfigSchema` must be listed.
|
|
180
|
+
*/
|
|
181
|
+
export declare function mergeIntoPairedConfig(existing: RunnerConfig | null, paired: Pick<RunnerConfig, 'api' | 'server'>): RunnerConfig;
|
|
142
182
|
export declare function saveConfig(config: RunnerConfig): void;
|
|
143
183
|
export {};
|
|
144
184
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
|
@@ -76,6 +76,26 @@ const ConfigSchema = z.object({
|
|
|
76
76
|
enabled: z.boolean().default(true),
|
|
77
77
|
})
|
|
78
78
|
.optional(),
|
|
79
|
+
/**
|
|
80
|
+
* #371: the machine owner's veto over installing agent CLIs.
|
|
81
|
+
*
|
|
82
|
+
* `agent_install` downloads and runs a vendor's software on this server on a
|
|
83
|
+
* word from the dashboard. The payload is not the server's to choose — the
|
|
84
|
+
* package and the installer URL are compiled into this runner — but «which
|
|
85
|
+
* version of Codex runs here» is still a decision about somebody else's
|
|
86
|
+
* machine, and its owner gets the last word on whether we may make it at all.
|
|
87
|
+
*
|
|
88
|
+
* Same shape and same rule as `[verify]` and `[checkpoints]`:
|
|
89
|
+
* `install_enabled = false` means the capability is not announced and
|
|
90
|
+
* `agent_install` is not offered as a command, so the dashboard draws no
|
|
91
|
+
* button rather than a button that answers «refused». The server cannot
|
|
92
|
+
* switch it back on: it is read from a root-owned file, here.
|
|
93
|
+
*/
|
|
94
|
+
agents: z
|
|
95
|
+
.object({
|
|
96
|
+
install_enabled: z.boolean().default(true),
|
|
97
|
+
})
|
|
98
|
+
.optional(),
|
|
79
99
|
});
|
|
80
100
|
export function loadConfig() {
|
|
81
101
|
const file = configFilePath();
|
|
@@ -91,6 +111,29 @@ export function requireConfig() {
|
|
|
91
111
|
}
|
|
92
112
|
return config;
|
|
93
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* The config a re-pairing should write: the new server's identity, everything
|
|
116
|
+
* else as the machine's owner left it.
|
|
117
|
+
*
|
|
118
|
+
* Lives here, beside the schema, rather than inline in `cmdPair` — because the
|
|
119
|
+
* failure mode is a section that is simply not mentioned. `saveConfig` re-parses
|
|
120
|
+
* and rewrites the whole file, so a key forgotten in this merge is not «left
|
|
121
|
+
* alone», it is **deleted from disk**: re-pairing a machine would silently lift
|
|
122
|
+
* the owner's veto over installing agents, or move Codex back to a shared
|
|
123
|
+
* credential store. Every optional section of `ConfigSchema` must be listed.
|
|
124
|
+
*/
|
|
125
|
+
export function mergeIntoPairedConfig(existing, paired) {
|
|
126
|
+
return {
|
|
127
|
+
api: paired.api,
|
|
128
|
+
server: paired.server,
|
|
129
|
+
...(existing?.mcp ? { mcp: existing.mcp } : {}),
|
|
130
|
+
...(existing?.codex ? { codex: existing.codex } : {}),
|
|
131
|
+
...(existing?.limits ? { limits: existing.limits } : {}),
|
|
132
|
+
...(existing?.verify ? { verify: existing.verify } : {}),
|
|
133
|
+
...(existing?.checkpoints ? { checkpoints: existing.checkpoints } : {}),
|
|
134
|
+
...(existing?.agents ? { agents: existing.agents } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
94
137
|
export function saveConfig(config) {
|
|
95
138
|
ConfigSchema.parse(config);
|
|
96
139
|
const dir = configDir();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface PublishFileArgs {
|
|
2
|
+
/** Корень рабочей копии сессии. */
|
|
3
|
+
root: string;
|
|
4
|
+
/** Путь относительно корня. */
|
|
5
|
+
relPath: string;
|
|
6
|
+
/** Одноразовый слот, выданный API. */
|
|
7
|
+
slotId: string;
|
|
8
|
+
/** Сколько байт разрешено, по мнению API. */
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
/** Адрес API, с которым спарен ЭТОТ раннер. */
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
/** Токен этого раннера. */
|
|
13
|
+
token: string;
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export interface PublishFileResult {
|
|
17
|
+
fileName: string;
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
20
|
+
/** Прочитать файл по правилам просмотра и убедиться, что его можно отдать. */
|
|
21
|
+
export declare function readFileForPublish(root: string, relPath: string, maxBytes: number): {
|
|
22
|
+
buffer: Buffer;
|
|
23
|
+
fileName: string;
|
|
24
|
+
displayPath: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function publishFile(args: PublishFileArgs): Promise<PublishFileResult>;
|
|
27
|
+
//# sourceMappingURL=file-publish.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveInsideRoot } from './fsview.js';
|
|
4
|
+
import { isSecretPath } from './policy.js';
|
|
5
|
+
import { log } from './log.js';
|
|
6
|
+
/**
|
|
7
|
+
* Выложить файл с этой машины в хранилище платформы (0.46.0).
|
|
8
|
+
*
|
|
9
|
+
* Что здесь происходит и почему именно так:
|
|
10
|
+
*
|
|
11
|
+
* - **Адрес собирается из СВОЕГО `apiUrl`, а не из присланного.** Команда с
|
|
12
|
+
* той стороны говорит только «какой файл» и «в какой слот», но не «куда
|
|
13
|
+
* отправить». Тот же принцип, по которому раннер отказывается брать код
|
|
14
|
+
* откуда угодно, кроме сервера, с которым спарен: иначе одна подделанная
|
|
15
|
+
* команда превращается в способ вытащить файл с чужой машины на чужой хост.
|
|
16
|
+
*
|
|
17
|
+
* - **Правила пути — те же, что у просмотра.** `resolveInsideRoot` (выход за
|
|
18
|
+
* корень, символические ссылки, `.git`) плюс `isSecretPath` (`.env`, ключи).
|
|
19
|
+
* Своих правил здесь нет ни одного.
|
|
20
|
+
*
|
|
21
|
+
* - **Содержимое НЕ маскируется.** `fs_view` заменяет секреты в предпросмотре,
|
|
22
|
+
* но опубликованный архив с подменёнными байтами — битый архив, а документ с
|
|
23
|
+
* тихо изменённым текстом — ложь. Защита здесь — отказ по `isSecretPath` и
|
|
24
|
+
* то, что файл выбирает человек, а не агент.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Свой потолок поверх присланного.
|
|
28
|
+
*
|
|
29
|
+
* Присланное число — просьба, а не приказ: сторона API может однажды попросить
|
|
30
|
+
* прочитать гигабайт, и машина владельца не обязана соглашаться.
|
|
31
|
+
*/
|
|
32
|
+
const HARD_MAX_BYTES = 64 * 1024 * 1024;
|
|
33
|
+
const UPLOAD_TIMEOUT_MS = 120_000;
|
|
34
|
+
/** Прочитать файл по правилам просмотра и убедиться, что его можно отдать. */
|
|
35
|
+
export function readFileForPublish(root, relPath, maxBytes) {
|
|
36
|
+
const target = resolveInsideRoot(root, relPath);
|
|
37
|
+
if (isSecretPath(target) || isSecretPath(path.basename(target))) {
|
|
38
|
+
throw new Error('This path is protected by runner policy');
|
|
39
|
+
}
|
|
40
|
+
const stat = fs.statSync(target);
|
|
41
|
+
if (!stat.isFile())
|
|
42
|
+
throw new Error('Not a regular file');
|
|
43
|
+
if (stat.size === 0)
|
|
44
|
+
throw new Error('The file is empty');
|
|
45
|
+
const ceiling = Math.min(maxBytes, HARD_MAX_BYTES);
|
|
46
|
+
if (stat.size > ceiling) {
|
|
47
|
+
throw new Error(`The file is larger than ${Math.round(ceiling / 1024 / 1024)}MB and cannot be published`);
|
|
48
|
+
}
|
|
49
|
+
const rootReal = fs.realpathSync(root);
|
|
50
|
+
return {
|
|
51
|
+
buffer: fs.readFileSync(target),
|
|
52
|
+
fileName: path.basename(target),
|
|
53
|
+
displayPath: path.relative(rootReal, target) || path.basename(target),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function publishFile(args) {
|
|
57
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
58
|
+
const { buffer, fileName, displayPath } = readFileForPublish(args.root, args.relPath, args.maxBytes);
|
|
59
|
+
const base = args.apiUrl.replace(/\/$/, '');
|
|
60
|
+
const response = await doFetch(`${base}/api/v1/dev/runner/uploads/${encodeURIComponent(args.slotId)}`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${args.token}`,
|
|
64
|
+
'Content-Type': 'application/octet-stream',
|
|
65
|
+
// Имя файла и путь — в заголовках, а не в теле: тело здесь это ровно
|
|
66
|
+
// байты файла и ничего больше. Кодирование обязательно — в заголовок
|
|
67
|
+
// нельзя положить произвольный UTF-8 и уж точно нельзя перевод строки.
|
|
68
|
+
'X-Devbridge-File-Name': encodeURIComponent(fileName),
|
|
69
|
+
'X-Devbridge-File-Path': encodeURIComponent(displayPath),
|
|
70
|
+
},
|
|
71
|
+
body: new Uint8Array(buffer),
|
|
72
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const body = (await response.json().catch(() => null));
|
|
76
|
+
const reason = body?.error?.message ?? `HTTP ${response.status}`;
|
|
77
|
+
log.warn('file-publish: upload rejected', { slotId: args.slotId, reason });
|
|
78
|
+
throw new Error(reason);
|
|
79
|
+
}
|
|
80
|
+
return { fileName, size: buffer.length };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=file-publish.js.map
|
package/dist/fsview.d.ts
CHANGED
|
@@ -16,5 +16,13 @@ export interface FsEntry {
|
|
|
16
16
|
type: 'dir' | 'file';
|
|
17
17
|
size: number | null;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Разрешение пути внутри корня рабочей копии.
|
|
21
|
+
*
|
|
22
|
+
* Экспортируется, потому что публикация файла (`file-publish.ts`) обязана
|
|
23
|
+
* применять РОВНО те же правила, что и просмотр: своя копия этой функции — это
|
|
24
|
+
* второй набор правил, который однажды разойдётся с первым.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveInsideRoot(root: string, relPath: string): string;
|
|
19
27
|
export declare function fsView(root: string, relPath?: string): FsViewResult;
|
|
20
28
|
//# sourceMappingURL=fsview.d.ts.map
|
package/dist/fsview.js
CHANGED
|
@@ -18,7 +18,14 @@ function assertRepoRoot(rootReal) {
|
|
|
18
18
|
throw new Error('The workspace path is not a git repository');
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Разрешение пути внутри корня рабочей копии.
|
|
23
|
+
*
|
|
24
|
+
* Экспортируется, потому что публикация файла (`file-publish.ts`) обязана
|
|
25
|
+
* применять РОВНО те же правила, что и просмотр: своя копия этой функции — это
|
|
26
|
+
* второй набор правил, который однажды разойдётся с первым.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveInsideRoot(root, relPath) {
|
|
22
29
|
let rootReal;
|
|
23
30
|
try {
|
|
24
31
|
rootReal = fs.realpathSync(root);
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
import { ClaudeAdapter } from './adapters/claude.js';
|
|
9
9
|
import { CodexAdapter } from './adapters/codex.js';
|
|
10
10
|
import { ensureCodexHome } from './adapters/codex-home.js';
|
|
11
|
-
import {
|
|
12
|
-
import { loadConfig, requireConfig, saveConfig } from './config.js';
|
|
11
|
+
import { sessionClaudePath } from './agent-binary.js';
|
|
12
|
+
import { loadConfig, mergeIntoPairedConfig, requireConfig, saveConfig, } from './config.js';
|
|
13
13
|
import { log } from './log.js';
|
|
14
14
|
import { installIsWritable, installPrefixFor, isSupervisedProcess, manualUpdateCommand, resolveInstalledPackageDir, } from './self-update.js';
|
|
15
15
|
import { applyStoredClaudeToken } from './agent-auth.js';
|
|
@@ -63,6 +63,11 @@ function argValue(args, flag) {
|
|
|
63
63
|
* create a session that immediately fails.
|
|
64
64
|
*/
|
|
65
65
|
function installedAgents() {
|
|
66
|
+
// #371: this list and the `agent_versions` frame answer about the same file
|
|
67
|
+
// as of C2 — `sessionClaudePath()` follows `USE_BUNDLED_CLAUDE`, so while the
|
|
68
|
+
// constant is true it still reports the SDK's bundled binary (today's
|
|
69
|
+
// behaviour, unchanged), and the moment it flips both speak about the system
|
|
70
|
+
// `claude` on PATH. One switch, not two lists drifting apart.
|
|
66
71
|
const agents = [];
|
|
67
72
|
// Claude comes with the Agent SDK — but «comes with» is a claim about THIS
|
|
68
73
|
// installation, not a law (ticket #225). The CLI is an optional platform
|
|
@@ -70,7 +75,7 @@ function installedAgents() {
|
|
|
70
75
|
// Claude, accepts Claude sessions, and cannot start a single one. Reported as
|
|
71
76
|
// measured, so the dashboard greys the agent out instead of offering a
|
|
72
77
|
// session that dies before its first word.
|
|
73
|
-
if (
|
|
78
|
+
if (sessionClaudePath())
|
|
74
79
|
agents.push('claude');
|
|
75
80
|
if (hasExecutable('codex'))
|
|
76
81
|
agents.push('codex');
|
|
@@ -138,6 +143,7 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
138
143
|
const verifyEnabled = config?.verify?.enabled !== false;
|
|
139
144
|
// Ticket #126: the same veto, for restore points. Default on.
|
|
140
145
|
const checkpointsEnabled = config?.checkpoints?.enabled !== false;
|
|
146
|
+
const agentInstallEnabled = config?.agents?.install_enabled !== false;
|
|
141
147
|
return {
|
|
142
148
|
agents: installedAgents(),
|
|
143
149
|
git: true,
|
|
@@ -145,6 +151,14 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
145
151
|
resumeEpoch: true,
|
|
146
152
|
/** Session 8: understands `maxSessions` and runs sessions side by side. */
|
|
147
153
|
parallelSessions: true,
|
|
154
|
+
/**
|
|
155
|
+
* 0.46.0: reports which sessions are actually holding a seat here, so the
|
|
156
|
+
* API stops having to guess the occupancy of this machine from its own
|
|
157
|
+
* rows. Announced rather than inferred from the version, because the API
|
|
158
|
+
* runs a compatibility sweep for runners that cannot say it — and that
|
|
159
|
+
* sweep must switch off the moment this one can.
|
|
160
|
+
*/
|
|
161
|
+
sessionSlots: true,
|
|
148
162
|
/**
|
|
149
163
|
* Session 9: can update itself on command. Reported as a capability rather
|
|
150
164
|
* than inferred from the version, because the dashboard must not offer a
|
|
@@ -178,6 +192,27 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
178
192
|
claude: hasExecutable('claude'),
|
|
179
193
|
codex: hasExecutable('codex'),
|
|
180
194
|
},
|
|
195
|
+
/**
|
|
196
|
+
* This build measures agent versions and can be told to install one (0.47.0).
|
|
197
|
+
*
|
|
198
|
+
* One flag for both halves on purpose. The dashboard needs to know whether
|
|
199
|
+
* an empty version row means «not installed» or «this runner cannot say»,
|
|
200
|
+
* and the API needs to know whether `agent_install` will be understood —
|
|
201
|
+
* and there is no build where one is true and the other is not.
|
|
202
|
+
*
|
|
203
|
+
* Kept out of the version data itself: capabilities are computed once per
|
|
204
|
+
* process and replayed on every reconnect (`ws-client.ts`), so a version
|
|
205
|
+
* put here would freeze at daemon start. Versions travel in the
|
|
206
|
+
* `agent_versions` frame.
|
|
207
|
+
*
|
|
208
|
+
* Withheld entirely when the machine owner said no (`[agents]
|
|
209
|
+
* install_enabled = false`), like `[verify]` and `[checkpoints]`: a control
|
|
210
|
+
* that is switched off must not look like a control that is broken. The
|
|
211
|
+
* measuring half keeps working — reading a version is not installing one.
|
|
212
|
+
*/
|
|
213
|
+
...(agentInstallEnabled
|
|
214
|
+
? { agentInstall: true }
|
|
215
|
+
: { agentInstallBlocked: 'disabled-by-config' }),
|
|
181
216
|
/**
|
|
182
217
|
* Where npm put this package, and the command that updates it here
|
|
183
218
|
* (0.27.0).
|
|
@@ -234,6 +269,13 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
234
269
|
* anything the previous version could not.
|
|
235
270
|
*/
|
|
236
271
|
gitRefs: true,
|
|
272
|
+
/**
|
|
273
|
+
* 0.46.0: умеет выложить файл из рабочей копии в хранилище платформы
|
|
274
|
+
* (`fs_publish`). Без флага дашборд не рисует кнопку «Опубликовать», а
|
|
275
|
+
* объясняет, что раннер надо обновить — кнопка, чей фрейм старый раннер
|
|
276
|
+
* молча уронит, хуже отсутствующей кнопки.
|
|
277
|
+
*/
|
|
278
|
+
filePublish: true,
|
|
237
279
|
/**
|
|
238
280
|
* Session 14: reads `.devbridge/project.json`. Announced even when
|
|
239
281
|
* verification is switched off — «this machine will not run recipes» and
|
|
@@ -422,6 +464,7 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
422
464
|
...(verifyEnabled
|
|
423
465
|
? ['verify_start', 'verify_status', 'verify_cancel', 'preview_checkout', 'preview_stop']
|
|
424
466
|
: []),
|
|
467
|
+
...(agentInstallEnabled ? ['agent_install'] : []),
|
|
425
468
|
],
|
|
426
469
|
};
|
|
427
470
|
}
|
|
@@ -449,15 +492,14 @@ async function cmdPair(args) {
|
|
|
449
492
|
fail(`pairing failed (HTTP ${response.status}): ${body?.error?.message ?? 'unknown error'}. ` +
|
|
450
493
|
'The code is one-time and expires in 10 minutes — generate a fresh one in the dashboard if needed.');
|
|
451
494
|
}
|
|
452
|
-
|
|
453
|
-
|
|
495
|
+
// Everything the machine's owner wrote by hand survives the re-pairing —
|
|
496
|
+
// `saveConfig` rewrites the whole file, so a section not carried across is a
|
|
497
|
+
// section deleted. The rule lives in `config.ts` beside the schema it has to
|
|
498
|
+
// keep up with, and is tested there.
|
|
499
|
+
const config = mergeIntoPairedConfig(loadConfig(), {
|
|
454
500
|
api: { url: apiUrl, ws_url: body.data.wsUrl },
|
|
455
501
|
server: { id: body.data.serverId, name: body.data.serverName, token: body.data.token },
|
|
456
|
-
|
|
457
|
-
// Carry settings the user wrote by hand — saveConfig re-parses through the
|
|
458
|
-
// schema, so a section dropped here is a section deleted from disk.
|
|
459
|
-
...(existing?.codex ? { codex: existing.codex } : {}),
|
|
460
|
-
};
|
|
502
|
+
});
|
|
461
503
|
saveConfig(config);
|
|
462
504
|
print(`Paired as "${body.data.serverName}" (server ${body.data.serverId}).`);
|
|
463
505
|
print('Start the daemon with: devbridge-runner install-service (or: devbridge-runner daemon)');
|
|
@@ -693,6 +735,7 @@ async function cmdDaemon() {
|
|
|
693
735
|
// frame from an API that has not noticed still cannot start a run.
|
|
694
736
|
verifyEnabled: config.verify?.enabled !== false,
|
|
695
737
|
checkpointsEnabled: config.checkpoints?.enabled !== false,
|
|
738
|
+
agentInstallEnabled: config.agents?.install_enabled !== false,
|
|
696
739
|
apiUrl: config.api.url,
|
|
697
740
|
// Used to fetch the files a user attaches to a message (session 10) — the
|
|
698
741
|
// same token the WS connection authenticates with, never passed onwards.
|
|
@@ -1047,13 +1090,19 @@ async function agentChecks() {
|
|
|
1047
1090
|
checks.push({
|
|
1048
1091
|
ok: cliPresent.claude,
|
|
1049
1092
|
name: 'claude cli',
|
|
1093
|
+
// Since 0.47.0 this is not a warning about signing in — it is «no Claude
|
|
1094
|
+
// sessions on this machine at all». The bundled binary is switched off
|
|
1095
|
+
// (`USE_BUNDLED_CLAUDE`), so the CLI on PATH is the one every session runs.
|
|
1050
1096
|
detail: cliPresent.claude
|
|
1051
1097
|
? `on ${me.user}'s PATH`
|
|
1052
|
-
: `not installed for ${me.user} — sessions
|
|
1098
|
+
: `not installed for ${me.user} — Claude sessions cannot start on this server`,
|
|
1053
1099
|
...(cliPresent.claude
|
|
1054
1100
|
? {}
|
|
1055
1101
|
: {
|
|
1056
|
-
|
|
1102
|
+
// The dashboard is the way in (Р9): it picks the version, checks the
|
|
1103
|
+
// disk first and puts the old binary back if the new one will not run.
|
|
1104
|
+
// The command stays for a machine nobody can reach the dashboard from.
|
|
1105
|
+
fix: `press «Install» on the Claude row of this server's card in DevBridge — or, by hand: sudo -iu ${me.user} sh -lc 'curl -fsSL https://claude.ai/install.sh | bash'`,
|
|
1057
1106
|
}),
|
|
1058
1107
|
});
|
|
1059
1108
|
// Codex is optional: plenty of machines only ever run Claude sessions, and a
|