@sema-agent/connectors 0.5.0 → 0.6.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,61 @@ 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.6.0 (2026-10-16)
8
+
9
+ ### Changed (breaking)
10
+
11
+ - `binaries.lock.json`, which `pinnedUpstream` reads (CN-027): an upstream is now
12
+ any number of archives and one program, `{"<upstream>": {"version",
13
+ "archives": [{"platform", "file", "source", "sha256", "unpack"}],
14
+ "program"}}`. An archive names one platform or `"*"` (every platform the lock
15
+ pins); its file name gives its format (`.tar.gz` or `.tgz`, `.zip`); `unpack`
16
+ is `{"entry": <name>}` (the one file of the archive, as before) or
17
+ `{"directory": <top directory>, "to": <path below bin>}` (every entry below
18
+ that directory). `program` is `{"binary": <path>}` or `{"node": <path>}`, a
19
+ Node.js script run by the Node.js that runs the connector. The lock of the
20
+ earlier shape (`source` and `platforms`) is refused; connector packages move
21
+ to the new shape with this version.
22
+ - `install` fetches every archive of the platform into the bin directory and
23
+ compares each with its sha256, and checks what it unpacks, before anything is
24
+ unpacked: an archive that does not match, or cannot be unpacked as the lock
25
+ says, installs nothing. Archives are streamed to disk instead of held in
26
+ memory; an archive is kept in the bin directory only once what it unpacks is
27
+ in place. The start check reads every archive's sha256, compares each tree
28
+ with its archive's listing (paths, kinds, sizes, modes, link targets; a tree
29
+ that differs is reinstalled by `install`) and the program with its entry.
30
+
31
+ ### Added
32
+
33
+ - `readHostUrl(value)` (CN-045): the one rule for an address a connector stores as a non-secret host handle and hands
34
+ its upstream program (an `https` URL, or an `http` URL on `127.0.0.1` or `localhost`, without credentials, a query
35
+ or a fragment, stored as the URL's origin and path without a trailing slash); moved here from the Gitea connector's
36
+ own `readHost` (CN-037), so a second connector that needs a host does not carry a second copy of the rule.
37
+ - `readStdinJson(limit, keys, optional?)` takes a third, optional list of keys a caller's JSON object may also carry
38
+ besides `keys` (CN-045): the same refusal shape (exactly `keys`, at most `optional` besides them, no other key, a
39
+ key named twice refused, every value a string). Omitted, behaviour is exactly as before.
40
+ - `pinnedUpstream({ ..., optionalHandles })` (CN-045): handles a connector's server may use when stored, but starts
41
+ without; given, an optional handle's `<NAME>_FILE` is checked the same way a required one's is (a regular file
42
+ inside the connector's directory) and named in `UpstreamStart.files`; absent, `start()` does not wait for it. A
43
+ required or an optional handle's `<NAME>_FILE` is kept from the program either way.
44
+ - Zip archives and archive trees (CN-027): a tree is checked whole before the
45
+ first byte is written (no entry outside its directory, no `..`, empty or `.`
46
+ segment, no absolute path, no name twice, nothing below a file or a link, no
47
+ hard link, device or pipe, a symbolic link only to a relative path below its
48
+ own directory), written with modes 0644 and 0755 only, zip entries streamed
49
+ through inflate with their size and CRC-32 checked; at most 1 GiB per
50
+ archive. Split, ZIP64 and encrypted zip archives are refused.
51
+ - `UpstreamStart.binDir`, the bin directory holding the pinned build.
52
+
53
+ ### Fixed
54
+
55
+ - The CLI kit's program name follows mount's whole name rule (CN-053): a
56
+ reserved name such as `sema-connector` is refused with the launcher's own
57
+ prefix, `sema-connector: mount refused (reserved_name): ...`, as `INTEGRATION.md`
58
+ says, instead of `sema-connector-sema-connector:`. A copy of the package whose
59
+ own manifest is out of reach (bundled, KL-14) refuses `statusCommand` in one
60
+ line with exit 1 instead of failing with a thrown error.
61
+
7
62
  ## 0.5.0 (2026-10-15)
8
63
 
9
64
  ### Added
package/INTEGRATION.md CHANGED
@@ -66,7 +66,8 @@ const result = mount({ name: 'gitea', configDir: dir })
66
66
  `@sema-agent/connectors` is consumed as an installed package, not bundled: `statusCommand` starts the
67
67
  `sema-connector` launcher of the very copy it belongs to, and `seed` resolves connector packages from that
68
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.
69
+ inlined into another program's bundle finds neither and refuses in one line (KL-14 in the repository's
70
+ `docs/KNOWN-LIMITS.md`). `mount` and `mountCommand` have no such dependency.
70
71
 
71
72
  The entry `mount` writes names the command `sema-connector`; the engine runs it from its `PATH`. The
72
73
  launcher is the `bin` of `@sema-agent/connectors`, and a connector is the `bin` of
package/dist/cli-kit.d.ts CHANGED
@@ -29,6 +29,6 @@ export type StdinJson = {
29
29
  readonly problem: string;
30
30
  };
31
31
  export declare const listed: (keys: readonly string[]) => string;
32
- export declare function readStdinJson(limit: number, keys: readonly string[]): Promise<StdinJson>;
32
+ export declare function readStdinJson(limit: number, keys: readonly string[], optional?: readonly string[]): Promise<StdinJson>;
33
33
  export declare function mountCommand(name: string, configDir: string): number;
34
34
  export declare function statusCommand(name: string, configDir: string): Promise<number>;
package/dist/cli-kit.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { resolveConfigDir } from './config-dir.js';
3
- import { CONNECTOR_NAME } from './connector-name.js';
4
3
  import { hasRepeatedKey } from './json-text.js';
5
- import { mount } from './mount.js';
4
+ import { mount, nameRefusal } from './mount.js';
6
5
  import { ownManifest } from './own-package.js';
7
6
  import { MCP_PROTOCOL_VERSION } from './session.js';
8
7
  import { statusUri } from './status.js';
9
8
  export function cliOutput(name) {
10
- const program = typeof name === 'string' && CONNECTOR_NAME.test(name) ? `sema-connector-${name}` : 'sema-connector';
9
+ const program = nameRefusal(name) === null ? `sema-connector-${name}` : 'sema-connector';
11
10
  return {
12
11
  program,
13
12
  say: (line) => void process.stdout.write(`${line}\n`),
@@ -62,12 +61,14 @@ export async function readStdinLine(limit) {
62
61
  return { ok: true, line };
63
62
  }
64
63
  export const listed = (keys) => (keys.length < 2 ? keys.join('') : `${keys.slice(0, -1).join(', ')} and ${keys[keys.length - 1]}`);
65
- export async function readStdinJson(limit, keys) {
64
+ export async function readStdinJson(limit, keys, optional = []) {
66
65
  const read = await readStdinLine(limit);
67
66
  if (!read.ok)
68
67
  return read;
69
- const exactly = keys.length === 1 ? `exactly the key ${listed(keys)}` : `exactly the keys ${listed(keys)}`;
70
- const shape = `standard input must be one JSON object with ${exactly}, ${keys.length === 1 ? 'a string' : 'each a string'}, on one line`;
68
+ const all = [...keys, ...optional];
69
+ const phrase = (how, names) => `${how} the ${names.length === 1 ? 'key' : 'keys'} ${listed(names)}`;
70
+ const exactly = [...(keys.length > 0 ? [phrase('exactly', keys)] : []), ...(optional.length > 0 ? [phrase('at most', optional)] : [])].join(' and ') || 'no keys';
71
+ const shape = `standard input must be one JSON object with ${exactly}, ${all.length === 1 ? 'a string' : 'each a string'}, on one line`;
71
72
  let value;
72
73
  try {
73
74
  value = JSON.parse(read.line);
@@ -81,11 +82,11 @@ export async function readStdinJson(limit, keys) {
81
82
  return { ok: false, problem: 'the object names a key more than once' };
82
83
  const object = value;
83
84
  const own = Object.keys(object);
84
- if (own.length !== keys.length || !keys.every((k) => own.includes(k)))
85
+ if (!keys.every((k) => own.includes(k)) || !own.every((k) => all.includes(k)))
85
86
  return { ok: false, problem: `the object must have ${exactly}` };
86
- if (!keys.every((k) => typeof object[k] === 'string'))
87
+ if (!own.every((k) => typeof object[k] === 'string'))
87
88
  return { ok: false, problem: shape };
88
- return { ok: true, values: Object.fromEntries(keys.map((k) => [k, object[k]])) };
89
+ return { ok: true, values: Object.fromEntries(own.map((k) => [k, object[k]])) };
89
90
  }
90
91
  export function mountCommand(name, configDir) {
91
92
  const { say, complain } = cliOutput(name);
@@ -143,7 +144,14 @@ function lineReader(stream) {
143
144
  }
144
145
  export async function statusCommand(name, configDir) {
145
146
  const { program, say, complain } = cliOutput(name);
146
- const own = ownManifest();
147
+ let own;
148
+ try {
149
+ own = ownManifest();
150
+ }
151
+ catch {
152
+ complain('the package\'s own manifest cannot be read, so its launcher cannot be found: this package is consumed installed, not bundled (INTEGRATION.md, KL-14)');
153
+ return 1;
154
+ }
147
155
  const child = spawn(process.execPath, [own.launcher, 'run', name, '--mode', 'standalone', '--transport', 'stdio', '--config-dir', configDir], { stdio: ['pipe', 'pipe', 'pipe'] });
148
156
  const exited = new Promise((resolve) => child.on('exit', (code) => resolve(code)));
149
157
  const errors = [];
@@ -0,0 +1,8 @@
1
+ export type HostReading = {
2
+ readonly ok: true;
3
+ readonly host: string;
4
+ } | {
5
+ readonly ok: false;
6
+ readonly problem: string;
7
+ };
8
+ export declare function readHostUrl(value: string): HostReading;
@@ -0,0 +1,19 @@
1
+ import { isLoopbackHttpOrHttps } from './jwks.js';
2
+ export function readHostUrl(value) {
3
+ if (/[\s\u0000-\u001f\u007f]/.test(value))
4
+ return { ok: false, problem: 'the host is not a URL (it holds whitespace or a control character)' };
5
+ let url;
6
+ try {
7
+ url = new URL(value);
8
+ }
9
+ catch {
10
+ return { ok: false, problem: 'the host is not a URL' };
11
+ }
12
+ if (url.username !== '' || url.password !== '')
13
+ return { ok: false, problem: 'the host carries credentials; a host URL never does (the token is stored on its own)' };
14
+ if (!isLoopbackHttpOrHttps(url))
15
+ return { ok: false, problem: 'the host must be an https URL, or an http URL on 127.0.0.1 or localhost' };
16
+ if (url.search !== '' || url.hash !== '')
17
+ return { ok: false, problem: 'the host URL takes no query and no fragment' };
18
+ return { ok: true, host: url.origin + url.pathname.replace(/\/+$/, '') };
19
+ }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export type { DefaultConnector, SeedOptions, SeedReading, SeedRefusal, SeedResul
11
11
  export { cliOutput, connectorCli, mountCommand, readStdinJson, readStdinLine, statusCommand } from './cli-kit.js';
12
12
  export type { CliOutput, ConnectorCommands, StdinJson, StdinLine } from './cli-kit.js';
13
13
  export { isLoopbackHttpOrHttps } from './jwks.js';
14
+ export { readHostUrl } from './host-url.js';
15
+ export type { HostReading } from './host-url.js';
14
16
  export { pinnedUpstream } from './pinned-upstream.js';
15
17
  export type { PinnedUpstream, PinnedUpstreamOptions, UpstreamStart } from './pinned-upstream.js';
16
18
  export { writeAll } from './write-all.js';
package/dist/index.js CHANGED
@@ -5,5 +5,6 @@ export { mount } from './mount.js';
5
5
  export { DEFAULT_CONNECTORS, SEED_VERSION, seed } from './seed.js';
6
6
  export { cliOutput, connectorCli, mountCommand, readStdinJson, readStdinLine, statusCommand } from './cli-kit.js';
7
7
  export { isLoopbackHttpOrHttps } from './jwks.js';
8
+ export { readHostUrl } from './host-url.js';
8
9
  export { pinnedUpstream } from './pinned-upstream.js';
9
10
  export { writeAll } from './write-all.js';
@@ -4,10 +4,12 @@ export interface PinnedUpstreamOptions {
4
4
  readonly upstream: string;
5
5
  readonly lockFile: URL;
6
6
  readonly handles: readonly string[];
7
+ readonly optionalHandles?: readonly string[];
7
8
  }
8
9
  export type UpstreamStart = {
9
10
  readonly ok: true;
10
11
  readonly connectorDir: string;
12
+ readonly binDir: string;
11
13
  readonly files: Readonly<Record<string, string>>;
12
14
  run(args: readonly string[], extra: Readonly<Record<string, string>>): Promise<number>;
13
15
  } | {