@bridge4dev/runner 0.70.1 → 0.71.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.
@@ -6,11 +6,13 @@
6
6
  * is on its knees», so it said nothing, and the first symptom a user saw was a
7
7
  * session that had gone quiet (plan `agent-sessions-host-resources.md` §5.3).
8
8
  *
9
- * Two files and nothing else. That is a security property, not an
10
- * implementation detail: this runner reports to a server, so what it may read
11
- * for telemetry is listed here in full — `/proc/loadavg` and `/proc/meminfo`,
12
- * both world-readable, neither containing a path, a command line, an
13
- * environment variable or a process name. No `/proc/<pid>` walk, no `ps`.
9
+ * Two files and one filesystem call, and nothing else. That is a security
10
+ * property, not an implementation detail: this runner reports to a server, so
11
+ * what it may read for telemetry is listed here in full — `/proc/loadavg` and
12
+ * `/proc/meminfo`, both world-readable, neither containing a path, a command
13
+ * line, an environment variable or a process name; and `statfs('/')` (0.71.0,
14
+ * #485), which answers four sizes of the root filesystem and nothing about what
15
+ * is on it. No `/proc/<pid>` walk, no `ps`, no directory listing, no `du`.
14
16
  *
15
17
  * **One module is allowed past that line, and it is named here so the rule and
16
18
  * the code cannot drift apart:** `session-stall.ts` reads `/proc/<pid>` for the
@@ -89,6 +91,13 @@ export interface HostLoadFrame {
89
91
  /** Zero on a machine with no swap; consumers must not divide blindly. */
90
92
  swapTotalBytes: number;
91
93
  swapFreeBytes: number;
94
+ /**
95
+ * The root filesystem (0.71.0, #485) – or `null` when it could not be read,
96
+ * which is «disk unknown» and never a reason to hold the rest of the frame
97
+ * back. It rides the same minute-long heartbeat as everything else here: a
98
+ * disk fills over hours, and a trigger of its own would buy nothing.
99
+ */
100
+ disk: HostDiskFrame | null;
92
101
  /**
93
102
  * When THIS MACHINE measured it, ISO.
94
103
  *
@@ -98,6 +107,31 @@ export interface HostLoadFrame {
98
107
  */
99
108
  at: string;
100
109
  }
110
+ /**
111
+ * Free space on the root filesystem – what a full disk stops the machine at.
112
+ *
113
+ * Mirror of `HostDiskFrameSchema` in `@devbridge/shared`. The same arithmetic as
114
+ * the DevBridge host's own reading (`admin-host.service.ts`), so «free» means one
115
+ * thing on the Fleet screen whichever machine it describes: `freeBytes` is
116
+ * `bavail` (what an unprivileged writer can still use, not `bfree`), `usedBytes`
117
+ * is total − `bfree` (what `df` calls used).
118
+ */
119
+ export interface HostDiskFrame {
120
+ /** Always {@link HOST_DISK_PATH} today; carried so the reading describes itself. */
121
+ path: string;
122
+ totalBytes: number;
123
+ freeBytes: number;
124
+ usedBytes: number;
125
+ }
126
+ /** The filesystem measured: the root, where running out stops everything. */
127
+ export declare const HOST_DISK_PATH = "/";
128
+ /** What `fs.statfsSync` answers, as far as a size is concerned. */
129
+ export interface DiskStats {
130
+ bsize: number;
131
+ blocks: number;
132
+ bfree: number;
133
+ bavail: number;
134
+ }
101
135
  /** Test seams. Production calls `readHostLoad()` with nothing. */
102
136
  export interface HostLoadSources {
103
137
  /**
@@ -111,7 +145,21 @@ export interface HostLoadSources {
111
145
  cpuCount?: number;
112
146
  /** The clock behind `at`. */
113
147
  now?: () => Date;
148
+ /**
149
+ * `statfs`, for the same reason as `procDir`: a full disk, a filesystem that
150
+ * refuses the call and a reading that does not add up are exactly the states a
151
+ * test machine is not in.
152
+ */
153
+ statfs?: (target: string) => DiskStats;
114
154
  }
155
+ /**
156
+ * Measure the root filesystem, or say honestly that we cannot.
157
+ *
158
+ * Every number has to be a whole, non-negative, safely representable count of
159
+ * bytes that fits inside the total – the API's schema holds the same line, and a
160
+ * reading it would refuse is better not sent than sent. Any doubt is `null`.
161
+ */
162
+ export declare function readRootDisk(statfs?: (target: string) => DiskStats): HostDiskFrame | null;
115
163
  /**
116
164
  * Measure this machine, or say honestly that we cannot.
117
165
  *
package/dist/host-load.js CHANGED
@@ -9,11 +9,13 @@ import path from 'node:path';
9
9
  * is on its knees», so it said nothing, and the first symptom a user saw was a
10
10
  * session that had gone quiet (plan `agent-sessions-host-resources.md` §5.3).
11
11
  *
12
- * Two files and nothing else. That is a security property, not an
13
- * implementation detail: this runner reports to a server, so what it may read
14
- * for telemetry is listed here in full — `/proc/loadavg` and `/proc/meminfo`,
15
- * both world-readable, neither containing a path, a command line, an
16
- * environment variable or a process name. No `/proc/<pid>` walk, no `ps`.
12
+ * Two files and one filesystem call, and nothing else. That is a security
13
+ * property, not an implementation detail: this runner reports to a server, so
14
+ * what it may read for telemetry is listed here in full — `/proc/loadavg` and
15
+ * `/proc/meminfo`, both world-readable, neither containing a path, a command
16
+ * line, an environment variable or a process name; and `statfs('/')` (0.71.0,
17
+ * #485), which answers four sizes of the root filesystem and nothing about what
18
+ * is on it. No `/proc/<pid>` walk, no `ps`, no directory listing, no `du`.
17
19
  *
18
20
  * **One module is allowed past that line, and it is named here so the rule and
19
21
  * the code cannot drift apart:** `session-stall.ts` reads `/proc/<pid>` for the
@@ -66,7 +68,36 @@ export const HOST_LOAD_HEARTBEAT_MS = 60_000;
66
68
  export const HOST_LOAD_MIN_DELTA_LOAD1 = 0.5;
67
69
  /** The same idea for memory, as a fraction — see `hostLoadChangedEnough`. */
68
70
  export const HOST_LOAD_MIN_DELTA_MEM_RATIO = 0.05;
71
+ /** The filesystem measured: the root, where running out stops everything. */
72
+ export const HOST_DISK_PATH = '/';
69
73
  const PROC_DIR = '/proc';
74
+ /**
75
+ * Measure the root filesystem, or say honestly that we cannot.
76
+ *
77
+ * Every number has to be a whole, non-negative, safely representable count of
78
+ * bytes that fits inside the total – the API's schema holds the same line, and a
79
+ * reading it would refuse is better not sent than sent. Any doubt is `null`.
80
+ */
81
+ export function readRootDisk(statfs = fs.statfsSync) {
82
+ try {
83
+ const stat = statfs(HOST_DISK_PATH);
84
+ const totalBytes = stat.blocks * stat.bsize;
85
+ const freeBytes = stat.bavail * stat.bsize;
86
+ const usedBytes = Math.max(0, totalBytes - stat.bfree * stat.bsize);
87
+ const whole = (value) => Number.isSafeInteger(value) && value >= 0;
88
+ if (!whole(totalBytes) || totalBytes === 0 || !whole(freeBytes) || !whole(usedBytes)) {
89
+ return null;
90
+ }
91
+ if (freeBytes > totalBytes || usedBytes > totalBytes)
92
+ return null;
93
+ return { path: HOST_DISK_PATH, totalBytes, freeBytes, usedBytes };
94
+ }
95
+ catch {
96
+ // ENOSYS in an odd container, EACCES under a locked-down profile: the disk
97
+ // is unknown, and the load beside it is still worth sending.
98
+ return null;
99
+ }
100
+ }
70
101
  /** kB in `/proc/meminfo` means kibibytes, and has since the field was added. */
71
102
  const MEMINFO_UNIT_BYTES = 1024;
72
103
  /**
@@ -158,6 +189,9 @@ export function readHostLoad(sources = {}) {
158
189
  memAvailableBytes,
159
190
  swapTotalBytes,
160
191
  swapFreeBytes,
192
+ // Its own failure is `null` inside the frame, never a missing frame: a
193
+ // machine whose filesystem refuses `statfs` still has a load worth showing.
194
+ disk: readRootDisk(sources.statfs),
161
195
  at,
162
196
  };
163
197
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.70.1";
1
+ export declare const RUNNER_VERSION = "0.71.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in sync with package.json by the release script (manual for now).
2
- export const RUNNER_VERSION = '0.70.1';
2
+ export const RUNNER_VERSION = '0.71.0';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge4dev/runner",
3
- "version": "0.70.1",
3
+ "version": "0.71.0",
4
4
  "description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
5
5
  "homepage": "https://bridge4.dev",
6
6
  "license": "MIT",