@x-tech-laboratories/x-libs 0.13.0 → 0.14.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.
Files changed (152) hide show
  1. package/README.md +1 -1
  2. package/dist/web/react/menus.js +11 -7
  3. package/package.json +1 -1
  4. package/dist/agent/client.d.ts +0 -32
  5. package/dist/agent/client.js +0 -148
  6. package/dist/agent/commands.d.ts +0 -23
  7. package/dist/agent/commands.js +0 -118
  8. package/dist/agent/keys.d.ts +0 -14
  9. package/dist/agent/keys.js +0 -81
  10. package/dist/agent/long_runs.d.ts +0 -32
  11. package/dist/agent/long_runs.js +0 -316
  12. package/dist/agent/protocol.d.ts +0 -148
  13. package/dist/agent/protocol.js +0 -131
  14. package/dist/agent/session.d.ts +0 -50
  15. package/dist/agent/session.js +0 -225
  16. package/dist/agent/tasks.d.ts +0 -17
  17. package/dist/agent/tasks.js +0 -15
  18. package/dist/backups.d.ts +0 -31
  19. package/dist/backups.js +0 -43
  20. package/dist/dates.d.ts +0 -9
  21. package/dist/dates.js +0 -60
  22. package/dist/durations.d.ts +0 -11
  23. package/dist/durations.js +0 -49
  24. package/dist/electron/app.d.ts +0 -13
  25. package/dist/electron/app.js +0 -27
  26. package/dist/electron/bridge.d.ts +0 -55
  27. package/dist/electron/bridge.js +0 -105
  28. package/dist/electron/events.d.ts +0 -21
  29. package/dist/electron/events.js +0 -25
  30. package/dist/electron/fetch.d.ts +0 -52
  31. package/dist/electron/fetch.js +0 -175
  32. package/dist/electron/window.d.ts +0 -96
  33. package/dist/electron/window.js +0 -128
  34. package/dist/log.d.ts +0 -30
  35. package/dist/log.js +0 -79
  36. package/dist/server/agents/agent.d.ts +0 -27
  37. package/dist/server/agents/agent.js +0 -16
  38. package/dist/server/agents/connection.d.ts +0 -51
  39. package/dist/server/agents/connection.js +0 -239
  40. package/dist/server/agents/hub.d.ts +0 -86
  41. package/dist/server/agents/hub.js +0 -241
  42. package/dist/server/agents/local.d.ts +0 -24
  43. package/dist/server/agents/local.js +0 -62
  44. package/dist/server/agents/route.d.ts +0 -3
  45. package/dist/server/agents/route.js +0 -24
  46. package/dist/server/backup_files.d.ts +0 -3
  47. package/dist/server/backup_files.js +0 -25
  48. package/dist/server/config.d.ts +0 -39
  49. package/dist/server/config.js +0 -109
  50. package/dist/server/cors.d.ts +0 -8
  51. package/dist/server/cors.js +0 -13
  52. package/dist/server/events.d.ts +0 -33
  53. package/dist/server/events.js +0 -68
  54. package/dist/server/hash_password.d.ts +0 -1
  55. package/dist/server/hash_password.js +0 -46
  56. package/dist/server/login_page.d.ts +0 -8
  57. package/dist/server/login_page.js +0 -79
  58. package/dist/server/passwords.d.ts +0 -8
  59. package/dist/server/passwords.js +0 -59
  60. package/dist/server/replies.d.ts +0 -12
  61. package/dist/server/replies.js +0 -41
  62. package/dist/server/sign_in.d.ts +0 -27
  63. package/dist/server/sign_in.js +0 -95
  64. package/dist/server/web_folder.d.ts +0 -11
  65. package/dist/server/web_folder.js +0 -40
  66. package/dist/web/dom.d.ts +0 -7
  67. package/dist/web/dom.js +0 -32
  68. package/dist/web/drag.d.ts +0 -3
  69. package/dist/web/drag.js +0 -13
  70. package/dist/web/errors.d.ts +0 -1
  71. package/dist/web/errors.js +0 -27
  72. package/dist/web/format.d.ts +0 -11
  73. package/dist/web/format.js +0 -82
  74. package/dist/web/menus.d.ts +0 -17
  75. package/dist/web/menus.js +0 -40
  76. package/dist/web/prefs.d.ts +0 -12
  77. package/dist/web/prefs.js +0 -62
  78. package/dist/web/react/badges.d.ts +0 -35
  79. package/dist/web/react/badges.js +0 -32
  80. package/dist/web/react/controls.d.ts +0 -86
  81. package/dist/web/react/controls.js +0 -154
  82. package/dist/web/react/dialog.d.ts +0 -45
  83. package/dist/web/react/dialog.js +0 -82
  84. package/dist/web/react/following.d.ts +0 -25
  85. package/dist/web/react/following.js +0 -34
  86. package/dist/web/react/layout.d.ts +0 -111
  87. package/dist/web/react/layout.js +0 -85
  88. package/dist/web/react/list_picker.d.ts +0 -19
  89. package/dist/web/react/list_picker.js +0 -50
  90. package/dist/web/react/markdown.d.ts +0 -8
  91. package/dist/web/react/markdown.js +0 -40
  92. package/dist/web/react/media.d.ts +0 -24
  93. package/dist/web/react/media.js +0 -18
  94. package/dist/web/react/menu_button.d.ts +0 -10
  95. package/dist/web/react/menu_button.js +0 -16
  96. package/dist/web/react/menus.d.ts +0 -31
  97. package/dist/web/react/page_view.d.ts +0 -16
  98. package/dist/web/react/page_view.js +0 -6
  99. package/dist/web/react/prefs.d.ts +0 -2
  100. package/dist/web/react/prefs.js +0 -11
  101. package/dist/web/react/progress.d.ts +0 -10
  102. package/dist/web/react/progress.js +0 -9
  103. package/dist/web/react/rating.d.ts +0 -8
  104. package/dist/web/react/rating.js +0 -23
  105. package/dist/web/react/reveal.d.ts +0 -10
  106. package/dist/web/react/reveal.js +0 -30
  107. package/dist/web/react/shell.d.ts +0 -45
  108. package/dist/web/react/shell.js +0 -94
  109. package/dist/web/react/shortcuts.d.ts +0 -23
  110. package/dist/web/react/shortcuts.js +0 -95
  111. package/dist/web/react/split.d.ts +0 -23
  112. package/dist/web/react/split.js +0 -94
  113. package/dist/web/react/states.d.ts +0 -36
  114. package/dist/web/react/states.js +0 -64
  115. package/dist/web/react/store.d.ts +0 -4
  116. package/dist/web/react/store.js +0 -7
  117. package/dist/web/react/suggestions.d.ts +0 -23
  118. package/dist/web/react/suggestions.js +0 -57
  119. package/dist/web/react/table.d.ts +0 -77
  120. package/dist/web/react/table.js +0 -398
  121. package/dist/web/react/test_stubs.d.ts +0 -1
  122. package/dist/web/react/test_stubs.js +0 -25
  123. package/dist/web/react/time.d.ts +0 -5
  124. package/dist/web/react/time.js +0 -19
  125. package/dist/web/react/toasts.d.ts +0 -29
  126. package/dist/web/react/toasts.js +0 -72
  127. package/dist/web/react/tree.d.ts +0 -28
  128. package/dist/web/react/tree.js +0 -183
  129. package/dist/web/react/volume.d.ts +0 -8
  130. package/dist/web/react/volume.js +0 -42
  131. package/dist/web/react/watch.d.ts +0 -3
  132. package/dist/web/react/watch.js +0 -5
  133. package/dist/web/router.d.ts +0 -7
  134. package/dist/web/router.js +0 -29
  135. package/dist/web/selection.d.ts +0 -25
  136. package/dist/web/selection.js +0 -106
  137. package/dist/web/split.d.ts +0 -15
  138. package/dist/web/split.js +0 -104
  139. package/dist/web/states.d.ts +0 -3
  140. package/dist/web/states.js +0 -10
  141. package/dist/web/store.d.ts +0 -11
  142. package/dist/web/store.js +0 -17
  143. package/dist/web/tables.d.ts +0 -28
  144. package/dist/web/tables.js +0 -88
  145. package/dist/web/theme.d.ts +0 -14
  146. package/dist/web/theme.js +0 -52
  147. package/dist/web/tree.d.ts +0 -40
  148. package/dist/web/tree.js +0 -130
  149. package/dist/web/virtual.d.ts +0 -7
  150. package/dist/web/virtual.js +0 -10
  151. package/dist/web/watch.d.ts +0 -11
  152. package/dist/web/watch.js +0 -37
