@matuto-ai/claude-code-broker-connector 0.0.0-stage → 0.2.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,17 @@
1
+ {
2
+ "name": "psst-connectors",
3
+ "owner": {
4
+ "name": "Bruno Orsolon"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "psst",
9
+ "source": "./",
10
+ "description": "Psst messaging for Claude Code",
11
+ "version": "0.2.0"
12
+ }
13
+ ],
14
+ "metadata": {
15
+ "description": "Native Psst agent-messaging connector for Claude Code"
16
+ }
17
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "psst",
3
+ "version": "0.2.0",
4
+ "description": "Session-bound Psst agent messaging with explicit receive and acknowledgment. No permission relay or unsolicited prompts.",
5
+ "author": { "name": "Bruno Orsolon" },
6
+ "repository": "https://github.com/matuto-ai/claude-code-broker-connector",
7
+ "license": "MIT"
8
+ }
package/.mcp.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "mcpServers": {
3
+ "psst": { "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/bin/plugin-mcp.mjs"] }
4
+ }
5
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Orsolon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,105 @@
1
- # Temporary Holding Version
1
+ # Psst for Claude Code
2
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.
3
+ Native Claude Code plugin for discovery, send, receive, explicit read acknowledgment,
4
+ and delivery status through [Psst](https://github.com/matuto-ai/psst).
5
+ Install **inside the existing Linux sandbox where Claude already runs**. No host
6
+ launcher, new container, broker source checkout, API model key, or npm dependency
7
+ is needed. The plugin uses the existing Claude login and normal permissions.
8
+
9
+ ## Install from GitHub
10
+
11
+ After this PR merges, install from `main` (before merge, use
12
+ `#chore/matuto-ai-npm-scope` instead):
13
+
14
+ ```sh
15
+ claude plugin marketplace add matuto-ai/claude-code-broker-connector#main
16
+ claude plugin install psst@psst-connectors
17
+ ```
18
+
19
+ Once released, pin the reviewed release tag instead of the branch. This package
20
+ is a private/custom marketplace, not a listing in Anthropic's official catalog.
21
+ Review the plugin prompt before installing. Requires **Linux, Node 24.19+ within
22
+ 24.x on PATH**, and Claude Code with exec-form hooks (audited with 2.1.309).
23
+
24
+ Configure the path to this client's private enrollment bundle before starting
25
+ Claude normally:
26
+
27
+ ```sh
28
+ export PSST_CONFIG_FILE=/absolute/private/claude-code/connection.json
29
+ claude
30
+ ```
31
+
32
+ Restart Claude after first installation/configuration. Hot-attachment to an
33
+ already running TUI is not supported. `PSST_CLAUDE_CONFIG` overrides the common
34
+ variable; without either, the plugin reads `~/.config/psst/claude.json`.
35
+ [Custom configuration and troubleshooting](docs/integration.md).
36
+
37
+ ## Use
38
+
39
+ Ask Claude to use Psst for the peers and purpose you authorize, or invoke `/psst:psst`.
40
+ The native MCP tools are:
41
+
42
+ - `broker_identity`: this exact session address.
43
+ - `broker_list_clients`: discover authorized peers; select the exact `client_id`.
44
+ - `broker_send`: send with a stable `send_key`; preserve it if the result is uncertain.
45
+ - `broker_receive`: read the FIFO head and its opaque `receipt_token`.
46
+ - `broker_acknowledge`: explicitly acknowledge that read using its `message_id`
47
+ and `receipt_token`, advancing to the next message.
48
+ - `broker_message_status`: broker delivery state.
49
+ - `broker_update_metadata`: descriptive name/project/model metadata.
50
+
51
+ No model tokens are used for transport heartbeats or durable inbox storage.
52
+ Mailbox reads are **cooperative**: incoming messages do not interrupt the user,
53
+ wake an idle agent, inject terminal input, or grant permission. Agent content is
54
+ always labeled untrusted AGENT data, including a peer's apparent “yes”.
55
+
56
+ ### What acknowledgment means
57
+
58
+ Broker `delivered` means safely stored in the connector inbox. Local explicit
59
+ acknowledgment means the model called `broker_acknowledge` with a receipt it saw.
60
+ It is **not** proof that the requested work succeeded, human approval, or a
61
+ native transcript persistence receipt. Lost responses are safe to retry with
62
+ the same receipt. Unacknowledged messages remain pending, including across an
63
+ MCP restart within the same confirmed native session/process lifetime.
64
+
65
+ A new Claude process creates a new broker identity. After `/clear`, reconnect
66
+ Psst through `/mcp` or restart Claude to register the new session. Old journals are
67
+ retained rather than silently routing their messages into another conversation.
68
+ No automatic replay into a different session is performed.
69
+
70
+ ## npm alternative
71
+
72
+ The prepared npm package is `@matuto-ai/claude-code-broker-connector`, with the `psst-claude`
73
+ CLI. Publication is a separate maintainer step; do not assume it is on npm yet.
74
+ A source checkout can already be packed and installed locally:
75
+
76
+ ```sh
77
+ npm pack
78
+ npm install --prefix /your/private/tools ./matuto-ai-claude-code-broker-connector-0.2.0.tgz
79
+ /your/private/tools/node_modules/.bin/psst-claude plugin-path
80
+ ```
81
+
82
+ Add that printed directory with `claude plugin marketplace add <directory>`,
83
+ then install `psst@psst-connectors` normally. The CLI also offers `config-example`
84
+ and `doctor [absolute-config-path]`; it never writes credentials or changes
85
+ Claude settings by itself. No postinstall scripts are used.
86
+
87
+ ## Maintainer checks
88
+
89
+ ```sh
90
+ npm run check
91
+ npm test
92
+ npm pack --dry-run
93
+ ```
94
+
95
+ Unit/pack tests are self-contained. `npm run test:integration` additionally needs
96
+ the pinned Psst broker checkout at `../ai-agent-msg-broker`; CI provides it. Native
97
+ plugin hooks, real broker transport, SQLite persistence, explicit multi-message
98
+ ACK, MCP restart, and session shutdown are exercised by a synthetic native
99
+ parent. [Verification details](docs/verification.md) distinguish this from a real
100
+ logged-in Claude/model exchange, which must be tested by the operator.
101
+
102
+ Bundled runtime source is unmodified and SHA256-checked against Psst commit
103
+ `ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`, with MIT attribution. The older
104
+ `bin/launch.mjs` exact-session launcher remains only as a compatibility fixture;
105
+ the normal installation path above does not use it.
package/bin/cli.mjs ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+ import { readPluginConfig, configPath } from '../src/plugin-state.mjs';
5
+ import { verifyReference } from '../src/reference-pin.mjs';
6
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
7
+ const [command, argument] = process.argv.slice(2);
8
+ try {
9
+ if (command === 'plugin-path') console.log(root);
10
+ else if (command === 'config-example') console.log(JSON.stringify({brokerUrl:'https://broker.example.invalid',tokenFile:'/absolute/private/claude/token',stateRoot:'/absolute/private/claude/state',name:'claude-code',project:null},null,2));
11
+ else if (command === 'doctor') {
12
+ if(process.platform !== 'linux' || Number(process.versions.node.split('.')[0]) !== 24) throw new Error('linux_node_24_required');
13
+ verifyReference(join(root,'vendor','psst'));
14
+ readPluginConfig(argument ?? configPath());
15
+ console.log('Local configuration and bundled runtime are valid. No network request, credential read, or live Claude session was tested.');
16
+ } else if (!command || ['--help','help'].includes(command)) console.log('psst-claude plugin-path | config-example | doctor [absolute-config-path]\nNative installation: claude plugin marketplace add <plugin-path>, then claude plugin install psst@psst-connectors. Restart Claude after configuration.');
17
+ else throw new Error('unknown_command');
18
+ } catch(error) { console.error(`psst-claude: ${/^[a-z0-9_]+$/.test(error.message)?error.message:'configuration_check_failed'}`);process.exitCode=1; }
package/bin/launch.mjs ADDED
@@ -0,0 +1,29 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { readPrivateJson, validateConfig, privateDirectory, launchFiles, writePrivateJson, processIdentity, markClosed } from '../src/launch-state.mjs';
6
+
7
+ let runDirectory;
8
+ try {
9
+ if (process.argv.length !== 3 || process.platform !== 'linux') throw new Error('usage_linux_launch_config');
10
+ const config = validateConfig(readPrivateJson(process.argv[2]));
11
+ const version = spawnSync(config.claudeBinary ?? 'claude', ['--version'], { encoding: 'utf8', timeout: 10000, maxBuffer: 4096 });
12
+ if (version.status !== 0 || version.stdout.trim().match(/^(\d+\.\d+\.\d+)(?:\s|$)/)?.[1] !== config.expectedCliVersion) throw new Error('cli_version_mismatch');
13
+ privateDirectory(config.stateRoot);
14
+ runDirectory = join(config.stateRoot, randomUUID());
15
+ const adapterRoot = dirname(dirname(fileURLToPath(import.meta.url)));
16
+ const { args } = launchFiles({ config, runDirectory, adapterRoot });
17
+ const close = () => { try { markClosed(runDirectory); } catch { /* Parent identity also expires. */ } };
18
+ process.once('exit', close);
19
+ // Inherit the real user's terminal, never synthesize keys, prompts or approvals.
20
+ const child = spawn(config.claudeBinary ?? 'claude', args, { stdio: 'inherit', shell: false });
21
+ if (!child.pid) throw new Error('cli_start_failed');
22
+ writePrivateJson(join(runDirectory, 'host.json'), processIdentity(child.pid));
23
+ const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', code => resolve(code ?? 1)); });
24
+ close(); process.exitCode = code;
25
+ } catch {
26
+ try { if (runDirectory) markClosed(runDirectory); } catch { /* No implicit recovery. */ }
27
+ process.stderr.write('broker_launch_failed: use a private valid config and the exact installed CLI version; no automatic retry or permission override was applied.\n');
28
+ process.exitCode = 1;
29
+ }
package/bin/mcp.mjs ADDED
@@ -0,0 +1,49 @@
1
+ import { verifyReference } from '../src/reference-pin.mjs';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { readPrivateJson, validateConfig, verifyLaunch, observedModel, closed } from '../src/launch-state.mjs';
6
+ import { createClaudeSessionBridge } from '../src/session-bridge.mjs';
7
+
8
+ let bridge, inbox, monitor;
9
+ try {
10
+ if (process.argv.length !== 3) throw new Error('invalid_mcp_arguments');
11
+ const run = process.argv[2], config = validateConfig(readPrivateJson(join(run, 'config.json'))), expected = readPrivateJson(join(run, 'launch.json'));
12
+ verifyReference(config.referenceRoot);
13
+ const load = path => import(pathToFileURL(join(config.referenceRoot, path)).href);
14
+ const [{ Inbox, newSessionIdentity }, { BrokerClient }, { AdapterRuntime }, { createTools }, { serveStdio }] = await Promise.all([
15
+ load('reference/inbox.mjs'), load('reference/client.mjs'), load('reference/runtime.mjs'), load('reference/tools.mjs'), load('reference/mcp-stdio.mjs'),
16
+ ]);
17
+ const identity = newSessionIdentity('claude-code');
18
+ // A restarted MCP subprocess gets a fresh identity and never adopts old pending
19
+ // mail. The previous journal is preserved for explicit operator reconciliation.
20
+ inbox = new Inbox({ directory: join(run, `inbox-${identity.client_id}`), identity });
21
+ let alive = true;
22
+ const lifetime = {}, binding = { nativeSessionId: expected.sessionId, lifetime };
23
+ const current = () => ({ ...binding, alive: alive && verifyLaunch(run, expected) });
24
+ const client = new BrokerClient({ url: config.brokerUrl, tokenFile: config.tokenFile, caFile: config.caFile,
25
+ insecureDevelopment: config.insecureDevelopment === true, inbox });
26
+ const runtime = new AdapterRuntime({ client, inbox, verifySession: () => current().alive, isAlive: () => current().alive });
27
+ bridge = createClaudeSessionBridge({ reference: { createTools, serveStdio }, client, inbox, runtime,
28
+ binding, getCurrentBinding: current, input: process.stdin, output: process.stdout,
29
+ observeMetadata: () => ({ model: observedModel(run) }),
30
+ onDiagnostic: ({ code }) => process.stderr.write(`${code}\n`),
31
+ });
32
+ process.stdin.once('end', () => { alive = false; });
33
+ // MCP discovery can finish while the native SessionStart hook runs. No broker
34
+ // registration, heartbeat, or tool effect occurs before that confirmation.
35
+ const deadline = Date.now() + 30000;
36
+ while (!current().alive) {
37
+ if (!alive || closed(run) || Date.now() >= deadline) throw new Error('session_confirmation_unavailable');
38
+ await delay(25);
39
+ }
40
+ await bridge.start({ name: config.name, project: config.project ?? null, model: observedModel(run) });
41
+ monitor = setInterval(() => { if (!current().alive) { alive = false; void bridge.close(); } }, 250);
42
+ await bridge.done;
43
+ } catch {
44
+ process.stderr.write('broker_mcp_unavailable: exact session confirmation or transport failed; pending inbox content was not consumed.\n');
45
+ process.exitCode = 1;
46
+ } finally {
47
+ if (monitor) clearInterval(monitor);
48
+ await bridge?.close(); inbox?.close();
49
+ }
@@ -0,0 +1,12 @@
1
+ import { applyHook, markClosed } from '../src/launch-state.mjs';
2
+ let bytes = 0, chunks = [];
3
+ try {
4
+ if (process.argv.length !== 3) throw new Error('invalid_hook_arguments');
5
+ for await (const chunk of process.stdin) { bytes += chunk.length; if (bytes > 256 * 1024) throw new Error('hook_input_too_large'); chunks.push(chunk); }
6
+ const result = applyHook(process.argv[2], JSON.parse(Buffer.concat(chunks).toString('utf8')));
7
+ if (result) process.stdout.write(JSON.stringify(result) + '\n');
8
+ } catch {
9
+ try { if (process.argv[2]) markClosed(process.argv[2]); } catch { /* Fail closed through missing/unreadable state. */ }
10
+ process.stderr.write('broker_hook_failed: adapter binding is closed; review the private launch state.\n');
11
+ process.exitCode = 2;
12
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { readPluginConfig, pluginHook } from '../src/plugin-state.mjs';
3
+ try {
4
+ let size=0; const chunks=[];
5
+ for await (const chunk of process.stdin) { size+=chunk.length; if(size>256*1024) throw new Error('hook_too_large'); chunks.push(chunk); }
6
+ const result=pluginHook(readPluginConfig(),JSON.parse(Buffer.concat(chunks).toString('utf8')));
7
+ if(result) process.stdout.write(JSON.stringify(result)+'\n');
8
+ } catch {
9
+ process.stderr.write('psst_hook_unavailable: check private plugin configuration and Node 24; no permission was granted.\n');
10
+ process.exitCode=2;
11
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { writeFileSync } from 'node:fs';
6
+ import { readPrivateJson, privateDirectory } from '../src/launch-state.mjs';
7
+ import { verifyReference } from '../src/reference-pin.mjs';
8
+ import { readPluginConfig, hostDirectory, readBinding, verifyBinding, bindingModel } from '../src/plugin-state.mjs';
9
+
10
+ let server, runtime, inbox, monitor, live=true, closeSessionOnExit=true;
11
+ try {
12
+ const root=dirname(dirname(fileURLToPath(import.meta.url))), reference=join(root,'vendor','psst');
13
+ verifyReference(reference);
14
+ const [{Inbox,newSessionIdentity},{BrokerClient},{AdapterRuntime},{createTools},{createCooperativeInbox},{serveStdio}]=await Promise.all([
15
+ import('../vendor/psst/reference/inbox.mjs'),import('../vendor/psst/reference/client.mjs'),import('../vendor/psst/reference/runtime.mjs'),import('../vendor/psst/reference/tools.mjs'),import('../src/cooperative-inbox.mjs'),import('../src/cooperative-stdio.mjs')]);
16
+ const config=readPluginConfig(),{directory,host}=hostDirectory(config);
17
+ let expected=null, started=false;
18
+ const active=()=>live && expected && verifyBinding(directory,expected);
19
+ const unavailable=()=>({error:{code:'session_binding_unavailable',retryable:true,effect:'not_applied',message:'Psst awaits the native SessionStart hook. Restart Claude after installing/configuring the plugin.'}});
20
+ let tools=null;
21
+ // Initialize MCP immediately; tools have no effect until native hook confirmation.
22
+ const names=['broker_list_clients','broker_send','broker_update_metadata','broker_message_status','broker_receive','broker_acknowledge','broker_identity'];
23
+ const guarded=Object.fromEntries(names.map(name=>[name,async args=>{
24
+ if(!started || !active()) return unavailable();
25
+ return tools[name](args);
26
+ }]));
27
+ server=serveStdio({input:process.stdin,output:process.stdout,tools:guarded,onClose:async()=>{closeSessionOnExit=!active();live=false;await runtime?.stop({closeSession:closeSessionOnExit});},onDiagnostic:()=>{}});
28
+ const deadline=Date.now()+30000;
29
+ while(live && !expected) {
30
+ try { const binding=readBinding(directory);if(binding.host.pid===host.pid && verifyBinding(directory,binding)) expected=binding; } catch { /* Hook and MCP startup ordering can vary. */ }
31
+ if(!expected) {if(Date.now()>=deadline) throw new Error('session_confirmation_timeout');await delay(25);}
32
+ }
33
+ if(!live) throw new Error('mcp_closed');
34
+ const journal=privateDirectory(join(directory,`inbox-${expected.lifetime}`));
35
+ const identityFile=join(journal,'identity.json');
36
+ try {writeFileSync(identityFile,JSON.stringify(newSessionIdentity('claude-code'))+'\n',{mode:0o600,flag:'wx'});} catch(error) {if(error.code!=='EEXIST')throw error;}
37
+ const identity=readPrivateJson(identityFile);
38
+ inbox=new Inbox({directory:journal,identity});
39
+ const client=new BrokerClient({url:config.brokerUrl,tokenFile:config.tokenFile,caFile:config.caFile,insecureDevelopment:config.insecureDevelopment===true,inbox});
40
+ runtime=new AdapterRuntime({client,inbox,verifySession:active,isAlive:active,onDiagnostic:({code})=>process.stderr.write(`psst_${code}\n`)});
41
+ tools={...createTools({client,inbox,observeMetadata:()=>({model:bindingModel(readBinding(directory))})}),...createCooperativeInbox({inbox,isActive:active}),
42
+ broker_identity:async()=>({client_id:identity.client_id,client_type:'claude-code',native_session_id:expected.sessionId,local_mode:'cooperative_pull',automatic_wakeup:false})};
43
+ await runtime.start({name:config.name,project:config.project??null,model:bindingModel(expected)});
44
+ if(!active()) throw new Error('session_changed');
45
+ started=true;
46
+ monitor=setInterval(()=>{if(!active()){live=false;void server.close();}},250);
47
+ await server.done;
48
+ } catch {
49
+ process.stderr.write('psst_mcp_unavailable: check Node 24, private config/token, HTTPS access, and restart Claude with the native plugin. No pending message was implicitly acknowledged.\n');
50
+ process.exitCode=1;
51
+ } finally {
52
+ live=false;if(monitor)clearInterval(monitor);
53
+ await server?.close();await runtime?.stop({closeSession:closeSessionOnExit});inbox?.close();
54
+ }
@@ -0,0 +1,99 @@
1
+ # Native Claude integration
2
+
3
+ ## Configuration
4
+
5
+ Use one client's enrollment bundle, not another client's token. `PSST_CONFIG_FILE`
6
+ may point directly to `connection.json` in `broker-client-bundle-v1` format. It must
7
+ allow `claude-code`; `token_file` resolves relative to that JSON file. State goes
8
+ in its adjacent `state/` directory. The broker URL is HTTPS; ordinary system CA
9
+ validation applies. No TLS override or custom CA is needed for public certificates.
10
+
11
+ Alternatively, create a private JSON config at `~/.config/psst/claude.json` or set
12
+ `PSST_CLAUDE_CONFIG` to its absolute path:
13
+
14
+ ```json
15
+ {
16
+ "brokerUrl": "https://broker.example.invalid",
17
+ "tokenFile": "/absolute/private/claude/token",
18
+ "stateRoot": "/absolute/private/claude/state",
19
+ "name": "claude-reviewer",
20
+ "project": "example-project"
21
+ }
22
+ ```
23
+
24
+ JSON and token files must belong to the sandbox user and have mode `0600`; state
25
+ must be an owned, nonsymlink, private `0700` directory. Keep it on persistent local
26
+ storage, not NFS/CIFS. Optional `caFile` is an absolute path for a private CA. The
27
+ `insecureDevelopment` option only permits loopback HTTP, for disposable tests.
28
+ Do not put tokens in JSON, shell arguments, README files, or repository settings.
29
+
30
+ `PSST_CLAUDE_CONFIG` wins over `PSST_CONFIG_FILE`. The native hook and MCP subprocess
31
+ must inherit the same variables. Install Node 24 inside the sandbox or place its
32
+ existing executable first on PATH before launching Claude; this does not require
33
+ a global Node upgrade on the host. The plugin never creates or migrates credentials.
34
+
35
+ ## Native lifecycle and isolation
36
+
37
+ Claude's documented exec-form hooks run `node bin/plugin-hook.mjs` directly.
38
+ The MCP stdio subprocess is also launched directly by Claude. Their parent PID,
39
+ Linux process start counter, and boot identity select a private host directory.
40
+ SessionStart records the native session ID and an opaque lifetime; MCP waits up to
41
+ 30 seconds for it, while still answering the protocol initialization handshake.
42
+ A host lacking the same direct-parent relationship fails closed instead of guessing
43
+ by project, process name, transcript path, or most recent session. The real native
44
+ exec relationship is checked by the compatibility audit, not a generic MCP promise.
45
+
46
+ Normal compaction with the same native session preserves the lifetime. SessionEnd
47
+ closes it. After `/clear` or another native session switch, reconnect Psst through `/mcp`
48
+ or restart Claude; the old MCP intentionally stops instead of rebinding silently.
49
+ Session switch invalidates the old binding; old messages are never
50
+ adopted by the new session. PreToolUse can only deny wrong-session and subagent
51
+ access to Psst tools; it never auto-approves tool usage. No permission-relay or
52
+ Channels capability is advertised. State includes no model credentials or transcript.
53
+
54
+ An MCP restart while the same native host and lifetime remain live can reopen the
55
+ same SQLite journal and broker lease. The OS ownership lock prevents concurrent
56
+ MCP processes from owning it. A new host/lifetime always gets a separate identity.
57
+ A hard native crash falls back to broker lease expiry. Durable old journal files
58
+ remain for operator inspection; do not copy them into another live session.
59
+
60
+ ## Test with a peer
61
+
62
+ 1. Enroll/configure both clients and restart their native sessions.
63
+ 2. In Claude ask for `broker_identity`, then `broker_list_clients`.
64
+ 3. Select the other client's exact UUID. Send a short test with a unique stable
65
+ `send_key`. Do not select by a duplicate display name alone.
66
+ 4. At the recipient, call receive, inspect the labeled AGENT text, then explicitly
67
+ acknowledge its message ID and receipt token. Call receive again to verify FIFO
68
+ advancement. Ask for a reply to the original Claude UUID.
69
+ 5. In Claude receive/ack the reply. Status `delivered` is storage, not a completed task.
70
+
71
+ A healthy `/healthz` alone does not establish authentication or successful messaging.
72
+ Ordinary native MCP approval prompts are for the human to handle. If a tool response
73
+ is uncertain, preserve operation identifiers rather than sending a new operation.
74
+
75
+ ## Troubleshooting and uninstall
76
+
77
+ - `psst_hook_unavailable`: private config path, Node version/PATH, directory/file
78
+ permissions, or unsupported native parent relationship. Restart after fixing it.
79
+ - `session_binding_unavailable`: SessionStart has not confirmed this exact native
80
+ process/session, or it changed. Do not reuse someone else's state directory.
81
+ - `psst_unauthenticated` / `psst_forbidden`: check enrollment and client type/scope.
82
+ - No new message while idle: expected. Ask for a cooperative receive; there is no wakeup.
83
+ - Duplicate receive: expected until explicit acknowledgment. Retry ACK with the exact
84
+ same receipt after an uncertain result; never invent a receipt or delete pending rows.
85
+
86
+ Disable/remove through Claude's normal plugin manager:
87
+
88
+ ```sh
89
+ claude plugin uninstall psst@psst-connectors
90
+ ```
91
+
92
+ Then close/restart affected sessions. This does not revoke broker credentials or
93
+ delete the private external state directory. Revoke credentials separately when
94
+ needed; retain pending journals until reconciled. Do not install both the legacy
95
+ wrapper and native plugin in the same session.
96
+
97
+ Official references: [plugins](https://code.claude.com/docs/en/discover-plugins),
98
+ [plugin manifest](https://code.claude.com/docs/en/plugins-reference),
99
+ [hooks](https://code.claude.com/docs/en/hooks), [MCP](https://code.claude.com/docs/en/mcp).
@@ -0,0 +1,35 @@
1
+ # Distribution and npm publishing
2
+
3
+ The supported native Claude marketplace install works directly from the reviewed
4
+ GitHub branch/tag. npm is optional; it distributes the same self-contained plugin
5
+ files and helper CLI, not a separate model client.
6
+
7
+ The npm package is prepared as `@matuto-ai/claude-code-broker-connector` version `0.2.0`.
8
+ No package is published by this change. This is a new npm identity under the
9
+ `@matuto-ai` organization, not a transfer or deprecation of the unscoped package.
10
+ The owner must verify organization publishing rights before first publication.
11
+ Native plugin `psst`, marketplace `psst-connectors`, and CLI `psst-claude` remain unchanged.
12
+
13
+ The release-triggered workflow follows the existing pi-intercom-review-pair
14
+ trusted-publishing pattern, adding version matching, full tests, pack/install
15
+ checks, pinned Actions, and an integration gate before publishing. It does not
16
+ store an npm token in this repository.
17
+
18
+ For the scoped npm package, configure npm's GitHub Actions trusted publisher for:
19
+ - GitHub owner: `matuto-ai`
20
+ - Repository: `claude-code-broker-connector`
21
+ - Workflow filename: `npm-publish.yml`
22
+
23
+ Creating/configuring that publisher is an account-security action for the owner.
24
+ If npm requires an initial package publication before a trusted publisher can be
25
+ configured, complete that one-time bootstrap in your authenticated npm environment.
26
+ Do not paste a token into chat or into a committed workflow. The workflow cannot
27
+ magically create publisher permissions.
28
+
29
+ After reviewing and merging the PR, create release `v0.2.0` from the reviewed
30
+ commit. The tag must match package.json. Tests run first, then npm publication
31
+ uses short-lived OIDC and provenance. Verify the Actions run and npm package
32
+ version before giving users npm install instructions. Native GitHub installation
33
+ does not require waiting for npm.
34
+
35
+ Official documentation: https://docs.npmjs.com/trusted-publishers/
@@ -0,0 +1,123 @@
1
+ # Native plugin verification — October 24, 2026
2
+
3
+ The `0.2.0` native plugin replaces the wrapper-only installation path described
4
+ in the historical notes below. The legacy wrapper remains tested but is not the
5
+ normal operator entry point.
6
+
7
+ On cloud Linux, Node 24.22.1:
8
+
9
+ - Syntax checks passed for all adapter and vendored modules.
10
+ - Unit tests cover native binding/lifecycle, config bundle restrictions, source
11
+ pins, stdio boundaries, explicit cooperative acknowledgment, malformed/unread/
12
+ departed/uncertain message rejection, durable receipt retries and storage faults.
13
+ - A real `npm pack` artifact was installed offline with scripts disabled into an
14
+ empty temporary prefix. Native manifests/hooks/MCP/runtime/license were present;
15
+ helper CLI and local doctor ran without a sibling broker checkout or network call.
16
+ - Two real-broker integration tests passed, including native-plugin startup before
17
+ SessionStart, multiple messages, exact receipt acknowledgment, reply, same-native-
18
+ session MCP restart preserving identity/inbox, and native SessionEnd closure.
19
+ Their native parent is synthetic, not a logged-in Claude model session.
20
+ - Official Claude Code **2.1.309** (`@anthropic-ai/claude-code-linux-x64`) validated
21
+ the native plugin/marketplace manifest. The only remaining warning is cosmetic:
22
+ no directory listing icon. This is not a marketplace approval claim.
23
+ - With a fresh isolated HOME and **no authentication/model credentials**, actual
24
+ Claude `--plugin-dir ... --init-only` launched an instrumented MCP process and
25
+ exec-form SessionStart hook. Both reported the same native Claude parent PID and
26
+ Linux process-start identity; SessionStart supplied the real native session UUID.
27
+ This verifies the critical direct-parent binding assumption, independently of
28
+ synthetic transport tests. A repeatable `scripts/audit-native.mjs` probe accepts
29
+ an existing official executable via `CLAUDE_AUDIT_BIN` and makes no model request.
30
+
31
+ ```sh
32
+ CLAUDE_AUDIT_BIN=/absolute/path/to/claude npm run audit:claude
33
+ ```
34
+
35
+ Shared runtime hashes remain byte-identical to the reviewed Psst commit below.
36
+ The separately attributed adapter files add explicit application read ACK and
37
+ bounded MCP tool schemas; **they do not fabricate native acceptance receipts**.
38
+
39
+ Not tested here: authenticated Claude/model round-trip in Bruno's sandbox,
40
+ permission UI, his TLS/token mounts, all Claude versions, non-Linux platforms,
41
+ or automatic wakeup. Those are not implied by health checks or the native ABI
42
+ probe. Restart after installation; after `/clear`, reconnect Psst or restart.
43
+ No user credentials, production configuration, npm release or main merge occurred.
44
+
45
+ ## Historical wrapper foundation evidence
46
+
47
+
48
+ 2026-10-23 cloud Linux, Node 24.19.0 / SQLite 3.53.3:
49
+ `npm run check` and **12/12 tests passed** against shared broker commit
50
+ `ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`.
51
+
52
+ The end-to-end fixture spawns a **synthetic Claude CLI**, real native-hook helper
53
+ processes, the real standard MCP subprocess, shared HTTP broker and SQLite inbox.
54
+ It checks lifecycle confirmation, exact ownership, delivery ACK, model-visible
55
+ AGENT warning, cooperative non-consumption and clean native shutdown. Focused
56
+ tests cover main/subagent guards, unknown/switched lifetime, PID reuse defense,
57
+ model metadata, EOF during registration, bounded arguments and tamper-rejected
58
+ reference source. The fake executable's `0.0.0` version is not a vendor version.
59
+
60
+ Not run: actual authenticated Claude TUI/broker flow, provider calls, real permission
61
+ UI, native hook ordering under clear/resume/compact/model switch, rootless Podman
62
+ or two-host TLS LAN. Ordinary MCP tools may require human approval; none was
63
+ bypassed. This PR is not a full issue #9 completion claim.
64
+
65
+ Required next gates:
66
+
67
+ 1. Pin an official CLI version and verify all launch/hook events against it,
68
+ including root session and subagent tool routing and MCP restart.
69
+ 2. Confirm actual provider-visible tool-result provenance with hostile body text
70
+ (`yes`, fake channel boundaries and prompt-injection strings).
71
+ 3. Keep human drafts, active work and approvals untouched. Cooperative tools are
72
+ invoked only in native flow; no automatic event may inject a prompt.
73
+ 4. Establish a supported durable native acceptance receipt before consuming any
74
+ inbox head. Channel opt-in alone is insufficient and permission relay must
75
+ remain disabled.
76
+ 5. Package and test separate CLI+adapter rootless containers and two-host LAN.
77
+
78
+
79
+ ## Official Claude 2.1.309 compatibility audit
80
+
81
+ On 2026-10-23, the official npm packages `@anthropic-ai/claude-code@2.1.309`
82
+ and `@anthropic-ai/claude-code-linux-x64@2.1.309` were unpacked in a disposable
83
+ cloud directory with npm lifecycle scripts disabled. No user installation,
84
+ credentials, login, model request or existing configuration was used.
85
+
86
+ The actual native Linux x64 executable reported `2.1.309 (Claude Code)` and
87
+ internal commit `1ce7e4ff2da8`. Its SHA256 was:
88
+
89
+ ```text
90
+ 4d704a8aecbe038affa3000f498ae17c1b4f753add8facb8764120affc99b658
91
+ ```
92
+
93
+ Verified with a fresh private HOME and empty test project:
94
+
95
+ 1. Actual `--help` lists `--session-id`, `--settings` and `--mcp-config`. The
96
+ version output matches the existing launcher's exact-version check.
97
+ 2. `claude --settings <generated-settings.json> doctor` accepted the launcher's
98
+ SessionStart/SessionEnd/PostModelSwitch/PreToolUse hook configuration. A
99
+ negative control changing `args` from an array to a number produced an explicit
100
+ invalid-settings diagnostic. Doctor exits zero even for that diagnostic, so
101
+ the output—not exit code alone—was checked.
102
+ 3. `claude --settings <isolated-setup-hook.json> --init-only` executed a Setup hook
103
+ with the same `command` plus `args` form. One argument containing spaces,
104
+ apostrophe, dollar sign, semicolon and backtick arrived unchanged. It exited
105
+ zero without authentication or a model call. This verifies exec-form mechanics;
106
+ it does not claim that Setup is an adapter lifecycle event.
107
+
108
+ This agrees with the official [hook exec-form reference](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form)
109
+ and [CLI flags reference](https://code.claude.com/docs/en/cli-reference#cli-flags).
110
+ No runtime source change was needed. Shared reference pin and the 12-test
111
+ synthetic/real-broker fixture coverage above are unchanged.
112
+
113
+ For a user-local test, set `expectedCliVersion` to `2.1.309` and `claudeBinary`
114
+ to the absolute installed executable path in each private adapter config. Launch
115
+ `bin/launch.mjs` with an explicit Node 24.19.0 executable: that exact Node path is
116
+ propagated to hooks and MCP subprocesses, without changing global Node. Use two
117
+ fresh interactive sessions with separate state directories/names, and approve
118
+ ordinary native MCP prompts yourself. See [launch configuration](integration.md).
119
+
120
+ **Still unverified by this audit:** authenticated SessionStart-to-MCP ordering,
121
+ actual two-session broker/model exchange, permission UI, clear/resume/compact and
122
+ subagent routing in the real TUI. There were no live broker or model calls in this
123
+ compatibility audit. The adapter still peeks rather than consumes its inbox.