@foxden-app/foxclaw 0.4.16 → 0.5.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/.env.example CHANGED
@@ -40,7 +40,9 @@ CODEX_CLI_BIN=/absolute/path/to/codex
40
40
  # Optional cross-node Codex auth sync.
41
41
  # Disabled by default. When enabled, FoxClaw uses Telegram Bot-to-Bot private
42
42
  # messages to move encrypted auth bundles between nodes. Enable Bot-to-Bot
43
- # Communication Mode for the participating bots in BotFather first.
43
+ # Communication Mode for each node's contact bot in BotFather first.
44
+ # In TG_BOT_TOKENS multi-bot mode, the first token is the default contact bot;
45
+ # put the node's contact token first and list peer contact @usernames below.
44
46
  # AUTH_SYNC_ENABLED=true
45
47
  # AUTH_SYNC_KEY=<shared-key-with-at-least-32-bytes>
46
48
  # AUTH_SYNC_PEERS=@other_node_bot,@third_node_bot
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.0 - 2026-06-04
6
+
7
+ ### 中文
8
+ - 明确跨节点 auth 同步推荐架构:每节点一个联系人 bot;多 bot 模式默认使用 `TG_BOT_TOKENS` 的第一个 token 作为联系人,同节点其他 bot 继续走本机 auth 镜像。
9
+ - `/auth sync status` 和 `/status` 现在显示联系人 bot,避免在非联系人 bot 上执行命令时误判实际发包身份。
10
+ - `/auth sync test` 升级为等待 peer 加密 pong 的真实握手,并显示未回应 peer;同时文档补充 BotFather MiniApp 入口、不要误进 Configure Mini App,以及 `push all` 只代表发送成功、不代表对端已导入。
11
+ - 跨节点 auth 同步现在会通过联系人 bot 私聊通知发送、接收、排队、导入、跳过、失败和恢复查询进度;所有 peer 都无法提供可用副本时会明确提示人工介入。
12
+
13
+ ### English
14
+ - Clarified the recommended cross-node auth sync topology: one contact bot per node. In multi-bot mode, the first `TG_BOT_TOKENS` entry is the default contact while other same-node bots keep using local auth mirroring.
15
+ - `/auth sync status` and `/status` now show the contact bot so commands run from a non-contact bot no longer obscure the actual sender identity.
16
+ - `/auth sync test` now waits for encrypted peer pong replies and reports missing peers. Documentation now covers the BotFather MiniApp entry point, avoids the Configure Mini App confusion, and explains that `push all` only proves send success, not peer import.
17
+ - Cross-node auth sync now sends contact-bot private notifications for send, receive, queue, import, skip, failure, and recovery query progress; it explicitly asks for manual intervention when every peer lacks an importable copy.
18
+
5
19
  ## 0.4.16 - 2026-06-04
6
20
 
7
21
  ### 中文
package/README.md CHANGED
@@ -212,7 +212,7 @@ TG_BOT_TOKEN=123456:token_a
212
212
 
213
213
  FoxClaw 仍然只运行一个系统服务。默认情况下,它会为每个 bot 启动独立 `codex app-server` 和独立 `CODEX_HOME`。因此 A 私聊运行 turn 时,B 私聊仍可独立切换自己的 `/auth`。候选凭据由 FoxClaw 在登录或刷新在线验证后镜像同步;切换或重载前还会从其他 Codex home 恢复同账号较新凭据。各 bot 的当前选择互不影响。每个 bot 首次私聊发送 `/help` 和 `/status`;`/auth` 会标明正在操作的 bot runtime,`/status` 会列出全部 bot 的连接、runtime 类型、当前 auth 和活动 turn 摘要。
214
214
 
215
- 多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_bot`。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
215
+ 多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_contact_bot`。推荐每台机器只选一个联系人 bot;同一节点内的其他 bot 继续走本机 auth 镜像。多 bot 模式下,默认用 `TG_BOT_TOKENS` 的第一个 token 作为联系人 bot。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本,并在联系人 bot 私聊里报告发送、接收、排队、导入、失败和人工介入提示。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
216
216
 
217
217
  如果你需要一路 Telegram 与终端互通 session,把同一个 token 同时填入 `TG_BOT_TOKENS` 和 `TG_BOT_TOKEN`。这个 bot 使用默认 `CODEX_HOME`(未设置时通常是 `~/.codex`)和默认 auth,因此能看到终端 Codex 的本地线程;它不再享有隔离 runtime 的“互不影响”保证,切换 auth 会影响终端和其他默认 runtime。
218
218
 
package/README_EN.md CHANGED
@@ -212,7 +212,7 @@ TG_BOT_TOKEN=123456:token_a
212
212
 
