@bridge4dev/runner 0.46.1 → 0.48.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/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();
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 { claudeCliPath } from './agent-binary.js';
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 (claudeCliPath())
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,
@@ -186,6 +192,27 @@ function runnerCapabilities(apiUrlOverride) {
186
192
  claude: hasExecutable('claude'),
187
193
  codex: hasExecutable('codex'),
188
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' }),
189
216
  /**
190
217
  * Where npm put this package, and the command that updates it here
191
218
  * (0.27.0).
@@ -437,6 +464,7 @@ function runnerCapabilities(apiUrlOverride) {
437
464
  ...(verifyEnabled
438
465
  ? ['verify_start', 'verify_status', 'verify_cancel', 'preview_checkout', 'preview_stop']
439
466
  : []),
467
+ ...(agentInstallEnabled ? ['agent_install'] : []),
440
468
  ],
441
469
  };
442
470
  }
@@ -464,15 +492,14 @@ async function cmdPair(args) {
464
492
  fail(`pairing failed (HTTP ${response.status}): ${body?.error?.message ?? 'unknown error'}. ` +
465
493
  'The code is one-time and expires in 10 minutes — generate a fresh one in the dashboard if needed.');
466
494
  }
467
- const existing = loadConfig();
468
- const config = {
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(), {
469
500
  api: { url: apiUrl, ws_url: body.data.wsUrl },
470
501
  server: { id: body.data.serverId, name: body.data.serverName, token: body.data.token },
471
- ...(existing?.mcp ? { mcp: existing.mcp } : {}),
472
- // Carry settings the user wrote by hand — saveConfig re-parses through the
473
- // schema, so a section dropped here is a section deleted from disk.
474
- ...(existing?.codex ? { codex: existing.codex } : {}),
475
- };
502
+ });
476
503
  saveConfig(config);
477
504
  print(`Paired as "${body.data.serverName}" (server ${body.data.serverId}).`);
478
505
  print('Start the daemon with: devbridge-runner install-service (or: devbridge-runner daemon)');
@@ -708,6 +735,7 @@ async function cmdDaemon() {
708
735
  // frame from an API that has not noticed still cannot start a run.
709
736
  verifyEnabled: config.verify?.enabled !== false,
710
737
  checkpointsEnabled: config.checkpoints?.enabled !== false,
738
+ agentInstallEnabled: config.agents?.install_enabled !== false,
711
739
  apiUrl: config.api.url,
712
740
  // Used to fetch the files a user attaches to a message (session 10) — the
713
741
  // same token the WS connection authenticates with, never passed onwards.
@@ -1062,13 +1090,19 @@ async function agentChecks() {
1062
1090
  checks.push({
1063
1091
  ok: cliPresent.claude,
1064
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.
1065
1096
  detail: cliPresent.claude
1066
1097
  ? `on ${me.user}'s PATH`
1067
- : `not installed for ${me.user} — sessions still run (the SDK bundles its own), but signing in needs the CLI`,
1098
+ : `not installed for ${me.user} — Claude sessions cannot start on this server`,
1068
1099
  ...(cliPresent.claude
1069
1100
  ? {}
1070
1101
  : {
1071
- fix: `sudo -iu ${me.user} sh -lc 'curl -fsSL https://claude.ai/install.sh | bash'`,
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'`,
1072
1106
  }),
1073
1107
  });
1074
1108
  // Codex is optional: plenty of machines only ever run Claude sessions, and a
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Mirror of the `#region level-events-mirror` block in
3
+ * `packages/shared/src/constants/realtime.ts` – the DevBridge side is the
4
+ * source of truth, exactly like `agent-registry.ts` mirrors the agent registry
5
+ * and `protocol.ts` mirrors the API's wire types.
6
+ *
7
+ * Copied rather than imported on purpose: this package is published to npm on
8
+ * its own and installed by users who have no DevBridge workspace, so a
9
+ * `@devbridge/shared` import would make the published tarball unresolvable.
10
+ *
11
+ * CHECKED: `levels.test.ts` reads both files and compares the region between
12
+ * the markers character by character. Edit the shared file first, then paste
13
+ * the region here – nothing but the region, and nothing of the region left out.
14
+ */
15
+ /**
16
+ * Level signals on the dev-session stream – frames that carry a whole current
17
+ * value rather than a step of the conversation (#366).
18
+ *
19
+ * `agent_tasks` (#113), `context_usage` and `rate_limits` (#279) are LEVELS:
20
+ * every frame replaces the previous one, so a dropped frame costs freshness and
21
+ * never correctness. In production they were 61 % of every Codex session's
22
+ * rows (57 008 + 56 591 of ~185 000, 05.09.2026) while drawing not one line in
23
+ * the transcript – and each one re-rendered the whole page. So they are treated
24
+ * differently at every hop, and this block is the one place that says how:
25
+ *
26
+ * - the runner sends one only when the value moved (the thresholds below);
27
+ * - the API publishes it as a META frame (no `id:`) and never stores a row;
28
+ * - the dashboard keeps it beside the feed, never in it.
29
+ *
30
+ * Mirrored verbatim into `packages/runner/src/levels.ts` – the runner cannot
31
+ * import this package (it is published to npm on its own). `levels.test.ts`
32
+ * compares the two regions character by character.
33
+ */
34
+ export declare const LEVEL_EVENT_TYPES: readonly ["agent_tasks", "context_usage", "rate_limits"];
35
+ /**
36
+ * A context-meter move smaller than BOTH of these is not worth a frame. The
37
+ * same pair gates the API's mirror onto the session row, so the ring a reload
38
+ * draws from the row and the ring the live frame draws agree to the percent.
39
+ */
40
+ export declare const CONTEXT_USAGE_MIN_DELTA_TOKENS = 2000;
41
+ export declare const CONTEXT_USAGE_MIN_DELTA_RATIO = 0.01;
42
+ /**
43
+ * An unchanged plan-usage snapshot is re-sent no more often than this. It is
44
+ * re-sent at all because the limits panel dates its figure by ARRIVAL
45
+ * («updated 12 min ago»): silence would read as staleness. Three minutes is
46
+ * the interval the Claude adapter already probes `/usage` at.
47
+ */
48
+ export declare const RATE_LIMITS_RESEND_INTERVAL_MS: number;
49
+ //# sourceMappingURL=levels.d.ts.map
package/dist/levels.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Mirror of the `#region level-events-mirror` block in
3
+ * `packages/shared/src/constants/realtime.ts` – the DevBridge side is the
4
+ * source of truth, exactly like `agent-registry.ts` mirrors the agent registry
5
+ * and `protocol.ts` mirrors the API's wire types.
6
+ *
7
+ * Copied rather than imported on purpose: this package is published to npm on
8
+ * its own and installed by users who have no DevBridge workspace, so a
9
+ * `@devbridge/shared` import would make the published tarball unresolvable.
10
+ *
11
+ * CHECKED: `levels.test.ts` reads both files and compares the region between
12
+ * the markers character by character. Edit the shared file first, then paste
13
+ * the region here – nothing but the region, and nothing of the region left out.
14
+ */
15
+ // #region level-events-mirror
16
+ /**
17
+ * Level signals on the dev-session stream – frames that carry a whole current
18
+ * value rather than a step of the conversation (#366).
19
+ *
20
+ * `agent_tasks` (#113), `context_usage` and `rate_limits` (#279) are LEVELS:
21
+ * every frame replaces the previous one, so a dropped frame costs freshness and
22
+ * never correctness. In production they were 61 % of every Codex session's
23
+ * rows (57 008 + 56 591 of ~185 000, 05.09.2026) while drawing not one line in
24
+ * the transcript – and each one re-rendered the whole page. So they are treated
25
+ * differently at every hop, and this block is the one place that says how:
26
+ *
27
+ * - the runner sends one only when the value moved (the thresholds below);
28
+ * - the API publishes it as a META frame (no `id:`) and never stores a row;
29
+ * - the dashboard keeps it beside the feed, never in it.
30
+ *
31
+ * Mirrored verbatim into `packages/runner/src/levels.ts` – the runner cannot
32
+ * import this package (it is published to npm on its own). `levels.test.ts`
33
+ * compares the two regions character by character.
34
+ */
35
+ export const LEVEL_EVENT_TYPES = ['agent_tasks', 'context_usage', 'rate_limits'];
36
+ /**
37
+ * A context-meter move smaller than BOTH of these is not worth a frame. The
38
+ * same pair gates the API's mirror onto the session row, so the ring a reload
39
+ * draws from the row and the ring the live frame draws agree to the percent.
40
+ */
41
+ export const CONTEXT_USAGE_MIN_DELTA_TOKENS = 2_000;
42
+ export const CONTEXT_USAGE_MIN_DELTA_RATIO = 0.01;
43
+ /**
44
+ * An unchanged plan-usage snapshot is re-sent no more often than this. It is
45
+ * re-sent at all because the limits panel dates its figure by ARRIVAL
46
+ * («updated 12 min ago»): silence would read as staleness. Three minutes is
47
+ * the interval the Claude adapter already probes `/usage` at.
48
+ */
49
+ export const RATE_LIMITS_RESEND_INTERVAL_MS = 3 * 60 * 1000;
50
+ // #endregion level-events-mirror
51
+ //# sourceMappingURL=levels.js.map