@zieglar/pi-chat-exchange 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/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/bin/mcp-stdio.js +196 -0
- package/dist/control/debug-log.js +67 -0
- package/dist/control/mcp/debug-transport.js +48 -0
- package/dist/control/mcp/messaging-tools.js +278 -0
- package/dist/control/mcp/ping.js +43 -0
- package/dist/control/mcp/server.js +153 -0
- package/dist/core/authorization.js +32 -0
- package/dist/core/reference.js +32 -0
- package/dist/core/rules.js +468 -0
- package/dist/endpoints/pi/contract.js +2 -0
- package/dist/endpoints/pi/session-host.js +39 -0
- package/dist/host/bound-endpoint.js +176 -0
- package/dist/host/endpoint-host.js +839 -0
- package/dist/host/mcp-binding-config.js +209 -0
- package/dist/host/ownership-recovery.js +257 -0
- package/dist/index.js +11 -0
- package/dist/protocol/capabilities.js +46 -0
- package/dist/protocol/endpoint-capabilities.js +15 -0
- package/dist/protocol/entities.js +185 -0
- package/dist/protocol/formats.js +13 -0
- package/dist/protocol/idempotency.js +93 -0
- package/dist/protocol/identity.js +130 -0
- package/dist/protocol/index.js +8 -0
- package/dist/protocol/lifecycle.js +125 -0
- package/dist/protocol/outcomes.js +55 -0
- package/dist/runtime/desktop-manifest.js +65 -0
- package/dist/runtime/diagnostics.js +34 -0
- package/dist/runtime/executables.js +222 -0
- package/dist/storage/capability.js +221 -0
- package/dist/storage/connection.js +94 -0
- package/dist/storage/creation-request.js +176 -0
- package/dist/storage/creation.js +553 -0
- package/dist/storage/current-schema.js +397 -0
- package/dist/storage/delivery-ack.js +177 -0
- package/dist/storage/errors.js +9 -0
- package/dist/storage/filesystem.js +116 -0
- package/dist/storage/inbox-discovery.js +135 -0
- package/dist/storage/messaging.js +156 -0
- package/dist/storage/migration.js +258 -0
- package/dist/storage/ownership.js +789 -0
- package/dist/storage/participant-identity.js +127 -0
- package/dist/storage/participant.js +700 -0
- package/dist/storage/recovery.js +99 -0
- package/dist/storage/runtime.js +113 -0
- package/dist/storage/schema.js +556 -0
- package/dist/storage/session-state.js +75 -0
- package/dist/storage/store.js +450 -0
- package/dist/storage/transaction.js +199 -0
- package/dist/storage/trust-root.js +164 -0
- package/dist/tunnel/background-manager.js +1727 -0
- package/dist/tunnel/codes.js +79 -0
- package/dist/tunnel/health.js +75 -0
- package/dist/tunnel/keychain.js +526 -0
- package/dist/tunnel/managed-binding.js +454 -0
- package/dist/tunnel/manager-panel.js +249 -0
- package/dist/tunnel/manager-ui.js +341 -0
- package/dist/tunnel/manager.js +372 -0
- package/dist/tunnel/messaging-activation-ui.js +85 -0
- package/dist/tunnel/messaging-config.js +246 -0
- package/dist/tunnel/multi-session-admission.js +31 -0
- package/dist/tunnel/multi-session-policy.js +314 -0
- package/dist/tunnel/offline-client.js +104 -0
- package/dist/tunnel/profile.js +319 -0
- package/dist/tunnel/redact.js +9 -0
- package/dist/tunnel/secure-input.js +318 -0
- package/dist/tunnel/session-process-mutex.js +338 -0
- package/dist/tunnel/setup-ui.js +443 -0
- package/dist/tunnel/setup.js +734 -0
- package/dist/tunnel/state.js +127 -0
- package/docs/architecture.md +170 -0
- package/docs/chatgpt-installation.md +149 -0
- package/docs/delivery-ack-contract.md +86 -0
- package/docs/independent-install.md +156 -0
- package/docs/multi-session-usage.md +77 -0
- package/docs/ownership-contract.md +429 -0
- package/docs/protocol-v2.md +269 -0
- package/docs/releases/0.2.0.md +59 -0
- package/docs/storage-contract.md +1241 -0
- package/docs/storage-security.md +138 -0
- package/docs/tunnel-manager-background.md +169 -0
- package/docs/tunnel-setup.md +214 -0
- package/native/tunnel-keychain.swift +432 -0
- package/package.json +77 -4
- package/pi-extension/README.md +125 -0
- package/pi-extension/allowlist.example.json +11 -0
- package/pi-extension/index.ts +2312 -0
- package/pi-extension/multi-session-commands.ts +93 -0
- package/pi-extension/multi-session-runtime.ts +75 -0
- package/pi-extension/tui-keys.ts +40 -0
- package/scripts/reconcile-session-process-mutex.mjs +262 -0
- package/scripts/recover-stale-ownership.mjs +91 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
First packaged release line of this extension. Version 0.1.1 was an internal
|
|
6
|
+
private version only; it was never published to npm, so nothing below is an
|
|
7
|
+
upgrade from a public release.
|
|
8
|
+
CE's own code is owner-authorized MIT, Copyright (c) 2026 zieglar.
|
|
9
|
+
Third-party license evidence is complete and `private: true` is removed from the manifest.
|
|
10
|
+
|
|
11
|
+
### Messaging
|
|
12
|
+
|
|
13
|
+
- Add the default-disabled, opt-in `ce_ack` MCP tool, advertised only when the
|
|
14
|
+
binding declares the Session-authorized ACK capability. The host supplies all
|
|
15
|
+
authority fields and equivalent calls replay the original receipt. An ACK
|
|
16
|
+
records a recipient reception statement — not proof of delivery, reading,
|
|
17
|
+
processing or task completion. BUSY rollback and uncertain outcomes never grant
|
|
18
|
+
retry permission.
|
|
19
|
+
|
|
20
|
+
### Tunnel lifecycle commands
|
|
21
|
+
|
|
22
|
+
- Add `/ce-tunnel doctor`: read-only qualification diagnostics. With no trusted
|
|
23
|
+
client installed it reports a safe refusal such as `CE_EXECUTABLE_UNTRUSTED`.
|
|
24
|
+
That refusal is the expected diagnosis, not an install failure, and `doctor` is
|
|
25
|
+
never an exit proof.
|
|
26
|
+
- Add `/ce-tunnel deactivate`: bounded, normal release of this Session's own
|
|
27
|
+
binding, retaining registration and history. It is not revoke and not Recovery.
|
|
28
|
+
|
|
29
|
+
### Multi-session and runtime
|
|
30
|
+
|
|
31
|
+
- Support independent Pi Sessions on a shared Tunnel: `/ce-tunnel convert` keeps
|
|
32
|
+
the current native Session's original endpoint and `/ce-tunnel register` gives a
|
|
33
|
+
new Session its own stable endpoint without starting transport.
|
|
34
|
+
- Support trusted portable runtime installations for the pinned Node launch path.
|
|
35
|
+
|
|
36
|
+
### Runtime qualification
|
|
37
|
+
|
|
38
|
+
- The 0.2.0 certification target is exactly **Node 24.21.0 + Pi 1.3.0 +
|
|
39
|
+
macOS arm64**.
|
|
40
|
+
- Node **24.22.1** was admitted earlier as a second permitted runtime and remains
|
|
41
|
+
declared in `engines`. That historical admission is **not** part of 0.2.0's
|
|
42
|
+
certification. Linux is experimental and unverified.
|
|
43
|
+
|
|
44
|
+
### Packaging and documentation
|
|
45
|
+
|
|
46
|
+
- Prepare the prebuilt Pi extension npm distribution and consumer documentation
|
|
47
|
+
closure. The consumer tarball ships the operating guides plus the protocol,
|
|
48
|
+
ownership, storage, ACK and Store-security contracts, so no repository access
|
|
49
|
+
is required to operate the package safely. `private: true` was removed once the third-party audit closed with three-layer evidence. The
|
|
50
|
+
owner-authorized MIT LICENSE ships in the prebuilt tarball; dependency licenses
|
|
51
|
+
remain separate and must be preserved at installation/redistribution.
|
|
52
|
+
- Document owned transport Stop, independently confirmed STOPPED, normal binding
|
|
53
|
+
release and then native Pi removal; Store, profile, credentials and history are
|
|
54
|
+
retained.
|
|
55
|
+
- Keep managed npm, physical tarball fallback and monorepo source development
|
|
56
|
+
routes distinct.
|
|
57
|
+
- Cloud transport is NOT_VERIFIED; offline synthetic fixtures only.
|
|
58
|
+
|
|
59
|
+
### Release-process note
|
|
60
|
+
|
|
61
|
+
The queued `.changeset/ce-e2b-ack-mcp.md` patch entry describes the same `ce_ack`
|
|
62
|
+
behavior change, and its content is already reflected in this 0.2.0 entry. It is
|
|
63
|
+
deliberately **not** consumed by this entry: Changesets remains the repository's
|
|
64
|
+
version authority, and a future Changesets version PR must reconcile and consume
|
|
65
|
+
it together with this hand-written 0.2.0 entry and the 0.2.0 version pins. Root
|
|
66
|
+
Changesets configuration and queued entries are untouched by this delta. With
|
|
67
|
+
`private` now removed, `privatePackages.version: false` no longer keeps the queued
|
|
68
|
+
changeset inert — the next Changesets version PR owns that reconciliation explicitly.
|
|
69
|
+
|
|
70
|
+
**Third-party evidence complete; `private: true` removed.** The 101-item dependency closure is fully audited: the two former evidence gaps (pi-tui and the Host peer) closed with three-layer exact-version evidence — registry tarball MIT declarations plus the upstream `v1.3.0` tag root LICENSE (MIT, (c) 2025 Mario Zechner); their artifacts omit license text (upstream packaging omission, not incompatibility). The per-version audit record is `docs/license-audit.md`, which is repo-only and not distributed in the tarball. Owner authorization covers CE's own code only — not a grant to relicense dependencies.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zieglar
|
|
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,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Pi Chat Exchange
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
让 ChatGPT 与 Pi 通过显式工具交换持久化消息:发送、发现收件箱、授权读取,以及独立授权的接收回执(ACK)。它不是自动任务调度器,也不把 ACK 或对话回复当作任务完成。
|
|
4
|
+
|
|
5
|
+
本包版本为 **0.2.0**。CE 自有代码已获所有者授权采用 **MIT**(见 [LICENSE](LICENSE));第三方许可证证据已按三层证据闭合(逐版本审计记录为 `docs/license-audit.md`,仅仓库保留、不随 tarball 分发)。0.2.0 的认证范围仅 **Node 24.21.0 / Pi 1.3.0 / macOS arm64**;cloud transport 为 NOT_VERIFIED,Linux 未验证。
|
|
6
|
+
|
|
7
|
+
## 当前能力与典型用法
|
|
8
|
+
|
|
9
|
+
- **持久化消息**:ChatGPT 发来计划或问题,Pi 显式查看 inbox、授权读取,再以新 Message 回复检查结果。
|
|
10
|
+
- **独立接收回执**:按需授权 payload read 与 ACK;接收回执不等于任务完成,也不触发正文执行。
|
|
11
|
+
- **多 Session**:多个项目中的真实 Pi Session 使用独立 Endpoint,共享 Tunnel;ChatGPT inbox 仍不隔离对话。
|
|
12
|
+
|
|
13
|
+
当前不提供自动任务调度、自动执行或 exactly-once delivery/execution。只需自动远端执行而不逐次授权时,本项目不适用。
|
|
14
|
+
|
|
15
|
+
## 选择连接方式
|
|
16
|
+
|
|
17
|
+
- **推荐:Secure MCP Tunnel。** `/ce-tunnel` 提供设置向导、显式激活、后台 Start/Stop 和健康状态;无需 Desktop stdio 插件。按 [Tunnel 快速开始](docs/tunnel-setup.md)操作。
|
|
18
|
+
- **Desktop 本地 stdio 插件。** 由支持本地插件的 ChatGPT Desktop 启动 bundled MCP server;需配置本机 Node 与外置 binding。见 [ChatGPT 安装](docs/chatgpt-installation.md)。不要与 Tunnel 同时争用同一 Endpoint。
|
|
19
|
+
|
|
20
|
+
## 安装 Pi 扩展
|
|
21
|
+
|
|
22
|
+
**信任提示:Pi 扩展以你的完整用户权限运行。安装前审查源码与制品来源;消息授权门槛不等于操作系统沙箱。**
|
|
23
|
+
|
|
24
|
+
正式日常使用请用**独立安装路径**:作者交付预构建私有 tarball,消费者物理安装后 `pi install` 注册,无需仓库/build,见 [独立安装指南](docs/independent-install.md)。下面是开发者源码路径。
|
|
25
|
+
|
|
26
|
+
先安装 Pi **1.3.0**、pnpm **12.9.1** 和批准的 Node **24.21.0 或 24.22.1**。Node 还必须通过完整 SQLite 身份、编译选项与 API 门槛,不是任意 `24.x`。Pi host 与 MCP child 分别校验;mise 或其他版本管理器均不是必需品。
|
|
27
|
+
|
|
28
|
+
从仓库根目录,在已选中批准 Node 的 shell 中执行:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
node --version
|
|
32
|
+
pnpm install --frozen-lockfile
|
|
33
|
+
pnpm --dir packages/pi-chat-exchange run build
|
|
34
|
+
pi -e ./packages/pi-chat-exchange/pi-extension/index.ts
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
扩展入口会导入包内 `dist/`;修改源码后须重新运行上述 build,再在已加载的 Session 中 `/reload`。`/reload` 不会编译源码,旧 `dist/` 会继续运行旧逻辑。
|
|
38
|
+
|
|
39
|
+
这是临时加载。若要长期从不同项目使用,在 build 后执行 `pi install /absolute/path/to/packages/pi-chat-exchange` 注册本地包,再正常启动 Pi。本地安装引用该目录,不复制源码;不要同时安装和 `-e` 加载第二份。本包不提供独立的 `ce` 安装 CLI;安装只经 Pi 的 managed npm / tarball / 本地目录三条路径。
|
|
40
|
+
|
|
41
|
+
进入交互 TUI 后:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
/ce-tunnel doctor
|
|
45
|
+
/ce-tunnel setup
|
|
46
|
+
/reload
|
|
47
|
+
/ce-tunnel activate
|
|
48
|
+
/ce-status
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Setup 的 Advanced · Messaging 必须显式授权当前 Session;只保存基本 transport 设置时默认不能通信。首次 reload 缺少 mapping 时可能拒绝,随后显式 activate,不清理数据库。启动真实 Tunnel 还需父 Pi 环境的 `CE_TUNNEL_BACKGROUND_ALLOW=1` 和单独 `/ce-tunnel start`;完整步骤、可信绝对路径启动方式和 ChatGPT 自定义 MCP 配置见 [Tunnel 指南](docs/tunnel-setup.md)。
|
|
52
|
+
|
|
53
|
+
## 多项目、多 Session
|
|
54
|
+
|
|
55
|
+
每个真实 Pi Session 注册不同的稳定 Endpoint;项目目录本身不是身份。共享的 `chatgpt/main` inbox **不是 ChatGPT 对话隔离**。
|
|
56
|
+
|
|
57
|
+
- `/ce-status`:当前 Pi / CE 身份与 Ownership 状态。
|
|
58
|
+
- `/ce-tunnel sessions`:经当前运行时授权的、所选配置中 active Endpoint 列表,不证明 MCP 已加载。
|
|
59
|
+
- `/ce-tunnel deactivate`:确认后正常解绑当前本地 CE binding,不删注册/历史,也不停止 Tunnel。
|
|
60
|
+
- `/ce-tunnel revoke <exact-endpoint>`:撤销注册的后续权限;需先核实 transport STOPPED,不等于正常解绑。
|
|
61
|
+
- `/reload`:有效 binding 重新校验并恢复;已正常释放或未绑定的 Session 需要显式 activate。
|
|
62
|
+
|
|
63
|
+
转换、注册、权限和生效顺序见 [多 Session 使用指南](docs/multi-session-usage.md)。不要直接修改生成文件或删除 Store/Owner 作为排障。
|
|
64
|
+
|
|
65
|
+
## 阅读导航
|
|
66
|
+
|
|
67
|
+
| 目标 | 文档 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| **独立安装(正式日常使用,预构建 tarball)** | [独立安装指南](docs/independent-install.md) |
|
|
70
|
+
| 首次设置、可信安装诊断、ChatGPT 连接、启动与排障 | [Tunnel 用户指南](docs/tunnel-setup.md) |
|
|
71
|
+
| Desktop stdio 打包与本机配置 | [ChatGPT 安装](docs/chatgpt-installation.md) |
|
|
72
|
+
| 工具参数、Inbox、payload/ACK、生命周期 | [Pi 扩展指南](pi-extension/README.md) |
|
|
73
|
+
| 多项目/Session 注册、解绑、撤销 | [多 Session 使用](docs/multi-session-usage.md) |
|
|
74
|
+
| 分层、身份、授权、验证与扩展点 | [架构指南](docs/architecture.md) |
|
|
75
|
+
| 后台进程所有权与停止约束 | [后台 Manager](docs/tunnel-manager-background.md) |
|
|
76
|
+
| 精确安全契约 | [协议 v2](docs/protocol-v2.md) · [Ownership](docs/ownership-contract.md) · [Storage](docs/storage-contract.md) · [ACK](docs/delivery-ack-contract.md) · [Store 安全边界](docs/storage-security.md) |
|
|
77
|
+
|
|
78
|
+
独立安装包(tarball)随包分发上表所有文档、CHANGELOG 与本版本 release notes,消费者无需访问仓库即可取得操作与安全契约。仅作者侧的治理与证据记录(如 `docs/license-audit.md`、`docs/runtime-qualification.md` 及各阶段审计记录)仅仓库保留,不进入消费者制品。
|
|
79
|
+
|
|
80
|
+
## 边界与验证
|
|
81
|
+
|
|
82
|
+
**Configured ≠ Pi-bound ≠ Connected ≠ 消息实证。** `CONNECTED` 只证明测得的传输健康;ACK 仅为 reception receipt,不证明 delivery、read、processing 或 task completion。
|
|
83
|
+
|
|
84
|
+
不自动启动/重连、接管过期 Owner、排空 Inbox、执行 payload、合并 Store 或重试未知结果。`DELIVERY_UNKNOWN != CANCELLED`。凭据文件不是加密保险箱;不要把 key、capability、正文或完整日志贴入诊断。
|
|
85
|
+
|
|
86
|
+
Store 平台 allowlist 为 macOS arm64 / Linux x64;两种批准 Node 的实测为 macOS arm64,不等于 Linux 或每种 Desktop UI 均已验收。已有本地人工双向消息与 Tunnel 启停验收不替代本次变更的隔离测试,也不证明机器冷启动或所有终端交互。
|
|
87
|
+
|
|
88
|
+
开发检查:从包目录执行 `pnpm run verify`(check → typecheck → build → test → plugin-package test);扩展测试前须 build。证据层次见[架构指南](docs/architecture.md#验证层次)。
|
|
89
|
+
|
|
90
|
+
许可证:**MIT**,Copyright (c) 2026 zieglar。第三方依赖保留各自许可,不由 CE 的 MIT 重新授权;`@earendil-works/pi-tui@1.3.0` 与 Host peer 的制品未随包附带 LICENSE 文本(上游打包省略;经 registry 制品与上游 v1.3.0 标签根 LICENSE 三层核实为 MIT)。完整逐版本审计记录为 `docs/license-audit.md`(仅仓库保留,不随 tarball 分发)。0.2.0 的认证范围仅 Node 24.21.0 / Pi 1.3.0 / macOS arm64;cloud NOT_VERIFIED,Linux 未验证。见 [0.2.0 release notes](docs/releases/0.2.0.md) 与 [安全卸载/升级/回滚](docs/independent-install.md#5-安全卸载升级与回滚)。
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stdio entrypoint for the pi-chat-exchange MCP server (composition only).
|
|
4
|
+
*
|
|
5
|
+
* Intended launch forms:
|
|
6
|
+
* node dist/bin/mcp-stdio.js
|
|
7
|
+
* Phase 1 smoke surface: exactly the `ping` tool, no durable state.
|
|
8
|
+
*
|
|
9
|
+
* node dist/bin/mcp-stdio.js --config /abs/path/binding.json
|
|
10
|
+
* CE-0002 B3-P3B messaging surface: binds ONE Endpoint from trusted,
|
|
11
|
+
* operator-controlled launch configuration (never from tool arguments,
|
|
12
|
+
* never from the environment), acquires its ownership lease with
|
|
13
|
+
* `requireUnowned`, renews it on a heartbeat, and releases it on clean
|
|
14
|
+
* shutdown. A prior crashed instance leaves an ownership row that refuses
|
|
15
|
+
* startup — fail closed, no takeover.
|
|
16
|
+
*
|
|
17
|
+
* tunnel-client run --profile pi-chat-exchange (with --mcp-command "node .../mcp-stdio.js")
|
|
18
|
+
*
|
|
19
|
+
* stdout is reserved for MCP protocol frames; diagnostics go to stderr and
|
|
20
|
+
* carry status codes only — never configuration values, identifiers from
|
|
21
|
+
* tool arguments, payloads, or capability secrets.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
|
+
import { runStdioServer, createExchangeMcpServer } from "../control/mcp/server.js";
|
|
27
|
+
import { CE_ACK_TOOL, MESSAGING_TOOL_NAMES } from "../control/mcp/messaging-tools.js";
|
|
28
|
+
import { PING_TOOL_NAME } from "../control/mcp/ping.js";
|
|
29
|
+
import { CeEndpointHost } from "../host/endpoint-host.js";
|
|
30
|
+
import { CeBoundEndpoint } from "../host/bound-endpoint.js";
|
|
31
|
+
import { McpBindingConfigError, MCP_BINDING_CONFIG_MAX_BYTES, parseMcpBindingConfig, } from "../host/mcp-binding-config.js";
|
|
32
|
+
import { debugEvent, errorSummary } from "../control/debug-log.js";
|
|
33
|
+
import { observeTransport } from "../control/mcp/debug-transport.js";
|
|
34
|
+
import { initializeStore, openStore } from "../storage/store.js";
|
|
35
|
+
import { createMcpPolicyAdmission } from "../tunnel/multi-session-admission.js";
|
|
36
|
+
debugEvent("server", "SERVER_ENTRY", { version: process.version });
|
|
37
|
+
// Monitor rather than handle exceptions: preserve Node's fatal exit behavior.
|
|
38
|
+
process.on("uncaughtExceptionMonitor", (error, origin) => {
|
|
39
|
+
debugEvent("process", origin === "unhandledRejection" ? "UNHANDLED_REJECTION" : "UNCAUGHT_EXCEPTION", { ...errorSummary(error), origin });
|
|
40
|
+
});
|
|
41
|
+
process.on("exit", (exitCode) => debugEvent("process", "PROCESS_EXIT", { exitCode }));
|
|
42
|
+
function fail(code, detail) {
|
|
43
|
+
debugEvent("server", "MCP_ERROR", { code, stage: "startup" });
|
|
44
|
+
console.error(detail === undefined ? code : `${code} ${detail}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
/** Read the binding configuration from an absolute path, refusing anything
|
|
48
|
+
* oversized or unreadable before a single durable write can happen. */
|
|
49
|
+
function loadBindingConfig(path) {
|
|
50
|
+
let size;
|
|
51
|
+
try {
|
|
52
|
+
size = statSync(path).size;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return fail("CE_MCP_CONFIG_INVALID", "config.unreadable");
|
|
56
|
+
}
|
|
57
|
+
if (size > MCP_BINDING_CONFIG_MAX_BYTES)
|
|
58
|
+
return fail("CE_MCP_CONFIG_INVALID", "config.too_large");
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = readFileSync(path, "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return fail("CE_MCP_CONFIG_INVALID", "config.unreadable");
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return fail("CE_MCP_CONFIG_INVALID", "config.not_json");
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return parseMcpBindingConfig(parsed);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof McpBindingConfigError)
|
|
78
|
+
return fail(error.code, error.field);
|
|
79
|
+
return fail("CE_MCP_CONFIG_INVALID", "config.unexpected");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Open (or initialize on first use) the shared durable store. */
|
|
83
|
+
function openBindingStore(config) {
|
|
84
|
+
const options = { directory: config.store.directory, filename: config.store.file, busyTimeoutMs: 5_000 };
|
|
85
|
+
debugEvent("store", "STORE_OPEN_STARTED");
|
|
86
|
+
const store = existsSync(join(options.directory, options.filename)) ? openStore(options) : initializeStore(options);
|
|
87
|
+
debugEvent("store", "STORE_OPEN_COMPLETED");
|
|
88
|
+
return store;
|
|
89
|
+
}
|
|
90
|
+
async function main() {
|
|
91
|
+
const argv = process.argv.slice(2);
|
|
92
|
+
if (argv.length === 0) {
|
|
93
|
+
await runStdioServer();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const [flag, configPath] = argv;
|
|
97
|
+
if (argv.length !== 2 || flag !== "--config" || configPath === undefined || !configPath.startsWith("/")) {
|
|
98
|
+
fail("CE_MCP_LAUNCH_INVALID", "usage: mcp-stdio.js [--config /absolute/path.json]");
|
|
99
|
+
}
|
|
100
|
+
const config = loadBindingConfig(configPath);
|
|
101
|
+
// Verify actual launch bytes before Store creation; managed provenance grants nothing.
|
|
102
|
+
const admission = createMcpPolicyAdmission(config, configPath);
|
|
103
|
+
admission?.({ kind: "begin" });
|
|
104
|
+
const host = new CeEndpointHost(openBindingStore(config), {
|
|
105
|
+
adapter: config.endpoint.adapter,
|
|
106
|
+
allowlist: [{ endpointId: config.endpoint.endpointId }],
|
|
107
|
+
recipients: config.recipients,
|
|
108
|
+
exchangeNamespace: config.exchange.namespace,
|
|
109
|
+
exchangeIdempotencyKey: config.exchange.idempotencyKey,
|
|
110
|
+
leaseTtlSeconds: config.lease.ttlSeconds,
|
|
111
|
+
payloadRead: { enabled: config.payloadRead.enabled },
|
|
112
|
+
ack: { enabled: config.ack.enabled },
|
|
113
|
+
});
|
|
114
|
+
debugEvent("authority", "AUTHORITY_CHECK_STARTED", { stage: "requireUnowned" });
|
|
115
|
+
const bound = CeBoundEndpoint.start(host, config.endpoint.endpointId, {
|
|
116
|
+
renewIntervalMs: config.lease.renewIntervalSeconds * 1_000,
|
|
117
|
+
...(admission === undefined ? {} : { admission }),
|
|
118
|
+
onEvent: (event) => {
|
|
119
|
+
debugEvent("authority", "AUTHORITY_CHECK_RESULT", { stage: event.kind });
|
|
120
|
+
if (event.kind === "renewed")
|
|
121
|
+
return;
|
|
122
|
+
// Codes only; renewal_deferred is retryable at the next tick,
|
|
123
|
+
// renewal_stopped has already blocked new sends.
|
|
124
|
+
console.error(event.kind === "renewal_deferred"
|
|
125
|
+
? `CE_MCP_RENEWAL_DEFERRED code=${event.code}`
|
|
126
|
+
: `CE_MCP_RENEWAL_STOPPED code=${event.code}`);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
debugEvent("authority", "OWNERSHIP_ACQUIRED", { owned: true });
|
|
130
|
+
// HANDOFF-CE-E2B-01: advertise `ce_ack` only when the validated binding
|
|
131
|
+
// declares the ack opt-in; the default surface stays exactly four tools.
|
|
132
|
+
const server = createExchangeMcpServer(undefined, {
|
|
133
|
+
port: bound, ackEnabled: config.ack.enabled, executionGuard: config.debugExecutionGuard,
|
|
134
|
+
...(admission === undefined ? {} : { admission: () => admission({ kind: "inbox" }) }),
|
|
135
|
+
});
|
|
136
|
+
if (config.debugExecutionGuard.enabled) {
|
|
137
|
+
debugEvent("server", "EXECUTION_GUARD_ARMED", { scope: "tools/call", stage: "unified-dispatch-entry" });
|
|
138
|
+
}
|
|
139
|
+
const transport = observeTransport(new StdioServerTransport());
|
|
140
|
+
debugEvent("server", "TRANSPORT_CREATED");
|
|
141
|
+
let initiated = false;
|
|
142
|
+
const initiateShutdown = async () => {
|
|
143
|
+
if (initiated)
|
|
144
|
+
return;
|
|
145
|
+
initiated = true;
|
|
146
|
+
try {
|
|
147
|
+
await server.close();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Transport already gone; the lease release below is what matters.
|
|
151
|
+
}
|
|
152
|
+
const shutdown = await bound.shutdown();
|
|
153
|
+
debugEvent("authority", "OWNERSHIP_RELEASED", { released: shutdown.released, detached: shutdown.detached });
|
|
154
|
+
console.error(shutdown.released
|
|
155
|
+
? `CE_MCP_STOPPED released=true detached=${shutdown.detached}`
|
|
156
|
+
: `CE_MCP_STOPPED released=false code=${shutdown.code ?? "CE_HOST_UNAVAILABLE"}`);
|
|
157
|
+
};
|
|
158
|
+
let resolveShutdown;
|
|
159
|
+
const shutdownRequested = new Promise((resolvePromise) => {
|
|
160
|
+
resolveShutdown = resolvePromise;
|
|
161
|
+
});
|
|
162
|
+
const requestShutdown = () => resolveShutdown();
|
|
163
|
+
// `on`, not `once`: a repeat SIGTERM/SIGINT during the drain window must be
|
|
164
|
+
// absorbed by this idempotent request instead of hitting Node's default
|
|
165
|
+
// disposition and killing the process before the lease is released.
|
|
166
|
+
process.on("SIGTERM", () => { debugEvent("process", "PROCESS_SIGNAL", { signal: "SIGTERM" }); requestShutdown(); });
|
|
167
|
+
process.on("SIGINT", () => { debugEvent("process", "PROCESS_SIGNAL", { signal: "SIGINT" }); requestShutdown(); });
|
|
168
|
+
process.stdin.once("end", requestShutdown);
|
|
169
|
+
process.stdin.once("close", requestShutdown);
|
|
170
|
+
server.onclose = requestShutdown;
|
|
171
|
+
await server.connect(transport);
|
|
172
|
+
debugEvent("server", "TRANSPORT_CONNECTED");
|
|
173
|
+
debugEvent("server", "SERVER_READY", { mode: "bound" });
|
|
174
|
+
// HANDOFF-CE-E2B-01: the diagnostic mirrors exactly what the server above
|
|
175
|
+
// advertises for this binding (same construction, same order) — ce_ack
|
|
176
|
+
// appears only when the operator ack opt-in is enabled.
|
|
177
|
+
const advertisedTools = [
|
|
178
|
+
PING_TOOL_NAME,
|
|
179
|
+
...MESSAGING_TOOL_NAMES,
|
|
180
|
+
...(config.ack.enabled ? [CE_ACK_TOOL.name] : []),
|
|
181
|
+
];
|
|
182
|
+
console.error(`CE_MCP_STARTED tools=${advertisedTools.join(",")}`);
|
|
183
|
+
await shutdownRequested;
|
|
184
|
+
await initiateShutdown();
|
|
185
|
+
}
|
|
186
|
+
main().catch((error) => {
|
|
187
|
+
debugEvent("server", "MCP_ERROR", { stage: "startup", ...errorSummary(error) });
|
|
188
|
+
// Sanitized: host refusals carry fixed codes; anything else collapses
|
|
189
|
+
// without echoing message text (which could contain operator paths).
|
|
190
|
+
const code = typeof error.code === "string"
|
|
191
|
+
? String(error.code)
|
|
192
|
+
: "CE_MCP_START_FAILED";
|
|
193
|
+
console.error(code.startsWith("CE_") ? code : "CE_MCP_START_FAILED");
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=mcp-stdio.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** DIAG-01 development diagnostics: always on, best effort, never stdout.
|
|
2
|
+
* Only explicitly projected metadata is accepted; never pass raw args/results/errors.
|
|
3
|
+
* The built module's ../../.debug-logs is inside the independent debug artifact.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, writeSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
const FIELDS = new Set(["stage", "method", "requestId", "requestIdType", "requestIdLength", "tool", "args", "errorType", "stackFrames", "code", "isError", "durationMs", "count", "released", "detached", "signal", "exitCode", "version", "fencing", "owned", "mode", "origin", "reason", "scope"]);
|
|
8
|
+
export function createDebugLog(directory) {
|
|
9
|
+
return (component, event, metadata = {}) => {
|
|
10
|
+
// Include serialization in the guard: even hostile getters/cycles cannot
|
|
11
|
+
// turn diagnostics into a business/protocol failure.
|
|
12
|
+
try {
|
|
13
|
+
const context = Object.fromEntries(Object.entries(metadata).filter(([key]) => FIELDS.has(key)));
|
|
14
|
+
const line = `${JSON.stringify({ ts: new Date().toISOString(), pid: process.pid, component, event, ...context })}\n`;
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
17
|
+
if (!lstatSync(directory).isDirectory() || lstatSync(directory).isSymbolicLink())
|
|
18
|
+
throw new Error("unsafe log directory");
|
|
19
|
+
const fd = openSync(join(directory, `ce-debug-${process.pid}.jsonl`), constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
20
|
+
try {
|
|
21
|
+
const stat = fstatSync(fd);
|
|
22
|
+
if (!stat.isFile() || (process.getuid !== undefined && stat.uid !== process.getuid()))
|
|
23
|
+
throw new Error("unsafe log file");
|
|
24
|
+
appendFileSync(fd, line);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
closeSync(fd);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch { /* diagnostics file unavailable: stderr mirror remains best effort */ }
|
|
31
|
+
try {
|
|
32
|
+
writeSync(2, line);
|
|
33
|
+
}
|
|
34
|
+
catch { /* closed stderr must not alter behavior */ }
|
|
35
|
+
}
|
|
36
|
+
catch { /* diagnostics must never escape */ }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export const debugEvent = createDebugLog(resolve(import.meta.dirname, "../../.debug-logs"));
|
|
40
|
+
const ARG_FIELDS = new Set(["to", "text", "idempotencyKey", "afterAuditSequence", "pageSize", "rawScanLimit", "messageId", "payloadDigest", "payloadLength", "messageCreatedAuditSequence"]);
|
|
41
|
+
export function argumentShape(args) {
|
|
42
|
+
try {
|
|
43
|
+
if (args === null || typeof args !== "object")
|
|
44
|
+
return [];
|
|
45
|
+
return Object.entries(args).slice(0, 32).map(([key, value]) => ({
|
|
46
|
+
field: ARG_FIELDS.has(key) ? key : "unknown-field",
|
|
47
|
+
type: value === null ? "null" : Array.isArray(value) ? "array" : typeof value,
|
|
48
|
+
...(typeof value === "string" ? { length: value.length } : {}),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function errorSummary(error) {
|
|
56
|
+
try {
|
|
57
|
+
// Error messages and first stack line can contain payloads/credentials.
|
|
58
|
+
// Retain source basenames/line:column only, never function names or paths.
|
|
59
|
+
const stackFrames = error instanceof Error ? (error.stack ?? "").split("\n").slice(1, 9)
|
|
60
|
+
.flatMap((line) => line.match(/[/\\]([A-Za-z0-9_.-]+\.[cm]?[jt]s:\d+:\d+)\)?$/)?.[1] ?? []) : [];
|
|
61
|
+
return { errorType: error instanceof TypeError ? "TypeError" : error instanceof Error ? "Error" : typeof error, stackFrames };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { errorType: "unknown" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=debug-log.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { debugEvent, errorSummary } from "../debug-log.js";
|
|
2
|
+
const METHODS = new Set(["initialize", "notifications/initialized", "tools/list", "tools/call", "ping", "notifications/cancelled"]);
|
|
3
|
+
function frameMetadata(frame) {
|
|
4
|
+
const id = "id" in frame ? frame.id : undefined;
|
|
5
|
+
return {
|
|
6
|
+
...("method" in frame ? { method: METHODS.has(frame.method) ? frame.method : "other" } : {}),
|
|
7
|
+
...(typeof id === "number" ? { requestId: id } : typeof id === "string" ? { requestIdType: "string", requestIdLength: id.length } : {}),
|
|
8
|
+
...("error" in frame ? { code: frame.error.code } : {}),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Observe the existing stdio transport without parsing/rewriting wire data.
|
|
12
|
+
* SDK connect installs callbacks before start. Preserve frame/extra/options,
|
|
13
|
+
* this binding, return/rejection semantics, and existing close/error callbacks.
|
|
14
|
+
*/
|
|
15
|
+
export function observeTransport(transport, log = debugEvent) {
|
|
16
|
+
const start = transport.start.bind(transport);
|
|
17
|
+
const send = transport.send.bind(transport);
|
|
18
|
+
transport.start = async () => {
|
|
19
|
+
const receive = transport.onmessage;
|
|
20
|
+
const close = transport.onclose;
|
|
21
|
+
const error = transport.onerror;
|
|
22
|
+
transport.onmessage = (frame, extra) => {
|
|
23
|
+
log("mcp", "MCP_REQUEST_RECEIVED", frameMetadata(frame));
|
|
24
|
+
if ("method" in frame) {
|
|
25
|
+
const aliases = new Map([["initialize", "MCP_INITIALIZE"], ["notifications/initialized", "MCP_INITIALIZED"], ["tools/list", "MCP_TOOLS_LIST"], ["tools/call", "MCP_TOOLS_CALL"]]);
|
|
26
|
+
const event = aliases.get(frame.method);
|
|
27
|
+
if (event !== undefined)
|
|
28
|
+
log("mcp", event, frameMetadata(frame));
|
|
29
|
+
}
|
|
30
|
+
receive?.call(transport, frame, extra);
|
|
31
|
+
};
|
|
32
|
+
transport.onclose = () => { log("mcp", "SERVER_STOPPED"); close?.call(transport); };
|
|
33
|
+
transport.onerror = (e) => { log("mcp", "MCP_ERROR", errorSummary(e)); error?.call(transport, e); };
|
|
34
|
+
return start();
|
|
35
|
+
};
|
|
36
|
+
transport.send = async (frame, options) => {
|
|
37
|
+
try {
|
|
38
|
+
await send(frame, options);
|
|
39
|
+
log("mcp", "error" in frame ? "MCP_ERROR" : "MCP_RESPONSE_SENT", frameMetadata(frame));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
log("mcp", "MCP_ERROR", errorSummary(error));
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return transport;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=debug-transport.js.map
|