aiden-runtime 4.1.5 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. package/package.json +13 -3
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggerBus.ts — v4.5 Phase 1: durable trigger bus.
10
+ *
11
+ * The CENTRAL persistence layer for the daemon. Every accepted
12
+ * trigger (file watcher fire, webhook delivery, email match,
13
+ * scheduled job tick) normalizes into a `trigger_events` row
14
+ * BEFORE returning 202 Accepted / marking the email seen /
15
+ * enqueuing schedule. ONE durable queue, not 4 parallel half-
16
+ * baked persistence layers.
17
+ *
18
+ * Concurrency semantics:
19
+ * - `insert()` uses INSERT OR IGNORE against the partial unique
20
+ * index on (source, idempotency_key). Duplicates return the
21
+ * existing id with `inserted: false`.
22
+ * - `claim()` atomically picks the oldest pending event and
23
+ * sets status='claimed' + claim_owner + claim_expires_at. A
24
+ * per-claim nonce (claimToken) is returned; subsequent
25
+ * release/markDone/markFailed validate it to prevent double-
26
+ * completion races across sibling daemons.
27
+ * - `reclaimExpired()` returns to 'pending' any claimed row
28
+ * whose lease has elapsed (called by the daemon ticker every
29
+ * 30s + on boot).
30
+ * - `markFailed()` increments attempts; on attempts >=
31
+ * maxAttempts the row moves to 'dead_letter' instead of
32
+ * returning to pending.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_CLAIM_LEASE_MS = void 0;
36
+ exports.createTriggerBus = createTriggerBus;
37
+ const node_crypto_1 = require("node:crypto");
38
+ exports.DEFAULT_CLAIM_LEASE_MS = 5 * 60000;
39
+ exports.DEFAULT_MAX_ATTEMPTS = 3;
40
+ function rowToTs(r) {
41
+ return {
42
+ id: r.id,
43
+ source: r.source,
44
+ sourceKey: r.source_key,
45
+ idempotencyKey: r.idempotency_key,
46
+ payload: safeJsonParse(r.payload_json),
47
+ status: r.status,
48
+ attempts: r.attempts,
49
+ claimOwner: r.claim_owner,
50
+ claimExpiresAt: r.claim_expires_at,
51
+ lastError: r.last_error,
52
+ createdAt: r.created_at,
53
+ updatedAt: r.updated_at,
54
+ completedAt: r.completed_at,
55
+ runId: r.run_id,
56
+ };
57
+ }
58
+ function safeJsonParse(s) {
59
+ try {
60
+ return JSON.parse(s);
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ }
66
+ function createTriggerBus(opts) {
67
+ const db = opts.db;
68
+ // In-memory map of valid claim tokens. Stored separately from
69
+ // SQLite so cross-daemon claim attempts can't forge a token by
70
+ // reading the row. Token is wiped on markDone/release/markFailed.
71
+ const activeClaims = new Map();
72
+ return {
73
+ insert(ev) {
74
+ const now = Date.now();
75
+ const payloadJson = JSON.stringify(ev.payload ?? {});
76
+ const result = db
77
+ .prepare(`INSERT OR IGNORE INTO trigger_events
78
+ (source, source_key, idempotency_key, payload_json,
79
+ status, attempts, created_at, updated_at)
80
+ VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)`)
81
+ .run(ev.source, ev.sourceKey, ev.idempotencyKey ?? null, payloadJson, now, now);
82
+ if (result.changes > 0) {
83
+ return { id: Number(result.lastInsertRowid), inserted: true };
84
+ }
85
+ // Dedup hit — return the existing id.
86
+ const existing = db
87
+ .prepare('SELECT id FROM trigger_events WHERE source = ? AND idempotency_key = ?')
88
+ .get(ev.source, ev.idempotencyKey ?? null);
89
+ if (!existing) {
90
+ // Defensive — shouldn't happen unless idempotency_key was null,
91
+ // in which case INSERT OR IGNORE wouldn't have skipped.
92
+ throw new Error('triggerBus.insert: INSERT OR IGNORE produced no row but no existing match found');
93
+ }
94
+ return { id: existing.id, inserted: false };
95
+ },
96
+ claim(opts2 = { ownerId: '' }) {
97
+ const leaseMs = opts2.leaseMs ?? exports.DEFAULT_CLAIM_LEASE_MS;
98
+ const now = Date.now();
99
+ const expires = now + leaseMs;
100
+ const claimToken = (0, node_crypto_1.randomUUID)();
101
+ const tx = db.transaction(() => {
102
+ // Pick the oldest pending event matching the optional source filter.
103
+ // v4.5 Phase 7 — honour cooldown: skip pending rows whose
104
+ // claim_expires_at is still in the future (re-set by
105
+ // markFailed with cooldownMs to delay re-claim).
106
+ const sql = opts2.source
107
+ ? `SELECT id FROM trigger_events
108
+ WHERE status = 'pending' AND source = ?
109
+ AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
110
+ ORDER BY created_at LIMIT 1`
111
+ : `SELECT id FROM trigger_events
112
+ WHERE status = 'pending'
113
+ AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
114
+ ORDER BY created_at LIMIT 1`;
115
+ const candidate = (opts2.source
116
+ ? db.prepare(sql).get(opts2.source, now)
117
+ : db.prepare(sql).get(now));
118
+ if (!candidate)
119
+ return null;
120
+ const upd = db
121
+ .prepare(`UPDATE trigger_events
122
+ SET status = 'claimed',
123
+ claim_owner = ?,
124
+ claim_expires_at = ?,
125
+ updated_at = ?,
126
+ attempts = attempts + 1
127
+ WHERE id = ? AND status = 'pending'`)
128
+ .run(opts2.ownerId, expires, now, candidate.id);
129
+ if (upd.changes === 0)
130
+ return null; // race lost
131
+ const row = db
132
+ .prepare('SELECT * FROM trigger_events WHERE id = ?')
133
+ .get(candidate.id);
134
+ return rowToTs(row);
135
+ });
136
+ const row = tx();
137
+ if (!row)
138
+ return null;
139
+ activeClaims.set(row.id, claimToken);
140
+ return { ...row, claimToken };
141
+ },
142
+ renewClaim(eventId, claimToken, extendMs) {
143
+ if (activeClaims.get(eventId) !== claimToken)
144
+ return false;
145
+ const now = Date.now();
146
+ const upd = db
147
+ .prepare(`UPDATE trigger_events
148
+ SET claim_expires_at = ?,
149
+ updated_at = ?
150
+ WHERE id = ? AND status = 'claimed'`)
151
+ .run(now + extendMs, now, eventId);
152
+ return upd.changes > 0;
153
+ },
154
+ release(eventId, claimToken) {
155
+ if (activeClaims.get(eventId) !== claimToken)
156
+ return;
157
+ const now = Date.now();
158
+ db.prepare(`UPDATE trigger_events
159
+ SET status = 'pending',
160
+ claim_owner = NULL,
161
+ claim_expires_at = NULL,
162
+ updated_at = ?
163
+ WHERE id = ? AND status = 'claimed'`).run(now, eventId);
164
+ activeClaims.delete(eventId);
165
+ },
166
+ markDone(eventId, claimToken, runId) {
167
+ if (activeClaims.get(eventId) !== claimToken)
168
+ return;
169
+ const now = Date.now();
170
+ db.prepare(`UPDATE trigger_events
171
+ SET status = 'done',
172
+ claim_owner = NULL,
173
+ claim_expires_at = NULL,
174
+ updated_at = ?,
175
+ completed_at = ?,
176
+ run_id = COALESCE(?, run_id)
177
+ WHERE id = ? AND status = 'claimed'`).run(now, now, runId ?? null, eventId);
178
+ activeClaims.delete(eventId);
179
+ },
180
+ markFailed(eventId, claimToken, error, opts2 = {}) {
181
+ if (activeClaims.get(eventId) !== claimToken)
182
+ return;
183
+ const max = opts2.maxAttempts ?? exports.DEFAULT_MAX_ATTEMPTS;
184
+ const now = Date.now();
185
+ const truncated = error.length > 1024 ? error.slice(0, 1024) + '…' : error;
186
+ // v4.5 Phase 7 — optional cooldown delays re-claim. When set,
187
+ // we stash `now + cooldownMs` in `claim_expires_at` while
188
+ // status='pending'. The claim picker filters out pending rows
189
+ // whose claim_expires_at is in the future (added below). Bus
190
+ // poll loop will pick the row up naturally once cooldown
191
+ // elapses — no explicit sleep needed.
192
+ const cooldownUntil = opts2.cooldownMs && opts2.cooldownMs > 0
193
+ ? now + opts2.cooldownMs
194
+ : null;
195
+ const tx = db.transaction(() => {
196
+ const row = db
197
+ .prepare('SELECT attempts FROM trigger_events WHERE id = ?')
198
+ .get(eventId);
199
+ if (!row)
200
+ return;
201
+ // attempts was already incremented at claim time. Move to
202
+ // dead_letter when the count hits max, else return to pending.
203
+ if (row.attempts >= max) {
204
+ db.prepare(`UPDATE trigger_events
205
+ SET status = 'dead_letter',
206
+ claim_owner = NULL,
207
+ claim_expires_at = NULL,
208
+ last_error = ?,
209
+ updated_at = ?,
210
+ completed_at = ?
211
+ WHERE id = ?`).run(truncated, now, now, eventId);
212
+ }
213
+ else {
214
+ db.prepare(`UPDATE trigger_events
215
+ SET status = 'pending',
216
+ claim_owner = NULL,
217
+ claim_expires_at = ?,
218
+ last_error = ?,
219
+ updated_at = ?
220
+ WHERE id = ?`).run(cooldownUntil, truncated, now, eventId);
221
+ }
222
+ });
223
+ tx();
224
+ activeClaims.delete(eventId);
225
+ },
226
+ reclaimExpired(now) {
227
+ const cutoff = now ?? Date.now();
228
+ const upd = db
229
+ .prepare(`UPDATE trigger_events
230
+ SET status = 'pending',
231
+ claim_owner = NULL,
232
+ claim_expires_at = NULL,
233
+ last_error = COALESCE(last_error, 'claim lease expired'),
234
+ updated_at = ?
235
+ WHERE status = 'claimed'
236
+ AND claim_expires_at IS NOT NULL
237
+ AND claim_expires_at < ?`)
238
+ .run(cutoff, cutoff);
239
+ return { reclaimed: upd.changes };
240
+ },
241
+ deadLetter(eventId, reason) {
242
+ const now = Date.now();
243
+ db.prepare(`UPDATE trigger_events
244
+ SET status = 'dead_letter',
245
+ claim_owner = NULL,
246
+ claim_expires_at = NULL,
247
+ last_error = ?,
248
+ updated_at = ?,
249
+ completed_at = ?
250
+ WHERE id = ?`).run(reason.length > 1024 ? reason.slice(0, 1024) + '…' : reason, now, now, eventId);
251
+ activeClaims.delete(eventId);
252
+ },
253
+ stats() {
254
+ const counts = db
255
+ .prepare(`SELECT status, COUNT(*) AS c FROM trigger_events GROUP BY status`)
256
+ .all();
257
+ const m = {};
258
+ for (const r of counts)
259
+ m[r.status] = r.c;
260
+ const oldest = db
261
+ .prepare(`SELECT MIN(created_at) AS t FROM trigger_events WHERE status = 'pending'`)
262
+ .get();
263
+ const oldestPendingMs = oldest.t != null ? Date.now() - oldest.t : null;
264
+ return {
265
+ pending: m.pending ?? 0,
266
+ claimed: m.claimed ?? 0,
267
+ running: m.running ?? 0,
268
+ deadLetter: m.dead_letter ?? 0,
269
+ oldestPendingMs,
270
+ };
271
+ },
272
+ get(eventId) {
273
+ const r = db
274
+ .prepare('SELECT * FROM trigger_events WHERE id = ?')
275
+ .get(eventId);
276
+ return r ? rowToTs(r) : null;
277
+ },
278
+ };
279
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/email/allowlist.ts — v4.5 Phase 4a.
10
+ *
11
+ * Compiles a per-trigger sender allowlist into a fast matcher.
12
+ * Glob syntax supported:
13
+ * - Exact: user@example.com
14
+ * - Domain: *@example.com
15
+ * - Prefix: alerts-*@example.com
16
+ * - Both: *-alerts-*@*.example.com
17
+ *
18
+ * Allowlist semantics:
19
+ * - Default (empty list) = REJECT all. Trigger is functional only
20
+ * after at least one --allow-sender is registered. This matches
21
+ * the explicit-over-implicit security posture.
22
+ * - Applied AFTER the automated-sender filter (we never trust
23
+ * `noreply@<allowed-domain>`).
24
+ * - Case-insensitive (RFC 5321: local-part is technically case-
25
+ * sensitive, but in practice IS not).
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.compileSenderAllowlist = compileSenderAllowlist;
29
+ /**
30
+ * Compile a list of address-pattern strings into a matcher.
31
+ * Empty list → matcher rejects every address (the explicit-allow
32
+ * default per Q-P4-1).
33
+ */
34
+ function compileSenderAllowlist(patterns) {
35
+ if (patterns.length === 0) {
36
+ return {
37
+ isAllowed: () => false,
38
+ size: () => 0,
39
+ };
40
+ }
41
+ const regexes = patterns
42
+ .map((p) => p.trim().toLowerCase())
43
+ .filter((p) => p.length > 0)
44
+ .map(patternToRegex);
45
+ return {
46
+ isAllowed(fromAddress) {
47
+ const addr = (fromAddress ?? '').trim().toLowerCase();
48
+ if (!addr)
49
+ return false;
50
+ for (const re of regexes)
51
+ if (re.test(addr))
52
+ return true;
53
+ return false;
54
+ },
55
+ size: () => regexes.length,
56
+ };
57
+ }
58
+ /**
59
+ * Convert a sender glob pattern into a case-insensitive RegExp
60
+ * anchored at both ends. Only `*` is supported (matches zero or
61
+ * more characters that are NOT a separator we care about — but
62
+ * since email addresses don't have hierarchical separators like
63
+ * filesystems do, `*` is greedy across the whole local-part or
64
+ * domain). Escape every other regex meta-char.
65
+ */
66
+ function patternToRegex(pat) {
67
+ // Escape regex specials except `*`, then convert `*` to `.*`.
68
+ const escaped = pat.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
69
+ return new RegExp('^' + escaped + '$');
70
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/email/automatedSender.ts — v4.5 Phase 4a.
10
+ *
11
+ * Detects mail from automated systems (noreply addresses, bounce
12
+ * notifications, mailing-list digests, calendar invites, etc.).
13
+ *
14
+ * Used at TWO points:
15
+ * 1. INGRESS — mark UID seen + skip (don't kick off an agent turn
16
+ * to "respond" to a bounce notification).
17
+ * 2. OUTBOUND — any future auto-reply path (Phase 5+) MUST NEVER
18
+ * reply to a sender flagged automated. This is the canonical
19
+ * defense against mail loops (the gateway's notification system
20
+ * bounces our reply, we reply to the bounce, ...).
21
+ *
22
+ * Two signal sources combined:
23
+ * - Substring match on the From / Reply-To address (noreply, etc.)
24
+ * - RFC headers: Auto-Submitted, Precedence, X-Auto-Response-Suppress,
25
+ * List-Unsubscribe.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.AUTOMATED_HEADERS = exports.NOREPLY_PATTERNS = void 0;
29
+ exports.isAutomatedSender = isAutomatedSender;
30
+ /** Address substrings — case-insensitive — that signal automated. */
31
+ exports.NOREPLY_PATTERNS = Object.freeze([
32
+ 'noreply',
33
+ 'no-reply',
34
+ 'no_reply',
35
+ 'donotreply',
36
+ 'do-not-reply',
37
+ 'mailer-daemon',
38
+ 'postmaster',
39
+ 'bounce',
40
+ 'notifications@',
41
+ 'automated@',
42
+ 'auto-confirm',
43
+ 'auto-reply',
44
+ 'automailer',
45
+ ]);
46
+ /** RFC headers that signal bulk/automated mail. */
47
+ exports.AUTOMATED_HEADERS = Object.freeze([
48
+ { header: 'Auto-Submitted', test: (v) => v.toLowerCase() !== 'no' },
49
+ { header: 'Precedence', test: (v) => ['bulk', 'list', 'junk'].includes(v.toLowerCase()) },
50
+ { header: 'X-Auto-Response-Suppress', test: (v) => v.length > 0 },
51
+ { header: 'List-Unsubscribe', test: (v) => v.length > 0 },
52
+ ]);
53
+ /**
54
+ * Return true when the sender appears to be an automated system.
55
+ *
56
+ * - `fromAddress` is the bare address (e.g. 'noreply@github.com'),
57
+ * NOT a display-name-decorated form. Caller should extract via
58
+ * mailparser's `parsed.from?.value[0]?.address`.
59
+ * - `headers` is a case-INSENSITIVE map. mailparser provides
60
+ * `parsed.headers` as a Map with lowercased keys; pass that
61
+ * normalized to `Record<string, string>` (multi-value headers
62
+ * pick the first value).
63
+ */
64
+ function isAutomatedSender(fromAddress, headers) {
65
+ const addr = (fromAddress ?? '').toLowerCase();
66
+ for (const p of exports.NOREPLY_PATTERNS)
67
+ if (addr.includes(p))
68
+ return true;
69
+ for (const r of exports.AUTOMATED_HEADERS) {
70
+ // Match case-insensitively against both the exact-case header
71
+ // name and the lowercased form so callers don't have to remember
72
+ // mailparser's specific quirk.
73
+ const v = headers[r.header] ?? headers[r.header.toLowerCase()];
74
+ if (typeof v === 'string' && v.length > 0 && r.test(v))
75
+ return true;
76
+ }
77
+ return false;
78
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/email/emailSeenStore.ts — v4.5 Phase 4a.
10
+ *
11
+ * Writer + reader for the `email_seen` table. Cross-restart authority
12
+ * for "have we processed this UID before?" + forensic audit trail.
13
+ *
14
+ * UNIQUE(route_id, uid_validity, uid) at the schema layer dedups
15
+ * concurrent or repeated writes for the same UID. The status field
16
+ * records WHY a UID was seen even when no trigger_event was emitted
17
+ * (skipped due to automated sender, allowlist, subject filter, etc.).
18
+ *
19
+ * Retention: AIDEN_DAEMON_EMAIL_RETENTION_DAYS env (default 30).
20
+ * Sweep runs on boot + daily.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.createEmailSeenStore = createEmailSeenStore;
24
+ function rowToTs(r) {
25
+ return {
26
+ id: r.id,
27
+ routeId: r.route_id,
28
+ mailbox: r.mailbox,
29
+ uidValidity: r.uid_validity,
30
+ uid: r.uid,
31
+ messageId: r.message_id,
32
+ fromAddress: r.from_address,
33
+ subject: r.subject,
34
+ receivedAt: r.received_at,
35
+ processedAt: r.processed_at,
36
+ triggerEventId: r.trigger_event_id,
37
+ status: r.status,
38
+ };
39
+ }
40
+ function createEmailSeenStore(opts) {
41
+ const db = opts.db;
42
+ return {
43
+ record(input) {
44
+ const now = Date.now();
45
+ // INSERT OR IGNORE returns 0 rows changed on conflict; in that
46
+ // case we look up the existing row id.
47
+ const r = db
48
+ .prepare(`INSERT OR IGNORE INTO email_seen
49
+ (route_id, mailbox, uid_validity, uid, message_id,
50
+ from_address, subject, received_at, processed_at,
51
+ trigger_event_id, status)
52
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
53
+ .run(input.routeId, input.mailbox, input.uidValidity, input.uid, input.messageId, input.fromAddress, input.subject, input.receivedAt, now, input.triggerEventId, input.status);
54
+ if (r.changes > 0)
55
+ return Number(r.lastInsertRowid);
56
+ const existing = db
57
+ .prepare(`SELECT id FROM email_seen WHERE route_id = ? AND uid_validity = ? AND uid = ?`)
58
+ .get(input.routeId, input.uidValidity, input.uid);
59
+ if (!existing) {
60
+ throw new Error('emailSeenStore.record: INSERT OR IGNORE skipped + no existing row found');
61
+ }
62
+ return existing.id;
63
+ },
64
+ get(routeId, uidValidity, uid) {
65
+ const r = db
66
+ .prepare(`SELECT * FROM email_seen WHERE route_id = ? AND uid_validity = ? AND uid = ?`)
67
+ .get(routeId, uidValidity, uid);
68
+ return r ? rowToTs(r) : null;
69
+ },
70
+ list(routeId, limit = 100) {
71
+ const rows = db
72
+ .prepare(`SELECT * FROM email_seen WHERE route_id = ?
73
+ ORDER BY received_at DESC LIMIT ?`)
74
+ .all(routeId, limit);
75
+ return rows.map(rowToTs);
76
+ },
77
+ sweep(retentionDays, now) {
78
+ const cutoff = (now ?? Date.now()) - retentionDays * 24 * 60 * 60 * 1000;
79
+ const r = db
80
+ .prepare(`DELETE FROM email_seen WHERE received_at < ?`)
81
+ .run(cutoff);
82
+ return { deleted: r.changes };
83
+ },
84
+ countForRoute(routeId) {
85
+ const r = db
86
+ .prepare(`SELECT COUNT(*) AS c FROM email_seen WHERE route_id = ?`)
87
+ .get(routeId);
88
+ return r.c;
89
+ },
90
+ isMessageIdSeen(messageId) {
91
+ if (!messageId)
92
+ return false;
93
+ const r = db
94
+ .prepare(`SELECT 1 FROM email_seen WHERE message_id = ? LIMIT 1`)
95
+ .get(messageId);
96
+ return r !== undefined;
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/email/emailSpec.ts — v4.5 Phase 4a.
10
+ *
11
+ * Typed spec for source='email' triggers, stored in
12
+ * triggers.spec_json. Parse + validation + defaults.
13
+ *
14
+ * Required:
15
+ * - name
16
+ * - imap.{host, user, password}
17
+ * - allowedSenders (at least one entry) — Q-P4-1 (a) reject-all
18
+ * default; users opt in to specific senders. Glob-style patterns
19
+ * supported (see allowlist.ts).
20
+ *
21
+ * Password handling: stored RAW in spec_json. daemon.db is chmod 600
22
+ * on POSIX (see db/connection.ts since Phase 3). Encryption-at-rest
23
+ * deferred to v4.6+ per Q-P4-4 (a). aiden trigger show deliberately
24
+ * omits the password.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.DEFAULT_IMAP = exports.DEFAULT_EMAIL_SPEC = void 0;
28
+ exports.parseEmailSpec = parseEmailSpec;
29
+ exports.DEFAULT_EMAIL_SPEC = {
30
+ mailbox: 'INBOX',
31
+ pollIntervalMs: 15000,
32
+ maxBodyBytes: 1048576,
33
+ deliverOnly: false,
34
+ attachmentPolicy: 'skip',
35
+ };
36
+ exports.DEFAULT_IMAP = {
37
+ port: 993,
38
+ tls: true,
39
+ authTimeoutMs: 10000,
40
+ };
41
+ function parseEmailSpec(raw) {
42
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
43
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
44
+ throw new Error('EmailSpec: input must be an object');
45
+ }
46
+ const o = obj;
47
+ const name = typeof o.name === 'string' && o.name.length > 0 ? o.name : '';
48
+ if (!name)
49
+ throw new Error('EmailSpec: name required');
50
+ const imapRaw = (o.imap && typeof o.imap === 'object') ? o.imap : {};
51
+ const imap = {
52
+ host: requireStr(imapRaw.host, 'imap.host'),
53
+ user: requireStr(imapRaw.user, 'imap.user'),
54
+ password: requireStr(imapRaw.password, 'imap.password'),
55
+ port: sanitizeNum(imapRaw.port, exports.DEFAULT_IMAP.port, 1),
56
+ tls: typeof imapRaw.tls === 'boolean' ? imapRaw.tls : exports.DEFAULT_IMAP.tls,
57
+ authTimeoutMs: sanitizeNum(imapRaw.authTimeoutMs, exports.DEFAULT_IMAP.authTimeoutMs, 1000),
58
+ };
59
+ const allowedSenders = Array.isArray(o.allowedSenders)
60
+ ? o.allowedSenders.filter((s) => typeof s === 'string' && s.trim().length > 0)
61
+ : [];
62
+ if (allowedSenders.length === 0) {
63
+ throw new Error('EmailSpec: allowedSenders required (at least one --allow-sender entry). Use "*@your-domain.com" for a whole domain.');
64
+ }
65
+ const allowedSubjectPatterns = Array.isArray(o.allowedSubjectPatterns)
66
+ ? o.allowedSubjectPatterns.filter((s) => typeof s === 'string')
67
+ : undefined;
68
+ if (allowedSubjectPatterns) {
69
+ // Compile-test each pattern at parse time so bad regexes surface early.
70
+ for (const p of allowedSubjectPatterns) {
71
+ try {
72
+ new RegExp(p);
73
+ }
74
+ catch (e) {
75
+ throw new Error(`EmailSpec: allowedSubjectPatterns has invalid regex ${JSON.stringify(p)}: ${e instanceof Error ? e.message : String(e)}`);
76
+ }
77
+ }
78
+ }
79
+ return {
80
+ name,
81
+ imap,
82
+ mailbox: typeof o.mailbox === 'string' && o.mailbox.length > 0 ? o.mailbox : exports.DEFAULT_EMAIL_SPEC.mailbox,
83
+ pollIntervalMs: sanitizeNum(o.pollIntervalMs, exports.DEFAULT_EMAIL_SPEC.pollIntervalMs, 1000),
84
+ allowedSenders,
85
+ allowedSubjectPatterns,
86
+ maxBodyBytes: sanitizeNum(o.maxBodyBytes, exports.DEFAULT_EMAIL_SPEC.maxBodyBytes, 1024),
87
+ promptTemplate: typeof o.promptTemplate === 'string' ? o.promptTemplate : undefined,
88
+ deliverOnly: typeof o.deliverOnly === 'boolean' ? o.deliverOnly : exports.DEFAULT_EMAIL_SPEC.deliverOnly,
89
+ attachmentPolicy: sanitizeAttachmentPolicy(o.attachmentPolicy),
90
+ };
91
+ }
92
+ function requireStr(v, label) {
93
+ if (typeof v !== 'string' || v.length === 0) {
94
+ throw new Error(`EmailSpec: ${label} required (non-empty string)`);
95
+ }
96
+ return v;
97
+ }
98
+ function sanitizeNum(v, fallback, min) {
99
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < min)
100
+ return fallback;
101
+ return v;
102
+ }
103
+ function sanitizeAttachmentPolicy(v) {
104
+ if (v === 'skip' || v === 'inline-text' || v === 'save-to-tmp')
105
+ return v;
106
+ return exports.DEFAULT_EMAIL_SPEC.attachmentPolicy;
107
+ }