@platless/axom-cli 0.3.13 → 0.5.9
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/dist/axom-patch/cli/README.md +30 -8
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +33 -6
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/{fileOps.js → files/text.js} +117 -38
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// The keeper-side half of the kit: listen on the 0600 socket, authenticate with the connection token, answer the generic
|
|
2
|
+
// `keeper.*` methods, dispatch to registered method handlers and push stream records under credit.
|
|
3
|
+
// Keeper-side module: node builtins and kit siblings only (the import guard enforces it).
|
|
4
|
+
import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { promises as fs, renameSync, unlinkSync } from 'node:fs';
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
import { sampleActivity } from './activity.js';
|
|
8
|
+
import { encodeFrame, LineDecoder, parseServerBound } from './ipc.js';
|
|
9
|
+
import { ErrorCode, frameLimitsOf, isProtocolWindow, KeeperMethodError, MAX_CONNECTIONS, MAX_EVT_RECORDS, PRE_HELLO_MAX_BYTES, } from './protocol.js';
|
|
10
|
+
const GENERIC = new Set(['keeper.hello', 'keeper.ping', 'keeper.info', 'keeper.activity', 'keeper.drain', 'keeper.stop', 'keeper.subscribe', 'keeper.unsubscribe']);
|
|
11
|
+
const MAX_CREDIT = 1_000_000_000;
|
|
12
|
+
const MAX_CREDIT_BYTES = Number.MAX_SAFE_INTEGER;
|
|
13
|
+
const wireError = (code, message) => ({ code, message });
|
|
14
|
+
class Subscription {
|
|
15
|
+
connection;
|
|
16
|
+
stream;
|
|
17
|
+
source;
|
|
18
|
+
credit;
|
|
19
|
+
/** Bytes the subscriber may still be sent; `Infinity` on a subscription without a byte window. */
|
|
20
|
+
byteCredit;
|
|
21
|
+
byteWindowed;
|
|
22
|
+
next;
|
|
23
|
+
pumping = false;
|
|
24
|
+
dirty = false;
|
|
25
|
+
closed = false;
|
|
26
|
+
started = false;
|
|
27
|
+
unwatch;
|
|
28
|
+
constructor(connection, stream, source, from, credit, creditBytes) {
|
|
29
|
+
this.connection = connection;
|
|
30
|
+
this.stream = stream;
|
|
31
|
+
this.source = source;
|
|
32
|
+
this.next = from;
|
|
33
|
+
this.credit = credit;
|
|
34
|
+
this.byteWindowed = creditBytes !== undefined;
|
|
35
|
+
this.byteCredit = creditBytes ?? Number.POSITIVE_INFINITY;
|
|
36
|
+
this.unwatch = source.watch(() => this.kick());
|
|
37
|
+
}
|
|
38
|
+
/** Begins pushing. Called after the subscribe reply is written so no `evt` can overtake its `res`. */
|
|
39
|
+
start() { this.started = true; this.kick(); }
|
|
40
|
+
grant(n, bytes) {
|
|
41
|
+
this.credit = Math.min(MAX_CREDIT, this.credit + n);
|
|
42
|
+
if (this.byteWindowed && bytes !== undefined)
|
|
43
|
+
this.byteCredit = Math.min(MAX_CREDIT_BYTES, this.byteCredit + bytes);
|
|
44
|
+
this.kick();
|
|
45
|
+
}
|
|
46
|
+
close() { if (this.closed)
|
|
47
|
+
return; this.closed = true; this.unwatch(); }
|
|
48
|
+
kick() {
|
|
49
|
+
this.dirty = true;
|
|
50
|
+
if (this.started && !this.pumping && !this.closed)
|
|
51
|
+
void this.pump();
|
|
52
|
+
}
|
|
53
|
+
async pump() {
|
|
54
|
+
this.pumping = true;
|
|
55
|
+
try {
|
|
56
|
+
while (this.dirty && !this.closed) {
|
|
57
|
+
this.dirty = false;
|
|
58
|
+
while (!this.closed && this.credit > 0 && this.byteCredit > 0) {
|
|
59
|
+
if (!await this.connection.writable())
|
|
60
|
+
return;
|
|
61
|
+
const limit = Math.min(this.credit, MAX_EVT_RECORDS);
|
|
62
|
+
const records = await (this.byteWindowed ? this.source.read(this.next, limit, { maxBytes: this.byteCredit }) : this.source.read(this.next, limit));
|
|
63
|
+
if (this.closed || records.length === 0)
|
|
64
|
+
break;
|
|
65
|
+
const { batch, bytes } = this.fit(records);
|
|
66
|
+
if (batch.length === 0) {
|
|
67
|
+
this.end('record_too_large');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.next = batch[batch.length - 1].o + 1;
|
|
71
|
+
this.credit -= batch.length;
|
|
72
|
+
if (this.byteWindowed)
|
|
73
|
+
this.byteCredit -= bytes;
|
|
74
|
+
this.connection.send({ t: 'evt', stream: this.stream, records: batch, ...(this.byteWindowed ? { bytes } : {}) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
this.end('source_error');
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
this.pumping = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Longest prefix of `records` whose evt frame stays under the outbound cap, and its size (each record's JSON line). */
|
|
86
|
+
fit(records) {
|
|
87
|
+
const budget = this.connection.limits.out - 256 - this.stream.length * 2;
|
|
88
|
+
let used = 0;
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const record of records) {
|
|
91
|
+
const size = Buffer.byteLength(JSON.stringify(record)) + 1;
|
|
92
|
+
if (used + size > budget)
|
|
93
|
+
break;
|
|
94
|
+
used += size;
|
|
95
|
+
count += 1;
|
|
96
|
+
}
|
|
97
|
+
return { batch: records.slice(0, count), bytes: used };
|
|
98
|
+
}
|
|
99
|
+
end(reason) {
|
|
100
|
+
this.close();
|
|
101
|
+
this.connection.removeSubscription(this.stream, this);
|
|
102
|
+
this.connection.send({ t: 'evt', stream: this.stream, records: [], closed: reason });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
class Connection {
|
|
106
|
+
server;
|
|
107
|
+
socket;
|
|
108
|
+
id = randomUUID();
|
|
109
|
+
limits;
|
|
110
|
+
authenticated = false;
|
|
111
|
+
decoder;
|
|
112
|
+
subscriptions = new Map();
|
|
113
|
+
dead = false;
|
|
114
|
+
authTimer;
|
|
115
|
+
drainWaiters = [];
|
|
116
|
+
constructor(server, socket, unauthenticatedTimeoutMs) {
|
|
117
|
+
this.server = server;
|
|
118
|
+
this.socket = socket;
|
|
119
|
+
this.limits = frameLimitsOf(server.options.kind);
|
|
120
|
+
this.decoder = new LineDecoder(Math.min(PRE_HELLO_MAX_BYTES, this.limits.in)); // widened to the kind's cap once hello succeeds
|
|
121
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
122
|
+
socket.on('error', () => this.destroy());
|
|
123
|
+
socket.on('close', () => this.destroy());
|
|
124
|
+
socket.on('drain', () => this.wake());
|
|
125
|
+
this.authTimer = setTimeout(() => { if (!this.authenticated)
|
|
126
|
+
this.destroy(); }, unauthenticatedTimeoutMs);
|
|
127
|
+
this.authTimer.unref();
|
|
128
|
+
}
|
|
129
|
+
get isDead() { return this.dead; }
|
|
130
|
+
pendingAnswers = 0;
|
|
131
|
+
/** No `res` frame of this connection is still waiting to leave the socket. Pushed `evt` data does not count: a stalled subscriber must not hold up a stop. */
|
|
132
|
+
get flushed() { return this.dead || this.pendingAnswers === 0; }
|
|
133
|
+
destroy() {
|
|
134
|
+
if (this.dead)
|
|
135
|
+
return;
|
|
136
|
+
this.dead = true;
|
|
137
|
+
clearTimeout(this.authTimer);
|
|
138
|
+
for (const sub of this.subscriptions.values())
|
|
139
|
+
sub.close();
|
|
140
|
+
this.subscriptions.clear();
|
|
141
|
+
this.socket.destroy();
|
|
142
|
+
this.wake();
|
|
143
|
+
this.server.forget(this);
|
|
144
|
+
}
|
|
145
|
+
wake() { const waiters = this.drainWaiters; this.drainWaiters = []; for (const w of waiters)
|
|
146
|
+
w(); }
|
|
147
|
+
/** Resolves true when the socket can take more data, false when the connection died meanwhile. */
|
|
148
|
+
async writable() {
|
|
149
|
+
while (!this.dead && this.socket.writableNeedDrain)
|
|
150
|
+
await new Promise((resolve) => this.drainWaiters.push(resolve));
|
|
151
|
+
return !this.dead;
|
|
152
|
+
}
|
|
153
|
+
/** Writes one frame. An oversized `res` is replaced by an error answer so the request still completes. */
|
|
154
|
+
send(frame, then) {
|
|
155
|
+
if (this.dead)
|
|
156
|
+
return;
|
|
157
|
+
let encoded;
|
|
158
|
+
try {
|
|
159
|
+
encoded = encodeFrame(frame);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
encoded = frame.t === 'res' ? encodeFrame({ t: 'res', id: frame.id, ok: false, error: wireError(ErrorCode.internal, 'Result is not serialisable') }) : encodeFrame({ t: 'evt', stream: frame.stream, records: [], closed: 'source_error' });
|
|
163
|
+
}
|
|
164
|
+
if (encoded.length > this.limits.out) {
|
|
165
|
+
if (frame.t !== 'res')
|
|
166
|
+
return; // the pump never builds such an evt
|
|
167
|
+
encoded = encodeFrame({ t: 'res', id: frame.id, ok: false, error: wireError(ErrorCode.frameTooLarge, `Response exceeds ${this.limits.out} bytes`) });
|
|
168
|
+
}
|
|
169
|
+
if (frame.t === 'res') {
|
|
170
|
+
this.pendingAnswers += 1;
|
|
171
|
+
this.socket.write(encoded.data, () => { this.pendingAnswers -= 1; then?.(); });
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
this.socket.write(encoded.data);
|
|
175
|
+
}
|
|
176
|
+
removeSubscription(stream, sub) { if (this.subscriptions.get(stream) === sub)
|
|
177
|
+
this.subscriptions.delete(stream); }
|
|
178
|
+
starts = [];
|
|
179
|
+
subscribe(stream, source, from, credit, creditBytes) {
|
|
180
|
+
this.subscriptions.get(stream)?.close();
|
|
181
|
+
const sub = new Subscription(this, stream, source, from, credit, creditBytes);
|
|
182
|
+
this.subscriptions.set(stream, sub);
|
|
183
|
+
this.starts.push(sub);
|
|
184
|
+
}
|
|
185
|
+
discardStarts() { const starts = this.starts; this.starts = []; for (const sub of starts)
|
|
186
|
+
this.unsubscribeIf(sub); }
|
|
187
|
+
unsubscribeIf(sub) { if (this.subscriptions.get(sub.stream) === sub) {
|
|
188
|
+
sub.close();
|
|
189
|
+
this.subscriptions.delete(sub.stream);
|
|
190
|
+
} }
|
|
191
|
+
startSubscriptions() { const starts = this.starts; this.starts = []; for (const sub of starts)
|
|
192
|
+
if (this.subscriptions.get(sub.stream) === sub)
|
|
193
|
+
sub.start(); }
|
|
194
|
+
unsubscribe(stream) {
|
|
195
|
+
const sub = this.subscriptions.get(stream);
|
|
196
|
+
if (!sub)
|
|
197
|
+
return false;
|
|
198
|
+
sub.close();
|
|
199
|
+
this.subscriptions.delete(stream);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
onData(chunk) {
|
|
203
|
+
let lines;
|
|
204
|
+
try {
|
|
205
|
+
lines = this.decoder.push(chunk);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
this.protocolViolation();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
if (this.dead)
|
|
213
|
+
return;
|
|
214
|
+
let frame;
|
|
215
|
+
try {
|
|
216
|
+
frame = parseServerBound(line);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
this.protocolViolation();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.dispatch(frame);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** A malformed or oversized frame ends this connection (and only this one). */
|
|
226
|
+
protocolViolation() { this.destroy(); }
|
|
227
|
+
dispatch(frame) {
|
|
228
|
+
if (frame.t === 'credit') {
|
|
229
|
+
if (!this.authenticated) {
|
|
230
|
+
this.destroy();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.subscriptions.get(frame.stream)?.grant(frame.n, frame.bytes);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const reply = (ok, payload, then) => this.send(ok ? { t: 'res', id: frame.id, ok: true, result: payload } : { t: 'res', id: frame.id, ok: false, error: payload }, then);
|
|
237
|
+
const fail = (code, message, then) => reply(false, wireError(code, message), then);
|
|
238
|
+
const { method } = frame;
|
|
239
|
+
const params = frame.params ?? {};
|
|
240
|
+
if (method === 'keeper.hello') { // synchronous on purpose: a pipelined request right behind it must see the authenticated state
|
|
241
|
+
const token = typeof params.token === 'string' ? Buffer.from(params.token) : Buffer.alloc(0);
|
|
242
|
+
const expected = Buffer.from(this.server.token);
|
|
243
|
+
if (token.length !== expected.length || !timingSafeEqual(token, expected)) {
|
|
244
|
+
fail(ErrorCode.badToken, 'Invalid keeper token', () => this.destroy());
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!isProtocolWindow(params.daemonProtocol)) {
|
|
248
|
+
fail(ErrorCode.badRequest, 'daemonProtocol must be [min, max]');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.authenticated = true;
|
|
252
|
+
clearTimeout(this.authTimer);
|
|
253
|
+
this.decoder.setMax(this.limits.in);
|
|
254
|
+
reply(true, this.server.hello());
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!this.authenticated && method !== 'keeper.ping') {
|
|
258
|
+
fail(ErrorCode.unauthenticated, 'keeper.hello required');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const context = {
|
|
262
|
+
connection: { id: this.id },
|
|
263
|
+
subscribe: (stream, source, from, credit, creditBytes) => this.subscribe(stream, source, from, credit, creditBytes),
|
|
264
|
+
unsubscribe: (stream) => { this.unsubscribe(stream); },
|
|
265
|
+
};
|
|
266
|
+
void (async () => {
|
|
267
|
+
try {
|
|
268
|
+
const result = GENERIC.has(method) ? await this.server.generic(method, params, context, () => reply(true, {}, () => this.server.stopped(params.mode)))
|
|
269
|
+
: await this.server.callHandler(method, params, context);
|
|
270
|
+
if (result !== NOT_REPLIED)
|
|
271
|
+
reply(true, result);
|
|
272
|
+
this.startSubscriptions();
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
this.discardStarts();
|
|
276
|
+
if (error instanceof KeeperMethodError)
|
|
277
|
+
fail(error.code, error.message);
|
|
278
|
+
else
|
|
279
|
+
fail(ErrorCode.internal, error instanceof Error ? error.message : String(error));
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const NOT_REPLIED = Symbol('replied');
|
|
285
|
+
export class KeeperServer {
|
|
286
|
+
options;
|
|
287
|
+
startedAt = Date.now();
|
|
288
|
+
handlers = new Map();
|
|
289
|
+
streamResolver;
|
|
290
|
+
connections = new Set();
|
|
291
|
+
net = net.createServer((socket) => this.accept(socket));
|
|
292
|
+
tokenValue = '';
|
|
293
|
+
drainingFlag = false;
|
|
294
|
+
startsWork = new Set();
|
|
295
|
+
samplers = [];
|
|
296
|
+
inFlightCount = 0;
|
|
297
|
+
stopState;
|
|
298
|
+
constructor(options) {
|
|
299
|
+
this.options = options;
|
|
300
|
+
if (options.activity)
|
|
301
|
+
this.samplers.push(options.activity);
|
|
302
|
+
this.net.on('error', () => { });
|
|
303
|
+
}
|
|
304
|
+
get token() { return this.tokenValue; }
|
|
305
|
+
get connectionCount() { return this.connections.size; }
|
|
306
|
+
/** Connections that passed `keeper.hello` (a daemon, not a stray probe). */
|
|
307
|
+
get authenticatedConnectionCount() { let count = 0; for (const connection of this.connections)
|
|
308
|
+
if (connection.authenticated)
|
|
309
|
+
count += 1; return count; }
|
|
310
|
+
get draining() { return this.drainingFlag; }
|
|
311
|
+
/** Domain requests currently running (the `keeper.*` methods are not counted). */
|
|
312
|
+
get inFlight() { return this.inFlightCount; }
|
|
313
|
+
/** Registers a domain method. The `keeper.*` namespace belongs to the kit. */
|
|
314
|
+
register(method, handler, options = {}) {
|
|
315
|
+
if (method.startsWith('keeper.'))
|
|
316
|
+
throw new Error(`The keeper.* namespace is reserved: ${method}`);
|
|
317
|
+
if (this.handlers.has(method))
|
|
318
|
+
throw new Error(`Method already registered: ${method}`);
|
|
319
|
+
this.handlers.set(method, handler);
|
|
320
|
+
if (options.startsWork)
|
|
321
|
+
this.startsWork.add(method);
|
|
322
|
+
}
|
|
323
|
+
/** Adds a source for `keeper.activity`. Each sample asks every sampler afresh; nothing is accumulated. */
|
|
324
|
+
addActivitySampler(sampler) { this.samplers.push(sampler); }
|
|
325
|
+
report(error) { try {
|
|
326
|
+
this.options.onError?.(error);
|
|
327
|
+
}
|
|
328
|
+
catch { /* diagnostics must not matter */ } }
|
|
329
|
+
/** Stops accepting new work (methods registered with `startsWork`). Everything else keeps working. Idempotent. */
|
|
330
|
+
drain() {
|
|
331
|
+
if (this.drainingFlag)
|
|
332
|
+
return;
|
|
333
|
+
this.drainingFlag = true;
|
|
334
|
+
try {
|
|
335
|
+
this.options.onDrain?.();
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
this.report(error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** How `keeper.subscribe{stream}` finds its source. */
|
|
342
|
+
setStreamResolver(resolver) { this.streamResolver = resolver; }
|
|
343
|
+
hello() {
|
|
344
|
+
const o = this.options;
|
|
345
|
+
return { kind: o.kind, protocolVersion: o.protocolVersion, buildVersion: o.buildVersion, gen: o.gen, pid: process.pid, startedAt: this.startedAt, capabilities: [...(o.capabilities ?? [])] };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Generates a fresh 32-byte token and listens on the 0600 socket. The token reaches `run/<gen>/token` (0600, atomic rename)
|
|
349
|
+
* only once this process actually owns the socket, before any connection is served: a second start for the same generation
|
|
350
|
+
* that fails with EADDRINUSE must leave the live keeper's token file alone.
|
|
351
|
+
*/
|
|
352
|
+
async listen() {
|
|
353
|
+
const token = randomBytes(32).toString('hex');
|
|
354
|
+
const temp = `${this.options.tokenFile}.${randomUUID()}.tmp`;
|
|
355
|
+
await fs.writeFile(temp, token, { mode: 0o600 });
|
|
356
|
+
try {
|
|
357
|
+
await new Promise((resolve, reject) => {
|
|
358
|
+
const failed = (error) => reject(error);
|
|
359
|
+
this.net.once('error', failed);
|
|
360
|
+
this.net.listen(this.options.socket, () => {
|
|
361
|
+
this.net.off('error', failed);
|
|
362
|
+
try {
|
|
363
|
+
renameSync(temp, this.options.tokenFile);
|
|
364
|
+
this.tokenValue = token;
|
|
365
|
+
resolve();
|
|
366
|
+
} // synchronous: no connection is accepted before this
|
|
367
|
+
catch (error) {
|
|
368
|
+
this.net.close();
|
|
369
|
+
reject(error);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
try {
|
|
376
|
+
unlinkSync(temp);
|
|
377
|
+
}
|
|
378
|
+
catch { /* already gone */ }
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
await fs.chmod(this.options.socket, 0o600);
|
|
382
|
+
}
|
|
383
|
+
async close() {
|
|
384
|
+
for (const connection of [...this.connections])
|
|
385
|
+
connection.destroy();
|
|
386
|
+
if (!this.net.listening)
|
|
387
|
+
return;
|
|
388
|
+
await new Promise((resolve) => this.net.close(() => resolve()));
|
|
389
|
+
}
|
|
390
|
+
forget(connection) { this.connections.delete(connection); }
|
|
391
|
+
accept(socket) {
|
|
392
|
+
if (this.connections.size >= (this.options.maxConnections ?? MAX_CONNECTIONS)) {
|
|
393
|
+
socket.destroy();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.connections.add(new Connection(this, socket, this.options.unauthenticatedTimeoutMs ?? 10_000));
|
|
397
|
+
}
|
|
398
|
+
async callHandler(method, params, context) {
|
|
399
|
+
const handler = this.handlers.get(method);
|
|
400
|
+
if (!handler)
|
|
401
|
+
throw new KeeperMethodError(ErrorCode.unknownMethod, `Unknown method: ${method}`);
|
|
402
|
+
if (this.drainingFlag && this.startsWork.has(method))
|
|
403
|
+
throw new KeeperMethodError(ErrorCode.draining, `Keeper is draining; ${method} starts new work`);
|
|
404
|
+
this.inFlightCount += 1;
|
|
405
|
+
try {
|
|
406
|
+
return await handler(params, context);
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
this.inFlightCount -= 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* `keeper.stop` was accepted (before its reply is written): from now on `info.stopping` is set and new work is refused (both modes:
|
|
414
|
+
* a force stop's onStop may still be tearing things down, and no `startsWork` call may slip in meanwhile).
|
|
415
|
+
* A force request upgrades a pending graceful one.
|
|
416
|
+
*/
|
|
417
|
+
beginStop(mode) {
|
|
418
|
+
if (!this.stopState)
|
|
419
|
+
this.stopState = { mode };
|
|
420
|
+
else if (mode === 'force')
|
|
421
|
+
this.stopState.mode = 'force';
|
|
422
|
+
this.drain();
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The stop reply has been written. Graceful: wait until no request is in flight and every answer has left its socket, bounded by
|
|
426
|
+
* `stopGraceMs`, then stop (also when the grace ran out). Force: stop now, even over a pending graceful stop. The stop happens once.
|
|
427
|
+
*/
|
|
428
|
+
stopped(mode) {
|
|
429
|
+
this.beginStop(mode);
|
|
430
|
+
const state = this.stopState;
|
|
431
|
+
if (state.finish) {
|
|
432
|
+
if (state.mode === 'force')
|
|
433
|
+
state.finish();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
let done = false;
|
|
437
|
+
let timer;
|
|
438
|
+
let poll;
|
|
439
|
+
const finish = () => {
|
|
440
|
+
if (done)
|
|
441
|
+
return;
|
|
442
|
+
done = true;
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
clearTimeout(poll);
|
|
445
|
+
try {
|
|
446
|
+
if (this.options.onStop)
|
|
447
|
+
this.options.onStop(state.mode);
|
|
448
|
+
else {
|
|
449
|
+
for (const connection of [...this.connections])
|
|
450
|
+
connection.destroy();
|
|
451
|
+
if (this.net.listening)
|
|
452
|
+
this.net.close();
|
|
453
|
+
this.options.exit?.(0);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
this.report(error);
|
|
458
|
+
this.stopState = undefined;
|
|
459
|
+
} // the keeper stays up and a later stop can try again
|
|
460
|
+
};
|
|
461
|
+
state.finish = finish;
|
|
462
|
+
if (state.mode === 'force') {
|
|
463
|
+
finish();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
timer = setTimeout(finish, this.options.stopGraceMs ?? 3000);
|
|
467
|
+
timer.unref();
|
|
468
|
+
const check = () => {
|
|
469
|
+
if (done)
|
|
470
|
+
return;
|
|
471
|
+
if (this.inFlightCount === 0 && [...this.connections].every((c) => c.flushed)) {
|
|
472
|
+
finish();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
poll = setTimeout(check, 10);
|
|
476
|
+
};
|
|
477
|
+
check();
|
|
478
|
+
}
|
|
479
|
+
async generic(method, params, context, replyStop) {
|
|
480
|
+
const o = this.options;
|
|
481
|
+
switch (method) {
|
|
482
|
+
case 'keeper.ping': return { pid: process.pid, identity: o.identity, gen: o.gen, kind: o.kind, buildVersion: o.buildVersion, protocolVersion: o.protocolVersion };
|
|
483
|
+
case 'keeper.info': return { ...this.hello(), identity: o.identity, draining: this.drainingFlag, stopping: this.stopState?.mode ?? null, inFlight: this.inFlightCount, connections: this.connections.size, uptimeMs: Date.now() - this.startedAt };
|
|
484
|
+
case 'keeper.activity': return sampleActivity(this.samplers, { timeoutMs: o.activityTimeoutMs, onError: (error) => this.report(error) });
|
|
485
|
+
case 'keeper.drain':
|
|
486
|
+
this.drain();
|
|
487
|
+
return { draining: true };
|
|
488
|
+
case 'keeper.stop': {
|
|
489
|
+
if (params.mode !== 'graceful' && params.mode !== 'force')
|
|
490
|
+
throw new KeeperMethodError(ErrorCode.badRequest, 'mode must be graceful or force');
|
|
491
|
+
this.beginStop(params.mode);
|
|
492
|
+
replyStop();
|
|
493
|
+
return NOT_REPLIED;
|
|
494
|
+
}
|
|
495
|
+
case 'keeper.subscribe': {
|
|
496
|
+
const { stream, creditBytes } = params;
|
|
497
|
+
const from = params.from ?? 0;
|
|
498
|
+
const credit = params.credit ?? 0;
|
|
499
|
+
if (typeof stream !== 'string' || !stream || !Number.isSafeInteger(from) || from < 0 || !Number.isSafeInteger(credit) || credit < 0)
|
|
500
|
+
throw new KeeperMethodError(ErrorCode.badRequest, 'subscribe needs stream, from >= 0 and credit >= 0');
|
|
501
|
+
if (creditBytes !== undefined && (!Number.isSafeInteger(creditBytes) || creditBytes <= 0))
|
|
502
|
+
throw new KeeperMethodError(ErrorCode.badRequest, 'creditBytes must be a positive integer');
|
|
503
|
+
const source = await this.streamResolver?.(stream);
|
|
504
|
+
if (!source)
|
|
505
|
+
throw new KeeperMethodError(ErrorCode.unknownStream, `Unknown stream: ${stream}`);
|
|
506
|
+
context.subscribe(stream, source, from, Math.min(credit, MAX_CREDIT), creditBytes);
|
|
507
|
+
return { stream, from };
|
|
508
|
+
}
|
|
509
|
+
case 'keeper.unsubscribe': {
|
|
510
|
+
if (typeof params.stream !== 'string')
|
|
511
|
+
throw new KeeperMethodError(ErrorCode.badRequest, 'unsubscribe needs stream');
|
|
512
|
+
context.unsubscribe(params.stream);
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
default: throw new KeeperMethodError(ErrorCode.unknownMethod, `Unknown method: ${method}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|