Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: ci

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: '24'
package-manager-cache: false
- run: npm ci
- run: npm run lint
- run: npm test
- name: Verify publish contents
run: npm pack --dry-run
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
# Changelog

All notable changes to `pilot-mcp` documented here. Format follows
All notable changes to the `pilotprotocol-mcp` npm adapter are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and adheres to
[Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.2.11] - 2026-08-07

### Fixed
- The exact obsolete `heartbeat --claude` command now exits successfully and
silently while current setup removes it. Machines carrying settings from
releases <=0.2.5 therefore no longer reject every Claude Code prompt before
the harness is restarted.
- Security and internals documentation now describe the current pre/post tool
enforcement boundary and the separate `pilotprotocol-mcp` npm package.
- Generated hook and MCP commands now pin `pilotprotocol-mcp@0.2.11`.
- The compatibility setup path now checksum-pins the installer-compatible
public core runtime `managed-runtime-v0.1.5` instead of the superseded
`v0.1.2` bundle.

## [0.2.10] - 2026-08-07

### Fixed
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# pilot-mcp
# pilotprotocol-mcp

The npm package is **`pilotprotocol-mcp`**. Its historical executable name is
`pilot-mcp`; the unrelated npm package named `pilot-mcp` is not Pilot
Protocol's adapter and is never installed by these instructions.

**Your agent's overlay network — local or hosted, your choice.** 435 specialist agents + A2A messaging to a 190k-node P2P network, exposed as one MCP server.

Expand Down
20 changes: 11 additions & 9 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ advisory on the GitHub Security Advisories tab once a fix is available.
## Scope

In scope:
- `pilot-mcp` npm package and its platform subpackages
- `pilotprotocol-mcp` npm package and its published adapter distributions
- The `pilot-mcp setup` auto-config flow (config file writes, daemon install)
- The MCP server implementations (stdio + HTTP)
- The Go binaries (`pilot-daemon`, `pilotctl`) shipped in the platform subpackages
- The stdio MCP server and native harness pre/post adapters
- Runtime discovery and the checksum-verified public core bootstrap retained
for compatibility with existing setup flows

Out of scope (report upstream):
- Vulnerabilities in the Pilot Protocol itself → security@pilotprotocol.network
Expand All @@ -27,19 +28,20 @@ Notable classes we explicitly defend against:

1. **Hook-execution attack via config write.** `pilot-mcp setup` writes to
harness config files. We do not write arbitrary executable commands —
only `npx -y pilotprotocol-mcp` and `pilot-mcp heartbeat --<harness>`, both stable
audited entrypoints. We never accept user-influenced strings into hook
command fields.
only version-pinned `pilotprotocol-mcp` stdio and native `hook --harness
<id> --phase pre|post` commands. The exact historical `heartbeat --claude`
invocation is retained only as a silent, non-networked compatibility shim
while setup removes obsolete settings. We never accept user-influenced
strings into hook command fields.

2. **Supply chain.** Releases are tagged + signed; npm publishes use
`--provenance`; Docker images are SBOM + Sigstore signed via the
Docker-built path. Tarball downloads in `install.js` go through the npm
registry (never raw GitHub Releases) for corporate-proxy compatibility.

3. **Daemon socket trust.** `pilot-mcp` talks to the local daemon via Unix
socket / named pipe. We rely on OS process boundary for auth; HTTP mode
uses a per-install bearer token in `~/.pilot/mcp-token` bound to loopback
by default.
socket / named pipe. We rely on the OS process boundary for local socket
access. The adapter does not expose an HTTP transport.

