@myagentroam/node 0.1.3 → 0.1.6
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 +9 -1
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +15 -4
- package/dist/claude-agent-sdk.d.ts +4 -16
- package/dist/claude-agent-sdk.js +18 -79
- package/dist/claude-channel.js +0 -1
- package/dist/codex-app-server.d.ts +5 -0
- package/dist/codex-app-server.js +26 -5
- package/dist/config.d.ts +3 -0
- package/dist/config.js +18 -2
- package/dist/connector/node-connector-options.d.ts +26 -0
- package/dist/connector/node-connector-options.js +1 -0
- package/dist/connector/node-operation-router.d.ts +9 -0
- package/dist/connector/node-operation-router.js +22 -0
- package/dist/connector.d.ts +62 -356
- package/dist/connector.js +714 -6176
- package/dist/control-outbound-scheduler.d.ts +26 -0
- package/dist/control-outbound-scheduler.js +121 -0
- package/dist/database.d.ts +4 -0
- package/dist/database.js +15 -10
- package/dist/event-buffer.js +0 -1
- package/dist/fake-runner.js +0 -1
- package/dist/health.js +0 -1
- package/dist/main.js +6 -1
- package/dist/migrations/v001.js +0 -1
- package/dist/migrations/v002.js +0 -1
- package/dist/migrations/v003.d.ts +4 -0
- package/dist/migrations/v003.js +44 -0
- package/dist/native-jsonl-reader.d.ts +14 -0
- package/dist/native-jsonl-reader.js +97 -0
- package/dist/native-session-history.d.ts +41 -4
- package/dist/native-session-history.js +340 -159
- package/dist/opencode-runtime.d.ts +33 -0
- package/dist/opencode-runtime.js +80 -0
- package/dist/opencode-server.d.ts +47 -0
- package/dist/opencode-server.js +250 -0
- package/dist/operational.js +10 -3
- package/dist/process-tree.js +0 -1
- package/dist/runner/abstract-runner.d.ts +153 -0
- package/dist/runner/abstract-runner.js +154 -0
- package/dist/runner/claude/managed-run-controller.d.ts +52 -0
- package/dist/runner/claude/managed-run-controller.js +326 -0
- package/dist/runner/claude-code-runner.d.ts +70 -0
- package/dist/runner/claude-code-runner.js +286 -0
- package/dist/runner/codex/conversation-parser.d.ts +59 -0
- package/dist/runner/codex/conversation-parser.js +816 -0
- package/dist/runner/codex/managed-run-controller.d.ts +80 -0
- package/dist/runner/codex/managed-run-controller.js +388 -0
- package/dist/runner/codex-runner.d.ts +85 -0
- package/dist/runner/codex-runner.js +365 -0
- package/dist/runner/fake-test-runner.d.ts +26 -0
- package/dist/runner/fake-test-runner.js +47 -0
- package/dist/runner/opencode/conversation-parser.d.ts +27 -0
- package/dist/runner/opencode/conversation-parser.js +217 -0
- package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
- package/dist/runner/opencode/managed-run-controller.js +451 -0
- package/dist/runner/opencode-runner.d.ts +60 -0
- package/dist/runner/opencode-runner.js +330 -0
- package/dist/runner/runner-registry.d.ts +19 -0
- package/dist/runner/runner-registry.js +75 -0
- package/dist/runner-command-engine.js +0 -1
- package/dist/runner-profiles.d.ts +4 -0
- package/dist/runner-profiles.js +76 -3
- package/dist/runner-usage.d.ts +2 -2
- package/dist/runner-usage.js +3 -6
- package/dist/runtime-state.js +26 -14
- package/dist/service/attachment-domain-state.d.ts +5 -0
- package/dist/service/attachment-domain-state.js +5 -0
- package/dist/service/conversation-history-service.d.ts +34 -0
- package/dist/service/conversation-history-service.js +158 -0
- package/dist/service/external-session-resume-service.d.ts +32 -0
- package/dist/service/external-session-resume-service.js +98 -0
- package/dist/service/fake-managed-run-service.d.ts +29 -0
- package/dist/service/fake-managed-run-service.js +62 -0
- package/dist/service/managed-runner-session-service.d.ts +12 -0
- package/dist/service/managed-runner-session-service.js +35 -0
- package/dist/service/native-session-projection-service.d.ts +51 -0
- package/dist/service/native-session-projection-service.js +271 -0
- package/dist/service/native-session-watch-service.d.ts +37 -0
- package/dist/service/native-session-watch-service.js +150 -0
- package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
- package/dist/service/node-connection-lifecycle-service.js +85 -0
- package/dist/service/node-control-channel.d.ts +32 -0
- package/dist/service/node-control-channel.js +85 -0
- package/dist/service/node-control-message-service.d.ts +28 -0
- package/dist/service/node-control-message-service.js +104 -0
- package/dist/service/node-request-service.d.ts +14 -0
- package/dist/service/node-request-service.js +50 -0
- package/dist/service/node-run-event-bridge.d.ts +40 -0
- package/dist/service/node-run-event-bridge.js +83 -0
- package/dist/service/node-terminal-channel.d.ts +29 -0
- package/dist/service/node-terminal-channel.js +182 -0
- package/dist/service/node-upgrade-coordinator.d.ts +29 -0
- package/dist/service/node-upgrade-coordinator.js +137 -0
- package/dist/service/node-workspace-coordinator.d.ts +32 -0
- package/dist/service/node-workspace-coordinator.js +80 -0
- package/dist/service/run-attachment-service.d.ts +37 -0
- package/dist/service/run-attachment-service.js +160 -0
- package/dist/service/run-domain-state.d.ts +4 -0
- package/dist/service/run-domain-state.js +4 -0
- package/dist/service/run-event-service.d.ts +31 -0
- package/dist/service/run-event-service.js +102 -0
- package/dist/service/run-workbench-service.d.ts +27 -0
- package/dist/service/run-workbench-service.js +113 -0
- package/dist/service/runner-channel-message-service.d.ts +13 -0
- package/dist/service/runner-channel-message-service.js +20 -0
- package/dist/service/runner-interaction-service.d.ts +40 -0
- package/dist/service/runner-interaction-service.js +246 -0
- package/dist/service/runner-service.d.ts +44 -0
- package/dist/service/runner-service.js +68 -0
- package/dist/service/session-catalog-service.d.ts +45 -0
- package/dist/service/session-catalog-service.js +224 -0
- package/dist/service/session-command-service.d.ts +23 -0
- package/dist/service/session-command-service.js +69 -0
- package/dist/service/session-context-service.d.ts +34 -0
- package/dist/service/session-context-service.js +87 -0
- package/dist/service/session-domain-state.d.ts +8 -0
- package/dist/service/session-domain-state.js +8 -0
- package/dist/service/session-identity-service.d.ts +24 -0
- package/dist/service/session-identity-service.js +86 -0
- package/dist/service/session-lifecycle-service.d.ts +31 -0
- package/dist/service/session-lifecycle-service.js +242 -0
- package/dist/service/session-message-service.d.ts +33 -0
- package/dist/service/session-message-service.js +157 -0
- package/dist/service/session-presentation-service.d.ts +33 -0
- package/dist/service/session-presentation-service.js +36 -0
- package/dist/service/session-query-service.d.ts +31 -0
- package/dist/service/session-query-service.js +77 -0
- package/dist/service/terminal-relay-state.d.ts +6 -0
- package/dist/service/terminal-relay-state.js +3 -0
- package/dist/service/terminal-service.d.ts +12 -0
- package/dist/service/terminal-service.js +52 -0
- package/dist/service/workbench-event-service.d.ts +21 -0
- package/dist/service/workbench-event-service.js +53 -0
- package/dist/service/workbench-manifest-service.d.ts +89 -0
- package/dist/service/workbench-manifest-service.js +70 -0
- package/dist/service/workspace-change-service.d.ts +9 -0
- package/dist/service/workspace-change-service.js +86 -0
- package/dist/service/workspace-content-search-service.d.ts +14 -0
- package/dist/service/workspace-content-search-service.js +477 -0
- package/dist/service/workspace-domain-state.d.ts +75 -0
- package/dist/service/workspace-domain-state.js +40 -0
- package/dist/service/workspace-file-service.d.ts +17 -0
- package/dist/service/workspace-file-service.js +109 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
- package/dist/service/workspace-queue-workbench-service.js +226 -0
- package/dist/service/workspace-service.d.ts +15 -0
- package/dist/service/workspace-service.js +67 -0
- package/dist/service/workspace-session-service.d.ts +23 -0
- package/dist/service/workspace-session-service.js +139 -0
- package/dist/service/workspace-upload-service.d.ts +37 -0
- package/dist/service/workspace-upload-service.js +240 -0
- package/dist/service/workspace-watch-service.d.ts +18 -0
- package/dist/service/workspace-watch-service.js +120 -0
- package/dist/service/workspace-workbench-service.d.ts +47 -0
- package/dist/service/workspace-workbench-service.js +223 -0
- package/dist/service.js +2 -3
- package/dist/storage.js +0 -1
- package/dist/supervisor.d.ts +1 -0
- package/dist/supervisor.js +154 -0
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +1 -2
- package/dist/util/node-operation-parsers.d.ts +92 -0
- package/dist/util/node-operation-parsers.js +382 -0
- package/dist/util/runner-native-session-parsers.d.ts +72 -0
- package/dist/util/runner-native-session-parsers.js +381 -0
- package/dist/util/secret-environment.d.ts +1 -0
- package/dist/util/secret-environment.js +25 -0
- package/dist/workspace.d.ts +62 -2
- package/dist/workspace.js +539 -57
- package/package.json +3 -2
- package/dist/capabilities.js.map +0 -1
- package/dist/claude-agent-sdk.js.map +0 -1
- package/dist/claude-channel.js.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/connector.js.map +0 -1
- package/dist/database.js.map +0 -1
- package/dist/event-buffer.js.map +0 -1
- package/dist/fake-runner.js.map +0 -1
- package/dist/health.js.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/migrations/v001.js.map +0 -1
- package/dist/migrations/v002.js.map +0 -1
- package/dist/native-session-history.js.map +0 -1
- package/dist/operational.js.map +0 -1
- package/dist/process-tree.js.map +0 -1
- package/dist/runner-command-engine.js.map +0 -1
- package/dist/runner-profiles.js.map +0 -1
- package/dist/runner-usage.js.map +0 -1
- package/dist/runtime-state.js.map +0 -1
- package/dist/service.js.map +0 -1
- package/dist/storage.js.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/workspace.js.map +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { createEnvelope } from '@myagentroam/protocol';
|
|
3
|
+
import { ControlOutboundScheduler } from '../control-outbound-scheduler.js';
|
|
4
|
+
import { nodeConnectEndpoint } from '../runner/codex/conversation-parser.js';
|
|
5
|
+
export class NodeControlChannel {
|
|
6
|
+
options;
|
|
7
|
+
socket;
|
|
8
|
+
outbound = new ControlOutboundScheduler(() => this.socket);
|
|
9
|
+
reconnectTimer;
|
|
10
|
+
attempts = 0;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
connect() {
|
|
15
|
+
const config = this.options.config();
|
|
16
|
+
if (config === undefined ||
|
|
17
|
+
this.options.stopped() ||
|
|
18
|
+
(this.socket !== undefined &&
|
|
19
|
+
(this.socket.readyState === WebSocket.OPEN ||
|
|
20
|
+
this.socket.readyState === WebSocket.CONNECTING)))
|
|
21
|
+
return;
|
|
22
|
+
this.options.onConnecting(this.attempts);
|
|
23
|
+
const socket = new WebSocket(nodeConnectEndpoint(config.serverUrl));
|
|
24
|
+
this.socket = socket;
|
|
25
|
+
socket.once('open', () => {
|
|
26
|
+
this.attempts = 0;
|
|
27
|
+
if (config.nodeId !== undefined && config.credential !== undefined)
|
|
28
|
+
this.send('authenticate', { nodeId: config.nodeId, credential: config.credential });
|
|
29
|
+
else if (config.registrationToken !== undefined)
|
|
30
|
+
this.send('register', {
|
|
31
|
+
registrationToken: config.registrationToken,
|
|
32
|
+
capabilities: this.options.capabilities()
|
|
33
|
+
});
|
|
34
|
+
else
|
|
35
|
+
socket.close(4000, 'NODE_CREDENTIAL_MISSING');
|
|
36
|
+
});
|
|
37
|
+
socket.on('message', (raw) => this.options.onMessage(raw.toString()));
|
|
38
|
+
socket.once('close', (code, reason) => {
|
|
39
|
+
if (this.socket !== socket)
|
|
40
|
+
return;
|
|
41
|
+
this.socket = undefined;
|
|
42
|
+
this.outbound.reset();
|
|
43
|
+
const reasonText = reason.toString();
|
|
44
|
+
this.options.onClose(code, reasonText);
|
|
45
|
+
if (!(code === 4000 && reasonText === 'NODE_CONNECTION_REPLACED'))
|
|
46
|
+
this.scheduleReconnect();
|
|
47
|
+
});
|
|
48
|
+
socket.once('error', () => undefined);
|
|
49
|
+
}
|
|
50
|
+
connected() {
|
|
51
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
52
|
+
}
|
|
53
|
+
/** Keeps transport injection at the transport boundary for contract tests. */
|
|
54
|
+
replaceSocketForTesting(socket) {
|
|
55
|
+
this.socket = socket;
|
|
56
|
+
}
|
|
57
|
+
close(code, reason) {
|
|
58
|
+
this.socket?.close(code, reason);
|
|
59
|
+
}
|
|
60
|
+
stop() {
|
|
61
|
+
if (this.reconnectTimer !== undefined)
|
|
62
|
+
clearTimeout(this.reconnectTimer);
|
|
63
|
+
this.reconnectTimer = undefined;
|
|
64
|
+
this.outbound.reset();
|
|
65
|
+
this.socket?.close();
|
|
66
|
+
this.socket = undefined;
|
|
67
|
+
}
|
|
68
|
+
send(type, payload, replyTo) {
|
|
69
|
+
this.sendEnvelope(createEnvelope(type, payload, { ...(replyTo === undefined ? {} : { replyTo }) }));
|
|
70
|
+
}
|
|
71
|
+
sendEnvelope(envelope) {
|
|
72
|
+
if (!this.connected())
|
|
73
|
+
return;
|
|
74
|
+
this.outbound.enqueue(envelope, envelope.type === 'workbench.event' ? 'normal' : 'high');
|
|
75
|
+
}
|
|
76
|
+
scheduleReconnect() {
|
|
77
|
+
if (this.options.stopped() || !this.options.reconnect || this.reconnectTimer !== undefined)
|
|
78
|
+
return;
|
|
79
|
+
this.options.onReconnectScheduled();
|
|
80
|
+
this.reconnectTimer = setTimeout(() => {
|
|
81
|
+
this.reconnectTimer = undefined;
|
|
82
|
+
this.connect();
|
|
83
|
+
}, this.options.reconnectDelay(this.attempts++));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type NodeEnvelope } from '@myagentroam/protocol';
|
|
2
|
+
import type { NodeConfig } from '../config.js';
|
|
3
|
+
import type { RunnerRegistry } from '../runner/runner-registry.js';
|
|
4
|
+
interface NodeControlMessageServiceOptions {
|
|
5
|
+
readonly config: () => NodeConfig | undefined;
|
|
6
|
+
readonly register: (nodeId: string, credential: string) => Promise<void>;
|
|
7
|
+
readonly connected: () => void;
|
|
8
|
+
readonly close: (code: number, reason: string) => void;
|
|
9
|
+
readonly inspectWorkspace: (envelope: NodeEnvelope) => Promise<void>;
|
|
10
|
+
readonly handleNodeRequest: (envelope: NodeEnvelope) => Promise<void>;
|
|
11
|
+
readonly acknowledgeWorkbenchEvent: (replyTo: string | undefined) => void;
|
|
12
|
+
readonly reconcileRuns: (envelope: NodeEnvelope) => void;
|
|
13
|
+
readonly runners: RunnerRegistry;
|
|
14
|
+
readonly deliverSessionChannelMessage: (envelope: NodeEnvelope) => void;
|
|
15
|
+
readonly completeUncontrolledCancellation: (runId: string) => void;
|
|
16
|
+
readonly send: (type: string, payload: unknown, replyTo?: string) => void;
|
|
17
|
+
}
|
|
18
|
+
/** Owns parsing and dispatch of the closed Node control-message protocol. */
|
|
19
|
+
export declare class NodeControlMessageService {
|
|
20
|
+
private readonly options;
|
|
21
|
+
constructor(options: NodeControlMessageServiceOptions);
|
|
22
|
+
handle(raw: string): void;
|
|
23
|
+
private dispatch;
|
|
24
|
+
private register;
|
|
25
|
+
private decideApproval;
|
|
26
|
+
private cancelRun;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { nodeEnvelopeSchema } from '@myagentroam/protocol';
|
|
2
|
+
/** Owns parsing and dispatch of the closed Node control-message protocol. */
|
|
3
|
+
export class NodeControlMessageService {
|
|
4
|
+
options;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
handle(raw) {
|
|
9
|
+
let envelope;
|
|
10
|
+
try {
|
|
11
|
+
envelope = nodeEnvelopeSchema.parse(JSON.parse(raw));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
this.options.close(4002, 'MESSAGE_INVALID');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.dispatch(envelope);
|
|
18
|
+
}
|
|
19
|
+
dispatch(envelope) {
|
|
20
|
+
if (envelope.type === 'registered') {
|
|
21
|
+
this.register(envelope);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (envelope.type === 'authenticated') {
|
|
25
|
+
this.options.connected();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (envelope.type === 'workspace.inspect') {
|
|
29
|
+
void this.options.inspectWorkspace(envelope);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (envelope.type === 'node.request') {
|
|
33
|
+
void this.options.handleNodeRequest(envelope);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (envelope.type === 'ack') {
|
|
37
|
+
const payload = envelope.payload;
|
|
38
|
+
if (payload.type === 'workbench.event')
|
|
39
|
+
this.options.acknowledgeWorkbenchEvent(envelope.replyTo);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (envelope.type === 'runs.reconcile') {
|
|
43
|
+
this.options.reconcileRuns(envelope);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (envelope.type === 'run.approval.decision') {
|
|
47
|
+
this.decideApproval(envelope);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (envelope.type === 'run.start') {
|
|
51
|
+
this.options.send('ack', { type: envelope.type }, envelope.id);
|
|
52
|
+
this.options.runners.startManagedRun(envelope);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (envelope.type === 'session.channel.message') {
|
|
56
|
+
this.options.deliverSessionChannelMessage(envelope);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (envelope.type === 'run.cancel' || envelope.type === 'run.interrupt') {
|
|
60
|
+
this.cancelRun(envelope);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (envelope.type === 'session.resume' || envelope.type === 'lease.reconcile') {
|
|
64
|
+
this.options.send('ack', { type: envelope.type }, envelope.id);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (envelope.type !== 'error')
|
|
68
|
+
this.options.send('error', { code: 'MESSAGE_TYPE_UNKNOWN' }, envelope.id);
|
|
69
|
+
}
|
|
70
|
+
register(envelope) {
|
|
71
|
+
const payload = envelope.payload;
|
|
72
|
+
if (typeof payload.nodeId !== 'string' ||
|
|
73
|
+
typeof payload.credential !== 'string' ||
|
|
74
|
+
this.options.config() === undefined) {
|
|
75
|
+
this.options.close(4002, 'MESSAGE_INVALID');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
void this.options.register(payload.nodeId, payload.credential);
|
|
79
|
+
}
|
|
80
|
+
decideApproval(envelope) {
|
|
81
|
+
const payload = envelope.payload;
|
|
82
|
+
if (typeof payload.runId !== 'string' ||
|
|
83
|
+
typeof payload.approvalId !== 'string' ||
|
|
84
|
+
(payload.decision !== 'APPROVED' && payload.decision !== 'REJECTED')) {
|
|
85
|
+
this.options.send('error', { code: 'MESSAGE_INVALID' }, envelope.id);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.options.runners.decideApproval({
|
|
89
|
+
runId: payload.runId,
|
|
90
|
+
approvalId: payload.approvalId,
|
|
91
|
+
decision: payload.decision
|
|
92
|
+
});
|
|
93
|
+
this.options.send('ack', { type: envelope.type }, envelope.id);
|
|
94
|
+
}
|
|
95
|
+
cancelRun(envelope) {
|
|
96
|
+
const payload = envelope.payload;
|
|
97
|
+
if (typeof payload.runId === 'string') {
|
|
98
|
+
const handled = this.options.runners.cancelManagedRun(payload.runId, envelope.type === 'run.cancel' ? 'CANCEL' : 'INTERRUPT');
|
|
99
|
+
if (!handled)
|
|
100
|
+
this.options.completeUncontrolledCancellation(payload.runId);
|
|
101
|
+
}
|
|
102
|
+
this.options.send('ack', { type: envelope.type }, envelope.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NodeEnvelope } from '@myagentroam/protocol';
|
|
2
|
+
interface NodeRequestServiceOptions {
|
|
3
|
+
readonly nodeId: () => string | undefined;
|
|
4
|
+
readonly databaseAvailable: () => boolean;
|
|
5
|
+
readonly execute: (operation: string, data: unknown) => Promise<unknown>;
|
|
6
|
+
readonly send: (type: string, payload: unknown, replyTo?: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/** Validates and executes the closed Admin-facing Node operation envelope. */
|
|
9
|
+
export declare class NodeRequestService {
|
|
10
|
+
private readonly options;
|
|
11
|
+
constructor(options: NodeRequestServiceOptions);
|
|
12
|
+
handle(envelope: NodeEnvelope): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { nodeLog } from '../operational.js';
|
|
2
|
+
import { validUserAuthorization } from '../runner/codex/conversation-parser.js';
|
|
3
|
+
/** Validates and executes the closed Admin-facing Node operation envelope. */
|
|
4
|
+
export class NodeRequestService {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
async handle(envelope) {
|
|
10
|
+
const payload = envelope.payload;
|
|
11
|
+
if (typeof payload.operation !== 'string' || !this.options.databaseAvailable()) {
|
|
12
|
+
this.options.send('node.response', { error: 'MESSAGE_INVALID' }, envelope.id);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!validUserAuthorization(payload.userAuthorization, envelope, payload.operation, this.options.nodeId())) {
|
|
16
|
+
this.options.send('node.response', { error: 'USER_AUTHORIZATION_REQUIRED' }, envelope.id);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const startedAt = performance.now();
|
|
20
|
+
try {
|
|
21
|
+
const data = payload.data !== null && typeof payload.data === 'object'
|
|
22
|
+
? {
|
|
23
|
+
...payload.data,
|
|
24
|
+
__workspaceWatchSessionHash: payload.userAuthorization
|
|
25
|
+
.sessionHash
|
|
26
|
+
}
|
|
27
|
+
: payload.data;
|
|
28
|
+
const result = await this.options.execute(payload.operation, data);
|
|
29
|
+
this.options.send('node.response', { result }, envelope.id);
|
|
30
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
31
|
+
if (durationMs >= 250)
|
|
32
|
+
nodeLog('node.operation.completed', {
|
|
33
|
+
nodeId: this.options.nodeId(),
|
|
34
|
+
operation: payload.operation,
|
|
35
|
+
requestId: envelope.id,
|
|
36
|
+
durationMs
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
nodeLog('node.operation.failed', {
|
|
41
|
+
nodeId: this.options.nodeId(),
|
|
42
|
+
operation: payload.operation,
|
|
43
|
+
requestId: envelope.id,
|
|
44
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
45
|
+
error: error instanceof Error ? error.message : 'INTERNAL_ERROR'
|
|
46
|
+
});
|
|
47
|
+
this.options.send('node.response', { error: error instanceof Error ? error.message : 'INTERNAL_ERROR' }, envelope.id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { NodeEnvelope, WorkbenchEventCategory } from '@myagentroam/protocol';
|
|
2
|
+
import type { FakeRunnerStatus } from '../fake-runner.js';
|
|
3
|
+
import type { CodexRunner } from '../runner/codex-runner.js';
|
|
4
|
+
import type { ClaudeCodeRunner } from '../runner/claude-code-runner.js';
|
|
5
|
+
import type { OpenCodeRunner } from '../runner/opencode-runner.js';
|
|
6
|
+
import type { NodeRuntimeState } from '../runtime-state.js';
|
|
7
|
+
import type { RunDomainState } from './run-domain-state.js';
|
|
8
|
+
import type { RunEventService } from './run-event-service.js';
|
|
9
|
+
import type { WorkbenchEventService } from './workbench-event-service.js';
|
|
10
|
+
import type { WorkspaceQueueWorkbenchService } from './workspace-queue-workbench-service.js';
|
|
11
|
+
import type { RunnerImageAttachment } from './run-attachment-service.js';
|
|
12
|
+
export declare function workbenchPayloadSessionId(runtime: NodeRuntimeState, category: WorkbenchEventCategory, payload: unknown): string | undefined;
|
|
13
|
+
interface NodeRunEventBridgeOptions {
|
|
14
|
+
readonly runtime: NodeRuntimeState;
|
|
15
|
+
readonly state: RunDomainState;
|
|
16
|
+
readonly events: RunEventService;
|
|
17
|
+
readonly workbench: WorkbenchEventService;
|
|
18
|
+
readonly queues: WorkspaceQueueWorkbenchService<RunnerImageAttachment>;
|
|
19
|
+
readonly codex: CodexRunner;
|
|
20
|
+
readonly claude: ClaudeCodeRunner;
|
|
21
|
+
readonly openCode: OpenCodeRunner;
|
|
22
|
+
readonly send: (type: string, payload: unknown, replyTo?: string) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Bridges process-local Run state to normalized control and Workbench events. */
|
|
25
|
+
export declare class NodeRunEventBridge {
|
|
26
|
+
private readonly options;
|
|
27
|
+
constructor(options: NodeRunEventBridgeOptions);
|
|
28
|
+
publishApproval(input: {
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
readonly approvalId: string;
|
|
31
|
+
readonly payload: unknown;
|
|
32
|
+
readonly expiresAt: number;
|
|
33
|
+
}): void;
|
|
34
|
+
emitRun(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
|
|
35
|
+
emitWorkbench(category: WorkbenchEventCategory, payload: unknown): void;
|
|
36
|
+
emitQueue(workspaceId: string, sessionId: string): void;
|
|
37
|
+
migrateRunnerSessionIdentity(sessionId: string, nativeSessionId: string): void;
|
|
38
|
+
reconcile(envelope: NodeEnvelope): void;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
2
|
+
export function workbenchPayloadSessionId(runtime, category, payload) {
|
|
3
|
+
if (!isPlainRecord(payload))
|
|
4
|
+
return undefined;
|
|
5
|
+
if (category === 'session' && isPlainRecord(payload.session)) {
|
|
6
|
+
const id = payload.session.id;
|
|
7
|
+
return typeof id === 'string' ? id : undefined;
|
|
8
|
+
}
|
|
9
|
+
if (category === 'run' && isNodeAgentRun(payload.run))
|
|
10
|
+
return payload.run.sessionId;
|
|
11
|
+
if (category === 'conversation' && isNodeConversationTurn(payload.turn))
|
|
12
|
+
return payload.turn.sessionId;
|
|
13
|
+
if (category === 'conversation' && isPlainRecord(payload.event)) {
|
|
14
|
+
const runId = payload.event.runId;
|
|
15
|
+
return typeof runId === 'string' ? runtime.getRun(runId)?.sessionId : undefined;
|
|
16
|
+
}
|
|
17
|
+
if (category === 'queue') {
|
|
18
|
+
const id = payload.sessionId;
|
|
19
|
+
return typeof id === 'string' ? id : undefined;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Bridges process-local Run state to normalized control and Workbench events. */
|
|
24
|
+
export class NodeRunEventBridge {
|
|
25
|
+
options;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
publishApproval(input) {
|
|
30
|
+
this.options.send('approval.request', input);
|
|
31
|
+
this.emitRun(input.runId, 'approval.request', input);
|
|
32
|
+
}
|
|
33
|
+
emitRun(runId, eventType, payload, status) {
|
|
34
|
+
this.options.events.emit(runId, eventType, payload, status);
|
|
35
|
+
}
|
|
36
|
+
emitWorkbench(category, payload) {
|
|
37
|
+
this.options.workbench.emit(category, payload);
|
|
38
|
+
}
|
|
39
|
+
emitQueue(workspaceId, sessionId) {
|
|
40
|
+
this.emitWorkbench('queue', {
|
|
41
|
+
workspaceId,
|
|
42
|
+
sessionId,
|
|
43
|
+
queue: this.options.queues.present(workspaceId, sessionId)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
migrateRunnerSessionIdentity(sessionId, nativeSessionId) {
|
|
47
|
+
for (const [turnId, managed] of this.options.codex.execution.managedTurns) {
|
|
48
|
+
if (managed.sessionId === sessionId)
|
|
49
|
+
this.options.codex.execution.managedTurns.set(turnId, {
|
|
50
|
+
...managed,
|
|
51
|
+
sessionId: nativeSessionId
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for (const [runId, managed] of this.options.codex.execution.runs) {
|
|
55
|
+
if (managed.sessionId === sessionId)
|
|
56
|
+
this.options.codex.execution.runs.set(runId, { ...managed, sessionId: nativeSessionId });
|
|
57
|
+
}
|
|
58
|
+
for (const [runId, managed] of this.options.claude.execution.runs) {
|
|
59
|
+
if (managed.marSessionId === sessionId)
|
|
60
|
+
this.options.claude.execution.runs.set(runId, {
|
|
61
|
+
...managed,
|
|
62
|
+
marSessionId: nativeSessionId
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
for (const [runId, managed] of this.options.openCode.execution) {
|
|
66
|
+
if (managed.sessionId === sessionId)
|
|
67
|
+
this.options.openCode.execution.set(runId, { ...managed, sessionId: nativeSessionId });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
reconcile(envelope) {
|
|
71
|
+
const payload = envelope.payload;
|
|
72
|
+
if (!Array.isArray(payload.runIds) || payload.runIds.some((id) => typeof id !== 'string')) {
|
|
73
|
+
this.options.send('error', { code: 'MESSAGE_INVALID' }, envelope.id);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const runId of payload.runIds) {
|
|
77
|
+
const known = this.options.state.active.has(runId);
|
|
78
|
+
this.options.runtime.reconcileRun(runId, known);
|
|
79
|
+
this.options.send('lease.reconcile', { runId, known });
|
|
80
|
+
}
|
|
81
|
+
this.options.send('ack', { type: 'runs.reconcile' }, envelope.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NodeConfig } from '../config.js';
|
|
2
|
+
import type { TerminalManager } from '../terminal.js';
|
|
3
|
+
interface NodeTerminalChannelOptions {
|
|
4
|
+
readonly config: () => NodeConfig | undefined;
|
|
5
|
+
readonly controlConnected: () => boolean;
|
|
6
|
+
readonly stopped: () => boolean;
|
|
7
|
+
readonly reconnect: boolean;
|
|
8
|
+
readonly reconnectDelay: (attempt: number) => number;
|
|
9
|
+
readonly manager: TerminalManager;
|
|
10
|
+
}
|
|
11
|
+
export declare class NodeTerminalChannel {
|
|
12
|
+
private readonly options;
|
|
13
|
+
private socket;
|
|
14
|
+
private reconnectTimer;
|
|
15
|
+
private attempts;
|
|
16
|
+
private readonly state;
|
|
17
|
+
constructor(options: NodeTerminalChannelOptions);
|
|
18
|
+
connect(): void;
|
|
19
|
+
disconnect(): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
private scheduleReconnect;
|
|
22
|
+
private handle;
|
|
23
|
+
private publish;
|
|
24
|
+
private sendFrame;
|
|
25
|
+
private send;
|
|
26
|
+
private invalid;
|
|
27
|
+
private releaseAttachments;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { terminalDataFrameSchema } from '@myagentroam/protocol';
|
|
3
|
+
import { nodeTerminalConnectEndpoint, isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
4
|
+
import { TerminalRelayState } from './terminal-relay-state.js';
|
|
5
|
+
export class NodeTerminalChannel {
|
|
6
|
+
options;
|
|
7
|
+
socket;
|
|
8
|
+
reconnectTimer;
|
|
9
|
+
attempts = 0;
|
|
10
|
+
state = new TerminalRelayState();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
options.manager.onFrame((frame) => this.publish(frame));
|
|
14
|
+
}
|
|
15
|
+
connect() {
|
|
16
|
+
const config = this.options.config();
|
|
17
|
+
if (config?.nodeId === undefined ||
|
|
18
|
+
config.credential === undefined ||
|
|
19
|
+
this.options.stopped() ||
|
|
20
|
+
(this.socket !== undefined &&
|
|
21
|
+
(this.socket.readyState === this.socket.OPEN ||
|
|
22
|
+
this.socket.readyState === this.socket.CONNECTING)))
|
|
23
|
+
return;
|
|
24
|
+
const socket = new WebSocket(nodeTerminalConnectEndpoint(config.serverUrl));
|
|
25
|
+
this.socket = socket;
|
|
26
|
+
socket.once('open', () => this.send({ type: 'authenticate', nodeId: config.nodeId, credential: config.credential }));
|
|
27
|
+
socket.on('message', (raw, isBinary) => isBinary ? socket.close(4002, 'TERMINAL_MESSAGE_INVALID') : this.handle(raw.toString()));
|
|
28
|
+
socket.once('close', () => {
|
|
29
|
+
if (this.socket !== socket)
|
|
30
|
+
return;
|
|
31
|
+
this.socket = undefined;
|
|
32
|
+
this.releaseAttachments();
|
|
33
|
+
this.scheduleReconnect();
|
|
34
|
+
});
|
|
35
|
+
socket.once('error', () => undefined);
|
|
36
|
+
}
|
|
37
|
+
disconnect() {
|
|
38
|
+
this.socket?.close();
|
|
39
|
+
}
|
|
40
|
+
stop() {
|
|
41
|
+
if (this.reconnectTimer !== undefined)
|
|
42
|
+
clearTimeout(this.reconnectTimer);
|
|
43
|
+
this.reconnectTimer = undefined;
|
|
44
|
+
this.releaseAttachments();
|
|
45
|
+
this.socket?.close();
|
|
46
|
+
this.socket = undefined;
|
|
47
|
+
}
|
|
48
|
+
scheduleReconnect() {
|
|
49
|
+
if (this.options.stopped() ||
|
|
50
|
+
!this.options.reconnect ||
|
|
51
|
+
!this.options.controlConnected() ||
|
|
52
|
+
this.reconnectTimer !== undefined)
|
|
53
|
+
return;
|
|
54
|
+
this.reconnectTimer = setTimeout(() => {
|
|
55
|
+
this.reconnectTimer = undefined;
|
|
56
|
+
this.connect();
|
|
57
|
+
}, this.options.reconnectDelay(this.attempts++));
|
|
58
|
+
}
|
|
59
|
+
handle(raw) {
|
|
60
|
+
let message;
|
|
61
|
+
try {
|
|
62
|
+
message = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
this.invalid();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!isPlainRecord(message) || typeof message.type !== 'string') {
|
|
69
|
+
this.invalid();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (message.type === 'authenticated') {
|
|
73
|
+
if (typeof message.nodeId !== 'string' ||
|
|
74
|
+
Object.keys(message).some((key) => key !== 'type' && key !== 'nodeId'))
|
|
75
|
+
this.invalid();
|
|
76
|
+
else
|
|
77
|
+
this.attempts = 0;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (message.type === 'attach') {
|
|
81
|
+
if (typeof message.ticket !== 'string' ||
|
|
82
|
+
typeof message.terminalId !== 'string' ||
|
|
83
|
+
typeof message.attachmentId !== 'string' ||
|
|
84
|
+
Object.keys(message).some((key) => !['type', 'ticket', 'terminalId', 'attachmentId'].includes(key))) {
|
|
85
|
+
this.invalid();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.state.attachments.set(message.ticket, {
|
|
89
|
+
terminalId: message.terminalId,
|
|
90
|
+
attachmentId: message.attachmentId
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (message.type === 'detach' || message.type === 'close') {
|
|
95
|
+
if (typeof message.ticket !== 'string' ||
|
|
96
|
+
Object.keys(message).some((key) => !['type', 'ticket'].includes(key))) {
|
|
97
|
+
this.invalid();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const attachment = this.state.attachments.get(message.ticket);
|
|
101
|
+
if (attachment === undefined)
|
|
102
|
+
return;
|
|
103
|
+
this.state.attachments.delete(message.ticket);
|
|
104
|
+
if (message.type === 'detach')
|
|
105
|
+
this.options.manager.detach(attachment.attachmentId);
|
|
106
|
+
else
|
|
107
|
+
void this.options.manager
|
|
108
|
+
.acceptFrame(attachment.attachmentId, {
|
|
109
|
+
type: 'close',
|
|
110
|
+
terminalId: attachment.terminalId
|
|
111
|
+
})
|
|
112
|
+
.catch(() => undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (message.type === 'frame') {
|
|
116
|
+
if (typeof message.ticket !== 'string' ||
|
|
117
|
+
Object.keys(message).some((key) => !['type', 'ticket', 'frame'].includes(key))) {
|
|
118
|
+
this.invalid();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const parsed = terminalDataFrameSchema.safeParse(message.frame);
|
|
122
|
+
const attachment = this.state.attachments.get(message.ticket);
|
|
123
|
+
if (!parsed.success || !['input', 'resize', 'close'].includes(parsed.data.type)) {
|
|
124
|
+
this.invalid();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (attachment === undefined)
|
|
128
|
+
return;
|
|
129
|
+
if (parsed.data.terminalId !== attachment.terminalId) {
|
|
130
|
+
this.invalid();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
void this.options.manager.acceptFrame(attachment.attachmentId, parsed.data).catch(() => {
|
|
134
|
+
this.state.attachments.delete(message.ticket);
|
|
135
|
+
this.options.manager.detach(attachment.attachmentId);
|
|
136
|
+
this.sendFrame(message.ticket, {
|
|
137
|
+
type: 'error',
|
|
138
|
+
terminalId: attachment.terminalId,
|
|
139
|
+
code: 'TERMINAL_FRAME_REJECTED'
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (message.type === 'shutdown' && Object.keys(message).length === 1) {
|
|
145
|
+
this.state.attachments.clear();
|
|
146
|
+
void this.options.manager.shutdown();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.invalid();
|
|
150
|
+
}
|
|
151
|
+
publish(frame) {
|
|
152
|
+
for (const [ticket, attachment] of this.state.attachments) {
|
|
153
|
+
if (attachment.terminalId !== frame.terminalId)
|
|
154
|
+
continue;
|
|
155
|
+
this.sendFrame(ticket, frame);
|
|
156
|
+
if (frame.type === 'exit')
|
|
157
|
+
this.state.attachments.delete(ticket);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
sendFrame(ticket, frame) {
|
|
161
|
+
this.send({ type: 'frame', ticket, frame });
|
|
162
|
+
}
|
|
163
|
+
send(message) {
|
|
164
|
+
const socket = this.socket;
|
|
165
|
+
if (socket === undefined || socket.readyState !== socket.OPEN)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
socket.send(JSON.stringify(message));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
socket.close(1011, 'TERMINAL_SEND_FAILED');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
invalid() {
|
|
175
|
+
this.socket?.close(4002, 'TERMINAL_MESSAGE_INVALID');
|
|
176
|
+
}
|
|
177
|
+
releaseAttachments() {
|
|
178
|
+
for (const attachment of this.state.attachments.values())
|
|
179
|
+
this.options.manager.detach(attachment.attachmentId);
|
|
180
|
+
this.state.attachments.clear();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NodeUpgradeStatus } from '@myagentroam/protocol';
|
|
2
|
+
export declare class NodeUpgradeCoordinator {
|
|
3
|
+
private readonly requestPath;
|
|
4
|
+
private readonly activeRunCount;
|
|
5
|
+
private readonly onStatus;
|
|
6
|
+
private readonly onReady;
|
|
7
|
+
private readonly now;
|
|
8
|
+
private status;
|
|
9
|
+
private timer;
|
|
10
|
+
constructor(requestPath: () => string, activeRunCount: () => number, onStatus: (status: NodeUpgradeStatus) => void, onReady: () => void, now?: () => number);
|
|
11
|
+
snapshot(): NodeUpgradeStatus;
|
|
12
|
+
loadResult(resultPath: string): Promise<void>;
|
|
13
|
+
request(data: unknown): {
|
|
14
|
+
status: 'ACCEPTED' | 'ALREADY_PENDING';
|
|
15
|
+
targetVersion: string;
|
|
16
|
+
activeRunCount: number;
|
|
17
|
+
};
|
|
18
|
+
cancel(): {
|
|
19
|
+
cancelled: true;
|
|
20
|
+
};
|
|
21
|
+
blocks(operation: string): boolean;
|
|
22
|
+
pending(): boolean;
|
|
23
|
+
stop(): void;
|
|
24
|
+
private checkReady;
|
|
25
|
+
private checkReadySafely;
|
|
26
|
+
private update;
|
|
27
|
+
private clearTimer;
|
|
28
|
+
}
|
|
29
|
+
export declare function nodeUpgradeRequestPath(databasePath: string): string;
|