@cotal-ai/connector-claude-code 0.3.2 → 0.4.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/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @cotal-ai/connector-claude-code
2
+
3
+ The Claude Code adapter: a bundled, installed plugin plus `claude/channel` push that turns a
4
+ real `claude` session into a Cotal mesh peer. A thin client over
5
+ [`@cotal-ai/connector-core`](../connector-core).
6
+
7
+ **Tier:** `extensions/`. Peer-depends [`@cotal-ai/core`](../../packages/core); self-registers on
8
+ import.
9
+
10
+ See [docs/claude-code-integration.md](../../docs/claude-code-integration.md) for the full
11
+ integration, and the [root AGENTS.md](../../AGENTS.md) for the tier rules.
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2B,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAmB3G;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SA8D7B,CAAC"}
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAIA,OAAO,EAA2B,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAoB3G;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SA2F7B,CAAC"}
package/dist/extension.js CHANGED
@@ -1,6 +1,9 @@
1
- import { resolve } from "node:path";
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
2
4
  import { fileURLToPath } from "node:url";
3
5
  import { loadAgentFile, registry } from "@cotal-ai/core";
6
+ import { launchEnv, mcpServerEnvKeys } from "@cotal-ai/connector-core";
4
7
  /** Name the cotal MCP server is registered under via --mcp-config (see buildLaunch). */
5
8
  const MCP_SERVER_NAME = "cotal";
