@sema-agent/connectors 0.0.0-stage → 0.1.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.
@@ -0,0 +1,163 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { closeSync, constants, fchmodSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync } from 'node:fs';
3
+ import { basename, dirname, isAbsolute, join } from 'node:path';
4
+ import { CONNECTOR_NAME } from './connector-name.js';
5
+ export const HANDLE_NAME = /^[A-Z][A-Z0-9_]*$/;
6
+ const EXPIRY_SUFFIX = '.meta.json';
7
+ 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
+ const FILE_MODE = 0o600;
9
+ const DIRECTORY_MODE = 0o700;
10
+ export function handleDirectory(configDir, connector) {
11
+ return join(configDir, 'connectors', connector);
12
+ }
13
+ export function handleFilePath(configDir, handle) {
14
+ return join(handleDirectory(configDir, handle.connector), handle.name);
15
+ }
16
+ export function readSecretValue(configDir, handle) {
17
+ const fd = openSync(handleFilePath(configDir, handle), constants.O_RDONLY | constants.O_NOFOLLOW);
18
+ try {
19
+ return readFileSync(fd);
20
+ }
21
+ finally {
22
+ closeSync(fd);
23
+ }
24
+ }
25
+ export function copyForLaunch(configDir, connector, handles) {
26
+ if (handles.length === 0)
27
+ return { files: new Map(), values: [], remove: () => undefined };
28
+ const dir = mkdtempSync(join(handleDirectory(configDir, connector), '.launch-'));
29
+ const remove = () => rmSync(dir, { recursive: true, force: true });
30
+ try {
31
+ const files = new Map();
32
+ const values = [];
33
+ for (const handle of handles) {
34
+ const bytes = readSecretValue(configDir, handle);
35
+ const path = join(dir, handle.name);
36
+ writeFileSync(path, bytes, { mode: FILE_MODE, flag: 'wx' });
37
+ files.set(handle.name, path);
38
+ values.push(bytes.toString('utf8'));
39
+ }
40
+ return { files, values, remove };
41
+ }
42
+ catch (e) {
43
+ remove();
44
+ throw e;
45
+ }
46
+ }
47
+ const validExpiry = (v) => typeof v === 'string' && TIMESTAMP.test(v) && Number.isFinite(Date.parse(v));
48
+ function entryProblem(path, kind, label) {
49
+ let st;
50
+ try {
51
+ st = lstatSync(path);
52
+ }
53
+ catch {
54
+ return `${label} cannot be read`;
55
+ }
56
+ if (st.isSymbolicLink())
57
+ return `${label} is a symbolic link`;
58
+ if (kind === 'directory' ? !st.isDirectory() : !st.isFile())
59
+ return `${label} is not a ${kind === 'directory' ? 'directory' : 'regular file'}`;
60
+ const allowed = kind === 'directory' ? DIRECTORY_MODE : FILE_MODE;
61
+ if ((st.mode & 0o777 & ~allowed) !== 0)
62
+ return `${label} has permissions wider than ${kind === 'directory' ? '0700' : '0600'}`;
63
+ return null;
64
+ }
65
+ function listHandles(dir, connector) {
66
+ const refuse = (problem) => ({ ok: false, problem: `connector ${connector}: ${problem}` });
67
+ try {
68
+ lstatSync(dir);
69
+ }
70
+ catch (e) {
71
+ if (e.code === 'ENOENT')
72
+ return { ok: true, handles: [] };
73
+ return refuse('the secrets directory cannot be read');
74
+ }
75
+ const dirProblem = entryProblem(dir, 'directory', 'the secrets directory');
76
+ if (dirProblem !== null)
77
+ return refuse(dirProblem);
78
+ let names;
79
+ try {
80
+ names = readdirSync(dir).sort();
81
+ }
82
+ catch {
83
+ return refuse('the secrets directory cannot be listed');
84
+ }
85
+ const handles = [];
86
+ for (const name of names) {
87
+ if (!HANDLE_NAME.test(name))
88
+ continue;
89
+ const problem = entryProblem(join(dir, name), 'file', `secret file ${name}`);
90
+ if (problem !== null)
91
+ return refuse(problem);
92
+ const expiryFile = name + EXPIRY_SUFFIX;
93
+ if (!names.includes(expiryFile)) {
94
+ handles.push({ connector, name, source: 'file' });
95
+ continue;
96
+ }
97
+ const expiryProblem = entryProblem(join(dir, expiryFile), 'file', `expiry file ${expiryFile}`);
98
+ if (expiryProblem !== null)
99
+ return refuse(expiryProblem);
100
+ let expiresAt;
101
+ try {
102
+ expiresAt = JSON.parse(readFileSync(join(dir, expiryFile), 'utf8')).expiresAt;
103
+ }
104
+ catch {
105
+ expiresAt = undefined;
106
+ }
107
+ if (!validExpiry(expiresAt))
108
+ return refuse(`expiry file ${expiryFile} does not hold {"expiresAt": "<RFC 3339 UTC time>"}`);
109
+ handles.push({ connector, name, source: 'file', expiresAt });
110
+ }
111
+ return { ok: true, handles };
112
+ }
113
+ function writePrivately(path, text) {
114
+ const tmp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString('hex')}.tmp`);
115
+ try {
116
+ const fd = openSync(tmp, 'wx', FILE_MODE);
117
+ try {
118
+ fchmodSync(fd, FILE_MODE);
119
+ writeSync(fd, text);
120
+ fsyncSync(fd);
121
+ }
122
+ finally {
123
+ closeSync(fd);
124
+ }
125
+ renameSync(tmp, path);
126
+ }
127
+ catch (e) {
128
+ rmSync(tmp, { force: true });
129
+ throw e;
130
+ }
131
+ }
132
+ function putHandle(dir, connector, name, value, options) {
133
+ if (!HANDLE_NAME.test(name))
134
+ throw new Error('a handle name is upper-case letters, digits and underscores, starting with a letter');
135
+ if (typeof value !== 'string')
136
+ throw new Error('a secret value is a string');
137
+ if (options.expiresAt !== undefined && !validExpiry(options.expiresAt))
138
+ throw new Error('expiresAt must be an RFC 3339 UTC time ending in Z');
139
+ mkdirSync(dir, { recursive: true, mode: DIRECTORY_MODE });
140
+ const dirProblem = entryProblem(dir, 'directory', 'the secrets directory');
141
+ if (dirProblem !== null)
142
+ throw new Error(`connector ${connector}: ${dirProblem}`);
143
+ writePrivately(join(dir, name), value);
144
+ const expiryFile = join(dir, name + EXPIRY_SUFFIX);
145
+ if (options.expiresAt === undefined) {
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 };
151
+ }
152
+ export function openFileSecrets(options) {
153
+ if (typeof options?.configDir !== 'string' || !isAbsolute(options.configDir))
154
+ throw new Error('configDir must be an absolute path');
155
+ if (typeof options.connector !== 'string' || !CONNECTOR_NAME.test(options.connector))
156
+ throw new Error('connector must be lowercase letters and digits in hyphen-separated words');
157
+ const { connector } = options;
158
+ const dir = handleDirectory(options.configDir, connector);
159
+ return {
160
+ list: () => listHandles(dir, connector),
161
+ put: (name, value, putOptions = {}) => putHandle(dir, connector, name, value, putOptions),
162
+ };
163
+ }
@@ -0,0 +1,59 @@
1
+ import type { ConnectorErrorCode, ConnectorMintedStatus, ConnectorReceipt } from '@sema-agent/connectors-contract';
2
+ import type { Principal } from './principal.js';
3
+ export type JsonRpcId = string | number;
4
+ export type Action = {
5
+ readonly to: 'upstream' | 'connector';
6
+ readonly data: Buffer | string;
7
+ } | {
8
+ readonly to: 'receipt';
9
+ readonly receipt: ConnectorReceipt;
10
+ };
11
+ export declare const INSPECT_LIMIT_BYTES: number;
12
+ export declare const INVALID_REQUEST = -32600;
13
+ export declare const INTERNAL_ERROR = -32603;
14
+ export declare const STATUS_BEFORE_INITIALIZE = "the status is not available before the connector has answered initialize";
15
+ export type Message = Record<string, unknown>;
16
+ export interface SessionOptions {
17
+ readonly connector: string;
18
+ readonly status: (tools: readonly string[]) => ConnectorMintedStatus;
19
+ readonly ownIdPrefix: string;
20
+ readonly withheld: (line: Buffer, parsed: unknown) => boolean;
21
+ }
22
+ export declare const isId: (v: unknown) => v is JsonRpcId;
23
+ export declare const idKey: (id: JsonRpcId) => string;
24
+ export type Reading = {
25
+ readonly kind: 'message';
26
+ readonly m: Message;
27
+ } | {
28
+ readonly kind: 'unreadable';
29
+ } | {
30
+ readonly kind: 'other';
31
+ readonly value?: unknown;
32
+ };
33
+ export declare function read(raw: Buffer): Reading;
34
+ export declare const errorLine: (id: JsonRpcId | null, code: number, message: string, dataCode: ConnectorErrorCode) => string;
35
+ export declare class Session {
36
+ private readonly options;
37
+ private readonly uri;
38
+ private readonly pending;
39
+ private readonly own;
40
+ private readonly retired;
41
+ private capabilities;
42
+ private ownCount;
43
+ constructor(options: SessionOptions);
44
+ fromUpstream(raw: Buffer, principal: Principal): Action[];
45
+ statusFor(id: JsonRpcId, principal: Principal): Action[];
46
+ upstreamOversized(): Action[];
47
+ fromConnector(raw: Buffer): Action[];
48
+ connectorOversized(): Action[];
49
+ private connectorUnreadable;
50
+ connectorGone(how: string, abnormal: boolean): Action[];
51
+ private forward;
52
+ private settle;
53
+ private settleReply;
54
+ private visibleTools;
55
+ private rewrite;
56
+ private readStatus;
57
+ private askTools;
58
+ private statusReply;
59
+ }
@@ -0,0 +1,256 @@
1
+ import { hasRepeatedKey } from './json-text.js';
2
+ import { normalizeTool } from './normalize.js';
3
+ import { REDACTED } from './redact.js';
4
+ import { isGranted } from './scope.js';
5
+ import { STATUS_MIME_TYPE, statusResource, statusUri } from './status.js';
6
+ export const INSPECT_LIMIT_BYTES = 4 * 1024 * 1024;
7
+ const LIMIT_TEXT = `${INSPECT_LIMIT_BYTES / (1024 * 1024)} MiB`;
8
+ const MAX_STATUS_PAGES = 64;
9
+ const FORBIDDEN_SCOPE = 'connector.forbidden_scope';
10
+ const INTERNAL = 'connector.internal';
11
+ export const INVALID_REQUEST = -32600;
12
+ const INVALID_PARAMS = -32602;
13
+ export const INTERNAL_ERROR = -32603;
14
+ const UNREADABLE_REQUEST = `the launcher forwards only single JSON-RPC messages of at most ${LIMIT_TEXT}, on one line and without repeated keys, which it can check against the granted scope`;
15
+ const WITHHELD = 'the connector wrote one of its secret values into this reply, so the launcher withheld it';
16
+ const NO_ID = 'the launcher forwards a request only with a string or number id, and needs one on the requests whose replies it rewrites or answers';
17
+ export const STATUS_BEFORE_INITIALIZE = 'the status is not available before the connector has answered initialize';
18
+ const TAKEN_ID = 'the connector may still answer an earlier request with this id (it has not answered it yet, or the launcher answered it after a line it could not read); use another id';
19
+ const UNREADABLE_REPLY = `the connector wrote a message the launcher cannot read (over ${LIMIT_TEXT}, a batch, or a repeated key), which may have been this reply`;
20
+ const ERROR_CODES = new Set(Object.keys({
21
+ 'connector.auth_required': 0,
22
+ 'connector.auth_expired': 0,
23
+ 'connector.forbidden_scope': 0,
24
+ 'connector.upstream_unreachable': 0,
25
+ 'connector.bad_assertion': 0,
26
+ 'connector.internal': 0,
27
+ 'connector.not_installed': 0,
28
+ }));
29
+ const NEWLINE = Buffer.from('\n');
30
+ const LINE_FEED = 0x0a;
31
+ const CARRIAGE_RETURN = 0x0d;
32
+ export const isId = (v) => typeof v === 'string' || typeof v === 'number';
33
+ export const idKey = (id) => `${typeof id}:${String(id)}`;
34
+ const asObject = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) ? v : null);
35
+ export function read(raw) {
36
+ const text = raw.toString('utf8');
37
+ let value;
38
+ try {
39
+ value = JSON.parse(text);
40
+ }
41
+ catch {
42
+ return { kind: 'other' };
43
+ }
44
+ if (Array.isArray(value))
45
+ return { kind: 'unreadable' };
46
+ const m = asObject(value);
47
+ if (m === null)
48
+ return { kind: 'other', value };
49
+ return hasRepeatedKey(text) ? { kind: 'unreadable' } : { kind: 'message', m };
50
+ }
51
+ const lineOf = (m) => JSON.stringify(m) + '\n';
52
+ export const errorLine = (id, code, message, dataCode) => lineOf({ jsonrpc: '2.0', id, error: { code, message, data: { code: dataCode } } });
53
+ const up = (data) => ({ to: 'upstream', data });
54
+ const down = (data) => ({ to: 'connector', data });
55
+ const same = (raw) => Buffer.concat([raw, NEWLINE]);
56
+ export class Session {
57
+ options;
58
+ uri;
59
+ pending = new Map();
60
+ own = new Map();
61
+ retired = new Set();
62
+ capabilities = null;
63
+ ownCount = 0;
64
+ constructor(options) {
65
+ this.options = options;
66
+ this.uri = statusUri(options.connector);
67
+ }
68
+ fromUpstream(raw, principal) {
69
+ const startedAt = new Date().toISOString();
70
+ const started = performance.now();
71
+ const grant = principal.scope;
72
+ const body = raw[raw.length - 1] === CARRIAGE_RETURN ? raw.subarray(0, -1) : raw;
73
+ if (body.includes(LINE_FEED) || body.includes(CARRIAGE_RETURN))
74
+ return [up(errorLine(null, INVALID_REQUEST, UNREADABLE_REQUEST, FORBIDDEN_SCOPE))];
75
+ const r = read(raw);
76
+ if (r.kind !== 'message')
77
+ return [up(errorLine(null, INVALID_REQUEST, UNREADABLE_REQUEST, FORBIDDEN_SCOPE))];
78
+ const m = r.m;
79
+ if (typeof m.method !== 'string')
80
+ return [down(same(raw))];
81
+ const params = asObject(m.params);
82
+ const name = params?.name;
83
+ const call = m.method === 'tools/call' && typeof name === 'string' && name !== '' ? { tool: name, startedAt, started } : undefined;
84
+ if ('id' in m && !isId(m.id))
85
+ return [up(errorLine(null, INVALID_REQUEST, NO_ID, FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
86
+ const id = isId(m.id) ? m.id : null;
87
+ if (id !== null && (this.pending.has(idKey(id)) || this.retired.has(idKey(id))))
88
+ return [up(errorLine(id, INVALID_REQUEST, TAKEN_ID, FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
89
+ const needsId = () => [up(errorLine(null, INVALID_REQUEST, NO_ID, FORBIDDEN_SCOPE))];
90
+ switch (m.method) {
91
+ case 'tools/call': {
92
+ if (typeof name !== 'string' || !isGranted(grant, name))
93
+ return [up(errorLine(id, INVALID_PARAMS, 'the requested tool is not in the granted scope', FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
94
+ return this.forward(raw, id, grant, undefined, undefined, call);
95
+ }
96
+ case 'resources/read':
97
+ if (params?.uri !== this.uri)
98
+ return this.forward(raw, id, grant);
99
+ return id === null ? needsId() : this.readStatus(id, grant);
100
+ case 'resources/list': {
101
+ if (id === null)
102
+ return needsId();
103
+ const firstPage = params?.cursor === undefined;
104
+ if (this.capabilities !== null && asObject(this.capabilities.resources) === null) {
105
+ return [up(lineOf({ jsonrpc: '2.0', id, result: { resources: firstPage ? [statusResource(this.options.connector)] : [] } }))];
106
+ }
107
+ return this.forward(raw, id, grant, 'resources/list', firstPage);
108
+ }
109
+ case 'initialize':
110
+ case 'tools/list':
111
+ return id === null ? needsId() : this.forward(raw, id, grant, m.method);
112
+ default:
113
+ return this.forward(raw, id, grant);
114
+ }
115
+ }
116
+ statusFor(id, principal) {
117
+ return this.readStatus(id, principal.scope);
118
+ }
119
+ upstreamOversized() {
120
+ return [up(errorLine(null, INVALID_REQUEST, UNREADABLE_REQUEST, FORBIDDEN_SCOPE))];
121
+ }
122
+ fromConnector(raw) {
123
+ const r = read(raw);
124
+ if (r.kind === 'unreadable')
125
+ return this.connectorUnreadable();
126
+ const withheld = this.options.withheld(raw, r.kind === 'message' ? r.m : r.value);
127
+ const pass = () => (withheld ? [] : [up(same(raw))]);
128
+ if (r.kind === 'other')
129
+ return pass();
130
+ const m = r.m;
131
+ if (m.method !== undefined)
132
+ return pass();
133
+ if (!isId(m.id) || !('result' in m || 'error' in m))
134
+ return pass();
135
+ const key = idKey(m.id);
136
+ if (this.retired.delete(key))
137
+ return [];
138
+ const own = this.own.get(key);
139
+ if (own !== undefined) {
140
+ const actions = withheld ? [up(errorLine(own.upstreamId, INTERNAL_ERROR, WITHHELD, INTERNAL))] : own.handle(m);
141
+ this.own.delete(key);
142
+ return actions;
143
+ }
144
+ const p = this.pending.get(key);
145
+ if (p === undefined)
146
+ return [];
147
+ const data = withheld ? errorLine(p.id, INTERNAL_ERROR, WITHHELD, INTERNAL) : p.rewrite === undefined || !('result' in m) ? same(raw) : this.rewrite(p, m, raw);
148
+ this.pending.delete(key);
149
+ return [up(data), ...(withheld ? this.settle(p.call, 'failed', INTERNAL) : this.settleReply(p.call, m))];
150
+ }
151
+ connectorOversized() {
152
+ return this.connectorUnreadable();
153
+ }
154
+ connectorUnreadable() {
155
+ const actions = [
156
+ ...[...this.pending.values()].flatMap((p) => [up(errorLine(p.id, INTERNAL_ERROR, UNREADABLE_REPLY, INTERNAL)), ...this.settle(p.call, 'failed', INTERNAL)]),
157
+ ...[...this.own.values()].map((o) => up(errorLine(o.upstreamId, INTERNAL_ERROR, UNREADABLE_REPLY, INTERNAL))),
158
+ ];
159
+ for (const key of this.pending.keys())
160
+ this.retired.add(key);
161
+ this.pending.clear();
162
+ this.own.clear();
163
+ return actions;
164
+ }
165
+ connectorGone(how, abnormal) {
166
+ const actions = [
167
+ ...[...this.pending.values()].flatMap((p) => [up(errorLine(p.id, INTERNAL_ERROR, `${how} before it answered`, INTERNAL)), ...this.settle(p.call, 'failed', INTERNAL)]),
168
+ ...[...this.own.values()].map((o) => up(errorLine(o.upstreamId, INTERNAL_ERROR, 'the connector ended before it listed its tools for the status', INTERNAL))),
169
+ ];
170
+ if (abnormal && this.pending.size === 0)
171
+ actions.push(up(errorLine(null, INTERNAL_ERROR, how, INTERNAL)));
172
+ this.pending.clear();
173
+ this.own.clear();
174
+ return actions;
175
+ }
176
+ forward(raw, id, grant, rewrite, firstPage, call) {
177
+ if (id !== null)
178
+ this.pending.set(idKey(id), { id, grant, ...(rewrite !== undefined ? { rewrite } : {}), ...(firstPage !== undefined ? { firstPage } : {}), ...(call !== undefined ? { call } : {}) });
179
+ return [down(same(raw))];
180
+ }
181
+ settle(call, outcome, errorCode) {
182
+ if (call === undefined)
183
+ return [];
184
+ const durationMs = Math.max(0, Math.round(performance.now() - call.started));
185
+ const tool = this.options.withheld(Buffer.from(call.tool, 'utf8'), call.tool) ? REDACTED : call.tool;
186
+ return [{ to: 'receipt', receipt: { connector: this.options.connector, tool, startedAt: call.startedAt, durationMs, outcome, ...(errorCode !== undefined ? { errorCode } : {}) } }];
187
+ }
188
+ settleReply(call, m) {
189
+ if ('error' in m) {
190
+ const code = asObject(asObject(m.error)?.data)?.code;
191
+ return this.settle(call, 'failed', typeof code === 'string' && ERROR_CODES.has(code) ? code : undefined);
192
+ }
193
+ return this.settle(call, asObject(m.result)?.isError === true ? 'failed' : 'ok');
194
+ }
195
+ visibleTools(tools, grant) {
196
+ return tools.flatMap((t) => {
197
+ const tool = asObject(t);
198
+ return tool !== null && typeof tool.name === 'string' && isGranted(grant, tool.name) ? [normalizeTool(tool)] : [];
199
+ });
200
+ }
201
+ rewrite(p, m, raw) {
202
+ const result = asObject(m.result);
203
+ if (p.rewrite === 'initialize') {
204
+ const capabilities = result === null ? null : asObject(result.capabilities);
205
+ if (result === null || capabilities === null)
206
+ return errorLine(p.id, INTERNAL_ERROR, 'the connector answered initialize without capabilities', INTERNAL);
207
+ this.capabilities = capabilities;
208
+ if (asObject(capabilities.resources) !== null)
209
+ return same(raw);
210
+ return lineOf({ ...m, result: { ...result, capabilities: { ...capabilities, resources: {} } } });
211
+ }
212
+ if (p.rewrite === 'tools/list') {
213
+ const tools = result?.tools;
214
+ if (result === null || !Array.isArray(tools))
215
+ return errorLine(p.id, INTERNAL_ERROR, 'the connector answered tools/list without a list of tools', INTERNAL);
216
+ return lineOf({ ...m, result: { ...result, tools: this.visibleTools(tools, p.grant) } });
217
+ }
218
+ const resources = result?.resources;
219
+ if (result === null || !Array.isArray(resources))
220
+ return errorLine(p.id, INTERNAL_ERROR, 'the connector answered resources/list without a list of resources', INTERNAL);
221
+ const kept = resources.filter((r) => asObject(r)?.uri !== this.uri);
222
+ return lineOf({ ...m, result: { ...result, resources: p.firstPage ? [...kept, statusResource(this.options.connector)] : kept } });
223
+ }
224
+ readStatus(id, grant) {
225
+ if (this.capabilities === null)
226
+ return [up(errorLine(id, INTERNAL_ERROR, STATUS_BEFORE_INITIALIZE, INTERNAL))];
227
+ if (asObject(this.capabilities.tools) === null)
228
+ return [up(this.statusReply(id, []))];
229
+ return this.askTools(id, undefined, [], 1, grant);
230
+ }
231
+ askTools(upstreamId, cursor, names, page, grant) {
232
+ const ownId = `${this.options.ownIdPrefix}/${++this.ownCount}`;
233
+ this.own.set(idKey(ownId), {
234
+ upstreamId,
235
+ handle: (reply) => {
236
+ const result = asObject(reply.result);
237
+ const tools = result?.tools;
238
+ if (result === null || !Array.isArray(tools))
239
+ return [up(this.statusReply(upstreamId, []))];
240
+ for (const t of this.visibleTools(tools, grant))
241
+ names.push(t.name);
242
+ const next = result.nextCursor;
243
+ if (typeof next !== 'string' || next === '')
244
+ return [up(this.statusReply(upstreamId, names))];
245
+ if (page >= MAX_STATUS_PAGES)
246
+ return [up(errorLine(upstreamId, INTERNAL_ERROR, `the connector listed its tools in more than ${MAX_STATUS_PAGES} pages`, INTERNAL))];
247
+ return this.askTools(upstreamId, next, names, page + 1, grant);
248
+ },
249
+ });
250
+ return [down(lineOf({ jsonrpc: '2.0', id: ownId, method: 'tools/list', ...(cursor !== undefined ? { params: { cursor } } : {}) }))];
251
+ }
252
+ statusReply(id, tools) {
253
+ const text = JSON.stringify(this.options.status(tools));
254
+ return lineOf({ jsonrpc: '2.0', id, result: { contents: [{ uri: this.uri, mimeType: STATUS_MIME_TYPE, text }] } });
255
+ }
256
+ }
@@ -0,0 +1,12 @@
1
+ import type { ConnectorMintedStatus } from '@sema-agent/connectors-contract';
2
+ import type { SecretHandle } from './secrets.js';
3
+ export declare const STATUS_MIME_TYPE = "application/json";
4
+ export declare const AUTHORIZE_URL_PATTERN: RegExp;
5
+ export declare function statusUri(connector: string): string;
6
+ export declare function statusResource(connector: string): Record<string, string>;
7
+ export declare function mintStatus(input: {
8
+ readonly handles: readonly SecretHandle[];
9
+ readonly nowMs: number;
10
+ readonly tools: readonly string[];
11
+ readonly authorizeUrl?: string;
12
+ }): ConnectorMintedStatus;
package/dist/status.js ADDED
@@ -0,0 +1,19 @@
1
+ export const STATUS_MIME_TYPE = 'application/json';
2
+ export const AUTHORIZE_URL_PATTERN = /^https?:\/\//;
3
+ export function statusUri(connector) {
4
+ return `sema-connector://${connector}/status`;
5
+ }
6
+ export function statusResource(connector) {
7
+ return {
8
+ uri: statusUri(connector),
9
+ name: 'status',
10
+ title: 'Connector status',
11
+ description: 'Authorization state and visible tools of this connector, as connector-status-v1 JSON.',
12
+ mimeType: STATUS_MIME_TYPE,
13
+ };
14
+ }
15
+ export function mintStatus(input) {
16
+ const state = input.handles.length === 0 ? 'unauthorized' : input.handles.some((h) => h.expiresAt !== undefined && Date.parse(h.expiresAt) <= input.nowMs) ? 'expired' : 'authorized';
17
+ const link = state === 'unauthorized' && input.authorizeUrl !== undefined && AUTHORIZE_URL_PATTERN.test(input.authorizeUrl) ? { authorizeUrl: input.authorizeUrl } : {};
18
+ return { state, tools: [...input.tools], ...link };
19
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@sema-agent/connectors",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Shared connector layer and the sema-connector launcher (MCP stdio proxy with principal, scope, normalize, status and trace stages).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "sema-connector": "bin/sema-connector.mjs"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "bin",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json"
26
+ },
27
+ "dependencies": {},
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/sema-agent/sema-connectors.git",
34
+ "directory": "packages/core"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.