@sema-agent/connectors 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/dist/cli-kit.d.ts +34 -0
- package/dist/cli-kit.js +224 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/jwks.d.ts +1 -0
- package/dist/jwks.js +4 -1
- package/dist/launcher.js +73 -58
- package/dist/lock-file.d.ts +1 -0
- package/dist/lock-file.js +34 -0
- package/dist/mount.d.ts +5 -1
- package/dist/mount.js +23 -36
- package/dist/pinned-upstream.d.ts +21 -0
- package/dist/pinned-upstream.js +347 -0
- package/dist/release-archive.d.ts +8 -0
- package/dist/release-archive.js +96 -0
- package/dist/resolve-connector.d.ts +8 -0
- package/dist/resolve-connector.js +28 -0
- package/dist/scope.js +2 -0
- package/dist/secrets.d.ts +16 -1
- package/dist/secrets.js +139 -38
- package/dist/seed.d.ts +35 -0
- package/dist/seed.js +134 -0
- package/dist/status.d.ts +1 -1
- package/dist/status.js +3 -1
- package/dist/write-all.d.ts +2 -0
- package/dist/write-all.js +19 -0
- package/package.json +1 -1
package/dist/launcher.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
3
|
import { isIPv4, isIPv6 } from 'node:net';
|
|
6
|
-
import {
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
7
6
|
import { absoluteDir, given } from './config-dir.js';
|
|
8
7
|
import { CONNECTOR_NAME } from './connector-name.js';
|
|
9
8
|
import { serveHttp } from './http.js';
|
|
10
9
|
import { keySetSource, obtainKeySet } from './jwks.js';
|
|
11
10
|
import { hostedResolver, standaloneResolver } from './principal.js';
|
|
12
11
|
import { Scrubber, secretMatcher } from './redact.js';
|
|
13
|
-
import {
|
|
12
|
+
import { resolveConnector } from './resolve-connector.js';
|
|
13
|
+
import { NO_HANDLES, copyForLaunch, handleDirectory } from './secrets.js';
|
|
14
|
+
import { seed } from './seed.js';
|
|
14
15
|
import { INSPECT_LIMIT_BYTES, Session } from './session.js';
|
|
15
16
|
import { mintStatus } from './status.js';
|
|
16
|
-
|
|
17
|
+
import { writeFailure } from './write-all.js';
|
|
18
|
+
const EXIT = { ok: 0, notSeeded: 1, guard: 2, notInstalled: 4, connectorFailed: 5 };
|
|
17
19
|
const NOT_INSTALLED = 'connector.not_installed';
|
|
18
20
|
const INTERNAL = 'connector.internal';
|
|
19
21
|
const STOP_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
@@ -21,20 +23,15 @@ const TEARDOWN_STEP_MS = 1000;
|
|
|
21
23
|
const STALL_STEPS = 10;
|
|
22
24
|
const STALL_MS = STALL_STEPS * TEARDOWN_STEP_MS;
|
|
23
25
|
const PIECE_BYTES = 1024;
|
|
24
|
-
const USAGE = 'usage: sema-connector run <name> --mode standalone|hosted --transport stdio|http [--listen <ip>:<port>] [--config-dir <absolute path>]';
|
|
26
|
+
const USAGE = 'usage: sema-connector run <name> --mode standalone|hosted --transport stdio|http [--listen <ip>:<port>] [--config-dir <absolute path>]\n sema-connector seed --config-dir <absolute path>';
|
|
25
27
|
const FLAGS = ['--mode', '--transport', '--listen', '--config-dir'];
|
|
28
|
+
const SEED_FLAGS = ['--config-dir'];
|
|
26
29
|
const CHILD_ENV = ['HOME', 'LOGNAME', 'USER', 'PATH', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'TZ'];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return 'expected the command "run"';
|
|
30
|
-
const name = argv[1];
|
|
31
|
-
if (name === undefined || name.startsWith('-'))
|
|
32
|
-
return 'missing the connector <name> after "run"';
|
|
33
|
-
if (!CONNECTOR_NAME.test(name))
|
|
34
|
-
return '<name> must be lowercase letters and digits in hyphen-separated words (it names the package @sema-agent/connector-<name>)';
|
|
30
|
+
const CONNECTOR_DIR = 'CONNECTOR_DIR';
|
|
31
|
+
function flagValues(argv, from, allowed) {
|
|
35
32
|
const values = new Map();
|
|
36
|
-
for (let i =
|
|
37
|
-
const flag =
|
|
33
|
+
for (let i = from; i < argv.length; i += 2) {
|
|
34
|
+
const flag = allowed.find((f) => f === argv[i]);
|
|
38
35
|
if (flag === undefined)
|
|
39
36
|
return `unknown argument at position ${i + 1}`;
|
|
40
37
|
if (values.has(flag))
|
|
@@ -44,6 +41,31 @@ function guard(argv, env) {
|
|
|
44
41
|
return `${flag} needs a value`;
|
|
45
42
|
values.set(flag, value);
|
|
46
43
|
}
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
function guard(argv, env) {
|
|
47
|
+
if (argv[0] === 'seed') {
|
|
48
|
+
const values = flagValues(argv, 1, SEED_FLAGS);
|
|
49
|
+
if (typeof values === 'string')
|
|
50
|
+
return values;
|
|
51
|
+
const configDirArgument = values.get('--config-dir');
|
|
52
|
+
if (configDirArgument === undefined)
|
|
53
|
+
return 'seed requires --config-dir <absolute path>, the configuration directory it seeds (there is no default)';
|
|
54
|
+
const configDir = absoluteDir(configDirArgument);
|
|
55
|
+
if (configDir === null)
|
|
56
|
+
return '--config-dir must be an absolute path';
|
|
57
|
+
return { command: 'seed', configDir };
|
|
58
|
+
}
|
|
59
|
+
if (argv[0] !== 'run')
|
|
60
|
+
return 'expected the command "run" or "seed"';
|
|
61
|
+
const name = argv[1];
|
|
62
|
+
if (name === undefined || name.startsWith('-'))
|
|
63
|
+
return 'missing the connector <name> after "run"';
|
|
64
|
+
if (!CONNECTOR_NAME.test(name))
|
|
65
|
+
return '<name> must be lowercase letters and digits in hyphen-separated words (it names the package @sema-agent/connector-<name>)';
|
|
66
|
+
const values = flagValues(argv, 2, FLAGS);
|
|
67
|
+
if (typeof values === 'string')
|
|
68
|
+
return values;
|
|
47
69
|
const mode = values.get('--mode');
|
|
48
70
|
const transport = values.get('--transport');
|
|
49
71
|
const listen = values.get('--listen');
|
|
@@ -66,7 +88,7 @@ function guard(argv, env) {
|
|
|
66
88
|
const configDir = absoluteDir(configDirArgument);
|
|
67
89
|
if (configDir === null)
|
|
68
90
|
return '--config-dir must be an absolute path';
|
|
69
|
-
return { name, mode, configDir };
|
|
91
|
+
return { command: 'run', name, mode, configDir };
|
|
70
92
|
}
|
|
71
93
|
if (transport !== 'http')
|
|
72
94
|
return 'hosted mode requires --transport http';
|
|
@@ -86,7 +108,7 @@ function guard(argv, env) {
|
|
|
86
108
|
const keySet = keySetSource(env, issuer);
|
|
87
109
|
if (typeof keySet === 'string')
|
|
88
110
|
return keySet;
|
|
89
|
-
return { name, mode, listen: address, issuer, audience, keySet };
|
|
111
|
+
return { command: 'run', name, mode, listen: address, issuer, audience, keySet };
|
|
90
112
|
}
|
|
91
113
|
function listenAddress(value) {
|
|
92
114
|
const m = /^(?:\[([^\]]+)\]|([^:[\]]+)):([1-9][0-9]{0,4})$/.exec(value);
|
|
@@ -97,31 +119,6 @@ function listenAddress(value) {
|
|
|
97
119
|
const port = Number(m[3]);
|
|
98
120
|
return hostOk && port <= 65535 ? { host, port } : null;
|
|
99
121
|
}
|
|
100
|
-
function resolveConnector(name) {
|
|
101
|
-
const packageName = `@sema-agent/connector-${name}`;
|
|
102
|
-
let manifestPath;
|
|
103
|
-
try {
|
|
104
|
-
manifestPath = createRequire(new URL('../package.json', import.meta.url)).resolve(`${packageName}/package.json`);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return { packageName, reason: 'is not installed where the launcher can resolve it' };
|
|
108
|
-
}
|
|
109
|
-
let bin;
|
|
110
|
-
try {
|
|
111
|
-
bin = JSON.parse(readFileSync(manifestPath, 'utf8')).bin;
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
return { packageName, reason: 'has a package.json that cannot be read' };
|
|
115
|
-
}
|
|
116
|
-
let rel;
|
|
117
|
-
if (typeof bin === 'string')
|
|
118
|
-
rel = bin;
|
|
119
|
-
else if (bin !== null && typeof bin === 'object' && !Array.isArray(bin) && Object.keys(bin).length === 1)
|
|
120
|
-
rel = Object.values(bin)[0];
|
|
121
|
-
if (typeof rel !== 'string' || rel === '')
|
|
122
|
-
return { packageName, reason: 'does not declare exactly one bin' };
|
|
123
|
-
return { packageName, binPath: join(dirname(manifestPath), rel) };
|
|
124
|
-
}
|
|
125
122
|
class Out {
|
|
126
123
|
stream;
|
|
127
124
|
held = [];
|
|
@@ -369,17 +366,19 @@ function pump(src, dst, framing, also = [], passing, giveUp) {
|
|
|
369
366
|
src.on('close', finish);
|
|
370
367
|
});
|
|
371
368
|
}
|
|
372
|
-
function childEnvironment(env, secrets) {
|
|
369
|
+
function childEnvironment(env, connectorDir, secrets) {
|
|
373
370
|
const out = {};
|
|
374
371
|
for (const name of CHILD_ENV) {
|
|
375
372
|
const value = env[name];
|
|
376
373
|
if (value !== undefined)
|
|
377
374
|
out[name] = value;
|
|
378
375
|
}
|
|
376
|
+
out[CONNECTOR_DIR] = connectorDir;
|
|
379
377
|
for (const [name, path] of secrets.files)
|
|
380
378
|
out[`${name}_FILE`] = path;
|
|
381
379
|
return out;
|
|
382
380
|
}
|
|
381
|
+
const hostedConnectorDir = (name) => join(tmpdir(), `sema-connector-${randomUUID()}`, 'connectors', name);
|
|
383
382
|
const describe = (o) => `connector process ${o.stalled ? `stopped reading its input (stalled input: none of it taken for ${STALL_MS / 1000} s) and ` : ''}${o.error ? `could not be started (${o.error.message})` : o.signal !== null ? `was terminated by signal ${o.signal}` : `exited with code ${String(o.code)}`}`;
|
|
384
383
|
const abnormal = (o) => o.code !== 0 || o.stalled === true;
|
|
385
384
|
function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstreamOut, log, stderrAfterExitMs) {
|
|
@@ -491,15 +490,14 @@ async function runStdio(packageName, binPath, childEnv, session, scrubber, stdou
|
|
|
491
490
|
return EXIT.connectorFailed;
|
|
492
491
|
}
|
|
493
492
|
async function runHosted(plan, keys, packageName, binPath, env, log) {
|
|
494
|
-
const childEnv = childEnvironment(env, NO_SECRETS);
|
|
495
493
|
const start = (deliver) => {
|
|
496
494
|
const session = new Session({
|
|
497
495
|
connector: plan.name,
|
|
498
|
-
status: (tools) => mintStatus({ handles:
|
|
496
|
+
status: (tools) => mintStatus({ handles: NO_HANDLES.handles, nowMs: Date.now(), tools }),
|
|
499
497
|
ownIdPrefix: `sema-connector/${randomUUID()}`,
|
|
500
|
-
withheld: secretMatcher(
|
|
498
|
+
withheld: secretMatcher(NO_HANDLES.values),
|
|
501
499
|
});
|
|
502
|
-
const run = startConnector(binPath,
|
|
500
|
+
const run = startConnector(binPath, childEnvironment(env, hostedConnectorDir(plan.name), NO_HANDLES), session, new Scrubber(NO_HANDLES.values), (data) => deliver(String(data)), null, log, TEARDOWN_STEP_MS);
|
|
503
501
|
return {
|
|
504
502
|
send: (raw, principal) => run.fromUpstream(raw, principal),
|
|
505
503
|
busy: () => run.toChild.behind(),
|
|
@@ -524,7 +522,24 @@ async function runHosted(plan, keys, packageName, binPath, env, log) {
|
|
|
524
522
|
setTimeout(abandon, TEARDOWN_STEP_MS);
|
|
525
523
|
return EXIT.ok;
|
|
526
524
|
}
|
|
527
|
-
const
|
|
525
|
+
const readingLine = (r) => `seed: ${r.name} ${r.outcome}${r.outcome === 'not_installed' ? ` (${r.message})` : r.outcome === 'refused' ? ` (${r.reason}: ${r.message})` : ''}\n`;
|
|
526
|
+
function runSeed(configDir, stdout, log) {
|
|
527
|
+
let result;
|
|
528
|
+
try {
|
|
529
|
+
result = seed({ configDir });
|
|
530
|
+
}
|
|
531
|
+
catch (e) {
|
|
532
|
+
log.line(`sema-connector: seed: failed (${writeFailure(e)}); the marker was not written\n`);
|
|
533
|
+
return EXIT.notSeeded;
|
|
534
|
+
}
|
|
535
|
+
for (const r of result.readings)
|
|
536
|
+
stdout.write(readingLine(r));
|
|
537
|
+
if (!result.ok) {
|
|
538
|
+
log.line(`sema-connector: seed: ${result.reason}: ${result.message}\n`);
|
|
539
|
+
return EXIT.notSeeded;
|
|
540
|
+
}
|
|
541
|
+
return result.readings.every((r) => r.outcome === 'mounted' || r.outcome === 'present' || r.outcome === 'not_installed') ? EXIT.ok : EXIT.notSeeded;
|
|
542
|
+
}
|
|
528
543
|
export async function main(argv, env) {
|
|
529
544
|
const stdout = new Out(process.stdout);
|
|
530
545
|
const log = new Log(new Out(process.stderr));
|
|
@@ -537,6 +552,8 @@ export async function main(argv, env) {
|
|
|
537
552
|
log.line(`sema-connector: ${plan}\n${USAGE}\n`);
|
|
538
553
|
return finish(EXIT.guard);
|
|
539
554
|
}
|
|
555
|
+
if (plan.command === 'seed')
|
|
556
|
+
return finish(runSeed(plan.configDir, stdout, log));
|
|
540
557
|
if (plan.mode === 'hosted') {
|
|
541
558
|
const keys = await obtainKeySet(plan.keySet);
|
|
542
559
|
if (!keys.ok) {
|
|
@@ -550,20 +567,18 @@ export async function main(argv, env) {
|
|
|
550
567
|
}
|
|
551
568
|
return finish(await runHosted(plan, keys.keys, resolved.packageName, resolved.binPath, env, log));
|
|
552
569
|
}
|
|
553
|
-
const listing = openFileSecrets({ configDir: plan.configDir, connector: plan.name }).list();
|
|
554
|
-
if (!listing.ok) {
|
|
555
|
-
log.line(`sema-connector: ${INTERNAL}: ${listing.problem}\n`);
|
|
556
|
-
return finish(EXIT.guard);
|
|
557
|
-
}
|
|
558
|
-
const { handles } = listing;
|
|
559
570
|
let secrets;
|
|
560
571
|
try {
|
|
561
|
-
secrets = copyForLaunch(plan.configDir, plan.name
|
|
572
|
+
secrets = copyForLaunch(plan.configDir, plan.name);
|
|
562
573
|
}
|
|
563
574
|
catch {
|
|
564
575
|
log.line(`sema-connector: ${INTERNAL}: connector ${plan.name}: its secrets cannot be read for this launch\n`);
|
|
565
576
|
return finish(EXIT.guard);
|
|
566
577
|
}
|
|
578
|
+
if (!secrets.ok) {
|
|
579
|
+
log.line(`sema-connector: ${INTERNAL}: ${secrets.problem}\n`);
|
|
580
|
+
return finish(EXIT.guard);
|
|
581
|
+
}
|
|
567
582
|
const resolved = resolveConnector(plan.name);
|
|
568
583
|
if ('reason' in resolved) {
|
|
569
584
|
secrets.remove();
|
|
@@ -572,12 +587,12 @@ export async function main(argv, env) {
|
|
|
572
587
|
}
|
|
573
588
|
const session = new Session({
|
|
574
589
|
connector: plan.name,
|
|
575
|
-
status: (tools) => mintStatus({ handles, nowMs: Date.now(), tools }),
|
|
590
|
+
status: (tools) => mintStatus({ handles: secrets.handles, nowMs: Date.now(), tools }),
|
|
576
591
|
ownIdPrefix: `sema-connector/${randomUUID()}`,
|
|
577
592
|
withheld: secretMatcher(secrets.values),
|
|
578
593
|
});
|
|
579
594
|
try {
|
|
580
|
-
return finish(await runStdio(resolved.packageName, resolved.binPath, childEnvironment(env, secrets), session, new Scrubber(secrets.values), stdout, log));
|
|
595
|
+
return finish(await runStdio(resolved.packageName, resolved.binPath, childEnvironment(env, handleDirectory(plan.configDir, plan.name), secrets), session, new Scrubber(secrets.values), stdout, log));
|
|
581
596
|
}
|
|
582
597
|
finally {
|
|
583
598
|
secrets.remove();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function takeLockFile(path: string): (() => void) | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { closeSync, linkSync, openSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { writeAll } from './write-all.js';
|
|
4
|
+
export function takeLockFile(path) {
|
|
5
|
+
const token = `${process.pid} ${randomBytes(12).toString('hex')}`;
|
|
6
|
+
const tmp = `${path}.${randomBytes(6).toString('hex')}`;
|
|
7
|
+
const fd = openSync(tmp, 'wx', 0o600);
|
|
8
|
+
try {
|
|
9
|
+
try {
|
|
10
|
+
writeAll(fd, token);
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
closeSync(fd);
|
|
14
|
+
}
|
|
15
|
+
linkSync(tmp, path);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
if (e.code === 'EEXIST')
|
|
19
|
+
return null;
|
|
20
|
+
throw e;
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
rmSync(tmp, { force: true });
|
|
24
|
+
}
|
|
25
|
+
return () => {
|
|
26
|
+
let held = '';
|
|
27
|
+
try {
|
|
28
|
+
held = readFileSync(path, 'utf8');
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
if (held === token)
|
|
32
|
+
rmSync(path, { force: true });
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/mount.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface MountOptions {
|
|
|
2
2
|
readonly name: string;
|
|
3
3
|
readonly configDir: string;
|
|
4
4
|
}
|
|
5
|
-
export type MountRefusal = 'config_dir' | 'invalid_name' | 'reserved_name' | 'unreadable' | 'unexpected_shape' | 'conflict' | 'locked' | 'changed_during_write';
|
|
5
|
+
export type MountRefusal = 'config_dir' | 'invalid_name' | 'reserved_name' | 'unreadable' | 'unexpected_shape' | 'conflict' | 'locked' | 'changed_during_write' | 'write_failed';
|
|
6
6
|
export type MountResult = {
|
|
7
7
|
readonly ok: true;
|
|
8
8
|
readonly changed: boolean;
|
|
@@ -12,6 +12,10 @@ export type MountResult = {
|
|
|
12
12
|
readonly reason: MountRefusal;
|
|
13
13
|
readonly message: string;
|
|
14
14
|
};
|
|
15
|
+
export declare function nameRefusal(name: unknown): {
|
|
16
|
+
readonly reason: 'invalid_name' | 'reserved_name';
|
|
17
|
+
readonly message: string;
|
|
18
|
+
} | null;
|
|
15
19
|
export declare function standaloneEntry(name: string, configDir: string): {
|
|
16
20
|
name: string;
|
|
17
21
|
transport: {
|
package/dist/mount.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { closeSync, fchmodSync,
|
|
1
|
+
import { closeSync, fchmodSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { absoluteDir } from './config-dir.js';
|
|
5
5
|
import { CONNECTOR_NAME } from './connector-name.js';
|
|
6
6
|
import { skipWhitespace, spansOf } from './json-text.js';
|
|
7
|
+
import { takeLockFile } from './lock-file.js';
|
|
8
|
+
import { writeAll, writeFailure } from './write-all.js';
|
|
7
9
|
const RESERVED_NAMES = ['sema-connector'];
|
|
10
|
+
export function nameRefusal(name) {
|
|
11
|
+
if (typeof name !== 'string' || !CONNECTOR_NAME.test(name))
|
|
12
|
+
return { reason: 'invalid_name', message: 'a connector name is lowercase letters and digits in hyphen-separated words' };
|
|
13
|
+
if (RESERVED_NAMES.includes(name))
|
|
14
|
+
return { reason: 'reserved_name', message: `${name} is a reserved name` };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
8
17
|
export function standaloneEntry(name, configDir) {
|
|
9
18
|
return { name, transport: { kind: 'stdio', command: 'sema-connector', args: ['run', name, '--mode', 'standalone', '--transport', 'stdio', '--config-dir', configDir] } };
|
|
10
19
|
}
|
|
@@ -69,32 +78,6 @@ function plan(before, entry) {
|
|
|
69
78
|
return { text };
|
|
70
79
|
}
|
|
71
80
|
const LOCK_FILE = '.mcp.json.lock';
|
|
72
|
-
function lock(dir) {
|
|
73
|
-
const path = join(dir, LOCK_FILE);
|
|
74
|
-
const token = `${process.pid} ${randomBytes(12).toString('hex')}`;
|
|
75
|
-
const tmp = join(dir, `${LOCK_FILE}.${randomBytes(6).toString('hex')}`);
|
|
76
|
-
writeFileSync(tmp, token, { mode: 0o600, flag: 'wx' });
|
|
77
|
-
try {
|
|
78
|
-
linkSync(tmp, path);
|
|
79
|
-
}
|
|
80
|
-
catch (e) {
|
|
81
|
-
if (e.code === 'EEXIST')
|
|
82
|
-
return null;
|
|
83
|
-
throw e;
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
rmSync(tmp, { force: true });
|
|
87
|
-
}
|
|
88
|
-
return () => {
|
|
89
|
-
let held = '';
|
|
90
|
-
try {
|
|
91
|
-
held = readFileSync(path, 'utf8');
|
|
92
|
-
}
|
|
93
|
-
catch { }
|
|
94
|
-
if (held === token)
|
|
95
|
-
rmSync(path, { force: true });
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
81
|
const readOrNull = (file) => {
|
|
99
82
|
try {
|
|
100
83
|
return readFileSync(file, 'utf8');
|
|
@@ -108,17 +91,22 @@ const readOrNull = (file) => {
|
|
|
108
91
|
export function mount(options) {
|
|
109
92
|
const refuse = (reason, message) => ({ ok: false, reason, message });
|
|
110
93
|
const name = options?.name;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return refuse('reserved_name', `${name} is a reserved name`);
|
|
94
|
+
const named = nameRefusal(name);
|
|
95
|
+
if (named !== null)
|
|
96
|
+
return refuse(named.reason, named.message);
|
|
115
97
|
const configDir = absoluteDir(options.configDir);
|
|
116
98
|
if (configDir === null)
|
|
117
99
|
return refuse('config_dir', 'configDir must be the configuration directory as an absolute path; mount reads no environment variable and has no default');
|
|
118
100
|
const dir = join(configDir, 'config.d');
|
|
119
101
|
const file = join(dir, 'mcp.json');
|
|
120
|
-
|
|
121
|
-
|
|
102
|
+
let unlock;
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(dir, { recursive: true });
|
|
105
|
+
unlock = takeLockFile(join(dir, LOCK_FILE));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return refuse('write_failed', `config.d or its lock could not be written (${writeFailure(e)}); config.d/mcp.json was left as it was`);
|
|
109
|
+
}
|
|
122
110
|
if (unlock === null)
|
|
123
111
|
return refuse('locked', `another mount is writing config.d/mcp.json; try again, and if no mount is running remove config.d/${LOCK_FILE}`);
|
|
124
112
|
try {
|
|
@@ -155,8 +143,7 @@ function write(file, entry, refuse) {
|
|
|
155
143
|
try {
|
|
156
144
|
if (mode !== null)
|
|
157
145
|
fchmodSync(fd, mode);
|
|
158
|
-
|
|
159
|
-
fsyncSync(fd);
|
|
146
|
+
writeAll(fd, text);
|
|
160
147
|
}
|
|
161
148
|
finally {
|
|
162
149
|
closeSync(fd);
|
|
@@ -169,7 +156,7 @@ function write(file, entry, refuse) {
|
|
|
169
156
|
}
|
|
170
157
|
catch (e) {
|
|
171
158
|
rmSync(tmp, { force: true });
|
|
172
|
-
|
|
159
|
+
return refuse('write_failed', `config.d/mcp.json could not be written (${writeFailure(e)}); it was left as it was and nothing else was written`);
|
|
173
160
|
}
|
|
174
161
|
return { ok: true, changed: true, file };
|
|
175
162
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Env } from './config-dir.js';
|
|
2
|
+
export interface PinnedUpstreamOptions {
|
|
3
|
+
readonly connector: string;
|
|
4
|
+
readonly upstream: string;
|
|
5
|
+
readonly lockFile: URL;
|
|
6
|
+
readonly handles: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export type UpstreamStart = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly connectorDir: string;
|
|
11
|
+
readonly files: Readonly<Record<string, string>>;
|
|
12
|
+
run(args: readonly string[], extra: Readonly<Record<string, string>>): Promise<number>;
|
|
13
|
+
} | {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly problem: string;
|
|
16
|
+
};
|
|
17
|
+
export interface PinnedUpstream {
|
|
18
|
+
install(configDir: string): Promise<number>;
|
|
19
|
+
start(env: Env): UpstreamStart;
|
|
20
|
+
}
|
|
21
|
+
export declare function pinnedUpstream(options: PinnedUpstreamOptions): PinnedUpstream;
|