6
9
  /** Channel ref for `--dangerously-load-development-channels`, which turns on the cotal MCP server's
@@ -27,16 +30,25 @@ export const claudeConnector = {
27
30
  name: "claude",
28
31
  pluginRoot: PLUGIN_ROOT,
29
32
  buildLaunch(opts) {
33
+ // Operator MCP servers shared with this agent (default none — see the --mcp-config block).
34
+ const shared = opts.mcpServers ?? {};
35
+ // claude auths via macOS Keychain / an OAuth token, not an env key → forward NO provider key.
36
+ // The OS allow-list (PATH/HOME/TERM/…) is the only thing inherited from the manager env, plus
37
+ // — only when a shared server declares them via `${VAR}` — the named secrets it needs (mcpKeys,
38
+ // by name). The operator's unrelated secrets don't reach the child (P3).
30
39
  const env = {
40
+ ...launchEnv({ mcpKeys: mcpServerEnvKeys(shared) }),
31
41
  COTAL_SPACE: opts.space,
32
42
  COTAL_NAME: opts.name,
33
43
  // Force the connector to emit channel wake-nudges: Claude doesn't advertise the
34
44
  // `claude/channel` capability back over MCP, so auto-detection would see it "off".
35
45
  COTAL_CHANNEL: "1",
36
- // Managed sessions mirror their own transcript to `tr-<name>` so peers can read
37
- // what the agent actually did. Personal sessions (no buildLaunch) never mirror.
38
- COTAL_TRANSCRIPT: "1",
39
46
  };
47
+ // A session can mirror its own transcript to `tr-<name>` so peers can read what the
48
+ // agent actually did — OFF by default (transcripts are verbose and may carry sensitive
49
+ // content); `--transcript` (opts.transcript === true) opts in. Personal sessions never mirror.
50
+ if (opts.transcript === true)
51
+ env.COTAL_TRANSCRIPT = "1";
40
52
  if (opts.role)
41
53
  env.COTAL_ROLE = opts.role;
42
54
  if (opts.id)
@@ -54,13 +66,39 @@ export const claudeConnector = {
54
66
  // can look something up under `npx` (no repo on disk) without prompting the operator
55
67
  // mid-demo. Additive under the default permission mode — leaves other tools as-is.
56
68
  args.push("--allowedTools", "WebFetch(domain:github.com),WebFetch(domain:raw.githubusercontent.com)");
57
- // Isolate the spawned session's MCP to ONLY the cotal server. --strict-mcp-config drops every
58
- // other MCP source — including the operator's personal ~/.claude.json servers (e.g. a headless
59
- // Chromium, a DB server) that a meshed teammate never needs and that, multiplied across several
60
- // spawns on a busy machine, starve memory and kill the session before it registers presence —
61
- // and --mcp-config re-supplies cotal so its tools + presence still load. The plugin itself stays
62
- // enabled (its hooks + the dev-channels wake path are unaffected; only MCP config is scoped).
63
- args.push("--strict-mcp-config", "--mcp-config", JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: "node", args: [MCP_CJS] } } }));
69
+ // Isolate the spawned session's MCP. --strict-mcp-config drops every ambient MCP source —
70
+ // including the operator's personal ~/.claude.json servers (e.g. a headless Chromium, a DB
71
+ // server) that a meshed teammate never needs and that, multiplied across several spawns on a
72
+ // busy machine, starve memory and kill the session before it registers presence — so the ONLY
73
+ // servers that load are the ones we name in --mcp-config: cotal (always, for its tools +
74
+ // presence) plus any the operator explicitly opted to share (`shared`, from the cotal config).
75
+ // The plugin itself stays enabled (its hooks + the dev-channels wake path are unaffected).
76
+ // cotal is spread LAST so a shared server can never shadow the mesh server by reusing its name.
77
+ const mcpServers = { ...shared, [MCP_SERVER_NAME]: { command: "node", args: [MCP_CJS] } };
78
+ // Default (no shared servers): pass the config inline, unchanged. With shared servers, write it
79
+ // to a file instead and pass the path. Either way the secret stays a `${VAR}` reference (Claude
80
+ // expands it from the child env at launch — see the mcpKeys forwarding above), never the resolved
81
+ // value, so nothing secret reaches disk or argv. We prefer the file when sharing because env
82
+ // expansion is only *documented* for --mcp-config files (inline expansion does work today, but
83
+ // isn't contracted), and a file keeps a potentially multi-server config off the process argv.
84
+ // Verified end-to-end on claude 2.1.183: ${VAR} expands in the --mcp-config file and the value
85
+ // is handed to the shared server. This is host-version behavior — if a future claude stops
86
+ // expanding here, a shared server would receive a literal `${VAR}`; re-check on host upgrades.
87
+ let mcpConfig;
88
+ if (Object.keys(shared).length === 0) {
89
+ mcpConfig = JSON.stringify({ mcpServers });
90
+ }
91
+ else {
92
+ // A private 0700 temp dir (unique per spawn) holds the 0600 config. mkdtemp can't be raced
93
+ // by a pre-created or symlinked path the way a predictable name in the world-writable tmpdir
94
+ // could, and a fresh file guarantees the 0600 mode applies on creation (mode is ignored on an
95
+ // overwrite). Left for the OS to reap: the file must outlive this call (Claude reads it at
96
+ // startup and on /mcp reconnect), and buildLaunch doesn't own the child's lifecycle.
97
+ const dir = mkdtempSync(join(tmpdir(), "cotal-mcp-"));
98
+ mcpConfig = join(dir, "mcp.json");
99
+ writeFileSync(mcpConfig, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
100
+ }
101
+ args.push("--strict-mcp-config", "--mcp-config", mcpConfig);
64
102
  // An agent file carries identity (read in-session via COTAL_AGENT_FILE) plus
65
103
  // persona + model, which can only be applied to a `claude` session at launch.
66
104
  if (opts.configPath) {
@@ -1 +1 @@
1
- {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"}
package/dist/hook.cjs CHANGED
@@ -14708,6 +14708,53 @@ var require_mod6 = __commonJS({
14708
14708
  var import_node_os = require("node:os");
14709
14709
  var import_node_fs2 = require("node:fs");
14710
14710
 
14711
+ // ../../packages/core/dist/subjects.js
14712
+ function subjectMatches(pattern, subject) {
14713
+ const p = pattern.split(".");
14714
+ const s = subject.split(".");
14715
+ for (let i = 0; i < p.length; i++) {
14716
+ if (p[i] === ">")
14717
+ return i < s.length;
14718
+ if (i >= s.length)
14719
+ return false;
14720
+ if (p[i] === "*")
14721
+ continue;
14722
+ if (p[i] !== s[i])
14723
+ return false;
14724
+ }
14725
+ return p.length === s.length;
14726
+ }
14727
+ function assertValidChannel(channel) {
14728
+ const segs = channel.split(".");
14729
+ if (!channel.length || segs.some((s) => s.length === 0))
14730
+ throw new Error(`invalid channel "${channel}": empty segment (no leading/trailing/double dots)`);
14731
+ segs.forEach((s, i) => {
14732
+ if (s === ">") {
14733
+ if (i !== segs.length - 1)
14734
+ throw new Error(`invalid channel "${channel}": '>' is only valid as the last segment`);
14735
+ return;
14736
+ }
14737
+ if (s === "*")
14738
+ return;
14739
+ if (!/^[A-Za-z0-9_-]+$/.test(s))
14740
+ throw new Error(`invalid channel "${channel}": segment "${s}" must be a NATS-safe token ([A-Za-z0-9_-]), '*', or '>' \u2014 policy channel names can't contain characters the wire layer would rewrite`);
14741
+ });
14742
+ return channel;
14743
+ }
14744
+ function channelInAllow(allow, channel) {
14745
+ return allow.some((a) => subjectMatches(a, channel));
14746
+ }
14747
+
14748
+ // ../../packages/core/dist/resolve.js
14749
+ function assertValidName(name) {
14750
+ if (name.length === 0 || name !== name.trim())
14751
+ throw new Error(`invalid name ${JSON.stringify(name)}: must be non-empty with no surrounding whitespace`);
14752
+ if (/[\r\n]/.test(name))
14753
+ throw new Error(`invalid name ${JSON.stringify(name)}: must be a single line`);
14754
+ if (name.includes("/"))
14755
+ throw new Error(`invalid name ${JSON.stringify(name)}: "/" is reserved (the owner/name separator)`);
14756
+ }
14757
+
14711
14758
  // ../../packages/core/dist/link.js
14712
14759
  function parseJoinLink(link) {
14713
14760
  const tls = link.startsWith("cotals://");
@@ -16419,10 +16466,28 @@ function loadAgentFile(path) {
16419
16466
  const name = str("name");
16420
16467
  if (!name)
16421
16468
  throw new Error(`agent file ${path}: "name" is required`);
16469
+ assertValidName(name);
16422
16470
  const kind = str("kind");
16423
16471
  if (kind && kind !== "agent" && kind !== "endpoint")
16424
16472
  throw new Error(`agent file ${path}: "kind" must be "agent" or "endpoint"`);
16425
- const known = /* @__PURE__ */ new Set(["name", "role", "kind", "description", "tags", "channels", "publish", "model"]);
16473
+ for (const old of ["channels", "publish"])
16474
+ if (old in fm)
16475
+ throw new Error(`agent file ${path}: "${old}" was renamed \u2014 use "subscribe"/"allowSubscribe" (read) and "allowPublish" (post)`);
16476
+ const subscribe = list("subscribe");
16477
+ const allowSubscribe = list("allowSubscribe");
16478
+ const allowPublish = list("allowPublish");
16479
+ for (const ch of [...subscribe ?? [], ...allowSubscribe ?? [], ...allowPublish ?? []])
16480
+ try {
16481
+ assertValidChannel(ch);
16482
+ } catch (e) {
16483
+ throw new Error(`agent file ${path}: ${e.message}`);
16484
+ }
16485
+ const effSubscribe = subscribe?.length ? subscribe : ["general"];
16486
+ const effAllow = allowSubscribe?.length ? allowSubscribe : effSubscribe;
16487
+ for (const ch of effSubscribe)
16488
+ if (!channelInAllow(effAllow, ch))
16489
+ throw new Error(`agent file ${path}: subscribe channel "${ch}" is not within allowSubscribe [${effAllow.join(", ")}]`);
16490
+ const known = /* @__PURE__ */ new Set(["name", "role", "kind", "description", "tags", "subscribe", "allowSubscribe", "allowPublish", "model", "capabilities", "owner"]);
16426
16491
  const meta = {};