213
213
  FoxClaw remains one system service. By default, it starts an independent `codex app-server` and `CODEX_HOME` for each bot. While bot A is running a turn, bot B can switch its own `/auth` selection. Candidate credentials are mirrored only after online-validated login or refresh; before switching or reloading, FoxClaw also restores a newer same-account credential from another Codex home when available. Current selections remain independent. Send `/help` and `/status` in a private chat with each bot after installation; `/auth` names the runtime being managed, and `/status` summarizes every bot's connection, runtime type, selected auth, and active turns.
214
214
 
215
- When multiple machines share the same legally owned account pool, optional cross-node auth sync can be enabled with `AUTH_SYNC_ENABLED=true`, `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS=@peer_bot`. FoxClaw sends encrypted auth bundles through Telegram Bot-to-Bot private messages. A locally verified refresh is pushed to peers, and a node with a bad local candidate can pull an already-held valid peer copy. Cross-node recovery never auto-refreshes tokens; `/auth refresh all confirm` first requests a cross-node refresh lease. For full config, `@BotFather` operations, and verification steps, read the [Cross-Node Auth Sync Setup Guide](./docs/cross-node-auth-sync.md).
215
+ When multiple machines share the same legally owned account pool, optional cross-node auth sync can be enabled with `AUTH_SYNC_ENABLED=true`, `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS=@peer_contact_bot`. The recommended topology is one contact bot per node; other bots on the same node continue to use local auth mirroring. In multi-bot mode, the first token in `TG_BOT_TOKENS` is the default contact bot. FoxClaw sends encrypted auth bundles through Telegram Bot-to-Bot private messages. A locally verified refresh is pushed to peers, and a node with a bad local candidate can pull an already-held valid peer copy. The contact bot private chat reports send, receive, queue, import, failure, and manual-intervention states. Cross-node recovery never auto-refreshes tokens; `/auth refresh all confirm` first requests a cross-node refresh lease. For full config, `@BotFather` operations, and verification steps, read the [Cross-Node Auth Sync Setup Guide](./docs/cross-node-auth-sync.md).
216
216
 
217
217
  To keep one Telegram bot interoperable with terminal Codex sessions, put the same token in both `TG_BOT_TOKENS` and `TG_BOT_TOKEN`. That bot uses the default `CODEX_HOME` (usually `~/.codex` when unset) and default auth, so it can see local terminal sessions. It no longer has the isolated runtime guarantee: `/auth` switches also affect the terminal and other default runtimes.
218
218
 
