@inneranimalmedia/agentsam-sdk 2.6.9 → 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.
Files changed (176) hide show
  1. package/README.md +18 -0
  2. package/apps/cad-creator/shared/cad/package.json +1 -1
  3. package/apps/client-cms-editor/backend/package.json +3 -3
  4. package/apps/client-cms-editor/frontend/package.json +4 -4
  5. package/apps/client-cms-editor/package-lock.json +8 -8
  6. package/apps/client-cms-editor/package.json +1 -1
  7. package/apps/client-cms-editor/shared/cms/package.json +2 -2
  8. package/docs/architecture/HOOKS-MODELS-SELF-HOSTING-PLAN.md +199 -0
  9. package/examples/hyperspace-demo/index.html +16 -0
  10. package/examples/hyperspace-demo/package.json +21 -0
  11. package/examples/hyperspace-demo/src/main.tsx +160 -0
  12. package/examples/hyperspace-demo/tsconfig.json +9 -0
  13. package/examples/hyperspace-demo/vite.config.ts +24 -0
  14. package/migrations/runtime/0005_agentsam_hooks.sql +87 -0
  15. package/package.json +23 -7
  16. package/packages/agentsam-brand/package.json +1 -1
  17. package/packages/agentsam-brand/src/core/v2/compile.js +1 -1
  18. package/packages/agentsam-brand/src/core/v2/schema.js +1 -1
  19. package/packages/agentsam-contracts/package.json +1 -1
  20. package/packages/agentsam-contracts/src/errors.ts +19 -10
  21. package/packages/agentsam-errors/package.json +11 -3
  22. package/packages/agentsam-errors/protocol/agentsam.error.v2.schema.json +821 -0
  23. package/packages/agentsam-errors/protocol/error-catalog.json +3512 -0
  24. package/packages/agentsam-errors/src/envelope.js +63 -11
  25. package/packages/agentsam-errors/src/error.js +6 -0
  26. package/packages/agentsam-errors/src/fingerprint.js +9 -1
  27. package/packages/agentsam-errors/src/normalize.js +51 -6
  28. package/packages/agentsam-errors/src/recovery.js +9 -35
  29. package/packages/agentsam-errors/src/transport.js +6 -1
  30. package/packages/agentsam-errors/src/vocabulary.js +515 -126
  31. package/packages/agentsam-errors/test/catalog-v2.test.mjs +106 -0
  32. package/packages/agentsam-errors/types/index.d.ts +157 -0
  33. package/packages/agentsam-goap/package.json +2 -2
  34. package/packages/agentsam-hooks/README.md +206 -0
  35. package/packages/agentsam-hooks/bin/agentsam-hooks.mjs +64 -0
  36. package/packages/agentsam-hooks/bindings/go/command.go +55 -0
  37. package/packages/agentsam-hooks/bindings/go/errors_generated.go +402 -0
  38. package/packages/agentsam-hooks/bindings/go/go.mod +3 -0
  39. package/packages/agentsam-hooks/bindings/go/hooks.go +285 -0
  40. package/packages/agentsam-hooks/bindings/go/hooks_test.go +48 -0
  41. package/packages/agentsam-hooks/bindings/rust/Cargo.toml +11 -0
  42. package/packages/agentsam-hooks/bindings/rust/src/errors_generated.rs +396 -0
  43. package/packages/agentsam-hooks/bindings/rust/src/lib.rs +397 -0
  44. package/packages/agentsam-hooks/package.json +69 -0
  45. package/packages/agentsam-hooks/schema/manifest.json +54 -0
  46. package/packages/agentsam-hooks/scripts/clean-protocol.mjs +17 -0
  47. package/packages/agentsam-hooks/scripts/sync-protocol.mjs +27 -0
  48. package/packages/agentsam-hooks/src/adapters/agents.js +82 -0
  49. package/packages/agentsam-hooks/src/adapters/callback.js +4 -0
  50. package/packages/agentsam-hooks/src/adapters/capability.js +122 -0
  51. package/packages/agentsam-hooks/src/adapters/command.js +104 -0
  52. package/packages/agentsam-hooks/src/adapters/composite.js +31 -0
  53. package/packages/agentsam-hooks/src/adapters/http.js +58 -0
  54. package/packages/agentsam-hooks/src/adapters/lsp.js +116 -0
  55. package/packages/agentsam-hooks/src/adapters/mcp.js +76 -0
  56. package/packages/agentsam-hooks/src/adapters/provider.js +129 -0
  57. package/packages/agentsam-hooks/src/config.js +143 -0
  58. package/packages/agentsam-hooks/src/contracts.js +284 -0
  59. package/packages/agentsam-hooks/src/errors-generated.js +2732 -0
  60. package/packages/agentsam-hooks/src/index.d.ts +193 -0
  61. package/packages/agentsam-hooks/src/index.js +14 -0
  62. package/packages/agentsam-hooks/src/policy.js +34 -0
  63. package/packages/agentsam-hooks/src/runtime.js +312 -0
  64. package/packages/agentsam-hooks/src/store.js +264 -0
  65. package/packages/agentsam-hooks/test/adapters.test.mjs +216 -0
  66. package/packages/agentsam-hooks/test/runtime.test.mjs +258 -0
  67. package/packages/agentsam-knowledge/package.json +1 -1
  68. package/packages/agentsam-nav/dist/Nav.d.ts +43 -0
  69. package/packages/agentsam-nav/dist/NavCollapsible.d.ts +10 -0
  70. package/packages/agentsam-nav/dist/NavContent.d.ts +2 -0
  71. package/packages/agentsam-nav/dist/NavFooter.d.ts +2 -0
  72. package/packages/agentsam-nav/dist/NavGroup.d.ts +3 -0
  73. package/packages/agentsam-nav/dist/NavHeader.d.ts +2 -0
  74. package/packages/agentsam-nav/dist/NavMenu.d.ts +22 -0
  75. package/packages/agentsam-nav/dist/NavProvider.d.ts +35 -0
  76. package/packages/agentsam-nav/dist/NavTrigger.d.ts +2 -0
  77. package/packages/agentsam-nav/dist/Surfaces.d.ts +41 -0
  78. package/packages/agentsam-nav/dist/browser.d.ts +11 -0
  79. package/packages/agentsam-nav/dist/browser.global.js +135 -0
  80. package/packages/agentsam-nav/dist/browser.js +7290 -0
  81. package/packages/agentsam-nav/dist/contracts.d.ts +76 -0
  82. package/packages/agentsam-nav/dist/index.d.ts +11 -0
  83. package/packages/agentsam-nav/dist/index.js +7291 -0
  84. package/packages/agentsam-nav/dist/legacy.css +83 -0
  85. package/packages/agentsam-nav/dist/shell.css +113 -0
  86. package/packages/agentsam-nav/dist/theme.css +113 -0
  87. package/packages/agentsam-nav/dist/utils.d.ts +1 -0
  88. package/packages/agentsam-nav/package.json +1 -1
  89. package/packages/agentsam-queue-control/package.json +1 -1
  90. package/packages/agentsam-repository/package.json +1 -1
  91. package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +670 -0
  92. package/packages/agentsam-vault/package.json +1 -1
  93. package/packages/agentsam-workbench/package.json +3 -3
  94. package/packages/connectors/cloudflare/package.json +1 -1
  95. package/packages/heuristic-theme/package.json +1 -1
  96. package/packages/identity/package.json +2 -2
  97. package/packages/providers/completeful/package.json +1 -1
  98. package/packages/theme-church-site/package.json +2 -2
  99. package/packages/theme-companions-site/package.json +2 -2
  100. package/packages/theme-floors-site/package.json +2 -2
  101. package/packages/theme-fuelnfree-site/package.json +2 -2
  102. package/packages/theme-handyman-site/package.json +2 -2
  103. package/packages/theme-insurance-site/package.json +2 -2
  104. package/packages/theme-scenes/package.json +1 -1
  105. package/packages/theme-shinshu-site/package.json +2 -2
  106. package/packages/work-graph/package.json +1 -1
  107. package/protocol/apps/agentsam.app.v1.schema.json +1 -1
  108. package/protocol/apps/agentsam.host-install.v1.schema.json +1 -1
  109. package/protocol/assets/asset-record.v1.schema.json +1 -1
  110. package/protocol/brand/brandpack.v1.schema.json +1 -1
  111. package/protocol/capabilities/capability-manifest.schema.json +1 -1
  112. package/protocol/capabilities/knowledge-search-input.schema.json +1 -0
  113. package/protocol/credentials/issue.v1.schema.json +1 -1
  114. package/protocol/database/agentsam.schema-pack.v1.schema.json +1 -1
  115. package/protocol/database/connection.v1.schema.json +1 -1
  116. package/protocol/embeddings/embedding-profile.v1.schema.json +1 -1
  117. package/protocol/errors/README.md +36 -0
  118. package/protocol/errors/agentsam.error.v2.schema.json +821 -0
  119. package/protocol/errors/error-catalog.json +3506 -158
  120. package/protocol/errors/error-envelope.schema.json +195 -52
  121. package/protocol/errors/recovery.v1.schema.json +1 -1
  122. package/protocol/features/agentsam.feature.v1.schema.json +1 -1
  123. package/protocol/hooks/README.md +18 -0
  124. package/protocol/hooks/agentsam.hook.output.v1.schema.json +27 -0
  125. package/protocol/hooks/agentsam.hook.receipt.v1.schema.json +61 -0
  126. package/protocol/hooks/agentsam.hook.v1.schema.json +35 -0
  127. package/protocol/hooks/agentsam.hooks.config.v1.schema.json +78 -0
  128. package/protocol/local-engine/benchmark.v1.schema.json +1 -1
  129. package/protocol/local-engine/engine.v1.schema.json +1 -1
  130. package/protocol/models/model-inventory-v2.schema.json +1 -1
  131. package/protocol/models/provider-model-snapshot.v1.schema.json +22 -0
  132. package/protocol/rpc/v1/errors.proto +1 -0
  133. package/protocol/runtime/agentsam.runtime.v1.schema.json +1 -1
  134. package/protocol/runtime/workspace-fs.v1.schema.json +1 -1
  135. package/protocol/sam/activity.v1.schema.json +1 -1
  136. package/protocol/sam/answer.v1.schema.json +1 -1
  137. package/protocol/sam/calibration.v1.schema.json +1 -1
  138. package/protocol/sam/decision-receipt.v1.schema.json +1 -1
  139. package/protocol/sam/evaluation.v1.schema.json +1 -1
  140. package/protocol/sam/operation.schema.json +1 -1
  141. package/protocol/sam/outcome.v1.schema.json +1 -1
  142. package/protocol/sam/question.v1.schema.json +1 -1
  143. package/protocol/sam/result.schema.json +1 -1
  144. package/protocol/sam/state.v1.schema.json +1 -1
  145. package/protocol/scoring/scorecard.v1.schema.json +1 -1
  146. package/protocol/skills/agentsam.interaction.v1.schema.json +1 -1
  147. package/protocol/skills/agentsam.skill.v1.schema.json +1 -1
  148. package/protocol/theme-refinery/product-metadata.schema.json +1 -1
  149. package/protocol/theme-refinery/reconciliation-receipt.schema.json +1 -1
  150. package/protocol/ui/icon.v1.schema.json +1 -1
  151. package/python/README.md +20 -0
  152. package/python/agentsam_sdk/errors/__init__.py +4 -0
  153. package/python/agentsam_sdk/errors/vocabulary.py +2730 -0
  154. package/python/agentsam_sdk/hooks/__init__.py +32 -0
  155. package/python/agentsam_sdk/hooks/adapters.py +132 -0
  156. package/python/agentsam_sdk/hooks/contracts.py +121 -0
  157. package/python/agentsam_sdk/hooks/runtime.py +253 -0
  158. package/python/tests/test_hooks.py +115 -0
  159. package/src/agent/responses-runner.js +145 -1
  160. package/src/cli/command-catalog.js +1 -0
  161. package/src/cli.js +9 -0
  162. package/src/commands/cheat-sheet.js +1 -0
  163. package/src/commands/hooks.js +260 -0
  164. package/src/commands/models.js +1 -1
  165. package/src/commands/shell.js +114 -9
  166. package/src/errors/diagnostic.js +9 -9
  167. package/src/index.js +32 -0
  168. package/src/knowledge/service/grpc-codec.js +2 -0
  169. package/src/lib/slash-commands.js +1 -0
  170. package/src/local/runtime-store.js +46 -0
  171. package/src/models/catalog.js +0 -49
  172. package/src/models/inventory-core.js +69 -0
  173. package/src/plugins/index.js +8 -1
  174. package/src/plugins/registry.js +56 -0
  175. package/src/rpc/generated/errors_pb.js +49 -1
  176. package/src/tools/runtime.js +13 -5