16427
16492
  for (const [k, v] of Object.entries(fm))
16428
16493
  if (!known.has(k) && typeof v === "string")
@@ -16433,9 +16498,12 @@ function loadAgentFile(path) {
16433
16498
  kind,
16434
16499
  description: str("description"),
16435
16500
  tags: list("tags"),
16436
- channels: list("channels"),
16437
- publish: list("publish"),
16501
+ subscribe,
16502
+ allowSubscribe,
16503
+ allowPublish,
16438
16504
  model: str("model"),
16505
+ capabilities: list("capabilities"),
16506
+ owner: str("owner"),
16439
16507
  meta: Object.keys(meta).length ? meta : void 0,
16440
16508
  persona: persona || void 0
16441
16509
  };
@@ -16493,9 +16561,17 @@ function configFromEnv(env = process.env) {
16493
16561
  const name = env.COTAL_NAME?.trim() || def?.name || (link ? (0, import_node_os.userInfo)().username : void 0);
16494
16562
  if (!name)
16495
16563
  throw new Error("COTAL_NAME, COTAL_AGENT_FILE or COTAL_LINK is required \u2014 a Cotal session needs an explicit identity from its launcher");
16496
- const channels = splitList(env.COTAL_CHANNELS);
16497
- const resolvedChannels = channels.length ? channels : def?.channels ?? link?.channels ?? ["general"];
16498
- const publish = splitList(env.COTAL_PUBLISH);
16564
+ const subscribe = splitList(env.COTAL_SUBSCRIBE);
16565
+ const resolvedSubscribe = subscribe.length ? subscribe : def?.subscribe ?? link?.channels ?? ["general"];
16566
+ const allowSub = splitList(env.COTAL_ALLOW_SUBSCRIBE);
16567
+ const resolvedAllowSub = allowSub.length ? allowSub : def?.allowSubscribe ?? resolvedSubscribe;
16568
+ for (const ch of resolvedSubscribe)
16569
+ if (!channelInAllow(resolvedAllowSub, ch))
16570
+ throw new Error(`COTAL config: subscribe channel "${ch}" is not within allowSubscribe [${resolvedAllowSub.join(", ")}]`);
16571
+ const allowPub = splitList(env.COTAL_ALLOW_PUBLISH);
16572
+ const resolvedAllowPub = allowPub.length ? allowPub : def?.allowPublish ?? [];
16573
+ for (const ch of [...resolvedSubscribe, ...resolvedAllowSub, ...resolvedAllowPub])
16574
+ assertValidChannel(ch);
16499
16575
  const credsPath = env.COTAL_CREDS?.trim();
16500
16576
  return {
16501
16577
  space: env.COTAL_SPACE?.trim() || link?.space || "demo",
@@ -16506,8 +16582,11 @@ function configFromEnv(env = process.env) {
16506
16582
  description: def?.description,
16507
16583
  tags: def?.tags,
16508
16584
  servers: env.COTAL_SERVERS?.trim() || link?.servers || DEFAULT_SERVER,
16509
- channels: resolvedChannels,
16510
- publish: publish.length ? publish : def?.publish ?? resolvedChannels,
16585
+ subscribe: resolvedSubscribe,
16586
+ allowSubscribe: resolvedAllowSub,
16587
+ // Post ACL is default-DENY: only what's explicitly declared (env > agent-file). The broker
16588
+ // enforces it under auth; in open mode posting is unrestricted regardless (see laneLine).
16589
+ allowPublish: resolvedAllowPub,
16511
16590
  kind: env.COTAL_KIND?.trim() || def?.kind || "agent",
16512
16591
  token: env.COTAL_TOKEN?.trim() || link?.token,
16513
16592
  user: link?.user,