@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/secrets.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { closeSync, constants, fchmodSync,
|
|
2
|
+
import { closeSync, constants, fchmodSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
4
4
|
import { CONNECTOR_NAME } from './connector-name.js';
|
|
5
|
+
import { hasRepeatedKey } from './json-text.js';
|
|
6
|
+
import { takeLockFile } from './lock-file.js';
|
|
7
|
+
import { writeAll } from './write-all.js';
|
|
8
|
+
export const isSecret = (handle) => handle.secret !== false;
|
|
5
9
|
export const HANDLE_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
6
|
-
const
|
|
10
|
+
const META_SUFFIX = '.meta.json';
|
|
11
|
+
const META_KEYS = ['expiresAt', 'secret'];
|
|
7
12
|
const TIMESTAMP = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?Z$/;
|
|
8
13
|
const FILE_MODE = 0o600;
|
|
9
14
|
const DIRECTORY_MODE = 0o700;
|
|
15
|
+
export const LOCK_FILE = '.auth.lock';
|
|
16
|
+
export const LOCK_WAIT_MS = 5000;
|
|
17
|
+
const LOCK_POLL_MS = 25;
|
|
10
18
|
export function handleDirectory(configDir, connector) {
|
|
11
19
|
return join(configDir, 'connectors', connector);
|
|
12
20
|
}
|
|
@@ -22,22 +30,75 @@ export function readSecretValue(configDir, handle) {
|
|
|
22
30
|
closeSync(fd);
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
33
|
+
const held = new Set();
|
|
34
|
+
const pause = (ms) => void Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
35
|
+
function locked(dir, connector, fn) {
|
|
36
|
+
const path = join(dir, LOCK_FILE);
|
|
37
|
+
if (held.has(path))
|
|
38
|
+
return { ok: true, value: fn() };
|
|
39
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
40
|
+
let release = takeLockFile(path);
|
|
41
|
+
while (release === null) {
|
|
42
|
+
if (Date.now() >= deadline)
|
|
43
|
+
return { ok: false, problem: `connector ${connector}: another process held connectors/${connector}/${LOCK_FILE} for ${LOCK_WAIT_MS / 1000} s (an auth writing the handles, or a launch copying them); a lock left by a process that died is removed by hand` };
|
|
44
|
+
pause(LOCK_POLL_MS);
|
|
45
|
+
release = takeLockFile(path);
|
|
46
|
+
}
|
|
47
|
+
held.add(path);
|
|
48
|
+
try {
|
|
49
|
+
const value = fn();
|
|
50
|
+
if (typeof value?.then === 'function')
|
|
51
|
+
throw new Error('withLock runs a synchronous function: the lock is released as soon as it returns');
|
|
52
|
+
return { ok: true, value };
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
held.delete(path);
|
|
56
|
+
release();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const NO_HANDLES = { handles: [], files: new Map(), values: [], remove: () => undefined };
|
|
60
|
+
export function copyForLaunch(configDir, connector) {
|
|
61
|
+
const dir = handleDirectory(configDir, connector);
|
|
62
|
+
try {
|
|
63
|
+
lstatSync(dir);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (e.code === 'ENOENT')
|
|
67
|
+
return { ok: true, ...NO_HANDLES };
|
|
68
|
+
return { ok: false, problem: `connector ${connector}: the secrets directory cannot be read` };
|
|
69
|
+
}
|
|
70
|
+
const dirProblem = entryProblem(dir, 'directory', 'the secrets directory');
|
|
71
|
+
if (dirProblem !== null)
|
|
72
|
+
return { ok: false, problem: `connector ${connector}: ${dirProblem}` };
|
|
73
|
+
const r = locked(dir, connector, () => copyHandles(configDir, dir, connector));
|
|
74
|
+
return r.ok ? r.value : r;
|
|
75
|
+
}
|
|
76
|
+
function copyHandles(configDir, dir, connector) {
|
|
77
|
+
const listing = listHandles(dir, connector);
|
|
78
|
+
if (!listing.ok)
|
|
79
|
+
return listing;
|
|
80
|
+
if (listing.handles.length === 0)
|
|
81
|
+
return { ok: true, ...NO_HANDLES };
|
|
82
|
+
const copies = mkdtempSync(join(dir, '.launch-'));
|
|
83
|
+
const remove = () => rmSync(copies, { recursive: true, force: true });
|
|
30
84
|
try {
|
|
31
85
|
const files = new Map();
|
|
32
86
|
const values = [];
|
|
33
|
-
for (const handle of handles) {
|
|
87
|
+
for (const handle of listing.handles) {
|
|
34
88
|
const bytes = readSecretValue(configDir, handle);
|
|
35
|
-
const path = join(
|
|
36
|
-
|
|
89
|
+
const path = join(copies, handle.name);
|
|
90
|
+
const fd = openSync(path, 'wx', FILE_MODE);
|
|
91
|
+
try {
|
|
92
|
+
writeAll(fd, bytes);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
closeSync(fd);
|
|
96
|
+
}
|
|
37
97
|
files.set(handle.name, path);
|
|
38
|
-
|
|
98
|
+
if (isSecret(handle))
|
|
99
|
+
values.push(bytes.toString('utf8'));
|
|
39
100
|
}
|
|
40
|
-
return { files, values, remove };
|
|
101
|
+
return { ok: true, handles: listing.handles, files, values, remove };
|
|
41
102
|
}
|
|
42
103
|
catch (e) {
|
|
43
104
|
remove();
|
|
@@ -45,6 +106,25 @@ export function copyForLaunch(configDir, connector, handles) {
|
|
|
45
106
|
}
|
|
46
107
|
}
|
|
47
108
|
const validExpiry = (v) => typeof v === 'string' && TIMESTAMP.test(v) && Number.isFinite(Date.parse(v));
|
|
109
|
+
function readMeta(text) {
|
|
110
|
+
let v;
|
|
111
|
+
try {
|
|
112
|
+
v = JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v) || hasRepeatedKey(text))
|
|
118
|
+
return null;
|
|
119
|
+
const meta = v;
|
|
120
|
+
if (!Object.keys(meta).every((k) => META_KEYS.includes(k)))
|
|
121
|
+
return null;
|
|
122
|
+
if (Object.hasOwn(meta, 'expiresAt') && !validExpiry(meta.expiresAt))
|
|
123
|
+
return null;
|
|
124
|
+
if (Object.hasOwn(meta, 'secret') && typeof meta.secret !== 'boolean')
|
|
125
|
+
return null;
|
|
126
|
+
return { ...(validExpiry(meta.expiresAt) ? { expiresAt: meta.expiresAt } : {}), secret: meta.secret !== false };
|
|
127
|
+
}
|
|
48
128
|
function entryProblem(path, kind, label) {
|
|
49
129
|
let st;
|
|
50
130
|
try {
|
|
@@ -89,24 +169,24 @@ function listHandles(dir, connector) {
|
|
|
89
169
|
const problem = entryProblem(join(dir, name), 'file', `secret file ${name}`);
|
|
90
170
|
if (problem !== null)
|
|
91
171
|
return refuse(problem);
|
|
92
|
-
const
|
|
93
|
-
if (!names.includes(
|
|
94
|
-
handles.push({ connector, name, source: 'file' });
|
|
172
|
+
const metaFile = name + META_SUFFIX;
|
|
173
|
+
if (!names.includes(metaFile)) {
|
|
174
|
+
handles.push({ connector, name, source: 'file', secret: true });
|
|
95
175
|
continue;
|
|
96
176
|
}
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
return refuse(
|
|
100
|
-
let
|
|
177
|
+
const metaProblem = entryProblem(join(dir, metaFile), 'file', `metadata file ${metaFile}`);
|
|
178
|
+
if (metaProblem !== null)
|
|
179
|
+
return refuse(metaProblem);
|
|
180
|
+
let meta;
|
|
101
181
|
try {
|
|
102
|
-
|
|
182
|
+
meta = readMeta(readFileSync(join(dir, metaFile), 'utf8'));
|
|
103
183
|
}
|
|
104
184
|
catch {
|
|
105
|
-
|
|
185
|
+
meta = null;
|
|
106
186
|
}
|
|
107
|
-
if (
|
|
108
|
-
return refuse(`
|
|
109
|
-
handles.push({ connector, name, source: 'file',
|
|
187
|
+
if (meta === null)
|
|
188
|
+
return refuse(`metadata file ${metaFile} does not hold a JSON object whose only keys are "expiresAt" (an RFC 3339 UTC time) and "secret" (true or false), each named once`);
|
|
189
|
+
handles.push({ connector, name, source: 'file', ...meta });
|
|
110
190
|
}
|
|
111
191
|
return { ok: true, handles };
|
|
112
192
|
}
|
|
@@ -116,8 +196,7 @@ function writePrivately(path, text) {
|
|
|
116
196
|
const fd = openSync(tmp, 'wx', FILE_MODE);
|
|
117
197
|
try {
|
|
118
198
|
fchmodSync(fd, FILE_MODE);
|
|
119
|
-
|
|
120
|
-
fsyncSync(fd);
|
|
199
|
+
writeAll(fd, text);
|
|
121
200
|
}
|
|
122
201
|
finally {
|
|
123
202
|
closeSync(fd);
|
|
@@ -129,6 +208,25 @@ function writePrivately(path, text) {
|
|
|
129
208
|
throw e;
|
|
130
209
|
}
|
|
131
210
|
}
|
|
211
|
+
function ensureDirectory(dir, connector) {
|
|
212
|
+
mkdirSync(dir, { recursive: true, mode: DIRECTORY_MODE });
|
|
213
|
+
const dirProblem = entryProblem(dir, 'directory', 'the secrets directory');
|
|
214
|
+
if (dirProblem !== null)
|
|
215
|
+
throw new Error(`connector ${connector}: ${dirProblem}`);
|
|
216
|
+
}
|
|
217
|
+
function writeHandle(dir, connector, name, value, options) {
|
|
218
|
+
const secret = options.secret !== false;
|
|
219
|
+
const expiry = options.expiresAt === undefined ? {} : { expiresAt: options.expiresAt };
|
|
220
|
+
const meta = { ...expiry, ...(secret ? {} : { secret: false }) };
|
|
221
|
+
const metaFile = join(dir, name + META_SUFFIX);
|
|
222
|
+
const writeMeta = () => (Object.keys(meta).length === 0 ? rmSync(metaFile, { force: true }) : writePrivately(metaFile, JSON.stringify(meta) + '\n'));
|
|
223
|
+
if (secret)
|
|
224
|
+
writeMeta();
|
|
225
|
+
writePrivately(join(dir, name), value);
|
|
226
|
+
if (!secret)
|
|
227
|
+
writeMeta();
|
|
228
|
+
return { connector, name, source: 'file', secret, ...expiry };
|
|
229
|
+
}
|
|
132
230
|
function putHandle(dir, connector, name, value, options) {
|
|
133
231
|
if (!HANDLE_NAME.test(name))
|
|
134
232
|
throw new Error('a handle name is upper-case letters, digits and underscores, starting with a letter');
|
|
@@ -136,18 +234,13 @@ function putHandle(dir, connector, name, value, options) {
|
|
|
136
234
|
throw new Error('a secret value is a string');
|
|
137
235
|
if (options.expiresAt !== undefined && !validExpiry(options.expiresAt))
|
|
138
236
|
throw new Error('expiresAt must be an RFC 3339 UTC time ending in Z');
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
rmSync(expiryFile, { force: true });
|
|
147
|
-
return { connector, name, source: 'file' };
|
|
148
|
-
}
|
|
149
|
-
writePrivately(expiryFile, JSON.stringify({ expiresAt: options.expiresAt }) + '\n');
|
|
150
|
-
return { connector, name, source: 'file', expiresAt: options.expiresAt };
|
|
237
|
+
if (options.secret !== undefined && typeof options.secret !== 'boolean')
|
|
238
|
+
throw new Error('secret must be true or false');
|
|
239
|
+
ensureDirectory(dir, connector);
|
|
240
|
+
const r = locked(dir, connector, () => writeHandle(dir, connector, name, value, options));
|
|
241
|
+
if (!r.ok)
|
|
242
|
+
throw new Error(r.problem);
|
|
243
|
+
return r.value;
|
|
151
244
|
}
|
|
152
245
|
export function openFileSecrets(options) {
|
|
153
246
|
if (typeof options?.configDir !== 'string' || !isAbsolute(options.configDir))
|
|
@@ -157,7 +250,15 @@ export function openFileSecrets(options) {
|
|
|
157
250
|
const { connector } = options;
|
|
158
251
|
const dir = handleDirectory(options.configDir, connector);
|
|
159
252
|
return {
|
|
253
|
+
directory: dir,
|
|
160
254
|
list: () => listHandles(dir, connector),
|
|
161
255
|
put: (name, value, putOptions = {}) => putHandle(dir, connector, name, value, putOptions),
|
|
256
|
+
withLock: (fn) => {
|
|
257
|
+
ensureDirectory(dir, connector);
|
|
258
|
+
const r = locked(dir, connector, fn);
|
|
259
|
+
if (!r.ok)
|
|
260
|
+
throw new Error(r.problem);
|
|
261
|
+
return r.value;
|
|
262
|
+
},
|
|
162
263
|
};
|
|
163
264
|
}
|
package/dist/seed.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MountRefusal } from './mount.js';
|
|
2
|
+
export interface DefaultConnector {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly since: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const DEFAULT_CONNECTORS: readonly DefaultConnector[];
|
|
7
|
+
export declare const SEED_VERSION: number;
|
|
8
|
+
export interface SeedOptions {
|
|
9
|
+
readonly configDir: string;
|
|
10
|
+
}
|
|
11
|
+
export type SeedReading = {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly outcome: 'mounted' | 'present' | 'conflict';
|
|
14
|
+
} | {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly outcome: 'not_installed';
|
|
17
|
+
readonly message: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly outcome: 'refused';
|
|
21
|
+
readonly reason: MountRefusal;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
};
|
|
24
|
+
export type SeedRefusal = 'config_dir' | 'locked' | 'unreadable' | 'unexpected_shape' | 'write_failed';
|
|
25
|
+
export type SeedResult = {
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
readonly readings: readonly SeedReading[];
|
|
28
|
+
readonly marker: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly reason: SeedRefusal;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly readings: readonly SeedReading[];
|
|
34
|
+
};
|
|
35
|
+
export declare function seed(options: SeedOptions): SeedResult;
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { absoluteDir } from './config-dir.js';
|
|
5
|
+
import { hasRepeatedKey } from './json-text.js';
|
|
6
|
+
import { mount, nameRefusal } from './mount.js';
|
|
7
|
+
import { takeLockFile } from './lock-file.js';
|
|
8
|
+
import { resolveConnector } from './resolve-connector.js';
|
|
9
|
+
import { writeFailure, writeAll } from './write-all.js';
|
|
10
|
+
export const DEFAULT_CONNECTORS = Object.freeze([Object.freeze({ name: 'gitea', since: 1 }), Object.freeze({ name: 'github', since: 1 })]);
|
|
11
|
+
export const SEED_VERSION = newestVersion(DEFAULT_CONNECTORS);
|
|
12
|
+
function newestVersion(catalog) {
|
|
13
|
+
const names = new Set();
|
|
14
|
+
for (const c of catalog) {
|
|
15
|
+
const refusal = nameRefusal(c.name);
|
|
16
|
+
if (refusal !== null)
|
|
17
|
+
throw new Error(`the default catalog names a connector mount refuses (${refusal.reason})`);
|
|
18
|
+
if (names.has(c.name))
|
|
19
|
+
throw new Error(`the default catalog names ${c.name} twice`);
|
|
20
|
+
names.add(c.name);
|
|
21
|
+
if (!Number.isSafeInteger(c.since) || c.since < 1)
|
|
22
|
+
throw new Error(`the default catalog gives ${c.name} a since that is not a positive integer`);
|
|
23
|
+
}
|
|
24
|
+
if (catalog.length === 0)
|
|
25
|
+
throw new Error('the default catalog is empty');
|
|
26
|
+
return Math.max(...catalog.map((c) => c.since));
|
|
27
|
+
}
|
|
28
|
+
const MARKER_FILE = '.sema-connector-seed.json';
|
|
29
|
+
const SEED_LOCK_FILE = `${MARKER_FILE}.lock`;
|
|
30
|
+
const RECORDED = ['mounted', 'present', 'not_installed'];
|
|
31
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
32
|
+
function readMarker(file) {
|
|
33
|
+
const where = `config.d/${MARKER_FILE}`;
|
|
34
|
+
let text;
|
|
35
|
+
try {
|
|
36
|
+
text = readFileSync(file, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e.code === 'ENOENT')
|
|
40
|
+
return { marker: null };
|
|
41
|
+
return { refusal: 'unreadable', message: `${where} cannot be read` };
|
|
42
|
+
}
|
|
43
|
+
let doc;
|
|
44
|
+
try {
|
|
45
|
+
doc = JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { refusal: 'unreadable', message: `${where} is not valid JSON` };
|
|
49
|
+
}
|
|
50
|
+
const shape = `${where} must be one JSON object {"version": <a positive integer>, "entries": {"<name>": "mounted" | "present" | "not_installed"}}`;
|
|
51
|
+
if (!isObject(doc) || Object.keys(doc).sort().join() !== 'entries,version' || hasRepeatedKey(text))
|
|
52
|
+
return { refusal: 'unexpected_shape', message: shape };
|
|
53
|
+
const { version, entries } = doc;
|
|
54
|
+
if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 1 || !isObject(entries))
|
|
55
|
+
return { refusal: 'unexpected_shape', message: shape };
|
|
56
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
57
|
+
if (nameRefusal(name) !== null || typeof value !== 'string' || !RECORDED.includes(value))
|
|
58
|
+
return { refusal: 'unexpected_shape', message: shape };
|
|
59
|
+
}
|
|
60
|
+
return { marker: { version, entries: entries } };
|
|
61
|
+
}
|
|
62
|
+
export function seed(options) {
|
|
63
|
+
const configDir = absoluteDir(options?.configDir);
|
|
64
|
+
if (configDir === null)
|
|
65
|
+
return { ok: false, reason: 'config_dir', message: 'configDir must be the configuration directory as an absolute path; the seed reads no environment variable and has no default', readings: [] };
|
|
66
|
+
const dir = join(configDir, 'config.d');
|
|
67
|
+
let unlock;
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dir, { recursive: true });
|
|
70
|
+
unlock = takeLockFile(join(dir, SEED_LOCK_FILE));
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return { ok: false, reason: 'write_failed', message: `config.d or the seed's lock could not be written (${writeFailure(e)}); nothing was mounted`, readings: [] };
|
|
74
|
+
}
|
|
75
|
+
if (unlock === null)
|
|
76
|
+
return { ok: false, reason: 'locked', message: `another seed is running on this directory; nothing was mounted: try again, and if no seed is running remove config.d/${SEED_LOCK_FILE}`, readings: [] };
|
|
77
|
+
try {
|
|
78
|
+
return seedLocked(configDir, dir);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
unlock();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function seedLocked(configDir, dir) {
|
|
85
|
+
const file = join(dir, MARKER_FILE);
|
|
86
|
+
const read = readMarker(file);
|
|
87
|
+
if ('refusal' in read)
|
|
88
|
+
return { ok: false, reason: read.refusal, message: `${read.message}; nothing was mounted`, readings: [] };
|
|
89
|
+
const before = read.marker;
|
|
90
|
+
const entries = { ...before?.entries };
|
|
91
|
+
const readings = [];
|
|
92
|
+
for (const { name } of DEFAULT_CONNECTORS) {
|
|
93
|
+
const recorded = before !== null && Object.hasOwn(before.entries, name) ? before.entries[name] : undefined;
|
|
94
|
+
if (recorded === 'mounted' || recorded === 'present') {
|
|
95
|
+
readings.push({ name, outcome: 'present' });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const resolved = resolveConnector(name);
|
|
99
|
+
if ('reason' in resolved) {
|
|
100
|
+
readings.push({ name, outcome: 'not_installed', message: `package ${resolved.packageName} ${resolved.reason}` });
|
|
101
|
+
entries[name] = 'not_installed';
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const r = mount({ name, configDir });
|
|
105
|
+
if (r.ok) {
|
|
106
|
+
const outcome = r.changed ? 'mounted' : 'present';
|
|
107
|
+
readings.push({ name, outcome });
|
|
108
|
+
entries[name] = outcome;
|
|
109
|
+
}
|
|
110
|
+
else if (r.reason === 'conflict')
|
|
111
|
+
readings.push({ name, outcome: 'conflict' });
|
|
112
|
+
else
|
|
113
|
+
readings.push({ name, outcome: 'refused', reason: r.reason, message: r.message });
|
|
114
|
+
}
|
|
115
|
+
const after = { version: SEED_VERSION, entries };
|
|
116
|
+
if (before === null || JSON.stringify(after) !== JSON.stringify(before)) {
|
|
117
|
+
const tmp = join(dir, `${MARKER_FILE}.${randomBytes(6).toString('hex')}.tmp`);
|
|
118
|
+
try {
|
|
119
|
+
const fd = openSync(tmp, 'wx', 0o666);
|
|
120
|
+
try {
|
|
121
|
+
writeAll(fd, JSON.stringify(after, null, 2) + '\n');
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
closeSync(fd);
|
|
125
|
+
}
|
|
126
|
+
renameSync(tmp, file);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
rmSync(tmp, { force: true });
|
|
130
|
+
return { ok: false, reason: 'write_failed', message: `config.d/${MARKER_FILE} could not be written (${writeFailure(e)}); it was left as it was, and the entries mounted stay mounted`, readings };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, readings, marker: file };
|
|
134
|
+
}
|
package/dist/status.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectorMintedStatus } from '@sema-agent/connectors-contract';
|
|
2
|
-
import type
|
|
2
|
+
import { type SecretHandle } from './secrets.js';
|
|
3
3
|
export declare const STATUS_MIME_TYPE = "application/json";
|
|
4
4
|
export declare const AUTHORIZE_URL_PATTERN: RegExp;
|
|
5
5
|
export declare function statusUri(connector: string): string;
|
package/dist/status.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSecret } from './secrets.js';
|
|
1
2
|
export const STATUS_MIME_TYPE = 'application/json';
|
|
2
3
|
export const AUTHORIZE_URL_PATTERN = /^https?:\/\//;
|
|
3
4
|
export function statusUri(connector) {
|
|
@@ -13,7 +14,8 @@ export function statusResource(connector) {
|
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
export function mintStatus(input) {
|
|
16
|
-
const
|
|
17
|
+
const secrets = input.handles.filter(isSecret);
|
|
18
|
+
const state = secrets.length === 0 ? 'unauthorized' : secrets.some((h) => h.expiresAt !== undefined && Date.parse(h.expiresAt) <= input.nowMs) ? 'expired' : 'authorized';
|
|
17
19
|
const link = state === 'unauthorized' && input.authorizeUrl !== undefined && AUTHORIZE_URL_PATTERN.test(input.authorizeUrl) ? { authorizeUrl: input.authorizeUrl } : {};
|
|
18
20
|
return { state, tools: [...input.tools], ...link };
|
|
19
21
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fstatSync, fsyncSync, writeSync } from 'node:fs';
|
|
2
|
+
export function writeAll(fd, data) {
|
|
3
|
+
const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
4
|
+
let written = 0;
|
|
5
|
+
while (written < bytes.length) {
|
|
6
|
+
const n = writeSync(fd, bytes, written, bytes.length - written, written);
|
|
7
|
+
if (n <= 0)
|
|
8
|
+
throw new Error(`a write stopped after ${written} of ${bytes.length} bytes`);
|
|
9
|
+
written += n;
|
|
10
|
+
}
|
|
11
|
+
fsyncSync(fd);
|
|
12
|
+
const size = fstatSync(fd).size;
|
|
13
|
+
if (size !== bytes.length)
|
|
14
|
+
throw new Error(`the file holds ${size} bytes after a write of ${bytes.length}`);
|
|
15
|
+
}
|
|
16
|
+
export function writeFailure(e) {
|
|
17
|
+
const code = e?.code;
|
|
18
|
+
return typeof code === 'string' ? code : e instanceof Error ? e.message : 'unknown error';
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/connectors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared connector layer and the sema-connector launcher (MCP stdio proxy with principal, scope, normalize, status and trace stages).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|