edge-ai-client-ts 1.0.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/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/bin/ec-ts.js +18 -0
- package/dist/buffer/disk-queue.d.ts +140 -0
- package/dist/buffer/disk-queue.js +370 -0
- package/dist/cli/devices.d.ts +1 -0
- package/dist/cli/devices.js +61 -0
- package/dist/cli/enroll.d.ts +2 -0
- package/dist/cli/enroll.js +89 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +116 -0
- package/dist/cli/messages.d.ts +1 -0
- package/dist/cli/messages.js +59 -0
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.js +112 -0
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +56 -0
- package/dist/cli/whoami.d.ts +2 -0
- package/dist/cli/whoami.js +41 -0
- package/dist/config/cmd-gate.d.ts +65 -0
- package/dist/config/cmd-gate.js +128 -0
- package/dist/config/settings.d.ts +209 -0
- package/dist/config/settings.js +627 -0
- package/dist/crypto/aes-gcm.d.ts +38 -0
- package/dist/crypto/aes-gcm.js +90 -0
- package/dist/crypto/hmac.d.ts +31 -0
- package/dist/crypto/hmac.js +52 -0
- package/dist/crypto/tls-guard.d.ts +36 -0
- package/dist/crypto/tls-guard.js +54 -0
- package/dist/daemon/manager.d.ts +82 -0
- package/dist/daemon/manager.js +461 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +63 -0
- package/dist/logging/file-logger.d.ts +21 -0
- package/dist/logging/file-logger.js +71 -0
- package/dist/network/ws-client.d.ts +221 -0
- package/dist/network/ws-client.js +1134 -0
- package/dist/session/fail-fast.d.ts +70 -0
- package/dist/session/fail-fast.js +122 -0
- package/dist/session/manager.d.ts +136 -0
- package/dist/session/manager.js +291 -0
- package/dist/session/persistence.d.ts +103 -0
- package/dist/session/persistence.js +194 -0
- package/dist/session/pi-rpc.d.ts +164 -0
- package/dist/session/pi-rpc.js +412 -0
- package/dist/session/sftp.d.ts +64 -0
- package/dist/session/sftp.js +335 -0
- package/dist/session/shell-frame.d.ts +77 -0
- package/dist/session/shell-frame.js +199 -0
- package/dist/session/shell.d.ts +124 -0
- package/dist/session/shell.js +300 -0
- package/docs/CONFIGURATION.md +169 -0
- package/docs/INSTALLATION.md +164 -0
- package/docs/PROTOCOL.md +248 -0
- package/docs/TROUBLESHOOTING.md +177 -0
- package/package.json +79 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY session manager — mirrors `edge-client/src/session/shell.rs`.
|
|
3
|
+
*
|
|
4
|
+
* Owns PTY sessions keyed by `session_id` (UUID v4 string). Spawns the
|
|
5
|
+
* shell via `node-pty`, pumps PTY stdout → framed data frames → an outbound
|
|
6
|
+
* channel, writes browser keystrokes (data frames) into the PTY, handles
|
|
7
|
+
* resize/close, and emits `shellExited` lifecycle events.
|
|
8
|
+
*
|
|
9
|
+
* Security: the PTY shell runs as the edge-client's Standard User (per
|
|
10
|
+
* SPEC.md) — no privilege escalation. Shell is only active when
|
|
11
|
+
* `shell.enabled = true` in config (default false); enforced by the
|
|
12
|
+
* constructor gate.
|
|
13
|
+
*
|
|
14
|
+
* Frame codec: the 17-byte binary frame format (`[16-byte session_id |
|
|
15
|
+
* 1-byte frame_type | payload]`) is provided by `shell-frame.ts`. We use
|
|
16
|
+
* it directly via a thin `FrameCodec` adapter that converts UUID strings
|
|
17
|
+
* to/from raw bytes at the codec boundary — keeping the wire bytes
|
|
18
|
+
* EXACTLY byte-compatible with the Rust `portable-pty` + `portable-pty`
|
|
19
|
+
* implementation.
|
|
20
|
+
*/
|
|
21
|
+
import type { IPty } from 'node-pty';
|
|
22
|
+
import { FrameType, type FrameTypeValue } from './shell-frame.js';
|
|
23
|
+
export type { IPty } from 'node-pty';
|
|
24
|
+
export { FrameType };
|
|
25
|
+
export type { FrameTypeValue };
|
|
26
|
+
/**
|
|
27
|
+
* Frame codec adapter — internally delegates to `shell-frame.ts`'s
|
|
28
|
+
* byte-exact codec, but exposes a string-keyed interface so this module
|
|
29
|
+
* can pass UUID strings around as the rest of `session/` does. Wire
|
|
30
|
+
* format is IDENTICAL to `shell-frame.ts` (no behavior drift possible).
|
|
31
|
+
*/
|
|
32
|
+
export interface FrameCodec {
|
|
33
|
+
encode(sessionId: string, frameType: FrameTypeValue, payload: Buffer): Buffer;
|
|
34
|
+
decode(data: Buffer): {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
frameType: FrameTypeValue;
|
|
37
|
+
payload: Buffer;
|
|
38
|
+
} | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Default codec backed by `shell-frame.ts`'s byte-exact primitives. */
|
|
41
|
+
export declare const inlineFrameCodec: FrameCodec;
|
|
42
|
+
/** Options passed to the PTY spawn function. */
|
|
43
|
+
export interface PtySpawnOptions {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly cols: number;
|
|
46
|
+
readonly rows: number;
|
|
47
|
+
readonly cwd: string;
|
|
48
|
+
readonly env: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Function that spawns a PTY. Production uses `node-pty`, tests inject mocks.
|
|
52
|
+
* Mirrors `pty.spawn(shell, args, opts)`. Returns IPty synchronously.
|
|
53
|
+
*/
|
|
54
|
+
export type PtyFactory = (program: string, args: string[], options: PtySpawnOptions) => IPty;
|
|
55
|
+
export interface ShellManagerOptions {
|
|
56
|
+
/** Shell gate — if false, all open() calls throw. Mirrors Rust shell.enabled. */
|
|
57
|
+
readonly enabled: boolean;
|
|
58
|
+
}
|
|
59
|
+
export type DataHandler = (sessionId: string, frameType: FrameTypeValue, payload: Buffer) => void;
|
|
60
|
+
export type ExitHandler = (sessionId: string, code: number) => void;
|
|
61
|
+
export declare class ShellManager {
|
|
62
|
+
private readonly enabled;
|
|
63
|
+
private readonly sessions;
|
|
64
|
+
private readonly emitter;
|
|
65
|
+
private codec;
|
|
66
|
+
/** Injectable PTY factory — tests inject a mock, production uses defaultPtyFactory. */
|
|
67
|
+
private readonly ptyFactory;
|
|
68
|
+
constructor(opts: ShellManagerOptions, codec?: FrameCodec, ptyFactory?: PtyFactory);
|
|
69
|
+
/**
|
|
70
|
+
* Open a new PTY shell session. The caller provides a UUID session_id
|
|
71
|
+
* (assigned by the session manager).
|
|
72
|
+
*
|
|
73
|
+
* Mirrors Rust `ShellManager::open`.
|
|
74
|
+
*/
|
|
75
|
+
open(sessionId: string, cols: number, rows: number, shell?: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Write data to a session's PTY stdin (browser keystrokes).
|
|
78
|
+
* Mirrors Rust `FrameType::Data` handling in `handle_frame`.
|
|
79
|
+
*/
|
|
80
|
+
write(sessionId: string, data: Buffer): void;
|
|
81
|
+
/**
|
|
82
|
+
* Resize a session's PTY.
|
|
83
|
+
* Mirrors Rust `FrameType::Resize` handling in `handle_frame`.
|
|
84
|
+
*/
|
|
85
|
+
resize(sessionId: string, cols: number, rows: number): void;
|
|
86
|
+
/**
|
|
87
|
+
* Close a session's PTY (browser-initiated).
|
|
88
|
+
* Mirrors Rust `close_session`.
|
|
89
|
+
*/
|
|
90
|
+
close(sessionId: string): void;
|
|
91
|
+
/**
|
|
92
|
+
* Handle an inbound binary frame from the browser (relayed by relay).
|
|
93
|
+
* Mirrors Rust `ShellManager::handle_frame`.
|
|
94
|
+
*/
|
|
95
|
+
handleFrame(frameType: FrameTypeValue, sessionId: string, payload: Buffer): void;
|
|
96
|
+
/**
|
|
97
|
+
* Register a handler for PTY data frames (sessionId, frameType, payload).
|
|
98
|
+
* The handler receives the structured payload, not raw wire bytes.
|
|
99
|
+
*/
|
|
100
|
+
onData(handler: DataHandler): void;
|
|
101
|
+
/**
|
|
102
|
+
* Register a handler for session exit events (sessionId, exitCode).
|
|
103
|
+
*/
|
|
104
|
+
onExit(handler: ExitHandler): void;
|
|
105
|
+
/**
|
|
106
|
+
* Register a handler for raw wire frames (Buffer).
|
|
107
|
+
* The binary WS transport layer listens on this to send outbound frames.
|
|
108
|
+
*/
|
|
109
|
+
onFrame(handler: (frame: Buffer) => void): void;
|
|
110
|
+
/** Number of active PTY sessions. For heartbeat metadata. */
|
|
111
|
+
activeCount(): number;
|
|
112
|
+
/**
|
|
113
|
+
* Close all sessions (edge shutdown). Mirrors Rust `close_all`.
|
|
114
|
+
*/
|
|
115
|
+
closeAll(): void;
|
|
116
|
+
}
|
|
117
|
+
/** Build a data frame (FrameType::Data). */
|
|
118
|
+
export declare function buildDataFrame(sessionId: string, payload: Buffer): Buffer;
|
|
119
|
+
/**
|
|
120
|
+
* Build a resize frame (FrameType::Resize). payload = cols (u16 BE) + rows (u16 BE).
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildResizeFrame(sessionId: string, cols: number, rows: number): Buffer;
|
|
123
|
+
/** Build a close frame (FrameType::Close). No payload. */
|
|
124
|
+
export declare function buildCloseFrame(sessionId: string): Buffer;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY session manager — mirrors `edge-client/src/session/shell.rs`.
|
|
3
|
+
*
|
|
4
|
+
* Owns PTY sessions keyed by `session_id` (UUID v4 string). Spawns the
|
|
5
|
+
* shell via `node-pty`, pumps PTY stdout → framed data frames → an outbound
|
|
6
|
+
* channel, writes browser keystrokes (data frames) into the PTY, handles
|
|
7
|
+
* resize/close, and emits `shellExited` lifecycle events.
|
|
8
|
+
*
|
|
9
|
+
* Security: the PTY shell runs as the edge-client's Standard User (per
|
|
10
|
+
* SPEC.md) — no privilege escalation. Shell is only active when
|
|
11
|
+
* `shell.enabled = true` in config (default false); enforced by the
|
|
12
|
+
* constructor gate.
|
|
13
|
+
*
|
|
14
|
+
* Frame codec: the 17-byte binary frame format (`[16-byte session_id |
|
|
15
|
+
* 1-byte frame_type | payload]`) is provided by `shell-frame.ts`. We use
|
|
16
|
+
* it directly via a thin `FrameCodec` adapter that converts UUID strings
|
|
17
|
+
* to/from raw bytes at the codec boundary — keeping the wire bytes
|
|
18
|
+
* EXACTLY byte-compatible with the Rust `portable-pty` + `portable-pty`
|
|
19
|
+
* implementation.
|
|
20
|
+
*/
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
import { EventEmitter } from 'node:events';
|
|
23
|
+
import { FrameType, encodeFrame, decodeFrame, sessionIdFromString, sessionIdToString, } from './shell-frame.js';
|
|
24
|
+
// Re-export the frame-type enum so consumers importing from `./shell.js`
|
|
25
|
+
// (which they have historically done) keep working.
|
|
26
|
+
export { FrameType };
|
|
27
|
+
/** Default codec backed by `shell-frame.ts`'s byte-exact primitives. */
|
|
28
|
+
export const inlineFrameCodec = {
|
|
29
|
+
encode(sessionId, frameType, payload) {
|
|
30
|
+
const sidBytes = sessionIdFromString(sessionId);
|
|
31
|
+
if (sidBytes === null) {
|
|
32
|
+
throw new RangeError(`shell.ts: invalid sessionId (not a valid UUID/hex): ${sessionId}`);
|
|
33
|
+
}
|
|
34
|
+
return encodeFrame({ sessionId: sidBytes, frameType, payload });
|
|
35
|
+
},
|
|
36
|
+
decode(data) {
|
|
37
|
+
const f = decodeFrame(data);
|
|
38
|
+
if (f === null)
|
|
39
|
+
return undefined;
|
|
40
|
+
const sidStr = sessionIdToString(f.sessionId);
|
|
41
|
+
if (sidStr === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
return { sessionId: sidStr, frameType: f.frameType, payload: Buffer.from(f.payload) };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/* (Old hex-string codec + helpers removed; see `shell-frame.ts` for the
|
|
47
|
+
* authoritative byte-exact implementation. Keeping these here would have
|
|
48
|
+
* meant producing DIFFERENT wire bytes than the Rust side, since the
|
|
49
|
+
* Rust `ShellFrame` writes the 16 raw UUID bytes directly — not a hex
|
|
50
|
+
* encoding of them.) */
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Default shell selection (mirrors Rust `default_shell()`)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
function defaultShell() {
|
|
55
|
+
if (process.platform === 'win32') {
|
|
56
|
+
return process.env.COMSPEC || 'cmd.exe';
|
|
57
|
+
}
|
|
58
|
+
// Unix: os.userInfo().shell, env SHELL, or /bin/sh
|
|
59
|
+
try {
|
|
60
|
+
const info = os.userInfo();
|
|
61
|
+
if (info.shell && info.shell.length > 0)
|
|
62
|
+
return info.shell;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// userInfo() can throw in certain container environments
|
|
66
|
+
}
|
|
67
|
+
if (process.env.SHELL && process.env.SHELL.length > 0) {
|
|
68
|
+
return process.env.SHELL;
|
|
69
|
+
}
|
|
70
|
+
return '/bin/sh';
|
|
71
|
+
}
|
|
72
|
+
/** Default PTY factory using node-pty (synchronous spawn). */
|
|
73
|
+
function defaultPtyFactory(program, args, options) {
|
|
74
|
+
// node-pty is imported statically at the top level for production use.
|
|
75
|
+
// Tests inject a mock factory via the constructor.
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
77
|
+
const pty = require('node-pty');
|
|
78
|
+
return pty.spawn(program, args, options);
|
|
79
|
+
}
|
|
80
|
+
export class ShellManager {
|
|
81
|
+
enabled;
|
|
82
|
+
sessions = new Map();
|
|
83
|
+
emitter = new EventEmitter();
|
|
84
|
+
codec;
|
|
85
|
+
/** Injectable PTY factory — tests inject a mock, production uses defaultPtyFactory. */
|
|
86
|
+
ptyFactory;
|
|
87
|
+
constructor(opts, codec, ptyFactory) {
|
|
88
|
+
this.enabled = opts.enabled;
|
|
89
|
+
this.codec = codec ?? inlineFrameCodec;
|
|
90
|
+
this.ptyFactory = ptyFactory ?? defaultPtyFactory;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Open a new PTY shell session. The caller provides a UUID session_id
|
|
94
|
+
* (assigned by the session manager).
|
|
95
|
+
*
|
|
96
|
+
* Mirrors Rust `ShellManager::open`.
|
|
97
|
+
*/
|
|
98
|
+
async open(sessionId, cols, rows, shell) {
|
|
99
|
+
if (!this.enabled) {
|
|
100
|
+
throw new Error('Shell is disabled (shell.enabled=false in config)');
|
|
101
|
+
}
|
|
102
|
+
if (this.sessions.has(sessionId)) {
|
|
103
|
+
// Guard against duplicate opens (mirrors Rust: "open for already-open session (ignored)")
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const shellCmd = shell ?? defaultShell();
|
|
107
|
+
const shellName = process.platform === 'win32' ? shellCmd : shellCmd;
|
|
108
|
+
const args = [];
|
|
109
|
+
const ptyInstance = this.ptyFactory(shellName, args, {
|
|
110
|
+
name: 'xterm-256color',
|
|
111
|
+
cols,
|
|
112
|
+
rows,
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
env: {
|
|
115
|
+
...process.env,
|
|
116
|
+
TERM: 'xterm-256color',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const handle = {
|
|
120
|
+
pty: ptyInstance,
|
|
121
|
+
kill() {
|
|
122
|
+
try {
|
|
123
|
+
ptyInstance.kill();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// process may already be dead
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
resize(c, r) {
|
|
130
|
+
try {
|
|
131
|
+
ptyInstance.resize(c, r);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// session may have been closed
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
write(data) {
|
|
138
|
+
try {
|
|
139
|
+
ptyInstance.write(data.toString('utf8'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// session may have been closed
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
this.sessions.set(sessionId, handle);
|
|
147
|
+
// Wire PTY stdout → frame emission.
|
|
148
|
+
// PTY data is a string (terminal output); we encode as UTF-8 Buffer.
|
|
149
|
+
ptyInstance.onData((data) => {
|
|
150
|
+
if (!this.sessions.has(sessionId))
|
|
151
|
+
return;
|
|
152
|
+
const payload = Buffer.from(data, 'utf8');
|
|
153
|
+
this.emitter.emit('data', sessionId, FrameType.Data, payload);
|
|
154
|
+
// Also emit raw wire bytes for the binary WS transport
|
|
155
|
+
const frame = this.codec.encode(sessionId, FrameType.Data, payload);
|
|
156
|
+
this.emitter.emit('frame', frame);
|
|
157
|
+
});
|
|
158
|
+
// Wire PTY exit → lifecycle event.
|
|
159
|
+
ptyInstance.onExit(({ exitCode }) => {
|
|
160
|
+
// Send a Close frame for this session (mirrors Rust reader thread EOF behavior).
|
|
161
|
+
const closePayload = Buffer.alloc(0);
|
|
162
|
+
this.emitter.emit('frame', this.codec.encode(sessionId, FrameType.Close, closePayload));
|
|
163
|
+
this.emitter.emit('exit', sessionId, exitCode);
|
|
164
|
+
// Remove the session (the PTY is dead).
|
|
165
|
+
this.sessions.delete(sessionId);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Write data to a session's PTY stdin (browser keystrokes).
|
|
170
|
+
* Mirrors Rust `FrameType::Data` handling in `handle_frame`.
|
|
171
|
+
*/
|
|
172
|
+
write(sessionId, data) {
|
|
173
|
+
const handle = this.sessions.get(sessionId);
|
|
174
|
+
if (handle === undefined)
|
|
175
|
+
return;
|
|
176
|
+
handle.write(data);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resize a session's PTY.
|
|
180
|
+
* Mirrors Rust `FrameType::Resize` handling in `handle_frame`.
|
|
181
|
+
*/
|
|
182
|
+
resize(sessionId, cols, rows) {
|
|
183
|
+
const handle = this.sessions.get(sessionId);
|
|
184
|
+
if (handle === undefined)
|
|
185
|
+
return;
|
|
186
|
+
handle.resize(cols, rows);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Close a session's PTY (browser-initiated).
|
|
190
|
+
* Mirrors Rust `close_session`.
|
|
191
|
+
*/
|
|
192
|
+
close(sessionId) {
|
|
193
|
+
const handle = this.sessions.get(sessionId);
|
|
194
|
+
if (handle === undefined)
|
|
195
|
+
return;
|
|
196
|
+
handle.kill();
|
|
197
|
+
this.sessions.delete(sessionId);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Handle an inbound binary frame from the browser (relayed by relay).
|
|
201
|
+
* Mirrors Rust `ShellManager::handle_frame`.
|
|
202
|
+
*/
|
|
203
|
+
handleFrame(frameType, sessionId, payload) {
|
|
204
|
+
switch (frameType) {
|
|
205
|
+
case FrameType.Data:
|
|
206
|
+
this.write(sessionId, payload);
|
|
207
|
+
break;
|
|
208
|
+
case FrameType.Resize: {
|
|
209
|
+
// payload = cols (u16 BE) + rows (u16 BE) — 4 bytes
|
|
210
|
+
if (payload.length >= 4) {
|
|
211
|
+
const cols = payload.readUInt16BE(0);
|
|
212
|
+
const rows = payload.readUInt16BE(2);
|
|
213
|
+
this.resize(sessionId, cols, rows);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case FrameType.Close:
|
|
218
|
+
this.close(sessionId);
|
|
219
|
+
break;
|
|
220
|
+
case FrameType.OpenControl: {
|
|
221
|
+
// payload = JSON { cols, rows } — default 80x24
|
|
222
|
+
let cols = 80;
|
|
223
|
+
let rows = 24;
|
|
224
|
+
if (payload.length > 0) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(payload.toString('utf8'));
|
|
227
|
+
if (typeof parsed.cols === 'number' && parsed.cols > 0)
|
|
228
|
+
cols = parsed.cols;
|
|
229
|
+
if (typeof parsed.rows === 'number' && parsed.rows > 0)
|
|
230
|
+
rows = parsed.rows;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// use defaults
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
void this.open(sessionId, cols, rows);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case FrameType.CloseControl:
|
|
240
|
+
this.close(sessionId);
|
|
241
|
+
break;
|
|
242
|
+
// Sftp and SftpRoot are handled by the sftp forwarder, not here
|
|
243
|
+
default:
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Register a handler for PTY data frames (sessionId, frameType, payload).
|
|
249
|
+
* The handler receives the structured payload, not raw wire bytes.
|
|
250
|
+
*/
|
|
251
|
+
onData(handler) {
|
|
252
|
+
this.emitter.on('data', handler);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Register a handler for session exit events (sessionId, exitCode).
|
|
256
|
+
*/
|
|
257
|
+
onExit(handler) {
|
|
258
|
+
this.emitter.on('exit', handler);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Register a handler for raw wire frames (Buffer).
|
|
262
|
+
* The binary WS transport layer listens on this to send outbound frames.
|
|
263
|
+
*/
|
|
264
|
+
onFrame(handler) {
|
|
265
|
+
this.emitter.on('frame', handler);
|
|
266
|
+
}
|
|
267
|
+
/** Number of active PTY sessions. For heartbeat metadata. */
|
|
268
|
+
activeCount() {
|
|
269
|
+
return this.sessions.size;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Close all sessions (edge shutdown). Mirrors Rust `close_all`.
|
|
273
|
+
*/
|
|
274
|
+
closeAll() {
|
|
275
|
+
const ids = [...this.sessions.keys()];
|
|
276
|
+
for (const id of ids) {
|
|
277
|
+
this.close(id);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// ShellFrame helpers (mirrors Rust factory methods)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
/** Build a data frame (FrameType::Data). */
|
|
285
|
+
export function buildDataFrame(sessionId, payload) {
|
|
286
|
+
return inlineFrameCodec.encode(sessionId, FrameType.Data, payload);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Build a resize frame (FrameType::Resize). payload = cols (u16 BE) + rows (u16 BE).
|
|
290
|
+
*/
|
|
291
|
+
export function buildResizeFrame(sessionId, cols, rows) {
|
|
292
|
+
const payload = Buffer.alloc(4);
|
|
293
|
+
payload.writeUInt16BE(cols, 0);
|
|
294
|
+
payload.writeUInt16BE(rows, 2);
|
|
295
|
+
return inlineFrameCodec.encode(sessionId, FrameType.Resize, payload);
|
|
296
|
+
}
|
|
297
|
+
/** Build a close frame (FrameType::Close). No payload. */
|
|
298
|
+
export function buildCloseFrame(sessionId) {
|
|
299
|
+
return inlineFrameCodec.encode(sessionId, FrameType.Close, Buffer.alloc(0));
|
|
300
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`edge-ai-client-ts` reads configuration from **two** sources,
|
|
4
|
+
merged with this precedence (highest → lowest):
|
|
5
|
+
|
|
6
|
+
1. **Environment variables** (preferred for secrets — never commit them).
|
|
7
|
+
2. **`config.toml`** (or `config.json`) in the platform-specific config
|
|
8
|
+
directory.
|
|
9
|
+
3. Built-in defaults.
|
|
10
|
+
|
|
11
|
+
> **Precedence example:** if `EDGE_AUTH_TOKEN` is set in the environment
|
|
12
|
+
> AND `relay.auth_token` is set in `config.toml`, the **environment
|
|
13
|
+
> variable wins**.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Config file location
|
|
18
|
+
|
|
19
|
+
| OS | Path |
|
|
20
|
+
|----|------|
|
|
21
|
+
| Linux | `~/.config/edge-ai-agent/config.toml` |
|
|
22
|
+
| macOS | `~/Library/Application Support/edge-ai-agent/config.toml` |
|
|
23
|
+
| Windows | `%APPDATA%\edge-ai-agent\config.toml` |
|
|
24
|
+
|
|
25
|
+
A JSON `config.json` is also accepted as a fallback (TOML takes
|
|
26
|
+
precedence if both exist).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Full schema
|
|
31
|
+
|
|
32
|
+
This schema is **identical** to the Rust `edge-client`'s `config.toml`.
|
|
33
|
+
Both clients read the same fields; mismatches are a bug.
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
# A stable identifier for this machine. Auto-generated on first run if
|
|
37
|
+
# omitted; do NOT change after enrolling (the relay binds your operator
|
|
38
|
+
# to a specific machine_id).
|
|
39
|
+
machine_id = "uuid-v4-or-fleet-name"
|
|
40
|
+
|
|
41
|
+
# Optional: pre-bind this client to an operator (otherwise set via
|
|
42
|
+
# `ec-ts enroll`).
|
|
43
|
+
owner_operator_id = "op_<your-operator-id>"
|
|
44
|
+
|
|
45
|
+
[server]
|
|
46
|
+
# WebSocket URL of the relay's /edge endpoint.
|
|
47
|
+
ws_url = "wss://relay.example.com/edge"
|
|
48
|
+
|
|
49
|
+
# Optional mode-specific URLs (Batch 5 — live network-mode switching).
|
|
50
|
+
# When the operator toggles network_mode='lan'/'wan' from the dashboard,
|
|
51
|
+
# the daemon transparently reconnects to the matching URL.
|
|
52
|
+
# ws_url_lan = "ws://192.168.1.10:3001/edge"
|
|
53
|
+
# ws_url_wan = "wss://wan-relay.example.com/edge"
|
|
54
|
+
|
|
55
|
+
[agent]
|
|
56
|
+
# Default agent id used when the dashboard does not pin one.
|
|
57
|
+
default_agent_id = "worker_default"
|
|
58
|
+
|
|
59
|
+
[buffer]
|
|
60
|
+
# Maximum in-memory queue size in MB before spill-to-disk is triggered.
|
|
61
|
+
max_memory_mb = 50
|
|
62
|
+
|
|
63
|
+
# How often (ms) the daemon flushes in-memory entries to the WS sender.
|
|
64
|
+
flush_interval_ms = 100
|
|
65
|
+
|
|
66
|
+
# How long (ms) an in-flight entry may wait for an ACK before being
|
|
67
|
+
# re-sent.
|
|
68
|
+
ack_timeout_ms = 5000
|
|
69
|
+
|
|
70
|
+
# Number of re-send attempts before an entry is dumped to encrypted disk
|
|
71
|
+
# (anti-data-loss — see docs/PROTOCOL.md).
|
|
72
|
+
max_retries = 3
|
|
73
|
+
|
|
74
|
+
# Buffer dump encryption key. Hex, 64 chars = 32 bytes.
|
|
75
|
+
# PREFER the EDGE_BUFFER_KEY env var (do not commit to source).
|
|
76
|
+
# key = "<64 hex chars>"
|
|
77
|
+
|
|
78
|
+
[logging]
|
|
79
|
+
# Log level: trace | debug | info | warn | error
|
|
80
|
+
level = "info"
|
|
81
|
+
|
|
82
|
+
# Local log directory (relative to config dir or absolute).
|
|
83
|
+
local_log_dir = "logs"
|
|
84
|
+
|
|
85
|
+
[shell]
|
|
86
|
+
# Enable the /shell-edge binary channel. Default OFF — opt in per node.
|
|
87
|
+
enabled = false
|
|
88
|
+
|
|
89
|
+
# Default TTY size when the dashboard opens a fresh shell.
|
|
90
|
+
default_cols = 80
|
|
91
|
+
default_rows = 24
|
|
92
|
+
|
|
93
|
+
[relay]
|
|
94
|
+
# Fleet shared secret for WS auth. PREFER the EDGE_AUTH_TOKEN env var.
|
|
95
|
+
# auth_token = "<your-fleet-shared-secret>"
|
|
96
|
+
|
|
97
|
+
# Optional: TLS SPKI fingerprint pin (SHA-256 hex of the
|
|
98
|
+
# SubjectPublicKeyInfo). Strongly recommended for production.
|
|
99
|
+
fingerprint = "<sha256-hex>"
|
|
100
|
+
|
|
101
|
+
[resources]
|
|
102
|
+
# Soft memory cap (MB). The daemon dumps the buffer to disk above this.
|
|
103
|
+
memory_max_mb = 2048
|
|
104
|
+
|
|
105
|
+
# Soft CPU quota (%). The daemon sheds load above this percentage.
|
|
106
|
+
cpu_quota_percent = 50
|
|
107
|
+
|
|
108
|
+
# Maximum concurrent tasks (sessions + RPCs).
|
|
109
|
+
tasks_max = 256
|
|
110
|
+
|
|
111
|
+
[cmd_gate]
|
|
112
|
+
# AGT-02 command gate mode. off | warn | block. Default: block.
|
|
113
|
+
mode = "block"
|
|
114
|
+
|
|
115
|
+
# Per-command timeout (seconds). Default: 60.
|
|
116
|
+
timeout_s = 60
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Environment variables
|
|
122
|
+
|
|
123
|
+
> **Treat all `EDGE_*` env vars as secrets.** Never commit them. Use your
|
|
124
|
+
> platform's secret manager (e.g. systemd `EnvironmentFile=` with mode
|
|
125
|
+
> `0600`, macOS launchd `EnvironmentVariables`, Windows Credential
|
|
126
|
+
> Manager, or a `.env` file with `chmod 600`).
|
|
127
|
+
|
|
128
|
+
| Variable | Equivalent config | Notes |
|
|
129
|
+
|----------|-------------------|-------|
|
|
130
|
+
| `EDGE_AUTH_TOKEN` | `relay.auth_token` | Fleet shared secret. **Required** in production. |
|
|
131
|
+
| `EDGE_BUFFER_KEY` | `buffer.key` | Hex, 64 chars. Required for offline buffer encryption. |
|
|
132
|
+
| `EDGE_BUFFER_KEY_PREVIOUS` | — | Hex, 64 chars. Used during key rotation. |
|
|
133
|
+
| `EDGE_RELAY_FINGERPRINT` | `relay.fingerprint` | Overrides config; recommended for prod. |
|
|
134
|
+
| `EDGE_OWNER_OPERATOR_ID` | `owner_operator_id` | Overrides enrolled operator. |
|
|
135
|
+
| `EDGEAI_REQUIRE_TLS` | — | `1` forces `wss://` outside production. |
|
|
136
|
+
| `NODE_ENV=production` | — | Implicitly forces `wss://`. |
|
|
137
|
+
| `EDGE_CMD_GATE_MODE` | `cmd_gate.mode` | `off` / `warn` / `block`. |
|
|
138
|
+
| `EDGE_FAIL_FAST` | — | Fail-fast policy toggle (`0`/`1`). |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Secret handling
|
|
143
|
+
|
|
144
|
+
- **Never** paste tokens, API keys, or fingerprints into:
|
|
145
|
+
- `config.toml` in a git repository
|
|
146
|
+
- shell history (use a password manager or `read -s`)
|
|
147
|
+
- Slack / chat / issue trackers
|
|
148
|
+
- **Always prefer env vars** for any value labeled "secret" or "token"
|
|
149
|
+
in the schema above.
|
|
150
|
+
- **Rotate** the fleet shared secret via the dashboard; the daemon
|
|
151
|
+
reconnects within the next heartbeat interval (~30 s).
|
|
152
|
+
- **Revoke** lost tokens in the dashboard immediately. The relay enforces
|
|
153
|
+
revocation on the next WS upgrade (no waiting for the daemon to
|
|
154
|
+
notice).
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Verifying your config
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Validate without running the daemon
|
|
162
|
+
ec-ts status --once
|
|
163
|
+
|
|
164
|
+
# Show effective config (with secrets redacted)
|
|
165
|
+
ec-ts whoami --verbose
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See [`docs/TROUBLESHOOTING.md`](TROUBLESHOOTING.md) for common
|
|
169
|
+
configuration mistakes.
|