@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 +55 -0
- package/INTEGRATION.md +2 -1
- package/dist/cli-kit.d.ts +1 -1
- package/dist/cli-kit.js +18 -10
- package/dist/host-url.d.ts +8 -0
- package/dist/host-url.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pinned-upstream.d.ts +2 -0
- package/dist/pinned-upstream.js +346 -85
- package/dist/release-archive.d.ts +22 -0
- package/dist/release-archive.js +462 -10
- package/dist/write-all.d.ts +2 -0
- package/dist/write-all.js +36 -6
- package/package.json +1 -1
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
|
|
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 =
|
|
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
|
|
70
|
-
const
|
|
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 (
|
|
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 (!
|
|
87
|
+
if (!own.every((k) => typeof object[k] === 'string'))
|
|
87
88
|
return { ok: false, problem: shape };
|
|
88
|
-
return { ok: true, values: Object.fromEntries(
|
|
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
|
-
|
|
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 = [];
|
package/dist/host-url.js
ADDED
|
@@ -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
|
} | {
|