@mininglamp-oss/cc-channel-octo 1.0.1
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 +349 -0
- package/LICENSE +191 -0
- package/README.md +577 -0
- package/config.bot.example.json +15 -0
- package/config.example.json +33 -0
- package/dist/agent-bridge.d.ts +79 -0
- package/dist/agent-bridge.js +392 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/commands.d.ts +57 -0
- package/dist/commands.js +121 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +278 -0
- package/dist/config.js +330 -0
- package/dist/config.js.map +1 -0
- package/dist/cron-evaluator.d.ts +53 -0
- package/dist/cron-evaluator.js +191 -0
- package/dist/cron-evaluator.js.map +1 -0
- package/dist/cron-fire-marker.d.ts +24 -0
- package/dist/cron-fire-marker.js +25 -0
- package/dist/cron-fire-marker.js.map +1 -0
- package/dist/cron-scheduler.d.ts +46 -0
- package/dist/cron-scheduler.js +114 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/cron-store.d.ts +62 -0
- package/dist/cron-store.js +63 -0
- package/dist/cron-store.js.map +1 -0
- package/dist/cron-tool.d.ts +44 -0
- package/dist/cron-tool.js +151 -0
- package/dist/cron-tool.js.map +1 -0
- package/dist/cwd-resolver.d.ts +72 -0
- package/dist/cwd-resolver.js +166 -0
- package/dist/cwd-resolver.js.map +1 -0
- package/dist/db-adapter.d.ts +21 -0
- package/dist/db-adapter.js +64 -0
- package/dist/db-adapter.js.map +1 -0
- package/dist/file-inline-wrap.d.ts +94 -0
- package/dist/file-inline-wrap.js +243 -0
- package/dist/file-inline-wrap.js.map +1 -0
- package/dist/gateway.d.ts +100 -0
- package/dist/gateway.js +420 -0
- package/dist/gateway.js.map +1 -0
- package/dist/group-config.d.ts +41 -0
- package/dist/group-config.js +104 -0
- package/dist/group-config.js.map +1 -0
- package/dist/group-context.d.ts +64 -0
- package/dist/group-context.js +396 -0
- package/dist/group-context.js.map +1 -0
- package/dist/inbound.d.ts +136 -0
- package/dist/inbound.js +667 -0
- package/dist/inbound.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +922 -0
- package/dist/index.js.map +1 -0
- package/dist/media-inbound.d.ts +38 -0
- package/dist/media-inbound.js +131 -0
- package/dist/media-inbound.js.map +1 -0
- package/dist/mention-utils.d.ts +99 -0
- package/dist/mention-utils.js +185 -0
- package/dist/mention-utils.js.map +1 -0
- package/dist/octo/api.d.ts +148 -0
- package/dist/octo/api.js +320 -0
- package/dist/octo/api.js.map +1 -0
- package/dist/octo/socket.d.ts +102 -0
- package/dist/octo/socket.js +793 -0
- package/dist/octo/socket.js.map +1 -0
- package/dist/octo/types.d.ts +126 -0
- package/dist/octo/types.js +35 -0
- package/dist/octo/types.js.map +1 -0
- package/dist/prompt-safety.d.ts +78 -0
- package/dist/prompt-safety.js +148 -0
- package/dist/prompt-safety.js.map +1 -0
- package/dist/session-router.d.ts +127 -0
- package/dist/session-router.js +432 -0
- package/dist/session-router.js.map +1 -0
- package/dist/session-store.d.ts +89 -0
- package/dist/session-store.js +297 -0
- package/dist/session-store.js.map +1 -0
- package/dist/skill-linker.d.ts +31 -0
- package/dist/skill-linker.js +160 -0
- package/dist/skill-linker.js.map +1 -0
- package/dist/stream-relay.d.ts +42 -0
- package/dist/stream-relay.js +243 -0
- package/dist/stream-relay.js.map +1 -0
- package/dist/url-policy.d.ts +103 -0
- package/dist/url-policy.js +290 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway — WS lifecycle management + bot registration + token refresh.
|
|
3
|
+
*/
|
|
4
|
+
import type { Config } from './config.js';
|
|
5
|
+
import type { BotMessage } from './octo/types.js';
|
|
6
|
+
export type MessageHandler = (msg: BotMessage) => void;
|
|
7
|
+
export declare class OctoGateway {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly options;
|
|
10
|
+
private socket;
|
|
11
|
+
private robotId;
|
|
12
|
+
private registration;
|
|
13
|
+
private _ownerUid;
|
|
14
|
+
private heartbeatTimer;
|
|
15
|
+
private lockFilePath;
|
|
16
|
+
/** Random per-process token written into the lock so ownership survives PID
|
|
17
|
+
* reuse: release only removes a lock that still carries OUR nonce. */
|
|
18
|
+
private readonly lockNonce;
|
|
19
|
+
private onMessage;
|
|
20
|
+
/** When true, new messages are silently dropped (shutdown draining). */
|
|
21
|
+
private _draining;
|
|
22
|
+
private isRefreshing;
|
|
23
|
+
private lastRefreshTime;
|
|
24
|
+
private readonly REFRESH_COOLDOWN_MS;
|
|
25
|
+
private heartbeatFailCount;
|
|
26
|
+
private readonly MAX_HEARTBEAT_FAILURES;
|
|
27
|
+
/** True while a heartbeat request is in flight — prevents overlapping ticks. */
|
|
28
|
+
private heartbeatInFlight;
|
|
29
|
+
/** Bumped on each startHeartbeat() so an orphaned tick from a prior run can't
|
|
30
|
+
* mutate the new counter (see the generation guard in the tick). */
|
|
31
|
+
private heartbeatGen;
|
|
32
|
+
constructor(config: Config, options?: {
|
|
33
|
+
handleSignals?: boolean;
|
|
34
|
+
});
|
|
35
|
+
get botId(): string;
|
|
36
|
+
/** G18: owner_uid returned by registerBot. Empty string until start() succeeds. */
|
|
37
|
+
get ownerUid(): string;
|
|
38
|
+
/** Set the message handler. Called for every incoming BotMessage. */
|
|
39
|
+
setMessageHandler(handler: MessageHandler): void;
|
|
40
|
+
/**
|
|
41
|
+
* Start the gateway: register → connect WS → heartbeat. Convenience wrapper
|
|
42
|
+
* that does registration and connection in one call (single-bot path + tests).
|
|
43
|
+
* Multi-bot startup calls register() and connect() separately so no socket
|
|
44
|
+
* begins ACKing messages before its message handler is installed.
|
|
45
|
+
*/
|
|
46
|
+
start(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Phase 1 of startup: acquire the lock and register the bot over REST. This
|
|
49
|
+
* populates botId/ownerUid but does NOT open the WebSocket, so no messages can
|
|
50
|
+
* arrive yet. Safe to call before the message handler is wired.
|
|
51
|
+
*/
|
|
52
|
+
register(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Phase 2 of startup: open the WebSocket and start the heartbeat. Call only
|
|
55
|
+
* AFTER setMessageHandler() so inbound messages are dispatched, not ACK'd and
|
|
56
|
+
* dropped. Registers signal handlers unless handleSignals is false.
|
|
57
|
+
*/
|
|
58
|
+
connect(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Start the REST-backed runtime services: the heartbeat / token-refresh loop
|
|
61
|
+
* and (unless handleSignals is false) the SIGINT/SIGTERM shutdown handlers.
|
|
62
|
+
* Called by connect() after the socket is opened. Multi-bot mode passes
|
|
63
|
+
* handleSignals=false so the orchestrator owns a single combined shutdown.
|
|
64
|
+
*/
|
|
65
|
+
startServices(): void;
|
|
66
|
+
/** Whether the gateway is draining (rejecting new messages). */
|
|
67
|
+
get draining(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Gracefully stop: set draining → wait for in-flight handlers →
|
|
70
|
+
* stop heartbeat → disconnect WS → release lock.
|
|
71
|
+
*
|
|
72
|
+
* @param activeHandlers - Set of in-flight handler promises to drain.
|
|
73
|
+
* Supplied by the orchestrator (index.ts) that tracks them.
|
|
74
|
+
* @param drainTimeoutMs - Max time (ms) to wait for in-flight handlers
|
|
75
|
+
* before force-proceeding. Default 10000.
|
|
76
|
+
*/
|
|
77
|
+
stop(activeHandlers?: Set<Promise<void>>, drainTimeoutMs?: number): Promise<void>;
|
|
78
|
+
private acquireLock;
|
|
79
|
+
/** Read the PID field of the existing lock, or null if unreadable. */
|
|
80
|
+
private readLockPid;
|
|
81
|
+
/**
|
|
82
|
+
* If the existing lock's holder is provably gone, remove it and return true so
|
|
83
|
+
* the caller can retry the atomic create. Returns false if the holder is alive
|
|
84
|
+
* (or the lock vanished — caller's retry will race fairly).
|
|
85
|
+
*/
|
|
86
|
+
private reclaimIfStale;
|
|
87
|
+
private releaseLock;
|
|
88
|
+
private createSocket;
|
|
89
|
+
private handleMessage;
|
|
90
|
+
private attemptTokenRefresh;
|
|
91
|
+
private startHeartbeat;
|
|
92
|
+
private stopHeartbeat;
|
|
93
|
+
private onShutdown;
|
|
94
|
+
/**
|
|
95
|
+
* Set a shutdown callback. Called on SIGINT/SIGTERM before process.exit.
|
|
96
|
+
* The orchestrator (index.ts) wires this to drain handlers + close store.
|
|
97
|
+
*/
|
|
98
|
+
setShutdownCallback(fn: () => Promise<void>): void;
|
|
99
|
+
private setupShutdownHandlers;
|
|
100
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway — WS lifecycle management + bot registration + token refresh.
|
|
3
|
+
*/
|
|
4
|
+
import { WKSocket } from './octo/socket.js';
|
|
5
|
+
import { registerBot, sendHeartbeat } from './octo/api.js';
|
|
6
|
+
import { isAllowedWsUrl } from './url-policy.js';
|
|
7
|
+
import { existsSync, writeFileSync, unlinkSync, readFileSync, mkdirSync } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
// Read version from package.json at load time (Q31: no hardcoded version).
|
|
12
|
+
const _require = createRequire(import.meta.url);
|
|
13
|
+
const PKG_VERSION = _require('../package.json').version;
|
|
14
|
+
export class OctoGateway {
|
|
15
|
+
config;
|
|
16
|
+
options;
|
|
17
|
+
socket = null;
|
|
18
|
+
robotId = '';
|
|
19
|
+
// Stored registration result from register(); consumed by connect().
|
|
20
|
+
registration = null;
|
|
21
|
+
// G18: owner_uid from registerBot; used by SessionRouter for future permission model.
|
|
22
|
+
_ownerUid = '';
|
|
23
|
+
heartbeatTimer = null;
|
|
24
|
+
lockFilePath;
|
|
25
|
+
/** Random per-process token written into the lock so ownership survives PID
|
|
26
|
+
* reuse: release only removes a lock that still carries OUR nonce. */
|
|
27
|
+
lockNonce = randomUUID();
|
|
28
|
+
onMessage = null;
|
|
29
|
+
/** When true, new messages are silently dropped (shutdown draining). */
|
|
30
|
+
_draining = false;
|
|
31
|
+
// Token refresh state
|
|
32
|
+
isRefreshing = false;
|
|
33
|
+
lastRefreshTime = 0;
|
|
34
|
+
REFRESH_COOLDOWN_MS = 60_000;
|
|
35
|
+
// Heartbeat failure tracking
|
|
36
|
+
heartbeatFailCount = 0;
|
|
37
|
+
MAX_HEARTBEAT_FAILURES = 3;
|
|
38
|
+
/** True while a heartbeat request is in flight — prevents overlapping ticks. */
|
|
39
|
+
heartbeatInFlight = false;
|
|
40
|
+
/** Bumped on each startHeartbeat() so an orphaned tick from a prior run can't
|
|
41
|
+
* mutate the new counter (see the generation guard in the tick). */
|
|
42
|
+
heartbeatGen = 0;
|
|
43
|
+
constructor(config, options = {}) {
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.options = options;
|
|
46
|
+
this.lockFilePath = join(config.dataDir, 'gateway.lock');
|
|
47
|
+
}
|
|
48
|
+
get botId() {
|
|
49
|
+
return this.robotId;
|
|
50
|
+
}
|
|
51
|
+
/** G18: owner_uid returned by registerBot. Empty string until start() succeeds. */
|
|
52
|
+
get ownerUid() {
|
|
53
|
+
return this._ownerUid;
|
|
54
|
+
}
|
|
55
|
+
/** Set the message handler. Called for every incoming BotMessage. */
|
|
56
|
+
setMessageHandler(handler) {
|
|
57
|
+
this.onMessage = handler;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start the gateway: register → connect WS → heartbeat. Convenience wrapper
|
|
61
|
+
* that does registration and connection in one call (single-bot path + tests).
|
|
62
|
+
* Multi-bot startup calls register() and connect() separately so no socket
|
|
63
|
+
* begins ACKing messages before its message handler is installed.
|
|
64
|
+
*/
|
|
65
|
+
async start() {
|
|
66
|
+
await this.register();
|
|
67
|
+
this.connect();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Phase 1 of startup: acquire the lock and register the bot over REST. This
|
|
71
|
+
* populates botId/ownerUid but does NOT open the WebSocket, so no messages can
|
|
72
|
+
* arrive yet. Safe to call before the message handler is wired.
|
|
73
|
+
*/
|
|
74
|
+
async register() {
|
|
75
|
+
this.acquireLock();
|
|
76
|
+
try {
|
|
77
|
+
const reg = await registerBot({
|
|
78
|
+
apiUrl: this.config.apiUrl,
|
|
79
|
+
botToken: this.config.botToken,
|
|
80
|
+
agentPlatform: 'cc-channel-octo',
|
|
81
|
+
agentVersion: PKG_VERSION,
|
|
82
|
+
});
|
|
83
|
+
this.robotId = reg.robot_id;
|
|
84
|
+
this._ownerUid = reg.owner_uid;
|
|
85
|
+
this.registration = reg;
|
|
86
|
+
console.log(`Bot registered: robot_id=${reg.robot_id}`);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// registerBot failed (bad token, network) AFTER we took the lock. Release
|
|
90
|
+
// it so a partial-startup failure doesn't leave a stale lock with this
|
|
91
|
+
// live PID — otherwise the next start refuses with "Another instance is
|
|
92
|
+
// running". The multi-bot startup cleanup only tears down bots that
|
|
93
|
+
// returned a BotStack, so this failed bot must clean up its own lock here.
|
|
94
|
+
this.releaseLock();
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Phase 2 of startup: open the WebSocket and start the heartbeat. Call only
|
|
100
|
+
* AFTER setMessageHandler() so inbound messages are dispatched, not ACK'd and
|
|
101
|
+
* dropped. Registers signal handlers unless handleSignals is false.
|
|
102
|
+
*/
|
|
103
|
+
connect() {
|
|
104
|
+
if (!this.registration) {
|
|
105
|
+
throw new Error('OctoGateway.connect() called before register()');
|
|
106
|
+
}
|
|
107
|
+
const reg = this.registration;
|
|
108
|
+
// SECURITY: the AES-CBC payload layer is unauthenticated, so transport
|
|
109
|
+
// integrity is the only tamper guarantee. Refuse a plaintext ws:// endpoint
|
|
110
|
+
// for any non-loopback host (a MITM could bit-flip ciphertext undetected).
|
|
111
|
+
// wss:// is required in production; ws://localhost is allowed for local dev.
|
|
112
|
+
if (!isAllowedWsUrl(reg.ws_url)) {
|
|
113
|
+
throw new Error(`OctoGateway.connect(): refusing insecure WebSocket URL "${reg.ws_url}" — ` +
|
|
114
|
+
`wss:// is required (ws:// permitted only for localhost). The message layer ` +
|
|
115
|
+
`is unauthenticated, so an unencrypted transport allows undetected tampering.`);
|
|
116
|
+
}
|
|
117
|
+
this.socket = this.createSocket(reg.ws_url, reg.robot_id, reg.im_token);
|
|
118
|
+
this.socket.connect();
|
|
119
|
+
this.startServices();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start the REST-backed runtime services: the heartbeat / token-refresh loop
|
|
123
|
+
* and (unless handleSignals is false) the SIGINT/SIGTERM shutdown handlers.
|
|
124
|
+
* Called by connect() after the socket is opened. Multi-bot mode passes
|
|
125
|
+
* handleSignals=false so the orchestrator owns a single combined shutdown.
|
|
126
|
+
*/
|
|
127
|
+
startServices() {
|
|
128
|
+
if (!this.registration) {
|
|
129
|
+
throw new Error('OctoGateway.startServices() called before register()');
|
|
130
|
+
}
|
|
131
|
+
this.startHeartbeat();
|
|
132
|
+
// Multi-bot: the orchestrator owns a single combined SIGINT/SIGTERM handler,
|
|
133
|
+
// so individual gateways skip registering their own (default true keeps the
|
|
134
|
+
// single-bot behavior unchanged).
|
|
135
|
+
if (this.options.handleSignals !== false) {
|
|
136
|
+
this.setupShutdownHandlers();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Whether the gateway is draining (rejecting new messages). */
|
|
140
|
+
get draining() {
|
|
141
|
+
return this._draining;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Gracefully stop: set draining → wait for in-flight handlers →
|
|
145
|
+
* stop heartbeat → disconnect WS → release lock.
|
|
146
|
+
*
|
|
147
|
+
* @param activeHandlers - Set of in-flight handler promises to drain.
|
|
148
|
+
* Supplied by the orchestrator (index.ts) that tracks them.
|
|
149
|
+
* @param drainTimeoutMs - Max time (ms) to wait for in-flight handlers
|
|
150
|
+
* before force-proceeding. Default 10000.
|
|
151
|
+
*/
|
|
152
|
+
async stop(activeHandlers, drainTimeoutMs = 10_000) {
|
|
153
|
+
// Mark draining — new messages will be dropped by handleMessage
|
|
154
|
+
this._draining = true;
|
|
155
|
+
// Wait for in-flight message handlers to complete (with timeout)
|
|
156
|
+
if (activeHandlers && activeHandlers.size > 0) {
|
|
157
|
+
console.log(`[cc-channel-octo] Draining ${activeHandlers.size} in-flight handler(s)...`);
|
|
158
|
+
const drainPromise = Promise.allSettled([...activeHandlers]);
|
|
159
|
+
const timeout = new Promise((r) => setTimeout(r, drainTimeoutMs));
|
|
160
|
+
await Promise.race([drainPromise, timeout]);
|
|
161
|
+
if (activeHandlers.size > 0) {
|
|
162
|
+
console.warn(`[cc-channel-octo] Drain timeout, ${activeHandlers.size} handler(s) still active`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.stopHeartbeat();
|
|
166
|
+
if (this.socket) {
|
|
167
|
+
await this.socket.disconnectAndWait();
|
|
168
|
+
this.socket = null;
|
|
169
|
+
}
|
|
170
|
+
this.releaseLock();
|
|
171
|
+
}
|
|
172
|
+
// --- Lock file ---
|
|
173
|
+
acquireLock() {
|
|
174
|
+
const dir = dirname(this.lockFilePath);
|
|
175
|
+
mkdirSync(dir, { recursive: true });
|
|
176
|
+
const content = `${process.pid} ${this.lockNonce}`;
|
|
177
|
+
// Try at most twice: first attempt, then once more after reclaiming a stale
|
|
178
|
+
// lock. The create is ATOMIC (flag 'wx' = O_EXCL) so two processes racing to
|
|
179
|
+
// acquire can't both succeed — the loser gets EEXIST and is handled as "held"
|
|
180
|
+
// (or reclaims only if the holder is provably dead).
|
|
181
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
182
|
+
try {
|
|
183
|
+
writeFileSync(this.lockFilePath, content, { mode: 0o600, flag: 'wx' });
|
|
184
|
+
return; // won the lock atomically
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const e = err;
|
|
188
|
+
if (e.code !== 'EEXIST')
|
|
189
|
+
throw err; // unexpected fs error
|
|
190
|
+
// Lock exists — decide whether the holder is alive.
|
|
191
|
+
if (attempt > 0 || !this.reclaimIfStale()) {
|
|
192
|
+
// Either we already reclaimed once and lost the re-create race (a
|
|
193
|
+
// concurrent process won — it IS running), or the holder is alive.
|
|
194
|
+
const held = this.readLockPid();
|
|
195
|
+
throw new Error(`Another instance is running (PID ${held ?? 'unknown'}). Lock file: ${this.lockFilePath}`);
|
|
196
|
+
}
|
|
197
|
+
// reclaimIfStale() removed a dead lock — loop once to re-create.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Read the PID field of the existing lock, or null if unreadable. */
|
|
202
|
+
readLockPid() {
|
|
203
|
+
try {
|
|
204
|
+
const pid = parseInt(readFileSync(this.lockFilePath, 'utf-8').trim().split(/\s+/)[0], 10);
|
|
205
|
+
return Number.isInteger(pid) ? pid : null;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* If the existing lock's holder is provably gone, remove it and return true so
|
|
213
|
+
* the caller can retry the atomic create. Returns false if the holder is alive
|
|
214
|
+
* (or the lock vanished — caller's retry will race fairly).
|
|
215
|
+
*/
|
|
216
|
+
reclaimIfStale() {
|
|
217
|
+
const pid = this.readLockPid();
|
|
218
|
+
if (pid === null) {
|
|
219
|
+
// Corrupt/empty/non-numeric lock (e.g. a partial write) — reclaim it.
|
|
220
|
+
try {
|
|
221
|
+
unlinkSync(this.lockFilePath);
|
|
222
|
+
}
|
|
223
|
+
catch { /* vanished — fine */ }
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0); // signal 0 = liveness check
|
|
228
|
+
return false; // holder is alive and signalable → genuinely held
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const e = err;
|
|
232
|
+
if (e.code === 'EPERM') {
|
|
233
|
+
// PID exists but owned by another user — can't be our bot (one service
|
|
234
|
+
// user per dataDir), so almost certainly a reused PID. Reclaim.
|
|
235
|
+
console.warn(`Lock PID ${pid} exists but is not signalable (EPERM) — likely a reused ` +
|
|
236
|
+
`PID; reclaiming the stale lock at ${this.lockFilePath}`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(`Removing stale lock file (PID ${pid} not found)`);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
unlinkSync(this.lockFilePath);
|
|
243
|
+
}
|
|
244
|
+
catch { /* vanished — fine */ }
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
releaseLock() {
|
|
249
|
+
try {
|
|
250
|
+
if (existsSync(this.lockFilePath)) {
|
|
251
|
+
const content = readFileSync(this.lockFilePath, 'utf-8').trim();
|
|
252
|
+
// Only remove the lock if it still carries OUR nonce — so a lock another
|
|
253
|
+
// instance acquired after a PID-reuse race is never deleted by us.
|
|
254
|
+
const [, nonce] = content.split(/\s+/);
|
|
255
|
+
if (nonce === this.lockNonce) {
|
|
256
|
+
unlinkSync(this.lockFilePath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* best effort */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// --- Socket factory ---
|
|
265
|
+
createSocket(wsUrl, uid, token) {
|
|
266
|
+
return new WKSocket({
|
|
267
|
+
wsUrl,
|
|
268
|
+
uid,
|
|
269
|
+
token,
|
|
270
|
+
onMessage: (msg) => this.handleMessage(msg),
|
|
271
|
+
onConnected: () => {
|
|
272
|
+
console.log('WS connected');
|
|
273
|
+
this.heartbeatFailCount = 0;
|
|
274
|
+
},
|
|
275
|
+
onDisconnected: () => {
|
|
276
|
+
console.log('WS disconnected');
|
|
277
|
+
},
|
|
278
|
+
onError: (err) => {
|
|
279
|
+
console.error('WS error:', err.message);
|
|
280
|
+
if (err.message.includes('Kicked') || err.message.includes('Connect failed')) {
|
|
281
|
+
void this.attemptTokenRefresh();
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// --- Bot registration + WS connection ---
|
|
287
|
+
// (register() + connect() above split the two phases; see start().)
|
|
288
|
+
handleMessage(msg) {
|
|
289
|
+
if (this._draining)
|
|
290
|
+
return; // Q6: reject new messages during shutdown
|
|
291
|
+
if (msg.from_uid === this.robotId)
|
|
292
|
+
return;
|
|
293
|
+
this.onMessage?.(msg);
|
|
294
|
+
}
|
|
295
|
+
// --- Token refresh ---
|
|
296
|
+
async attemptTokenRefresh() {
|
|
297
|
+
if (this.isRefreshing)
|
|
298
|
+
return;
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
if (now - this.lastRefreshTime < this.REFRESH_COOLDOWN_MS) {
|
|
301
|
+
const remaining = Math.ceil((this.REFRESH_COOLDOWN_MS - (now - this.lastRefreshTime)) / 1000);
|
|
302
|
+
console.log(`Token refresh cooldown (${remaining}s remaining)`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.isRefreshing = true;
|
|
306
|
+
this.lastRefreshTime = now;
|
|
307
|
+
try {
|
|
308
|
+
console.log('Attempting token refresh...');
|
|
309
|
+
// Q33: Stop heartbeat during refresh to avoid empty pings
|
|
310
|
+
this.stopHeartbeat();
|
|
311
|
+
if (this.socket) {
|
|
312
|
+
await this.socket.disconnectAndWait();
|
|
313
|
+
this.socket = null;
|
|
314
|
+
}
|
|
315
|
+
const reg = await registerBot({
|
|
316
|
+
apiUrl: this.config.apiUrl,
|
|
317
|
+
botToken: this.config.botToken,
|
|
318
|
+
forceRefresh: true,
|
|
319
|
+
agentPlatform: 'cc-channel-octo',
|
|
320
|
+
agentVersion: PKG_VERSION,
|
|
321
|
+
});
|
|
322
|
+
this.robotId = reg.robot_id;
|
|
323
|
+
this._ownerUid = reg.owner_uid;
|
|
324
|
+
this.registration = reg;
|
|
325
|
+
console.log('Token refreshed, reconnecting...');
|
|
326
|
+
this.socket = this.createSocket(reg.ws_url, reg.robot_id, reg.im_token);
|
|
327
|
+
this.socket.connect();
|
|
328
|
+
this.startHeartbeat(); // Q33: Restart API heartbeat after successful refresh
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
console.error('Token refresh failed:', String(err));
|
|
332
|
+
this.startHeartbeat(); // Q33: Restore heartbeat on failure for self-healing
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
this.isRefreshing = false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// --- Heartbeat (API-level, 30s interval) ---
|
|
339
|
+
startHeartbeat() {
|
|
340
|
+
this.stopHeartbeat();
|
|
341
|
+
this.heartbeatFailCount = 0;
|
|
342
|
+
const gen = ++this.heartbeatGen;
|
|
343
|
+
this.heartbeatTimer = setInterval(() => {
|
|
344
|
+
// Overlap guard: if the previous heartbeat hasn't settled (degraded API
|
|
345
|
+
// where a request takes ~>= the 30s interval), skip this tick instead of
|
|
346
|
+
// piling up concurrent requests and racing heartbeatFailCount.
|
|
347
|
+
if (this.heartbeatInFlight)
|
|
348
|
+
return;
|
|
349
|
+
this.heartbeatInFlight = true;
|
|
350
|
+
void (async () => {
|
|
351
|
+
try {
|
|
352
|
+
await sendHeartbeat({
|
|
353
|
+
apiUrl: this.config.apiUrl,
|
|
354
|
+
botToken: this.config.botToken,
|
|
355
|
+
});
|
|
356
|
+
// Generation guard: a tick from a superseded startHeartbeat() (e.g.
|
|
357
|
+
// after a token refresh re-armed the timer) must not touch the live
|
|
358
|
+
// counter or it could spuriously reset/trip the new one.
|
|
359
|
+
if (gen !== this.heartbeatGen)
|
|
360
|
+
return;
|
|
361
|
+
this.heartbeatFailCount = 0;
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
if (gen !== this.heartbeatGen)
|
|
365
|
+
return;
|
|
366
|
+
this.heartbeatFailCount++;
|
|
367
|
+
console.error(`Heartbeat failed (${this.heartbeatFailCount}/${this.MAX_HEARTBEAT_FAILURES}):`, String(err));
|
|
368
|
+
if (this.heartbeatFailCount >= this.MAX_HEARTBEAT_FAILURES) {
|
|
369
|
+
console.error('Max heartbeat failures reached, triggering reconnect...');
|
|
370
|
+
this.heartbeatFailCount = 0;
|
|
371
|
+
void this.attemptTokenRefresh();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
// Guard the flag reset by generation too: an orphaned tick from a
|
|
376
|
+
// superseded startHeartbeat() must NOT clear the live generation's
|
|
377
|
+
// in-flight flag, or the next live tick could start a 2nd concurrent
|
|
378
|
+
// request — exactly the overlap this guard prevents. stopHeartbeat()
|
|
379
|
+
// resets the flag when it abandons a generation.
|
|
380
|
+
if (gen === this.heartbeatGen)
|
|
381
|
+
this.heartbeatInFlight = false;
|
|
382
|
+
}
|
|
383
|
+
})();
|
|
384
|
+
}, 30_000);
|
|
385
|
+
}
|
|
386
|
+
stopHeartbeat() {
|
|
387
|
+
if (this.heartbeatTimer) {
|
|
388
|
+
clearInterval(this.heartbeatTimer);
|
|
389
|
+
this.heartbeatTimer = null;
|
|
390
|
+
}
|
|
391
|
+
// Invalidate any in-flight tick so its result is ignored, and allow the next
|
|
392
|
+
// startHeartbeat to issue immediately.
|
|
393
|
+
this.heartbeatGen++;
|
|
394
|
+
this.heartbeatInFlight = false;
|
|
395
|
+
}
|
|
396
|
+
// --- Graceful shutdown ---
|
|
397
|
+
onShutdown = null;
|
|
398
|
+
/**
|
|
399
|
+
* Set a shutdown callback. Called on SIGINT/SIGTERM before process.exit.
|
|
400
|
+
* The orchestrator (index.ts) wires this to drain handlers + close store.
|
|
401
|
+
*/
|
|
402
|
+
setShutdownCallback(fn) {
|
|
403
|
+
this.onShutdown = fn;
|
|
404
|
+
}
|
|
405
|
+
setupShutdownHandlers() {
|
|
406
|
+
const shutdown = async (signal) => {
|
|
407
|
+
console.log(`Received ${signal}, shutting down...`);
|
|
408
|
+
if (this.onShutdown) {
|
|
409
|
+
await this.onShutdown();
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
await this.stop();
|
|
413
|
+
}
|
|
414
|
+
process.exit(0);
|
|
415
|
+
};
|
|
416
|
+
process.once('SIGINT', () => void shutdown('SIGINT'));
|
|
417
|
+
process.once('SIGTERM', () => void shutdown('SIGTERM'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1.0: GROUP.md / THREAD.md per-conversation instruction files.
|
|
3
|
+
*
|
|
4
|
+
* Operators can drop a markdown file with custom instructions for a specific
|
|
5
|
+
* group (or, later, thread) into a configured directory. Its contents are
|
|
6
|
+
* injected into the agent's system prompt as a trusted instruction block, so a
|
|
7
|
+
* group can have its own persona / rules without code changes.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY — read carefully. The `[Group instructions]` block is injected into
|
|
10
|
+
* the system prompt UNSANITIZED, so its contents are trusted. That trust holds
|
|
11
|
+
* ONLY if the file is writable solely by the operator (the gateway process user).
|
|
12
|
+
*
|
|
13
|
+
* Placing `groupConfigDir` outside `cwdBase` is necessary but NOT sufficient:
|
|
14
|
+
* under the shipped defaults (`allowedTools: '*'`, `bypassPermissions`) the agent
|
|
15
|
+
* has `Bash`/`Write` and can write ABSOLUTE paths anywhere the gateway user can
|
|
16
|
+
* write — `cwdBase` is a starting dir, not a chroot. So a malicious user in one
|
|
17
|
+
* group could drive the agent to write `<groupConfigDir>/<otherGroup>.md` and
|
|
18
|
+
* inject persistent, trusted instructions into a different group.
|
|
19
|
+
*
|
|
20
|
+
* The real protection is OS-level: `groupConfigDir` and its files MUST be made
|
|
21
|
+
* non-writable by the gateway process user (e.g. root-owned, mode 0755/0644),
|
|
22
|
+
* and/or the deployment hardened (drop `Bash`, sandboxed FS, unprivileged user).
|
|
23
|
+
* As cheap defense-in-depth, loadGroupConfig() refuses to inject a file that is
|
|
24
|
+
* group- or world-writable. The group id is filename-pinned to a safe slug so a
|
|
25
|
+
* crafted id can't traverse out of the config dir.
|
|
26
|
+
*/
|
|
27
|
+
/** Max bytes of an instruction file we will inject (keeps the prompt bounded). */
|
|
28
|
+
export declare const MAX_GROUP_CONFIG_BYTES = 16384;
|
|
29
|
+
/**
|
|
30
|
+
* Load the instruction file for a group, or undefined when none applies.
|
|
31
|
+
*
|
|
32
|
+
* Looks for `<groupConfigDir>/<groupId>.md`. Returns the trimmed contents,
|
|
33
|
+
* truncated to MAX_GROUP_CONFIG_BYTES. Returns undefined when:
|
|
34
|
+
* - groupConfigDir is not configured,
|
|
35
|
+
* - groupId is empty or unsafe as a path segment,
|
|
36
|
+
* - the file does not exist or is unreadable.
|
|
37
|
+
*
|
|
38
|
+
* Never throws — a misconfigured dir or unreadable file degrades to "no custom
|
|
39
|
+
* instructions" rather than failing the turn.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadGroupConfig(groupConfigDir: string | undefined, groupId: string): string | undefined;
|