@x-tech-laboratories/x-libs 0.2.0 → 0.3.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 +12 -5
- package/dist/agent/client.d.ts +9 -6
- package/dist/agent/client.js +3 -3
- package/dist/agent/keys.js +31 -119
- package/dist/agent/long_runs.d.ts +5 -5
- package/dist/agent/long_runs.js +9 -10
- package/dist/agent/protocol.d.ts +20 -14
- package/dist/agent/protocol.js +34 -17
- package/dist/agent/session.d.ts +11 -8
- package/dist/agent/session.js +13 -9
- package/dist/agent/tasks.d.ts +17 -0
- package/dist/agent/tasks.js +15 -0
- package/dist/durations.js +18 -20
- package/dist/server/agents/agent.d.ts +4 -4
- package/dist/server/agents/connection.d.ts +8 -7
- package/dist/server/agents/connection.js +11 -9
- package/dist/server/agents/hub.d.ts +12 -12
- package/dist/server/agents/hub.js +14 -14
- package/dist/server/agents/local.d.ts +10 -7
- package/dist/server/agents/local.js +12 -9
- package/dist/server/agents/route.d.ts +1 -1
- package/dist/server/hash_password.js +16 -34
- package/dist/web/format.js +25 -29
- package/package.json +9 -2
package/dist/agent/session.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// One connection to the server: says hello, with its signature of the server's challenge when it logs in with a key,
|
|
2
2
|
// answers each request, and sends streamed output only as far as the server's credit allows. Closing it stops the
|
|
3
3
|
// streams it was sending; long runs carry on.
|
|
4
|
-
import { run_command, stream_command } from "./commands.js";
|
|
5
4
|
import { sign_challenge } from "./keys.js";
|
|
6
|
-
import { encode_chunk, encode_message, is_server_message, parse_server_message,
|
|
5
|
+
import { encode_chunk, encode_message, is_server_message, parse_server_message, } from "./protocol.js";
|
|
7
6
|
const socket_open = 1;
|
|
8
7
|
// ── Output ───────────────────────────────────────────────────────────────────
|
|
9
8
|
// Sends a stream's stdout as binary frames within the credit granted, then its end once all of it has gone.
|
|
@@ -90,14 +89,14 @@ export class AgentSession {
|
|
|
90
89
|
}
|
|
91
90
|
say_hello(signature) {
|
|
92
91
|
this.greeted = true;
|
|
93
|
-
const hello = { type: "hello", protocol: protocol_version, host: this.options.host, agent_version: this.options.agent_version, runs: this.runs.list() };
|
|
92
|
+
const hello = { type: "hello", protocol: this.options.app.protocol_version, host: this.options.host, agent_version: this.options.agent_version, runs: this.runs.list() };
|
|
94
93
|
this.send(signature === undefined ? hello : { ...hello, signature });
|
|
95
94
|
this.options.on_hello?.(this);
|
|
96
95
|
}
|
|
97
96
|
// Handles one text frame from the server; anything that is not a message is ignored.
|
|
98
97
|
receive(text) {
|
|
99
98
|
this.last_heard = Date.now();
|
|
100
|
-
const message = parse_server_message(text);
|
|
99
|
+
const message = parse_server_message(text, this.options.app);
|
|
101
100
|
if (message === null)
|
|
102
101
|
return;
|
|
103
102
|
if (is_server_message(message))
|
|
@@ -118,7 +117,7 @@ export class AgentSession {
|
|
|
118
117
|
// ── Sending ──────────────────────────────────────────────────────────────
|
|
119
118
|
send(message) {
|
|
120
119
|
if (!this.closed && this.socket.readyState === socket_open)
|
|
121
|
-
this.socket.send(encode_message(message));
|
|
120
|
+
this.socket.send(encode_message(message, this.options.app));
|
|
122
121
|
}
|
|
123
122
|
send_chunk(id, data) {
|
|
124
123
|
if (!this.closed && this.socket.readyState === socket_open)
|
|
@@ -138,7 +137,7 @@ export class AgentSession {
|
|
|
138
137
|
return;
|
|
139
138
|
case "challenge":
|
|
140
139
|
if (this.options.key && !this.greeted)
|
|
141
|
-
this.say_hello(sign_challenge(this.options.key, this.options.app, this.options.host, message.nonce));
|
|
140
|
+
this.say_hello(sign_challenge(this.options.key, this.options.app.name, this.options.host, message.nonce));
|
|
142
141
|
return;
|
|
143
142
|
case "credit":
|
|
144
143
|
this.pumps.get(message.id)?.grant(message.bytes);
|
|
@@ -147,7 +146,7 @@ export class AgentSession {
|
|
|
147
146
|
this.pumps.get(message.id)?.cancel();
|
|
148
147
|
return;
|
|
149
148
|
case "stream":
|
|
150
|
-
this.start_pump(message.id, () =>
|
|
149
|
+
this.start_pump(message.id, () => this.open_stream(message.task, message.stdin), message.window);
|
|
151
150
|
return;
|
|
152
151
|
case "attach":
|
|
153
152
|
this.start_pump(message.id, () => this.runs.tail(message.run_id, message.offset), message.window);
|
|
@@ -192,12 +191,17 @@ export class AgentSession {
|
|
|
192
191
|
this.send({ type: "failed", id, error: error.message });
|
|
193
192
|
}
|
|
194
193
|
}
|
|
194
|
+
open_stream(task, stdin) {
|
|
195
|
+
if (this.options.runner.stream === undefined)
|
|
196
|
+
throw new Error("This agent does not stream its tasks.");
|
|
197
|
+
return this.options.runner.stream(task, { stdin });
|
|
198
|
+
}
|
|
195
199
|
async value_for(message) {
|
|
196
200
|
switch (message.type) {
|
|
197
201
|
case "run":
|
|
198
|
-
return
|
|
202
|
+
return this.options.runner.run(message.task, { timeout_in_seconds: message.timeout_in_seconds, stdin: message.stdin });
|
|
199
203
|
case "start":
|
|
200
|
-
return this.runs.start(message.run_id, message.
|
|
204
|
+
return this.runs.start(message.run_id, message.task, message.stdin);
|
|
201
205
|
case "poll":
|
|
202
206
|
return this.runs.poll(message.run_id, message.offset);
|
|
203
207
|
case "stop":
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CommandStream } from "./commands.ts";
|
|
2
|
+
import type { RunResult } from "./protocol.ts";
|
|
3
|
+
export interface LongRunLaunch {
|
|
4
|
+
program: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface TaskRunner<Task> {
|
|
8
|
+
run(task: Task, options: {
|
|
9
|
+
timeout_in_seconds: number;
|
|
10
|
+
stdin?: string | null;
|
|
11
|
+
}): Promise<RunResult>;
|
|
12
|
+
stream?(task: Task, options: {
|
|
13
|
+
stdin?: string | null;
|
|
14
|
+
}): CommandStream;
|
|
15
|
+
launch(task: Task, folder: string, has_stdin: boolean): LongRunLaunch;
|
|
16
|
+
}
|
|
17
|
+
export declare function shell_runner(process_name: string): TaskRunner<string>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// How an agent carries out the app's tasks: to their end, as a stream, or as a long run's own process. Shell tasks, a
|
|
2
|
+
// command line run by sh, come built in; an app with typed operations gives its own.
|
|
3
|
+
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
4
|
+
import { run_command, stream_command } from "./commands.js";
|
|
5
|
+
// ── Shell ────────────────────────────────────────────────────────────────────
|
|
6
|
+
// The outer shell runs the command with its output in the run's folder, then writes the exit code in one rename.
|
|
7
|
+
const wrapper = 'sh -c "$2" > "$1/out" 2>&1; echo $? > "$1/exit.part"; mv -f "$1/exit.part" "$1/exit"';
|
|
8
|
+
// Runs each task as a command line under sh; `process_name`, such as "x-backups-run", is what ps shows for a long run.
|
|
9
|
+
export function shell_runner(process_name) {
|
|
10
|
+
return {
|
|
11
|
+
run: (command, options) => run_command(command, options),
|
|
12
|
+
stream: (command, options) => stream_command(command, options),
|
|
13
|
+
launch: (command, folder) => ({ program: "sh", args: ["-c", wrapper, process_name, folder, command] }),
|
|
14
|
+
};
|
|
15
|
+
}
|
package/dist/durations.js
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
// Reads durations such as "2h30m" from settings and writes them back in words, for the server and the agent alike.
|
|
2
|
-
|
|
3
|
-
const unit_names = [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// A duration in seconds; `key` names the setting in the error.
|
|
2
|
+
import parse from "parse-duration";
|
|
3
|
+
const unit_names = [["weeks", 604800], ["days", 86400], ["hours", 3600], ["minutes", 60], ["seconds", 1]];
|
|
4
|
+
const words = new Intl.DurationFormat("en", { style: "long" });
|
|
5
|
+
// A duration in whole seconds; `key` names the setting in the error.
|
|
7
6
|
export function parse_duration(value, key) {
|
|
8
7
|
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
9
8
|
return Math.floor(value);
|
|
10
|
-
const text = String(value).trim()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (
|
|
9
|
+
const text = String(value).trim();
|
|
10
|
+
// A bare number is seconds, where parse-duration would read milliseconds.
|
|
11
|
+
const seconds = /^\d+$/.test(text) ? Number(text) : parse(text, "s");
|
|
12
|
+
if (seconds === null || seconds === undefined || !Number.isFinite(seconds) || seconds < 0) {
|
|
14
13
|
throw new Error(`\`${key}\` must be a duration such as "1w", "7d", "2h30m" or "60s", not ${JSON.stringify(value)}`);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
seconds += Number(count) * unit_seconds[unit];
|
|
18
|
-
return seconds;
|
|
14
|
+
}
|
|
15
|
+
return Math.floor(seconds);
|
|
19
16
|
}
|
|
20
|
-
// The two largest whole units, such as "1 day 2 hours".
|
|
17
|
+
// The two largest whole units, such as "1 day, 2 hours".
|
|
21
18
|
export function format_duration(seconds) {
|
|
22
|
-
if (seconds
|
|
19
|
+
if (seconds < 1)
|
|
23
20
|
return "0 seconds";
|
|
24
|
-
const parts =
|
|
21
|
+
const parts = {};
|
|
25
22
|
let remaining = Math.floor(seconds);
|
|
26
|
-
for (const [
|
|
23
|
+
for (const [name, size] of unit_names) {
|
|
24
|
+
if (Object.keys(parts).length === 2)
|
|
25
|
+
break;
|
|
27
26
|
if (remaining < size)
|
|
28
27
|
continue;
|
|
29
|
-
|
|
28
|
+
parts[name] = Math.floor(remaining / size);
|
|
30
29
|
remaining %= size;
|
|
31
|
-
parts.push(`${count} ${name}${count === 1 ? "" : "s"}`);
|
|
32
30
|
}
|
|
33
|
-
return
|
|
31
|
+
return words.format(parts);
|
|
34
32
|
}
|
|
35
33
|
export function format_age(seconds) {
|
|
36
34
|
return seconds < 0 ? "never" : `${format_duration(seconds)} ago`;
|
|
@@ -14,10 +14,10 @@ export declare class AgentError extends Error {
|
|
|
14
14
|
constructor(message: string, unreachable?: boolean);
|
|
15
15
|
}
|
|
16
16
|
export declare function ended_stream(stderr: string): CommandStream;
|
|
17
|
-
export interface Agent {
|
|
18
|
-
run(
|
|
19
|
-
stream(
|
|
20
|
-
start(run_id: string,
|
|
17
|
+
export interface Agent<Task> {
|
|
18
|
+
run(task: Task, options: AgentRunOptions): Promise<RunResult>;
|
|
19
|
+
stream(task: Task, options?: AgentStreamOptions): CommandStream;
|
|
20
|
+
start(run_id: string, task: Task, options?: AgentStreamOptions): Promise<LongRun>;
|
|
21
21
|
poll(run_id: string, offset: number): Promise<LongRunPoll>;
|
|
22
22
|
attach(run_id: string, offset: number): CommandStream;
|
|
23
23
|
stop(run_id: string): Promise<LongRun>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentMessage, type AppMessage, type LongRun, type LongRunPoll, type RunResult, type ServerMessage } from "../../agent/protocol.ts";
|
|
1
|
+
import { type AgentApp, type AgentMessage, type AppMessage, type LongRun, type LongRunPoll, type RunResult, type ServerMessage } from "../../agent/protocol.ts";
|
|
2
2
|
import { type Agent, type AgentRunOptions, type AgentStreamOptions, type CommandStream } from "./agent.ts";
|
|
3
3
|
export interface ServerSocket {
|
|
4
4
|
readonly readyState: number;
|
|
@@ -16,12 +16,13 @@ export interface ConnectionOptions {
|
|
|
16
16
|
export type Hello = Omit<Extract<AgentMessage, {
|
|
17
17
|
type: "hello";
|
|
18
18
|
}>, "signature">;
|
|
19
|
-
export declare class AgentConnection implements Agent {
|
|
19
|
+
export declare class AgentConnection<Task> implements Agent<Task> {
|
|
20
20
|
readonly host: string;
|
|
21
21
|
readonly hello: Hello;
|
|
22
22
|
readonly connected_epoch: number;
|
|
23
23
|
on_message: ((message: AppMessage) => void) | null;
|
|
24
24
|
private readonly socket;
|
|
25
|
+
private readonly app;
|
|
25
26
|
private readonly options;
|
|
26
27
|
private readonly pending;
|
|
27
28
|
private readonly streams;
|
|
@@ -29,18 +30,18 @@ export declare class AgentConnection implements Agent {
|
|
|
29
30
|
private last_heard;
|
|
30
31
|
private readonly heartbeat;
|
|
31
32
|
private closed_reason;
|
|
32
|
-
constructor(socket: ServerSocket, host: string, hello: Hello, options: ConnectionOptions);
|
|
33
|
+
constructor(socket: ServerSocket, app: AgentApp<Task>, host: string, hello: Hello, options: ConnectionOptions);
|
|
33
34
|
get is_open(): boolean;
|
|
34
|
-
run(
|
|
35
|
-
stream(
|
|
36
|
-
start(run_id: string,
|
|
35
|
+
run(task: Task, options: AgentRunOptions): Promise<RunResult>;
|
|
36
|
+
stream(task: Task, options?: AgentStreamOptions): CommandStream;
|
|
37
|
+
start(run_id: string, task: Task, options?: AgentStreamOptions): Promise<LongRun>;
|
|
37
38
|
poll(run_id: string, offset: number): Promise<LongRunPoll>;
|
|
38
39
|
attach(run_id: string, offset: number): CommandStream;
|
|
39
40
|
stop(run_id: string): Promise<LongRun>;
|
|
40
41
|
forget(run_id: string): Promise<void>;
|
|
41
42
|
runs(): Promise<LongRun[]>;
|
|
42
43
|
request(type: string, body?: Record<string, unknown>): Promise<unknown>;
|
|
43
|
-
send(message: ServerMessage | AppMessage): void;
|
|
44
|
+
send(message: ServerMessage<Task> | AppMessage): void;
|
|
44
45
|
receive(data: Buffer, is_binary: boolean): void;
|
|
45
46
|
closed(reason: string): void;
|
|
46
47
|
close(code: number, reason: string): void;
|
|
@@ -82,6 +82,7 @@ export class AgentConnection {
|
|
|
82
82
|
// Takes the app's own messages from the agent that are not replies.
|
|
83
83
|
on_message = null;
|
|
84
84
|
socket;
|
|
85
|
+
app;
|
|
85
86
|
options;
|
|
86
87
|
pending = new Map();
|
|
87
88
|
streams = new Map();
|
|
@@ -89,8 +90,9 @@ export class AgentConnection {
|
|
|
89
90
|
last_heard = Date.now();
|
|
90
91
|
heartbeat;
|
|
91
92
|
closed_reason = null;
|
|
92
|
-
constructor(socket, host, hello, options) {
|
|
93
|
+
constructor(socket, app, host, hello, options) {
|
|
93
94
|
this.socket = socket;
|
|
95
|
+
this.app = app;
|
|
94
96
|
this.host = host;
|
|
95
97
|
this.hello = hello;
|
|
96
98
|
this.options = options;
|
|
@@ -101,20 +103,20 @@ export class AgentConnection {
|
|
|
101
103
|
return this.closed_reason === null;
|
|
102
104
|
}
|
|
103
105
|
// ── Agent ────────────────────────────────────────────────────────────────
|
|
104
|
-
async run(
|
|
106
|
+
async run(task, options) {
|
|
105
107
|
try {
|
|
106
108
|
const timeout = options.timeout_in_seconds * 1000 + run_reply_slack_in_milliseconds;
|
|
107
|
-
return await this.send_request("run", {
|
|
109
|
+
return await this.send_request("run", { task, stdin: options.stdin ?? null, timeout_in_seconds: options.timeout_in_seconds }, timeout);
|
|
108
110
|
}
|
|
109
111
|
catch (error) {
|
|
110
112
|
return { exit_code: -1, stdout: "", stderr: error.message };
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
|
-
stream(
|
|
114
|
-
return this.open_stream((id, window) => ({ type: "stream", id,
|
|
115
|
+
stream(task, options = {}) {
|
|
116
|
+
return this.open_stream((id, window) => ({ type: "stream", id, task, stdin: options.stdin ?? null, window }));
|
|
115
117
|
}
|
|
116
|
-
start(run_id,
|
|
117
|
-
return this.send_request("start", { run_id,
|
|
118
|
+
start(run_id, task, options = {}) {
|
|
119
|
+
return this.send_request("start", { run_id, task, stdin: options.stdin ?? null });
|
|
118
120
|
}
|
|
119
121
|
poll(run_id, offset) {
|
|
120
122
|
return this.send_request("poll", { run_id, offset });
|
|
@@ -137,7 +139,7 @@ export class AgentConnection {
|
|
|
137
139
|
// ── Socket ───────────────────────────────────────────────────────────────
|
|
138
140
|
send(message) {
|
|
139
141
|
if (this.is_open && this.socket.readyState === socket_open)
|
|
140
|
-
this.socket.send(encode_message(message));
|
|
142
|
+
this.socket.send(encode_message(message, this.app));
|
|
141
143
|
}
|
|
142
144
|
// Handles one frame from the agent after its hello.
|
|
143
145
|
receive(data, is_binary) {
|
|
@@ -148,7 +150,7 @@ export class AgentConnection {
|
|
|
148
150
|
this.streams.get(chunk.id)?.receive(chunk.data);
|
|
149
151
|
return;
|
|
150
152
|
}
|
|
151
|
-
const message = parse_agent_message(data.toString("utf8"));
|
|
153
|
+
const message = parse_agent_message(data.toString("utf8"), this.app);
|
|
152
154
|
if (message === null)
|
|
153
155
|
return;
|
|
154
156
|
if (!is_agent_message(message)) {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { IncomingHttpHeaders } from "node:http";
|
|
3
|
-
import { type AppMessage, type LongRun, type LongRunPoll, type RunResult } from "../../agent/protocol.ts";
|
|
3
|
+
import { type AgentApp, type AppMessage, type LongRun, type LongRunPoll, type RunResult } from "../../agent/protocol.ts";
|
|
4
4
|
import { type Agent, type AgentRunOptions, type AgentStreamOptions, type CommandStream } from "./agent.ts";
|
|
5
5
|
import { AgentConnection, type ConnectionOptions, type ServerSocket } from "./connection.ts";
|
|
6
6
|
export interface AgentTokenLookup {
|
|
7
7
|
host_for(token: string): string | null;
|
|
8
8
|
seen?(host: string): void;
|
|
9
9
|
}
|
|
10
|
-
export interface HubOptions {
|
|
11
|
-
app:
|
|
10
|
+
export interface HubOptions<Task> {
|
|
11
|
+
app: AgentApp<Task>;
|
|
12
12
|
local?: {
|
|
13
13
|
host: string;
|
|
14
|
-
agent: Agent
|
|
14
|
+
agent: Agent<Task>;
|
|
15
15
|
};
|
|
16
16
|
tokens?: AgentTokenLookup;
|
|
17
|
-
on_message?: (host: string, message: AppMessage, connection: AgentConnection) => void;
|
|
17
|
+
on_message?: (host: string, message: AppMessage, connection: AgentConnection<Task>) => void;
|
|
18
18
|
hello_timeout_in_milliseconds?: number;
|
|
19
19
|
connection?: Partial<ConnectionOptions>;
|
|
20
20
|
}
|
|
@@ -45,8 +45,8 @@ export declare const close_codes: {
|
|
|
45
45
|
readonly no_hello: 4005;
|
|
46
46
|
readonly bad_key: 4006;
|
|
47
47
|
};
|
|
48
|
-
export declare class AgentHub extends EventEmitter<HubEvents> {
|
|
49
|
-
readonly app:
|
|
48
|
+
export declare class AgentHub<Task> extends EventEmitter<HubEvents> {
|
|
49
|
+
readonly app: AgentApp<Task>;
|
|
50
50
|
private readonly local;
|
|
51
51
|
private readonly tokens;
|
|
52
52
|
private readonly on_message;
|
|
@@ -55,7 +55,7 @@ export declare class AgentHub extends EventEmitter<HubEvents> {
|
|
|
55
55
|
private readonly connections;
|
|
56
56
|
private readonly logins;
|
|
57
57
|
private agent_keys;
|
|
58
|
-
constructor(options: HubOptions);
|
|
58
|
+
constructor(options: HubOptions<Task>);
|
|
59
59
|
set_local_host(host: string): void;
|
|
60
60
|
set_agent_keys(lines: Map<string, string>): void;
|
|
61
61
|
has_agent_key(host: string): boolean;
|
|
@@ -70,10 +70,10 @@ export declare class AgentHub extends EventEmitter<HubEvents> {
|
|
|
70
70
|
is_connected(host: string): boolean;
|
|
71
71
|
status(host: string): AgentStatus | null;
|
|
72
72
|
statuses(): AgentStatus[];
|
|
73
|
-
agent(host: string): Agent | null;
|
|
74
|
-
run(host: string,
|
|
75
|
-
stream(host: string,
|
|
76
|
-
start(host: string, run_id: string,
|
|
73
|
+
agent(host: string): Agent<Task> | null;
|
|
74
|
+
run(host: string, task: Task, options: AgentRunOptions): Promise<RunResult>;
|
|
75
|
+
stream(host: string, task: Task, options?: AgentStreamOptions): CommandStream;
|
|
76
|
+
start(host: string, run_id: string, task: Task, options?: AgentStreamOptions): Promise<LongRun>;
|
|
77
77
|
poll(host: string, run_id: string, offset: number): Promise<LongRunPoll>;
|
|
78
78
|
attach(host: string, run_id: string, offset: number): CommandStream;
|
|
79
79
|
stop(host: string, run_id: string): Promise<LongRun>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
7
|
import { parse_public_key, verify_challenge } from "../../agent/keys.js";
|
|
8
|
-
import { encode_message, is_agent_message, key_host_header, parse_agent_message,
|
|
8
|
+
import { encode_message, is_agent_message, key_host_header, parse_agent_message, } from "../../agent/protocol.js";
|
|
9
9
|
import { AgentError, ended_stream } from "./agent.js";
|
|
10
10
|
import { AgentConnection } from "./connection.js";
|
|
11
11
|
// Close codes the hub hangs up with.
|
|
@@ -79,7 +79,7 @@ export class AgentHub extends EventEmitter {
|
|
|
79
79
|
}
|
|
80
80
|
// Who an agent's upgrade request says it is: the host it names to log in with that host's key, or a token's host.
|
|
81
81
|
admit(headers) {
|
|
82
|
-
const header = key_host_header(this.app);
|
|
82
|
+
const header = key_host_header(this.app.name);
|
|
83
83
|
const named = headers[header];
|
|
84
84
|
if (typeof named === "string" && named !== "") {
|
|
85
85
|
if (named === this.local?.host)
|
|
@@ -105,19 +105,19 @@ export class AgentHub extends EventEmitter {
|
|
|
105
105
|
const hello_timer = setTimeout(() => socket.close(close_codes.no_hello, "No hello came."), this.hello_timeout);
|
|
106
106
|
const nonce = login === "key" ? randomBytes(32).toString("base64url") : null;
|
|
107
107
|
if (nonce !== null)
|
|
108
|
-
socket.send(encode_message({ type: "challenge", nonce }));
|
|
108
|
+
socket.send(encode_message({ type: "challenge", nonce }, this.app));
|
|
109
109
|
socket.on("message", (raw, is_binary) => {
|
|
110
110
|
const data = as_buffer(raw);
|
|
111
111
|
if (connection !== null) {
|
|
112
112
|
connection.receive(data, is_binary);
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
|
-
const hello = is_binary ? null : parse_agent_message(data.toString("utf8"));
|
|
115
|
+
const hello = is_binary ? null : parse_agent_message(data.toString("utf8"), this.app);
|
|
116
116
|
if (hello === null || !is_agent_message(hello) || hello.type !== "hello")
|
|
117
117
|
return;
|
|
118
118
|
clearTimeout(hello_timer);
|
|
119
|
-
if (hello.protocol !== protocol_version) {
|
|
120
|
-
socket.close(close_codes.protocol, `This server speaks agent protocol ${protocol_version}, not ${hello.protocol}.`);
|
|
119
|
+
if (hello.protocol !== this.app.protocol_version) {
|
|
120
|
+
socket.close(close_codes.protocol, `This server speaks agent protocol ${this.app.protocol_version}, not ${hello.protocol}.`);
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
const { signature, ...greeting } = hello;
|
|
@@ -126,7 +126,7 @@ export class AgentHub extends EventEmitter {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
this.connections.get(host)?.close(close_codes.replaced, "A newer connection from the same host replaced this one.");
|
|
129
|
-
const current = new AgentConnection(socket, host, greeting, this.connection_options);
|
|
129
|
+
const current = new AgentConnection(socket, this.app, host, greeting, this.connection_options);
|
|
130
130
|
current.on_message = (message) => this.on_message?.(host, message, current);
|
|
131
131
|
connection = current;
|
|
132
132
|
this.connections.set(host, current);
|
|
@@ -149,7 +149,7 @@ export class AgentHub extends EventEmitter {
|
|
|
149
149
|
}
|
|
150
150
|
proves_key(host, nonce, signature) {
|
|
151
151
|
const declared = this.agent_keys.get(host);
|
|
152
|
-
return declared !== undefined && typeof signature === "string" && verify_challenge(declared.key, this.app, host, nonce, signature);
|
|
152
|
+
return declared !== undefined && typeof signature === "string" && verify_challenge(declared.key, this.app.name, host, nonce, signature);
|
|
153
153
|
}
|
|
154
154
|
disconnect(host, code = 1000, reason = "The server hung up.") {
|
|
155
155
|
this.connections.get(host)?.close(code, reason);
|
|
@@ -181,15 +181,15 @@ export class AgentHub extends EventEmitter {
|
|
|
181
181
|
}
|
|
182
182
|
// ── Commands ─────────────────────────────────────────────────────────────
|
|
183
183
|
// A run on a host with no agent answers with exit code -1 rather than rejecting, as a command that failed does.
|
|
184
|
-
run(host,
|
|
184
|
+
run(host, task, options) {
|
|
185
185
|
const agent = this.agent(host);
|
|
186
|
-
return agent === null ? Promise.resolve({ exit_code: -1, stdout: "", stderr: not_connected(host) }) : agent.run(
|
|
186
|
+
return agent === null ? Promise.resolve({ exit_code: -1, stdout: "", stderr: not_connected(host) }) : agent.run(task, options);
|
|
187
187
|
}
|
|
188
|
-
stream(host,
|
|
189
|
-
return this.agent(host)?.stream(
|
|
188
|
+
stream(host, task, options = {}) {
|
|
189
|
+
return this.agent(host)?.stream(task, options) ?? ended_stream(not_connected(host));
|
|
190
190
|
}
|
|
191
|
-
start(host, run_id,
|
|
192
|
-
return this.require(host).then((agent) => agent.start(run_id,
|
|
191
|
+
start(host, run_id, task, options = {}) {
|
|
192
|
+
return this.require(host).then((agent) => agent.start(run_id, task, options));
|
|
193
193
|
}
|
|
194
194
|
poll(host, run_id, offset) {
|
|
195
195
|
return this.require(host).then((agent) => agent.poll(run_id, offset));
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { LongRuns, type LongRunOptions } from "../../agent/long_runs.ts";
|
|
2
2
|
import type { AppRequests } from "../../agent/session.ts";
|
|
3
|
+
import type { TaskRunner } from "../../agent/tasks.ts";
|
|
3
4
|
import { type Agent, type AgentRunOptions, type AgentStreamOptions, type CommandStream, type LongRun, type LongRunPoll, type RunResult } from "./agent.ts";
|
|
4
|
-
export interface LocalAgentOptions {
|
|
5
|
+
export interface LocalAgentOptions<Task> {
|
|
6
|
+
runner: TaskRunner<Task>;
|
|
5
7
|
long_runs?: LongRunOptions;
|
|
6
8
|
requests?: AppRequests;
|
|
7
9
|
}
|
|
8
|
-
export declare class LocalAgent implements Agent {
|
|
9
|
-
readonly long_runs: LongRuns
|
|
10
|
+
export declare class LocalAgent<Task> implements Agent<Task> {
|
|
11
|
+
readonly long_runs: LongRuns<Task>;
|
|
12
|
+
private readonly runner;
|
|
10
13
|
private readonly requests;
|
|
11
|
-
constructor(state_directory: string, options
|
|
12
|
-
run(
|
|
13
|
-
stream(
|
|
14
|
-
start(run_id: string,
|
|
14
|
+
constructor(state_directory: string, options: LocalAgentOptions<Task>);
|
|
15
|
+
run(task: Task, options: AgentRunOptions): Promise<RunResult>;
|
|
16
|
+
stream(task: Task, options?: AgentStreamOptions): CommandStream;
|
|
17
|
+
start(run_id: string, task: Task, options?: AgentStreamOptions): Promise<LongRun>;
|
|
15
18
|
poll(run_id: string, offset: number): Promise<LongRunPoll>;
|
|
16
19
|
attach(run_id: string, offset: number): CommandStream;
|
|
17
20
|
stop(run_id: string): Promise<LongRun>;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
// The agent built into the server for its own machine. It runs commands itself with no socket between, through the
|
|
2
2
|
// same code a remote agent uses, so a command behaves the same on every host.
|
|
3
3
|
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
4
|
-
import { run_command, stream_command } from "../../agent/commands.js";
|
|
5
4
|
import { LongRuns } from "../../agent/long_runs.js";
|
|
6
5
|
import { AgentError, ended_stream, } from "./agent.js";
|
|
7
6
|
// ── Local agent ──────────────────────────────────────────────────────────────
|
|
8
7
|
export class LocalAgent {
|
|
9
8
|
long_runs;
|
|
9
|
+
runner;
|
|
10
10
|
requests;
|
|
11
11
|
// Long runs keep their output under the state directory, so they outlive a restart of the server.
|
|
12
|
-
constructor(state_directory, options
|
|
13
|
-
this.
|
|
12
|
+
constructor(state_directory, options) {
|
|
13
|
+
this.runner = options.runner;
|
|
14
|
+
this.long_runs = new LongRuns(state_directory, options.runner, options.long_runs);
|
|
14
15
|
this.requests = options.requests ?? {};
|
|
15
16
|
}
|
|
16
|
-
run(
|
|
17
|
-
return
|
|
17
|
+
run(task, options) {
|
|
18
|
+
return this.runner.run(task, options);
|
|
18
19
|
}
|
|
19
|
-
stream(
|
|
20
|
-
|
|
20
|
+
stream(task, options = {}) {
|
|
21
|
+
if (this.runner.stream === undefined)
|
|
22
|
+
return ended_stream("This agent does not stream its tasks.");
|
|
23
|
+
return this.runner.stream(task, options);
|
|
21
24
|
}
|
|
22
|
-
start(run_id,
|
|
23
|
-
return as_agent_error(() => this.long_runs.start(run_id,
|
|
25
|
+
start(run_id, task, options = {}) {
|
|
26
|
+
return as_agent_error(() => this.long_runs.start(run_id, task, options.stdin ?? null));
|
|
24
27
|
}
|
|
25
28
|
poll(run_id, offset) {
|
|
26
29
|
return as_agent_error(() => this.long_runs.poll(run_id, offset));
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import type { AgentHub } from "./hub.ts";
|
|
3
|
-
export declare function register_agent_route(app: FastifyInstance, hub: AgentHub
|
|
3
|
+
export declare function register_agent_route<Task>(app: FastifyInstance, hub: AgentHub<Task>, path?: string): Promise<void>;
|
|
@@ -3,46 +3,28 @@
|
|
|
3
3
|
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { text } from "node:stream/consumers";
|
|
6
|
+
import password from "@inquirer/password";
|
|
6
7
|
import { account_name_pattern, hash_password, set_account } from "./passwords.js";
|
|
7
8
|
// ── Asking ───────────────────────────────────────────────────────────────────
|
|
8
|
-
//
|
|
9
|
-
function ask_hidden(question) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
process.stderr.write(question);
|
|
12
|
-
process.stdin.setRawMode(true);
|
|
13
|
-
process.stdin.setEncoding("utf8");
|
|
14
|
-
let answer = "";
|
|
15
|
-
const on_data = (chunk) => {
|
|
16
|
-
for (const character of chunk) {
|
|
17
|
-
if (character === "\r" || character === "\n") {
|
|
18
|
-
process.stdin.setRawMode(false);
|
|
19
|
-
process.stdin.pause();
|
|
20
|
-
process.stdin.off("data", on_data);
|
|
21
|
-
process.stderr.write("\n");
|
|
22
|
-
resolve(answer);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
if (character === "\u0003")
|
|
26
|
-
process.exit(130);
|
|
27
|
-
if (character === "\u007f")
|
|
28
|
-
answer = answer.slice(0, -1);
|
|
29
|
-
else
|
|
30
|
-
answer += character;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
process.stdin.on("data", on_data);
|
|
34
|
-
process.stdin.resume();
|
|
35
|
-
});
|
|
36
|
-
}
|
|
9
|
+
// Asks twice at a terminal without echoing, on stderr so stdout stays clean; reads one line from a pipe.
|
|
37
10
|
async function read_password() {
|
|
38
11
|
if (!process.stdin.isTTY)
|
|
39
12
|
return (await text(process.stdin)).replace(/\r?\n$/, "");
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
13
|
+
const ask = (message) => password({ message, mask: false }, { output: process.stderr });
|
|
14
|
+
try {
|
|
15
|
+
const first = await ask("Password:");
|
|
16
|
+
if (first !== (await ask("Again:"))) {
|
|
17
|
+
console.error("The passwords differ.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
return first;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// Ctrl-C at the prompt ends the command as a shell would.
|
|
24
|
+
if (error.name === "ExitPromptError")
|
|
25
|
+
process.exit(130);
|
|
26
|
+
throw error;
|
|
44
27
|
}
|
|
45
|
-
return password;
|
|
46
28
|
}
|
|
47
29
|
// ── Command ──────────────────────────────────────────────────────────────────
|
|
48
30
|
// Runs the command with `<name> [file]` from argv; `variable` is the environment variable the app reads the file from.
|