@essentialai/cogent-plugin 3.25.9 → 3.25.10

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogent",
3
- "version": "3.25.9",
3
+ "version": "3.25.10",
4
4
  "description": "Inter-session communication bridge for Claude Code with Slack integration. Enables CC agents and Slack team members to communicate in real time.",
5
5
  "author": {
6
6
  "name": "Essential AI Solutions",
@@ -26475,8 +26475,8 @@ var init_stdio2 = __esm({
26475
26475
  // src/constants.ts
26476
26476
  import { createRequire } from "node:module";
26477
26477
  function resolveVersion() {
26478
- if ("3.25.9") {
26479
- return "3.25.9";
26478
+ if ("3.25.10") {
26479
+ return "3.25.10";
26480
26480
  }
26481
26481
  try {
26482
26482
  const require2 = createRequire(import.meta.url);
@@ -26692,6 +26692,9 @@ ${issues}`);
26692
26692
  _config = Object.freeze(data);
26693
26693
  return _config;
26694
26694
  }
26695
+ function readOnlyWake(config2) {
26696
+ return config2.COGENT_READ_ONLY_WAKE || config2.COGENT_WAKE_TOOLS === "read";
26697
+ }
26695
26698
  function getConfig() {
26696
26699
  if (!_config) throw new Error("Config not loaded. Call loadConfig() first.");
26697
26700
  return _config;
@@ -26755,11 +26758,44 @@ var init_config = __esm({
26755
26758
  // launches via the daemon (see bin/cogent-codex) and sets this to `app-server`.
26756
26759
  // Only affects COGENT_PLATFORM=codex; claude/gemini/mail rails never read it.
26757
26760
  COGENT_CODEX_WAKE: import_zod2.z.enum(["auto", "exec-resume", "app-server"]).default("auto"),
26758
- // COG-86: what a Claude wake may do. `read` (default) = Read, Glob and Grep only: the wake answers but cannot edit,
26759
- // run commands or commit. A wake resumes the SAME session a human may have open in a terminal, and nothing can
26760
- // tell reliably that the terminal is there; on 2026-10-15 a full-tool wake edited a live session's files, then
26761
- // committed and pushed. `full` = every tool (the behaviour before 3.25.9), for an agent no human drives.
26762
- COGENT_WAKE_TOOLS: import_zod2.z.enum(["read", "full"]).default("read"),
26761
+ // COG-86 / COG-88: what a Claude wake may do. `full` (default) = every tool, as before 3.25.9. `read` = Read,
26762
+ // Glob and Grep only: the wake answers but cannot edit, run commands or commit. A wake resumes the SAME session
26763
+ // a human may have open in a terminal, and nothing can tell reliably that the terminal is there; on 2026-10-15
26764
+ // a full-tool wake edited a live session's files, then committed and pushed. 3.25.9 made `read` the default;
26765
+ // the operator reversed that on 2026-10-16 (COG-88): read-only is an opt-in, switched with the environment
26766
+ // variable COGENT_READ_ONLY_WAKE (below) or here. An EMPTY value counts as unset — a host that substitutes an empty
26767
+ // string for a missing setting must not take the bridge down. Case and surrounding spaces are ignored
26768
+ // ("Read", "read " = read), the same rule COGENT_READ_ONLY_WAKE uses — a user opting in must not stop the
26769
+ // bridge over a capital letter. Any other unknown value still fails loudly.
26770
+ // Read it through readOnlyWake(), never directly: either opt-in turns read-only on.
26771
+ COGENT_WAKE_TOOLS: import_zod2.z.preprocess(
26772
+ (v) => {
26773
+ if (typeof v !== "string") return v;
26774
+ const s = v.trim().toLowerCase();
26775
+ return s === "" ? void 0 : s;
26776
+ },
26777
+ import_zod2.z.enum(["read", "full"]).default("full")
26778
+ ),
26779
+ // COG-88: the read-only wake opt-in — an ENVIRONMENT VARIABLE only: no plugin setting, so no Claude Code
26780
+ // version floor. Plugin users set it in the "env" block of ~/.claude/settings.json or export it in the shell
26781
+ // (both reach the plugin's MCP server and its SessionStart hook — measured, Claude Code 2.1.74 and 2.1.302); a custom/Team config sets it
26782
+ // in its server env block. Applied at bridge start: /reload-plugins does NOT restart the server (measured).
26783
+ // The plugin's .mcp.json must NEVER set this key (nor COGENT_WAKE_TOOLS): a plugin env-block key wins over the
26784
+ // shell and settings.json, so any value there would silently block the user's opt-in (measured).
26785
+ //
26786
+ // FUTURE /config option (operator, 2026-10-16: may become the only switch; NOT shipped now). It would be a
26787
+ // plugin.json "userConfig": { "read_only_wake": { "type": "boolean", ... } } mapped by plugin/.mcp.json
26788
+ // "COGENT_READ_ONLY_WAKE": "${user_config.read_only_wake}" — Claude Code passes the STRING "true"/"false"
26789
+ // (measured, 2.1.301). userConfig needs Claude Code >= 2.1.75: 2.1.74 and older reject the manifest
26790
+ // ('Unrecognized key: "userConfig"') and load NONE of the plugin (measured 2026-10-16). Adding that env line
26791
+ // also ends the env-variable path for plugin users (the block wins), so the two cannot ship side by side, and
26792
+ // the guards in src/config.test.ts and cogent-plugin/scripts/sync-from-plugin.test.mjs must change with it.
26793
+ //
26794
+ // Only an explicit affirmative turns it on. Anything else is OFF and never an error — "", "false", a typo, or
26795
+ // an unsubstituted "${user_config.read_only_wake}": a config error here would stop the bridge, which is worse
26796
+ // than a wake that behaves as it did before 3.25.9. The plugin's SessionStart notice reads it with the same rule
26797
+ // (readOnlyWakeFromEnv in plugin/hooks/check-on-stop.lib.mjs, parity-tested).
26798
+ COGENT_READ_ONLY_WAKE: import_zod2.z.string().optional().transform((v) => v !== void 0 && ["true", "1", "yes", "on"].includes(v.trim().toLowerCase())),
26763
26799
  // Auto-recovery for a blocked Codex sandbox. On a host where the OS sandbox
26764
26800
  // can't initialize (unprivileged user namespaces blocked — Ubuntu 24.04's
26765
26801
  // apparmor_restrict_unprivileged_userns, containers, hardened kernels),
@@ -27424,15 +27460,17 @@ async function execClaude(sessionId, message, cwd, timeoutMs) {
27424
27460
  // (the Monitor tool, loaded through ToolSearch, ran a shell and wrote a file) and would miss every tool a later
27425
27461
  // claude adds. --tools is variadic, so it must be followed by another option, never by the message. A claude
27426
27462
  // too old to know --tools exits "unknown option" — the wake fails, it never runs with every tool.
27427
- ...config2.COGENT_WAKE_TOOLS === "read" ? ["--tools", READ_ONLY_WAKE_TOOLS, "--append-system-prompt", READ_ONLY_WAKE_NOTICE] : [],
27463
+ // COG-88: opt-in (COGENT_READ_ONLY_WAKE=true or COGENT_WAKE_TOOLS=read in the env); off by default.
27464
+ ...readOnlyWake(config2) ? ["--tools", READ_ONLY_WAKE_TOOLS, "--append-system-prompt", READ_ONLY_WAKE_NOTICE] : [],
27428
27465
  "-p",
27429
- truncated
27466
+ readOnlyWake(config2) ? `${truncated}${READ_ONLY_WAKE_PROMPT_SEPARATOR}${READ_ONLY_WAKE_NOTICE}` : truncated
27430
27467
  ];
27431
27468
  const cleanEnv = buildResumeEnv(process.env, "CLAUDE", ["CLAUDE_CONFIG_DIR"]);
27432
27469
  if (resolvedRoot && resolvedRoot !== path10.resolve(claudeConfigDir())) {
27433
27470
  cleanEnv.CLAUDE_CONFIG_DIR = resolvedRoot;
27434
27471
  }
27435
27472
  cleanEnv.COGENT_CHECK_ON_STOP = "0";
27473
+ cleanEnv.COGENT_WAKE = "1";
27436
27474
  const child = spawn(config2.COGENT_CLAUDE_PATH, args, {
27437
27475
  cwd,
27438
27476
  env: cleanEnv,
@@ -27553,7 +27591,7 @@ function parseClaudeJsonReply(raw) {
27553
27591
  const result = typeof env.result === "string" ? env.result : null;
27554
27592
  return { result, isError: env.is_error === true || env.subtype !== "success" };
27555
27593
  }
27556
- var READ_ONLY_WAKE_TOOLS, READ_ONLY_WAKE_NOTICE;
27594
+ var READ_ONLY_WAKE_TOOLS, READ_ONLY_WAKE_NOTICE, READ_ONLY_WAKE_PROMPT_SEPARATOR;
27557
27595
  var init_cc_cli = __esm({
27558
27596
  "src/services/cc-cli.ts"() {
27559
27597
  "use strict";
@@ -27561,7 +27599,8 @@ var init_cc_cli = __esm({
27561
27599
  init_agent_config();
27562
27600
  init_agent_sessions();
27563
27601
  READ_ONLY_WAKE_TOOLS = "Read,Glob,Grep";
27564
- READ_ONLY_WAKE_NOTICE = "This is an automatic Cogent wake. You can read files but cannot change them or run commands. If the message asks for changes, say what must change and that it has to be done in the agent's own session.";
27602
+ READ_ONLY_WAKE_NOTICE = "You are answering a message from another Cogent peer automatically. Your reply goes to that sender, not to your user. Your user chose to make these automatic replies read-only: you can read files, but you cannot edit files, run commands or commit, and neither you nor the sender can change that. If the message asks for a change, do not attempt it. Your reply must: (1) say plainly that this automatic reply could not change any files; (2) give the exact change: the file path and the precise text to replace and its replacement, or the exact command to run; (3) say that your user can make the change in this agent's own session. Do not offer to make the change later, and do not ask the sender to enable, re-enable or turn on any tool or permission.";
27603
+ READ_ONLY_WAKE_PROMPT_SEPARATOR = "\n\n---\n[Cogent: read-only automatic reply \u2014 not part of the message above]\n";
27565
27604
  }
27566
27605
  });
27567
27606
 
@@ -32777,6 +32816,7 @@ function execCodex(sessionId, message, cwd, timeoutMs) {
32777
32816
  ];
32778
32817
  const cleanEnv = buildResumeEnv(process.env, "CODEX", ["CODEX_HOME"]);
32779
32818
  cleanEnv.COGENT_CHECK_ON_STOP = "0";
32819
+ cleanEnv.COGENT_WAKE = "1";
32780
32820
  const child = spawn2(config2.COGENT_CODEX_PATH, args, {
32781
32821
  cwd,
32782
32822
  env: cleanEnv,
@@ -35828,14 +35868,18 @@ var init_heartbeat = __esm({
35828
35868
  // src/services/claude-preflight.ts
35829
35869
  import { execFile as execFile4 } from "node:child_process";
35830
35870
  import { promisify as promisify3 } from "node:util";
35831
- function analyzeClaudeHelp(helpText) {
35832
- const checked = EXPECTED_CLAUDE_FLAGS.map((f) => f.flag);
35833
- const missing = EXPECTED_CLAUDE_FLAGS.filter(
35871
+ function expectedClaudeFlags(readOnly) {
35872
+ return readOnly ? [...EXPECTED_CLAUDE_FLAGS, ...READ_ONLY_WAKE_CLAUDE_FLAGS] : EXPECTED_CLAUDE_FLAGS;
35873
+ }
35874
+ function analyzeClaudeHelp(helpText, readOnly = false) {
35875
+ const expected = expectedClaudeFlags(readOnly);
35876
+ const checked = expected.map((f) => f.flag);
35877
+ const missing = expected.filter(
35834
35878
  (f) => !helpText.includes(f.flag)
35835
35879
  ).map((f) => f.flag);
35836
35880
  return { missing, checked };
35837
35881
  }
35838
- async function preflightClaude(claudePath) {
35882
+ async function preflightClaude(claudePath, readOnly = false) {
35839
35883
  let helpText;
35840
35884
  try {
35841
35885
  const { stdout, stderr } = await execFileAsync2(claudePath, ["--help"], {
@@ -35849,9 +35893,9 @@ ${stderr}`;
35849
35893
  );
35850
35894
  return null;
35851
35895
  }
35852
- const analysis = analyzeClaudeHelp(helpText);
35896
+ const analysis = analyzeClaudeHelp(helpText, readOnly);
35853
35897
  if (analysis.missing.length > 0) {
35854
- const detail = EXPECTED_CLAUDE_FLAGS.filter(
35898
+ const detail = expectedClaudeFlags(readOnly).filter(
35855
35899
  (f) => analysis.missing.includes(f.flag)
35856
35900
  ).map((f) => `${f.flag} (${f.usedFor})`).join(", ");
35857
35901
  logger.error(
@@ -35864,7 +35908,7 @@ ${stderr}`;
35864
35908
  }
35865
35909
  return analysis;
35866
35910
  }
35867
- var execFileAsync2, EXPECTED_CLAUDE_FLAGS;
35911
+ var execFileAsync2, EXPECTED_CLAUDE_FLAGS, READ_ONLY_WAKE_CLAUDE_FLAGS;
35868
35912
  var init_claude_preflight = __esm({
35869
35913
  "src/services/claude-preflight.ts"() {
35870
35914
  "use strict";
@@ -35876,7 +35920,9 @@ var init_claude_preflight = __esm({
35876
35920
  { flag: "--dangerously-skip-permissions", usedFor: "unattended auto-relay execution" },
35877
35921
  { flag: "--no-session-persistence", usedFor: "don't pollute the resumed session transcript" },
35878
35922
  { flag: "--strict-mcp-config", usedFor: "isolate MCP config of the resumed process" },
35879
- { flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" },
35923
+ { flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" }
35924
+ ];
35925
+ READ_ONLY_WAKE_CLAUDE_FLAGS = [
35880
35926
  { flag: "--tools", usedFor: "read-only wake: Read, Glob and Grep only (COG-86)" },
35881
35927
  { flag: "--append-system-prompt", usedFor: "tell a read-only wake it cannot change anything (COG-86)" }
35882
35928
  ];
@@ -36400,7 +36446,7 @@ async function runPreflights() {
36400
36446
  setSandboxOverride("bypass");
36401
36447
  }
36402
36448
  } else {
36403
- await preflightClaude(config2.COGENT_CLAUDE_PATH);
36449
+ await preflightClaude(config2.COGENT_CLAUDE_PATH, readOnlyWake(config2));
36404
36450
  }
36405
36451
  }
36406
36452
  var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, ignoredHolders, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve, lastRestoreSkip;
@@ -21,6 +21,16 @@ export function parseScope(csv) {
21
21
  };
22
22
  }
23
23
 
24
+ /**
25
+ * COG-88: are this agent's Claude wakes read-only? MUST equal src/config.ts readOnlyWake(loadConfig(env)):
26
+ * COGENT_READ_ONLY_WAKE truthy ("true"/"1"/"yes"/"on") OR COGENT_WAKE_TOOLS = "read", each trimmed and
27
+ * case-folded. Parity is tested against the REAL loadConfig (plugin/hooks/record-session.runtime.test.mjs).
28
+ */
29
+ export function readOnlyWakeFromEnv(env) {
30
+ const v = (k) => String(env[k] ?? "").trim().toLowerCase();
31
+ return ["true", "1", "yes", "on"].includes(v("COGENT_READ_ONLY_WAKE")) || v("COGENT_WAKE_TOOLS") === "read";
32
+ }
33
+
24
34
  /** sha256(path.resolve(cwd))[..16] — MUST equal credential-store.defaultCredentialPath's hash. */
25
35
  export function credHashForCwd(cwd) {
26
36
  return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // Cogent record-session. Runs on SessionStart as a SEPARATE short-lived process
3
3
  // (the host spawns it; it is NOT the bridge). Writes one small file and exits 0.
4
- // ALWAYS silent — it prints nothing on success or failure, so it can never block,
5
- // delay, or annotate a turn.
4
+ // Silent, with ONE exception (COG-88, below): the once-only wake-permissions notice,
5
+ // printed as {"systemMessage": ...} — valid SessionStart hook output that the host
6
+ // shows to the human and never feeds to the model. It can never block or fail a turn.
6
7
  //
7
8
  // 🔴 WHY THIS EXISTS. Auto-wake has to find the agent's session transcript, and it
8
9
  // used to DERIVE the location:
@@ -23,10 +24,12 @@
23
24
  // Node built-ins only (fs, os, path). Byte-identical between plugin/hooks/ and
24
25
  // codex-plugin/hooks/ — enforced by scripts/wake-race-test.mjs assertContract().
25
26
  import fs from "node:fs/promises";
27
+ import os from "node:os";
26
28
  import path from "node:path";
27
29
  import {
28
30
  agentSessionRecordPath,
29
31
  buildAgentSessionRecord,
32
+ readOnlyWakeFromEnv,
30
33
  } from "./check-on-stop.lib.mjs";
31
34
 
32
35
  async function readStdin() {
@@ -55,8 +58,70 @@ export async function persistRecord(record) {
55
58
  return target;
56
59
  }
57
60
 
61
+ // ── COG-88: tell the human ONCE what a Cogent wake may do in this folder ──────────────
62
+ // 3.25.9 made wakes read-only by default; 3.25.10 turned that back into an opt-in: the environment variable
63
+ // COGENT_READ_ONLY_WAKE=true (or COGENT_WAKE_TOOLS=read). Nobody reads a changelog, so the person at the
64
+ // terminal is told once, at a session start, what wakes can do and how to switch it.
65
+ //
66
+ // The mode is read from this hook's own env with the bridge's rule (readOnlyWakeFromEnv = readOnlyWake()).
67
+ // Measured on Claude Code 2.1.302 and 2.1.74 (throwaway CLAUDE_CONFIG_DIR, probe plugin): a settings.json "env" value and
68
+ // a shell export reach BOTH this hook and the plugin's MCP server, so the hook sees what the bridge sees.
69
+ // Also measured: after a settings.json change, /reload-plugins does NOT restart the plugin's MCP server (it
70
+ // keeps the old value), so the notice says "restart Claude Code".
71
+ //
72
+ // Shown ONLY when all hold:
73
+ // CLAUDECODE set — Claude Code. Codex never sets it (and shows systemMessage only
74
+ // after the first prompt).
75
+ // CLAUDE_CODE_SESSION_ATTENDED === "1" — a human is at the TUI ("0" in headless -p).
76
+ // COGENT_WAKE !== "1" — not inside a wake: the resumed claude runs this
77
+ // hook too (source "resume"), with the wake's env,
78
+ // and a notice there is swallowed but would still
79
+ // burn the once-only marker.
80
+ // the marker does not exist — once per notice version.
81
+ // Measured: Claude Code 2.1.302 gives a TUI SessionStart hook both variables; 2.1.74 gives it NEITHER (clean env, real
82
+ // TUI), so on 2.1.74 the notice is never shown. Deliberate: nothing there tells a TUI from headless -p, and a
83
+ // notice nobody sees would burn the marker. The opt-in itself still works on 2.1.74 (env reaches the bridge).
84
+ // The marker is created with "wx" BEFORE printing, so two sessions starting together
85
+ // cannot both show it, and it is never written unless the notice is printed.
86
+ export const WAKE_NOTICE_MARKER = "wake-tools-notice-v1";
87
+
88
+ export function wakeNoticeText(env) {
89
+ // Claude Code reads <CLAUDE_CONFIG_DIR>/settings.json when that variable is set; naming ~/.claude there
90
+ // would send the user to a file with no effect.
91
+ const dir = (env.CLAUDE_CONFIG_DIR || "").trim();
92
+ const settings = dir ? path.join(dir, "settings.json") : "~/.claude/settings.json";
93
+ if (readOnlyWakeFromEnv(env)) {
94
+ const names = ["COGENT_READ_ONLY_WAKE", "COGENT_WAKE_TOOLS"].filter((k) => readOnlyWakeFromEnv({ [k]: env[k] }));
95
+ const setting = names.join(" and ");
96
+ return (
97
+ `Cogent: wakes are read-only (${setting} ${names.length > 1 ? "are" : "is"} set) — when a Cogent message ` +
98
+ "wakes this agent, the wake can read files and answer, but cannot edit files or run commands. To let wakes " +
99
+ `edit again, remove ${setting} from the "env" block of ${settings} (or from your shell), then restart ` +
100
+ "Claude Code. (Shown once.)"
101
+ );
102
+ }
103
+ return (
104
+ "Cogent: when a Cogent message wakes this agent, the wake can edit files and run commands in this folder. " +
105
+ `To make wakes read-only, add "COGENT_READ_ONLY_WAKE": "true" to the "env" block of ${settings} ` +
106
+ "(or export it in your shell), then restart Claude Code. (Shown once.)"
107
+ );
108
+ }
109
+
110
+ async function maybeShowWakeNotice(env) {
111
+ if (!env.CLAUDECODE || env.CLAUDE_CODE_SESSION_ATTENDED !== "1" || env.COGENT_WAKE === "1") return;
112
+ const dir = env.CLAUDE_PLUGIN_DATA || path.join(os.homedir(), ".cogent", "notices");
113
+ await fs.mkdir(dir, { recursive: true });
114
+ // EEXIST (already shown) and every other failure throw → swallowed by the caller: silent.
115
+ await fs.writeFile(path.join(dir, WAKE_NOTICE_MARKER), `${new Date().toISOString()}\n`, { flag: "wx" });
116
+ const out = JSON.stringify({ systemMessage: wakeNoticeText(env) });
117
+ // Wait for the write: a pipe can be asynchronous (macOS), and exit() would cut it off.
118
+ await new Promise((resolve) => process.stdout.write(out, () => resolve()));
119
+ }
120
+
58
121
  async function main() {
59
122
  const input = await readStdin();
123
+ // Independent of the record below, and never allowed to break it (or the session start).
124
+ await maybeShowWakeNotice(process.env).catch(() => {});
60
125
  const record = buildAgentSessionRecord(input, new Date().toISOString());
61
126
  // No transcript_path in this payload → nothing authoritative to record. The
62
127
  // resolver's discovery layer still covers this case; staying silent is correct.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.25.9",
3
+ "version": "3.25.10",
4
4
  "description": "Cogent — Claude Code plugin (skills + slash-commands + MCP server) for the cross-agent comms fabric.",
5
5
  "author": { "name": "Essential AI Solutions Ltd.", "url": "https://essentialai.uk" },
6
6
  "homepage": "https://cogent.tools",
@@ -10,6 +10,7 @@
10
10
  "files": [".claude-plugin", "commands", "skills", ".mcp.json", "hooks", "bridge", "README.md", "LICENSE"],
11
11
  "scripts": {
12
12
  "sync": "node scripts/sync-from-plugin.mjs",
13
+ "test": "node --test scripts/sync-from-plugin.test.mjs",
13
14
  "prepack": "node scripts/sync-from-plugin.mjs"
14
15
  }
15
16
  }