@cotal-ai/connector-core 0.8.1 → 0.8.2
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/dist/control.d.ts +21 -2
- package/dist/control.d.ts.map +1 -1
- package/dist/control.js +81 -9
- package/dist/control.js.map +1 -1
- package/dist/launch.d.ts +9 -3
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +46 -13
- package/dist/launch.js.map +1 -1
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +17 -5
- package/dist/relay.js.map +1 -1
- package/dist/runtime.d.ts +21 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +25 -10
- package/dist/runtime.js.map +1 -1
- package/package.json +2 -2
package/dist/control.d.ts
CHANGED
|
@@ -17,8 +17,27 @@ export interface HookEvent {
|
|
|
17
17
|
}
|
|
18
18
|
/** Maps one hook event to the JSON reply the runtime applies. */
|
|
19
19
|
export type HookHandle = (agent: MeshAgent, ev: HookEvent) => Promise<Record<string, unknown>>;
|
|
20
|
+
export interface ControlServerOpts {
|
|
21
|
+
/** Fail loud on a bind we can't hold. A managed listener (the in-agent MCP server, the Hermes
|
|
22
|
+
* sidecar) MUST own its endpoint: if `listen` errors (e.g. a squatter already holds the win32
|
|
23
|
+
* pipe → `EADDRINUSE`; libuv binds with `FILE_FLAG_FIRST_PIPE_INSTANCE`), the process exits
|
|
24
|
+
* rather than running on with a hijacked or no-op control plane. Default off (an ad-hoc/test
|
|
25
|
+
* server logs and stays up). */
|
|
26
|
+
fatalBind?: boolean;
|
|
27
|
+
/** Cooperative shutdown: invoked on an AUTHENTICATED `{op:"shutdown"}` frame. The connector runs
|
|
28
|
+
* its own clean teardown (close the server, `agent.stop()` to leave the mesh, then exit) — the
|
|
29
|
+
* server never owns `process.exit`. Absent → shutdown frames are accepted + acked but inert. */
|
|
30
|
+
onShutdown?: () => void;
|
|
31
|
+
}
|
|
20
32
|
/** The context block injected into a turn when peer messages are waiting (else undefined). */
|
|
21
33
|
export declare function formatInjection(items: InboxItem[]): string | undefined;
|
|
22
|
-
/** Start the control
|
|
23
|
-
|
|
34
|
+
/** Start the authenticated control server. One newline-delimited JSON {@link ControlFrame} → one
|
|
35
|
+
* reply per connection. The first thing every connection does is validate its `token` against the
|
|
36
|
+
* endpoint's (constant-time) — a mismatch is dropped before `handle` (or `onShutdown`) ever runs,
|
|
37
|
+
* so an unauthenticated local process that finds/guesses the path still can't drive presence,
|
|
38
|
+
* inject peer messages, or shut the agent down. */
|
|
39
|
+
export declare function startControlServer(agent: MeshAgent, endpoint: {
|
|
40
|
+
path: string;
|
|
41
|
+
token: string;
|
|
42
|
+
}, handle: HookHandle, opts?: ControlServerOpts): Server;
|
|
24
43
|
//# sourceMappingURL=control.d.ts.map
|
package/dist/control.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAGrD,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvD,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAU/F,MAAM,WAAW,iBAAiB;IAChC;;;;qCAIiC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;qGAEiG;IACjG,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AA+BD,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS,CAKtE;AAED;;;;oDAIoD;AACpD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACzC,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,iBAAsB,GAC3B,MAAM,CAkFR"}
|
package/dist/control.js
CHANGED
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createServer } from "node:net";
|
|
12
12
|
import { existsSync, unlinkSync } from "node:fs";
|
|
13
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
14
|
+
/** Hard cap on the first (only) frame: a control request is a token + one small lifecycle event —
|
|
15
|
+
* kilobytes at most. Generous headroom for a legit event, tiny next to the ~512MB string limit an
|
|
16
|
+
* unauthenticated spewer would otherwise drive `buf` toward to crash the process. */
|
|
17
|
+
const MAX_FRAME_BYTES = 1 << 20; // 1 MiB
|
|
18
|
+
/** ABSOLUTE deadline (not an idle timeout — a slow-loris dribbling one byte at a time would keep
|
|
19
|
+
* resetting an idle timer) for a connection to deliver its complete auth frame. Past it, an
|
|
20
|
+
* unauthenticated connection is dropped so a local process can't camp on a finite pipe instance.
|
|
21
|
+
* Cleared the instant a full frame is in hand (the token-bearing client then owns the connection). */
|
|
22
|
+
const AUTH_DEADLINE_MS = 5_000;
|
|
23
|
+
/** Constant-time match of a presented token against the endpoint's. Both sides are SHA-256'd first
|
|
24
|
+
* so the compare is fixed-length (and length-independent) regardless of the presented value — a
|
|
25
|
+
* non-string or wrong-length token can never throw `timingSafeEqual` or leak length via timing. */
|
|
26
|
+
function tokenMatches(presented, digest) {
|
|
27
|
+
if (typeof presented !== "string")
|
|
28
|
+
return false;
|
|
29
|
+
return timingSafeEqual(createHash("sha256").update(presented).digest(), digest);
|
|
30
|
+
}
|
|
13
31
|
function who(i) {
|
|
14
32
|
return i.fromRole ? `${i.fromName}/${i.fromRole}` : i.fromName;
|
|
15
33
|
}
|
|
@@ -29,11 +47,21 @@ export function formatInjection(items) {
|
|
|
29
47
|
const tail = `(Reply with cotal_send / cotal_dm, or cotal_roster to see who's here.)`;
|
|
30
48
|
return `${head}\n${items.map(fmtItem).join("\n")}\n${tail}`;
|
|
31
49
|
}
|
|
32
|
-
/** Start the control
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
/** Start the authenticated control server. One newline-delimited JSON {@link ControlFrame} → one
|
|
51
|
+
* reply per connection. The first thing every connection does is validate its `token` against the
|
|
52
|
+
* endpoint's (constant-time) — a mismatch is dropped before `handle` (or `onShutdown`) ever runs,
|
|
53
|
+
* so an unauthenticated local process that finds/guesses the path still can't drive presence,
|
|
54
|
+
* inject peer messages, or shut the agent down. */
|
|
55
|
+
export function startControlServer(agent, endpoint, handle, opts = {}) {
|
|
56
|
+
const { path } = endpoint;
|
|
57
|
+
const digest = createHash("sha256").update(endpoint.token).digest();
|
|
58
|
+
// Stale-socket cleanup is POSIX-only: a win32 named pipe is not a filesystem entry to unlink, and
|
|
59
|
+
// a live one there is a SQUATTER the fatal `EADDRINUSE` is meant to catch — never clear it. (With
|
|
60
|
+
// a token-random path a stale POSIX socket from a dead predecessor is itself near-impossible, but
|
|
61
|
+
// the unlink stays as cheap insurance against an exact-path leftover.)
|
|
62
|
+
if (process.platform !== "win32" && existsSync(path)) {
|
|
35
63
|
try {
|
|
36
|
-
unlinkSync(
|
|
64
|
+
unlinkSync(path);
|
|
37
65
|
}
|
|
38
66
|
catch {
|
|
39
67
|
/* ignore */
|
|
@@ -41,19 +69,52 @@ export function startControlServer(agent, socketPath, handle) {
|
|
|
41
69
|
}
|
|
42
70
|
const server = createServer((sock) => {
|
|
43
71
|
let buf = "";
|
|
72
|
+
let handled = false; // one frame per connection — ignore anything after the first line
|
|
44
73
|
sock.setEncoding("utf8");
|
|
74
|
+
// Bound an UNAUTHENTICATED peer: on Windows the named pipe's default DACL lets any local process
|
|
75
|
+
// connect, so a client that streams bytes with no newline (would grow `buf` toward the ~512MB
|
|
76
|
+
// string limit → crashes this long-lived process), that connects and sends nothing, OR that
|
|
77
|
+
// dribbles a byte at a time (a slow-loris) to camp on a finite pipe instance must be cut off
|
|
78
|
+
// BEFORE auth. MAX_FRAME_BYTES caps a spewing one; an ABSOLUTE deadline (reset-proof, unlike an
|
|
79
|
+
// idle timeout) reaps a silent/slow one. A legit client sends one small line immediately.
|
|
80
|
+
const deadline = setTimeout(() => sock.destroy(), AUTH_DEADLINE_MS);
|
|
81
|
+
deadline.unref?.(); // never hold the process open on an unauthenticated connection
|
|
82
|
+
sock.on("close", () => clearTimeout(deadline));
|
|
45
83
|
sock.on("data", async (d) => {
|
|
84
|
+
if (handled)
|
|
85
|
+
return;
|
|
46
86
|
buf += d;
|
|
87
|
+
if (buf.length > MAX_FRAME_BYTES) {
|
|
88
|
+
sock.destroy(); // oversized pre-newline — drop hard, never half-close (it keeps spewing)
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
47
91
|
const nl = buf.indexOf("\n");
|
|
48
92
|
if (nl < 0)
|
|
49
93
|
return; // wait for the full line
|
|
50
|
-
|
|
94
|
+
handled = true;
|
|
95
|
+
clearTimeout(deadline); // full frame in hand — the token-bearing client now owns the connection
|
|
96
|
+
let frame = {};
|
|
51
97
|
try {
|
|
52
|
-
|
|
98
|
+
frame = JSON.parse(buf.slice(0, nl) || "{}");
|
|
53
99
|
}
|
|
54
100
|
catch {
|
|
55
|
-
/* malformed —
|
|
101
|
+
/* malformed — fails the auth check below and is dropped */
|
|
102
|
+
}
|
|
103
|
+
if (!tokenMatches(frame.token, digest)) {
|
|
104
|
+
sock.destroy(); // unauthenticated — drop hard before handle/onShutdown (no half-open)
|
|
105
|
+
return;
|
|
56
106
|
}
|
|
107
|
+
if (frame.op === "shutdown") {
|
|
108
|
+
try {
|
|
109
|
+
sock.end(JSON.stringify({ ok: true }) + "\n");
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* client gone */
|
|
113
|
+
}
|
|
114
|
+
opts.onShutdown?.();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const ev = (frame.event ?? {});
|
|
57
118
|
const reply = await handle(agent, ev);
|
|
58
119
|
try {
|
|
59
120
|
sock.end(JSON.stringify(reply) + "\n");
|
|
@@ -66,8 +127,19 @@ export function startControlServer(agent, socketPath, handle) {
|
|
|
66
127
|
/* ignore client errors */
|
|
67
128
|
});
|
|
68
129
|
});
|
|
69
|
-
|
|
70
|
-
server.
|
|
130
|
+
let bound = false;
|
|
131
|
+
server.on("error", (e) => {
|
|
132
|
+
process.stderr.write(`[cotal-connector] control server error: ${e.message}\n`);
|
|
133
|
+
// A bind we never held (listen errored before "listening", e.g. EADDRINUSE from a squatter) is
|
|
134
|
+
// fatal for a managed listener — better to die than serve a hijacked/no-op control plane. A
|
|
135
|
+
// post-bind error is just logged.
|
|
136
|
+
if (opts.fatalBind && !bound)
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
139
|
+
server.listen(path, () => {
|
|
140
|
+
bound = true;
|
|
141
|
+
process.stderr.write(`[cotal-connector] control socket: ${path}\n`); // path is leakage-safe; the token never logs
|
|
142
|
+
});
|
|
71
143
|
return server;
|
|
72
144
|
}
|
|
73
145
|
//# sourceMappingURL=control.js.map
|
package/dist/control.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.js","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"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"}
|
package/dist/launch.d.ts
CHANGED
|
@@ -23,9 +23,15 @@ import type { McpServerSpec } from "@cotal-ai/core";
|
|
|
23
23
|
export declare const MODEL_PROVIDER_KEYS: readonly ["OPENCODE_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "NOUS_API_KEY"];
|
|
24
24
|
/** Build the base env a spawned agent runs with: the OS allow-list plus any named keys the
|
|
25
25
|
* connector declares the agent needs — `providerKeys` (the model-provider key) and `mcpKeys`
|
|
26
|
-
* (the `${VAR}` secrets a shared MCP server references, see {@link mcpServerEnvKeys}). Every
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* (the `${VAR}` secrets a shared MCP server references, see {@link mcpServerEnvKeys}). Every entry
|
|
27
|
+
* is copied from the manager's env BY NAME and only when present — never required, never spread
|
|
28
|
+
* wholesale, so the operator's unrelated secrets don't bleed into the child (P3).
|
|
29
|
+
*
|
|
30
|
+
* Matching is CASE-INSENSITIVE and each value is copied under the OS's OWN key casing: Windows
|
|
31
|
+
* spells these `Path`/`ComSpec`/`windir`, so a canonical-only copy would either miss them (a plain
|
|
32
|
+
* read of `process.env.SystemRoot` differs from `process.env.systemroot`) or, worse, emit BOTH
|
|
33
|
+
* `Path` and `PATH` — a case-duplicate Windows process creation chokes on. Keying off the source
|
|
34
|
+
* env's actual casing (one entry per lowercased name) forwards each var exactly once. */
|
|
29
35
|
export declare function launchEnv(opts?: {
|
|
30
36
|
providerKeys?: readonly string[];
|
|
31
37
|
mcpKeys?: readonly string[];
|
package/dist/launch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AA0DpD;;;;+EAI+E;AAC/E,eAAO,MAAM,mBAAmB,4GAMtB,CAAC;AAEX;;;;;;;;;;0FAU0F;AAC1F,wBAAgB,SAAS,CACvB,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GAC3E,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAexB;AAED;;;;;mFAKmF;AACnF,wBAAgB,MAAM,CAAC,IAAI,EAAE;IAC3B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMzB;AAED;;;;kEAIkE;AAClE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,MAAM,EAAE,CAcjF"}
|
package/dist/launch.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
* COLORTERM), resolve home/config/data roots (HOME / XDG_*_HOME on Unix,
|
|
3
3
|
* USERPROFILE / APPDATA / LOCALAPPDATA on Windows), locale (LANG / LC_*), timezone (TZ), temp
|
|
4
4
|
* dirs, session/runtime dir (XDG_RUNTIME_DIR), and the shell it may invoke. NOT a model key,
|
|
5
|
-
* NOT an operator secret. A fixed, named allow-list
|
|
5
|
+
* NOT an operator secret. A fixed, named allow-list; each entry is forwarded only when present,
|
|
6
|
+
* so the Unix-only and Windows-only names below coexist harmlessly on either OS. Names are matched
|
|
7
|
+
* case-insensitively against the source env and copied under the source's own key (see
|
|
8
|
+
* {@link launchEnv}), so Windows casing (`Path`, `ComSpec`, `windir`) is forwarded without ever
|
|
9
|
+
* emitting a case-duplicate (`Path` AND `PATH`) that Windows process creation would choke on. */
|
|
6
10
|
const OS_ENV_ALLOW = [
|
|
7
11
|
"PATH",
|
|
8
12
|
"HOME",
|
|
@@ -32,6 +36,22 @@ const OS_ENV_ALLOW = [
|
|
|
32
36
|
"APPDATA",
|
|
33
37
|
"LOCALAPPDATA",
|
|
34
38
|
"XDG_RUNTIME_DIR",
|
|
39
|
+
// Windows system env. SystemRoot is mandatory: without it a spawned process aborts at startup
|
|
40
|
+
// (node `InitializeOnce`, winsock/ICU can't load) — and a `pty`-runtime (ConPTY) child does NOT
|
|
41
|
+
// inherit it the way a plain child_process does, so a manager-spawned agent dies before its first
|
|
42
|
+
// line. The rest let agents resolve the system drive, arch, and Program/Data roots they shell out
|
|
43
|
+
// to. Absent on POSIX (skipped); present only on Windows.
|
|
44
|
+
"SystemRoot",
|
|
45
|
+
"windir",
|
|
46
|
+
"SystemDrive",
|
|
47
|
+
"PROCESSOR_ARCHITECTURE",
|
|
48
|
+
"NUMBER_OF_PROCESSORS",
|
|
49
|
+
"ALLUSERSPROFILE",
|
|
50
|
+
"ProgramData",
|
|
51
|
+
"ProgramFiles",
|
|
52
|
+
"ProgramFiles(x86)",
|
|
53
|
+
"CommonProgramFiles",
|
|
54
|
+
"PUBLIC",
|
|
35
55
|
];
|
|
36
56
|
/** Model-provider API keys a key-based connector may forward to its child. claude needs none
|
|
37
57
|
* (macOS Keychain / OAuth token, not an env key) → strong isolation for free; opencode/hermes
|
|
@@ -47,21 +67,34 @@ export const MODEL_PROVIDER_KEYS = [
|
|
|
47
67
|
];
|
|
48
68
|
/** Build the base env a spawned agent runs with: the OS allow-list plus any named keys the
|
|
49
69
|
* connector declares the agent needs — `providerKeys` (the model-provider key) and `mcpKeys`
|
|
50
|
-
* (the `${VAR}` secrets a shared MCP server references, see {@link mcpServerEnvKeys}). Every
|
|
51
|
-
*
|
|
52
|
-
*
|
|
70
|
+
* (the `${VAR}` secrets a shared MCP server references, see {@link mcpServerEnvKeys}). Every entry
|
|
71
|
+
* is copied from the manager's env BY NAME and only when present — never required, never spread
|
|
72
|
+
* wholesale, so the operator's unrelated secrets don't bleed into the child (P3).
|
|
73
|
+
*
|
|
74
|
+
* Matching is CASE-INSENSITIVE and each value is copied under the OS's OWN key casing: Windows
|
|
75
|
+
* spells these `Path`/`ComSpec`/`windir`, so a canonical-only copy would either miss them (a plain
|
|
76
|
+
* read of `process.env.SystemRoot` differs from `process.env.systemroot`) or, worse, emit BOTH
|
|
77
|
+
* `Path` and `PATH` — a case-duplicate Windows process creation chokes on. Keying off the source
|
|
78
|
+
* env's actual casing (one entry per lowercased name) forwards each var exactly once. */
|
|
53
79
|
export function launchEnv(opts = {}) {
|
|
54
80
|
const env = {};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
81
|
+
// lowercased name -> the OS's actual key casing; one entry per var (the OS env has no case-dup),
|
|
82
|
+
// so every allow-list name resolves to a single source key and the result carries no case-dup.
|
|
83
|
+
const sourceKey = new Map();
|
|
84
|
+
for (const k of Object.keys(process.env))
|
|
85
|
+
sourceKey.set(k.toLowerCase(), k);
|
|
86
|
+
const copy = (name) => {
|
|
87
|
+
const src = sourceKey.get(name.toLowerCase());
|
|
88
|
+
if (src === undefined)
|
|
89
|
+
return;
|
|
90
|
+
const v = process.env[src];
|
|
62
91
|
if (v !== undefined)
|
|
63
|
-
env[
|
|
64
|
-
}
|
|
92
|
+
env[src] = v;
|
|
93
|
+
};
|
|
94
|
+
for (const k of OS_ENV_ALLOW)
|
|
95
|
+
copy(k);
|
|
96
|
+
for (const k of [...(opts.providerKeys ?? []), ...(opts.mcpKeys ?? [])])
|
|
97
|
+
copy(k);
|
|
65
98
|
return env;
|
|
66
99
|
}
|
|
67
100
|
/** The agent's resolved access policy as `COTAL_*` env, when present. Forwarded by each connector
|
package/dist/launch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"AAkBA
|
|
1
|
+
{"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"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"}
|
package/dist/relay.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAkCA,6FAA6F;AAC7F,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAiDlD"}
|
package/dist/relay.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* entry points are one-liners over {@link runHookRelay}.
|
|
9
9
|
*/
|
|
10
10
|
import { connect } from "node:net";
|
|
11
|
-
import {
|
|
12
|
-
import { controlSocketPath } from "./runtime.js";
|
|
11
|
+
import { hasIdentity } from "./config.js";
|
|
13
12
|
const TIMEOUT_MS = 2000;
|
|
14
13
|
function readStdin() {
|
|
15
14
|
return new Promise((resolve) => {
|
|
@@ -35,9 +34,22 @@ function done(out) {
|
|
|
35
34
|
export async function runHookRelay() {
|
|
36
35
|
if (!hasIdentity())
|
|
37
36
|
return done(""); // plain session, not a managed one — no-op
|
|
37
|
+
// Path + token come from the launch env (shared with the in-agent server, which we inherited from);
|
|
38
|
+
// never recomputed from public identity. Absent → not an authenticated control session: no-op (fail
|
|
39
|
+
// open, so a hook never blocks the user's session).
|
|
40
|
+
const path = process.env.COTAL_CONTROL_SOCKET;
|
|
41
|
+
const token = process.env.COTAL_CONTROL_TOKEN;
|
|
42
|
+
if (!path || !token)
|
|
43
|
+
return done("");
|
|
38
44
|
const raw = (await readStdin()).trim() || "{}";
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
let event = {};
|
|
46
|
+
try {
|
|
47
|
+
event = JSON.parse(raw); // the hook event the runtime piped in
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* malformed — relay an empty event under a valid token */
|
|
51
|
+
}
|
|
52
|
+
const sock = connect(path);
|
|
41
53
|
let reply = "";
|
|
42
54
|
let settled = false;
|
|
43
55
|
const finish = (out) => {
|
|
@@ -54,7 +66,7 @@ export async function runHookRelay() {
|
|
|
54
66
|
};
|
|
55
67
|
const timer = setTimeout(() => finish(""), TIMEOUT_MS);
|
|
56
68
|
sock.setEncoding("utf8");
|
|
57
|
-
sock.on("connect", () => sock.write(
|
|
69
|
+
sock.on("connect", () => sock.write(JSON.stringify({ token, event }) + "\n"));
|
|
58
70
|
sock.on("data", (d) => {
|
|
59
71
|
reply += d;
|
|
60
72
|
const nl = reply.indexOf("\n");
|
package/dist/relay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay.js","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"relay.js","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* A connector's local control endpoint: the OS path its lifecycle hooks (and the manager's
|
|
3
|
+
* cooperative-shutdown call) connect to, plus the shared secret that authenticates the first frame.
|
|
4
|
+
*
|
|
5
|
+
* The path id is `sha256(space\0name\0pid\0token)` (base64url, ≤32) — unguessable and
|
|
6
|
+
* collision-free without leaking identity; the 256-bit `token` is the actual auth boundary (the
|
|
7
|
+
* server validates it with a constant-time compare before doing anything — see `control.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Transport is per-platform but the same `node:net` path string drives both: win32 has no
|
|
10
|
+
* filesystem AF_UNIX socket Node can bind, so the path is a named pipe (`\\.\pipe\…`) whose default
|
|
11
|
+
* DACL lets ANY local process connect — which is exactly why the token, not the path, is the
|
|
12
|
+
* security boundary there. POSIX uses a per-user `tmpdir` socket.
|
|
13
|
+
*
|
|
14
|
+
* Minted ONCE at launch (in the manager's process, via the connector's `buildLaunch`). Both ends —
|
|
15
|
+
* the in-agent server that LISTENS and the short-lived hooks that CONNECT — then read `path`+`token`
|
|
16
|
+
* from the child env (`COTAL_CONTROL_SOCKET`/`COTAL_CONTROL_TOKEN`), never recompute them from
|
|
17
|
+
* public identity; the manager keeps them in memory for the cooperative shutdown. (`process.pid` is
|
|
18
|
+
* just generation-time entropy — the value flows by env, so it never has to match across processes.)
|
|
5
19
|
*/
|
|
6
|
-
export declare function
|
|
20
|
+
export declare function controlEndpoint(space: string, name: string, token?: string): {
|
|
21
|
+
path: string;
|
|
22
|
+
token: string;
|
|
23
|
+
};
|
|
7
24
|
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAA8C,GACpD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQjC"}
|
package/dist/runtime.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
function tok(s) {
|
|
5
|
-
const t = s.trim().replace(ILLEGAL, "_");
|
|
6
|
-
return t.length ? t.slice(0, 40) : "_";
|
|
7
|
-
}
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
4
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* A connector's local control endpoint: the OS path its lifecycle hooks (and the manager's
|
|
6
|
+
* cooperative-shutdown call) connect to, plus the shared secret that authenticates the first frame.
|
|
7
|
+
*
|
|
8
|
+
* The path id is `sha256(space\0name\0pid\0token)` (base64url, ≤32) — unguessable and
|
|
9
|
+
* collision-free without leaking identity; the 256-bit `token` is the actual auth boundary (the
|
|
10
|
+
* server validates it with a constant-time compare before doing anything — see `control.ts`).
|
|
11
|
+
*
|
|
12
|
+
* Transport is per-platform but the same `node:net` path string drives both: win32 has no
|
|
13
|
+
* filesystem AF_UNIX socket Node can bind, so the path is a named pipe (`\\.\pipe\…`) whose default
|
|
14
|
+
* DACL lets ANY local process connect — which is exactly why the token, not the path, is the
|
|
15
|
+
* security boundary there. POSIX uses a per-user `tmpdir` socket.
|
|
16
|
+
*
|
|
17
|
+
* Minted ONCE at launch (in the manager's process, via the connector's `buildLaunch`). Both ends —
|
|
18
|
+
* the in-agent server that LISTENS and the short-lived hooks that CONNECT — then read `path`+`token`
|
|
19
|
+
* from the child env (`COTAL_CONTROL_SOCKET`/`COTAL_CONTROL_TOKEN`), never recompute them from
|
|
20
|
+
* public identity; the manager keeps them in memory for the cooperative shutdown. (`process.pid` is
|
|
21
|
+
* just generation-time entropy — the value flows by env, so it never has to match across processes.)
|
|
12
22
|
*/
|
|
13
|
-
export function
|
|
14
|
-
|
|
23
|
+
export function controlEndpoint(space, name, token = randomBytes(32).toString("base64url")) {
|
|
24
|
+
const id = createHash("sha256")
|
|
25
|
+
.update(`${space}\0${name}\0${process.pid}\0${token}`)
|
|
26
|
+
.digest("base64url")
|
|
27
|
+
.slice(0, 32);
|
|
28
|
+
const path = process.platform === "win32" ? `\\\\.\\pipe\\cotal-${id}` : join(tmpdir(), `cotal-${id}.sock`);
|
|
29
|
+
return { path, token };
|
|
15
30
|
}
|
|
16
31
|
//# sourceMappingURL=runtime.js.map
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,IAAY,EACZ,QAAgB,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;IAErD,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;SAC5B,MAAM,CAAC,GAAG,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;SACrD,MAAM,CAAC,WAAW,CAAC;SACnB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,MAAM,IAAI,GACR,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACjG,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotal-ai/connector-core",
|
|
3
3
|
"description": "Shared MCP-bridge runtime for Cotal connectors: the mesh agent, cotal_* tools, and hook relay.",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.2",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@cotal-ai/core": ">=0.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@cotal-ai/core": "0.8.
|
|
28
|
+
"@cotal-ai/core": "0.8.2"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist"
|