aiden-runtime 4.8.1 → 4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +37 -6
  3. package/dist/cli/v4/chatSession.js +53 -13
  4. package/dist/cli/v4/commands/daemon.js +53 -3
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +45 -26
  7. package/dist/cli/v4/commands/help.js +5 -0
  8. package/dist/cli/v4/commands/hooks.js +466 -0
  9. package/dist/cli/v4/commands/hooksSlash.js +33 -0
  10. package/dist/cli/v4/commands/index.js +13 -1
  11. package/dist/cli/v4/commands/mcp.js +89 -1
  12. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  13. package/dist/cli/v4/commands/memory.js +707 -0
  14. package/dist/cli/v4/commands/memorySlash.js +38 -0
  15. package/dist/cli/v4/commands/recovery.js +1 -1
  16. package/dist/cli/v4/commands/skin.js +7 -0
  17. package/dist/cli/v4/commands/theme.js +217 -0
  18. package/dist/cli/v4/commands/trigger.js +1 -1
  19. package/dist/cli/v4/design/tokens.js +52 -4
  20. package/dist/cli/v4/display.js +39 -26
  21. package/dist/cli/v4/replyRenderer.js +6 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/cli/v4/ui/progressBar.js +179 -0
  24. package/dist/cli/v4/util/closestAction.js +48 -0
  25. package/dist/core/v4/aidenAgent.js +45 -2
  26. package/dist/core/v4/daemon/api/runs.js +131 -0
  27. package/dist/core/v4/daemon/bootstrap.js +368 -13
  28. package/dist/core/v4/daemon/db/migrations.js +169 -0
  29. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  30. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  31. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  32. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  33. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  34. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  35. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  36. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  37. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  38. package/dist/core/v4/daemon/triggerBus.js +50 -19
  39. package/dist/core/v4/hooks/auditQuery.js +67 -0
  40. package/dist/core/v4/hooks/dispatcher.js +286 -0
  41. package/dist/core/v4/hooks/index.js +46 -0
  42. package/dist/core/v4/hooks/lifecycle.js +27 -0
  43. package/dist/core/v4/hooks/manifest.js +142 -0
  44. package/dist/core/v4/hooks/registry.js +149 -0
  45. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  46. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  47. package/dist/core/v4/hooks/trust.js +14 -0
  48. package/dist/core/v4/identity/contextManager.js +83 -0
  49. package/dist/core/v4/identity/daemonId.js +85 -0
  50. package/dist/core/v4/identity/enforcement.js +103 -0
  51. package/dist/core/v4/identity/executionContext.js +153 -0
  52. package/dist/core/v4/identity/hookExecution.js +62 -0
  53. package/dist/core/v4/identity/httpContext.js +68 -0
  54. package/dist/core/v4/identity/ids.js +185 -0
  55. package/dist/core/v4/identity/index.js +60 -0
  56. package/dist/core/v4/identity/subprocessContext.js +98 -0
  57. package/dist/core/v4/identity/traceparent.js +114 -0
  58. package/dist/core/v4/logger/index.js +3 -1
  59. package/dist/core/v4/logger/logger.js +28 -1
  60. package/dist/core/v4/logger/redact.js +149 -0
  61. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  62. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  63. package/dist/core/v4/mcp/install/backup.js +78 -0
  64. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  65. package/dist/core/v4/mcp/install/clients.js +203 -0
  66. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  67. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  68. package/dist/core/v4/mcp/install/profiles.js +109 -0
  69. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  70. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  71. package/dist/core/v4/memory/projectRoot.js +76 -0
  72. package/dist/core/v4/memory/reviewer/index.js +162 -0
  73. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  74. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  75. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  76. package/dist/core/v4/memoryManager.js +57 -10
  77. package/dist/core/v4/paths.js +2 -0
  78. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  79. package/dist/core/v4/theme/bundledThemes.js +106 -0
  80. package/dist/core/v4/theme/themeLoader.js +160 -0
  81. package/dist/core/v4/theme/themeRegistry.js +97 -0
  82. package/dist/core/v4/theme/themeWatcher.js +95 -0
  83. package/dist/core/v4/toolRegistry.js +71 -8
  84. package/dist/core/v4/update/depWarningFilter.js +76 -0
  85. package/dist/core/v4/update/executeInstall.js +41 -35
  86. package/dist/core/v4/update/platformInstructions.js +128 -0
  87. package/dist/moat/approvalEngine.js +4 -0
  88. package/dist/moat/memoryGuard.js +8 -1
  89. package/dist/providers/v4/anthropicAdapter.js +10 -4
  90. package/dist/tools/v4/backends/local.js +19 -2
  91. package/dist/tools/v4/sessions/recallSession.js +6 -1
  92. package/package.json +3 -1
  93. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  94. package/themes/default.yaml +52 -0
  95. package/themes/dracula.yaml +32 -0
  96. package/themes/light.yaml +32 -0
  97. package/themes/monochrome.yaml +31 -0
  98. package/themes/tokyo-night.yaml +32 -0
  99. package/dist/core/pluginSystem.js +0 -121
  100. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -0,0 +1,286 @@
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/hooks/dispatcher.ts — v4.9.0 Slice 12a.
10
+ *
11
+ * Given an event + payload + execution context, queries the DB for
12
+ * active subscriptions, runs each through the subprocess runner,
13
+ * aggregates decisions per the authority/mode model, and writes a
14
+ * `hook_executions` audit row for every firing.
15
+ *
16
+ * Aggregation rules:
17
+ * - If ANY `mandatory_policy` subscription's net outcome is `block`
18
+ * (including via on_error/on_timeout policy promotion), the
19
+ * overall dispatch returns `decision: 'block'`. Earliest blocker
20
+ * wins.
21
+ * - `transform_input` / `transform_output` patches apply in
22
+ * priority order (highest first); subsequent hooks see the
23
+ * patched payload.
24
+ * - Best-effort observers + advisory_policy hooks never block the
25
+ * overall flow — their outcomes only land in the audit table.
26
+ */
27
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ var desc = Object.getOwnPropertyDescriptor(m, k);
30
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
31
+ desc = { enumerable: true, get: function() { return m[k]; } };
32
+ }
33
+ Object.defineProperty(o, k2, desc);
34
+ }) : (function(o, m, k, k2) {
35
+ if (k2 === undefined) k2 = k;
36
+ o[k2] = m[k];
37
+ }));
38
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
39
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
40
+ }) : function(o, v) {
41
+ o["default"] = v;
42
+ });
43
+ var __importStar = (this && this.__importStar) || (function () {
44
+ var ownKeys = function(o) {
45
+ ownKeys = Object.getOwnPropertyNames || function (o) {
46
+ var ar = [];
47
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
48
+ return ar;
49
+ };
50
+ return ownKeys(o);
51
+ };
52
+ return function (mod) {
53
+ if (mod && mod.__esModule) return mod;
54
+ var result = {};
55
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
56
+ __setModuleDefault(result, mod);
57
+ return result;
58
+ };
59
+ })();
60
+ var __importDefault = (this && this.__importDefault) || function (mod) {
61
+ return (mod && mod.__esModule) ? mod : { "default": mod };
62
+ };
63
+ Object.defineProperty(exports, "__esModule", { value: true });
64
+ exports.runHookSubprocess = exports.CONSECUTIVE_FAILURE_THRESHOLD = void 0;
65
+ exports.setAutoDisableLogger = setAutoDisableLogger;
66
+ exports.dispatchHook = dispatchHook;
67
+ const node_path_1 = __importDefault(require("node:path"));
68
+ const identity_1 = require("../identity");
69
+ const subprocessRunner_1 = require("./runtime/subprocessRunner");
70
+ Object.defineProperty(exports, "runHookSubprocess", { enumerable: true, get: function () { return subprocessRunner_1.runHookSubprocess; } });
71
+ const trust_1 = require("./trust");
72
+ /**
73
+ * v4.9.0 Slice 12b — auto-disable threshold. After this many
74
+ * consecutive non-`ok` outcomes the dispatcher auto-revokes the
75
+ * hook regardless of `on_error` / `on_timeout` policy. Defense in
76
+ * depth: a poorly-configured `on_error: 'allow'` won't mask a
77
+ * permanently broken hook.
78
+ */
79
+ exports.CONSECUTIVE_FAILURE_THRESHOLD = 3;
80
+ let _autoDisableLogger = null;
81
+ function setAutoDisableLogger(fn) { _autoDisableLogger = fn; }
82
+ /**
83
+ * Look up the most recent N `hook_executions` rows for a hook so the
84
+ * `hook.auto_disabled` log line can cross-reference the failures.
85
+ */
86
+ function recentExecutionIds(db, hookId, n) {
87
+ const rows = db.prepare(`SELECT hook_execution_id FROM hook_executions WHERE hook_id = ?
88
+ ORDER BY started_at DESC LIMIT ?`).all(hookId, n);
89
+ return rows.map((r) => r.hook_execution_id);
90
+ }
91
+ /**
92
+ * Apply auto-disable policy after a single subscription firing.
93
+ * Branches:
94
+ * - status='ok' → reset consecutive_failures = 0
95
+ * - status in {timeout,crash,malformed} → increment counter
96
+ * * if `on_error|on_timeout == 'disable_hook'` → immediate revoke
97
+ * * else if counter >= threshold → 3-strike revoke
98
+ *
99
+ * `testMode=true` (used by `aiden hooks test`) skips ALL counter
100
+ * mutation and policy application — pure dry-run.
101
+ */
102
+ function applyAutoDisablePolicy(db, hookId, subId, status, policy, testMode) {
103
+ if (testMode)
104
+ return;
105
+ if (status === 'ok') {
106
+ db.prepare(`UPDATE hooks SET consecutive_failures=0, updated_at=? WHERE hook_id=?`)
107
+ .run(new Date().toISOString(), hookId);
108
+ return;
109
+ }
110
+ // Increment counter for any non-ok outcome.
111
+ db.prepare(`UPDATE hooks SET consecutive_failures = consecutive_failures + 1,
112
+ updated_at=? WHERE hook_id=?`)
113
+ .run(new Date().toISOString(), hookId);
114
+ // Immediate revoke when the subscription explicitly opts in.
115
+ if (policy === 'disable_hook') {
116
+ (0, trust_1.markRevoked)(db, hookId);
117
+ _autoDisableLogger?.({
118
+ hookId, subscriptionId: subId,
119
+ reason: `auto_disable:${status}`,
120
+ hookExecutionIds: recentExecutionIds(db, hookId, 3),
121
+ });
122
+ return;
123
+ }
124
+ // 3-strike defense in depth.
125
+ const row = db.prepare(`SELECT consecutive_failures AS n FROM hooks WHERE hook_id=?`)
126
+ .get(hookId);
127
+ if (row && row.n >= exports.CONSECUTIVE_FAILURE_THRESHOLD) {
128
+ (0, trust_1.markRevoked)(db, hookId);
129
+ _autoDisableLogger?.({
130
+ hookId, subscriptionId: subId,
131
+ reason: `auto_disable:three_strikes:${status}`,
132
+ hookExecutionIds: recentExecutionIds(db, hookId, 3),
133
+ });
134
+ }
135
+ }
136
+ /**
137
+ * Match a subscription against the dispatch context. Currently
138
+ * supports tool-name matching for `tool.call.*` events; other matcher
139
+ * shapes (paths, etc.) extend by adding branches here.
140
+ */
141
+ function matches(sub, ctx) {
142
+ if (!sub.matcher_json)
143
+ return true;
144
+ try {
145
+ const m = JSON.parse(sub.matcher_json);
146
+ if (m.tools && m.tools.length > 0) {
147
+ if (!ctx.toolName || !m.tools.includes(ctx.toolName))
148
+ return false;
149
+ }
150
+ return true;
151
+ }
152
+ catch {
153
+ return true;
154
+ } // malformed matcher → permissive
155
+ }
156
+ /**
157
+ * Read the hook's entrypoint argv from its manifest. We re-parse on
158
+ * each dispatch (sub-ms; the file is tiny) so a manifest edit doesn't
159
+ * require a daemon restart — only the entrypoint code change does,
160
+ * and that trips drift detection on the next scan.
161
+ */
162
+ async function readEntrypoint(manifestPath) {
163
+ try {
164
+ const { parseHookManifest } = await Promise.resolve().then(() => __importStar(require('./manifest')));
165
+ const m = await parseHookManifest(manifestPath);
166
+ return { argv: m.entrypoint.argv, cwd: m.manifestDir };
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ function writeAudit(db, row) {
173
+ db.prepare(`INSERT INTO hook_executions
174
+ (hook_execution_id, hook_id, subscription_id, event,
175
+ run_id, trace_id, span_id, parent_span_id, tool_call_id,
176
+ status, decision, elapsed_ms, exit_code,
177
+ payload_hash, response_hash, stdout_preview, stderr_preview,
178
+ error_kind, error_message, started_at, finished_at)
179
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.hookExecId, row.hookId, row.subscriptionId, row.event, row.ctx.runId ?? null, row.ctx.traceId ?? null, row.ctx.spanId ?? null, row.ctx.parentSpanId ?? null, row.ctx.toolCallId ?? null, row.status, row.decision, row.outcome?.elapsedMs ?? 0, row.outcome?.exitCode ?? null, row.outcome?.payloadHash ?? null, row.outcome?.responseHash ?? null, row.outcome?.stdoutPreview ?? null, row.outcome?.stderrPreview ?? null, row.outcome?.errorKind ?? null, row.outcome?.errorMessage ?? null, row.startedAt, row.finishedAt);
180
+ }
181
+ /**
182
+ * Dispatch all subscriptions matching `event` + `ctx`. Always
183
+ * resolves (never throws); decision aggregation is fail-closed for
184
+ * `mandatory_policy` and fail-open for everything else.
185
+ */
186
+ async function dispatchHook(db, event, payload, ctx) {
187
+ const subs = db.prepare(`SELECT s.*, h.manifest_path, h.trust_state, h.enabled AS hook_enabled, h.name AS name
188
+ FROM hook_subscriptions s
189
+ JOIN hooks h ON h.hook_id = s.hook_id
190
+ WHERE s.event = ? AND s.enabled = 1
191
+ AND h.enabled = 1 AND h.trust_state = 'trusted'
192
+ ORDER BY s.priority DESC, s.subscription_id ASC`).all(event);
193
+ const fired = [];
194
+ let workingPayload = { ...payload };
195
+ let blockReason;
196
+ let userMessage;
197
+ let modelMessage;
198
+ let blocked = false;
199
+ for (const sub of subs) {
200
+ if (!matches(sub, ctx))
201
+ continue;
202
+ const ep = await readEntrypoint(sub.manifest_path);
203
+ const startedAt = new Date().toISOString();
204
+ const hookExecId = (0, identity_1.newHookExecId)();
205
+ let outcome = null;
206
+ let policyDecision = 'allow';
207
+ if (!ep) {
208
+ // Couldn't read manifest → treat as crash, apply on_error.
209
+ outcome = {
210
+ status: 'crash', exitCode: null, elapsedMs: 0, payloadHash: '',
211
+ stdoutPreview: '', stderrPreview: '',
212
+ errorKind: 'ManifestReadFailed',
213
+ errorMessage: `could not re-read manifest at ${sub.manifest_path}`,
214
+ };
215
+ }
216
+ else {
217
+ outcome = await (0, subprocessRunner_1.runHookSubprocess)({
218
+ argv: ep.argv,
219
+ cwd: ep.cwd,
220
+ payload: { event, hook_id: sub.hook_id, subscription_id: sub.subscription_id,
221
+ run_id: ctx.runId, trace_id: ctx.traceId, parent_span_id: ctx.parentSpanId,
222
+ payload: workingPayload },
223
+ timeoutMs: sub.timeout_ms,
224
+ hookId: sub.hook_id,
225
+ event,
226
+ runId: ctx.runId,
227
+ traceId: ctx.traceId,
228
+ parentSpanId: ctx.parentSpanId,
229
+ });
230
+ }
231
+ // Map outcome → policy decision.
232
+ if (outcome.status === 'ok') {
233
+ const d = outcome.response?.decision ?? 'none';
234
+ if (d === 'block' && sub.authority === 'decision') {
235
+ policyDecision = 'block';
236
+ }
237
+ else {
238
+ policyDecision = 'allow';
239
+ }
240
+ // Apply transform_* patches (priority order is loop order).
241
+ if (outcome.response?.patch && (sub.authority === 'transform_input' || sub.authority === 'transform_output')) {
242
+ workingPayload = { ...workingPayload, ...outcome.response.patch };
243
+ }
244
+ }
245
+ else {
246
+ // Non-ok statuses route via on_error / on_timeout.
247
+ const policy = outcome.status === 'timeout' ? sub.on_timeout : sub.on_error;
248
+ if (policy === 'block' && sub.mode === 'mandatory_policy') {
249
+ policyDecision = 'block';
250
+ }
251
+ // For 'disable_hook' policy in 12a: we don't auto-disable here;
252
+ // we just treat as allow (12b's CLI surfaces the failure).
253
+ }
254
+ const finishedAt = new Date().toISOString();
255
+ writeAudit(db, {
256
+ hookExecId, hookId: sub.hook_id, subscriptionId: sub.subscription_id,
257
+ event, ctx, status: outcome.status, decision: outcome.response?.decision ?? policyDecision,
258
+ outcome, startedAt, finishedAt,
259
+ });
260
+ // v4.9.0 Slice 12b — auto-disable policy. `disable_hook` triggers
261
+ // immediate revoke; 3 consecutive failures triggers revoke
262
+ // regardless of policy (defense in depth).
263
+ const policy = outcome.status === 'timeout' ? sub.on_timeout : sub.on_error;
264
+ applyAutoDisablePolicy(db, sub.hook_id, sub.subscription_id, outcome.status, policy, ctx.testMode === true);
265
+ fired.push({
266
+ hookId: sub.hook_id, subscriptionId: sub.subscription_id,
267
+ status: outcome.status, decision: outcome.response?.decision ?? policyDecision,
268
+ elapsedMs: outcome.elapsedMs,
269
+ });
270
+ if (policyDecision === 'block' && !blocked) {
271
+ blocked = true;
272
+ blockReason = outcome.response?.reason ?? (outcome.errorMessage ?? 'blocked by hook');
273
+ userMessage = outcome.response?.user_message;
274
+ modelMessage = outcome.response?.model_message;
275
+ // For mandatory_policy block, we still run later hooks so they
276
+ // can record audit rows — but mark `decision='block'`.
277
+ }
278
+ }
279
+ return {
280
+ decision: blocked ? 'block' : 'allow',
281
+ reason: blockReason, user_message: userMessage, model_message: modelMessage,
282
+ payload: workingPayload, fired,
283
+ };
284
+ }
285
+ // Silences unused-import lint when no path matcher fires.
286
+ void node_path_1.default;
@@ -0,0 +1,46 @@
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/hooks/index.ts — v4.9.0 Slice 12a barrel.
10
+ *
11
+ * Public surface for the hook subsystem. Other v4 modules should
12
+ * import from here, not from the individual files, so the internal
13
+ * layout stays free to evolve.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.HookBlockedError = exports.runToolWithHooks = exports.countByStatus = exports.failureRates = exports.queryHookExecutions = exports.fireApprovalResponded = exports.fireApprovalRequested = exports.fireSessionEnd = exports.fireSessionStart = exports.CONSECUTIVE_FAILURE_THRESHOLD = exports.setAutoDisableLogger = exports.runHookSubprocess = exports.dispatchHook = exports.markUntrusted = exports.markRevoked = exports.markTrusted = exports.listHooks = exports.scanAndLoadHooks = exports.ON_ERROR_POLICIES = exports.MODES = exports.AUTHORITIES = exports.HOOK_EVENTS = exports.parseHookManifest = void 0;
17
+ var manifest_1 = require("./manifest");
18
+ Object.defineProperty(exports, "parseHookManifest", { enumerable: true, get: function () { return manifest_1.parseHookManifest; } });
19
+ Object.defineProperty(exports, "HOOK_EVENTS", { enumerable: true, get: function () { return manifest_1.HOOK_EVENTS; } });
20
+ Object.defineProperty(exports, "AUTHORITIES", { enumerable: true, get: function () { return manifest_1.AUTHORITIES; } });
21
+ Object.defineProperty(exports, "MODES", { enumerable: true, get: function () { return manifest_1.MODES; } });
22
+ Object.defineProperty(exports, "ON_ERROR_POLICIES", { enumerable: true, get: function () { return manifest_1.ON_ERROR_POLICIES; } });
23
+ var registry_1 = require("./registry");
24
+ Object.defineProperty(exports, "scanAndLoadHooks", { enumerable: true, get: function () { return registry_1.scanAndLoadHooks; } });
25
+ Object.defineProperty(exports, "listHooks", { enumerable: true, get: function () { return registry_1.listHooks; } });
26
+ var trust_1 = require("./trust");
27
+ Object.defineProperty(exports, "markTrusted", { enumerable: true, get: function () { return trust_1.markTrusted; } });
28
+ Object.defineProperty(exports, "markRevoked", { enumerable: true, get: function () { return trust_1.markRevoked; } });
29
+ Object.defineProperty(exports, "markUntrusted", { enumerable: true, get: function () { return trust_1.markUntrusted; } });
30
+ var dispatcher_1 = require("./dispatcher");
31
+ Object.defineProperty(exports, "dispatchHook", { enumerable: true, get: function () { return dispatcher_1.dispatchHook; } });
32
+ Object.defineProperty(exports, "runHookSubprocess", { enumerable: true, get: function () { return dispatcher_1.runHookSubprocess; } });
33
+ Object.defineProperty(exports, "setAutoDisableLogger", { enumerable: true, get: function () { return dispatcher_1.setAutoDisableLogger; } });
34
+ Object.defineProperty(exports, "CONSECUTIVE_FAILURE_THRESHOLD", { enumerable: true, get: function () { return dispatcher_1.CONSECUTIVE_FAILURE_THRESHOLD; } });
35
+ var lifecycle_1 = require("./lifecycle");
36
+ Object.defineProperty(exports, "fireSessionStart", { enumerable: true, get: function () { return lifecycle_1.fireSessionStart; } });
37
+ Object.defineProperty(exports, "fireSessionEnd", { enumerable: true, get: function () { return lifecycle_1.fireSessionEnd; } });
38
+ Object.defineProperty(exports, "fireApprovalRequested", { enumerable: true, get: function () { return lifecycle_1.fireApprovalRequested; } });
39
+ Object.defineProperty(exports, "fireApprovalResponded", { enumerable: true, get: function () { return lifecycle_1.fireApprovalResponded; } });
40
+ var auditQuery_1 = require("./auditQuery");
41
+ Object.defineProperty(exports, "queryHookExecutions", { enumerable: true, get: function () { return auditQuery_1.queryHookExecutions; } });
42
+ Object.defineProperty(exports, "failureRates", { enumerable: true, get: function () { return auditQuery_1.failureRates; } });
43
+ Object.defineProperty(exports, "countByStatus", { enumerable: true, get: function () { return auditQuery_1.countByStatus; } });
44
+ var toolHookGate_1 = require("./toolHookGate");
45
+ Object.defineProperty(exports, "runToolWithHooks", { enumerable: true, get: function () { return toolHookGate_1.runToolWithHooks; } });
46
+ Object.defineProperty(exports, "HookBlockedError", { enumerable: true, get: function () { return toolHookGate_1.HookBlockedError; } });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fireSessionStart = fireSessionStart;
4
+ exports.fireSessionEnd = fireSessionEnd;
5
+ exports.fireApprovalRequested = fireApprovalRequested;
6
+ exports.fireApprovalResponded = fireApprovalResponded;
7
+ const dispatcher_1 = require("./dispatcher");
8
+ async function safeFire(db, event, payload, ctx) {
9
+ if (!db)
10
+ return;
11
+ try {
12
+ await (0, dispatcher_1.dispatchHook)(db, event, payload, ctx);
13
+ }
14
+ catch { /* fail-open — lifecycle hooks never throw out */ }
15
+ }
16
+ async function fireSessionStart(db, payload, ctx = {}) {
17
+ return safeFire(db, 'session.start', payload, ctx);
18
+ }
19
+ async function fireSessionEnd(db, payload, ctx = {}) {
20
+ return safeFire(db, 'session.end', payload, ctx);
21
+ }
22
+ async function fireApprovalRequested(db, payload, ctx = {}) {
23
+ return safeFire(db, 'approval.requested', payload, ctx);
24
+ }
25
+ async function fireApprovalResponded(db, payload, ctx = {}) {
26
+ return safeFire(db, 'approval.responded', payload, ctx);
27
+ }
@@ -0,0 +1,142 @@
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/hooks/manifest.ts — v4.9.0 Slice 12a.
10
+ *
11
+ * Parse + validate HOOK.yaml manifests. Strict schema validation —
12
+ * malformed files are rejected with a precise error rather than
13
+ * silently ignored. Returns a typed `HookManifest` that downstream
14
+ * registry + runner code can rely on.
15
+ *
16
+ * Why strict: a manifest is privileged input (declares timeouts,
17
+ * authority, error policy). Soft-failing on bad fields lets a bad
18
+ * file silently bypass the policy the user intended.
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.ON_ERROR_POLICIES = exports.MODES = exports.AUTHORITIES = exports.HOOK_EVENTS = void 0;
25
+ exports.parseHookManifest = parseHookManifest;
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const js_yaml_1 = __importDefault(require("js-yaml"));
29
+ exports.HOOK_EVENTS = [
30
+ 'tool.call.pre',
31
+ 'tool.call.post',
32
+ 'session.start',
33
+ 'session.end',
34
+ 'approval.requested',
35
+ 'approval.responded',
36
+ ];
37
+ exports.AUTHORITIES = [
38
+ 'observe', 'decision', 'transform_input', 'transform_output',
39
+ ];
40
+ exports.MODES = [
41
+ 'best_effort_observer', 'advisory_policy', 'mandatory_policy',
42
+ ];
43
+ exports.ON_ERROR_POLICIES = ['allow', 'block', 'disable_hook'];
44
+ const ID_RE = /^[a-z0-9][a-z0-9_-]{1,63}$/;
45
+ function fail(p, msg) {
46
+ throw new Error(`HOOK.yaml at ${p}: ${msg}`);
47
+ }
48
+ /** Parse a HOOK.yaml file and return a validated manifest. */
49
+ async function parseHookManifest(manifestPath) {
50
+ const raw = await node_fs_1.promises.readFile(manifestPath, 'utf8');
51
+ let doc;
52
+ try {
53
+ doc = js_yaml_1.default.load(raw);
54
+ }
55
+ catch (e) {
56
+ fail(manifestPath, `invalid YAML: ${e instanceof Error ? e.message : String(e)}`);
57
+ }
58
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
59
+ fail(manifestPath, 'root must be a YAML mapping');
60
+ }
61
+ const m = doc;
62
+ // Required scalars.
63
+ const id = m.id;
64
+ if (typeof id !== 'string' || !ID_RE.test(id)) {
65
+ fail(manifestPath, '`id` must be 2-64 chars of [a-z0-9_-] starting with [a-z0-9]');
66
+ }
67
+ const name = m.name;
68
+ if (typeof name !== 'string' || name.length === 0)
69
+ fail(manifestPath, '`name` must be a non-empty string');
70
+ const runtime = m.runtime;
71
+ if (runtime !== 'subprocess')
72
+ fail(manifestPath, '`runtime` must be `subprocess` (v4.9.0 supports only subprocess)');
73
+ // entrypoint.argv
74
+ const ep = m.entrypoint;
75
+ if (!ep || typeof ep !== 'object' || !Array.isArray(ep.argv) || ep.argv.length === 0 ||
76
+ !ep.argv.every((a) => typeof a === 'string' && a.length > 0)) {
77
+ fail(manifestPath, '`entrypoint.argv` must be a non-empty array of non-empty strings');
78
+ }
79
+ // subscriptions[]
80
+ const subsRaw = m.subscriptions;
81
+ if (!Array.isArray(subsRaw) || subsRaw.length === 0)
82
+ fail(manifestPath, '`subscriptions` must be a non-empty array');
83
+ const subs = subsRaw.map((s, i) => {
84
+ if (!s || typeof s !== 'object' || Array.isArray(s))
85
+ fail(manifestPath, `subscriptions[${i}] must be a mapping`);
86
+ const sub = s;
87
+ if (!exports.HOOK_EVENTS.includes(sub.event)) {
88
+ fail(manifestPath, `subscriptions[${i}].event must be one of: ${exports.HOOK_EVENTS.join(', ')}`);
89
+ }
90
+ if (!exports.AUTHORITIES.includes(sub.authority)) {
91
+ fail(manifestPath, `subscriptions[${i}].authority must be one of: ${exports.AUTHORITIES.join(', ')}`);
92
+ }
93
+ if (!exports.MODES.includes(sub.mode)) {
94
+ fail(manifestPath, `subscriptions[${i}].mode must be one of: ${exports.MODES.join(', ')}`);
95
+ }
96
+ const tms = sub.timeout_ms;
97
+ if (typeof tms !== 'number' || !Number.isFinite(tms) || tms <= 0 || tms > 30000) {
98
+ fail(manifestPath, `subscriptions[${i}].timeout_ms must be a positive number <= 30000`);
99
+ }
100
+ if (!exports.ON_ERROR_POLICIES.includes(sub.on_error)) {
101
+ fail(manifestPath, `subscriptions[${i}].on_error must be one of: ${exports.ON_ERROR_POLICIES.join(', ')}`);
102
+ }
103
+ if (!exports.ON_ERROR_POLICIES.includes(sub.on_timeout)) {
104
+ fail(manifestPath, `subscriptions[${i}].on_timeout must be one of: ${exports.ON_ERROR_POLICIES.join(', ')}`);
105
+ }
106
+ const out = {
107
+ event: sub.event,
108
+ authority: sub.authority,
109
+ mode: sub.mode,
110
+ timeout_ms: tms,
111
+ on_error: sub.on_error,
112
+ on_timeout: sub.on_timeout,
113
+ };
114
+ if (typeof sub.priority === 'number' && Number.isFinite(sub.priority))
115
+ out.priority = sub.priority;
116
+ if (sub.matcher && typeof sub.matcher === 'object' && !Array.isArray(sub.matcher)) {
117
+ const mm = sub.matcher;
118
+ const matcher = {};
119
+ if (Array.isArray(mm.tools) && mm.tools.every((t) => typeof t === 'string'))
120
+ matcher.tools = mm.tools;
121
+ if (Array.isArray(mm.paths) && mm.paths.every((p2) => typeof p2 === 'string'))
122
+ matcher.paths = mm.paths;
123
+ out.matcher = matcher;
124
+ }
125
+ return out;
126
+ });
127
+ // capabilities (optional, warn-only in 12a — just shape-check).
128
+ let caps;
129
+ if (m.capabilities && typeof m.capabilities === 'object' && !Array.isArray(m.capabilities)) {
130
+ caps = m.capabilities;
131
+ }
132
+ return {
133
+ id, name,
134
+ version: typeof m.version === 'string' ? m.version : undefined,
135
+ runtime: 'subprocess',
136
+ entrypoint: { argv: ep.argv.slice() },
137
+ subscriptions: subs,
138
+ capabilities: caps,
139
+ manifestDir: node_path_1.default.dirname(manifestPath),
140
+ manifestPath,
141
+ };
142
+ }
@@ -0,0 +1,149 @@
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/hooks/registry.ts — v4.9.0 Slice 12a.
10
+ *
11
+ * Scans `~/.aiden/hooks/<name>/HOOK.yaml` (global) and
12
+ * `<projectRoot>/.aiden/hooks/<name>/HOOK.yaml` (project-scoped),
13
+ * parses each manifest, computes the entrypoint SHA256, and
14
+ * UPSERTs into the `hooks` + `hook_subscriptions` +
15
+ * `hook_capability_grants` tables.
16
+ *
17
+ * Drift detection: if a row already exists for the same
18
+ * `manifest_path` and the new `code_hash` differs from the
19
+ * stored one, the row is marked `trust_state='drifted'` and
20
+ * `enabled=0`. (Slice 12b's CLI surfaces these for explicit
21
+ * re-trust.) New entries land with `enabled=0` and
22
+ * `trust_state='untrusted'` — explicit user action to trust.
23
+ */
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.scanAndLoadHooks = scanAndLoadHooks;
29
+ exports.listHooks = listHooks;
30
+ const node_fs_1 = require("node:fs");
31
+ const node_crypto_1 = require("node:crypto");
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ const identity_1 = require("../identity");
34
+ const manifest_1 = require("./manifest");
35
+ async function sha256File(p) {
36
+ try {
37
+ const raw = await node_fs_1.promises.readFile(p);
38
+ return (0, node_crypto_1.createHash)('sha256').update(raw).digest('hex');
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ async function safeReaddir(dir) {
45
+ try {
46
+ return await node_fs_1.promises.readdir(dir);
47
+ }
48
+ catch (e) {
49
+ if (e.code === 'ENOENT')
50
+ return [];
51
+ throw e;
52
+ }
53
+ }
54
+ /**
55
+ * Scan global + (optional) project hook directories. Returns a counts
56
+ * summary. Per-manifest errors are accumulated, NOT thrown — a bad
57
+ * hook never blocks loading the rest.
58
+ */
59
+ async function scanAndLoadHooks(db, opts) {
60
+ const log = opts.log ?? (() => { });
61
+ const result = { loaded: 0, errored: 0, drifted: 0, errors: [] };
62
+ const sources = [
63
+ { dir: node_path_1.default.join(opts.aidenRoot, 'hooks'), source: 'global' },
64
+ ];
65
+ if (opts.projectRoot) {
66
+ sources.push({ dir: node_path_1.default.join(opts.projectRoot, '.aiden', 'hooks'), source: 'project' });
67
+ }
68
+ for (const src of sources) {
69
+ const entries = await safeReaddir(src.dir);
70
+ for (const entry of entries) {
71
+ const candidate = node_path_1.default.join(src.dir, entry, 'HOOK.yaml');
72
+ try {
73
+ await node_fs_1.promises.access(candidate);
74
+ }
75
+ catch {
76
+ continue; /* not a hook directory */
77
+ }
78
+ try {
79
+ const manifest = await (0, manifest_1.parseHookManifest)(candidate);
80
+ const entrypointAbs = node_path_1.default.resolve(manifest.manifestDir, manifest.entrypoint.argv[manifest.entrypoint.argv.length - 1]);
81
+ const codeHash = (await sha256File(entrypointAbs))
82
+ ?? (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(manifest.entrypoint.argv)).digest('hex');
83
+ const drifted = upsertHook(db, manifest, codeHash, src.source);
84
+ if (drifted)
85
+ result.drifted += 1;
86
+ result.loaded += 1;
87
+ log('info', `[hooks] loaded ${manifest.id} (${src.source})${drifted ? ' — DRIFTED, disabled' : ''}`);
88
+ }
89
+ catch (e) {
90
+ const msg = e instanceof Error ? e.message : String(e);
91
+ result.errored += 1;
92
+ result.errors.push({ path: candidate, message: msg });
93
+ log('warn', `[hooks] failed to load ${candidate}: ${msg}`);
94
+ }
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ /**
100
+ * UPSERT the hooks row + its subscriptions + capability grants.
101
+ * Returns true when an existing row was found with a different
102
+ * `code_hash` (drift case).
103
+ */
104
+ function upsertHook(db, m, codeHash, source) {
105
+ const now = new Date().toISOString();
106
+ // Look up existing row by manifest_path.
107
+ const existing = db.prepare(`SELECT * FROM hooks WHERE manifest_path = ?`).get(m.manifestPath);
108
+ let drifted = false;
109
+ let hookId;
110
+ if (existing) {
111
+ hookId = existing.hook_id;
112
+ if (existing.code_hash !== codeHash) {
113
+ drifted = true;
114
+ db.prepare(`UPDATE hooks SET name=?, version=?, source=?, runtime=?, code_hash=?,
115
+ trust_state='drifted', enabled=0, updated_at=?
116
+ WHERE hook_id = ?`).run(m.name, m.version ?? null, source, m.runtime, codeHash, now, hookId);
117
+ }
118
+ else {
119
+ db.prepare(`UPDATE hooks SET name=?, version=?, source=?, runtime=?, updated_at=? WHERE hook_id = ?`).run(m.name, m.version ?? null, source, m.runtime, now, hookId);
120
+ }
121
+ }
122
+ else {
123
+ hookId = (0, identity_1.newHookId)();
124
+ db.prepare(`INSERT INTO hooks
125
+ (hook_id, name, version, source, runtime, manifest_path, code_hash, enabled, trust_state, created_at, updated_at)
126
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'untrusted', ?, ?)`).run(hookId, m.name, m.version ?? null, source, m.runtime, m.manifestPath, codeHash, now, now);
127
+ }
128
+ // Replace subscriptions wholesale — simpler than diffing.
129
+ db.prepare(`DELETE FROM hook_subscriptions WHERE hook_id = ?`).run(hookId);
130
+ for (const s of m.subscriptions) {
131
+ db.prepare(`INSERT INTO hook_subscriptions
132
+ (subscription_id, hook_id, event, matcher_json, authority, mode, priority, timeout_ms, on_error, on_timeout, enabled)
133
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run((0, identity_1.newHookSubId)(), hookId, s.event, s.matcher ? JSON.stringify(s.matcher) : null, s.authority, s.mode, s.priority ?? 0, s.timeout_ms, s.on_error, s.on_timeout);
134
+ }
135
+ // Capability grants are warn-only — store but don't enforce.
136
+ db.prepare(`DELETE FROM hook_capability_grants WHERE hook_id = ?`).run(hookId);
137
+ if (m.capabilities) {
138
+ for (const [cap, scope] of Object.entries(m.capabilities)) {
139
+ db.prepare(`INSERT INTO hook_capability_grants
140
+ (grant_id, hook_id, capability, scope_json, granted_by, granted_at)
141
+ VALUES (?, ?, ?, ?, ?, ?)`).run((0, identity_1.newHookId)(), hookId, cap, JSON.stringify(scope), null, now);
142
+ }
143
+ }
144
+ return drifted;
145
+ }
146
+ /** Read all rows for diagnostic / dispatcher use. */
147
+ function listHooks(db) {
148
+ return db.prepare(`SELECT * FROM hooks ORDER BY name`).all();
149
+ }