@@ -1,316 +0,0 @@
1
- // Long runs: tasks that keep going on their own, in their own process group, so they outlive the connection that
2
- // started them and the agent itself. Each writes its output and exit code into its own folder under the state
3
- // directory, which is all a later agent needs to report on it, follow it, or stop it.
4
- //
5
- // A run's folder holds `run.json` (its pid and when the process began), `out` (stdout and stderr together), `exit`
6
- // once it has ended, and `stopped` once it has been asked to stop. Its stdin, which may carry a secret, is handed over
7
- // through a pipe and never written down.
8
- // ── Imports ──────────────────────────────────────────────────────────────────
9
- import { spawn } from "node:child_process";
10
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
11
- import { open } from "node:fs/promises";
12
- import { constants } from "node:os";
13
- import { join } from "node:path";
14
- import { Readable } from "node:stream";
15
- import { kill_grace_in_milliseconds, signal_group } from "./commands.js";
16
- import { is_run_id } from "./protocol.js";
17
- // ── Settings ─────────────────────────────────────────────────────────────────
18
- // A poll returns at most this much output, and the next poll carries on from there.
19
- export const poll_limit_in_bytes = 1024 * 1024;
20
- // A followed run is read in pieces of this size.
21
- const tail_chunk_in_bytes = 64 * 1024;
22
- export class LongRunError extends Error {
23
- constructor(message) {
24
- super(message);
25
- this.name = "LongRunError";
26
- }
27
- }
28
- // ── Long runs ────────────────────────────────────────────────────────────────
29
- export class LongRuns {
30
- directory;
31
- grace;
32
- tail_interval;
33
- runner;
34
- // The runs this agent started, whose exit it hears of directly.
35
- children = new Map();
36
- constructor(state_directory, runner, options = {}) {
37
- this.directory = join(state_directory, "runs");
38
- this.grace = options.kill_grace_in_milliseconds ?? kill_grace_in_milliseconds;
39
- this.tail_interval = options.tail_interval_in_milliseconds ?? 250;
40
- this.runner = runner;
41
- mkdirSync(this.directory, { recursive: true, mode: 0o700 });
42
- }
43
- // ── Starting ─────────────────────────────────────────────────────────────
44
- start(run_id, task, stdin) {
45
- const folder = this.folder(run_id);
46
- if (existsSync(folder))
47
- throw new LongRunError(`A run named '${run_id}' already exists.`);
48
- mkdirSync(folder, { mode: 0o700 });
49
- writeFileSync(join(folder, "out"), "", { mode: 0o600 });
50
- const launch = this.runner.launch(task, folder, stdin !== null);
51
- const child = spawn(launch.program, launch.args, {
52
- detached: true,
53
- stdio: [stdin === null ? "ignore" : "pipe", "ignore", "ignore"],
54
- });
55
- if (child.pid === undefined) {
56
- rmSync(folder, { recursive: true, force: true });
57
- throw new LongRunError(`Could not start ${launch.program}.`);
58
- }
59
- if (stdin !== null) {
60
- child.stdin.on("error", () => { });
61
- child.stdin.end(stdin);
62
- }
63
- // A process killed before it could write its exit code is given the shell's code for that signal.
64
- child.on("exit", (code, signal) => {
65
- this.children.delete(run_id);
66
- const exit_file = join(folder, "exit");
67
- if (existsSync(folder) && !existsSync(exit_file)) {
68
- const exit_code = code ?? 128 + (signal === null ? 0 : constants.signals[signal]);
69
- writeFileSync(exit_file, `${exit_code}\n`);
70
- }
71
- });
72
- child.on("error", () => { });
73
- child.unref();
74
- this.children.set(run_id, child);
75
- const record = { run_id, pid: child.pid, start_ticks: start_ticks_of(child.pid), started_epoch: now_seconds() };
76
- write_atomically(join(folder, "run.json"), JSON.stringify(record));
77
- return this.describe(record);
78
- }
79
- // ── Reading ──────────────────────────────────────────────────────────────
80
- // Every run in the state directory, as found now, so a new agent knows the ones an earlier one started.
81
- list() {
82
- const runs = [];
83
- for (const name of readdirSync(this.directory)) {
84
- const record = this.record(name);
85
- if (record !== null)
86
- runs.push(this.describe(record));
87
- }
88
- return runs.sort((first, second) => first.started_epoch - second.started_epoch || first.run_id.localeCompare(second.run_id));
89
- }
90
- get(run_id) {
91
- return this.describe(this.require(run_id));
92
- }
93
- async poll(run_id, offset, limit = poll_limit_in_bytes) {
94
- const record = this.require(run_id);
95
- // The state is read before the output, so output read after an ending is known to be complete.
96
- const run = this.describe(record);
97
- const out = join(this.folder(run_id), "out");
98
- const size = statSync(out).size;
99
- const start = Math.max(0, Math.min(Math.trunc(offset), size));
100
- let bytes = await read_range(out, start, Math.min(limit, size - start));
101
- if (start + bytes.length < size)
102
- bytes = whole_text(bytes);
103
- return {
104
- state: run.state,
105
- exit_code: run.exit_code,
106
- alive: run.state === "running",
107
- size,
108
- new_output: bytes.toString("utf8"),
109
- next_offset: start + bytes.length,
110
- };
111
- }
112
- // Follows a run's output from an offset as it is written, ending once the run has ended and all of it is read.
113
- tail(run_id, offset) {
114
- this.require(run_id);
115
- const out = join(this.folder(run_id), "out");
116
- let position = Math.max(0, Math.trunc(offset));
117
- let following = true;
118
- let reading = false;
119
- let wait_timer;
120
- let wake;
121
- let settle;
122
- const done = new Promise((resolve) => (settle = resolve));
123
- const pause = () => new Promise((resolve) => {
124
- wake = resolve;
125
- wait_timer = setTimeout(resolve, this.tail_interval);
126
- });
127
- const pump = async () => {
128
- if (reading || !following)
129
- return;
130
- reading = true;
131
- try {
132
- while (following) {
133
- const run = this.describe(this.require(run_id));
134
- const chunk = await read_range(out, position, tail_chunk_in_bytes);
135
- if (!following)
136
- return;
137
- if (chunk.length > 0) {
138
- position += chunk.length;
139
- if (!stdout.push(chunk))
140
- return;
141
- continue;
142
- }
143
- if (run.state !== "running") {
144
- following = false;
145
- stdout.push(null);
146
- settle({ exit_code: run.exit_code ?? -1, stderr: run.state === "exited" ? "" : `The run was ${run.state}.` });
147
- return;
148
- }
149
- await pause();
150
- }
151
- }
152
- catch (error) {
153
- following = false;
154
- stdout.destroy();
155
- settle({ exit_code: -1, stderr: `Could not follow the run: ${error.message}` });
156
- }
157
- finally {
158
- reading = false;
159
- }
160
- };
161
- const stdout = new Readable({
162
- read: () => void pump(),
163
- destroy: (error, callback) => {
164
- if (following)
165
- settle({ exit_code: -1, stderr: "Stopped following the run." });
166
- following = false;
167
- clearTimeout(wait_timer);
168
- wake?.();
169
- callback(error);
170
- },
171
- });
172
- return { stdout, done, stop: () => stdout.destroy() };
173
- }
174
- // ── Stopping ─────────────────────────────────────────────────────────────
175
- // Asks the run's process group to stop, kills it after the grace period, and settles once it has ended.
176
- async stop(run_id) {
177
- const record = this.require(run_id);
178
- if (this.describe(record).state !== "running")
179
- return this.describe(record);
180
- writeFileSync(join(this.folder(run_id), "stopped"), "");
181
- signal_group(record.pid, "SIGTERM");
182
- const deadline = Date.now() + this.grace;
183
- while (this.is_alive(record)) {
184
- if (Date.now() >= deadline) {
185
- signal_group(record.pid, "SIGKILL");
186
- break;
187
- }
188
- await new Promise((resolve) => setTimeout(resolve, 50));
189
- }
190
- while (this.is_alive(record))
191
- await new Promise((resolve) => setTimeout(resolve, 50));
192
- return this.describe(record);
193
- }
194
- // Removes an ended run's folder.
195
- forget(run_id) {
196
- const record = this.require(run_id);
197
- if (this.describe(record).state === "running")
198
- throw new LongRunError(`The run '${run_id}' is still running.`);
199
- rmSync(this.folder(run_id), { recursive: true, force: true });
200
- }
201
- // Forgets runs that ended longer ago than the age given.
202
- prune(max_age_in_seconds) {
203
- const cutoff = Date.now() - max_age_in_seconds * 1000;
204
- for (const run of this.list()) {
205
- if (run.state === "running")
206
- continue;
207
- const exit_file = join(this.folder(run.run_id), "exit");
208
- const ended = existsSync(exit_file) ? statSync(exit_file).mtimeMs : run.started_epoch * 1000;
209
- if (ended < cutoff)
210
- this.forget(run.run_id);
211
- }
212
- }
213
- // ── Helpers ──────────────────────────────────────────────────────────────
214
- folder(run_id) {
215
- if (!is_run_id(run_id))
216
- throw new LongRunError(`'${run_id}' is not a valid run id.`);
217
- return join(this.directory, run_id);
218
- }
219
- record(run_id) {
220
- if (!is_run_id(run_id))
221
- return null;
222
- try {
223
- return JSON.parse(readFileSync(join(this.folder(run_id), "run.json"), "utf8"));
224
- }
225
- catch {
226
- return null;
227
- }
228
- }
229
- require(run_id) {
230
- const record = this.record(run_id);
231
- if (record === null)
232
- throw new LongRunError(`No run named '${run_id}'.`);
233
- return record;
234
- }
235
- is_alive(record) {
236
- const child = this.children.get(record.run_id);
237
- if (child !== undefined)
238
- return child.exitCode === null && child.signalCode === null;
239
- try {
240
- process.kill(record.pid, 0);
241
- }
242
- catch (error) {
243
- if (error.code !== "EPERM")
244
- return false;
245
- }
246
- return record.start_ticks === null || start_ticks_of(record.pid) === record.start_ticks;
247
- }
248
- describe(record) {
249
- const folder = this.folder(record.run_id);
250
- const stopped = existsSync(join(folder, "stopped"));
251
- const exit_code = read_exit_code(join(folder, "exit"));
252
- let state;
253
- if (exit_code !== null)
254
- state = stopped ? "stopped" : "exited";
255
- else if (this.is_alive(record))
256
- state = "running";
257
- else
258
- state = stopped ? "stopped" : "lost";
259
- return { run_id: record.run_id, pid: record.pid, started_epoch: record.started_epoch, state, exit_code };
260
- }
261
- }
262
- // ── Files ────────────────────────────────────────────────────────────────────
263
- function read_exit_code(path) {
264
- try {
265
- const text = readFileSync(path, "utf8").trim();
266
- return /^-?\d+$/.test(text) ? Number(text) : null;
267
- }
268
- catch {
269
- return null;
270
- }
271
- }
272
- async function read_range(path, offset, length) {
273
- if (length <= 0)
274
- return Buffer.alloc(0);
275
- const handle = await open(path, "r");
276
- try {
277
- const buffer = Buffer.alloc(length);
278
- const { bytesRead } = await handle.read(buffer, 0, length, offset);
279
- return buffer.subarray(0, bytesRead);
280
- }
281
- finally {
282
- await handle.close();
283
- }
284
- }
285
- // Cuts a piece of output back to its last whole line, or failing that its last whole character.
286
- function whole_text(bytes) {
287
- const last_newline = bytes.lastIndexOf(0x0a);
288
- if (last_newline !== -1)
289
- return bytes.subarray(0, last_newline + 1);
290
- let start = bytes.length - 1;
291
- while (start > 0 && start > bytes.length - 4 && (bytes[start] & 0xc0) === 0x80)
292
- start--;
293
- const lead = bytes[start];
294
- const needed = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
295
- return bytes.length - start < needed ? bytes.subarray(0, start) : bytes;
296
- }
297
- function write_atomically(path, text) {
298
- writeFileSync(`${path}.part`, text, { mode: 0o600 });
299
- renameSync(`${path}.part`, path);
300
- }
301
- // When the process began, from /proc; null where /proc is missing.
302
- function start_ticks_of(pid) {
303
- try {
304
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
305
- // The fields after the command name, which may itself hold spaces and parentheses; start time is field 22.
306
- const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
307
- const ticks = Number(fields[19]);
308
- return Number.isFinite(ticks) ? ticks : null;
309
- }
310
- catch {
311
- return null;
312
- }
313
- }
314
- function now_seconds() {
315
- return Math.floor(Date.now() / 1000);
316
- }
@@ -1,148 +0,0 @@
1
- export interface AgentApp<Task> {
2
- name: string;
3
- protocol_version: number;
4
- task_field: string;
5
- is_task(value: unknown): value is Task;
6
- }
7
- export declare function shell_app(name: string, protocol_version?: number): AgentApp<string>;
8
- export interface RunResult {
9
- exit_code: number;
10
- stdout: string;
11
- stderr: string;
12
- }
13
- export type LongRunState = "running" | "exited" | "stopped" | "lost";
14
- export interface LongRun {
15
- run_id: string;
16
- pid: number;
17
- started_epoch: number;
18
- state: LongRunState;
19
- exit_code: number | null;
20
- }
21
- export interface LongRunPoll {
22
- state: LongRunState;
23
- exit_code: number | null;
24
- alive: boolean;
25
- size: number;
26
- new_output: string;
27
- next_offset: number;
28
- }
29
- export interface AppMessage {
30
- type: string;
31
- [field: string]: unknown;
32
- }
33
- export interface Requests<Task> {
34
- run: {
35
- body: {
36
- task: Task;
37
- stdin: string | null;
38
- timeout_in_seconds: number;
39
- };
40
- reply: RunResult;
41
- };
42
- start: {
43
- body: {
44
- run_id: string;
45
- task: Task;
46
- stdin: string | null;
47
- };
48
- reply: LongRun;
49
- };
50
- poll: {
51
- body: {
52
- run_id: string;
53
- offset: number;
54
- };
55
- reply: LongRunPoll;
56
- };
57
- stop: {
58
- body: {
59
- run_id: string;
60
- };
61
- reply: LongRun;
62
- };
63
- forget: {
64
- body: {
65
- run_id: string;
66
- };
67
- reply: null;
68
- };
69
- runs: {
70
- body: Record<never, never>;
71
- reply: LongRun[];
72
- };
73
- }
74
- export type RequestType = keyof Requests<unknown>;
75
- export type RequestMessage<Task> = {
76
- [K in RequestType]: {
77
- type: K;
78
- id: string;
79
- } & Requests<Task>[K]["body"];
80
- }[RequestType];
81
- export type ServerMessage<Task> = RequestMessage<Task> | {
82
- type: "stream";
83
- id: string;
84
- task: Task;
85
- stdin: string | null;
86
- window: number;
87
- } | {
88
- type: "attach";
89
- id: string;
90
- run_id: string;
91
- offset: number;
92
- window: number;
93
- } | {
94
- type: "credit";
95
- id: string;
96
- bytes: number;
97
- } | {
98
- type: "cancel";
99
- id: string;
100
- } | {
101
- type: "ping";
102
- epoch_ms: number;
103
- } | {
104
- type: "pong";
105
- epoch_ms: number;
106
- } | {
107
- type: "challenge";
108
- nonce: string;
109
- };
110
- export type AgentMessage = {
111
- type: "hello";
112
- protocol: number;
113
- host: string;
114
- agent_version: string;
115
- runs: LongRun[];
116
- signature?: string;
117
- } | {
118
- type: "reply";
119
- id: string;
120
- value: unknown;
121
- } | {
122
- type: "failed";
123
- id: string;
124
- error: string;
125
- } | {
126
- type: "stream_end";
127
- id: string;
128
- exit_code: number;
129
- stderr: string;
130
- } | {
131
- type: "ping";
132
- epoch_ms: number;
133
- } | {
134
- type: "pong";
135
- epoch_ms: number;
136
- };
137
- export declare function parse_server_message<Task>(text: string, app: AgentApp<Task>): ServerMessage<Task> | AppMessage | null;
138
- export declare function parse_agent_message(text: string, app: AgentApp<unknown>): AgentMessage | AppMessage | null;
139
- export declare function is_server_message<Task>(message: ServerMessage<Task> | AppMessage): message is ServerMessage<Task>;
140
- export declare function is_agent_message(message: AgentMessage | AppMessage): message is AgentMessage;
141
- export declare function encode_message<Task>(message: ServerMessage<Task> | AgentMessage | AppMessage, app: AgentApp<Task>): string;
142
- export declare function encode_chunk(id: string, data: Uint8Array): Buffer<ArrayBuffer>;
143
- export declare function decode_chunk(frame: Uint8Array): {
144
- id: string;
145
- data: Buffer;
146
- } | null;
147
- export declare function key_host_header(app_name: string): string;
148
- export declare function is_run_id(value: string): boolean;
@@ -1,131 +0,0 @@
1
- // The messages the server and a host's agent exchange over the agent's WebSocket, shared by both sides.
2
- //
3
- // Every message but a stream's output is one JSON text frame with a `type`. A request from the server carries an `id`
4
- // the agent answers with one `reply` or `failed` of the same id. A stream or an attach sends its output as binary
5
- // frames tagged with its id, only as far as the credit the server has granted, then one `stream_end`. Either side
6
- // sends `ping` while idle and drops a connection that has been silent too long.
7
- //
8
- // A run, a long run and a stream each carry the app's task: a shell command for one app, a typed operation for another.
9
- // The app names the field its task travels in and checks it, so each app keeps its own wire format and version.
10
- //
11
- // An agent logging in with its host's key, rather than a token, names the host in the upgrade; the server's first
12
- // message is then a `challenge`, and the agent's hello carries its signature of that nonce (see keys.ts).
13
- //
14
- // An app adds its own messages beside these: any other `type` is handed to the app, and one carrying a string `id` is
15
- // a request the app's handler answers like any other.
16
- // An app whose tasks are shell command lines, sent in a `command` field.
17
- export function shell_app(name, protocol_version = 1) {
18
- return { name, protocol_version, task_field: "command", is_task: (value) => typeof value === "string" };
19
- }
20
- const id_fields = { id: "string" };
21
- const run_fields = { ...id_fields, run_id: "string" };
22
- const server_fields = {
23
- run: { ...id_fields, task: "task", stdin: "string_or_null", timeout_in_seconds: "number" },
24
- start: { ...run_fields, task: "task", stdin: "string_or_null" },
25
- poll: { ...run_fields, offset: "number" },
26
- stop: run_fields,
27
- forget: run_fields,
28
- runs: id_fields,
29
- stream: { ...id_fields, task: "task", stdin: "string_or_null", window: "number" },
30
- attach: { ...run_fields, offset: "number", window: "number" },
31
- credit: { ...id_fields, bytes: "number" },
32
- cancel: id_fields,
33
- ping: { epoch_ms: "number" },
34
- pong: { epoch_ms: "number" },
35
- challenge: { nonce: "string" },
36
- };
37
- const agent_fields = {
38
- hello: { protocol: "number", host: "string", agent_version: "string", runs: "array" },
39
- reply: { ...id_fields, value: "any" },
40
- failed: { ...id_fields, error: "string" },
41
- stream_end: { ...id_fields, exit_code: "number", stderr: "string" },
42
- ping: { epoch_ms: "number" },
43
- pong: { epoch_ms: "number" },
44
- };
45
- function has_kind(value, kind, app) {
46
- if (kind === "any")
47
- return value !== undefined;
48
- if (kind === "task")
49
- return app.is_task(value);
50
- if (kind === "array")
51
- return Array.isArray(value);
52
- if (kind === "string_or_null")
53
- return value === null || typeof value === "string";
54
- if (kind === "number")
55
- return typeof value === "number" && Number.isFinite(value);
56
- return typeof value === kind;
57
- }
58
- // A known message with every field it needs, an app's message for any other type, or null for one that is neither.
59
- function parse_with(text, fields, app) {
60
- let message;
61
- try {
62
- message = JSON.parse(text);
63
- }
64
- catch {
65
- return null;
66
- }
67
- if (typeof message !== "object" || message === null || Array.isArray(message))
68
- return null;
69
- const record = message;
70
- if (typeof record.type !== "string" || record.type === "")
71
- return null;
72
- if (!Object.hasOwn(fields, record.type))
73
- return record;
74
- // The task arrives in the app's own field and is read as `task`.
75
- const known_fields = fields[record.type];
76
- if (Object.hasOwn(known_fields, "task") && app.task_field !== "task") {
77
- record.task = record[app.task_field];
78
- delete record[app.task_field];
79
- }
80
- for (const [name, kind] of Object.entries(known_fields)) {
81
- if (!has_kind(record[name], kind, app))
82
- return null;
83
- }
84
- return message;
85
- }
86
- // A message from the server, or null for one that is not a message at all.
87
- export function parse_server_message(text, app) {
88
- return parse_with(text, server_fields, app);
89
- }
90
- // A message from an agent, or null for one that is not a message at all.
91
- export function parse_agent_message(text, app) {
92
- return parse_with(text, agent_fields, app);
93
- }
94
- // Whether a parsed message is one of this protocol's own rather than the app's.
95
- export function is_server_message(message) {
96
- return Object.hasOwn(server_fields, message.type);
97
- }
98
- export function is_agent_message(message) {
99
- return Object.hasOwn(agent_fields, message.type);
100
- }
101
- // The message as one text frame, with any task in the app's own field.
102
- export function encode_message(message, app) {
103
- if (!Object.hasOwn(server_fields, message.type) || !("task" in message))
104
- return JSON.stringify(message);
105
- const { task, ...rest } = message;
106
- return JSON.stringify({ ...rest, [app.task_field]: task });
107
- }
108
- // ── Output frames ────────────────────────────────────────────────────────────
109
- // A binary frame is one byte giving the id's length, the id, then the output bytes.
110
- export function encode_chunk(id, data) {
111
- const id_bytes = Buffer.from(id, "utf8");
112
- if (id_bytes.length > 255)
113
- throw new Error("A stream id is at most 255 bytes.");
114
- return Buffer.concat([Buffer.from([id_bytes.length]), id_bytes, data]);
115
- }
116
- export function decode_chunk(frame) {
117
- const bytes = Buffer.from(frame.buffer, frame.byteOffset, frame.byteLength);
118
- if (bytes.length < 1 || bytes.length < 1 + bytes[0])
119
- return null;
120
- return { id: bytes.subarray(1, 1 + bytes[0]).toString("utf8"), data: bytes.subarray(1 + bytes[0]) };
121
- }
122
- // ── Key login ────────────────────────────────────────────────────────────────
123
- // The upgrade header an agent logging in with its key names its host in, such as "x-nix-agent-host".
124
- export function key_host_header(app_name) {
125
- return `${app_name}-agent-host`;
126
- }
127
- // ── Long run ids ─────────────────────────────────────────────────────────────
128
- // A long run's id names its folder on the agent, so it is kept to safe characters.
129
- export function is_run_id(value) {
130
- return /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(value);
131
- }
@@ -1,50 +0,0 @@
1
- import type { KeyObject } from "node:crypto";
2
- import type { LongRuns } from "./long_runs.ts";
3
- import { type AgentApp, type AgentMessage, type AppMessage } from "./protocol.ts";
4
- import type { TaskRunner } from "./tasks.ts";
5
- export type AppRequests = Record<string, (message: AppMessage) => unknown>;
6
- export interface SessionHooks<Task> {
7
- requests?: AppRequests;
8
- on_message?: (message: AppMessage, session: AgentSession<Task>) => void;
9
- on_hello?: (session: AgentSession<Task>) => void;
10
- }
11
- export interface SessionOptions<Task> extends SessionHooks<Task> {
12
- app: AgentApp<Task>;
13
- runner: TaskRunner<Task>;
14
- host: string;
15
- agent_version: string;
16
- heartbeat_in_milliseconds: number;
17
- key?: KeyObject | null;
18
- }
19
- export interface MessageSocket {
20
- readonly readyState: number;
21
- send(data: string | Uint8Array<ArrayBuffer>): void;
22
- close(code?: number, reason?: string): void;
23
- }
24
- export declare class AgentSession<Task> {
25
- private readonly socket;
26
- private readonly runs;
27
- private readonly options;
28
- private readonly pumps;
29
- private last_heard;
30
- private readonly heartbeat;
31
- private closed;
32
- private greeted;
33
- constructor(socket: MessageSocket, runs: LongRuns<Task>, options: SessionOptions<Task>);
34
- private say_hello;
35
- receive(text: string): void;
36
- close(): void;
37
- send(message: AgentMessage | AppMessage): void;
38
- send_chunk(id: string, data: Buffer): void;
39
- end_stream(id: string, result: {
40
- exit_code: number;
41
- stderr: string;
42
- }): void;
43
- private handle;
44
- private handle_app;
45
- private start_pump;
46
- private answer;
47
- private open_stream;
48
- private value_for;
49
- private beat;
50
- }