@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +349 -0
  2. package/LICENSE +191 -0
  3. package/README.md +577 -0
  4. package/config.bot.example.json +15 -0
  5. package/config.example.json +33 -0
  6. package/dist/agent-bridge.d.ts +79 -0
  7. package/dist/agent-bridge.js +392 -0
  8. package/dist/agent-bridge.js.map +1 -0
  9. package/dist/commands.d.ts +57 -0
  10. package/dist/commands.js +121 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/config.d.ts +278 -0
  13. package/dist/config.js +330 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/cron-evaluator.d.ts +53 -0
  16. package/dist/cron-evaluator.js +191 -0
  17. package/dist/cron-evaluator.js.map +1 -0
  18. package/dist/cron-fire-marker.d.ts +24 -0
  19. package/dist/cron-fire-marker.js +25 -0
  20. package/dist/cron-fire-marker.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +46 -0
  22. package/dist/cron-scheduler.js +114 -0
  23. package/dist/cron-scheduler.js.map +1 -0
  24. package/dist/cron-store.d.ts +62 -0
  25. package/dist/cron-store.js +63 -0
  26. package/dist/cron-store.js.map +1 -0
  27. package/dist/cron-tool.d.ts +44 -0
  28. package/dist/cron-tool.js +151 -0
  29. package/dist/cron-tool.js.map +1 -0
  30. package/dist/cwd-resolver.d.ts +72 -0
  31. package/dist/cwd-resolver.js +166 -0
  32. package/dist/cwd-resolver.js.map +1 -0
  33. package/dist/db-adapter.d.ts +21 -0
  34. package/dist/db-adapter.js +64 -0
  35. package/dist/db-adapter.js.map +1 -0
  36. package/dist/file-inline-wrap.d.ts +94 -0
  37. package/dist/file-inline-wrap.js +243 -0
  38. package/dist/file-inline-wrap.js.map +1 -0
  39. package/dist/gateway.d.ts +100 -0
  40. package/dist/gateway.js +420 -0
  41. package/dist/gateway.js.map +1 -0
  42. package/dist/group-config.d.ts +41 -0
  43. package/dist/group-config.js +104 -0
  44. package/dist/group-config.js.map +1 -0
  45. package/dist/group-context.d.ts +64 -0
  46. package/dist/group-context.js +396 -0
  47. package/dist/group-context.js.map +1 -0
  48. package/dist/inbound.d.ts +136 -0
  49. package/dist/inbound.js +667 -0
  50. package/dist/inbound.js.map +1 -0
  51. package/dist/index.d.ts +33 -0
  52. package/dist/index.js +922 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/media-inbound.d.ts +38 -0
  55. package/dist/media-inbound.js +131 -0
  56. package/dist/media-inbound.js.map +1 -0
  57. package/dist/mention-utils.d.ts +99 -0
  58. package/dist/mention-utils.js +185 -0
  59. package/dist/mention-utils.js.map +1 -0
  60. package/dist/octo/api.d.ts +148 -0
  61. package/dist/octo/api.js +320 -0
  62. package/dist/octo/api.js.map +1 -0
  63. package/dist/octo/socket.d.ts +102 -0
  64. package/dist/octo/socket.js +793 -0
  65. package/dist/octo/socket.js.map +1 -0
  66. package/dist/octo/types.d.ts +126 -0
  67. package/dist/octo/types.js +35 -0
  68. package/dist/octo/types.js.map +1 -0
  69. package/dist/prompt-safety.d.ts +78 -0
  70. package/dist/prompt-safety.js +148 -0
  71. package/dist/prompt-safety.js.map +1 -0
  72. package/dist/session-router.d.ts +127 -0
  73. package/dist/session-router.js +432 -0
  74. package/dist/session-router.js.map +1 -0
  75. package/dist/session-store.d.ts +89 -0
  76. package/dist/session-store.js +297 -0
  77. package/dist/session-store.js.map +1 -0
  78. package/dist/skill-linker.d.ts +31 -0
  79. package/dist/skill-linker.js +160 -0
  80. package/dist/skill-linker.js.map +1 -0
  81. package/dist/stream-relay.d.ts +42 -0
  82. package/dist/stream-relay.js +243 -0
  83. package/dist/stream-relay.js.map +1 -0
  84. package/dist/url-policy.d.ts +103 -0
  85. package/dist/url-policy.js +290 -0
  86. package/dist/url-policy.js.map +1 -0
  87. 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
+ }
@@ -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;