@mebular/fleet 0.0.0-stage → 0.1.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/dist/agentdir.d.ts +43 -0
- package/dist/agentdir.d.ts.map +1 -0
- package/dist/agentdir.js +107 -0
- package/dist/agentdir.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1135 -0
- package/dist/cli.js.map +1 -0
- package/dist/collab/chatter.d.ts +54 -0
- package/dist/collab/chatter.d.ts.map +1 -0
- package/dist/collab/chatter.js +94 -0
- package/dist/collab/chatter.js.map +1 -0
- package/dist/collab/dag.d.ts +27 -0
- package/dist/collab/dag.d.ts.map +1 -0
- package/dist/collab/dag.js +107 -0
- package/dist/collab/dag.js.map +1 -0
- package/dist/collab/negotiation.d.ts +60 -0
- package/dist/collab/negotiation.d.ts.map +1 -0
- package/dist/collab/negotiation.js +90 -0
- package/dist/collab/negotiation.js.map +1 -0
- package/dist/collab/tree.d.ts +39 -0
- package/dist/collab/tree.d.ts.map +1 -0
- package/dist/collab/tree.js +177 -0
- package/dist/collab/tree.js.map +1 -0
- package/dist/config.d.ts +123 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +206 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/join.d.ts +50 -0
- package/dist/join.d.ts.map +1 -0
- package/dist/join.js +167 -0
- package/dist/join.js.map +1 -0
- package/dist/jointoken.d.ts +144 -0
- package/dist/jointoken.d.ts.map +1 -0
- package/dist/jointoken.js +472 -0
- package/dist/jointoken.js.map +1 -0
- package/dist/model.d.ts +64 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +158 -0
- package/dist/model.js.map +1 -0
- package/dist/onboard.d.ts +90 -0
- package/dist/onboard.d.ts.map +1 -0
- package/dist/onboard.js +510 -0
- package/dist/onboard.js.map +1 -0
- package/dist/protocol/envelope.d.ts +94 -0
- package/dist/protocol/envelope.d.ts.map +1 -0
- package/dist/protocol/envelope.js +169 -0
- package/dist/protocol/envelope.js.map +1 -0
- package/dist/protocol/events.d.ts +51 -0
- package/dist/protocol/events.d.ts.map +1 -0
- package/dist/protocol/events.js +87 -0
- package/dist/protocol/events.js.map +1 -0
- package/dist/qr.d.ts +15 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +52 -0
- package/dist/qr.js.map +1 -0
- package/dist/quickstart.d.ts +167 -0
- package/dist/quickstart.d.ts.map +1 -0
- package/dist/quickstart.js +482 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/quota.d.ts +41 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +84 -0
- package/dist/quota.js.map +1 -0
- package/dist/runtime/agent.d.ts +100 -0
- package/dist/runtime/agent.d.ts.map +1 -0
- package/dist/runtime/agent.js +260 -0
- package/dist/runtime/agent.js.map +1 -0
- package/dist/runtime/executor.d.ts +42 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +87 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/fairness.d.ts +25 -0
- package/dist/runtime/fairness.d.ts.map +1 -0
- package/dist/runtime/fairness.js +79 -0
- package/dist/runtime/fairness.js.map +1 -0
- package/dist/runtime/node.d.ts +91 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +203 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/openchamber-http.d.ts +27 -0
- package/dist/runtime/openchamber-http.d.ts.map +1 -0
- package/dist/runtime/openchamber-http.js +118 -0
- package/dist/runtime/openchamber-http.js.map +1 -0
- package/dist/runtime/openchamber.d.ts +38 -0
- package/dist/runtime/openchamber.d.ts.map +1 -0
- package/dist/runtime/openchamber.js +43 -0
- package/dist/runtime/openchamber.js.map +1 -0
- package/dist/runtime/planner.d.ts +23 -0
- package/dist/runtime/planner.d.ts.map +1 -0
- package/dist/runtime/planner.js +17 -0
- package/dist/runtime/planner.js.map +1 -0
- package/dist/runtime/worker.d.ts +96 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +296 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/security.d.ts +462 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1197 -0
- package/dist/security.js.map +1 -0
- package/dist/store/daemon-store.d.ts +33 -0
- package/dist/store/daemon-store.d.ts.map +1 -0
- package/dist/store/daemon-store.js +115 -0
- package/dist/store/daemon-store.js.map +1 -0
- package/dist/store/file-store.d.ts +22 -0
- package/dist/store/file-store.d.ts.map +1 -0
- package/dist/store/file-store.js +63 -0
- package/dist/store/file-store.js.map +1 -0
- package/dist/store/mebular-store.d.ts +22 -0
- package/dist/store/mebular-store.d.ts.map +1 -0
- package/dist/store/mebular-store.js +53 -0
- package/dist/store/mebular-store.js.map +1 -0
- package/dist/store/message-store.d.ts +26 -0
- package/dist/store/message-store.d.ts.map +1 -0
- package/dist/store/message-store.js +51 -0
- package/dist/store/message-store.js.map +1 -0
- package/dist/surface.d.ts +43 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +515 -0
- package/dist/surface.js.map +1 -0
- package/dist/transport/null.d.ts +7 -0
- package/dist/transport/null.d.ts.map +1 -0
- package/dist/transport/null.js +16 -0
- package/dist/transport/null.js.map +1 -0
- package/dist/transport/spool.d.ts +12 -0
- package/dist/transport/spool.d.ts.map +1 -0
- package/dist/transport/spool.js +54 -0
- package/dist/transport/spool.js.map +1 -0
- package/dist/transport/types.d.ts +21 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +6 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/version.json +6 -0
- package/package.json +37 -4
- package/protocol/collab.example.json +23 -0
- package/protocol/daemon-home.example.json +66 -0
- package/protocol/envelope.example.json +12 -0
- package/protocol/state-machine.json +14 -0
- package/protocol/task-tree.example.json +72 -0
- package/src/agentdir.ts +138 -0
- package/src/cli.ts +1192 -0
- package/src/collab/chatter.ts +116 -0
- package/src/collab/dag.ts +124 -0
- package/src/collab/negotiation.ts +130 -0
- package/src/collab/tree.ts +184 -0
- package/src/config.ts +286 -0
- package/src/index.ts +39 -0
- package/src/join.ts +227 -0
- package/src/jointoken.ts +609 -0
- package/src/model.ts +203 -0
- package/src/onboard.ts +601 -0
- package/src/protocol/envelope.ts +228 -0
- package/src/protocol/events.ts +135 -0
- package/src/qr.ts +68 -0
- package/src/quickstart.ts +632 -0
- package/src/quota.ts +112 -0
- package/src/runtime/agent.ts +313 -0
- package/src/runtime/executor.ts +114 -0
- package/src/runtime/fairness.ts +90 -0
- package/src/runtime/node.ts +250 -0
- package/src/runtime/openchamber-http.ts +137 -0
- package/src/runtime/openchamber.ts +66 -0
- package/src/runtime/planner.ts +36 -0
- package/src/runtime/worker.ts +354 -0
- package/src/security.ts +1660 -0
- package/src/store/daemon-store.ts +144 -0
- package/src/store/file-store.ts +75 -0
- package/src/store/mebular-store.ts +66 -0
- package/src/store/message-store.ts +62 -0
- package/src/surface.ts +535 -0
- package/src/transport/null.ts +20 -0
- package/src/transport/spool.ts +62 -0
- package/src/transport/types.ts +27 -0
- package/README.md +0 -3
package/src/config.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Fleet 设备配置与用户主密钥 IO(Step 1b):全部 **0600**、默认脱敏、绝不回显密钥材料。
|
|
2
|
+
//
|
|
3
|
+
// 目录(device home)权限 0700;配置文件 `fleet.config.json`;主密钥 `master-key.json`;
|
|
4
|
+
// 事件存储 `store.jsonl`(设备身份 `store.jsonl.identity.json` 由 core 落盘)。
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { basename, dirname, join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const SUBTLE = globalThis.crypto.subtle;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 平台策略:POSIX mode 位是否构成有效保护。
|
|
15
|
+
*
|
|
16
|
+
* **Windows 上不成立**(`chmod`/`mode` 只有只读位语义,`stat().mode` 恒为 0666/0444),
|
|
17
|
+
* 因此 doctor 在 win32 上对权限项**显式 SKIP**(写入 `skipped`),而不是对 0666 判 FAIL。
|
|
18
|
+
* 威胁模型差异见 `ONBOARDING.md` 的 Windows 章节:Windows 依赖用户配置目录的 ACL
|
|
19
|
+
* (`%USERPROFILE%` 默认仅本人可读)与 OS 会话边界,等价性由平台保证而非 fleet 断言。
|
|
20
|
+
*
|
|
21
|
+
* 参数可注入(默认 `process.platform`),便于在任意平台确定性地测试两种分支。
|
|
22
|
+
*/
|
|
23
|
+
export function permissionsApplicable(platform: string = process.platform): boolean {
|
|
24
|
+
return platform !== 'win32';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** core `Mebular` 需要的加密材料(用户主密钥)。令牌加入的设备**只有主公钥**(无主私钥)。 */
|
|
28
|
+
export interface FleetEncryption {
|
|
29
|
+
userMasterKey: Uint8Array;
|
|
30
|
+
userMasterPrivateKey?: CryptoKey;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 生成 Ed25519 用户主密钥。 */
|
|
34
|
+
export async function generateMasterKey(): Promise<FleetEncryption> {
|
|
35
|
+
const keyPair = (await SUBTLE.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])) as CryptoKeyPair;
|
|
36
|
+
const userMasterKey = new Uint8Array(await SUBTLE.exportKey('raw', keyPair.publicKey));
|
|
37
|
+
return { userMasterKey, userMasterPrivateKey: keyPair.privateKey };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 写入主密钥文件(0600)。无主私钥(令牌加入设备)时只写主公钥。 */
|
|
41
|
+
export async function writeMasterKeyFile(path: string, encryption: FleetEncryption): Promise<void> {
|
|
42
|
+
const record: { v: number; alg: string; publicKey: string; privateKeyPkcs8?: string } = {
|
|
43
|
+
v: 1,
|
|
44
|
+
alg: 'Ed25519',
|
|
45
|
+
publicKey: Buffer.from(encryption.userMasterKey).toString('base64'),
|
|
46
|
+
};
|
|
47
|
+
if (encryption.userMasterPrivateKey !== undefined) {
|
|
48
|
+
const pkcs8 = new Uint8Array(await SUBTLE.exportKey('pkcs8', encryption.userMasterPrivateKey));
|
|
49
|
+
record.privateKeyPkcs8 = Buffer.from(pkcs8).toString('base64');
|
|
50
|
+
}
|
|
51
|
+
await mkdir(dirname(path), { recursive: true });
|
|
52
|
+
await writeFile(path, JSON.stringify(record), { mode: 0o600 });
|
|
53
|
+
if (permissionsApplicable()) await chmod(path, 0o600);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 读取主密钥文件(不校验权限;权限由 doctor 断言)。 */
|
|
57
|
+
export async function readMasterKeyFile(path: string): Promise<FleetEncryption> {
|
|
58
|
+
const parsed = JSON.parse(await readFile(path, 'utf-8')) as {
|
|
59
|
+
v?: number;
|
|
60
|
+
publicKey?: string;
|
|
61
|
+
privateKeyPkcs8?: string;
|
|
62
|
+
};
|
|
63
|
+
// 容忍缺省 `v`:`mebular` 侧生成的主密钥文件(<home>/user-master-key.json)不带版本字段。
|
|
64
|
+
if ((parsed.v !== undefined && parsed.v !== 1) || typeof parsed.publicKey !== 'string') {
|
|
65
|
+
throw new Error('master key file: 形状非法');
|
|
66
|
+
}
|
|
67
|
+
if (parsed.privateKeyPkcs8 === undefined) {
|
|
68
|
+
// 令牌加入设备:只有主公钥(无主私钥),用于验证对端/设备证书链。
|
|
69
|
+
return { userMasterKey: new Uint8Array(Buffer.from(parsed.publicKey, 'base64')) };
|
|
70
|
+
}
|
|
71
|
+
const userMasterKey = new Uint8Array(Buffer.from(parsed.publicKey, 'base64'));
|
|
72
|
+
const userMasterPrivateKey = await SUBTLE.importKey(
|
|
73
|
+
'pkcs8',
|
|
74
|
+
Buffer.from(parsed.privateKeyPkcs8, 'base64'),
|
|
75
|
+
{ name: 'Ed25519' },
|
|
76
|
+
true,
|
|
77
|
+
['sign'],
|
|
78
|
+
);
|
|
79
|
+
return { userMasterKey, userMasterPrivateKey };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 文件权限(低 9 位八进制)。 */
|
|
83
|
+
export async function fileMode(path: string): Promise<number> {
|
|
84
|
+
return (await stat(path)).mode & 0o777;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type FleetAgentKind = 'echo' | 'command' | 'hermes' | 'openchamber';
|
|
88
|
+
|
|
89
|
+
export interface FleetAgentConfig {
|
|
90
|
+
name: string;
|
|
91
|
+
kind: FleetAgentKind;
|
|
92
|
+
/** kind=command 时的可执行文件 */
|
|
93
|
+
command?: string;
|
|
94
|
+
baseArgs?: string[];
|
|
95
|
+
timeoutMs?: number;
|
|
96
|
+
concurrency?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface FleetPeerConfig {
|
|
100
|
+
device: string;
|
|
101
|
+
/** 可选 multiaddr(用于拨号;缺省则等待对端拨入) */
|
|
102
|
+
addr?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FleetConfig {
|
|
106
|
+
v: 1;
|
|
107
|
+
device: string;
|
|
108
|
+
dir: string;
|
|
109
|
+
storagePath: string;
|
|
110
|
+
masterKeyFile: string;
|
|
111
|
+
namespace: string;
|
|
112
|
+
/** 本机监听 multiaddr(libp2p) */
|
|
113
|
+
listen: string;
|
|
114
|
+
/** 已登记对端(地址簿;连接准入由 peerWhitelist/信任链决定) */
|
|
115
|
+
peers: FleetPeerConfig[];
|
|
116
|
+
/** 本机 agent 注册表(按 to.agent 名路由) */
|
|
117
|
+
agents: FleetAgentConfig[];
|
|
118
|
+
quotaLimitPerDevice?: number;
|
|
119
|
+
/**
|
|
120
|
+
* join 服务(T2):`fleet node` 启动最小 HTTP 端点 `POST /mebular/join` 以签发委派证书。
|
|
121
|
+
* 令牌是 bearer 凭据(短 TTL + 一次性 nonce);建议仅在 LAN 绑定。
|
|
122
|
+
*/
|
|
123
|
+
joinService?: { enabled: boolean; bind?: string; port?: number };
|
|
124
|
+
/**
|
|
125
|
+
* W2 存储模式:`daemon`(默认于统一上车;走守护 app 接口,fleet 客户端化)/ `embedded`(**仅测试/CI**)。
|
|
126
|
+
* 缺省(未设)= `embedded`,以保持既有测试/验收不变;`quickstart` 会写 `daemon`。
|
|
127
|
+
*/
|
|
128
|
+
store?: 'daemon' | 'embedded';
|
|
129
|
+
/** W2 守护 app 端点与令牌(daemon 模式必需) */
|
|
130
|
+
daemon?: { endpoint: string; token?: string; tokenFile?: string };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const fleetConfigPath = (dir: string): string => join(dir, 'fleet.config.json');
|
|
134
|
+
export const fleetMasterKeyPath = (dir: string): string => join(dir, 'master-key.json');
|
|
135
|
+
export const fleetStoragePath = (dir: string): string => join(dir, 'store.jsonl');
|
|
136
|
+
|
|
137
|
+
/** 校验配置;返回错误列表(空 = 合法)。 */
|
|
138
|
+
export function validateFleetConfig(input: unknown): string[] {
|
|
139
|
+
const errors: string[] = [];
|
|
140
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) return ['config 必须是对象'];
|
|
141
|
+
const c = input as Record<string, unknown>;
|
|
142
|
+
if (c.v !== 1) errors.push('v 必须为 1');
|
|
143
|
+
for (const key of ['device', 'dir', 'storagePath', 'masterKeyFile', 'namespace', 'listen']) {
|
|
144
|
+
if (typeof c[key] !== 'string' || (c[key] as string).length === 0) errors.push(`${key} 必须为非空字符串`);
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(c.peers)) errors.push('peers 必须为数组');
|
|
147
|
+
if (c.store !== undefined && c.store !== 'daemon' && c.store !== 'embedded') errors.push('store 必须为 daemon|embedded');
|
|
148
|
+
if (c.daemon !== undefined) {
|
|
149
|
+
const d = c.daemon as Record<string, unknown>;
|
|
150
|
+
if (typeof d !== 'object' || d === null || typeof d.endpoint !== 'string' || d.endpoint.length === 0) errors.push('daemon.endpoint 必填');
|
|
151
|
+
else if (d.token !== undefined && typeof d.token !== 'string') errors.push('daemon.token 非法');
|
|
152
|
+
else if (d.tokenFile !== undefined && typeof d.tokenFile !== 'string') errors.push('daemon.tokenFile 非法');
|
|
153
|
+
}
|
|
154
|
+
if (c.joinService !== undefined) {
|
|
155
|
+
const js = c.joinService as Record<string, unknown>;
|
|
156
|
+
if (typeof js !== 'object' || js === null || typeof js.enabled !== 'boolean') errors.push('joinService 形状非法');
|
|
157
|
+
else if (js.port !== undefined && (typeof js.port !== 'number' || js.port < 0 || js.port > 65535)) errors.push('joinService.port 非法');
|
|
158
|
+
else if (js.bind !== undefined && typeof js.bind !== 'string') errors.push('joinService.bind 非法');
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(c.agents) || (c.agents as unknown[]).length === 0) errors.push('agents 必须为非空数组');
|
|
161
|
+
for (const agent of (Array.isArray(c.agents) ? c.agents : []) as Array<Record<string, unknown>>) {
|
|
162
|
+
if (typeof agent?.name !== 'string' || agent.name.length === 0) errors.push('agent.name 非法');
|
|
163
|
+
if (!['echo', 'command', 'hermes', 'openchamber'].includes(String(agent?.kind))) {
|
|
164
|
+
errors.push(`agent.kind 非法:${String(agent?.kind)}`);
|
|
165
|
+
}
|
|
166
|
+
if (agent?.kind === 'command' && (typeof agent.command !== 'string' || agent.command.length === 0)) {
|
|
167
|
+
errors.push('command agent 需要 command');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return errors;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 解析 `--agent name:kind[,name:kind…]`(+ `--agent-command` / `--agent-base-args`)。
|
|
175
|
+
* `command` agent 一律经调用方给定的 `command`(推荐 `process.execPath`)+ `baseArgs`
|
|
176
|
+
* (如 fixture 路径)调用——**不依赖 shebang/可执行位**,Windows 亦然。
|
|
177
|
+
*/
|
|
178
|
+
export function parseAgentSpecs(
|
|
179
|
+
value: string,
|
|
180
|
+
options: { command?: string; baseArgs?: readonly string[] } = {},
|
|
181
|
+
): FleetAgentConfig[] {
|
|
182
|
+
return value
|
|
183
|
+
.split(',')
|
|
184
|
+
.map((entry) => entry.trim())
|
|
185
|
+
.filter((entry) => entry.length > 0)
|
|
186
|
+
.map((entry) => {
|
|
187
|
+
const [name, kind] = entry.split(':');
|
|
188
|
+
const agent: FleetAgentConfig = { name: name || 'echo', kind: (kind as FleetAgentKind) || 'echo' };
|
|
189
|
+
if (agent.kind === 'command') {
|
|
190
|
+
if (options.command !== undefined) agent.command = options.command;
|
|
191
|
+
if (options.baseArgs !== undefined && options.baseArgs.length > 0) agent.baseArgs = [...options.baseArgs];
|
|
192
|
+
}
|
|
193
|
+
return agent;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 保存配置(0600)。 */
|
|
198
|
+
export async function saveFleetConfig(path: string, config: FleetConfig): Promise<void> {
|
|
199
|
+
await mkdir(dirname(path), { recursive: true });
|
|
200
|
+
await writeFile(path, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
201
|
+
if (permissionsApplicable()) await chmod(path, 0o600);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 加载配置(形状非法即抛错)。 */
|
|
205
|
+
export async function loadFleetConfig(path: string): Promise<FleetConfig> {
|
|
206
|
+
const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
|
|
207
|
+
const errors = validateFleetConfig(parsed);
|
|
208
|
+
if (errors.length > 0) throw new Error(`config 非法:${errors.join('; ')}`);
|
|
209
|
+
return parsed as FleetConfig;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* F-UNI:任务工具的 home 适配——两种布局都支持:
|
|
214
|
+
* 1) fleet home:`<dir>/fleet.config.json`(本机 fleet 设备)
|
|
215
|
+
* 2) **守护 home**:`<dir>/config.json`(`mebular serve`,含 root/delegated 两种身份)
|
|
216
|
+
* 由守护发起 `mebular mcp` 时 home 是守护 home,故这里把守护配置映射为等价 FleetConfig;
|
|
217
|
+
* 业务逻辑(session/工具 handler)保持**唯一**在 fleet 侧。
|
|
218
|
+
*/
|
|
219
|
+
export function daemonConfigToFleet(dir: string, cfg: Record<string, unknown>): FleetConfig {
|
|
220
|
+
const encryption = (cfg.encryption ?? {}) as Record<string, unknown>;
|
|
221
|
+
const sync = (cfg.sync ?? {}) as Record<string, unknown>;
|
|
222
|
+
const network = (cfg.network ?? {}) as Record<string, unknown>;
|
|
223
|
+
const libp2p = (network.libp2p ?? {}) as Record<string, unknown>;
|
|
224
|
+
const namespaces = Array.isArray(sync.namespaces) ? sync.namespaces.filter((s): s is string => typeof s === 'string' && s.length > 0) : [];
|
|
225
|
+
const listen = Array.isArray(libp2p.listen) && typeof libp2p.listen[0] === 'string' ? libp2p.listen[0] : '/ip4/0.0.0.0/tcp/4001';
|
|
226
|
+
const peers = (Array.isArray(network.peers) ? network.peers : [])
|
|
227
|
+
.filter((p): p is Record<string, unknown> => typeof p === 'object' && p !== null && typeof (p as Record<string, unknown>).device === 'string')
|
|
228
|
+
.map((p) => ({ device: String(p.device), ...(typeof p.addr === 'string' && p.addr.length > 0 ? { addr: p.addr } : {}) }));
|
|
229
|
+
const storagePath = typeof cfg.storagePath === 'string' && cfg.storagePath.length > 0 ? cfg.storagePath : join(dir, 'store.jsonl');
|
|
230
|
+
const masterKeyFile = typeof encryption.userMasterPublicKeyFile === 'string' && encryption.userMasterPublicKeyFile.length > 0
|
|
231
|
+
? encryption.userMasterPublicKeyFile
|
|
232
|
+
: typeof encryption.keyFile === 'string' && encryption.keyFile.length > 0
|
|
233
|
+
? encryption.keyFile
|
|
234
|
+
: join(dir, 'user-master-key.json');
|
|
235
|
+
const device = typeof cfg.deviceId === 'string' && cfg.deviceId.length > 0 ? cfg.deviceId : `device-${basename(dir) || 'local'}`;
|
|
236
|
+
return {
|
|
237
|
+
v: 1,
|
|
238
|
+
device,
|
|
239
|
+
dir,
|
|
240
|
+
storagePath,
|
|
241
|
+
masterKeyFile,
|
|
242
|
+
namespace: namespaces[0] ?? 'tasks',
|
|
243
|
+
listen,
|
|
244
|
+
peers,
|
|
245
|
+
// 守护侧只作任务板(board),不注册执行型 agent;占位满足校验(kind 属合法枚举)
|
|
246
|
+
agents: [{ name: 'board', kind: 'echo' }],
|
|
247
|
+
...(typeof cfg.quotaLimitPerDevice === 'number' ? { quotaLimitPerDevice: cfg.quotaLimitPerDevice } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 读任务工具上下文配置:优先 fleet.config.json,否则按守护 config.json 适配。 */
|
|
252
|
+
export async function loadToolConfig(dir: string): Promise<FleetConfig> {
|
|
253
|
+
const fleetPath = fleetConfigPath(dir);
|
|
254
|
+
if (existsSync(fleetPath)) return loadFleetConfig(fleetPath);
|
|
255
|
+
const daemonPath = join(dir, 'config.json');
|
|
256
|
+
if (!existsSync(daemonPath)) {
|
|
257
|
+
throw new Error(`缺少配置:${fleetPath} 与 ${daemonPath} 均不存在(需要 fleet.config.json 或守护 config.json)`);
|
|
258
|
+
}
|
|
259
|
+
const raw = JSON.parse(await readFile(daemonPath, 'utf-8')) as Record<string, unknown>;
|
|
260
|
+
return daemonConfigToFleet(dir, raw);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface OnboardInput {
|
|
264
|
+
dir: string;
|
|
265
|
+
device: string;
|
|
266
|
+
/** 已存在的用户主密钥文件(导入);不给则生成 */
|
|
267
|
+
masterKeyFile?: string;
|
|
268
|
+
peerDevice?: string;
|
|
269
|
+
peerAddr?: string;
|
|
270
|
+
namespace?: string;
|
|
271
|
+
listen?: string;
|
|
272
|
+
agents?: FleetAgentConfig[];
|
|
273
|
+
quotaLimitPerDevice?: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface OnboardResult {
|
|
277
|
+
config: FleetConfig;
|
|
278
|
+
alreadyOnboarded: boolean;
|
|
279
|
+
masterKeyCreated: boolean;
|
|
280
|
+
masterKeyFingerprint: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 主密钥指纹(sha256 前 12 hex;**非密钥材料**,可安全展示/用于匹配)。 */
|
|
284
|
+
export function masterKeyFingerprint(publicKey: Uint8Array): string {
|
|
285
|
+
return `sha256:${createHash('sha256').update(publicKey).digest('hex').slice(0, 12)}`;
|
|
286
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @mebular/fleet —— 跨设备多 Agent 协作(任务=记忆)的公共入口。
|
|
2
|
+
//
|
|
3
|
+
// 边界(红线,见 DESIGN.md 与仓库 SEALING.md):
|
|
4
|
+
// - 只消费 `@mebular/core` 的**公共 API**(`import { ... } from '@mebular/core'`),
|
|
5
|
+
// 不得 `import` core 的内部路径(`src/**`);
|
|
6
|
+
// - core 不得反向依赖本包(`tests/fleet/boundary.test.ts` 强制);
|
|
7
|
+
// - 墙钟(`expiresAt`)不进一致性判定。
|
|
8
|
+
export * from './protocol/envelope.js';
|
|
9
|
+
export * from './protocol/events.js';
|
|
10
|
+
export * from './model.js';
|
|
11
|
+
export * from './quota.js';
|
|
12
|
+
export * from './agentdir.js';
|
|
13
|
+
export * from './surface.js';
|
|
14
|
+
export * from './config.js';
|
|
15
|
+
export * from './onboard.js';
|
|
16
|
+
export * from './security.js';
|
|
17
|
+
export * from './quickstart.js';
|
|
18
|
+
export * from './jointoken.js';
|
|
19
|
+
export * from './qr.js';
|
|
20
|
+
export * from './join.js';
|
|
21
|
+
export * from './collab/dag.js';
|
|
22
|
+
export * from './collab/tree.js';
|
|
23
|
+
export * from './collab/negotiation.js';
|
|
24
|
+
export * from './collab/chatter.js';
|
|
25
|
+
export * from './transport/types.js';
|
|
26
|
+
export * from './transport/spool.js';
|
|
27
|
+
export * from './transport/null.js';
|
|
28
|
+
export * from './store/file-store.js';
|
|
29
|
+
export * from './store/mebular-store.js';
|
|
30
|
+
export * from './store/message-store.js';
|
|
31
|
+
export * from './store/daemon-store.js';
|
|
32
|
+
export * from './runtime/planner.js';
|
|
33
|
+
export * from './runtime/fairness.js';
|
|
34
|
+
export * from './runtime/executor.js';
|
|
35
|
+
export * from './runtime/agent.js';
|
|
36
|
+
export * from './runtime/openchamber.js';
|
|
37
|
+
export * from './runtime/openchamber-http.js';
|
|
38
|
+
export * from './runtime/node.js';
|
|
39
|
+
export * from './runtime/worker.js';
|
package/src/join.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// 信任模型 v2(T2):B 持**加入令牌**加入(不复制主密钥)。
|
|
2
|
+
//
|
|
3
|
+
// 流程:生成设备密钥 → 向 inviter 的 join 端点请求**委派证书**(令牌验签/TTL/一次性)→
|
|
4
|
+
// 落盘身份文件(含证书链,**无主私钥**)→ 写**主公钥**文件 → onboard(登记 inviter 地址)→ 声明成员。
|
|
5
|
+
// 主密钥私钥全程不出现;inviter 可为**任意**在册设备。
|
|
6
|
+
|
|
7
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { constants } from 'node:fs';
|
|
10
|
+
import { Mebular, IdentityManager, bytesToHex, hexToBytes, EndpointBook, FileEndpointStore, derivePeerIdHex, RELAY_SEEDS_KEY } from '@mebular/core';
|
|
11
|
+
import {
|
|
12
|
+
fleetConfigPath,
|
|
13
|
+
fleetMasterKeyPath,
|
|
14
|
+
fleetStoragePath,
|
|
15
|
+
loadFleetConfig,
|
|
16
|
+
masterKeyFingerprint,
|
|
17
|
+
readMasterKeyFile,
|
|
18
|
+
writeMasterKeyFile,
|
|
19
|
+
type FleetAgentConfig,
|
|
20
|
+
type FleetEncryption,
|
|
21
|
+
} from './config.js';
|
|
22
|
+
import type { ServiceInstaller } from './quickstart.js';
|
|
23
|
+
import { offlineMebularOptions, onboardDevice } from './onboard.js';
|
|
24
|
+
import { agentMcpConfig, configureDaemonHome } from './quickstart.js';
|
|
25
|
+
import { decodeJoinToken, requestJoin } from './jointoken.js';
|
|
26
|
+
|
|
27
|
+
export interface JoinWithTokenInput {
|
|
28
|
+
dir: string;
|
|
29
|
+
token: string;
|
|
30
|
+
device: string;
|
|
31
|
+
namespace?: string;
|
|
32
|
+
listen?: string;
|
|
33
|
+
agents: FleetAgentConfig[];
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
/** W2:同时配置统一守护(delegated 身份 + fleet daemon 客户端 + 令牌) */
|
|
36
|
+
daemon?: boolean;
|
|
37
|
+
daemonPort?: number;
|
|
38
|
+
joinPort?: number;
|
|
39
|
+
installDaemon?: ServiceInstaller;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface JoinWithTokenResult {
|
|
43
|
+
/** C2:写入地址簿的 hint 条数(0 = 旧令牌无 hints) */
|
|
44
|
+
hinted: number;
|
|
45
|
+
device: string;
|
|
46
|
+
dir: string;
|
|
47
|
+
peer: string;
|
|
48
|
+
namespace: string;
|
|
49
|
+
fingerprint: string;
|
|
50
|
+
alreadyJoined: boolean;
|
|
51
|
+
inviterDeviceId: string;
|
|
52
|
+
next: string[];
|
|
53
|
+
/** W2:统一守护配置结果 */
|
|
54
|
+
daemon?: { endpoint: string; installed: boolean; note?: string };
|
|
55
|
+
/** W2:Agent MCP 配置片段 */
|
|
56
|
+
agentMcp: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* C2:把邀请方 hints 落盘到 `<dir>/net/peers.json`(0600)。
|
|
61
|
+
* 同时写 **deviceId 键**(可读)与 **peerId 键**(由 inviterPublicKey 派生,首次拨号即可命中)。
|
|
62
|
+
* 返回写入条数;无 hints(旧令牌)→ 0。任何失败都不应让 join 失败。
|
|
63
|
+
*/
|
|
64
|
+
export async function persistInviterHints(dir: string, token: {
|
|
65
|
+
inviterDeviceId: string;
|
|
66
|
+
inviterPublicKey: string;
|
|
67
|
+
endpoints?: string[];
|
|
68
|
+
relaySeeds?: string[];
|
|
69
|
+
}): Promise<number> {
|
|
70
|
+
try {
|
|
71
|
+
const hints = Array.isArray(token.endpoints) ? token.endpoints.filter((a) => typeof a === 'string' && a.length > 0) : [];
|
|
72
|
+
const seeds = Array.isArray(token.relaySeeds) ? token.relaySeeds.filter((a) => typeof a === 'string' && a.length > 0) : [];
|
|
73
|
+
if (hints.length === 0 && seeds.length === 0) return 0;
|
|
74
|
+
const book = new EndpointBook({ store: new FileEndpointStore(join(dir, 'net', 'peers.json')) });
|
|
75
|
+
await book.load();
|
|
76
|
+
let count = 0;
|
|
77
|
+
const keys = [token.inviterDeviceId];
|
|
78
|
+
try {
|
|
79
|
+
keys.push(derivePeerIdHex(hexToBytes(token.inviterPublicKey)));
|
|
80
|
+
} catch {
|
|
81
|
+
// 公钥非法:只写 deviceId 键
|
|
82
|
+
}
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
if (hints.length > 0) count += await book.upsert(key, hints, 'paired');
|
|
85
|
+
}
|
|
86
|
+
// relay seeds 存保留键(不是对端地址;app 启动时并入 libp2p.relayServers)
|
|
87
|
+
if (seeds.length > 0) await book.upsert(RELAY_SEEDS_KEY, seeds, 'config');
|
|
88
|
+
return count;
|
|
89
|
+
} catch {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** B:用令牌加入(**不持有主密钥私钥**)。 */
|
|
95
|
+
export async function joinWithToken(input: JoinWithTokenInput): Promise<JoinWithTokenResult> {
|
|
96
|
+
const token = decodeJoinToken(input.token);
|
|
97
|
+
const namespace = input.namespace ?? token.namespace;
|
|
98
|
+
const listen = input.listen ?? '/ip4/0.0.0.0/tcp/4001';
|
|
99
|
+
|
|
100
|
+
const alreadyJoined = await exists(fleetConfigPath(input.dir));
|
|
101
|
+
if (alreadyJoined) {
|
|
102
|
+
const existing = await loadFleetConfig(fleetConfigPath(input.dir));
|
|
103
|
+
if (existing.device !== input.device) {
|
|
104
|
+
throw new Error(`目录已被设备 ${existing.device} 占用(--dir ${input.dir});换目录或先停用。`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await mkdir(input.dir, { recursive: true });
|
|
109
|
+
const storagePath = fleetStoragePath(input.dir);
|
|
110
|
+
const keyPath = fleetMasterKeyPath(input.dir);
|
|
111
|
+
let peerAddr: string | undefined;
|
|
112
|
+
let securityApproval: { approvalId: string; nonce: string; initiator?: string; cosign?: unknown } | undefined;
|
|
113
|
+
|
|
114
|
+
if (!alreadyJoined) {
|
|
115
|
+
const im = new IdentityManager();
|
|
116
|
+
const deviceKey = await im.generateDeviceKey(input.device, input.device);
|
|
117
|
+
const issued = await requestJoin({
|
|
118
|
+
endpoint: token.endpoint,
|
|
119
|
+
token: input.token,
|
|
120
|
+
deviceId: input.device,
|
|
121
|
+
devicePublicKeyHex: bytesToHex(deviceKey.publicKey),
|
|
122
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
123
|
+
});
|
|
124
|
+
peerAddr = issued.inviterMultiaddrs[0];
|
|
125
|
+
securityApproval = issued.securityApproval;
|
|
126
|
+
const privateKeyPkcs8 = await IdentityManager.exportPrivateKey(deviceKey.privateKey);
|
|
127
|
+
const record = {
|
|
128
|
+
deviceId: input.device,
|
|
129
|
+
deviceName: input.device,
|
|
130
|
+
publicKeyHex: bytesToHex(deviceKey.publicKey),
|
|
131
|
+
certificate: issued.certificate,
|
|
132
|
+
certificateChain: issued.chain,
|
|
133
|
+
createdAt: Date.now(),
|
|
134
|
+
privateKeyPkcs8,
|
|
135
|
+
};
|
|
136
|
+
await writeFile(`${storagePath}.identity.json`, JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
137
|
+
const publicOnly: FleetEncryption = { userMasterKey: hexToBytes(token.masterPublicKey) };
|
|
138
|
+
await writeMasterKeyFile(keyPath, publicOnly);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// C2:把邀请方 hints(token.endpoints)写入本机地址簿 <dir>/net/peers.json(0600)
|
|
142
|
+
// 机制在 core(EndpointBook/FileEndpointStore),路径由 app 决定;不改变任何授权。
|
|
143
|
+
const hinted = await persistInviterHints(input.dir, token);
|
|
144
|
+
|
|
145
|
+
const onboard = await onboardDevice({
|
|
146
|
+
dir: input.dir,
|
|
147
|
+
device: input.device,
|
|
148
|
+
namespace,
|
|
149
|
+
listen,
|
|
150
|
+
agents: input.agents,
|
|
151
|
+
peerDevice: token.inviterDeviceId,
|
|
152
|
+
...(peerAddr !== undefined ? { peerAddr } : {}),
|
|
153
|
+
});
|
|
154
|
+
const config = onboard.config;
|
|
155
|
+
const encryption = await readMasterKeyFile(config.masterKeyFile);
|
|
156
|
+
|
|
157
|
+
const mebular = new Mebular(offlineMebularOptions(config, encryption) as never);
|
|
158
|
+
await mebular.initialize();
|
|
159
|
+
try {
|
|
160
|
+
await mebular.declareNamespaceSubscription({
|
|
161
|
+
namespace,
|
|
162
|
+
active: true,
|
|
163
|
+
note: 'join-token self',
|
|
164
|
+
...(securityApproval !== undefined
|
|
165
|
+
? {
|
|
166
|
+
initiator: securityApproval.initiator,
|
|
167
|
+
nonce: securityApproval.nonce,
|
|
168
|
+
...(securityApproval.cosign !== undefined ? { cosign: securityApproval.cosign as never } : {}),
|
|
169
|
+
}
|
|
170
|
+
: {}),
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
await mebular.shutdown();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// W2 B2:delegated 守护 home(无主私钥)+ fleet daemon 客户端 + (可选)mebular-serve
|
|
177
|
+
let daemonInfo: { endpoint: string; installed: boolean; note?: string } | null = null;
|
|
178
|
+
if (input.daemon === true) {
|
|
179
|
+
const publicKeyFile = join(input.dir, 'user-master-key.json');
|
|
180
|
+
await writeFile(publicKeyFile, JSON.stringify({ publicKey: Buffer.from(encryption.userMasterKey).toString('base64') }, null, 2), { mode: 0o600 });
|
|
181
|
+
const peer = peerAddr ?? config.peers[0]?.addr;
|
|
182
|
+
const peers = peer !== undefined ? [{ device: token.inviterDeviceId, addr: peer }] : [{ device: token.inviterDeviceId }];
|
|
183
|
+
const { endpoint } = await configureDaemonHome(
|
|
184
|
+
input.dir,
|
|
185
|
+
config,
|
|
186
|
+
namespace,
|
|
187
|
+
input.joinPort ?? 4002,
|
|
188
|
+
input.daemonPort ?? 7331,
|
|
189
|
+
{ userMasterPublicKeyFile: publicKeyFile },
|
|
190
|
+
peers,
|
|
191
|
+
);
|
|
192
|
+
let installed = false;
|
|
193
|
+
let note: string | undefined;
|
|
194
|
+
if (input.installDaemon !== undefined) {
|
|
195
|
+
const outcome = await input.installDaemon(input.dir);
|
|
196
|
+
installed = outcome.installed;
|
|
197
|
+
note = outcome.note;
|
|
198
|
+
}
|
|
199
|
+
daemonInfo = { endpoint, installed, ...(note !== undefined ? { note } : {}) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
hinted,
|
|
204
|
+
device: config.device,
|
|
205
|
+
dir: config.dir,
|
|
206
|
+
peer: token.inviterDeviceId,
|
|
207
|
+
namespace,
|
|
208
|
+
fingerprint: masterKeyFingerprint(encryption.userMasterKey),
|
|
209
|
+
alreadyJoined,
|
|
210
|
+
inviterDeviceId: token.inviterDeviceId,
|
|
211
|
+
next: [
|
|
212
|
+
'本机已声明订阅;对端同步后即可互通(订阅零门槛,无需批准)',
|
|
213
|
+
...(hinted > 0 ? [`已写入邀请方地址 hints(${hinted} 条):本机可在无 --addr 时自动选路连通`] : ['令牌未携带地址 hints(旧令牌):首次连接仍需显式 --addr']),
|
|
214
|
+
],
|
|
215
|
+
...(daemonInfo !== null ? { daemon: daemonInfo } : {}),
|
|
216
|
+
agentMcp: agentMcpConfig(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function exists(path: string): Promise<boolean> {
|
|
221
|
+
try {
|
|
222
|
+
await access(path, constants.F_OK);
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|