@inneranimalmedia/agentsam-sdk 2.6.8 → 2.6.10
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 +18 -0
- package/apps/README.md +25 -0
- package/apps/cad-creator/shared/cad/package.json +1 -1
- package/apps/client-cms-editor/backend/package.json +5 -7
- package/apps/client-cms-editor/frontend/package.json +5 -7
- package/apps/client-cms-editor/frontend/tsconfig.json +27 -8
- package/apps/client-cms-editor/package-lock.json +8 -8
- package/apps/client-cms-editor/package.json +1 -1
- package/apps/client-cms-editor/shared/cms/package.json +3 -5
- package/docs/LOCAL_ENGINE_COMPUTE_AUDIT_PLAN.md +299 -0
- package/docs/architecture/HOOKS-MODELS-SELF-HOSTING-PLAN.md +199 -0
- package/examples/hyperspace-demo/index.html +16 -0
- package/examples/hyperspace-demo/package.json +21 -0
- package/examples/hyperspace-demo/src/main.tsx +160 -0
- package/examples/hyperspace-demo/tsconfig.json +9 -0
- package/examples/hyperspace-demo/vite.config.ts +24 -0
- package/migrations/runtime/0005_agentsam_hooks.sql +87 -0
- package/package.json +37 -10
- package/packages/agentsam-brand/package.json +1 -1
- package/packages/agentsam-brand/src/core/v2/compile.js +1 -1
- package/packages/agentsam-brand/src/core/v2/schema.js +1 -1
- package/packages/agentsam-contracts/package.json +71 -20
- package/packages/agentsam-contracts/scripts/build.mjs +18 -0
- package/packages/agentsam-contracts/src/errors.ts +19 -10
- package/packages/agentsam-contracts/test/contracts.test.mjs +4 -1
- package/packages/agentsam-contracts/test/repository.test.mjs +4 -1
- package/packages/agentsam-contracts/tsconfig.build.json +10 -0
- package/packages/agentsam-errors/package.json +13 -4
- package/packages/agentsam-errors/protocol/agentsam.error.v2.schema.json +821 -0
- package/packages/agentsam-errors/protocol/error-catalog.json +3512 -0
- package/packages/agentsam-errors/src/envelope.js +63 -11
- package/packages/agentsam-errors/src/error.js +6 -0
- package/packages/agentsam-errors/src/fingerprint.js +9 -1
- package/packages/agentsam-errors/src/normalize.js +51 -6
- package/packages/agentsam-errors/src/recovery.js +9 -35
- package/packages/agentsam-errors/src/transport.js +6 -1
- package/packages/agentsam-errors/src/vocabulary.js +515 -126
- package/packages/agentsam-errors/test/catalog-v2.test.mjs +106 -0
- package/packages/agentsam-errors/types/index.d.ts +157 -0
- package/packages/agentsam-goap/package.json +2 -2
- package/packages/agentsam-hooks/README.md +206 -0
- package/packages/agentsam-hooks/bin/agentsam-hooks.mjs +64 -0
- package/packages/agentsam-hooks/bindings/go/command.go +55 -0
- package/packages/agentsam-hooks/bindings/go/errors_generated.go +402 -0
- package/packages/agentsam-hooks/bindings/go/go.mod +3 -0
- package/packages/agentsam-hooks/bindings/go/hooks.go +285 -0
- package/packages/agentsam-hooks/bindings/go/hooks_test.go +48 -0
- package/packages/agentsam-hooks/bindings/rust/Cargo.toml +11 -0
- package/packages/agentsam-hooks/bindings/rust/src/errors_generated.rs +396 -0
- package/packages/agentsam-hooks/bindings/rust/src/lib.rs +397 -0
- package/packages/agentsam-hooks/package.json +69 -0
- package/packages/agentsam-hooks/schema/manifest.json +54 -0
- package/packages/agentsam-hooks/scripts/clean-protocol.mjs +17 -0
- package/packages/agentsam-hooks/scripts/sync-protocol.mjs +27 -0
- package/packages/agentsam-hooks/src/adapters/agents.js +82 -0
- package/packages/agentsam-hooks/src/adapters/callback.js +4 -0
- package/packages/agentsam-hooks/src/adapters/capability.js +122 -0
- package/packages/agentsam-hooks/src/adapters/command.js +104 -0
- package/packages/agentsam-hooks/src/adapters/composite.js +31 -0
- package/packages/agentsam-hooks/src/adapters/http.js +58 -0
- package/packages/agentsam-hooks/src/adapters/lsp.js +116 -0
- package/packages/agentsam-hooks/src/adapters/mcp.js +76 -0
- package/packages/agentsam-hooks/src/adapters/provider.js +129 -0
- package/packages/agentsam-hooks/src/config.js +143 -0
- package/packages/agentsam-hooks/src/contracts.js +284 -0
- package/packages/agentsam-hooks/src/errors-generated.js +2732 -0
- package/packages/agentsam-hooks/src/index.d.ts +193 -0
- package/packages/agentsam-hooks/src/index.js +14 -0
- package/packages/agentsam-hooks/src/policy.js +34 -0
- package/packages/agentsam-hooks/src/runtime.js +312 -0
- package/packages/agentsam-hooks/src/store.js +264 -0
- package/packages/agentsam-hooks/test/adapters.test.mjs +216 -0
- package/packages/agentsam-hooks/test/runtime.test.mjs +258 -0
- package/packages/agentsam-knowledge/package.json +3 -2
- package/packages/agentsam-nav/package.json +1 -1
- package/packages/agentsam-queue-control/package.json +1 -1
- package/packages/agentsam-rapid-rust/src/main.rs +1 -0
- package/packages/agentsam-repository/package.json +3 -2
- package/packages/agentsam-vault/package.json +8 -5
- package/packages/agentsam-workbench/package.json +45 -18
- package/packages/connectors/cloudflare/package.json +1 -1
- package/packages/heuristic-theme/package.json +1 -1
- package/packages/identity/package.json +3 -2
- package/packages/identity/tests/lane1-packed-consumer.test.mjs +9 -1
- package/packages/providers/completeful/package.json +1 -1
- package/packages/theme-church-site/README.md +2 -2
- package/packages/theme-church-site/package.json +2 -3
- package/packages/theme-church-site/src/index.js +1 -1
- package/packages/theme-church-site/test/theme.test.mjs +1 -1
- package/packages/theme-companions-site/README.md +2 -2
- package/packages/theme-companions-site/package.json +2 -3
- package/packages/theme-companions-site/src/index.js +1 -1
- package/packages/theme-companions-site/test/theme.test.mjs +1 -1
- package/packages/theme-floors-site/README.md +2 -2
- package/packages/theme-floors-site/package.json +2 -3
- package/packages/theme-floors-site/src/index.js +1 -1
- package/packages/theme-floors-site/test/theme.test.mjs +1 -1
- package/packages/theme-fuelnfree-site/README.md +2 -2
- package/packages/theme-fuelnfree-site/package.json +2 -3
- package/packages/theme-fuelnfree-site/src/index.js +1 -1
- package/packages/theme-fuelnfree-site/test/theme.test.mjs +1 -1
- package/packages/theme-handyman-site/README.md +2 -2
- package/packages/theme-handyman-site/package.json +2 -3
- package/packages/theme-handyman-site/src/index.js +1 -1
- package/packages/theme-handyman-site/test/theme.test.mjs +1 -1
- package/packages/theme-insurance-site/README.md +2 -2
- package/packages/theme-insurance-site/package.json +2 -3
- package/packages/theme-insurance-site/src/index.js +1 -1
- package/packages/theme-insurance-site/test/theme.test.mjs +1 -1
- package/packages/theme-scenes/package.json +1 -1
- package/packages/theme-shinshu-site/README.md +2 -2
- package/packages/theme-shinshu-site/package.json +2 -3
- package/packages/theme-shinshu-site/src/index.js +1 -1
- package/packages/theme-shinshu-site/test/theme.test.mjs +1 -1
- package/packages/work-graph/package.json +3 -2
- package/protocol/apps/agentsam.app.v1.schema.json +1 -1
- package/protocol/apps/agentsam.host-install.v1.schema.json +1 -1
- package/protocol/assets/asset-record.v1.schema.json +1 -1
- package/protocol/brand/brandpack.v1.schema.json +1 -1
- package/protocol/capabilities/capability-manifest.schema.json +1 -1
- package/protocol/capabilities/knowledge-search-input.schema.json +1 -0
- package/protocol/credentials/issue.v1.schema.json +1 -1
- package/protocol/database/agentsam.schema-pack.v1.schema.json +1 -1
- package/protocol/database/connection.v1.schema.json +1 -1
- package/protocol/embeddings/embedding-profile.v1.schema.json +1 -1
- package/protocol/errors/README.md +36 -0
- package/protocol/errors/agentsam.error.v2.schema.json +821 -0
- package/protocol/errors/error-catalog.json +3506 -158
- package/protocol/errors/error-envelope.schema.json +195 -52
- package/protocol/errors/recovery.v1.schema.json +1 -1
- package/protocol/features/agentsam.feature.v1.schema.json +1 -1
- package/protocol/hooks/README.md +18 -0
- package/protocol/hooks/agentsam.hook.output.v1.schema.json +27 -0
- package/protocol/hooks/agentsam.hook.receipt.v1.schema.json +61 -0
- package/protocol/hooks/agentsam.hook.v1.schema.json +35 -0
- package/protocol/hooks/agentsam.hooks.config.v1.schema.json +78 -0
- package/protocol/local-engine/benchmark.v1.schema.json +14 -0
- package/protocol/local-engine/engine.v1.schema.json +17 -0
- package/protocol/models/model-inventory-v2.schema.json +1 -1
- package/protocol/models/provider-model-snapshot.v1.schema.json +22 -0
- package/protocol/rpc/v1/errors.proto +1 -0
- package/protocol/runtime/agentsam.runtime.v1.schema.json +1 -1
- package/protocol/runtime/workspace-fs.v1.schema.json +1 -1
- package/protocol/sam/activity.v1.schema.json +1 -1
- package/protocol/sam/answer.v1.schema.json +1 -1
- package/protocol/sam/calibration.v1.schema.json +1 -1
- package/protocol/sam/decision-receipt.v1.schema.json +1 -1
- package/protocol/sam/evaluation.v1.schema.json +1 -1
- package/protocol/sam/operation.schema.json +1 -1
- package/protocol/sam/outcome.v1.schema.json +1 -1
- package/protocol/sam/question.v1.schema.json +1 -1
- package/protocol/sam/result.schema.json +1 -1
- package/protocol/sam/state.v1.schema.json +1 -1
- package/protocol/scoring/scorecard.v1.schema.json +1 -1
- package/protocol/skills/agentsam.interaction.v1.schema.json +1 -1
- package/protocol/skills/agentsam.skill.v1.schema.json +1 -1
- package/protocol/theme-refinery/product-metadata.schema.json +1 -1
- package/protocol/theme-refinery/reconciliation-receipt.schema.json +1 -1
- package/protocol/ui/icon.v1.schema.json +1 -1
- package/python/README.md +20 -0
- package/python/agentsam_sdk/errors/__init__.py +4 -0
- package/python/agentsam_sdk/errors/vocabulary.py +2730 -0
- package/python/agentsam_sdk/hooks/__init__.py +32 -0
- package/python/agentsam_sdk/hooks/adapters.py +132 -0
- package/python/agentsam_sdk/hooks/contracts.py +121 -0
- package/python/agentsam_sdk/hooks/runtime.py +253 -0
- package/python/tests/test_hooks.py +115 -0
- package/src/agent/responses-runner.js +145 -1
- package/src/cli/command-catalog.js +2 -0
- package/src/cli.js +19 -0
- package/src/commands/app.js +10 -0
- package/src/commands/cheat-sheet.js +1 -0
- package/src/commands/engine.js +10 -0
- package/src/commands/hooks.js +260 -0
- package/src/commands/machine.js +71 -38
- package/src/commands/models.js +1 -1
- package/src/commands/package.js +10 -3
- package/src/commands/runtime.js +25 -10
- package/src/commands/setup.js +30 -6
- package/src/commands/shell.js +114 -9
- package/src/commands/terminal.js +20 -8
- package/src/errors/contract.js +4 -4
- package/src/errors/diagnostic.js +10 -10
- package/src/errors/index.js +1 -1
- package/src/index.js +52 -0
- package/src/indexing/ingest/materials.js +237 -30
- package/src/knowledge/service/grpc-codec.js +2 -0
- package/src/lib/setup/runtime.js +4 -2
- package/src/lib/slash-commands.js +1 -0
- package/src/local/runtime-store.js +46 -0
- package/src/local-engine/assets.js +37 -0
- package/src/local-engine/audit.js +133 -0
- package/src/local-engine/benchmark.js +26 -0
- package/src/local-engine/contracts.js +101 -0
- package/src/local-engine/engines.js +142 -0
- package/src/local-engine/index.js +18 -0
- package/src/local-engine/runtime.js +39 -0
- package/src/models/catalog.js +0 -49
- package/src/models/inventory-core.js +69 -0
- package/src/plugins/index.js +8 -1
- package/src/plugins/registry.js +56 -0
- package/src/rpc/generated/errors_pb.js +49 -1
- package/src/tools/runtime.js +13 -5
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { matchesHookInput, normalizeHookEvent } from './contracts.js';
|
|
2
|
+
|
|
3
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
4
|
+
function json(value, fallback) {
|
|
5
|
+
if (value && typeof value === 'object') return structuredClone(value);
|
|
6
|
+
try { return JSON.parse(String(value || '')); } catch { return structuredClone(fallback); }
|
|
7
|
+
}
|
|
8
|
+
function jsonText(value, fallback) { return JSON.stringify(value == null ? fallback : value); }
|
|
9
|
+
function id(prefix) {
|
|
10
|
+
const uuid = globalThis.crypto?.randomUUID?.() || `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
11
|
+
return `${prefix}_${uuid.replaceAll('-', '').slice(0, 24)}`;
|
|
12
|
+
}
|
|
13
|
+
async function rows(statement) {
|
|
14
|
+
const result = await statement.all();
|
|
15
|
+
return Array.isArray(result) ? result : result?.results || [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeStoredHook(row = {}) {
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
id: clean(row.id),
|
|
21
|
+
owner_id: clean(row.owner_id) || 'local',
|
|
22
|
+
hook_key: clean(row.hook_key),
|
|
23
|
+
event_type: normalizeHookEvent(row.event_type),
|
|
24
|
+
source_kind: clean(row.source_kind) || 'stored',
|
|
25
|
+
scope_type: clean(row.scope_type) || 'project',
|
|
26
|
+
scope_ref: clean(row.scope_ref) || null,
|
|
27
|
+
handler_type: clean(row.handler_type) || 'log_only',
|
|
28
|
+
handler_config: json(row.handler_config_json ?? row.handler_config, {}),
|
|
29
|
+
match: json(row.match_json ?? row.match, {}),
|
|
30
|
+
failure_mode: clean(row.failure_mode) || 'open',
|
|
31
|
+
priority: Number(row.priority ?? 100),
|
|
32
|
+
timeout_ms: Number(row.timeout_ms ?? 30_000),
|
|
33
|
+
is_active: row.is_active == null ? true : Boolean(Number(row.is_active)),
|
|
34
|
+
workflow_id: clean(row.workflow_id) || null,
|
|
35
|
+
description: clean(row.description) || null,
|
|
36
|
+
metadata: json(row.metadata_json ?? row.metadata, {}),
|
|
37
|
+
revision: Number(row.revision ?? 1),
|
|
38
|
+
created_at_unix: Number(row.created_at_unix || 0) || null,
|
|
39
|
+
updated_at_unix: Number(row.updated_at_unix || 0) || null,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function scopeMatches(hook, context = {}) {
|
|
44
|
+
if (!hook.scope_ref || hook.scope_type === 'global') return true;
|
|
45
|
+
const expected = hook.scope_ref;
|
|
46
|
+
if (hook.scope_type === 'account') return expected === clean(context.owner_id || context.account_id);
|
|
47
|
+
if (hook.scope_type === 'repository') return expected === clean(context.repository_id);
|
|
48
|
+
if (hook.scope_type === 'project') return [context.project_id, context.project_root].map(clean).includes(expected);
|
|
49
|
+
if (hook.scope_type === 'session') return expected === clean(context.session_id);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Storage-agnostic hook definition and receipt store. `db` may be Cloudflare
|
|
55
|
+
* D1 or AgentSam's injected SQLite adapter; schema installation stays an
|
|
56
|
+
* explicit schema-pack/migration concern.
|
|
57
|
+
*/
|
|
58
|
+
export function createHookStore(db, options = {}) {
|
|
59
|
+
if (!db?.prepare) throw new TypeError('hook_store_database_required');
|
|
60
|
+
const ownerId = clean(options.ownerId) || 'local';
|
|
61
|
+
|
|
62
|
+
async function listHooks(query = {}) {
|
|
63
|
+
const requestedOwner = clean(query.owner_id) || ownerId;
|
|
64
|
+
const clauses = ['owner_id = ?'];
|
|
65
|
+
const values = [requestedOwner];
|
|
66
|
+
if (query.event_type) { clauses.push('event_type = ?'); values.push(normalizeHookEvent(query.event_type)); }
|
|
67
|
+
if (query.activeOnly !== false) clauses.push('is_active = 1');
|
|
68
|
+
const found = await rows(db.prepare(`
|
|
69
|
+
SELECT * FROM agentsam_hook
|
|
70
|
+
WHERE ${clauses.join(' AND ')}
|
|
71
|
+
ORDER BY event_type, priority, hook_key
|
|
72
|
+
`).bind(...values));
|
|
73
|
+
const normalized = found.map(normalizeStoredHook);
|
|
74
|
+
return Object.freeze(query.context ? normalized.filter((row) => scopeMatches(row, query.context)) : normalized);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getHook(hookId) {
|
|
78
|
+
const row = await db.prepare('SELECT * FROM agentsam_hook WHERE owner_id = ? AND (id = ? OR hook_key = ?) LIMIT 1')
|
|
79
|
+
.bind(ownerId, clean(hookId), clean(hookId)).first();
|
|
80
|
+
return row ? normalizeStoredHook(row) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function upsertHook(value = {}) {
|
|
84
|
+
const hookKey = clean(value.hook_key || value.hookKey || value.id);
|
|
85
|
+
if (!hookKey) throw new TypeError('hook_key_required');
|
|
86
|
+
const eventType = normalizeHookEvent(value.event_type || value.eventType || value.hook);
|
|
87
|
+
const scopeType = clean(value.scope_type || value.scopeType) || 'project';
|
|
88
|
+
const scopeRef = clean(value.scope_ref || value.scopeRef) || null;
|
|
89
|
+
const existing = await db.prepare(`
|
|
90
|
+
SELECT id, revision FROM agentsam_hook
|
|
91
|
+
WHERE owner_id = ? AND scope_type = ? AND IFNULL(scope_ref, '') = IFNULL(?, '') AND hook_key = ?
|
|
92
|
+
LIMIT 1
|
|
93
|
+
`).bind(ownerId, scopeType, scopeRef, hookKey).first();
|
|
94
|
+
const hookId = clean(value.id) || clean(existing?.id) || id('hook');
|
|
95
|
+
const sourceKind = clean(value.source_kind || value.sourceKind) || 'stored';
|
|
96
|
+
const handlerType = clean(value.handler_type || value.handlerType) || 'log_only';
|
|
97
|
+
const failureMode = clean(value.failure_mode || value.failureMode) || 'open';
|
|
98
|
+
const priority = Number(value.priority ?? 100);
|
|
99
|
+
const timeoutMs = Number(value.timeout_ms ?? value.timeoutMs ?? 30_000);
|
|
100
|
+
const active = value.is_active ?? value.enabled ?? true;
|
|
101
|
+
const handlerConfig = value.handler_config ?? value.handlerConfig ?? {};
|
|
102
|
+
const match = value.match ?? value.match_json ?? {};
|
|
103
|
+
const metadata = value.metadata ?? {};
|
|
104
|
+
const workflowId = clean(value.workflow_id || value.workflowId) || null;
|
|
105
|
+
const description = clean(value.description) || null;
|
|
106
|
+
|
|
107
|
+
if (existing) {
|
|
108
|
+
await db.prepare(`
|
|
109
|
+
UPDATE agentsam_hook SET
|
|
110
|
+
event_type = ?, source_kind = ?, handler_type = ?, handler_config_json = ?, match_json = ?,
|
|
111
|
+
failure_mode = ?, priority = ?, timeout_ms = ?, is_active = ?, workflow_id = ?, description = ?,
|
|
112
|
+
metadata_json = ?, revision = revision + 1, updated_at_unix = unixepoch()
|
|
113
|
+
WHERE id = ? AND owner_id = ?
|
|
114
|
+
`).bind(eventType, sourceKind, handlerType, jsonText(handlerConfig, {}), jsonText(match, {}), failureMode,
|
|
115
|
+
priority, timeoutMs, active ? 1 : 0, workflowId, description, jsonText(metadata, {}), existing.id, ownerId).run();
|
|
116
|
+
} else {
|
|
117
|
+
await db.prepare(`
|
|
118
|
+
INSERT INTO agentsam_hook (
|
|
119
|
+
id, owner_id, hook_key, event_type, source_kind, scope_type, scope_ref,
|
|
120
|
+
handler_type, handler_config_json, match_json, failure_mode, priority,
|
|
121
|
+
timeout_ms, is_active, workflow_id, description, metadata_json
|
|
122
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
123
|
+
`).bind(hookId, ownerId, hookKey, eventType, sourceKind, scopeType, scopeRef,
|
|
124
|
+
handlerType, jsonText(handlerConfig, {}), jsonText(match, {}), failureMode, priority,
|
|
125
|
+
timeoutMs, active ? 1 : 0, workflowId, description, jsonText(metadata, {})).run();
|
|
126
|
+
}
|
|
127
|
+
return getHook(hookId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function setHookActive(hookId, active) {
|
|
131
|
+
const result = await db.prepare(`
|
|
132
|
+
UPDATE agentsam_hook SET is_active = ?, revision = revision + 1, updated_at_unix = unixepoch()
|
|
133
|
+
WHERE owner_id = ? AND (id = ? OR hook_key = ?)
|
|
134
|
+
`).bind(active ? 1 : 0, ownerId, clean(hookId), clean(hookId)).run();
|
|
135
|
+
return Number(result?.changes || result?.meta?.changes || 0) > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function removeHook(hookId) {
|
|
139
|
+
const result = await db.prepare('DELETE FROM agentsam_hook WHERE owner_id = ? AND (id = ? OR hook_key = ?)')
|
|
140
|
+
.bind(ownerId, clean(hookId), clean(hookId)).run();
|
|
141
|
+
return Number(result?.changes || result?.meta?.changes || 0) > 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function recordExecution(receipt = {}, correlation = {}) {
|
|
145
|
+
const invocation = receipt.invocation || {};
|
|
146
|
+
const safeInvocation = Object.fromEntries(
|
|
147
|
+
['session_id', 'run_id', 'turn_id', 'message_id', 'agent_id', 'parent_agent_id', 'source']
|
|
148
|
+
.map((key) => [key, clean(invocation[key])])
|
|
149
|
+
.filter(([, value]) => value),
|
|
150
|
+
);
|
|
151
|
+
const safeCorrelation = Object.fromEntries(
|
|
152
|
+
['trace_id', 'request_id', 'workflow_id', 'plan_id', 'todo_id', 'project_id', 'repository_id', 'parent_execution_id']
|
|
153
|
+
.map((key) => [key, clean(correlation.context?.[key])])
|
|
154
|
+
.filter(([, value]) => value),
|
|
155
|
+
);
|
|
156
|
+
const hookKey = clean(receipt.hook_id || correlation.hook_key);
|
|
157
|
+
if (!hookKey) throw new TypeError('hook_receipt_hook_id_required');
|
|
158
|
+
const error = receipt.error || {};
|
|
159
|
+
const sourceKind = clean(correlation.source_kind || invocation.metadata?.hook_source) || 'config';
|
|
160
|
+
const status = receipt.status === 'completed' ? 'completed'
|
|
161
|
+
: error.error_code === 'DEADLINE_EXCEEDED' || error.failure_class === 'timeout' ? 'timeout' : 'failed';
|
|
162
|
+
const ranAt = Number(receipt.completed_at || receipt.started_at || Date.now());
|
|
163
|
+
const ranAtUnix = ranAt > 10_000_000_000 ? Math.floor(ranAt / 1000) : Math.floor(ranAt);
|
|
164
|
+
const executionId = clean(correlation.id) || id('hexec');
|
|
165
|
+
const safeReceipt = {
|
|
166
|
+
schema: receipt.schema,
|
|
167
|
+
hook_id: hookKey,
|
|
168
|
+
hook: receipt.hook,
|
|
169
|
+
invocation: safeInvocation,
|
|
170
|
+
status: receipt.status,
|
|
171
|
+
started_at: receipt.started_at,
|
|
172
|
+
completed_at: receipt.completed_at,
|
|
173
|
+
duration_ms: receipt.duration_ms,
|
|
174
|
+
input_keys: receipt.input_keys || [],
|
|
175
|
+
output_keys: receipt.output_keys || [],
|
|
176
|
+
...(receipt.error ? { error: receipt.error } : {}),
|
|
177
|
+
...(receipt.native_evidence ? { native_evidence: receipt.native_evidence } : {}),
|
|
178
|
+
};
|
|
179
|
+
await db.prepare(`
|
|
180
|
+
INSERT INTO agentsam_hook_execution (
|
|
181
|
+
id, owner_id, hook_id, hook_key, source_kind, event_type, invocation_id,
|
|
182
|
+
agent_run_id, session_id, conversation_id, status, duration_ms,
|
|
183
|
+
input_keys_json, output_keys_json, decision, reason, error_code,
|
|
184
|
+
error_message, receipt_json, correlation_json, ran_at_unix
|
|
185
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
186
|
+
`).bind(
|
|
187
|
+
executionId, ownerId, clean(correlation.hook_id) || null, hookKey, sourceKind,
|
|
188
|
+
normalizeHookEvent(receipt.hook), clean(correlation.invocation_id) || null,
|
|
189
|
+
clean(invocation.run_id || correlation.agent_run_id) || null,
|
|
190
|
+
clean(invocation.session_id || correlation.session_id) || null,
|
|
191
|
+
clean(correlation.conversation_id) || null, status, Math.max(0, Number(receipt.duration_ms || 0)),
|
|
192
|
+
jsonText(receipt.input_keys, []), jsonText(receipt.output_keys, []),
|
|
193
|
+
clean(correlation.decision) || null, clean(error.reason || correlation.reason) || null,
|
|
194
|
+
clean(error.error_code) || null, clean(error.message).slice(0, 512) || null,
|
|
195
|
+
jsonText(safeReceipt, {}), jsonText(safeCorrelation, {}), ranAtUnix,
|
|
196
|
+
).run();
|
|
197
|
+
return executionId;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function listExecutions(query = {}) {
|
|
201
|
+
const limit = Math.max(1, Math.min(500, Number(query.limit || 50)));
|
|
202
|
+
const clauses = ['owner_id = ?'];
|
|
203
|
+
const values = [clean(query.owner_id) || ownerId];
|
|
204
|
+
if (query.hook_key) { clauses.push('hook_key = ?'); values.push(clean(query.hook_key)); }
|
|
205
|
+
if (query.status) { clauses.push('status = ?'); values.push(clean(query.status)); }
|
|
206
|
+
values.push(limit);
|
|
207
|
+
const found = await rows(db.prepare(`
|
|
208
|
+
SELECT * FROM agentsam_hook_execution
|
|
209
|
+
WHERE ${clauses.join(' AND ')}
|
|
210
|
+
ORDER BY ran_at_unix DESC, id DESC LIMIT ?
|
|
211
|
+
`).bind(...values));
|
|
212
|
+
return Object.freeze(found.map((row) => Object.freeze({
|
|
213
|
+
...row,
|
|
214
|
+
input_keys: json(row.input_keys_json, []),
|
|
215
|
+
output_keys: json(row.output_keys_json, []),
|
|
216
|
+
receipt: json(row.receipt_json, {}),
|
|
217
|
+
correlation: json(row.correlation_json, {}),
|
|
218
|
+
})));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Object.freeze({ ownerId, listHooks, getHook, upsertHook, setHookActive, removeHook, recordExecution, listExecutions });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Register active stored rows into the same runtime used by code/config hooks. */
|
|
225
|
+
export async function registerStoredHooks(runtime, store, options = {}) {
|
|
226
|
+
if (!runtime?.register) throw new TypeError('hook_runtime_required');
|
|
227
|
+
if (!store?.listHooks) throw new TypeError('hook_store_required');
|
|
228
|
+
const configured = await store.listHooks({
|
|
229
|
+
event_type: options.event_type,
|
|
230
|
+
activeOnly: true,
|
|
231
|
+
context: options.context || {},
|
|
232
|
+
});
|
|
233
|
+
const baseDirectory = options.cwd || (typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '');
|
|
234
|
+
for (const row of configured) {
|
|
235
|
+
let handler;
|
|
236
|
+
if (row.handler_type === 'log_only') handler = async () => ({});
|
|
237
|
+
else if (row.handler_type === 'command' || row.handler_type === 'http') {
|
|
238
|
+
const { createConfiguredHookAdapter } = await import('./config.js');
|
|
239
|
+
handler = createConfiguredHookAdapter({ type: row.handler_type, ...row.handler_config }, baseDirectory, options);
|
|
240
|
+
} else if (typeof options.resolveHandler === 'function') {
|
|
241
|
+
handler = await options.resolveHandler(row);
|
|
242
|
+
}
|
|
243
|
+
if (typeof handler !== 'function') throw new Error(`stored_hook_handler_unavailable:${row.hook_key}:${row.handler_type}`);
|
|
244
|
+
runtime.register(row.event_type, {
|
|
245
|
+
id: row.hook_key,
|
|
246
|
+
priority: row.priority,
|
|
247
|
+
timeout_ms: row.timeout_ms,
|
|
248
|
+
failure_mode: row.failure_mode,
|
|
249
|
+
enabled: row.is_active,
|
|
250
|
+
matches: (envelope) => matchesHookInput(row.match, envelope.input),
|
|
251
|
+
handler,
|
|
252
|
+
metadata: {
|
|
253
|
+
...row.metadata,
|
|
254
|
+
hook_source: 'stored',
|
|
255
|
+
adapter_type: row.handler_type,
|
|
256
|
+
stored_hook_id: row.id,
|
|
257
|
+
owner_id: row.owner_id,
|
|
258
|
+
scope_type: row.scope_type,
|
|
259
|
+
scope_ref: row.scope_ref,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return configured;
|
|
264
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import {
|
|
8
|
+
createAgentCapabilityAdapter,
|
|
9
|
+
createCompositeCapabilityAdapter,
|
|
10
|
+
createHookRuntime,
|
|
11
|
+
createHookRuntimeFromConfig,
|
|
12
|
+
createHookStore,
|
|
13
|
+
createLspCapabilityAdapter,
|
|
14
|
+
createMcpCapabilityAdapter,
|
|
15
|
+
findHookConfig,
|
|
16
|
+
registerStoredHooks,
|
|
17
|
+
} from '../src/index.js';
|
|
18
|
+
|
|
19
|
+
function temporaryDirectory(t) {
|
|
20
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-hooks-'));
|
|
21
|
+
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
22
|
+
return directory;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('explicit project config discovers and runs a relative command adapter', async (t) => {
|
|
26
|
+
const root = temporaryDirectory(t);
|
|
27
|
+
const nested = path.join(root, 'src', 'nested');
|
|
28
|
+
fs.mkdirSync(path.join(root, '.agentsam'), { recursive: true });
|
|
29
|
+
fs.mkdirSync(path.join(root, 'hooks'), { recursive: true });
|
|
30
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
31
|
+
fs.writeFileSync(path.join(root, 'hooks', 'allow.mjs'), `let s='';process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{const e=JSON.parse(s);console.log(JSON.stringify({permission_decision:e.input.tool_name==='safe'?'allow':'deny'}));});\n`);
|
|
32
|
+
const configFile = path.join(root, '.agentsam', 'hooks.json');
|
|
33
|
+
fs.writeFileSync(configFile, JSON.stringify({
|
|
34
|
+
schema: 'agentsam.hooks.config.v1',
|
|
35
|
+
adapters: { policy: { type: 'command', command: process.execPath, args: ['hooks/allow.mjs'], cwd: '..', timeout_ms: 2000 } },
|
|
36
|
+
hooks: { pre_tool_use: [{ id: 'policy', adapter: 'policy', failure_mode: 'closed' }] },
|
|
37
|
+
}));
|
|
38
|
+
// cwd paths are intentionally constrained to the config directory by default.
|
|
39
|
+
assert.throws(() => createHookRuntimeFromConfig(configFile), /outside_config_directory/);
|
|
40
|
+
|
|
41
|
+
fs.copyFileSync(path.join(root, 'hooks', 'allow.mjs'), path.join(root, '.agentsam', 'allow.mjs'));
|
|
42
|
+
fs.writeFileSync(configFile, JSON.stringify({
|
|
43
|
+
schema: 'agentsam.hooks.config.v1',
|
|
44
|
+
adapters: { policy: { type: 'command', command: process.execPath, args: ['allow.mjs'], timeout_ms: 2000 } },
|
|
45
|
+
hooks: { pre_tool_use: [{ id: 'policy', adapter: 'policy', failure_mode: 'closed', match: { tool_name: { $eq: 'safe' } } }] },
|
|
46
|
+
}));
|
|
47
|
+
assert.equal(findHookConfig(nested), configFile);
|
|
48
|
+
const { runtime } = createHookRuntimeFromConfig(configFile);
|
|
49
|
+
const result = await runtime.dispatch('pre_tool_use', { tool_name: 'safe', tool_args: {} });
|
|
50
|
+
assert.equal(result.output.permission_decision, 'allow');
|
|
51
|
+
const skipped = await runtime.dispatch('pre_tool_use', { tool_name: 'other', tool_args: {} });
|
|
52
|
+
assert.equal(skipped.receipts.length, 0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('MCP adapter discovers tools through a host port', async () => {
|
|
56
|
+
const calls = [];
|
|
57
|
+
const adapter = await createMcpCapabilityAdapter({
|
|
58
|
+
servers: { docs: {} },
|
|
59
|
+
listTools: async (server) => ({ tools: [{ name: 'search', description: 'Search docs', inputSchema: { type: 'object', properties: { q: { type: 'string' } } } }] }),
|
|
60
|
+
callTool: async (server, tool, args) => { calls.push({ server, tool, args }); return { matches: 2 }; },
|
|
61
|
+
});
|
|
62
|
+
assert.equal(adapter.toolDescriptors()[0].name, 'mcp.docs.search');
|
|
63
|
+
assert.deepEqual(await adapter.invoke('mcp.docs.search', { q: 'hooks' }), { matches: 2 });
|
|
64
|
+
assert.deepEqual(calls[0], { server: 'docs', tool: 'search', args: { q: 'hooks' } });
|
|
65
|
+
|
|
66
|
+
const timedOut = await createMcpCapabilityAdapter({
|
|
67
|
+
servers: { docs: { transport: 'stdio', tools: [{ name: 'search' }] } },
|
|
68
|
+
callTool: async () => { throw new Error('request timed out'); },
|
|
69
|
+
});
|
|
70
|
+
await assert.rejects(timedOut.invoke('mcp.docs.search', {}), (error) => {
|
|
71
|
+
assert.equal(error.reason, 'mcp_timeout');
|
|
72
|
+
assert.equal(error.adapter, 'mcp');
|
|
73
|
+
assert.equal(error.protocol, 'mcp');
|
|
74
|
+
assert.equal(error.transport, 'stdio');
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('LSP adapter maps portable capabilities to standard protocol methods', async () => {
|
|
80
|
+
const calls = [];
|
|
81
|
+
const adapter = createLspCapabilityAdapter({
|
|
82
|
+
languages: { typescript: { command: 'typescript-language-server' } },
|
|
83
|
+
request: async (language, method, params) => { calls.push({ language, method, params }); return { contents: 'number' }; },
|
|
84
|
+
});
|
|
85
|
+
const result = await adapter.invoke('lsp.hover', { language: 'typescript', document_uri: 'file:///project/a.ts', line: 2, character: 4 });
|
|
86
|
+
assert.deepEqual(result, { contents: 'number' });
|
|
87
|
+
assert.equal(calls[0].method, 'textDocument/hover');
|
|
88
|
+
assert.deepEqual(calls[0].params.position, { line: 2, character: 4 });
|
|
89
|
+
|
|
90
|
+
const unavailable = createLspCapabilityAdapter({
|
|
91
|
+
languages: { rust: { command: 'rust-analyzer' } },
|
|
92
|
+
request: async () => { throw Object.assign(new Error('server not running'), { code: 'ENOENT' }); },
|
|
93
|
+
});
|
|
94
|
+
await assert.rejects(unavailable.invoke('lsp.hover', { language: 'rust', document_uri: 'file:///a.rs', line: 0, character: 0 }), (error) => {
|
|
95
|
+
assert.equal(error.reason, 'lsp_server_unavailable');
|
|
96
|
+
assert.equal(error.adapter, 'lsp');
|
|
97
|
+
assert.equal(error.protocol, 'lsp');
|
|
98
|
+
assert.equal(error.transport, 'stdio');
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('sub-agent adapter emits lifecycle hooks around host scheduling', async () => {
|
|
104
|
+
const lifecycle = [];
|
|
105
|
+
const hooks = createHookRuntime({ hooks: {
|
|
106
|
+
subagent_start: ({ input }) => { lifecycle.push(`start:${input.agent_id}`); },
|
|
107
|
+
subagent_stop: ({ input }) => { lifecycle.push(`stop:${input.status}`); },
|
|
108
|
+
} });
|
|
109
|
+
const adapter = createAgentCapabilityAdapter({
|
|
110
|
+
agents: [{ id: 'reviewer', description: 'Review a bounded patch.' }],
|
|
111
|
+
hookRuntime: hooks,
|
|
112
|
+
runAgent: async ({ task }) => ({ summary: `reviewed ${task}` }),
|
|
113
|
+
});
|
|
114
|
+
const result = await adapter.invoke('agent.delegate.reviewer', { task: 'the hook package' });
|
|
115
|
+
assert.equal(result.summary, 'reviewed the hook package');
|
|
116
|
+
assert.deepEqual(lifecycle, ['start:reviewer', 'stop:completed']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('composite adapter rejects duplicate names and routes distinct capabilities', async () => {
|
|
120
|
+
const first = { toolDescriptors: () => [{ name: 'one' }], invoke: async () => 1 };
|
|
121
|
+
const second = { toolDescriptors: () => [{ name: 'two' }], invoke: async () => 2 };
|
|
122
|
+
const composite = createCompositeCapabilityAdapter([first, second]);
|
|
123
|
+
assert.equal(await composite.invoke('two'), 2);
|
|
124
|
+
assert.throws(() => createCompositeCapabilityAdapter([first, first]).toolDescriptors(), /duplicate_capability/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('portable hook store keeps scoped definitions and value-free execution evidence', async () => {
|
|
128
|
+
const database = new DatabaseSync(':memory:');
|
|
129
|
+
const schemaFile = path.resolve(import.meta.dirname, '../schema/migrations/sqlite/001_hooks_core.sql');
|
|
130
|
+
database.exec(fs.readFileSync(schemaFile, 'utf8'));
|
|
131
|
+
const db = {
|
|
132
|
+
prepare(sql) {
|
|
133
|
+
const statement = database.prepare(sql);
|
|
134
|
+
return {
|
|
135
|
+
bind(...values) {
|
|
136
|
+
return {
|
|
137
|
+
run: async () => statement.run(...values),
|
|
138
|
+
first: async () => statement.get(...values) ?? null,
|
|
139
|
+
all: async () => ({ results: statement.all(...values) }),
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const store = createHookStore(db, { ownerId: 'acct_test' });
|
|
146
|
+
await store.upsertHook({
|
|
147
|
+
hook_key: 'audit.prompt',
|
|
148
|
+
event_type: 'user_prompt_submitted',
|
|
149
|
+
source_kind: 'stored',
|
|
150
|
+
scope_type: 'project',
|
|
151
|
+
scope_ref: '/workspace/demo',
|
|
152
|
+
handler_type: 'log_only',
|
|
153
|
+
match: { prompt: { $contains: 'persist' } },
|
|
154
|
+
priority: 20,
|
|
155
|
+
});
|
|
156
|
+
assert.equal((await store.listHooks({ context: { project_root: '/workspace/other' } })).length, 0);
|
|
157
|
+
assert.equal((await store.listHooks({ context: { project_root: '/workspace/demo' } })).length, 1);
|
|
158
|
+
|
|
159
|
+
const runtime = createHookRuntime();
|
|
160
|
+
await registerStoredHooks(runtime, store, { cwd: '/workspace/demo', context: { project_root: '/workspace/demo' } });
|
|
161
|
+
const skipped = await runtime.dispatch('user_prompt_submitted', { prompt: 'unmatched' }, { session_id: 'sess_1' });
|
|
162
|
+
assert.equal(skipped.receipts.length, 0);
|
|
163
|
+
const dispatched = await runtime.dispatch('user_prompt_submitted', { prompt: 'do not persist me' }, {
|
|
164
|
+
session_id: 'sess_1',
|
|
165
|
+
metadata: { private_note: 'also do not persist me' },
|
|
166
|
+
});
|
|
167
|
+
await store.recordExecution(dispatched.receipts[0], {
|
|
168
|
+
source_kind: 'stored',
|
|
169
|
+
context: { trace_id: 'trace_safe', private_note: 'still do not persist me' },
|
|
170
|
+
});
|
|
171
|
+
const executions = await store.listExecutions();
|
|
172
|
+
assert.equal(executions.length, 1);
|
|
173
|
+
assert.deepEqual(executions[0].input_keys, ['prompt']);
|
|
174
|
+
assert.doesNotMatch(executions[0].receipt_json, /do not persist me/);
|
|
175
|
+
assert.deepEqual(executions[0].receipt.invocation, { session_id: 'sess_1' });
|
|
176
|
+
assert.deepEqual(executions[0].correlation, { trace_id: 'trace_safe' });
|
|
177
|
+
|
|
178
|
+
await store.recordExecution({
|
|
179
|
+
schema: 'agentsam.hook.receipt.v1', hook_id: 'audit.prompt', hook: 'post_tool_use', status: 'failed',
|
|
180
|
+
started_at: 1, completed_at: 2, duration_ms: 1, input_keys: ['tool_name'], output_keys: [],
|
|
181
|
+
error: {
|
|
182
|
+
error_code: 'DEADLINE_EXCEEDED', reason: 'hook_http_timeout', domain: 'hook', failure_class: 'timeout',
|
|
183
|
+
stage: 'execute', feature: 'hooks.post_tool_use', failure_behavior: 'fail_open', retryable: false,
|
|
184
|
+
side_effect_state: 'confirmed_applied', adapter: 'http', protocol: 'http', transport: 'https',
|
|
185
|
+
fingerprint: 'err_1234567890abcdef', message: 'redacted timeout',
|
|
186
|
+
},
|
|
187
|
+
native_evidence: { code: 'ETIMEDOUT', exception_type: 'Error', message: 'redacted timeout' },
|
|
188
|
+
});
|
|
189
|
+
const failed = (await store.listExecutions()).find((row) => row.status === 'timeout');
|
|
190
|
+
assert.equal(failed.error_code, 'DEADLINE_EXCEEDED');
|
|
191
|
+
assert.equal(failed.reason, 'hook_http_timeout');
|
|
192
|
+
assert.equal(failed.receipt.error.failure_behavior, 'fail_open');
|
|
193
|
+
assert.equal(failed.receipt.error.side_effect_state, 'confirmed_applied');
|
|
194
|
+
assert.equal(failed.receipt.native_evidence.code, 'ETIMEDOUT');
|
|
195
|
+
database.close();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('generated package schemas stay byte-identical to protocol authority', () => {
|
|
199
|
+
const root = path.resolve(import.meta.dirname, '../../..');
|
|
200
|
+
for (const filename of [
|
|
201
|
+
'agentsam.hook.v1.schema.json',
|
|
202
|
+
'agentsam.hook.output.v1.schema.json',
|
|
203
|
+
'agentsam.hook.receipt.v1.schema.json',
|
|
204
|
+
'agentsam.hooks.config.v1.schema.json',
|
|
205
|
+
]) {
|
|
206
|
+
assert.equal(
|
|
207
|
+
fs.readFileSync(path.join(root, 'protocol', 'hooks', filename), 'utf8'),
|
|
208
|
+
fs.readFileSync(path.join(root, 'packages', 'agentsam-hooks', 'protocol', filename), 'utf8'),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
assert.equal(
|
|
212
|
+
fs.readFileSync(path.join(root, 'migrations', 'runtime', '0005_agentsam_hooks.sql'), 'utf8'),
|
|
213
|
+
fs.readFileSync(path.join(root, 'packages', 'agentsam-hooks', 'schema', 'migrations', 'sqlite', '001_hooks_core.sql'), 'utf8'),
|
|
214
|
+
);
|
|
215
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(root, 'packages', 'agentsam-hooks', 'schema', 'manifest.json'))).id, 'agentsam.hooks');
|
|
216
|
+
});
|