4. **Identity exfiltration.** The user's Ed25519 private key (`~/.pilot/identity.json`)
is never read by `pilot-mcp` directly — only the daemon touches it.
Expand Down
12 changes: 12 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// pilot-mcp attach [flags] → configure harness hooks around an already-adopted core node
// pilot-mcp doctor → diagnose daemon/registry/harness state
// pilot-mcp hook --harness <id> → native pre/post tool enforcement bridge
// pilot-mcp heartbeat --claude → silent compatibility shim for obsolete hooks
// pilot-mcp tour → first-run guided demo (one specialist call)
// pilot-mcp export-identity → write identity to portable file
// pilot-mcp import-identity <f> → load identity from portable file
Expand Down Expand Up @@ -72,6 +73,17 @@ async function main() {
await runHook(parseFlags(args.slice(1)));
break;
}
case 'heartbeat': {
// Releases <=0.2.5 installed this command in Claude Code's
// UserPromptSubmit hook. Current setup removes that hook because prompt
// submission is not a tool-action enforcement boundary, but an existing
// Claude process can keep the old settings loaded until it restarts.
// Keep the exact legacy invocation as a silent allow so an obsolete
// config cannot reject every prompt while migration catches up.
const { runLegacyHeartbeat } = await import('./src/hooks/legacy-heartbeat.js');
await runLegacyHeartbeat(args.slice(1));
break;
}
case 'picoclaw-hook': {
const { runPicoClawRPC } = await import('./src/hooks/picoclaw-rpc.js');
await runPicoClawRPC();
Expand Down
20 changes: 14 additions & 6 deletions docs/INTERNALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,21 @@ from (in order):

If none exist, we surface an MCP error pointing at `pilot-mcp setup`.

## Per-turn injection is NOT MCP's job
## Native action hooks complement MCP

MCP only fires on `tools/call`. It cannot inject context into the system or
user prompt on every turn. For the "always check pilot before web_search"
directive to land every turn, we need per-harness hooks:
- Claude Code: `UserPromptSubmit` hook in `~/.claude/settings.json`
(`pilot-mcp heartbeat --claude` returns `additionalContext`)
MCP governs only calls routed through Pilot's MCP tools. Where a harness offers
native interception, setup also installs a pre/post action boundary so Pilot
can evaluate the exact tool input before execution and retain the result after
execution. In particular, Claude Code uses `PreToolUse`, `PostToolUse`, and
`PostToolUseFailure` in `~/.claude/settings.json`.

Releases <=0.2.5 briefly installed a `UserPromptSubmit` heartbeat command that
never existed. Setup now removes only that obsolete Pilot entry, preserving
unrelated prompt hooks. The CLI retains the exact historical
`heartbeat --claude` spelling as a silent compatibility shim so a running
Claude process with cached settings cannot reject prompts before restart.

Other harness boundaries include:
- OpenHands: Claude-compatible hook JSON, discovered per repository from
`.openhands/hooks.json`; a user-home hook is not fleet-wide enforcement
- PicoClaw: `hooks.processes.PreMessage` with `inject_output: true` — but ONLY
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pilotprotocol-mcp",
"version": "0.2.10",
"version": "0.2.11",
"mcpName": "io.github.pilot-protocol/pilot-mcp",
"description": "Your agent's overlay network. MCP server exposing 436 Pilot specialist agents + P2P A2A messaging. One install configures every harness on your machine.",
"type": "module",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
"url": "https://github.com/pilot-protocol/pilot-mcp",
"source": "github"
},
"version": "0.2.10",
"version": "0.2.11",
"websiteUrl": "https://pilotprotocol.network",
"packages": [
{
"registryType": "npm",
"identifier": "pilotprotocol-mcp",
"version": "0.2.10",
"version": "0.2.11",
"transport": { "type": "stdio" }
}
]
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/legacy-heartbeat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Claude Code treats exit 0 with no stdout as "allow" for UserPromptSubmit.
// Do not print JSON or context here: this command exists only to make the
// obsolete <=0.2.5 hook harmless until setup removes it from settings.json.
export async function runLegacyHeartbeat(args) {
if (args.length !== 1 || args[0] !== '--claude') {
throw new Error('heartbeat is a legacy compatibility command; only --claude is supported');
}
}
2 changes: 1 addition & 1 deletion src/setup/harnesses/claude.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function registerMcp() {
current.mcpServers = current.mcpServers ?? {};
current.mcpServers.pilot = {
command: 'npx',
args: ['-y', 'pilotprotocol-mcp@0.2.10'],
args: ['-y', 'pilotprotocol-mcp@0.2.11'],
};
writeFileSync(SETTINGS, JSON.stringify(current, null, 2));
}
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/cline.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function configure() {
mkdirSync(dirname(settings), { recursive: true });
const current = existsSync(settings) ? JSON.parse(readFileSync(settings, 'utf8')) : {};
current.mcpServers = current.mcpServers ?? {};
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
writeFileSync(settings, JSON.stringify(current, null, 2));
installNativeHook('PreToolUse', 'pre');
installNativeHook('PostToolUse', 'post');
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const HOOKS = join(HOME, '.codex', 'hooks.json');
const BLOCK = `
[mcp_servers.pilot]
command = "npx"
args = ["-y", "pilotprotocol-mcp@0.2.10"]
args = ["-y", "pilotprotocol-mcp@0.2.11"]
`;

export async function configure() {
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/continue.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ schema: v1
mcpServers:
- name: pilot
command: npx
args: ["-y", "pilotprotocol-mcp@0.2.10"]
args: ["-y", "pilotprotocol-mcp@0.2.11"]
`;

export async function configure() {
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function configure() {
mkdirSync(dirname(settings), { recursive: true });
const current = existsSync(settings) ? JSON.parse(readFileSync(settings, 'utf8')) : {};
current['github.copilot.chat.mcp.servers'] = current['github.copilot.chat.mcp.servers'] ?? {};
current['github.copilot.chat.mcp.servers'].pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current['github.copilot.chat.mcp.servers'].pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
writeFileSync(settings, JSON.stringify(current, null, 2));
installHooks();
}
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function configure() {
mkdirSync(dirname(MCP_JSON), { recursive: true });
const current = existsSync(MCP_JSON) ? JSON.parse(readFileSync(MCP_JSON, 'utf8')) : {};
current.mcpServers = current.mcpServers ?? {};
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
writeFileSync(MCP_JSON, JSON.stringify(current, null, 2));

const hooks = existsSync(HOOKS_JSON) ? JSON.parse(readFileSync(HOOKS_JSON, 'utf8')) : { version: 1, hooks: {} };
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function configure() {
mkdirSync(dirname(SETTINGS), { recursive: true });
const current = existsSync(SETTINGS) ? JSON.parse(readFileSync(SETTINGS, 'utf8')) : {};
current.mcpServers = current.mcpServers ?? {};
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
current.hooksConfig = current.hooksConfig ?? {};
if (current.hooksConfig.enabled === undefined) current.hooksConfig.enabled = true;
current.hooks = current.hooks ?? {};
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/hermes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function configure() {
}
document.setIn(['mcp_servers', 'pilot'], {
command: 'npx',
args: ['-y', 'pilotprotocol-mcp@0.2.10'],
args: ['-y', 'pilotprotocol-mcp@0.2.11'],
});
installHook(document, 'pre_tool_call', 'pre');
installHook(document, 'post_tool_call', 'post');
Expand Down
4 changes: 2 additions & 2 deletions src/setup/harnesses/junie.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ const CLI_CONFIG = join(HOME, '.junie', 'config.json');
export async function configure() {
if (!existsSync(CLI_CONFIG)) {
mkdirSync(dirname(CLI_CONFIG), { recursive: true });
writeFileSync(CLI_CONFIG, JSON.stringify({ mcpServers: { pilot: { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] } } }, null, 2));
writeFileSync(CLI_CONFIG, JSON.stringify({ mcpServers: { pilot: { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] } } }, null, 2));
return;
}
const current = JSON.parse(readFileSync(CLI_CONFIG, 'utf8'));
current.mcpServers = current.mcpServers ?? {};
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
writeFileSync(CLI_CONFIG, JSON.stringify(current, null, 2));
}
2 changes: 1 addition & 1 deletion src/setup/harnesses/openclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function configure() {
if (!existsSync(CONFIG)) return; // OpenClaw not configured yet — caller already skipped
const current = JSON.parse(readFileSync(CONFIG, 'utf8'));
current.mcpServers = current.mcpServers ?? {};
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current.mcpServers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
writeFileSync(CONFIG, JSON.stringify(current, null, 2));
mkdirSync(join(HOME, '.pilot', 'integrations'), { recursive: true });
cpSync(SOURCE_PLUGIN, INSTALLED_PLUGIN, { recursive: true, force: true });
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/openhands.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const CONFIG = join(HOME, '.openhands', 'config.toml');
const MCP_BLOCK = `
[mcp.stdio_servers.pilot]
command = "npx"
args = ["-y", "pilotprotocol-mcp@0.2.10"]
args = ["-y", "pilotprotocol-mcp@0.2.11"]
`;

export async function configure(options = {}) {
Expand Down
4 changes: 2 additions & 2 deletions src/setup/harnesses/picoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function configure() {
current.tools = current.tools ?? {};
current.tools.mcp = current.tools.mcp ?? {};
current.tools.mcp.servers = current.tools.mcp.servers ?? {};
current.tools.mcp.servers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.10'] };
current.tools.mcp.servers.pilot = { command: 'npx', args: ['-y', 'pilotprotocol-mcp@0.2.11'] };
current.hooks = current.hooks ?? {};
current.hooks.enabled = true;
current.hooks.defaults = current.hooks.defaults ?? {};
Expand All @@ -26,7 +26,7 @@ export async function configure() {
enabled: true,
priority: 10,
transport: 'stdio',
command: ['npx', '-y', 'pilotprotocol-mcp@0.2.10', 'picoclaw-hook'],
command: ['npx', '-y', 'pilotprotocol-mcp@0.2.11', 'picoclaw-hook'],
intercept: ['before_tool', 'after_tool'],
};
writeFileSync(CONFIG, JSON.stringify(current, null, 2));
Expand Down
2 changes: 1 addition & 1 deletion src/setup/harnesses/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Requiring a separate global install makes onboarding appear successful while
// the first real tool call fails with "pilot-mcp: command not found".

export const PILOT_RUNNER = 'npx -y pilotprotocol-mcp@0.2.10';
export const PILOT_RUNNER = 'npx -y pilotprotocol-mcp@0.2.11';

export function hookCommand(harness, phase) {
return `${PILOT_RUNNER} hook --harness ${harness} --phase ${phase}`;
Expand Down
18 changes: 9 additions & 9 deletions src/setup/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@ import { pilotctlBinaryPath } from '../daemon-bridge.js';
const DEFAULT_MANIFEST = 'https://pilotprotocol.network/.well-known/latest.json';
const MAX_RUNTIME_ARCHIVE_BYTES = 128 * 1024 * 1024;
const MANAGED_RUNTIME = Object.freeze({
tag: 'managed-runtime-v0.1.2',
tag: 'managed-runtime-v0.1.5',
platforms: Object.freeze({
'darwin-amd64': Object.freeze({
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.2/pilot-darwin-amd64.tar.gz',
sha256: '878f5e029424a3726f6ebec4195da619794844bf36487e1c3c8e223cc64a90ff',
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.5/pilot-darwin-amd64.tar.gz',
sha256: '0d8bbda818bbfa1df9544fe114353eca097d3f7b7e356c4c5ceac1eaaf3f6d63',
}),
'darwin-arm64': Object.freeze({
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.2/pilot-darwin-arm64.tar.gz',
sha256: '889217ebcc6ecbef77dd694d0ca553c45688183e1c4db739f215bad0c4f3a2a4',
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.5/pilot-darwin-arm64.tar.gz',
sha256: '9df13081c5340f24c18c7941ad964c7c8c67b64d0f7a7fe98eee5043ee2fe915',
}),
'linux-amd64': Object.freeze({
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.2/pilot-linux-amd64.tar.gz',
sha256: '94f46dff66bd1032d5ce8930d917a7ed9583a03deba4609d9de5f9049acf4fd8',
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.5/pilot-linux-amd64.tar.gz',
sha256: '432c04cd27b66e422b5d50778a7f7c220cd20b248348d2fcbad7e0a13e4de498',
}),
'linux-arm64': Object.freeze({
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.2/pilot-linux-arm64.tar.gz',
sha256: 'e79140e684506b2a244a7e2e0564bee085f5c826bcfbe918ea5e1bb926286c0c',
url: 'https://github.com/pilot-protocol/pilotprotocol/releases/download/managed-runtime-v0.1.5/pilot-linux-arm64.tar.gz',
sha256: 'ce873cc9838a7845956028050358f66a33f87af7c8983d2e63065d89104c2181',
}),
}),
});
Expand Down
Loading