@sema-agent/connectors 0.4.0 → 0.5.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 +30 -0
- package/INTEGRATION.md +20 -1
- package/dist/cli-kit.js +1 -3
- package/dist/secrets.d.ts +1 -0
- package/dist/secrets.js +34 -0
- package/dist/session.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to this package are documented here. Released sections are
|
|
|
4
4
|
frozen: once a version is published its section is never edited again; new text
|
|
5
5
|
goes into the next version's section.
|
|
6
6
|
|
|
7
|
+
## 0.5.0 (2026-10-15)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `FileSecrets.remove(name)` (CN-048): removes a handle's value file and its
|
|
12
|
+
metadata file together, inside the lock of the connector's directory
|
|
13
|
+
(unless `withLock` already holds it); returns whether the handle existed
|
|
14
|
+
before. Absent is a no-op.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `FileSecrets.put` refuses the empty string (CN-048): a handle always has a
|
|
19
|
+
value, so `put(name, '')` throws a `TypeError` instead of writing one.
|
|
20
|
+
Nothing is written; an existing handle's value and metadata are untouched
|
|
21
|
+
byte for byte. A connector that needs to take a handle away uses `remove`.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- A `tools/call` outside the granted scope, sent as a notification (no
|
|
26
|
+
`id`): the launcher no longer answers it with an id-null
|
|
27
|
+
`connector.forbidden_scope` error and no longer writes its receipt (CN-047).
|
|
28
|
+
Nothing answers a notification, whatever refuses it; this already held for
|
|
29
|
+
a parked session's refusal (CN-046) and now holds for the scope judge's
|
|
30
|
+
too, the same form (`id === null ? [] : ...`).
|
|
31
|
+
- `mountCommand` (and every CLI kit command) with a connector name `mount`
|
|
32
|
+
refuses no longer throws before `mount` is called: the refusal arrives as the
|
|
33
|
+
documented one line on standard error, `mount refused (invalid_name): ...`,
|
|
34
|
+
with exit 1 (CN-050). `cliOutput` gives such a name the launcher's own program
|
|
35
|
+
name instead of refusing it itself; the name rule is judged once, by `mount`.
|
|
36
|
+
|
|
7
37
|
## 0.4.0 (2026-10-15)
|
|
8
38
|
|
|
9
39
|
### Added
|
package/INTEGRATION.md
CHANGED
|
@@ -39,7 +39,10 @@ const result = mount({ name: 'gitea', configDir: dir })
|
|
|
39
39
|
- `mountCommand(name, configDir): number` is the same call with the CLI's output: on success one line on
|
|
40
40
|
standard output (`mounted connector <name> in <file>; restart the engine to apply`, or
|
|
41
41
|
`connector <name> is already mounted in <file>`) and exit 0; on a refusal one line on standard error,
|
|
42
|
-
`mount refused (<reason>): <message>`, and exit 1.
|
|
42
|
+
`sema-connector-<name>: mount refused (<reason>): <message>`, and exit 1. The prefix is the program name
|
|
43
|
+
of the connector's own CLI; a name `mount` refuses (`invalid_name`, `reserved_name`) gets the launcher's own,
|
|
44
|
+
`sema-connector: `, since no connector program exists for it. A thin wrapper returns its number as its
|
|
45
|
+
exit code and passes both streams through.
|
|
43
46
|
- `mount` does not check that the connector package is installed: an entry for a connector that is not
|
|
44
47
|
installed makes the launcher exit 4 (`connector.not_installed`) when the engine starts it. A tool that
|
|
45
48
|
wants only installed connectors mounted uses the seed.
|
|
@@ -58,6 +61,22 @@ const result = mount({ name: 'gitea', configDir: dir })
|
|
|
58
61
|
command line). A tool that offers "mount the defaults" runs the seed; a tool that offers "mount this one"
|
|
59
62
|
calls `mount`.
|
|
60
63
|
|
|
64
|
+
## Installing the pieces
|
|
65
|
+
|
|
66
|
+
`@sema-agent/connectors` is consumed as an installed package, not bundled: `statusCommand` starts the
|
|
67
|
+
`sema-connector` launcher of the very copy it belongs to, and `seed` resolves connector packages from that
|
|
68
|
+
copy's place in `node_modules`, both through the package's own `package.json` (`import.meta.url`), so a copy
|
|
69
|
+
inlined into another program's bundle finds neither. `mount` and `mountCommand` have no such dependency.
|
|
70
|
+
|
|
71
|
+
The entry `mount` writes names the command `sema-connector`; the engine runs it from its `PATH`. The
|
|
72
|
+
launcher is the `bin` of `@sema-agent/connectors`, and a connector is the `bin` of
|
|
73
|
+
`@sema-agent/connector-<name>`, installed where that launcher resolves it (the same `node_modules`, a global
|
|
74
|
+
install, or `NODE_PATH`). A tool that mounts a connector does not install either; it tells the person what to
|
|
75
|
+
install (`npm install -g @sema-agent/connectors @sema-agent/connector-<name>`, then the connector's `install`
|
|
76
|
+
and `auth` with the same `CONFIG_LOCAL_DIR`). An entry whose launcher is not on the engine's `PATH`, or whose
|
|
77
|
+
connector package is not installed, fails at the engine's next start (exit 4, `connector.not_installed`);
|
|
78
|
+
`seed` mounts only connectors the launcher that runs it finds.
|
|
79
|
+
|
|
61
80
|
## What the engine sees
|
|
62
81
|
|
|
63
82
|
Entries are read by the engine from `config.d/mcp.json` through the server's local file configuration
|
package/dist/cli-kit.js
CHANGED
|
@@ -7,9 +7,7 @@ import { ownManifest } from './own-package.js';
|
|
|
7
7
|
import { MCP_PROTOCOL_VERSION } from './session.js';
|
|
8
8
|
import { statusUri } from './status.js';
|
|
9
9
|
export function cliOutput(name) {
|
|
10
|
-
|
|
11
|
-
throw new Error('a connector name is lowercase letters and digits in hyphen-separated words');
|
|
12
|
-
const program = `sema-connector-${name}`;
|
|
10
|
+
const program = typeof name === 'string' && CONNECTOR_NAME.test(name) ? `sema-connector-${name}` : 'sema-connector';
|
|
13
11
|
return {
|
|
14
12
|
program,
|
|
15
13
|
say: (line) => void process.stdout.write(`${line}\n`),
|
package/dist/secrets.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface FileSecrets {
|
|
|
20
20
|
readonly directory: string;
|
|
21
21
|
list(): SecretListing;
|
|
22
22
|
put(name: string, value: string, options?: PutOptions): SecretHandle;
|
|
23
|
+
remove(name: string): boolean;
|
|
23
24
|
withLock<T>(fn: () => T): T;
|
|
24
25
|
}
|
|
25
26
|
export declare const isSecret: (handle: SecretHandle) => boolean;
|
package/dist/secrets.js
CHANGED
|
@@ -232,6 +232,8 @@ function putHandle(dir, connector, name, value, options) {
|
|
|
232
232
|
throw new Error('a handle name is upper-case letters, digits and underscores, starting with a letter');
|
|
233
233
|
if (typeof value !== 'string')
|
|
234
234
|
throw new Error('a secret value is a string');
|
|
235
|
+
if (value === '')
|
|
236
|
+
throw new TypeError('a secret value is never the empty string (use remove to take the handle away)');
|
|
235
237
|
if (options.expiresAt !== undefined && !validExpiry(options.expiresAt))
|
|
236
238
|
throw new Error('expiresAt must be an RFC 3339 UTC time ending in Z');
|
|
237
239
|
if (options.secret !== undefined && typeof options.secret !== 'boolean')
|
|
@@ -242,6 +244,37 @@ function putHandle(dir, connector, name, value, options) {
|
|
|
242
244
|
throw new Error(r.problem);
|
|
243
245
|
return r.value;
|
|
244
246
|
}
|
|
247
|
+
function removeHandle(dir, connector, name) {
|
|
248
|
+
if (!HANDLE_NAME.test(name))
|
|
249
|
+
throw new Error('a handle name is upper-case letters, digits and underscores, starting with a letter');
|
|
250
|
+
try {
|
|
251
|
+
lstatSync(dir);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
if (e.code === 'ENOENT')
|
|
255
|
+
return false;
|
|
256
|
+
throw new Error(`connector ${connector}: the secrets directory cannot be read`);
|
|
257
|
+
}
|
|
258
|
+
const dirProblem = entryProblem(dir, 'directory', 'the secrets directory');
|
|
259
|
+
if (dirProblem !== null)
|
|
260
|
+
throw new Error(`connector ${connector}: ${dirProblem}`);
|
|
261
|
+
const r = locked(dir, connector, () => {
|
|
262
|
+
let existed = true;
|
|
263
|
+
try {
|
|
264
|
+
rmSync(join(dir, name));
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
if (e.code !== 'ENOENT')
|
|
268
|
+
throw e;
|
|
269
|
+
existed = false;
|
|
270
|
+
}
|
|
271
|
+
rmSync(join(dir, name + META_SUFFIX), { force: true });
|
|
272
|
+
return existed;
|
|
273
|
+
});
|
|
274
|
+
if (!r.ok)
|
|
275
|
+
throw new Error(r.problem);
|
|
276
|
+
return r.value;
|
|
277
|
+
}
|
|
245
278
|
export function openFileSecrets(options) {
|
|
246
279
|
if (typeof options?.configDir !== 'string' || !isAbsolute(options.configDir))
|
|
247
280
|
throw new Error('configDir must be an absolute path');
|
|
@@ -253,6 +286,7 @@ export function openFileSecrets(options) {
|
|
|
253
286
|
directory: dir,
|
|
254
287
|
list: () => listHandles(dir, connector),
|
|
255
288
|
put: (name, value, putOptions = {}) => putHandle(dir, connector, name, value, putOptions),
|
|
289
|
+
remove: (name) => removeHandle(dir, connector, name),
|
|
256
290
|
withLock: (fn) => {
|
|
257
291
|
ensureDirectory(dir, connector);
|
|
258
292
|
const r = locked(dir, connector, fn);
|
package/dist/session.js
CHANGED
|
@@ -118,7 +118,7 @@ export class Session {
|
|
|
118
118
|
switch (m.method) {
|
|
119
119
|
case 'tools/call': {
|
|
120
120
|
if (typeof name !== 'string' || !isGranted(grant, name))
|
|
121
|
-
return [up(errorLine(id, INVALID_PARAMS, 'the requested tool is not in the granted scope', FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
|
|
121
|
+
return id === null ? [] : [up(errorLine(id, INVALID_PARAMS, 'the requested tool is not in the granted scope', FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
|
|
122
122
|
if (this.options.parked === true)
|
|
123
123
|
return id === null ? [] : [up(errorLine(id, INTERNAL_ERROR, noCredentials(this.options.connector), AUTH_REQUIRED)), ...this.settle(call, 'refused', AUTH_REQUIRED)];
|
|
124
124
|
return this.forward(raw, id, grant, undefined, undefined, call);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/connectors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|