@mininglamp-oss/cc-channel-octo 1.0.1-dev.60b73f3
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 +287 -0
- package/dist/config.js +332 -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 +81 -0
- package/dist/group-context.js +466 -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 +932 -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 +108 -0
- package/dist/mention-utils.js +199 -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 +144 -0
- package/dist/session-router.js +490 -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
package/dist/config.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loading.
|
|
3
|
+
*
|
|
4
|
+
* Two-layer, bot-first model:
|
|
5
|
+
* - GLOBAL `~/.cc-channel-octo/config.json` — shared defaults + a `bots` list.
|
|
6
|
+
* Never holds a botToken.
|
|
7
|
+
* - PER-BOT `~/.cc-channel-octo/<id>/config.json` — that bot's botToken + any
|
|
8
|
+
* overrides. Each bot is a self-contained subtree:
|
|
9
|
+
* <baseDir>/<id>/{config.json, SOUL.md, data/, workspace/, memory/}
|
|
10
|
+
* - `baseDir` is the directory containing the global config.json. Per-bot dirs
|
|
11
|
+
* are DERIVED from `<baseDir>/<id>/…` (not separately configurable) so a bot
|
|
12
|
+
* can never point its data outside its own subtree.
|
|
13
|
+
*
|
|
14
|
+
* env overrides still apply to the shared/global layer.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, existsSync, statSync, realpathSync } from 'node:fs';
|
|
17
|
+
import { resolve as resolvePath, sep, dirname, join as pathJoin } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { isAllowedApiUrl } from './url-policy.js';
|
|
20
|
+
/**
|
|
21
|
+
* Default global config path: `~/.cc-channel-octo/config.json`. This is the
|
|
22
|
+
* single, fixed production location (no env/CLI override). Tests pass an
|
|
23
|
+
* explicit path, which also sets `baseDir` to that file's directory.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_CONFIG_PATH = pathJoin(homedir(), '.cc-channel-octo', 'config.json');
|
|
26
|
+
function defaults() {
|
|
27
|
+
return {
|
|
28
|
+
botToken: '',
|
|
29
|
+
apiUrl: '',
|
|
30
|
+
// baseDir is set by loadConfig() from the config path's directory; the
|
|
31
|
+
// per-bot dirs below are DERIVED in resolveBotConfigs() as
|
|
32
|
+
// <baseDir>/<botId>/{workspace,data,memory}. Left empty here.
|
|
33
|
+
baseDir: '',
|
|
34
|
+
cwdBase: '',
|
|
35
|
+
cwd: '',
|
|
36
|
+
dataDir: '',
|
|
37
|
+
memoryBase: '',
|
|
38
|
+
sdk: {
|
|
39
|
+
// Q2: default to wildcard — operators tighten only when they need to.
|
|
40
|
+
allowedTools: '*',
|
|
41
|
+
permissionMode: 'bypassPermissions',
|
|
42
|
+
// #100: load project-scope settings so the SDK discovers skills symlinked
|
|
43
|
+
// into the session sandbox's .claude/skills/. Memory stays isolated via the
|
|
44
|
+
// inline settings.autoMemoryDirectory pin (flagSettings > projectSettings).
|
|
45
|
+
settingSources: ['project'],
|
|
46
|
+
},
|
|
47
|
+
rateLimit: {
|
|
48
|
+
maxPerMinute: 5,
|
|
49
|
+
},
|
|
50
|
+
context: {
|
|
51
|
+
maxContextChars: 6000,
|
|
52
|
+
historyLimit: 40,
|
|
53
|
+
},
|
|
54
|
+
maxResponseChars: 524_288, // 512 KB (Q32)
|
|
55
|
+
dispatchTimeoutMs: 300_000, // 5 min (#141)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function readConfigFile(configFilePath) {
|
|
59
|
+
if (!existsSync(configFilePath)) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
// Q12: Warn if config file is readable by group/others (contains botToken).
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(configFilePath);
|
|
65
|
+
const mode = stat.mode & 0o777;
|
|
66
|
+
if (mode & 0o077) {
|
|
67
|
+
console.warn(`[cc-channel-octo] WARNING: ${configFilePath} has mode ${mode.toString(8)} — ` +
|
|
68
|
+
`secrets may be exposed to other users. Fix with: chmod 600 ${configFilePath}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Best-effort check — don't block startup if stat fails.
|
|
73
|
+
}
|
|
74
|
+
const raw = readFileSync(configFilePath, 'utf-8');
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(raw);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
throw new Error(`Failed to parse config file ${configFilePath}: ${msg}`);
|
|
82
|
+
}
|
|
83
|
+
// Strip top-level keys starting with "_" (e.g. _comment).
|
|
84
|
+
const cleaned = {};
|
|
85
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
86
|
+
if (!k.startsWith('_'))
|
|
87
|
+
cleaned[k] = v;
|
|
88
|
+
}
|
|
89
|
+
return cleaned;
|
|
90
|
+
}
|
|
91
|
+
function mergeConfig(base, override) {
|
|
92
|
+
return {
|
|
93
|
+
botToken: override.botToken ?? base.botToken,
|
|
94
|
+
apiUrl: override.apiUrl ?? base.apiUrl,
|
|
95
|
+
// baseDir + derived dirs are filled by loadConfig()/resolveBotConfigs(),
|
|
96
|
+
// not by config-file merge.
|
|
97
|
+
baseDir: base.baseDir,
|
|
98
|
+
cwdBase: base.cwdBase,
|
|
99
|
+
cwd: base.cwd,
|
|
100
|
+
dataDir: base.dataDir,
|
|
101
|
+
memoryBase: base.memoryBase,
|
|
102
|
+
groupConfigDir: override.groupConfigDir ?? base.groupConfigDir,
|
|
103
|
+
sdk: {
|
|
104
|
+
...base.sdk,
|
|
105
|
+
...(override.sdk ?? {}),
|
|
106
|
+
},
|
|
107
|
+
rateLimit: {
|
|
108
|
+
...base.rateLimit,
|
|
109
|
+
...(override.rateLimit ?? {}),
|
|
110
|
+
},
|
|
111
|
+
context: {
|
|
112
|
+
...base.context,
|
|
113
|
+
...(override.context ?? {}),
|
|
114
|
+
},
|
|
115
|
+
maxResponseChars: override.maxResponseChars ?? base.maxResponseChars,
|
|
116
|
+
dispatchTimeoutMs: override.dispatchTimeoutMs ?? base.dispatchTimeoutMs,
|
|
117
|
+
botBlocklist: override.botBlocklist ?? base.botBlocklist,
|
|
118
|
+
allowedBotUids: override.allowedBotUids ?? base.allowedBotUids,
|
|
119
|
+
mentionFreeGroups: override.mentionFreeGroups ?? base.mentionFreeGroups,
|
|
120
|
+
bots: override.bots ?? base.bots,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* SSRF protection for apiUrl: implemented in url-policy.ts (isAllowedApiUrl).
|
|
125
|
+
* S6 fix: now rejects https://127.0.0.1 too — https doesn't make a private
|
|
126
|
+
* address safe (could be a self-signed mitmproxy).
|
|
127
|
+
*/
|
|
128
|
+
export function loadConfig(configPath) {
|
|
129
|
+
const path = configPath ?? DEFAULT_CONFIG_PATH;
|
|
130
|
+
// Migration aid: if the fixed global config is missing but a legacy
|
|
131
|
+
// ./config.json exists in the cwd, point the operator at the move rather than
|
|
132
|
+
// failing later with a cryptic "Missing required config: apiUrl".
|
|
133
|
+
if (configPath === undefined && !existsSync(path) && existsSync('./config.json')) {
|
|
134
|
+
throw new Error(`No config at ${path}, but ./config.json exists. The config location moved: ` +
|
|
135
|
+
`cc-channel-octo now loads ~/.cc-channel-octo/config.json (shared, no token) plus ` +
|
|
136
|
+
`~/.cc-channel-octo/<botId>/config.json (per-bot token). Move your settings there ` +
|
|
137
|
+
`(see config.example.json / config.bot.example.json).`);
|
|
138
|
+
}
|
|
139
|
+
const fileCfg = readConfigFile(path);
|
|
140
|
+
// Config comes ONLY from config.json (global + per-bot layers) — there is no
|
|
141
|
+
// environment-variable override path (#103). The sole exception that still
|
|
142
|
+
// *reads* the environment lives nowhere here: `sdk.anthropicBaseUrl` is set in
|
|
143
|
+
// config.json and forwarded to the SDK subprocess by agent-bridge.
|
|
144
|
+
const final = mergeConfig(defaults(), fileCfg);
|
|
145
|
+
// baseDir = the directory containing the global config.json. Every bot's
|
|
146
|
+
// subtree lives at <baseDir>/<botId>/…. resolveBotConfigs() derives the
|
|
147
|
+
// per-bot dirs from this.
|
|
148
|
+
final.baseDir = dirname(resolvePath(path));
|
|
149
|
+
// apiUrl is shared and required at the global layer (a per-bot config.json may
|
|
150
|
+
// still override it, re-checked per bot in resolveBotConfigs). botToken is NOT
|
|
151
|
+
// validated here — it lives in each bot's <id>/config.json.
|
|
152
|
+
if (!final.apiUrl) {
|
|
153
|
+
throw new Error('Missing required config: apiUrl (set CC_OCTO_API_URL or config.json)');
|
|
154
|
+
}
|
|
155
|
+
if (!isAllowedApiUrl(final.apiUrl)) {
|
|
156
|
+
throw new Error(`Unsafe apiUrl: ${final.apiUrl} — must be https:// or http://localhost/http://127.0.0.1 (SSRF protection)`);
|
|
157
|
+
}
|
|
158
|
+
// Q1: the gateway endpoint receives the Anthropic API key and all prompt /
|
|
159
|
+
// response content, so it gets the same SSRF policy as apiUrl.
|
|
160
|
+
if (final.sdk.anthropicBaseUrl && !isAllowedApiUrl(final.sdk.anthropicBaseUrl)) {
|
|
161
|
+
throw new Error(`Unsafe sdk.anthropicBaseUrl: ${final.sdk.anthropicBaseUrl} — must be https:// ` +
|
|
162
|
+
`or http://localhost/http://127.0.0.1 (SSRF protection)`);
|
|
163
|
+
}
|
|
164
|
+
return final;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Enforce that `groupConfigDir` (whose files are injected UNSANITIZED into the
|
|
168
|
+
* system prompt) is not the same as, nor nested under, the agent-writable
|
|
169
|
+
* `cwdBase`. Otherwise a user-driven agent could write its own future
|
|
170
|
+
* system-prompt instructions.
|
|
171
|
+
*
|
|
172
|
+
* Uses realpathSync.native for paths that exist (so symlinks can't dodge the
|
|
173
|
+
* boundary) and falls back to lexical resolve() for not-yet-created dirs.
|
|
174
|
+
*/
|
|
175
|
+
function assertGroupConfigDirOutsideCwd(cfg) {
|
|
176
|
+
if (!cfg.groupConfigDir)
|
|
177
|
+
return;
|
|
178
|
+
const cwdBase = cfg.cwdBase ?? cfg.cwd;
|
|
179
|
+
const cwdBaseResolved = canonicalize(cwdBase);
|
|
180
|
+
const groupDirResolved = canonicalize(cfg.groupConfigDir);
|
|
181
|
+
if (groupDirResolved === cwdBaseResolved || isPathInside(groupDirResolved, cwdBaseResolved)) {
|
|
182
|
+
throw new Error(`Unsafe groupConfigDir: ${cfg.groupConfigDir} is the same as or nested under ` +
|
|
183
|
+
`cwdBase (${cwdBase}). It must be operator-controlled and outside the ` +
|
|
184
|
+
`agent-writable sandbox, since its files are injected into the system prompt.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Resolve to a real path when it exists (defeats symlink dodges), else lexical. */
|
|
188
|
+
function canonicalize(p) {
|
|
189
|
+
try {
|
|
190
|
+
return realpathSync.native(p);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return resolvePath(p);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** True when `child` is strictly inside `parent` (both already resolved). */
|
|
197
|
+
function isPathInside(child, parent) {
|
|
198
|
+
const parentWithSep = parent.endsWith(sep) ? parent : parent + sep;
|
|
199
|
+
return child.startsWith(parentWithSep);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* v0.3 multi-bot: expand a loaded Config into one concrete Config per bot.
|
|
203
|
+
*
|
|
204
|
+
* - Single-bot (no `bots`): returns `[config]` with `botId` defaulted to
|
|
205
|
+
* `default`, unchanged otherwise — fully backward compatible.
|
|
206
|
+
* - Multi-bot: returns one Config per `bots[]` entry. Each inherits the base
|
|
207
|
+
* config and applies its overrides. To guarantee bots never share history,
|
|
208
|
+
* cwd, or lock files, each bot's `dataDir` and `cwdBase` are namespaced by its
|
|
209
|
+
* id UNLESS the entry sets them explicitly.
|
|
210
|
+
*
|
|
211
|
+
* Throws on missing/duplicate bot tokens or duplicate ids (fail fast at boot).
|
|
212
|
+
*/
|
|
213
|
+
/**
|
|
214
|
+
* Expand a loaded GLOBAL config into one concrete Config per bot.
|
|
215
|
+
*
|
|
216
|
+
* Two-layer, bot-first model:
|
|
217
|
+
* - Single-bot (no `bots`): one bot with id `default`. Its token/overrides come
|
|
218
|
+
* from the global config and/or `<baseDir>/default/config.json`.
|
|
219
|
+
* - Multi-bot: one Config per `bots[]` entry (selected by `id`). For each, the
|
|
220
|
+
* effective config is: global shared fields ⊕ inline `bots[]` fields ⊕
|
|
221
|
+
* `<baseDir>/<id>/config.json` (per-dir file wins).
|
|
222
|
+
*
|
|
223
|
+
* Every bot's directories are DERIVED (never configurable):
|
|
224
|
+
* data = <baseDir>/<id>/data
|
|
225
|
+
* workspace = <baseDir>/<id>/workspace (cwdBase)
|
|
226
|
+
* memory = <baseDir>/<id>/memory
|
|
227
|
+
* and its personality from `<baseDir>/<id>/SOUL.md` (overrides systemPrompt).
|
|
228
|
+
*
|
|
229
|
+
* Throws on missing/duplicate tokens, duplicate ids, invalid id slugs, or unsafe
|
|
230
|
+
* apiUrl (fail fast at boot).
|
|
231
|
+
*/
|
|
232
|
+
export function resolveBotConfigs(config) {
|
|
233
|
+
// Single-bot: synthesize one entry with id "default".
|
|
234
|
+
const entries = config.bots && config.bots.length > 0
|
|
235
|
+
? config.bots
|
|
236
|
+
: [{ id: 'default', botToken: config.botToken || undefined }];
|
|
237
|
+
const seenIds = new Set();
|
|
238
|
+
const seenTokens = new Set();
|
|
239
|
+
const resolvedBots = entries.map((bot, i) => {
|
|
240
|
+
const id = bot.id ?? `bot${i}`;
|
|
241
|
+
// The id becomes a path segment for the bot's subtree, so restrict it to a
|
|
242
|
+
// conservative slug — otherwise ids like "../ops" or "a/b" could escape or
|
|
243
|
+
// alias the intended directory, defeating isolation.
|
|
244
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(id) || id === '.' || id === '..') {
|
|
245
|
+
throw new Error(`Bot "${id}": invalid id — use only letters, digits, dot, underscore, hyphen (no path separators)`);
|
|
246
|
+
}
|
|
247
|
+
if (seenIds.has(id)) {
|
|
248
|
+
throw new Error(`Duplicate bot id "${id}" — ids must be unique`);
|
|
249
|
+
}
|
|
250
|
+
seenIds.add(id);
|
|
251
|
+
// Derive the bot's self-contained subtree under baseDir.
|
|
252
|
+
const botRoot = pathJoin(config.baseDir, id);
|
|
253
|
+
const botDataDir = pathJoin(botRoot, 'data');
|
|
254
|
+
const botCwdBase = pathJoin(botRoot, 'workspace');
|
|
255
|
+
const botMemoryBase = pathJoin(botRoot, 'memory');
|
|
256
|
+
// #100: per-bot skills (<baseDir>/<id>/skills) + install-wide global skills
|
|
257
|
+
// (<baseDir>/skills). Symlinked into each session sandbox by skill-linker.
|
|
258
|
+
const botSkillsDir = pathJoin(botRoot, 'skills');
|
|
259
|
+
const globalSkillsDir = pathJoin(config.baseDir, 'skills');
|
|
260
|
+
// Per-bot config.json (in the bot's own subtree) is the highest-priority
|
|
261
|
+
// layer: global shared ⊕ inline bots[] entry ⊕ <baseDir>/<id>/config.json.
|
|
262
|
+
const perBotFile = readConfigFile(pathJoin(botRoot, 'config.json'));
|
|
263
|
+
const botToken = perBotFile.botToken ?? bot.botToken ?? '';
|
|
264
|
+
if (!botToken) {
|
|
265
|
+
throw new Error(`Bot "${id}": missing botToken — set it in ${pathJoin(botRoot, 'config.json')}`);
|
|
266
|
+
}
|
|
267
|
+
if (seenTokens.has(botToken)) {
|
|
268
|
+
throw new Error(`Duplicate botToken across bots — each bot needs a distinct token`);
|
|
269
|
+
}
|
|
270
|
+
seenTokens.add(botToken);
|
|
271
|
+
// openclaw-style SOUL.md in the bot's subtree overrides systemPrompt (which
|
|
272
|
+
// may come from the per-bot file, the inline entry, or the shared config).
|
|
273
|
+
const botSoul = loadSoul(botRoot);
|
|
274
|
+
const sharedSystemPrompt = config.sdk.systemPrompt;
|
|
275
|
+
const botSystemPrompt = botSoul ?? perBotFile.sdk?.systemPrompt ?? bot.systemPrompt ?? sharedSystemPrompt;
|
|
276
|
+
const apiUrl = perBotFile.apiUrl ?? bot.apiUrl ?? config.apiUrl;
|
|
277
|
+
const model = perBotFile.sdk?.model ?? bot.model ?? config.sdk.model;
|
|
278
|
+
const resolved = {
|
|
279
|
+
...config,
|
|
280
|
+
bots: undefined, // a per-bot config is single-bot
|
|
281
|
+
botId: id,
|
|
282
|
+
botToken,
|
|
283
|
+
apiUrl,
|
|
284
|
+
baseDir: config.baseDir,
|
|
285
|
+
dataDir: botDataDir,
|
|
286
|
+
cwdBase: botCwdBase,
|
|
287
|
+
cwd: botCwdBase,
|
|
288
|
+
memoryBase: botMemoryBase,
|
|
289
|
+
skillsDir: botSkillsDir,
|
|
290
|
+
globalSkillsDir,
|
|
291
|
+
botBlocklist: perBotFile.botBlocklist ?? bot.botBlocklist ?? config.botBlocklist,
|
|
292
|
+
allowedBotUids: perBotFile.allowedBotUids ?? bot.allowedBotUids ?? config.allowedBotUids,
|
|
293
|
+
mentionFreeGroups: perBotFile.mentionFreeGroups ?? bot.mentionFreeGroups ?? config.mentionFreeGroups,
|
|
294
|
+
groupConfigDir: perBotFile.groupConfigDir ?? config.groupConfigDir,
|
|
295
|
+
sdk: {
|
|
296
|
+
...config.sdk,
|
|
297
|
+
...(perBotFile.sdk ?? {}),
|
|
298
|
+
...(model !== undefined ? { model } : {}),
|
|
299
|
+
...(botSystemPrompt !== undefined ? { systemPrompt: botSystemPrompt } : {}),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
if (!isAllowedApiUrl(resolved.apiUrl)) {
|
|
303
|
+
throw new Error(`Bot "${id}": unsafe apiUrl ${resolved.apiUrl} (SSRF protection)`);
|
|
304
|
+
}
|
|
305
|
+
// GROUP.md trust boundary: groupConfigDir must not be the bot's writable cwd.
|
|
306
|
+
assertGroupConfigDirOutsideCwd(resolved);
|
|
307
|
+
return resolved;
|
|
308
|
+
});
|
|
309
|
+
return resolvedBots;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* v1.1: openclaw-style per-bot personality. Read `<botRoot>/SOUL.md` if it
|
|
313
|
+
* exists and return its trimmed contents as the bot's "soul" (voice/stance/
|
|
314
|
+
* boundaries), to be composed into the agent system prompt. Mirrors openclaw's
|
|
315
|
+
* SOUL.md: a file you edit, not a config string. When the file is absent or
|
|
316
|
+
* empty, returns undefined so the caller falls back to the `systemPrompt`
|
|
317
|
+
* config string. Best-effort — a read error never blocks startup.
|
|
318
|
+
*/
|
|
319
|
+
export function loadSoul(botRoot) {
|
|
320
|
+
const path = pathJoin(botRoot, 'SOUL.md');
|
|
321
|
+
if (!existsSync(path))
|
|
322
|
+
return undefined;
|
|
323
|
+
try {
|
|
324
|
+
const content = readFileSync(path, 'utf-8').trim();
|
|
325
|
+
return content.length > 0 ? content : undefined;
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
console.warn(`[cc-channel-octo] WARNING: failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #115: Cron schedule evaluation — pure functions, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Supports two schedule forms:
|
|
5
|
+
* - **5-field cron** `"minute hour dom month dow"` — each field is `*`, an
|
|
6
|
+
* integer, `*` + `/step`, an `a-b` range, or a comma list of those. Standard
|
|
7
|
+
* Unix semantics: dom/dow are OR'd when both are restricted (a task fires when
|
|
8
|
+
* either matches), matching cron's historical behavior.
|
|
9
|
+
* - **one-shot ISO datetime** `"2026-06-09T09:00:00Z"` — fires once at that
|
|
10
|
+
* instant, then never again.
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free (a tiny evaluator beats pulling a cron library for this).
|
|
13
|
+
*
|
|
14
|
+
* TIMEZONE: cron fields are matched against the gateway process's LOCAL time
|
|
15
|
+
* (`Date#getHours()` etc.), so `"0 9 * * *"` means 9am in the server's timezone.
|
|
16
|
+
* One-shot ISO datetimes are absolute instants (honor any offset/`Z` in the
|
|
17
|
+
* string). Set `TZ=...` on the process to control cron-field interpretation.
|
|
18
|
+
*/
|
|
19
|
+
/** A parsed 5-field cron expression: each field is the set of allowed values. */
|
|
20
|
+
export interface ParsedCron {
|
|
21
|
+
minute: Set<number>;
|
|
22
|
+
hour: Set<number>;
|
|
23
|
+
dom: Set<number>;
|
|
24
|
+
month: Set<number>;
|
|
25
|
+
dow: Set<number>;
|
|
26
|
+
/** True when dom/dow were both restricted (affects OR semantics). */
|
|
27
|
+
domRestricted: boolean;
|
|
28
|
+
dowRestricted: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Parse a 5-field cron expression. Returns null when invalid. */
|
|
31
|
+
export declare function parseCronExpression(expr: string): ParsedCron | null;
|
|
32
|
+
/** True when `date` (local time) matches the parsed cron. */
|
|
33
|
+
export declare function matchesCron(p: ParsedCron, date: Date): boolean;
|
|
34
|
+
/** Heuristic: is this schedule a one-shot ISO datetime (vs a cron expr)? */
|
|
35
|
+
export declare function isOneShotSchedule(schedule: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Strictly parse a one-shot ISO-8601 datetime → Unix ms, or null if invalid.
|
|
38
|
+
*
|
|
39
|
+
* `new Date()` alone is too lenient (e.g. it rolls "2026-13-13T…" into a real
|
|
40
|
+
* but wrong instant instead of rejecting it). We additionally require the
|
|
41
|
+
* canonical shape via regex AND verify the authored wall-clock fields name a
|
|
42
|
+
* real calendar instant, so an out-of-range month/day/hour can't sneak through
|
|
43
|
+
* as a silently shifted time — for ALL zone forms (Z, ±hh:mm, or none).
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseOneShot(schedule: string): number | null;
|
|
46
|
+
/**
|
|
47
|
+
* Compute the next fire time (Unix ms) strictly after `fromMs`, or null when
|
|
48
|
+
* there is none (a past/invalid one-shot, or an impossible cron).
|
|
49
|
+
*
|
|
50
|
+
* - one-shot ISO: its instant if still in the future, else null.
|
|
51
|
+
* - cron: scan minute-by-minute from the next whole minute, up to ~366 days.
|
|
52
|
+
*/
|
|
53
|
+
export declare function computeNextRun(schedule: string, recurring: boolean, fromMs: number): number | null;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #115: Cron schedule evaluation — pure functions, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Supports two schedule forms:
|
|
5
|
+
* - **5-field cron** `"minute hour dom month dow"` — each field is `*`, an
|
|
6
|
+
* integer, `*` + `/step`, an `a-b` range, or a comma list of those. Standard
|
|
7
|
+
* Unix semantics: dom/dow are OR'd when both are restricted (a task fires when
|
|
8
|
+
* either matches), matching cron's historical behavior.
|
|
9
|
+
* - **one-shot ISO datetime** `"2026-06-09T09:00:00Z"` — fires once at that
|
|
10
|
+
* instant, then never again.
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free (a tiny evaluator beats pulling a cron library for this).
|
|
13
|
+
*
|
|
14
|
+
* TIMEZONE: cron fields are matched against the gateway process's LOCAL time
|
|
15
|
+
* (`Date#getHours()` etc.), so `"0 9 * * *"` means 9am in the server's timezone.
|
|
16
|
+
* One-shot ISO datetimes are absolute instants (honor any offset/`Z` in the
|
|
17
|
+
* string). Set `TZ=...` on the process to control cron-field interpretation.
|
|
18
|
+
*/
|
|
19
|
+
const FIELD_RANGES = [
|
|
20
|
+
['minute', 0, 59],
|
|
21
|
+
['hour', 0, 23],
|
|
22
|
+
['dom', 1, 31],
|
|
23
|
+
['month', 1, 12],
|
|
24
|
+
['dow', 0, 6], // 0 = Sunday
|
|
25
|
+
];
|
|
26
|
+
/** Expand one cron field into a set of allowed integers, or null if invalid. */
|
|
27
|
+
function parseField(raw, min, max) {
|
|
28
|
+
const out = new Set();
|
|
29
|
+
for (const part of raw.split(',')) {
|
|
30
|
+
const seg = part.trim();
|
|
31
|
+
if (seg === '')
|
|
32
|
+
return null;
|
|
33
|
+
// step: "*/n" or "a-b/n" or "a/n"
|
|
34
|
+
let stepStr;
|
|
35
|
+
let rangeStr = seg;
|
|
36
|
+
const slash = seg.indexOf('/');
|
|
37
|
+
if (slash !== -1) {
|
|
38
|
+
rangeStr = seg.slice(0, slash);
|
|
39
|
+
stepStr = seg.slice(slash + 1);
|
|
40
|
+
}
|
|
41
|
+
let step = 1;
|
|
42
|
+
if (stepStr !== undefined) {
|
|
43
|
+
if (!/^\d+$/.test(stepStr))
|
|
44
|
+
return null;
|
|
45
|
+
step = Number(stepStr);
|
|
46
|
+
if (step < 1)
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
let lo;
|
|
50
|
+
let hi;
|
|
51
|
+
if (rangeStr === '*') {
|
|
52
|
+
lo = min;
|
|
53
|
+
hi = max;
|
|
54
|
+
}
|
|
55
|
+
else if (/^\d+$/.test(rangeStr)) {
|
|
56
|
+
lo = hi = Number(rangeStr);
|
|
57
|
+
// A bare number with a step (e.g. "5/10") means "from 5 to max, step".
|
|
58
|
+
if (stepStr !== undefined)
|
|
59
|
+
hi = max;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const m = /^(\d+)-(\d+)$/.exec(rangeStr);
|
|
63
|
+
if (!m)
|
|
64
|
+
return null;
|
|
65
|
+
lo = Number(m[1]);
|
|
66
|
+
hi = Number(m[2]);
|
|
67
|
+
}
|
|
68
|
+
if (lo < min || hi > max || lo > hi)
|
|
69
|
+
return null;
|
|
70
|
+
for (let v = lo; v <= hi; v += step)
|
|
71
|
+
out.add(v);
|
|
72
|
+
}
|
|
73
|
+
return out.size > 0 ? out : null;
|
|
74
|
+
}
|
|
75
|
+
/** Parse a 5-field cron expression. Returns null when invalid. */
|
|
76
|
+
export function parseCronExpression(expr) {
|
|
77
|
+
const fields = expr.trim().split(/\s+/);
|
|
78
|
+
if (fields.length !== 5)
|
|
79
|
+
return null;
|
|
80
|
+
const sets = [];
|
|
81
|
+
for (let i = 0; i < 5; i++) {
|
|
82
|
+
const [, min, max] = FIELD_RANGES[i];
|
|
83
|
+
const set = parseField(fields[i], min, max);
|
|
84
|
+
if (!set)
|
|
85
|
+
return null;
|
|
86
|
+
sets.push(set);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
minute: sets[0],
|
|
90
|
+
hour: sets[1],
|
|
91
|
+
dom: sets[2],
|
|
92
|
+
month: sets[3],
|
|
93
|
+
dow: sets[4],
|
|
94
|
+
domRestricted: fields[2] !== '*',
|
|
95
|
+
dowRestricted: fields[4] !== '*',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** True when `date` (local time) matches the parsed cron. */
|
|
99
|
+
export function matchesCron(p, date) {
|
|
100
|
+
if (!p.minute.has(date.getMinutes()))
|
|
101
|
+
return false;
|
|
102
|
+
if (!p.hour.has(date.getHours()))
|
|
103
|
+
return false;
|
|
104
|
+
if (!p.month.has(date.getMonth() + 1))
|
|
105
|
+
return false;
|
|
106
|
+
const domOk = p.dom.has(date.getDate());
|
|
107
|
+
const dowOk = p.dow.has(date.getDay());
|
|
108
|
+
// Standard cron OR semantics: if BOTH dom and dow are restricted, match when
|
|
109
|
+
// EITHER matches; otherwise both (the unrestricted one is always true) must.
|
|
110
|
+
if (p.domRestricted && p.dowRestricted)
|
|
111
|
+
return domOk || dowOk;
|
|
112
|
+
return domOk && dowOk;
|
|
113
|
+
}
|
|
114
|
+
/** Heuristic: is this schedule a one-shot ISO datetime (vs a cron expr)? */
|
|
115
|
+
export function isOneShotSchedule(schedule) {
|
|
116
|
+
// Cron exprs are space-separated fields; ISO datetimes contain 'T' and no spaces.
|
|
117
|
+
return schedule.includes('T') && !/\s/.test(schedule.trim());
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Strictly parse a one-shot ISO-8601 datetime → Unix ms, or null if invalid.
|
|
121
|
+
*
|
|
122
|
+
* `new Date()` alone is too lenient (e.g. it rolls "2026-13-13T…" into a real
|
|
123
|
+
* but wrong instant instead of rejecting it). We additionally require the
|
|
124
|
+
* canonical shape via regex AND verify the authored wall-clock fields name a
|
|
125
|
+
* real calendar instant, so an out-of-range month/day/hour can't sneak through
|
|
126
|
+
* as a silently shifted time — for ALL zone forms (Z, ±hh:mm, or none).
|
|
127
|
+
*/
|
|
128
|
+
export function parseOneShot(schedule) {
|
|
129
|
+
const s = schedule.trim();
|
|
130
|
+
// YYYY-MM-DDThh:mm(:ss(.fff)?)? with optional Z or ±hh:mm offset.
|
|
131
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})?$/.exec(s);
|
|
132
|
+
if (!m)
|
|
133
|
+
return null;
|
|
134
|
+
const t = new Date(s).getTime();
|
|
135
|
+
if (Number.isNaN(t))
|
|
136
|
+
return null;
|
|
137
|
+
// Reject lenient rollover. Calendar validity (is "Feb 31" a real date? is hour
|
|
138
|
+
// 25 valid?) is INDEPENDENT of the timezone, so validate the authored
|
|
139
|
+
// wall-clock fields with a zone-free UTC round-trip probe: if Date.UTC had to
|
|
140
|
+
// roll any field over, the rendered field won't match what the user wrote.
|
|
141
|
+
// This catches offset rollover (e.g. 2026-02-31T00:00:00+08:00) too — the old
|
|
142
|
+
// code skipped the check for numeric offsets and let it roll into March.
|
|
143
|
+
const yr = Number(m[1]);
|
|
144
|
+
const mo = Number(m[2]);
|
|
145
|
+
const day = Number(m[3]);
|
|
146
|
+
const hh = Number(m[4]);
|
|
147
|
+
const mm = Number(m[5]);
|
|
148
|
+
const ss = m[6] !== undefined ? Number(m[6]) : 0;
|
|
149
|
+
const probe = new Date(Date.UTC(yr, mo - 1, day, hh, mm, ss));
|
|
150
|
+
if (probe.getUTCFullYear() !== yr ||
|
|
151
|
+
probe.getUTCMonth() !== mo - 1 ||
|
|
152
|
+
probe.getUTCDate() !== day ||
|
|
153
|
+
probe.getUTCHours() !== hh ||
|
|
154
|
+
probe.getUTCMinutes() !== mm ||
|
|
155
|
+
probe.getUTCSeconds() !== ss) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return t;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compute the next fire time (Unix ms) strictly after `fromMs`, or null when
|
|
162
|
+
* there is none (a past/invalid one-shot, or an impossible cron).
|
|
163
|
+
*
|
|
164
|
+
* - one-shot ISO: its instant if still in the future, else null.
|
|
165
|
+
* - cron: scan minute-by-minute from the next whole minute, up to ~366 days.
|
|
166
|
+
*/
|
|
167
|
+
export function computeNextRun(schedule, recurring, fromMs) {
|
|
168
|
+
if (isOneShotSchedule(schedule)) {
|
|
169
|
+
const t = parseOneShot(schedule);
|
|
170
|
+
if (t === null)
|
|
171
|
+
return null;
|
|
172
|
+
return t > fromMs ? t : null;
|
|
173
|
+
}
|
|
174
|
+
const parsed = parseCronExpression(schedule);
|
|
175
|
+
if (!parsed)
|
|
176
|
+
return null;
|
|
177
|
+
void recurring; // cron exprs are inherently recurring; flag kept for symmetry
|
|
178
|
+
// Start at the next whole minute boundary after fromMs.
|
|
179
|
+
const start = new Date(fromMs);
|
|
180
|
+
start.setSeconds(0, 0);
|
|
181
|
+
start.setMinutes(start.getMinutes() + 1);
|
|
182
|
+
const MAX_MINUTES = 366 * 24 * 60;
|
|
183
|
+
const cursor = new Date(start);
|
|
184
|
+
for (let i = 0; i < MAX_MINUTES; i++) {
|
|
185
|
+
if (matchesCron(parsed, cursor))
|
|
186
|
+
return cursor.getTime();
|
|
187
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
188
|
+
}
|
|
189
|
+
return null; // impossible schedule (e.g. Feb 31)
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=cron-evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-evaluator.js","sourceRoot":"","sources":["../src/cron-evaluator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #115: Cron-fire authenticity marker.
|
|
3
|
+
*
|
|
4
|
+
* Synthetic cron messages bypass the group @mention gate. To stop a malicious
|
|
5
|
+
* group member from forging that bypass by putting `_cronFire: true` in a real
|
|
6
|
+
* message payload, the scheduler stamps each synthetic message with a secret
|
|
7
|
+
* nonce generated ONCE at process start, and the router accepts the bypass only
|
|
8
|
+
* when the nonce matches. The nonce never leaves the process (it is not derived
|
|
9
|
+
* from anything an attacker can observe), so an inbound WS message cannot carry
|
|
10
|
+
* the right value.
|
|
11
|
+
*
|
|
12
|
+
* Shared in its own tiny module so both `cron-scheduler` (stamp) and
|
|
13
|
+
* `session-router` (verify) depend on it without a circular import.
|
|
14
|
+
*/
|
|
15
|
+
/** Per-process secret. Regenerated each start — synthetic messages are
|
|
16
|
+
* in-memory and short-lived, so a fresh nonce per process is fine. */
|
|
17
|
+
export declare const CRON_FIRE_NONCE: string;
|
|
18
|
+
/** Payload key carrying the nonce on a synthetic cron message. */
|
|
19
|
+
export declare const CRON_FIRE_NONCE_KEY = "_cronFireNonce";
|
|
20
|
+
/** True only for a genuine in-process cron fire (marker + matching nonce). */
|
|
21
|
+
export declare function isAuthenticCronFire(payload: {
|
|
22
|
+
_cronFire?: unknown;
|
|
23
|
+
[k: string]: unknown;
|
|
24
|
+
}): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #115: Cron-fire authenticity marker.
|
|
3
|
+
*
|
|
4
|
+
* Synthetic cron messages bypass the group @mention gate. To stop a malicious
|
|
5
|
+
* group member from forging that bypass by putting `_cronFire: true` in a real
|
|
6
|
+
* message payload, the scheduler stamps each synthetic message with a secret
|
|
7
|
+
* nonce generated ONCE at process start, and the router accepts the bypass only
|
|
8
|
+
* when the nonce matches. The nonce never leaves the process (it is not derived
|
|
9
|
+
* from anything an attacker can observe), so an inbound WS message cannot carry
|
|
10
|
+
* the right value.
|
|
11
|
+
*
|
|
12
|
+
* Shared in its own tiny module so both `cron-scheduler` (stamp) and
|
|
13
|
+
* `session-router` (verify) depend on it without a circular import.
|
|
14
|
+
*/
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
/** Per-process secret. Regenerated each start — synthetic messages are
|
|
17
|
+
* in-memory and short-lived, so a fresh nonce per process is fine. */
|
|
18
|
+
export const CRON_FIRE_NONCE = randomBytes(16).toString('hex');
|
|
19
|
+
/** Payload key carrying the nonce on a synthetic cron message. */
|
|
20
|
+
export const CRON_FIRE_NONCE_KEY = '_cronFireNonce';
|
|
21
|
+
/** True only for a genuine in-process cron fire (marker + matching nonce). */
|
|
22
|
+
export function isAuthenticCronFire(payload) {
|
|
23
|
+
return payload._cronFire === true && payload[CRON_FIRE_NONCE_KEY] === CRON_FIRE_NONCE;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=cron-fire-marker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-fire-marker.js","sourceRoot":"","sources":["../src/cron-fire-marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;uEACuE;AACvE,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAE/D,kEAAkE;AAClE,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD,8EAA8E;AAC9E,MAAM,UAAU,mBAAmB,CAAC,OAAsD;IACxF,OAAO,OAAO,CAAC,SAAS,KAAK,IAAI,IAAI,OAAO,CAAC,mBAAmB,CAAC,KAAK,eAAe,CAAC;AACxF,CAAC"}
|