@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.
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 +287 -0
  13. package/dist/config.js +332 -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 +81 -0
  46. package/dist/group-context.js +466 -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 +932 -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 +108 -0
  58. package/dist/mention-utils.js +199 -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 +144 -0
  73. package/dist/session-router.js +490 -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,46 @@
1
+ /**
2
+ * #115: Cron scheduler — a resident per-bot loop that fires due tasks.
3
+ *
4
+ * Every tick it loads the bot's cron.json, and for each enabled task whose
5
+ * `nextRun` is past, synthesizes a BotMessage (the task's prompt as a Text
6
+ * message, bound to the task's session coords, marked `_cronFire`) and hands it
7
+ * to `onFire` — which the gateway wires to the same `onInbound` real messages
8
+ * use, so a fired task runs through the entire normal pipeline.
9
+ *
10
+ * Best-effort throughout: a failing task is logged and skipped, never crashing
11
+ * the loop. Missed tasks (process was down across their window) fire ONCE on
12
+ * catch-up, then advance to the next future occurrence — no thundering herd.
13
+ */
14
+ import type { BotMessage } from './octo/types.js';
15
+ import { CronStore, type CronTask } from './cron-store.js';
16
+ /** How often the scheduler scans cron.json (ms). 30s → ≤30s firing latency. */
17
+ export declare const CRON_TICK_MS = 30000;
18
+ export interface CronSchedulerOptions {
19
+ cronStore: CronStore;
20
+ /**
21
+ * Invoked with a synthetic BotMessage when a task is due (= onInbound). Fire
22
+ * is non-blocking; the scheduler does not await it and nextRun advances
23
+ * regardless (a failed fire is logged at the handler's own catch site —
24
+ * attributed to the task via the `cron:<id>:<ts>` message_id — not retried, to
25
+ * avoid an error loop hammering the channel).
26
+ */
27
+ onFire: (msg: BotMessage) => void;
28
+ /** Log prefix, e.g. "[bot-id] " in multi-bot mode. */
29
+ label?: string;
30
+ }
31
+ /** Build the synthetic inbound message for a fired task. */
32
+ export declare function synthesizeCronMessage(task: CronTask): BotMessage;
33
+ export declare class CronScheduler {
34
+ private readonly opts;
35
+ private timer;
36
+ constructor(opts: CronSchedulerOptions);
37
+ /** Arm the periodic scan. Idempotent. */
38
+ start(): void;
39
+ /** Stop scanning. */
40
+ stop(): void;
41
+ /**
42
+ * One scan: fire due tasks, advance/drop them, persist. Exposed for tests.
43
+ * Never throws.
44
+ */
45
+ tick(): void;
46
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * #115: Cron scheduler — a resident per-bot loop that fires due tasks.
3
+ *
4
+ * Every tick it loads the bot's cron.json, and for each enabled task whose
5
+ * `nextRun` is past, synthesizes a BotMessage (the task's prompt as a Text
6
+ * message, bound to the task's session coords, marked `_cronFire`) and hands it
7
+ * to `onFire` — which the gateway wires to the same `onInbound` real messages
8
+ * use, so a fired task runs through the entire normal pipeline.
9
+ *
10
+ * Best-effort throughout: a failing task is logged and skipped, never crashing
11
+ * the loop. Missed tasks (process was down across their window) fire ONCE on
12
+ * catch-up, then advance to the next future occurrence — no thundering herd.
13
+ */
14
+ import { MessageType } from './octo/types.js';
15
+ import { computeNextRun } from './cron-evaluator.js';
16
+ import { CRON_FIRE_NONCE, CRON_FIRE_NONCE_KEY } from './cron-fire-marker.js';
17
+ /** How often the scheduler scans cron.json (ms). 30s → ≤30s firing latency. */
18
+ export const CRON_TICK_MS = 30_000;
19
+ /** Build the synthetic inbound message for a fired task. */
20
+ export function synthesizeCronMessage(task) {
21
+ return {
22
+ message_id: `cron:${task.id}:${Date.now()}`,
23
+ message_seq: 0,
24
+ from_uid: task.fromUid,
25
+ from_name: task.fromName,
26
+ channel_id: task.channelId,
27
+ channel_type: task.channelType,
28
+ timestamp: Math.floor(Date.now() / 1000),
29
+ payload: {
30
+ type: MessageType.Text,
31
+ content: task.prompt,
32
+ // Synthetic marker + per-process nonce: lets the router bypass the group
33
+ // @mention gate for genuine in-process cron fires only (see
34
+ // session-router isCronFire / cron-fire-marker). A forged inbound payload
35
+ // can set `_cronFire` but cannot know the secret nonce. Allowed by the
36
+ // MessagePayload index signature; never set on real inbound messages.
37
+ _cronFire: true,
38
+ [CRON_FIRE_NONCE_KEY]: CRON_FIRE_NONCE,
39
+ },
40
+ };
41
+ }
42
+ export class CronScheduler {
43
+ opts;
44
+ timer = null;
45
+ constructor(opts) {
46
+ this.opts = opts;
47
+ }
48
+ /** Arm the periodic scan. Idempotent. */
49
+ start() {
50
+ if (this.timer)
51
+ return;
52
+ this.timer = setInterval(() => this.tick(), CRON_TICK_MS);
53
+ this.timer.unref(); // never keep the process alive on the cron loop alone
54
+ }
55
+ /** Stop scanning. */
56
+ stop() {
57
+ if (this.timer) {
58
+ clearInterval(this.timer);
59
+ this.timer = null;
60
+ }
61
+ }
62
+ /**
63
+ * One scan: fire due tasks, advance/drop them, persist. Exposed for tests.
64
+ * Never throws.
65
+ */
66
+ tick() {
67
+ const now = Date.now();
68
+ // Single atomic read-modify-write: fire due tasks and persist the survivor
69
+ // set in one synchronous pass, so a concurrent tool create/delete (which
70
+ // also goes through cronStore.update) can't lose updates against us.
71
+ try {
72
+ this.opts.cronStore.update((tasks) => {
73
+ const survivors = [];
74
+ let changed = false;
75
+ for (const task of tasks) {
76
+ if (!task.enabled || task.nextRun === null || task.nextRun > now) {
77
+ survivors.push(task);
78
+ continue;
79
+ }
80
+ changed = true;
81
+ // Due. Fire (best-effort; onFire is fire-and-forget).
82
+ const lateMin = Math.round((now - task.nextRun) / 60_000);
83
+ if (lateMin >= 1) {
84
+ console.warn(`[cc-channel-octo] ${this.opts.label ?? ''}cron: task ${task.id} (${task.schedule}) ` +
85
+ `fired ${lateMin} min late (catch-up)`);
86
+ }
87
+ try {
88
+ // Fire-and-forget: onFire (= onInbound) drives the full pipeline,
89
+ // which swallows its own errors and posts a user-facing reply. A
90
+ // FAILED fire is attributed to this task at handleMessage's catch
91
+ // site (via the `cron:<id>:<ts>` message_id) — not here, because the
92
+ // returned value is void and never rejects.
93
+ this.opts.onFire(synthesizeCronMessage(task));
94
+ }
95
+ catch (err) {
96
+ console.error(`[cc-channel-octo] ${this.opts.label ?? ''}cron: onFire threw for ${task.id}: ${String(err)}`);
97
+ }
98
+ task.lastRun = now;
99
+ if (task.recurring) {
100
+ task.nextRun = computeNextRun(task.schedule, true, now);
101
+ survivors.push(task); // keep; next future occurrence (or null → inert)
102
+ }
103
+ // one-shot: drop (not pushed to survivors)
104
+ }
105
+ // Return the SAME reference when nothing fired so update() skips the write.
106
+ return changed ? survivors : tasks;
107
+ });
108
+ }
109
+ catch (err) {
110
+ console.error(`[cc-channel-octo] ${this.opts.label ?? ''}cron: tick failed: ${String(err)}`);
111
+ }
112
+ }
113
+ }
114
+ //# sourceMappingURL=cron-scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-scheduler.js","sourceRoot":"","sources":["../src/cron-scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * #115: Per-bot cron task persistence — `<baseDir>/<botId>/cron.json`.
3
+ *
4
+ * Holds the scheduled tasks a bot's agent has registered via the cron tool. Read
5
+ * by both the cron tool (agent turn, under the session lock) and the scheduler
6
+ * tick (every ~30s). Node is single-threaded and all I/O here is synchronous, so
7
+ * a tool write and a scheduler read can never interleave mid-operation; the
8
+ * atomic temp+rename below additionally guarantees a reader never sees a partial
9
+ * file even across a crash.
10
+ */
11
+ import type { ChannelType } from './octo/types.js';
12
+ /** One scheduled task. Persisted as a plain JSON object. */
13
+ export interface CronTask {
14
+ /** Stable handle (uuid) — used by cron_delete; not user-chosen. */
15
+ id: string;
16
+ /** 5-field cron expression OR a one-shot ISO datetime. */
17
+ schedule: string;
18
+ /** true = re-schedule after each fire; false = delete after firing once. */
19
+ recurring: boolean;
20
+ /** Prompt injected as the synthetic message's text (≤ MAX_PROMPT_BYTES). */
21
+ prompt: string;
22
+ /** Bound session coords — where the fired task runs and replies. */
23
+ channelId: string;
24
+ channelType: ChannelType;
25
+ fromUid: string;
26
+ fromName?: string;
27
+ /** uid that registered the task (owner-gate source of truth). */
28
+ createdBy: string;
29
+ /** Scheduler skips disabled tasks (kept for a future cron_disable). */
30
+ enabled: boolean;
31
+ /** Unix ms of creation. */
32
+ createdAt: number;
33
+ /** Unix ms of the last fire, or null if never fired. */
34
+ lastRun: number | null;
35
+ /** Unix ms of the next fire (the scheduler's due check), or null if none. */
36
+ nextRun: number | null;
37
+ }
38
+ /** Max prompt size (bytes) accepted into a task. */
39
+ export declare const MAX_PROMPT_BYTES = 2048;
40
+ /** Max number of tasks per bot. */
41
+ export declare const MAX_TASKS_PER_BOT = 50;
42
+ /** Load/save a single bot's cron.json. */
43
+ export declare class CronStore {
44
+ private readonly cronJsonPath;
45
+ constructor(cronJsonPath: string);
46
+ /** Parse cron.json. Throws on malformed JSON (loud, not silent). */
47
+ load(): CronTask[];
48
+ /** Like load(), but returns [] when the file does not exist. */
49
+ loadOrEmpty(): CronTask[];
50
+ /** Atomically write the task array (temp file + rename). */
51
+ save(tasks: CronTask[]): void;
52
+ /**
53
+ * Atomic read-modify-write: load the current tasks, apply `mutator`, persist
54
+ * the result, and return it. The whole sequence runs **synchronously** (no
55
+ * await), so under Node's single-threaded model no other turn or scheduler
56
+ * tick can interleave between the load and the save — eliminating the
57
+ * lost-update race that separate load()+save() calls would risk if a caller
58
+ * ever introduced an await between them. All cron mutations (create, delete,
59
+ * scheduler advance) go through this one method.
60
+ */
61
+ update(mutator: (tasks: CronTask[]) => CronTask[]): CronTask[];
62
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * #115: Per-bot cron task persistence — `<baseDir>/<botId>/cron.json`.
3
+ *
4
+ * Holds the scheduled tasks a bot's agent has registered via the cron tool. Read
5
+ * by both the cron tool (agent turn, under the session lock) and the scheduler
6
+ * tick (every ~30s). Node is single-threaded and all I/O here is synchronous, so
7
+ * a tool write and a scheduler read can never interleave mid-operation; the
8
+ * atomic temp+rename below additionally guarantees a reader never sees a partial
9
+ * file even across a crash.
10
+ */
11
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
12
+ /** Max prompt size (bytes) accepted into a task. */
13
+ export const MAX_PROMPT_BYTES = 2048;
14
+ /** Max number of tasks per bot. */
15
+ export const MAX_TASKS_PER_BOT = 50;
16
+ /** Load/save a single bot's cron.json. */
17
+ export class CronStore {
18
+ cronJsonPath;
19
+ constructor(cronJsonPath) {
20
+ this.cronJsonPath = cronJsonPath;
21
+ }
22
+ /** Parse cron.json. Throws on malformed JSON (loud, not silent). */
23
+ load() {
24
+ const raw = readFileSync(this.cronJsonPath, 'utf8');
25
+ const parsed = JSON.parse(raw);
26
+ if (!Array.isArray(parsed)) {
27
+ throw new Error(`cron.json is not an array: ${this.cronJsonPath}`);
28
+ }
29
+ return parsed;
30
+ }
31
+ /** Like load(), but returns [] when the file does not exist. */
32
+ loadOrEmpty() {
33
+ if (!existsSync(this.cronJsonPath))
34
+ return [];
35
+ return this.load();
36
+ }
37
+ /** Atomically write the task array (temp file + rename). */
38
+ save(tasks) {
39
+ const tmp = `${this.cronJsonPath}.tmp`;
40
+ writeFileSync(tmp, JSON.stringify(tasks, null, 2), { mode: 0o600 });
41
+ renameSync(tmp, this.cronJsonPath);
42
+ }
43
+ /**
44
+ * Atomic read-modify-write: load the current tasks, apply `mutator`, persist
45
+ * the result, and return it. The whole sequence runs **synchronously** (no
46
+ * await), so under Node's single-threaded model no other turn or scheduler
47
+ * tick can interleave between the load and the save — eliminating the
48
+ * lost-update race that separate load()+save() calls would risk if a caller
49
+ * ever introduced an await between them. All cron mutations (create, delete,
50
+ * scheduler advance) go through this one method.
51
+ */
52
+ update(mutator) {
53
+ const current = this.loadOrEmpty();
54
+ const next = mutator(current);
55
+ // Skip the write when the mutator returns the same array reference
56
+ // unchanged (e.g. an idle scheduler tick with nothing due) — avoids
57
+ // rewriting cron.json on every 30s tick.
58
+ if (next !== current)
59
+ this.save(next);
60
+ return next;
61
+ }
62
+ }
63
+ //# sourceMappingURL=cron-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-store.js","sourceRoot":"","sources":["../src/cron-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA8B9E,oDAAoD;AACpD,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AACrC,mCAAmC;AACnC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,0CAA0C;AAC1C,MAAM,OAAO,SAAS;IACS;IAA7B,YAA6B,YAAoB;QAApB,iBAAY,GAAZ,YAAY,CAAQ;IAAG,CAAC;IAErD,oEAAoE;IACpE,IAAI;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,MAAoB,CAAC;IAC9B,CAAC;IAED,gEAAgE;IAChE,WAAW;QACT,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC;YAAE,OAAO,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;IAED,4DAA4D;IAC5D,IAAI,CAAC,KAAiB;QACpB,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,YAAY,MAAM,CAAC;QACvC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACpE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACrC,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,OAA0C;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,mEAAmE;QACnE,oEAAoE;QACpE,yCAAyC;QACzC,IAAI,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * #115: Cron tool — an in-process MCP server letting the agent register, list,
3
+ * and delete per-bot scheduled tasks. Tools surface to the model as
4
+ * `mcp__cron__cron_create` / `_list` / `_delete`.
5
+ *
6
+ * The server is built PER TURN (`createCronToolServer`) with the current
7
+ * message's raw channel coords + the bot owner uid, so:
8
+ * - a created task BINDS to the session that created it (fires + replies there);
9
+ * - creation/deletion is GATED to the bot owner (registerBot.owner_uid). The
10
+ * agent is driven by untrusted IM users, so this server-side check — not LLM
11
+ * judgment — is what stops a prompt-injected agent from registering a
12
+ * malicious recurring task. (See SECURITY_PROMPT_PREFIX for the advisory
13
+ * defense-in-depth layer.)
14
+ */
15
+ import { z } from 'zod';
16
+ import type { ChannelType } from './octo/types.js';
17
+ import { CronStore } from './cron-store.js';
18
+ /** MCP server name; tools surface as `mcp__cron__<tool>`. */
19
+ export declare const CRON_TOOL_SERVER_NAME = "cron";
20
+ /** Raw coords of the session creating a task — what a fired task binds to. */
21
+ export interface CronSessionCoords {
22
+ channelId: string;
23
+ channelType: ChannelType;
24
+ fromUid: string;
25
+ fromName?: string;
26
+ }
27
+ /**
28
+ * Build the cron tool DEFINITIONS for one agent turn. Exported separately from
29
+ * the server so tests can invoke each handler directly (the MCP server keeps its
30
+ * tools in private internals). `coords` binds created tasks to this session;
31
+ * `ownerUid` gates create/delete to the bot owner.
32
+ */
33
+ export declare function buildCronTools(cronStore: CronStore, coords: CronSessionCoords, ownerUid: string): (import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
34
+ schedule: z.ZodString;
35
+ prompt: z.ZodString;
36
+ recurring: z.ZodOptional<z.ZodBoolean>;
37
+ }> | import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{}> | import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
38
+ id: z.ZodString;
39
+ }>)[];
40
+ /**
41
+ * Build the cron MCP server for one agent turn. `coords` binds created tasks to
42
+ * this session; `ownerUid` gates create/delete to the bot owner.
43
+ */
44
+ export declare function createCronToolServer(cronStore: CronStore, coords: CronSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * #115: Cron tool — an in-process MCP server letting the agent register, list,
3
+ * and delete per-bot scheduled tasks. Tools surface to the model as
4
+ * `mcp__cron__cron_create` / `_list` / `_delete`.
5
+ *
6
+ * The server is built PER TURN (`createCronToolServer`) with the current
7
+ * message's raw channel coords + the bot owner uid, so:
8
+ * - a created task BINDS to the session that created it (fires + replies there);
9
+ * - creation/deletion is GATED to the bot owner (registerBot.owner_uid). The
10
+ * agent is driven by untrusted IM users, so this server-side check — not LLM
11
+ * judgment — is what stops a prompt-injected agent from registering a
12
+ * malicious recurring task. (See SECURITY_PROMPT_PREFIX for the advisory
13
+ * defense-in-depth layer.)
14
+ */
15
+ import { z } from 'zod';
16
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
17
+ import { MAX_PROMPT_BYTES, MAX_TASKS_PER_BOT } from './cron-store.js';
18
+ import { computeNextRun, isOneShotSchedule, parseCronExpression } from './cron-evaluator.js';
19
+ /** MCP server name; tools surface as `mcp__cron__<tool>`. */
20
+ export const CRON_TOOL_SERVER_NAME = 'cron';
21
+ function jsonResult(value) {
22
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
23
+ }
24
+ function errResult(msg) {
25
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
26
+ }
27
+ /** A task rendered for the model (nextRun as ISO, no internal-only churn). */
28
+ function summarize(t) {
29
+ return {
30
+ id: t.id,
31
+ schedule: t.schedule,
32
+ recurring: t.recurring,
33
+ prompt: t.prompt,
34
+ nextRun: t.nextRun ? new Date(t.nextRun).toISOString() : null,
35
+ enabled: t.enabled,
36
+ };
37
+ }
38
+ /**
39
+ * Build the cron tool DEFINITIONS for one agent turn. Exported separately from
40
+ * the server so tests can invoke each handler directly (the MCP server keeps its
41
+ * tools in private internals). `coords` binds created tasks to this session;
42
+ * `ownerUid` gates create/delete to the bot owner.
43
+ */
44
+ export function buildCronTools(cronStore, coords, ownerUid) {
45
+ const isOwner = coords.fromUid === ownerUid && ownerUid !== '';
46
+ return [
47
+ tool('cron_create', 'Schedule a task: at the given time the bot re-runs `prompt` in THIS chat, ' +
48
+ 'as if you received it as a message, and posts the result here. ' +
49
+ '`schedule` is a 5-field cron expression ("0 9 * * 1-5" = weekdays 9am) ' +
50
+ 'or a one-shot ISO datetime ("2026-06-09T09:00:00Z"). Set recurring=false ' +
51
+ 'for a one-time reminder. Only the bot owner may create tasks.', {
52
+ schedule: z.string().min(1).describe('5-field cron expression or one-shot ISO datetime.'),
53
+ prompt: z.string().min(1).describe('The instruction to run when the task fires.'),
54
+ recurring: z.boolean().optional().describe('Re-run on every match (default: cron→true, one-shot→false).'),
55
+ }, async (args) => {
56
+ try {
57
+ if (!isOwner) {
58
+ return errResult('Only the bot owner can create scheduled tasks.');
59
+ }
60
+ const oneShot = isOneShotSchedule(args.schedule);
61
+ // Validate the schedule.
62
+ if (!oneShot && !parseCronExpression(args.schedule)) {
63
+ return errResult(`Invalid cron expression: ${args.schedule}`);
64
+ }
65
+ if (Buffer.byteLength(args.prompt, 'utf8') > MAX_PROMPT_BYTES) {
66
+ return errResult(`prompt too long (max ${MAX_PROMPT_BYTES} bytes).`);
67
+ }
68
+ const recurring = args.recurring ?? !oneShot;
69
+ const now = Date.now();
70
+ const nextRun = computeNextRun(args.schedule, recurring, now);
71
+ if (nextRun === null) {
72
+ return errResult(oneShot
73
+ ? 'one-shot time is in the past or invalid.'
74
+ : `schedule never matches (impossible cron): ${args.schedule}`);
75
+ }
76
+ const task = {
77
+ id: crypto.randomUUID(),
78
+ schedule: args.schedule,
79
+ recurring,
80
+ prompt: args.prompt,
81
+ channelId: coords.channelId,
82
+ channelType: coords.channelType,
83
+ fromUid: coords.fromUid,
84
+ fromName: coords.fromName,
85
+ createdBy: coords.fromUid,
86
+ enabled: true,
87
+ createdAt: now,
88
+ lastRun: null,
89
+ nextRun,
90
+ };
91
+ // Atomic read-modify-write; re-check the cap inside the mutator so a
92
+ // concurrent create can't push us over the limit.
93
+ let capped = false;
94
+ cronStore.update((tasks) => {
95
+ if (tasks.length >= MAX_TASKS_PER_BOT) {
96
+ capped = true;
97
+ return tasks;
98
+ }
99
+ return [...tasks, task];
100
+ });
101
+ if (capped) {
102
+ return errResult(`task limit reached (max ${MAX_TASKS_PER_BOT}). Delete one first.`);
103
+ }
104
+ return jsonResult({ created: summarize(task) });
105
+ }
106
+ catch (err) {
107
+ return errResult(err instanceof Error ? err.message : String(err));
108
+ }
109
+ }),
110
+ tool('cron_list', 'List the scheduled tasks bound to this bot.', {}, async () => {
111
+ try {
112
+ return jsonResult({ tasks: cronStore.loadOrEmpty().map(summarize) });
113
+ }
114
+ catch (err) {
115
+ return errResult(err instanceof Error ? err.message : String(err));
116
+ }
117
+ }),
118
+ tool('cron_delete', 'Delete a scheduled task by its id. Only the bot owner may delete tasks.', { id: z.string().min(1).describe('The task id (from cron_list).') }, async (args) => {
119
+ try {
120
+ if (!isOwner) {
121
+ return errResult('Only the bot owner can delete scheduled tasks.');
122
+ }
123
+ let found = false;
124
+ cronStore.update((tasks) => {
125
+ const next = tasks.filter((t) => t.id !== args.id);
126
+ found = next.length !== tasks.length;
127
+ return found ? next : tasks;
128
+ });
129
+ if (!found) {
130
+ return errResult(`no task with id ${args.id}`);
131
+ }
132
+ return jsonResult({ deleted: args.id });
133
+ }
134
+ catch (err) {
135
+ return errResult(err instanceof Error ? err.message : String(err));
136
+ }
137
+ }),
138
+ ];
139
+ }
140
+ /**
141
+ * Build the cron MCP server for one agent turn. `coords` binds created tasks to
142
+ * this session; `ownerUid` gates create/delete to the bot owner.
143
+ */
144
+ export function createCronToolServer(cronStore, coords, ownerUid) {
145
+ return createSdkMcpServer({
146
+ name: CRON_TOOL_SERVER_NAME,
147
+ version: '1.0.0',
148
+ tools: buildCronTools(cronStore, coords, ownerUid),
149
+ });
150
+ }
151
+ //# sourceMappingURL=cron-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-tool.js","sourceRoot":"","sources":["../src/cron-tool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Q3: Per-session cwd isolation under a shared `cwdBase`.
3
+ *
4
+ * Each session maps to a deterministic 16-hex sha256 prefix directory inside
5
+ * `cwdBase`, so one user's working tree cannot be read or mutated from another
6
+ * user's session while operators still allocate a single disk root for the bot.
7
+ *
8
+ * The partition key is the *exact* `sessionKey` the SessionRouter already
9
+ * produced for history (`SessionRouter.sessionKey()`), prefixed by the channel
10
+ * kind. Reusing the router key verbatim — rather than re-deriving spaceId or
11
+ * channel_id from the raw message — guarantees the cwd partition can never
12
+ * drift from the history partition:
13
+ *
14
+ * - DM: sessionKey = `${spaceId}:${uid}` (or bare `uid`) → `dm:<key>`
15
+ * - Group: sessionKey = `${channel_id}` → `group:<key>`
16
+ *
17
+ * Group sessionKey is the channel id alone, so ALL members of a group share one
18
+ * sandbox (a group is a collective workspace). DM is per-user, so each peer gets
19
+ * a private sandbox. The `kind` prefix keeps a DM key and a group key that
20
+ * happen to be byte-identical from colliding. (Group sharing reverses the
21
+ * per-(channel×user) split from PR #64 — intentional; space isolation is
22
+ * provided by one-bot-per-space, each with its own cwdBase.)
23
+ */
24
+ /**
25
+ * Per-session routing context for cwd isolation. `kind` is the channel class
26
+ * and `sessionKey` is the router-produced key the session's history is stored
27
+ * under — see `SessionRouter.sessionKey()`. Passing the router key directly
28
+ * (instead of re-deriving uid/spaceId/channel_id here) is what keeps the cwd
29
+ * and history partitions byte-for-byte consistent.
30
+ */
31
+ export type SessionCtx = {
32
+ kind: 'dm' | 'group';
33
+ sessionKey: string;
34
+ };
35
+ /** 7 days — long enough for a vacation, short enough to bound disk growth. */
36
+ export declare const DEFAULT_CWD_TTL_MS: number;
37
+ /**
38
+ * Compute the SDK auto-memory directory for a session under `memoryBase`.
39
+ *
40
+ * Deliberately a PURE function — unlike resolveSessionCwd it does NOT mkdir,
41
+ * touch mtime, or write a registry marker. The SDK creates the directory on
42
+ * first use, and we want auto-memory to be PERMANENT (no TTL): `memoryBase`
43
+ * lives outside `cwdBase`, so the cwd TTL sweep (cleanupExpiredCwds) never sees
44
+ * it. Uses the same kind-prefixed sha256 hashing as the cwd sandbox so the
45
+ * memory partition tracks the session partition exactly (group=shared per
46
+ * channel, DM=private per peer).
47
+ */
48
+ export declare function resolveMemoryDir(memoryBase: string, ctx: SessionCtx): string;
49
+ /**
50
+ * Resolve and ensure the per-session cwd exists. Idempotent — safe to call
51
+ * on every turn. Returns the absolute path under `cwdBase`.
52
+ *
53
+ * Note: the TTL tracks last *bot turn* (this function bumps the dir mtime on
54
+ * every call), not arbitrary filesystem activity inside the sandbox. A session
55
+ * with no inbound message for `ttlMs` is reclaimed even if a background process
56
+ * is still touching files inside it.
57
+ */
58
+ export declare function resolveSessionCwd(cwdBase: string, ctx: SessionCtx): string;
59
+ /**
60
+ * Sweep `cwdBase` for hashed session dirs whose mtime is older than `ttlMs`
61
+ * and remove them. Best-effort: failures are logged, never thrown — the bot
62
+ * must continue running even if disk cleanup hits a permission error.
63
+ *
64
+ * Silent no-op when `cwdBase` does not exist (e.g. first-run before any
65
+ * session has been resolved).
66
+ *
67
+ * A dir is eligible for deletion only when ALL hold: the name matches the
68
+ * 16-hex pattern, it is a real directory (not a symlink), and it has a matching
69
+ * entry in the `.cc-octo-sessions` registry (P0-3). The registry entry is
70
+ * removed alongside the dir.
71
+ */
72
+ export declare function cleanupExpiredCwds(cwdBase: string, ttlMs?: number): void;