@tomflow/chatloop-tools 0.4.8 → 0.4.9
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/README.md +10 -8
- package/bin/cli.mjs +12 -1
- package/lib/tunnel-auth.mjs +64 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# @tomflow/chatloop-tools — Chat Loop 工具包
|
|
2
2
|
|
|
3
|
-
**v0.4.
|
|
3
|
+
**v0.4.9 · Node.js >=20 · 当前验证平台 macOS;实际发布版本以 npm Registry 为准**
|
|
4
4
|
|
|
5
|
-
本包是**本地 CLI + 统一 stdio MCP Gateway**(以下 API 合同适用于 v0.4.
|
|
5
|
+
本包是**本地 CLI + 统一 stdio MCP Gateway**(以下 API 合同适用于 v0.4.9,低版本以各自安装版说明为准):一套命令管理多个工具模块的启动、停止、重启、状态;一个 MCP 入口统一对外暴露各模块的工具 API。目前代理 **Desktop Commander 0.2.44** 原有 26 个工具,新增 **Chat Loop Extension** 四个专用 Action CLI 工具(总计 30 个 MCP 工具),Repomix 尚未接入。它**不是** ChatGPT Plugin、Secure MCP Tunnel 或 Chrome 扩展;但从 v0.4.6 起**随 npm 自动安装一份 Chat Loop 私有的对应平台 tunnel-client 二进制依赖**,不会使用 Local Dev/Homebrew 上的程序,也不包含任何用户凭据。
|
|
6
6
|
|
|
7
7
|
> **给人类和 AI 的 API 合同**:此 README 随 npm 包分发,可独立阅读;对外的稳定使用入口是 `chatloop-tools` CLI(兼容别名 `chat-loop-local`)和 `chatloop-tools mcp` 提供的 MCP 协议。**无承诺的 JavaScript import/exports API**,不要依赖 `lib/` 内部模块。研发架构文档在仓库 `docs/17-local-tools-multi-tool-plan.md`,不随 npm 包发行。
|
|
8
8
|
|
|
9
9
|
## Chat Loop 专属 tunnel-client:打包内置,不依赖 Local Dev
|
|
10
10
|
|
|
11
|
-
v0.4.
|
|
11
|
+
v0.4.9 延续 v0.4.6 的独立 npm 平台二进制依赖分发 OpenAI 官方 [tunnel-client v0.0.19](https://github.com/openai/tunnel-client/releases/tag/v0.0.19) 的 macOS Intel(x64)/Apple Silicon(arm64) **完整发行二进制与 cloudflared 同伴程序**分别封装在同一 `@tomflow` scope 的 `@tomflow/chatloop-tunnel-darwin-{x64,arm64}@0.0.19` 平台子包,主包用 `optionalDependencies` 由 npm **自动只安装匹配当前 OS/CPU 的包**。这仍属于 Chat Loop Tools 的内部依赖,不安装到 PATH、Homebrew 或 Local Dev。两份官方 ZIP 均按上游 SHA256SUMS 验证;将许可证、NOTICE、第三方许可证、SPDX 和二进制 SHA256 溯源一并保留。启动只调用本包自身的绝对路径,不搜索 PATH、不依赖本机 Homebrew 或 Local Dev。
|
|
12
12
|
|
|
13
|
-
**版本注意**:`@latest` 始终取 npm Registry 当前最新版本;如 Registry 尚未提供 v0.4.
|
|
13
|
+
**版本注意**:`@latest` 始终取 npm Registry 当前最新版本;如 Registry 尚未提供 v0.4.9,下列新认证命令只能在本仓源码或独立 v0.4.9 tarball 环境中使用。
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel doctor
|
|
17
17
|
npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel --version
|
|
18
|
-
#
|
|
18
|
+
# 在 OpenAI Platform 为本用户的 Chat Loop 专属 Tunnel 创建 Restricted Runtime API key(Tunnels Read + Use)后:
|
|
19
|
+
npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel auth
|
|
20
|
+
# 上一命令要求真实终端,以隐藏输入方式安全保存 key,不在 argv/输出中出现;随后启动:
|
|
19
21
|
npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools start tunnel
|
|
20
22
|
# 兼容入口(等价于 start tunnel,不再另开未托管前台进程):
|
|
21
23
|
npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel run
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
`tunnel doctor` 校验当前架构的 tunnel-client/cloudflared 哈希,报告 `BUNDLED_READY` 不代表连接已认证。**v0.4.
|
|
26
|
+
`tunnel doctor` 校验当前架构的 tunnel-client/cloudflared 哈希,报告 `BUNDLED_READY` 不代表连接已认证。**v0.4.9** 注册唯一组件 ID `tunnel`,由同一个 Supervisor 执行 `start|stop|restart|status tunnel`;无参命令统一涵盖已启用的 Desktop Commander 与 Tunnel。启动时只读产品自己的 `~/.chat-loop/tunnel/profiles/chat-loop.yaml`,缺少配置以 `PROFILE_NOT_FOUND` 失败而不启动。启动的真实二进制路径固定为本包对应平台依赖,唯一管理器持有子进程句柄;重复启动不复制进程,失去所有权时保留 `owner.json` 证据并安全拒绝接管、发信号。仅能停止本管理器直接持有的子进程,绝不按进程名查杀其他 Tunnel。
|
|
25
27
|
|
|
26
28
|
Supervisor 自己创建的独立 Unix 健康 Socket `<state-dir>/tools/tunnel/health.sock` 通过 `/readyz` 判断**本地启动就绪**;结果 `after=RUNNING / evidence=LOCAL_READYZ / connection=UNVERIFIED` **不代表** OpenAI 云端请求成功。最后仍必须经 ChatGPT 真实 tools/call 验证。统一 Tunnel 子进程不继承外部的 `OPENAI_ADMIN_KEY` 或 `CONTROL_PLANE_API_KEY`:若自己的 profile 使用 `env:CONTROL_PLANE_API_KEY`,用户须明确在启动该 CLI 时提供产品独立的 `CHAT_LOOP_TUNNEL_CONTROL_PLANE_API_KEY`,由适配器专门映射;也可让产品自己的 profile 指向用户自有受保护的 `file:` 密钥引用。不要把凭据放在 CLI 参数、聊天或仓库里。
|
|
27
29
|
|
|
@@ -31,7 +33,7 @@ Supervisor 自己创建的独立 Unix 健康 Socket `<state-dir>/tools/tunnel/he
|
|
|
31
33
|
|
|
32
34
|
- **已经验证**:仓库内 CLI 生命周期、真实 Desktop Commander MCP 握手和只读调用、26 项工具发现、单工具故障隔离、Gateway 断线重连;全部在本机测试或隔离状态目录内完成。
|
|
33
35
|
- **已验证(2026-11-05)**:v0.4.0 本地 tarball 在仓库外 macOS 临时目录用 npm 安装(`--ignore-scripts`),安装后的 `doctor/status/start/stop` 正常;MCP initialize、26 工具列表与 `read_file` 调用读回临时文件均通过。未改现有 Local Dev/Tunnel。
|
|
34
|
-
- **已验收历史与当前缺口要区分**:旧 v0.4.3 通过用户独立 Tunnel → ChatGPT 的 26 工具发现和真实只读 tools/call;正式 npm v0.4.5 已发布、npx latest 本地 MCP 30
|
|
36
|
+
- **已验收历史与当前缺口要区分**:旧 v0.4.3 通过用户独立 Tunnel → ChatGPT 的 26 工具发现和真实只读 tools/call;正式 npm v0.4.5 已发布、npx latest 本地 MCP 30 工具发现通过,这些是旧版阶段的历史记录;**当前专用 Tunnel 已通过独立 Runtime Key 和新 Chat 随机只读挑战验收**(证据以仓库 `context/CURRENT.md` 为准,本文随 npm 包分发时不代表运行中的在线状态);真实 macOS Chrome 0.3.29 + Native Host + 本地 MCP tools/call 的 Text/DOM/PNG/Reload **已通过**,并在 Reload 后正式 Audit 复核版本/Owner 未变化。Windows/Linux、新版 MCP 2026 协议、长任务取消、最小权限及公开 Chat Loop 产品 Plugin 发布也未验收。
|
|
35
37
|
- **高风险权限**:Desktop Commander 在独立 `HOME` 下运行,但仍继承**当前操作系统用户的文件、进程与命令权限**;这不是文件系统沙箱。只在获用户明确授权的机器上运行,勿以管理员权限启动,勿向不可信客户端暴露;生产级目录白名单/命令审批**尚未实现**。
|
|
36
38
|
|
|
37
39
|
## 2. 安装及入口
|
|
@@ -66,7 +68,7 @@ npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools mcp
|
|
|
66
68
|
cd packages/local-tools
|
|
67
69
|
npm pack
|
|
68
70
|
# 在仓库外一个全新的目录:
|
|
69
|
-
npm install /absolute/path/to/tomflow-chatloop-tools-0.4.
|
|
71
|
+
npm install /absolute/path/to/tomflow-chatloop-tools-0.4.9.tgz
|
|
70
72
|
./node_modules/.bin/chat-loop-local doctor
|
|
71
73
|
./node_modules/.bin/chat-loop-local status
|
|
72
74
|
```
|
package/bin/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {CONTRACT,ACTIONS,registry,ensurePrivateDir,paths,
|
|
|
9
9
|
safeFallback,recoverStaleLock,exitCode,talk} from '../lib/control.mjs';
|
|
10
10
|
import {runGateway} from '../lib/gateway.mjs';
|
|
11
11
|
import {tunnelDoctor,runTunnel} from '../lib/tunnel.mjs';
|
|
12
|
+
import {promptTunnelRuntimeKey,storeTunnelRuntimeKey} from '../lib/tunnel-auth.mjs';
|
|
12
13
|
|
|
13
14
|
const require=createRequire(import.meta.url);
|
|
14
15
|
const command=process.argv[2]??'doctor';
|
|
@@ -107,13 +108,23 @@ else if(command==='doctor'){
|
|
|
107
108
|
}else if(extra.length===1&&extra[0]==='run'){
|
|
108
109
|
// Compatibility alias: all tunnel execution now belongs to the Supervisor.
|
|
109
110
|
await runControl('start',['tunnel']);
|
|
111
|
+
}else if(extra.length===1&&extra[0]==='auth'){
|
|
112
|
+
try{
|
|
113
|
+
const key=await promptTunnelRuntimeKey();
|
|
114
|
+
emit({contract:'chat-loop.local-tools.v1',command,action:'auth',
|
|
115
|
+
...storeTunnelRuntimeKey(key)});
|
|
116
|
+
}catch(e){
|
|
117
|
+
emit({contract:'chat-loop.local-tools.v1',command,action:'auth',
|
|
118
|
+
status:'FAILED',code:e.code||'CREDENTIAL_SETUP_FAILED'});
|
|
119
|
+
process.exitCode=1;
|
|
120
|
+
}
|
|
110
121
|
}else if(extra.length===1&&extra[0]==='--version'){
|
|
111
122
|
try{await runTunnel(['--version']);}
|
|
112
123
|
catch(e){emit({contract:'chat-loop.local-tools.v1',command,status:'FAILED',
|
|
113
124
|
code:e.code||'TUNNEL_EXEC_FAILED'});process.exitCode=1;}
|
|
114
125
|
}else{
|
|
115
126
|
emit({contract:'chat-loop.local-tools.v1',command,status:'FAILED',
|
|
116
|
-
code:'INVALID_ARGUMENT',usage:'tunnel doctor | tunnel --version | tunnel run (managed)'});
|
|
127
|
+
code:'INVALID_ARGUMENT',usage:'tunnel doctor | tunnel --version | tunnel auth | tunnel run (managed)'});
|
|
117
128
|
process.exitCode=2;
|
|
118
129
|
}
|
|
119
130
|
}else if(command==='capabilities'){
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// One-time Chat Loop runtime credential storage. Never inspect Local Dev's secrets.
|
|
2
|
+
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import {existsSync,lstatSync,mkdirSync,readFileSync,renameSync,unlinkSync,writeFileSync} from 'node:fs';
|
|
4
|
+
import {homedir} from 'node:os';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
import {Writable} from 'node:stream';
|
|
7
|
+
import {createInterface} from 'node:readline';
|
|
8
|
+
|
|
9
|
+
const fail=(code)=>Object.assign(new Error(code),{code});
|
|
10
|
+
function privateFile(path){
|
|
11
|
+
const stat=lstatSync(path);
|
|
12
|
+
if(!stat.isFile()||stat.isSymbolicLink()||(stat.mode&0o077)||
|
|
13
|
+
(process.getuid&&stat.uid!==process.getuid()))throw fail('INSECURE_PROFILE');
|
|
14
|
+
}
|
|
15
|
+
function privateDirectory(path){
|
|
16
|
+
const stat=lstatSync(path);
|
|
17
|
+
if(!stat.isDirectory()||stat.isSymbolicLink()||(stat.mode&0o077)||
|
|
18
|
+
(process.getuid&&stat.uid!==process.getuid()))throw fail('INSECURE_CREDENTIAL_DIR');
|
|
19
|
+
}
|
|
20
|
+
function putAtomic(path,contents){
|
|
21
|
+
const temp=path+'.'+randomUUID()+'.tmp';
|
|
22
|
+
try{
|
|
23
|
+
writeFileSync(temp,contents,{flag:'wx',mode:0o600});
|
|
24
|
+
renameSync(temp,path);
|
|
25
|
+
}finally{if(existsSync(temp))unlinkSync(temp);}
|
|
26
|
+
}
|
|
27
|
+
export function storeTunnelRuntimeKey(key,{root=join(homedir(),'.chat-loop','tunnel')}={}){
|
|
28
|
+
if(typeof key!=='string'||!key||key!==key.trim()||key.length>4096||
|
|
29
|
+
/[\r\n\x00-\x1f\x7f]/.test(key))throw fail('INVALID_CREDENTIAL');
|
|
30
|
+
const profile=join(root,'profiles','chat-loop.yaml');
|
|
31
|
+
try{privateFile(profile);}catch(e){
|
|
32
|
+
throw fail(e.code==='ENOENT'?'PROFILE_NOT_FOUND':e.code||'INSECURE_PROFILE');
|
|
33
|
+
}
|
|
34
|
+
const original=readFileSync(profile,'utf8');
|
|
35
|
+
const secretDir=join(root,'secrets'),secretPath=join(secretDir,'runtime-api-key');
|
|
36
|
+
const oldRef='env:CONTROL_PLANE_API_KEY',fileRef='file:'+secretPath;
|
|
37
|
+
if(original.includes(oldRef)){
|
|
38
|
+
if(original.split(oldRef).length!==2)throw fail('AMBIGUOUS_AUTH_REFERENCE');
|
|
39
|
+
}else if(!original.includes(fileRef))throw fail('FOREIGN_AUTH_REFERENCE');
|
|
40
|
+
if(!existsSync(secretDir))mkdirSync(secretDir,{recursive:true,mode:0o700});
|
|
41
|
+
privateDirectory(secretDir);
|
|
42
|
+
if(existsSync(secretPath))privateFile(secretPath);
|
|
43
|
+
const updated=original.replace(oldRef,fileRef);
|
|
44
|
+
// The secret is never written to argv, the profile or the CLI result.
|
|
45
|
+
putAtomic(secretPath,key);
|
|
46
|
+
if(updated!==original)putAtomic(profile,updated);
|
|
47
|
+
return {status:'CREDENTIAL_STORED',connection:'UNVERIFIED',profile};
|
|
48
|
+
}
|
|
49
|
+
export async function promptTunnelRuntimeKey(){
|
|
50
|
+
if(!process.stdin.isTTY||!process.stderr.isTTY)throw fail('INTERACTIVE_TTY_REQUIRED');
|
|
51
|
+
// readline enters raw terminal mode; its output is muted so pasted keys are not echoed.
|
|
52
|
+
const silent=new Writable({write(_chunk,_encoding,callback){callback();}});
|
|
53
|
+
const input=createInterface({input:process.stdin,output:silent,terminal:true});
|
|
54
|
+
try{
|
|
55
|
+
process.stderr.write('Paste the restricted Chat Loop runtime key (input hidden), then Enter: ');
|
|
56
|
+
return await new Promise((resolve,reject)=>{
|
|
57
|
+
input.question('',answer=>resolve(answer));
|
|
58
|
+
input.once('SIGINT',()=>reject(fail('CANCELLED')));
|
|
59
|
+
});
|
|
60
|
+
}finally{
|
|
61
|
+
input.close();
|
|
62
|
+
process.stderr.write('\n');
|
|
63
|
+
}
|
|
64
|
+
}
|