@source-repo/rpc-cli 4.6.0 → 5.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/README.md +1 -0
- package/dist/console.d.ts +15 -2
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +49 -15
- package/dist/console.js.map +1 -1
- package/dist/console.types.json +459 -0
- package/dist/credentials.d.ts +77 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +118 -0
- package/dist/credentials.js.map +1 -0
- package/dist/enrolment.d.ts +91 -0
- package/dist/enrolment.d.ts.map +1 -0
- package/dist/enrolment.js +94 -0
- package/dist/enrolment.js.map +1 -0
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +69 -3
- package/dist/extract.js.map +1 -1
- package/dist/grants.d.ts +25 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +62 -0
- package/dist/grants.js.map +1 -0
- package/dist/index.js +219 -71
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +323 -1
- package/dist/mcp.js.map +1 -1
- package/dist/network.d.ts +47 -1
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +17 -1
- package/dist/network.js.map +1 -1
- package/dist/node.d.ts +10 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +12 -0
- package/dist/node.js.map +1 -1
- package/dist/pairing.d.ts +52 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +57 -0
- package/dist/pairing.js.map +1 -0
- package/dist/tasks.d.ts +189 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +485 -0
- package/dist/tasks.js.map +1 -0
- package/dist/web/app.css +1 -1
- package/dist/web/app.js +12 -12
- package/dist/web/app.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync, readFileSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { basename, resolve } from 'node:path';
|
|
5
|
-
import { createDerivedAuthenticator,
|
|
5
|
+
import { createDerivedAuthenticator, createTokenAuthenticator, firstAuthenticator, defaultSecureWebPort, defaultSecureWebSocketPort, defaultWebPort, defaultWebSocketPort, namespaceProblems, readableNameFor } from '@source-repo/rpc';
|
|
6
6
|
import { extractSchema } from './extract.js';
|
|
7
7
|
import { startConsole } from './console.js';
|
|
8
8
|
import { startBroker } from './broker.js';
|
|
@@ -15,6 +15,9 @@ import { startFake } from './fake.js';
|
|
|
15
15
|
import { replaySession, startRecording } from './record.js';
|
|
16
16
|
import { checkPeer, diffPeers } from './conform.js';
|
|
17
17
|
import { bench, benchArguments } from './bench.js';
|
|
18
|
+
import { loadAuthFile, loadSigningKeys, loadTls, scriptCredentials } from './credentials.js';
|
|
19
|
+
import { grantLines, loadAiGrants } from './grants.js';
|
|
20
|
+
import { defaultTaskFile, startTaskFile, taskFileSkeleton, taskFileSkeletonNotes } from './tasks.js';
|
|
18
21
|
/**
|
|
19
22
|
* msgrpc extract - read the contract out of TypeScript source and write it to a file
|
|
20
23
|
* msgrpc check - compare the source against a written contract and report breaking changes
|
|
@@ -32,6 +35,8 @@ const usage = `source-rpc <command> [options] --version prints the CLI
|
|
|
32
35
|
console browse a live network in a browser: peers, what they expose, calls and events
|
|
33
36
|
broker run a WebSocket bus: relays between the peers that connect to it, until Ctrl-C
|
|
34
37
|
node make this machine scriptable from another one, and nothing else, until Ctrl-C
|
|
38
|
+
run start console, node and serve roles together from one JSON task file, until Ctrl-C
|
|
39
|
+
with no file named it runs ./source-rpc.tasks.json; --init writes one to start from
|
|
35
40
|
mcp serve the network to an MCP client over stdio: list peers, describe them, call them
|
|
36
41
|
stdio carries the protocol, so it is not for interactive use; --port opens a second
|
|
37
42
|
door over streamable HTTP, so two clients can share one node
|
|
@@ -70,6 +75,8 @@ const usage = `source-rpc <command> [options] --version prints the CLI
|
|
|
70
75
|
|
|
71
76
|
peers / describe / call / watch
|
|
72
77
|
--broker <url> an MQTT network, e.g. mqtt://localhost:1883
|
|
78
|
+
SOURCE_RPC_MQTT_USERNAME and SOURCE_RPC_MQTT_PASSWORD are used
|
|
79
|
+
as broker credentials when set
|
|
73
80
|
--hub <url> a socket.io network, e.g. http://hub:7843
|
|
74
81
|
one of --broker and --hub is required; both watches both
|
|
75
82
|
--prefix <topic> topic namespace, default the transport's own
|
|
@@ -86,6 +93,8 @@ const usage = `source-rpc <command> [options] --version prints the CLI
|
|
|
86
93
|
|
|
87
94
|
console
|
|
88
95
|
--broker <url> an MQTT network, e.g. mqtt://localhost:1883
|
|
96
|
+
SOURCE_RPC_MQTT_USERNAME and SOURCE_RPC_MQTT_PASSWORD are used
|
|
97
|
+
as broker credentials when set
|
|
89
98
|
--hub <url> a socket.io network, e.g. http://hub:7843
|
|
90
99
|
one of --broker and --hub is required; both watches both
|
|
91
100
|
--prefix <topic> topic namespace, default the transport's own
|
|
@@ -102,6 +111,8 @@ const usage = `source-rpc <command> [options] --version prints the CLI
|
|
|
102
111
|
|
|
103
112
|
mcp
|
|
104
113
|
--broker <url> an MQTT network
|
|
114
|
+
SOURCE_RPC_MQTT_USERNAME and SOURCE_RPC_MQTT_PASSWORD are used
|
|
115
|
+
as broker credentials when set
|
|
105
116
|
--hub <url> a socket.io network
|
|
106
117
|
one of --broker and --hub is required; both watches both
|
|
107
118
|
--prefix <topic> topic namespace, default the transport's own
|
|
@@ -122,6 +133,7 @@ const usage = `source-rpc <command> [options] --version prints the CLI
|
|
|
122
133
|
nothing can script it. The peer must authenticate as that name,
|
|
123
134
|
so the key it presents reaches it out of band - deliberately not
|
|
124
135
|
something this bus can hand over
|
|
136
|
+
--grants <file> what an AI principal may do here; see node --grants above
|
|
125
137
|
--port <n> serve streamable HTTP here as a second door beside stdio, so a
|
|
126
138
|
second client shares this node's scripts, fakes and watches
|
|
127
139
|
rather than forking them. No default: absent means stdio only
|
|
@@ -162,6 +174,19 @@ const usage = `source-rpc <command> [options] --version prints the CLI
|
|
|
162
174
|
on a broker, --sign at both ends is what makes the grant work:
|
|
163
175
|
without it nothing can prove who a caller is and every call is
|
|
164
176
|
refused
|
|
177
|
+
--grants <file> what an AI principal may do here, as a grants document. Without one
|
|
178
|
+
a badged principal may observe and nothing else, which is the
|
|
179
|
+
default everywhere. SIGHUP re-reads it, so a grant can be closed
|
|
180
|
+
without stopping the node
|
|
181
|
+
|
|
182
|
+
run [<file.json>] defaults to ./source-rpc.tasks.json, in this directory only
|
|
183
|
+
shared network settings and console, node or serve tasks; relative paths are resolved from the
|
|
184
|
+
task file. Each task's credentials are its own: a key file under 'sign', an auth file under
|
|
185
|
+
'auth', or the same secrets written inline in either. Full format:
|
|
186
|
+
https://source-repo.github.io/rpc/tools/cli#task-files
|
|
187
|
+
--init write a task file to start from, with three roles and fresh signing
|
|
188
|
+
secrets, and refuse to write over one that exists. --broker, --hub
|
|
189
|
+
and --scriptable-by fill in what they name
|
|
165
190
|
|
|
166
191
|
strip <file…>
|
|
167
192
|
--out <dir> where each decorator-free twin lands, under the same file name.
|
|
@@ -252,30 +277,26 @@ const withHistory = (next, previous) => {
|
|
|
252
277
|
}
|
|
253
278
|
return next;
|
|
254
279
|
};
|
|
280
|
+
/**
|
|
281
|
+
* HMAC keys for the console, read from a file rather than a flag: a secret on the command line is
|
|
282
|
+
* visible to anyone who can run ps.
|
|
283
|
+
*
|
|
284
|
+
* { "name": "console-1", "secret": "…", "peers": { "plantServer": "…" } }
|
|
285
|
+
*
|
|
286
|
+
* `peers` is optional. Supplying it makes the console check signatures on what it receives too,
|
|
287
|
+
* which means an unsigned peer's frames are then dropped.
|
|
288
|
+
*/
|
|
255
289
|
const readSigningKeys = (path, command) => {
|
|
256
|
-
let keys;
|
|
257
290
|
try {
|
|
258
|
-
|
|
291
|
+
const signing = loadSigningKeys(path);
|
|
292
|
+
if (signing.readableByOthers)
|
|
293
|
+
process.stderr.write(`source-rpc ${command}: ${path} is readable by other users\n`);
|
|
294
|
+
return signing;
|
|
259
295
|
}
|
|
260
296
|
catch (e) {
|
|
261
|
-
process.stderr.write(`source-rpc ${command}:
|
|
262
|
-
process.exit(1);
|
|
263
|
-
}
|
|
264
|
-
if (typeof keys.secret !== 'string' || !keys.secret) {
|
|
265
|
-
process.stderr.write(`source-rpc ${command}: ${path} has no "secret"\n`);
|
|
297
|
+
process.stderr.write(`source-rpc ${command}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
266
298
|
process.exit(1);
|
|
267
299
|
}
|
|
268
|
-
try {
|
|
269
|
-
// Worth saying out loud: this file is the console's identity on the network.
|
|
270
|
-
if (statSync(path).mode & 0o077)
|
|
271
|
-
process.stderr.write(`source-rpc ${command}: ${path} is readable by other users\n`);
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// Not worth failing over if the mode cannot be read.
|
|
275
|
-
}
|
|
276
|
-
const sign = createHmacSigner(keys.secret);
|
|
277
|
-
const verify = keys.peers ? createHmacVerifier((peer) => keys.peers?.[peer]) : undefined;
|
|
278
|
-
return { keys, sign, verify };
|
|
279
300
|
};
|
|
280
301
|
/**
|
|
281
302
|
* Bearer tokens, read from a file or the environment. Never a flag, for the same reason the signing
|
|
@@ -299,37 +320,68 @@ const readSigningKeys = (path, command) => {
|
|
|
299
320
|
* is now impossible either way. Lifetimes are short and renewal does not exist, so stopping the
|
|
300
321
|
* node means its scripts' credentials expire on their own; immediate revocation is the grants
|
|
301
322
|
* work, not this.
|
|
323
|
+
*
|
|
324
|
+
* The minting itself lives in credentials.ts, because `run` mints the same credentials from a task
|
|
325
|
+
* file and two implementations of a credential are two things to get subtly different.
|
|
302
326
|
*/
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
name,
|
|
313
|
-
token: await mintDerivedCredential({
|
|
314
|
-
credentialId: `${script}-${issuedAt.toString(36)}`,
|
|
315
|
-
subject: name,
|
|
316
|
-
// The provenance the AI boundary reads. A script is a program this node started,
|
|
317
|
-
// whoever wrote it - the honest claim, and never a claim about what wrote it.
|
|
318
|
-
roles: ['ai-program'],
|
|
319
|
-
issuer,
|
|
320
|
-
generation: 2,
|
|
321
|
-
issuedAt,
|
|
322
|
-
expiresAt: issuedAt + SCRIPT_CREDENTIAL_MS
|
|
323
|
-
}, auth.derive)
|
|
324
|
-
};
|
|
325
|
-
};
|
|
327
|
+
const scriptCredentialsFor = (auth, issuer, command) => scriptCredentials(auth, issuer, (message) => process.stderr.write(`source-rpc ${command}: ${message}\n`));
|
|
328
|
+
const readAiGrants = (path, command) => {
|
|
329
|
+
try {
|
|
330
|
+
return loadAiGrants(path);
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
process.stderr.write(`source-rpc ${command}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
326
336
|
};
|
|
327
337
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
338
|
+
* Refusals reach the operator; permitted calls do not.
|
|
339
|
+
*
|
|
340
|
+
* Both are audit, and both belong in the fleet-side sink rather than here - but of the two, a
|
|
341
|
+
* refusal is the one somebody is standing at a terminal wondering about, and printing every allowed
|
|
342
|
+
* call would bury it. The sentence the library supplies is the whole line: it already says which
|
|
343
|
+
* grant was wanted and why the answer was no.
|
|
331
344
|
*/
|
|
332
|
-
const
|
|
345
|
+
const aiDecisionReporter = (command) => (record) => {
|
|
346
|
+
if (!record.allowed)
|
|
347
|
+
process.stderr.write(`source-rpc ${command}: refused ${record.source} calling ${record.method}: ${record.reason}\n`);
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Re-read the grants document on SIGHUP, so a grant can be closed without restarting the node.
|
|
351
|
+
*
|
|
352
|
+
* A signal rather than a file watcher, deliberately. A watcher fires on a half-written file and
|
|
353
|
+
* has to be taught what an atomic replace looks like on three platforms; a signal is an operator
|
|
354
|
+
* saying *now*, which is the same instinct as everything else here - a change in what is permitted
|
|
355
|
+
* is something somebody states rather than something that happens when a file is touched.
|
|
356
|
+
*
|
|
357
|
+
* A failed reload keeps the document that was already in force. The alternative - falling back to
|
|
358
|
+
* no document - reads as "closed, therefore safe" and is in fact the node quietly disagreeing with
|
|
359
|
+
* the policy its operator believes is loaded.
|
|
360
|
+
*/
|
|
361
|
+
const reloadGrantsOnHangUp = (path, initial, apply, command) => {
|
|
362
|
+
// Not every platform has SIGHUP, and Windows has none of this. Nothing else is affected.
|
|
363
|
+
if (process.platform === 'win32')
|
|
364
|
+
return;
|
|
365
|
+
let current = initial;
|
|
366
|
+
process.on('SIGHUP', () => {
|
|
367
|
+
let next;
|
|
368
|
+
try {
|
|
369
|
+
next = loadAiGrants(path);
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
process.stderr.write(`source-rpc ${command}: grants unchanged, ${e instanceof Error ? e.message : String(e)}\n`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// The revision exists so a rollback is visible. Applied anyway - an operator may be
|
|
376
|
+
// deliberately reverting - but never silently, since the other cause is a stale file.
|
|
377
|
+
if (current && next.revision < current.revision)
|
|
378
|
+
process.stderr.write(`source-rpc ${command}: grants revision went backwards, ${current.revision} to ${next.revision}\n`);
|
|
379
|
+
current = next;
|
|
380
|
+
apply(next);
|
|
381
|
+
for (const line of grantLines(next))
|
|
382
|
+
process.stderr.write(`source-rpc ${command}: ${line}\n`);
|
|
383
|
+
});
|
|
384
|
+
};
|
|
333
385
|
/**
|
|
334
386
|
* Certificate and key for a server this command opens, or undefined for plain HTTP.
|
|
335
387
|
*
|
|
@@ -349,10 +401,10 @@ const readTls = (argv, command) => {
|
|
|
349
401
|
process.exit(1);
|
|
350
402
|
}
|
|
351
403
|
try {
|
|
352
|
-
return
|
|
404
|
+
return loadTls(cert, key);
|
|
353
405
|
}
|
|
354
406
|
catch (e) {
|
|
355
|
-
process.stderr.write(`source-rpc ${command}:
|
|
407
|
+
process.stderr.write(`source-rpc ${command}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
356
408
|
process.exit(1);
|
|
357
409
|
}
|
|
358
410
|
};
|
|
@@ -383,29 +435,17 @@ const readAuth = (argv, command) => {
|
|
|
383
435
|
...(tokens ? { tokens } : {})
|
|
384
436
|
};
|
|
385
437
|
}
|
|
386
|
-
let auth;
|
|
387
|
-
try {
|
|
388
|
-
auth = JSON.parse(readFileSync(path, 'utf8'));
|
|
389
|
-
}
|
|
390
|
-
catch (e) {
|
|
391
|
-
process.stderr.write(`source-rpc ${command}: cannot read tokens from ${path}: ${e.message}\n`);
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
if (!auth.token && !auth.tokens) {
|
|
395
|
-
// An empty file is the failure that looks like success: the command starts, and the bus it
|
|
396
|
-
// meant to gate is open. Better to refuse than to run unauthenticated on request.
|
|
397
|
-
process.stderr.write(`source-rpc ${command}: ${path} has neither "token" nor "tokens"\n`);
|
|
398
|
-
process.exit(1);
|
|
399
|
-
}
|
|
400
438
|
try {
|
|
439
|
+
const loaded = loadAuthFile(path);
|
|
401
440
|
// Worth saying out loud: whoever can read this file can be these peers.
|
|
402
|
-
if (
|
|
441
|
+
if (loaded.readableByOthers)
|
|
403
442
|
process.stderr.write(`source-rpc ${command}: ${path} is readable by other users\n`);
|
|
443
|
+
return loaded.auth;
|
|
404
444
|
}
|
|
405
|
-
catch {
|
|
406
|
-
|
|
445
|
+
catch (e) {
|
|
446
|
+
process.stderr.write(`source-rpc ${command}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
447
|
+
process.exit(1);
|
|
407
448
|
}
|
|
408
|
-
return auth;
|
|
409
449
|
};
|
|
410
450
|
/**
|
|
411
451
|
* The flags every command that joins a network takes, read once.
|
|
@@ -915,17 +955,24 @@ const runMcp = async (argv) => {
|
|
|
915
955
|
process.exit(1);
|
|
916
956
|
}
|
|
917
957
|
}
|
|
918
|
-
const credentialFor = scriptsDir ?
|
|
958
|
+
const credentialFor = scriptsDir ? scriptCredentialsFor(readAuth(argv, 'mcp'), network.name, 'mcp') : undefined;
|
|
959
|
+
const grantsPath = argument(argv, '--grants', '');
|
|
960
|
+
const aiGrants = grantsPath ? readAiGrants(grantsPath, 'mcp') : undefined;
|
|
919
961
|
const running = await startMcp({ ...network, ...(contracts ? { contracts: resolve(contracts) } : {}), ...(argv.includes('--allow-exec') ? { allowExec: true } : {}),
|
|
920
962
|
...(scriptsDir ? { scripts: resolve(scriptsDir) } : {}),
|
|
921
963
|
...(credentialFor ? { credentialFor } : {}),
|
|
922
964
|
...(scriptableBy.length ? { scriptableBy } : {}),
|
|
965
|
+
...(aiGrants ? { aiGrants } : {}),
|
|
966
|
+
onAiDecision: aiDecisionReporter('mcp'),
|
|
923
967
|
...(doorPort ? { port: doorPort, host: doorHost, ...(doorToken ? { doorToken } : {}) } : {}) }).catch((e) => {
|
|
924
968
|
// The refusal a wide bind without a token earns arrives here, with its sentence intact.
|
|
925
969
|
process.stderr.write(`source-rpc ${e.message}\n`);
|
|
926
970
|
process.exit(1);
|
|
927
971
|
});
|
|
928
972
|
// Nothing is written to stdout here: it carries the protocol. See mcp.ts.
|
|
973
|
+
if (scriptsDir || aiGrants)
|
|
974
|
+
for (const line of grantLines(aiGrants))
|
|
975
|
+
process.stderr.write(`source-rpc mcp: ${line}\n`);
|
|
929
976
|
const stop = () => void running
|
|
930
977
|
.close()
|
|
931
978
|
.then(() => process.exit(0))
|
|
@@ -945,8 +992,17 @@ const runNode = async (argv) => {
|
|
|
945
992
|
process.stderr.write('source-rpc node: needs --scripts <dir> and at least one --scriptable-by <peer>, or it offers nothing to anybody\n');
|
|
946
993
|
process.exit(1);
|
|
947
994
|
}
|
|
948
|
-
const credentialFor =
|
|
949
|
-
const
|
|
995
|
+
const credentialFor = scriptCredentialsFor(readAuth(argv, 'node'), network.name, 'node');
|
|
996
|
+
const grantsPath = argument(argv, '--grants', '');
|
|
997
|
+
const aiGrants = grantsPath ? readAiGrants(grantsPath, 'node') : undefined;
|
|
998
|
+
const running = await startNode({
|
|
999
|
+
...network,
|
|
1000
|
+
scripts: resolve(scriptsDir),
|
|
1001
|
+
scriptableBy,
|
|
1002
|
+
...(credentialFor ? { credentialFor } : {}),
|
|
1003
|
+
...(aiGrants ? { aiGrants } : {}),
|
|
1004
|
+
onAiDecision: aiDecisionReporter('node')
|
|
1005
|
+
}).catch((e) => {
|
|
950
1006
|
process.stderr.write(`source-rpc node: cannot start: ${e.message}\n`);
|
|
951
1007
|
process.exit(1);
|
|
952
1008
|
});
|
|
@@ -963,6 +1019,12 @@ const runNode = async (argv) => {
|
|
|
963
1019
|
// node is unreachable for the thing it exists to do. Said now rather than discovered as a
|
|
964
1020
|
// Forbidden on the other machine.
|
|
965
1021
|
process.stderr.write('source-rpc node: on a broker without --sign nothing can prove who a caller is, so every scripting call will be refused. Give both ends keys.\n');
|
|
1022
|
+
// Said whether or not a document was given, because closed-by-default means "it is running" and
|
|
1023
|
+
// "it can do something" are separately true, and this node's scripts carry `ai-program`.
|
|
1024
|
+
for (const line of grantLines(aiGrants))
|
|
1025
|
+
process.stderr.write(`source-rpc node: ${line}\n`);
|
|
1026
|
+
if (grantsPath)
|
|
1027
|
+
reloadGrantsOnHangUp(grantsPath, aiGrants, running.setAiGrants, 'node');
|
|
966
1028
|
const stop = () => void running
|
|
967
1029
|
.close()
|
|
968
1030
|
.then(() => process.exit(0))
|
|
@@ -1002,6 +1064,79 @@ const runConsole = async (argv) => {
|
|
|
1002
1064
|
process.on('SIGINT', stop);
|
|
1003
1065
|
process.on('SIGTERM', stop);
|
|
1004
1066
|
};
|
|
1067
|
+
const taskStartedLine = (task) => {
|
|
1068
|
+
if (task.type === 'console')
|
|
1069
|
+
return `${task.id}: console ${task.name} on ${task.url}`;
|
|
1070
|
+
if (task.type === 'node')
|
|
1071
|
+
return `${task.id}: node ${task.name}`;
|
|
1072
|
+
return `${task.id}: serve ${task.name} answering ${task.namespaces?.join(', ')}`;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Writes a task file to start from, and refuses to write over one that is already there.
|
|
1076
|
+
*
|
|
1077
|
+
* Refusing matters more here than it usually does: the file it would replace holds signing secrets,
|
|
1078
|
+
* and overwriting it does not lose a configuration that can be typed again - it loses the identity
|
|
1079
|
+
* every other machine on the network was told to expect, and does it silently.
|
|
1080
|
+
*/
|
|
1081
|
+
const initTaskFile = (argv, file) => {
|
|
1082
|
+
const skeleton = taskFileSkeleton({
|
|
1083
|
+
...(argument(argv, '--broker', '') ? { broker: argument(argv, '--broker', '') } : {}),
|
|
1084
|
+
...(argument(argv, '--hub', '') ? { hub: argument(argv, '--hub', '') } : {}),
|
|
1085
|
+
...(argument(argv, '--scriptable-by', '') ? { controller: argument(argv, '--scriptable-by', '') } : {})
|
|
1086
|
+
});
|
|
1087
|
+
try {
|
|
1088
|
+
// wx rather than a check and a write: between the two there is a window, and the thing in it
|
|
1089
|
+
// is a key file.
|
|
1090
|
+
writeFileSync(file, `${JSON.stringify(skeleton, undefined, 4)}\n`, { flag: 'wx', mode: 0o600 });
|
|
1091
|
+
}
|
|
1092
|
+
catch (e) {
|
|
1093
|
+
const already = e.code === 'EEXIST';
|
|
1094
|
+
process.stderr.write(`source-rpc run: ${already ? `${file} already exists, and it may hold this host's signing secrets - name a new file or move that one aside` : `cannot write ${file}: ${e.message}`}\n`);
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
for (const note of taskFileSkeletonNotes(file, argument(argv, '--scriptable-by', 'controller')))
|
|
1098
|
+
process.stdout.write(`source-rpc run: ${note}\n`);
|
|
1099
|
+
};
|
|
1100
|
+
const runTasks = async (argv) => {
|
|
1101
|
+
const [, named, ...extra] = positionals(argv);
|
|
1102
|
+
if (extra.length) {
|
|
1103
|
+
process.stderr.write(`source-rpc run: give it one task file, or none to use ./${defaultTaskFile}\n`);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
const file = named ?? defaultTaskFile;
|
|
1107
|
+
if (argv.includes('--init'))
|
|
1108
|
+
return initTaskFile(argv, file);
|
|
1109
|
+
// Checked before startTaskFile so that the answer to "run" with nothing set up is the thing to
|
|
1110
|
+
// do next, rather than an ENOENT for a file the operator never mentioned.
|
|
1111
|
+
if (!named && !existsSync(file)) {
|
|
1112
|
+
process.stderr.write(`source-rpc run: no ${defaultTaskFile} here, and no task file named. Write one with 'source-rpc run --init', or name one.\n`);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
const running = await startTaskFile(file, {
|
|
1116
|
+
started: (task) => process.stdout.write(`source-rpc run: started ${taskStartedLine(task)}\n`),
|
|
1117
|
+
warning: (message) => process.stderr.write(`source-rpc run: ${message}\n`)
|
|
1118
|
+
});
|
|
1119
|
+
process.stdout.write(`source-rpc run: ${running.tasks.length} tasks running from ${running.file}\n`);
|
|
1120
|
+
let stopping = false;
|
|
1121
|
+
const stop = () => {
|
|
1122
|
+
if (stopping)
|
|
1123
|
+
return;
|
|
1124
|
+
stopping = true;
|
|
1125
|
+
void running
|
|
1126
|
+
.close()
|
|
1127
|
+
.then(() => process.exit(0))
|
|
1128
|
+
.catch((e) => {
|
|
1129
|
+
process.stderr.write(`source-rpc run: shutdown failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
});
|
|
1132
|
+
};
|
|
1133
|
+
process.on('SIGINT', stop);
|
|
1134
|
+
process.on('SIGTERM', stop);
|
|
1135
|
+
// The same hang-up that re-reads a node's grants, for every node this file started.
|
|
1136
|
+
if (process.platform !== 'win32')
|
|
1137
|
+
process.on('SIGHUP', () => running.reloadGrants());
|
|
1138
|
+
await new Promise(() => { });
|
|
1139
|
+
};
|
|
1005
1140
|
const main = () => {
|
|
1006
1141
|
// `source-rpc describe plantServer | head -4` closes stdout while there is still output to
|
|
1007
1142
|
// write, and Node turns that into an unhandled 'error' event: a stack trace where a command
|
|
@@ -1033,7 +1168,10 @@ const main = () => {
|
|
|
1033
1168
|
process.exit(1);
|
|
1034
1169
|
};
|
|
1035
1170
|
if (command === 'strip') {
|
|
1036
|
-
|
|
1171
|
+
// Past the command word, as every other verb here does. Without the slice the first file
|
|
1172
|
+
// to be stripped is one called `strip`, which fails as a missing file rather than saying
|
|
1173
|
+
// no file was named - and it means the command has never worked from the command line.
|
|
1174
|
+
const files = positionals(argv).slice(1);
|
|
1037
1175
|
const out = argument(argv, '--out', '');
|
|
1038
1176
|
if (!files.length || !out) {
|
|
1039
1177
|
process.stderr.write('source-rpc strip: give it one or more .ts files and --out <dir>, e.g. strip scripts/hello.ts --out scripts/stripped\n');
|
|
@@ -1069,6 +1207,10 @@ const main = () => {
|
|
|
1069
1207
|
void runNode(argv).catch(fail);
|
|
1070
1208
|
return;
|
|
1071
1209
|
}
|
|
1210
|
+
if (command === 'run') {
|
|
1211
|
+
void runTasks(argv).catch(fail);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1072
1214
|
if (command === 'console') {
|
|
1073
1215
|
void runConsole(argv).catch(fail);
|
|
1074
1216
|
return;
|
|
@@ -1119,6 +1261,12 @@ const main = () => {
|
|
|
1119
1261
|
}
|
|
1120
1262
|
if (command !== 'extract' && command !== 'check') {
|
|
1121
1263
|
process.stderr.write(usage);
|
|
1264
|
+
// Pointed at rather than started. Typing the bare command is what someone does to see what
|
|
1265
|
+
// this is, and answering that by joining a bus under whatever identities happen to be in
|
|
1266
|
+
// this directory - and opening a console, and possibly making the machine scriptable -
|
|
1267
|
+
// would be a great deal to have happen while reading the help.
|
|
1268
|
+
if (!command && existsSync(defaultTaskFile))
|
|
1269
|
+
process.stderr.write(`\nthere is a ${defaultTaskFile} here: 'source-rpc run' starts it\n`);
|
|
1122
1270
|
process.exit(command ? 1 : 0);
|
|
1123
1271
|
}
|
|
1124
1272
|
const { schema, diagnostics } = extractSchema(project);
|