@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.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +5 -0
- package/LICENSE +21 -0
- package/README.md +104 -2
- package/bin/cli.mjs +18 -0
- package/bin/launch.mjs +29 -0
- package/bin/mcp.mjs +49 -0
- package/bin/native-hook.mjs +12 -0
- package/bin/plugin-hook.mjs +11 -0
- package/bin/plugin-mcp.mjs +54 -0
- package/docs/integration.md +99 -0
- package/docs/publishing.md +35 -0
- package/docs/verification.md +123 -0
- package/hooks/hooks.json +61 -0
- package/package.json +39 -4
- package/scripts/audit-native.mjs +23 -0
- package/skills/psst/SKILL.md +14 -0
- package/src/cooperative-inbox.mjs +50 -0
- package/src/cooperative-stdio.mjs +156 -0
- package/src/launch-state.mjs +115 -0
- package/src/plugin-state.mjs +61 -0
- package/src/reference-pin.mjs +21 -0
- package/src/session-bridge.mjs +67 -0
- package/vendor/psst/LICENSE +21 -0
- package/vendor/psst/NOTICE.md +11 -0
- package/vendor/psst/reference/client.mjs +318 -0
- package/vendor/psst/reference/errors.mjs +42 -0
- package/vendor/psst/reference/inbox.mjs +289 -0
- package/vendor/psst/reference/mcp-stdio.mjs +153 -0
- package/vendor/psst/reference/runtime.mjs +306 -0
- package/vendor/psst/reference/tools.mjs +59 -0
- package/vendor/psst/src/protocol.mjs +299 -0
|
@@ -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
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
|
-
#
|
|
1
|
+
# Psst for Claude Code
|
|
2
2
|
|
|
3
|
-
|
|
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.
|