@@ -3,6 +3,7 @@ import type { AuthMirrorCandidateRecord, AuthMirrorImportResult } from './mirror
3
3
  export interface AuthSyncConfig {
4
4
  enabled: boolean;
5
5
  transport: 'telegram-private';
6
+ transportLabel?: string | null;
6
7
  key: string | null;
7
8
  peers: string[];
8
9
  nodeId: string | null;
@@ -18,6 +19,7 @@ export interface AuthSyncStatus {
18
19
  enabled: boolean;
19
20
  nodeId: string | null;
20
21
  transport: 'telegram-private';
22
+ transportLabel: string | null;
21
23
  peers: string[];
22
24
  pendingImports: number;
23
25
  lastSentAt: string | null;
@@ -33,6 +35,101 @@ export interface AuthSyncValidationResult {
33
35
  ok: boolean;
34
36
  reason?: string | null;
35
37
  }
38
+ export type AuthSyncRemoteImportMode = 'push' | 'pull';
39
+ export type AuthSyncPullResponseResult = 'sent' | 'candidate_not_found' | 'account_mismatch' | 'not_newer';
40
+ export type AuthSyncNotification = {
41
+ kind: 'candidate_publish_started';
42
+ candidateName: string;
43
+ peers: string[];
44
+ } | {
45
+ kind: 'candidate_publish_completed';
46
+ candidateName: string;
47
+ peers: string[];
48
+ } | {
49
+ kind: 'candidate_publish_failed';
50
+ candidateName: string;
51
+ peers: string[];
52
+ reason: string;
53
+ } | {
54
+ kind: 'push_all_started';
55
+ candidateCount: number;
56
+ peers: string[];
57
+ } | {
58
+ kind: 'push_all_completed';
59
+ sent: number;
60
+ skipped: number;
61
+ peers: string[];
62
+ } | {
63
+ kind: 'push_all_failed';
64
+ sent: number;
65
+ skipped: number;
66
+ peers: string[];
67
+ reason: string;
68
+ } | {
69
+ kind: 'remote_bundle_received';
70
+ candidateName: string;
71
+ sourceNodeId: string;
72
+ sourceLabel: string;
73
+ peer: string;
74
+ queued: boolean;
75
+ queueLength: number;
76
+ } | {
77
+ kind: 'remote_import_imported';
78
+ candidateName: string;
79
+ sourceNodeId: string;
80
+ sourceLabel: string;
81
+ peer: string;
82
+ mode: AuthSyncRemoteImportMode;
83
+ } | {
84
+ kind: 'remote_import_skipped';
85
+ candidateName: string;
86
+ sourceNodeId: string;
87
+ sourceLabel: string;
88
+ peer: string;
89
+ mode: AuthSyncRemoteImportMode;
90
+ reason: string;
91
+ } | {
92
+ kind: 'remote_import_failed';
93
+ candidateName: string;
94
+ sourceNodeId: string;
95
+ sourceLabel: string;
96
+ peer: string;
97
+ mode: AuthSyncRemoteImportMode;
98
+ reason: string;
99
+ } | {
100
+ kind: 'recovery_started';
101
+ candidateName: string;
102
+ peers: string[];
103
+ } | {
104
+ kind: 'recovery_peer_empty';
105
+ candidateName: string;
106
+ peer: string;
107
+ reason: string;
108
+ } | {
109
+ kind: 'recovery_peer_bundle_received';
110
+ candidateName: string;
111
+ peer: string;
112
+ sourceNodeId: string;
113
+ } | {
114
+ kind: 'recovery_failed';
115
+ candidateName: string;
116
+ peers: string[];
117
+ reason: string;
118
+ } | {
119
+ kind: 'pull_request_received';
120
+ candidateName: string;
121
+ peer: string;
122
+ requesterNodeId: string;
123
+ } | {
124
+ kind: 'pull_response_sent';
125
+ candidateName: string;
126
+ peer: string;
127
+ result: AuthSyncPullResponseResult;
128
+ reason: string | null;
129
+ } | {
130
+ kind: 'sync_error';
131
+ reason: string;
132
+ };
36
133
  export interface AuthSyncImportCallbacks {
37
134
  readLocalCandidate: (candidateName: string) => Promise<AuthMirrorCandidateRecord | null>;
38
135
  listLocalCandidates: () => Promise<AuthMirrorCandidateRecord[]>;
@@ -42,6 +139,7 @@ export interface AuthSyncImportCallbacks {
42
139
  label?: string | null;
43
140
  }) => Promise<AuthMirrorImportResult>;
44
141
  isIdle: () => boolean;
142
+ notify?: (event: AuthSyncNotification) => Promise<void>;
45
143
  }
46
144
  export interface AuthSyncTransport {
47
145
  send: (peer: string, envelope: string) => Promise<void>;
@@ -51,6 +149,11 @@ export interface AuthSyncLeaseResult {
51
149
  leaseId: string | null;
52
150
  reason?: string | null;
53
151
  }
152
+ export interface AuthSyncTestResult {
153
+ sent: number;
154
+ replied: number;
155
+ missing: string[];
156
+ }
54
157
  export declare class CrossNodeAuthSync {
55
158
  private readonly config;
56
159
  private readonly logger;
@@ -63,10 +166,12 @@ export declare class CrossNodeAuthSync {
63
166
  private readonly pendingImports;
64
167
  private readonly pendingPulls;
65
168
  private readonly pendingLeases;
169
+ private readonly pendingTests;
66
170
  private seenNonces;
67
171
  private timer;
68
172
  private activeRemoteLease;
69
173
  private activeLocalLease;
174
+ private lastNotifiedError;
70
175
  private state;
71
176
  constructor(config: AuthSyncConfig, logger: Logger, transport: AuthSyncTransport, callbacks: AuthSyncImportCallbacks);
72
177
  initialize(): Promise<void>;
@@ -86,13 +191,12 @@ export declare class CrossNodeAuthSync {
86
191
  }): Promise<boolean>;
87
192
  acquireRefreshLease(reason: string): Promise<AuthSyncLeaseResult>;
88
193
  releaseRefreshLease(leaseId: string | null): Promise<void>;
89
- testPeers(): Promise<{
90
- sent: number;
91
- }>;
194
+ testPeers(): Promise<AuthSyncTestResult>;
92
195
  handleIncomingEnvelope(rawEnvelope: string, peer: AuthSyncPeerIdentity): Promise<boolean>;
93
196
  private handleMessage;
94
197
  private handlePullRequest;
95
198
  private handlePullResponse;
199
+ private markPullPeerUnavailable;
96
200
  private handleDigest;
97
201
  private handleLeaseRequest;
98
202
  private handleLeaseReply;
@@ -101,12 +205,18 @@ export declare class CrossNodeAuthSync {
101
205
  private validateAndImport;
102
206
  private sendToAll;
103
207
  private sendToPeer;
208
+ private handleTestPong;
209
+ private finishPendingTest;
104
210
  private sealEnvelope;
105
211
  private openEnvelope;
106
212
  private isReady;
107
213
  private isAllowedPeer;
214
+ private matchConfiguredPeer;
108
215
  private expireLeases;
109
216
  private recordError;
217
+ private rejectImport;
218
+ private notify;
219
+ private notifyError;
110
220
  private writeState;
111
221
  }
112
222
  export declare function readAccessTokenExpiresAtMs(raw: string): number | null;