@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 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. A thin wrapper returns its number as its exit code.
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
- if (typeof name !== 'string' || !CONNECTOR_NAME.test(name))
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.4.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",