aiden-runtime 4.8.1 → 4.9.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.
- package/README.md +88 -1
- package/dist/cli/v4/aidenCLI.js +35 -4
- package/dist/cli/v4/chatSession.js +34 -9
- package/dist/cli/v4/commands/daemon.js +47 -2
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +1 -1
- package/dist/cli/v4/commands/help.js +2 -0
- package/dist/cli/v4/commands/hooks.js +428 -0
- package/dist/cli/v4/commands/index.js +5 -1
- package/dist/cli/v4/commands/mcp.js +89 -1
- package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
- package/dist/cli/v4/commands/memory.js +702 -0
- package/dist/cli/v4/commands/recovery.js +1 -1
- package/dist/cli/v4/commands/skin.js +7 -0
- package/dist/cli/v4/commands/theme.js +217 -0
- package/dist/cli/v4/commands/trigger.js +1 -1
- package/dist/cli/v4/design/tokens.js +52 -4
- package/dist/cli/v4/display.js +39 -26
- package/dist/cli/v4/replyRenderer.js +6 -5
- package/dist/cli/v4/skinEngine.js +67 -0
- package/dist/core/v4/aidenAgent.js +45 -2
- package/dist/core/v4/daemon/api/runs.js +131 -0
- package/dist/core/v4/daemon/bootstrap.js +368 -13
- package/dist/core/v4/daemon/db/migrations.js +169 -0
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
- package/dist/core/v4/daemon/incarnationStore.js +47 -0
- package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
- package/dist/core/v4/daemon/runs/reclaim.js +88 -0
- package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
- package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
- package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
- package/dist/core/v4/daemon/spans/spanStore.js +113 -0
- package/dist/core/v4/daemon/triggerBus.js +50 -19
- package/dist/core/v4/hooks/auditQuery.js +67 -0
- package/dist/core/v4/hooks/dispatcher.js +286 -0
- package/dist/core/v4/hooks/index.js +46 -0
- package/dist/core/v4/hooks/lifecycle.js +27 -0
- package/dist/core/v4/hooks/manifest.js +142 -0
- package/dist/core/v4/hooks/registry.js +149 -0
- package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
- package/dist/core/v4/hooks/toolHookGate.js +76 -0
- package/dist/core/v4/hooks/trust.js +14 -0
- package/dist/core/v4/identity/contextManager.js +83 -0
- package/dist/core/v4/identity/daemonId.js +85 -0
- package/dist/core/v4/identity/enforcement.js +103 -0
- package/dist/core/v4/identity/executionContext.js +153 -0
- package/dist/core/v4/identity/hookExecution.js +62 -0
- package/dist/core/v4/identity/httpContext.js +68 -0
- package/dist/core/v4/identity/ids.js +185 -0
- package/dist/core/v4/identity/index.js +60 -0
- package/dist/core/v4/identity/subprocessContext.js +98 -0
- package/dist/core/v4/identity/traceparent.js +114 -0
- package/dist/core/v4/logger/index.js +3 -1
- package/dist/core/v4/logger/logger.js +28 -1
- package/dist/core/v4/logger/redact.js +149 -0
- package/dist/core/v4/logger/sinks/fileSink.js +13 -0
- package/dist/core/v4/logger/sinks/stdSink.js +19 -1
- package/dist/core/v4/mcp/install/backup.js +78 -0
- package/dist/core/v4/mcp/install/clientPaths.js +90 -0
- package/dist/core/v4/mcp/install/clients.js +203 -0
- package/dist/core/v4/mcp/install/healthCheck.js +83 -0
- package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
- package/dist/core/v4/mcp/install/profiles.js +109 -0
- package/dist/core/v4/mcp/install/wslDetect.js +62 -0
- package/dist/core/v4/memory/namespaceRegistry.js +117 -0
- package/dist/core/v4/memory/projectRoot.js +76 -0
- package/dist/core/v4/memory/reviewer/index.js +162 -0
- package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
- package/dist/core/v4/memory/reviewer/prompt.js +105 -0
- package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
- package/dist/core/v4/memoryManager.js +57 -10
- package/dist/core/v4/paths.js +2 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
- package/dist/core/v4/theme/bundledThemes.js +106 -0
- package/dist/core/v4/theme/themeLoader.js +160 -0
- package/dist/core/v4/theme/themeRegistry.js +97 -0
- package/dist/core/v4/theme/themeWatcher.js +95 -0
- package/dist/core/v4/toolRegistry.js +71 -8
- package/dist/moat/approvalEngine.js +4 -0
- package/dist/moat/memoryGuard.js +8 -1
- package/dist/providers/v4/anthropicAdapter.js +10 -4
- package/dist/tools/v4/backends/local.js +19 -2
- package/dist/tools/v4/sessions/recallSession.js +6 -1
- package/package.json +3 -1
- package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
- package/themes/default.yaml +52 -0
- package/themes/dracula.yaml +32 -0
- package/themes/light.yaml +32 -0
- package/themes/monochrome.yaml +31 -0
- package/themes/tokyo-night.yaml +32 -0
- package/dist/core/pluginSystem.js +0 -121
- 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
|
+
}
|