@@ -0,0 +1,285 @@
1
+ // Package agentsamhooks implements the provider-neutral agentsam.hook.v1
2
+ // envelope and an ordered callback runtime. Command and HTTP transports can
3
+ // exchange the same JSON without changing lifecycle semantics.
4
+ package agentsamhooks
5
+
6
+ import (
7
+ "context"
8
+ "encoding/json"
9
+ "errors"
10
+ "fmt"
11
+ "os"
12
+ "regexp"
13
+ "sort"
14
+ "strings"
15
+ "sync"
16
+ "time"
17
+ )
18
+
19
+ const (
20
+ ProtocolSchema = "agentsam.hook.v1"
21
+ ReceiptSchema = "agentsam.hook.receipt.v1"
22
+ )
23
+
24
+ var Events = []string{
25
+ "session_start", "session_end", "user_prompt_submitted", "user_prompt_transformed",
26
+ "pre_model_use", "post_model_use", "pre_tool_use", "post_tool_use",
27
+ "post_tool_use_failure", "error_occurred", "agent_stop", "subagent_start", "subagent_stop",
28
+ }
29
+
30
+ var eventSet = func() map[string]bool {
31
+ values := map[string]bool{}
32
+ for _, event := range Events { values[event] = true }
33
+ return values
34
+ }()
35
+
36
+ // Invocation carries host-owned correlation IDs, never credentials.
37
+ type Invocation struct {
38
+ SessionID string `json:"session_id,omitempty"`
39
+ RunID string `json:"run_id,omitempty"`
40
+ TurnID string `json:"turn_id,omitempty"`
41
+ MessageID string `json:"message_id,omitempty"`
42
+ AgentID string `json:"agent_id,omitempty"`
43
+ ParentAgentID string `json:"parent_agent_id,omitempty"`
44
+ Source string `json:"source,omitempty"`
45
+ Metadata map[string]any `json:"metadata,omitempty"`
46
+ }
47
+
48
+ type Envelope struct {
49
+ Schema string `json:"schema"`
50
+ Hook string `json:"hook"`
51
+ Timestamp int64 `json:"timestamp"`
52
+ CWD string `json:"cwd"`
53
+ Invocation Invocation `json:"invocation"`
54
+ Input map[string]any `json:"input"`
55
+ }
56
+
57
+ type Output struct {
58
+ PermissionDecision string `json:"permission_decision,omitempty"`
59
+ PermissionDecisionReason string `json:"permission_decision_reason,omitempty"`
60
+ ModifiedArgs map[string]any `json:"modified_args,omitempty"`
61
+ ModifiedRequest map[string]any `json:"modified_request,omitempty"`
62
+ ModifiedResult any `json:"modified_result,omitempty"`
63
+ ModifiedPrompt *string `json:"modified_prompt,omitempty"`
64
+ ModifiedTransformedPrompt *string `json:"modified_transformed_prompt,omitempty"`
65
+ ModifiedConfig map[string]any `json:"modified_config,omitempty"`
66
+ AdditionalContext string `json:"additional_context,omitempty"`
67
+ SuppressOutput bool `json:"suppress_output,omitempty"`
68
+ ErrorHandling string `json:"error_handling,omitempty"`
69
+ RetryCount int `json:"retry_count,omitempty"`
70
+ UserNotification string `json:"user_notification,omitempty"`
71
+ Decision string `json:"decision,omitempty"`
72
+ Reason string `json:"reason,omitempty"`
73
+ CleanupActions []string `json:"cleanup_actions,omitempty"`
74
+ SessionSummary string `json:"session_summary,omitempty"`
75
+ Metadata map[string]any `json:"metadata,omitempty"`
76
+ }
77
+
78
+ func (o Output) Validate(event string) error {
79
+ if o.PermissionDecision != "" && o.PermissionDecision != "allow" && o.PermissionDecision != "deny" && o.PermissionDecision != "ask" {
80
+ return fmt.Errorf("invalid_permission_decision:%s", o.PermissionDecision)
81
+ }
82
+ if o.PermissionDecision != "" && event != "pre_tool_use" && event != "pre_model_use" {
83
+ return fmt.Errorf("permission_decision_not_supported:%s", event)
84
+ }
85
+ if o.ErrorHandling != "" && o.ErrorHandling != "retry" && o.ErrorHandling != "skip" && o.ErrorHandling != "abort" {
86
+ return fmt.Errorf("invalid_error_handling:%s", o.ErrorHandling)
87
+ }
88
+ if o.ErrorHandling != "" && event != "error_occurred" { return fmt.Errorf("error_handling_not_supported:%s", event) }
89
+ if o.RetryCount < 0 || o.RetryCount > 10 { return errors.New("retry_count_must_be_between_0_and_10") }
90
+ if o.Decision != "" && (event != "agent_stop" || (o.Decision != "allow" && o.Decision != "block")) {
91
+ return fmt.Errorf("invalid_stop_decision:%s", o.Decision)
92
+ }
93
+ if o.ModifiedConfig != nil && event != "session_start" { return fmt.Errorf("modified_config_not_supported:%s", event) }
94
+ return nil
95
+ }
96
+
97
+ type Handler func(context.Context, Envelope) (*Output, error)
98
+
99
+ type Definition struct {
100
+ ID string
101
+ Priority float64
102
+ Timeout time.Duration
103
+ FailureMode string
104
+ Disabled bool
105
+ Handler Handler
106
+ Metadata map[string]any
107
+ }
108
+
109
+ type Receipt struct {
110
+ Schema string `json:"schema"`
111
+ HookID string `json:"hook_id"`
112
+ Hook string `json:"hook"`
113
+ Status string `json:"status"`
114
+ StartedAt int64 `json:"started_at"`
115
+ CompletedAt int64 `json:"completed_at"`
116
+ DurationMS int64 `json:"duration_ms"`
117
+ InputKeys []string `json:"input_keys"`
118
+ OutputKeys []string `json:"output_keys"`
119
+ Error map[string]any `json:"error,omitempty"`
120
+ NativeEvidence map[string]any `json:"native_evidence,omitempty"`
121
+ }
122
+
123
+ type DispatchResult struct {
124
+ Schema string `json:"schema"`
125
+ Hook string `json:"hook"`
126
+ Input map[string]any `json:"input"`
127
+ Output Output `json:"output"`
128
+ Receipts []Receipt `json:"receipts"`
129
+ Errors []map[string]any `json:"errors"`
130
+ }
131
+
132
+ type Runtime struct {
133
+ mu sync.RWMutex
134
+ hooks map[string][]Definition
135
+ now func() time.Time
136
+ onReceipt func(Receipt)
137
+ }
138
+
139
+ func NewRuntime(onReceipt func(Receipt)) *Runtime {
140
+ return &Runtime{hooks: map[string][]Definition{}, now: time.Now, onReceipt: onReceipt}
141
+ }
142
+
143
+ func normalizeEvent(event string) (string, error) {
144
+ event = strings.TrimSpace(event)
145
+ if !eventSet[event] { return "", fmt.Errorf("unsupported_hook_event:%s", event) }
146
+ return event, nil
147
+ }
148
+
149
+ func (r *Runtime) Register(event string, definition Definition) error {
150
+ event, err := normalizeEvent(event); if err != nil { return err }
151
+ if definition.ID == "" || definition.Handler == nil { return errors.New("hook_id_and_handler_required") }
152
+ if definition.Timeout <= 0 { definition.Timeout = 10 * time.Second }
153
+ if definition.FailureMode == "" {
154
+ if event == "pre_tool_use" || event == "pre_model_use" { definition.FailureMode = "closed" } else { definition.FailureMode = "open" }
155
+ }
156
+ if definition.FailureMode != "open" && definition.FailureMode != "closed" && definition.FailureMode != "error" {
157
+ return fmt.Errorf("invalid_hook_failure_mode:%s", definition.FailureMode)
158
+ }
159
+ r.mu.Lock(); defer r.mu.Unlock()
160
+ for _, row := range r.hooks[event] { if row.ID == definition.ID { return fmt.Errorf("duplicate_hook_id:%s", definition.ID) } }
161
+ r.hooks[event] = append(r.hooks[event], definition)
162
+ sort.Slice(r.hooks[event], func(i, j int) bool {
163
+ if r.hooks[event][i].Priority == r.hooks[event][j].Priority { return r.hooks[event][i].ID < r.hooks[event][j].ID }
164
+ return r.hooks[event][i].Priority < r.hooks[event][j].Priority
165
+ })
166
+ return nil
167
+ }
168
+
169
+ func cloneMap(value map[string]any) map[string]any {
170
+ if value == nil { return map[string]any{} }
171
+ encoded, _ := json.Marshal(value); output := map[string]any{}; _ = json.Unmarshal(encoded, &output); return output
172
+ }
173
+
174
+ func keys(value map[string]any) []string { output := make([]string, 0, len(value)); for key := range value { output = append(output, key) }; sort.Strings(output); return output }
175
+
176
+ func outputKeys(value Output) []string { encoded, _ := json.Marshal(value); object := map[string]any{}; _ = json.Unmarshal(encoded, &object); return keys(object) }
177
+
178
+ func apply(event string, input map[string]any, output Output) {
179
+ if output.ModifiedArgs != nil { input["tool_args"] = cloneMap(output.ModifiedArgs) }
180
+ if output.ModifiedRequest != nil { input["request"] = cloneMap(output.ModifiedRequest) }
181
+ if output.ModifiedResult != nil { if event == "post_tool_use" { input["tool_result"] = output.ModifiedResult } else { input["model_result"] = output.ModifiedResult } }
182
+ if output.ModifiedPrompt != nil { input["prompt"] = *output.ModifiedPrompt }
183
+ if output.ModifiedTransformedPrompt != nil { input["transformed_prompt"] = *output.ModifiedTransformedPrompt }
184
+ if output.ModifiedConfig != nil {
185
+ config, _ := input["config"].(map[string]any); config = cloneMap(config)
186
+ for key, value := range output.ModifiedConfig { config[key] = value }; input["config"] = config
187
+ }
188
+ }
189
+
190
+ func merge(target *Output, update Output, contexts *[]string) {
191
+ if update.PermissionDecision != "" { target.PermissionDecision = update.PermissionDecision; target.PermissionDecisionReason = update.PermissionDecisionReason }
192
+ if update.ModifiedArgs != nil { target.ModifiedArgs = update.ModifiedArgs }; if update.ModifiedRequest != nil { target.ModifiedRequest = update.ModifiedRequest }
193
+ if update.ModifiedResult != nil { target.ModifiedResult = update.ModifiedResult }; if update.ModifiedPrompt != nil { target.ModifiedPrompt = update.ModifiedPrompt }
194
+ if update.ModifiedTransformedPrompt != nil { target.ModifiedTransformedPrompt = update.ModifiedTransformedPrompt }
195
+ if update.ModifiedConfig != nil {
196
+ if target.ModifiedConfig == nil { target.ModifiedConfig = map[string]any{} }
197
+ for key, value := range update.ModifiedConfig { target.ModifiedConfig[key] = value }
198
+ }
199
+ if update.AdditionalContext != "" { *contexts = append(*contexts, update.AdditionalContext) }
200
+ target.SuppressOutput = target.SuppressOutput || update.SuppressOutput
201
+ if update.ErrorHandling != "" { target.ErrorHandling = update.ErrorHandling }; if update.RetryCount > target.RetryCount { target.RetryCount = update.RetryCount }
202
+ if update.UserNotification != "" { target.UserNotification = update.UserNotification }; if update.Decision != "" { target.Decision = update.Decision; target.Reason = update.Reason }
203
+ target.CleanupActions = append(target.CleanupActions, update.CleanupActions...); if update.SessionSummary != "" { target.SessionSummary = update.SessionSummary }
204
+ }
205
+
206
+ var hookSecretPatterns = []*regexp.Regexp{
207
+ regexp.MustCompile(`(?i)(authorization\s*[:=]\s*bearer\s+)[^\s,;]+`),
208
+ regexp.MustCompile(`(?i)((?:api[_-]?key|password|secret|token)\s*[:=]\s*)[^\s,;]+`),
209
+ }
210
+
211
+ func safeHookError(err error) string {
212
+ message := err.Error()
213
+ for _, pattern := range hookSecretPatterns { message = pattern.ReplaceAllString(message, `${1}[REDACTED]`) }
214
+ if len(message) > 512 { message = message[:512] }
215
+ return message
216
+ }
217
+
218
+ func hookFingerprint(values ...string) string {
219
+ text := strings.Join(values, "\x1f")
220
+ hash := uint64(0xcbf29ce484222325)
221
+ for index := 0; index < len(text); index++ { hash ^= uint64(text[index]); hash *= 0x100000001b3 }
222
+ return fmt.Sprintf("err_%016x", hash)
223
+ }
224
+
225
+ func canonicalHookFailure(definition Definition, event string, hookErr error) (map[string]any, map[string]any) {
226
+ reason := NormalizeErrorReason(hookErr.Error())
227
+ message := safeHookError(hookErr)
228
+ if strings.HasPrefix(reason, "mcp_") || strings.HasPrefix(reason, "lsp_") {
229
+ if _, ok := ErrorReasonPolicies["hook_"+reason]; ok { reason = "hook_"+reason }
230
+ }
231
+ policy := ErrorReasonPolicies[reason]
232
+ isPost := event == "post_tool_use" || event == "post_model_use"
233
+ behavior := "abort"
234
+ if definition.FailureMode == "closed" { behavior = "fail_closed" } else if definition.FailureMode == "open" { behavior = "fail_open" } else if isPost { behavior = "no_replay" }
235
+ sideEffect := "not_started"
236
+ if isPost { sideEffect = "confirmed_applied" }
237
+ adapter, _ := definition.Metadata["adapter_type"].(string)
238
+ if adapter == "" {
239
+ for _, candidate := range []string{"http", "mcp", "lsp", "command"} { if strings.HasPrefix(reason, "hook_"+candidate+"_") { adapter = candidate; break } }
240
+ }
241
+ protocol := adapter
242
+ transport := ""
243
+ if adapter == "command" { protocol = ProtocolSchema; transport = "process" }
244
+ retryable := policy.Retryable && sideEffect != "confirmed_applied" && behavior != "fail_closed"
245
+ feature := "hooks." + event
246
+ errorValue := map[string]any{
247
+ "error_code": policy.Code, "reason": reason, "domain": policy.Domain, "failure_class": policy.FailureClass,
248
+ "stage": policy.DefaultStage, "feature": feature, "failure_behavior": behavior, "retryable": retryable,
249
+ "side_effect_state": sideEffect, "adapter": nil, "protocol": nil, "transport": nil,
250
+ "fingerprint": hookFingerprint(policy.Code, reason, policy.Domain, policy.FailureClass, policy.DefaultStage, feature, behavior, sideEffect, adapter, protocol, transport, definition.ID),
251
+ "message": message,
252
+ }
253
+ if adapter != "" { errorValue["adapter"] = adapter }; if protocol != "" { errorValue["protocol"] = protocol }; if transport != "" { errorValue["transport"] = transport }
254
+ return errorValue, map[string]any{"code": nil, "exception_type": fmt.Sprintf("%T", hookErr), "message": message}
255
+ }
256
+
257
+ func (r *Runtime) Dispatch(ctx context.Context, event string, input map[string]any, invocation Invocation, cwd string) (DispatchResult, error) {
258
+ event, err := normalizeEvent(event); if err != nil { return DispatchResult{}, err }
259
+ if cwd == "" { cwd, _ = os.Getwd() }
260
+ r.mu.RLock(); definitions := append([]Definition(nil), r.hooks[event]...); r.mu.RUnlock()
261
+ working := cloneMap(input); combined := Output{}; contexts := []string{}; receipts := []Receipt{}; failures := []map[string]any{}
262
+ for _, definition := range definitions {
263
+ if definition.Disabled { continue }
264
+ started := r.now(); envelope := Envelope{Schema: ProtocolSchema, Hook: event, Timestamp: started.UnixMilli(), CWD: cwd, Invocation: invocation, Input: cloneMap(working)}
265
+ hookCtx, cancel := context.WithTimeout(ctx, definition.Timeout); output, hookErr := definition.Handler(hookCtx, envelope); cancel(); completed := r.now()
266
+ receipt := Receipt{Schema: ReceiptSchema, HookID: definition.ID, Hook: event, StartedAt: started.UnixMilli(), CompletedAt: completed.UnixMilli(), DurationMS: completed.Sub(started).Milliseconds(), InputKeys: keys(envelope.Input)}
267
+ if hookErr == nil && output != nil { hookErr = output.Validate(event) }
268
+ if hookErr != nil {
269
+ receipt.Status = "failed"
270
+ receipt.Error, receipt.NativeEvidence = canonicalHookFailure(definition, event, hookErr)
271
+ failure := map[string]any{"hook_id": definition.ID, "hook": event, "native_evidence": receipt.NativeEvidence}
272
+ for key, value := range receipt.Error { failure[key] = value }; failures = append(failures, failure)
273
+ receipts = append(receipts, receipt); if r.onReceipt != nil { r.onReceipt(receipt) }
274
+ if definition.FailureMode == "error" { return DispatchResult{}, fmt.Errorf("hook_execution_failed:%s: %w", definition.ID, hookErr) }
275
+ if definition.FailureMode == "closed" { combined.PermissionDecision = "deny"; combined.PermissionDecisionReason = fmt.Sprintf("Hook '%s' failed closed: %s", definition.ID, safeHookError(hookErr)); break }
276
+ continue
277
+ }
278
+ if output == nil { output = &Output{} }; receipt.Status = "completed"; receipt.OutputKeys = outputKeys(*output); receipts = append(receipts, receipt); if r.onReceipt != nil { r.onReceipt(receipt) }
279
+ apply(event, working, *output); merge(&combined, *output, &contexts)
280
+ if (event == "pre_tool_use" || event == "pre_model_use") && (output.PermissionDecision == "deny" || output.PermissionDecision == "ask") { break }
281
+ if event == "agent_stop" && output.Decision == "block" { break }
282
+ }
283
+ combined.AdditionalContext = strings.Join(contexts, "\n\n")
284
+ return DispatchResult{Schema: "agentsam.hook.dispatch.v1", Hook: event, Input: working, Output: combined, Receipts: receipts, Errors: failures}, nil
285
+ }
@@ -0,0 +1,48 @@
1
+ package agentsamhooks
2
+
3
+ import (
4
+ "context"
5
+ "testing"
6
+ "time"
7
+ )
8
+
9
+ func TestRuntimeComposesPortablePreToolHooks(t *testing.T) {
10
+ runtime := NewRuntime(nil)
11
+ if err := runtime.Register("pre_tool_use", Definition{
12
+ ID: "policy", Priority: 10, Timeout: time.Second,
13
+ Handler: func(ctx context.Context, envelope Envelope) (*Output, error) {
14
+ args := envelope.Input["tool_args"].(map[string]any)
15
+ args["bounded"] = true
16
+ return &Output{PermissionDecision: "allow", ModifiedArgs: args, AdditionalContext: "Go policy"}, nil
17
+ },
18
+ }); err != nil { t.Fatal(err) }
19
+ result, err := runtime.Dispatch(context.Background(), "pre_tool_use", map[string]any{
20
+ "tool_name": "read", "tool_args": map[string]any{"value": float64(1)},
21
+ }, Invocation{SessionID: "session-test"}, ".")
22
+ if err != nil { t.Fatal(err) }
23
+ if result.Output.PermissionDecision != "allow" { t.Fatalf("decision = %q", result.Output.PermissionDecision) }
24
+ if result.Output.AdditionalContext != "Go policy" { t.Fatalf("context = %q", result.Output.AdditionalContext) }
25
+ if result.Input["tool_args"].(map[string]any)["bounded"] != true { t.Fatal("modified args not applied") }
26
+ if len(result.Receipts) != 1 || result.Receipts[0].Schema != ReceiptSchema { t.Fatal("receipt missing") }
27
+ }
28
+
29
+ func TestPermissionHooksFailClosed(t *testing.T) {
30
+ runtime := NewRuntime(nil)
31
+ _ = runtime.Register("pre_model_use", Definition{
32
+ ID: "broken", Timeout: time.Second,
33
+ Handler: func(context.Context, Envelope) (*Output, error) { return nil, context.DeadlineExceeded },
34
+ })
35
+ result, err := runtime.Dispatch(context.Background(), "pre_model_use", map[string]any{"request": map[string]any{}}, Invocation{}, ".")
36
+ if err != nil { t.Fatal(err) }
37
+ if result.Output.PermissionDecision != "deny" { t.Fatalf("decision = %q", result.Output.PermissionDecision) }
38
+ failure := result.Receipts[0].Error
39
+ if failure["error_code"] != "DEADLINE_EXCEEDED" || failure["reason"] != "hook_handler_timeout" { t.Fatalf("canonical failure = %#v", failure) }
40
+ if failure["failure_behavior"] != "fail_closed" || failure["side_effect_state"] != "not_started" { t.Fatalf("policy semantics = %#v", failure) }
41
+ }
42
+
43
+ func TestGeneratedErrorAliasesShareCanonicalPolicy(t *testing.T) {
44
+ reason := NormalizeErrorReason("AGENTSAM_HOOK_HTTP_FAILED")
45
+ if reason != "hook_http_request_failed" { t.Fatalf("reason = %q", reason) }
46
+ policy := ErrorReasonPolicies[reason]
47
+ if policy.Code != "UNAVAILABLE" || policy.Domain != "hook" || !policy.Retryable { t.Fatalf("policy = %#v", policy) }
48
+ }
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "agentsam-hooks"
3
+ version = "2.6.10"
4
+ edition = "2021"
5
+ description = "Language-neutral agentsam.hook.v1 contracts and callback runtime"
6
+ license = "MIT"
7
+ repository = "https://github.com/SamPrimeaux/agentsam-sdk"
8
+
9
+ [dependencies]
10
+ serde = { version = "1", features = ["derive"] }
11
+ serde_json = "1"