@guilz-dev/belay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/agent-belay-logo.png +0 -0
  4. package/dist/adapters/claude/adapter.d.ts +7 -0
  5. package/dist/adapters/claude/adapter.js +114 -0
  6. package/dist/adapters/claude/hooks.d.ts +13 -0
  7. package/dist/adapters/claude/hooks.js +49 -0
  8. package/dist/adapters/claude/runtime-entry.d.ts +4 -0
  9. package/dist/adapters/claude/runtime-entry.js +260 -0
  10. package/dist/adapters/codex/adapter.d.ts +7 -0
  11. package/dist/adapters/codex/adapter.js +73 -0
  12. package/dist/adapters/codex/hooks.d.ts +21 -0
  13. package/dist/adapters/codex/hooks.js +78 -0
  14. package/dist/adapters/codex/runtime-entry.d.ts +4 -0
  15. package/dist/adapters/codex/runtime-entry.js +237 -0
  16. package/dist/adapters/cursor/adapter.d.ts +7 -0
  17. package/dist/adapters/cursor/adapter.js +29 -0
  18. package/dist/adapters/cursor/hooks.d.ts +2 -0
  19. package/dist/adapters/cursor/hooks.js +26 -0
  20. package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
  21. package/dist/adapters/cursor/runtime-entry.js +143 -0
  22. package/dist/adapters/layouts/claude.d.ts +2 -0
  23. package/dist/adapters/layouts/claude.js +40 -0
  24. package/dist/adapters/layouts/codex.d.ts +2 -0
  25. package/dist/adapters/layouts/codex.js +43 -0
  26. package/dist/adapters/layouts/cursor.d.ts +2 -0
  27. package/dist/adapters/layouts/cursor.js +40 -0
  28. package/dist/adapters/layouts/index.d.ts +7 -0
  29. package/dist/adapters/layouts/index.js +23 -0
  30. package/dist/adapters/layouts/protected-paths.d.ts +3 -0
  31. package/dist/adapters/layouts/protected-paths.js +15 -0
  32. package/dist/adapters/layouts/scope.d.ts +19 -0
  33. package/dist/adapters/layouts/scope.js +86 -0
  34. package/dist/adapters/layouts/types.d.ts +14 -0
  35. package/dist/adapters/layouts/types.js +1 -0
  36. package/dist/adapters/registry.d.ts +4 -0
  37. package/dist/adapters/registry.js +14 -0
  38. package/dist/adapters/shared/gate-runtime.d.ts +51 -0
  39. package/dist/adapters/shared/gate-runtime.js +518 -0
  40. package/dist/adapters/shared/repo-root.d.ts +2 -0
  41. package/dist/adapters/shared/repo-root.js +17 -0
  42. package/dist/adapters/types.d.ts +19 -0
  43. package/dist/adapters/types.js +1 -0
  44. package/dist/branding.d.ts +3 -0
  45. package/dist/branding.js +3 -0
  46. package/dist/bundle/claude-runtime.mjs +5323 -0
  47. package/dist/bundle/codex-runtime.mjs +5310 -0
  48. package/dist/bundle/cursor-runtime.mjs +5208 -0
  49. package/dist/cleanup-orphans.d.ts +7 -0
  50. package/dist/cleanup-orphans.js +59 -0
  51. package/dist/cli.d.ts +2 -0
  52. package/dist/cli.js +631 -0
  53. package/dist/commands/approve.d.ts +14 -0
  54. package/dist/commands/approve.js +65 -0
  55. package/dist/commands/audit.d.ts +59 -0
  56. package/dist/commands/audit.js +132 -0
  57. package/dist/commands/classify-for-report.d.ts +9 -0
  58. package/dist/commands/classify-for-report.js +85 -0
  59. package/dist/commands/doctor.d.ts +3 -0
  60. package/dist/commands/doctor.js +366 -0
  61. package/dist/commands/dogfood.d.ts +5 -0
  62. package/dist/commands/dogfood.js +71 -0
  63. package/dist/commands/explain.d.ts +3 -0
  64. package/dist/commands/explain.js +133 -0
  65. package/dist/commands/health-snapshot.d.ts +2 -0
  66. package/dist/commands/health-snapshot.js +166 -0
  67. package/dist/commands/init-wizard.d.ts +16 -0
  68. package/dist/commands/init-wizard.js +50 -0
  69. package/dist/commands/metrics.d.ts +7 -0
  70. package/dist/commands/metrics.js +89 -0
  71. package/dist/commands/recover.d.ts +3 -0
  72. package/dist/commands/recover.js +105 -0
  73. package/dist/commands/report.d.ts +3 -0
  74. package/dist/commands/report.js +65 -0
  75. package/dist/commands/revoke.d.ts +5 -0
  76. package/dist/commands/revoke.js +22 -0
  77. package/dist/commands/simulate.d.ts +14 -0
  78. package/dist/commands/simulate.js +55 -0
  79. package/dist/commands/status.d.ts +5 -0
  80. package/dist/commands/status.js +107 -0
  81. package/dist/config-io.d.ts +23 -0
  82. package/dist/config-io.js +180 -0
  83. package/dist/conformance/guarantee-table.d.ts +14 -0
  84. package/dist/conformance/guarantee-table.js +95 -0
  85. package/dist/conformance/types.d.ts +6 -0
  86. package/dist/conformance/types.js +1 -0
  87. package/dist/core/approval-service.d.ts +26 -0
  88. package/dist/core/approval-service.js +41 -0
  89. package/dist/core/approval-token.d.ts +11 -0
  90. package/dist/core/approval-token.js +61 -0
  91. package/dist/core/approval.d.ts +19 -0
  92. package/dist/core/approval.js +58 -0
  93. package/dist/core/audit-analysis.d.ts +10 -0
  94. package/dist/core/audit-analysis.js +147 -0
  95. package/dist/core/audit-metrics.d.ts +51 -0
  96. package/dist/core/audit-metrics.js +155 -0
  97. package/dist/core/audit-query.d.ts +11 -0
  98. package/dist/core/audit-query.js +142 -0
  99. package/dist/core/audit-summary.d.ts +33 -0
  100. package/dist/core/audit-summary.js +111 -0
  101. package/dist/core/audit-types.d.ts +65 -0
  102. package/dist/core/audit-types.js +2 -0
  103. package/dist/core/capability/allowlist.d.ts +10 -0
  104. package/dist/core/capability/allowlist.js +53 -0
  105. package/dist/core/capability/broker.d.ts +17 -0
  106. package/dist/core/capability/broker.js +29 -0
  107. package/dist/core/capability/index.d.ts +5 -0
  108. package/dist/core/capability/index.js +4 -0
  109. package/dist/core/capability/paths.d.ts +1 -0
  110. package/dist/core/capability/paths.js +20 -0
  111. package/dist/core/capability/reasons.d.ts +2 -0
  112. package/dist/core/capability/reasons.js +4 -0
  113. package/dist/core/capability/types.d.ts +10 -0
  114. package/dist/core/capability/types.js +1 -0
  115. package/dist/core/capability-approval.d.ts +28 -0
  116. package/dist/core/capability-approval.js +43 -0
  117. package/dist/core/classify-subagent.d.ts +2 -0
  118. package/dist/core/classify-subagent.js +69 -0
  119. package/dist/core/classify-tool.d.ts +3 -0
  120. package/dist/core/classify-tool.js +311 -0
  121. package/dist/core/config-layers.d.ts +23 -0
  122. package/dist/core/config-layers.js +59 -0
  123. package/dist/core/config.d.ts +219 -0
  124. package/dist/core/config.js +720 -0
  125. package/dist/core/control-plane-isolation.d.ts +10 -0
  126. package/dist/core/control-plane-isolation.js +83 -0
  127. package/dist/core/custom-command-match.d.ts +2 -0
  128. package/dist/core/custom-command-match.js +8 -0
  129. package/dist/core/egress/allowlist.d.ts +7 -0
  130. package/dist/core/egress/allowlist.js +33 -0
  131. package/dist/core/egress/env.d.ts +3 -0
  132. package/dist/core/egress/env.js +17 -0
  133. package/dist/core/egress/fingerprint.d.ts +3 -0
  134. package/dist/core/egress/fingerprint.js +35 -0
  135. package/dist/core/egress/policy.d.ts +8 -0
  136. package/dist/core/egress/policy.js +47 -0
  137. package/dist/core/egress/proxy-server.d.ts +21 -0
  138. package/dist/core/egress/proxy-server.js +263 -0
  139. package/dist/core/egress/types.d.ts +25 -0
  140. package/dist/core/egress/types.js +1 -0
  141. package/dist/core/egress-approval.d.ts +48 -0
  142. package/dist/core/egress-approval.js +129 -0
  143. package/dist/core/fingerprint.d.ts +6 -0
  144. package/dist/core/fingerprint.js +24 -0
  145. package/dist/core/gate-contract.d.ts +48 -0
  146. package/dist/core/gate-contract.js +50 -0
  147. package/dist/core/gate-engine.d.ts +20 -0
  148. package/dist/core/gate-engine.js +172 -0
  149. package/dist/core/glob.d.ts +1 -0
  150. package/dist/core/glob.js +39 -0
  151. package/dist/core/index.d.ts +19 -0
  152. package/dist/core/index.js +15 -0
  153. package/dist/core/integrity.d.ts +15 -0
  154. package/dist/core/integrity.js +68 -0
  155. package/dist/core/judge-api-key.d.ts +4 -0
  156. package/dist/core/judge-api-key.js +11 -0
  157. package/dist/core/judge-config.d.ts +29 -0
  158. package/dist/core/judge-config.js +85 -0
  159. package/dist/core/judge-doctor.d.ts +7 -0
  160. package/dist/core/judge-doctor.js +124 -0
  161. package/dist/core/judgment.d.ts +6 -0
  162. package/dist/core/judgment.js +38 -0
  163. package/dist/core/notify.d.ts +13 -0
  164. package/dist/core/notify.js +44 -0
  165. package/dist/core/path-utils.d.ts +12 -0
  166. package/dist/core/path-utils.js +107 -0
  167. package/dist/core/reclassify.d.ts +15 -0
  168. package/dist/core/reclassify.js +82 -0
  169. package/dist/core/recover-advice.d.ts +30 -0
  170. package/dist/core/recover-advice.js +177 -0
  171. package/dist/core/recover-git-probe.d.ts +8 -0
  172. package/dist/core/recover-git-probe.js +50 -0
  173. package/dist/core/recover-select.d.ts +10 -0
  174. package/dist/core/recover-select.js +60 -0
  175. package/dist/core/scrub.d.ts +3 -0
  176. package/dist/core/scrub.js +87 -0
  177. package/dist/core/shell-substitution.d.ts +6 -0
  178. package/dist/core/shell-substitution.js +130 -0
  179. package/dist/core/shell-tokenizer.d.ts +5 -0
  180. package/dist/core/shell-tokenizer.js +129 -0
  181. package/dist/core/shell-unparseable.d.ts +4 -0
  182. package/dist/core/shell-unparseable.js +96 -0
  183. package/dist/core/transactional/diff-evaluator.d.ts +2 -0
  184. package/dist/core/transactional/diff-evaluator.js +84 -0
  185. package/dist/core/transactional/eligibility.d.ts +4 -0
  186. package/dist/core/transactional/eligibility.js +44 -0
  187. package/dist/core/transactional/git-worktree.d.ts +13 -0
  188. package/dist/core/transactional/git-worktree.js +189 -0
  189. package/dist/core/transactional/index.d.ts +5 -0
  190. package/dist/core/transactional/index.js +4 -0
  191. package/dist/core/transactional/reasons.d.ts +4 -0
  192. package/dist/core/transactional/reasons.js +8 -0
  193. package/dist/core/transactional/runner.d.ts +2 -0
  194. package/dist/core/transactional/runner.js +113 -0
  195. package/dist/core/transactional/types.d.ts +46 -0
  196. package/dist/core/transactional/types.js +1 -0
  197. package/dist/core/types.d.ts +90 -0
  198. package/dist/core/types.js +1 -0
  199. package/dist/core/v2/adapter.d.ts +14 -0
  200. package/dist/core/v2/adapter.js +118 -0
  201. package/dist/core/v2/containment.d.ts +19 -0
  202. package/dist/core/v2/containment.js +91 -0
  203. package/dist/core/v2/egress-classify.d.ts +7 -0
  204. package/dist/core/v2/egress-classify.js +216 -0
  205. package/dist/core/v2/fingerprint.d.ts +1 -0
  206. package/dist/core/v2/fingerprint.js +4 -0
  207. package/dist/core/v2/index.d.ts +12 -0
  208. package/dist/core/v2/index.js +10 -0
  209. package/dist/core/v2/judge-audit.d.ts +2 -0
  210. package/dist/core/v2/judge-audit.js +15 -0
  211. package/dist/core/v2/judge-factory.d.ts +25 -0
  212. package/dist/core/v2/judge-factory.js +75 -0
  213. package/dist/core/v2/judge-outbound.d.ts +12 -0
  214. package/dist/core/v2/judge-outbound.js +73 -0
  215. package/dist/core/v2/judge.d.ts +47 -0
  216. package/dist/core/v2/judge.js +264 -0
  217. package/dist/core/v2/launcher-resolve.d.ts +12 -0
  218. package/dist/core/v2/launcher-resolve.js +190 -0
  219. package/dist/core/v2/overrides.d.ts +7 -0
  220. package/dist/core/v2/overrides.js +37 -0
  221. package/dist/core/v2/parser.d.ts +21 -0
  222. package/dist/core/v2/parser.js +213 -0
  223. package/dist/core/v2/types.d.ts +67 -0
  224. package/dist/core/v2/types.js +1 -0
  225. package/dist/core/v2/verdict.d.ts +2 -0
  226. package/dist/core/v2/verdict.js +699 -0
  227. package/dist/corpus/evaluate.d.ts +24 -0
  228. package/dist/corpus/evaluate.js +69 -0
  229. package/dist/defaults.d.ts +18 -0
  230. package/dist/defaults.js +155 -0
  231. package/dist/egress-daemon.d.ts +1 -0
  232. package/dist/egress-daemon.js +52 -0
  233. package/dist/index.d.ts +17 -0
  234. package/dist/index.js +15 -0
  235. package/dist/installer/bootstrap.d.ts +5 -0
  236. package/dist/installer/bootstrap.js +61 -0
  237. package/dist/installer/runtime-artifacts.d.ts +3 -0
  238. package/dist/installer/runtime-artifacts.js +23 -0
  239. package/dist/installer/scope-config.d.ts +8 -0
  240. package/dist/installer/scope-config.js +25 -0
  241. package/dist/installer.d.ts +22 -0
  242. package/dist/installer.js +169 -0
  243. package/dist/node-resolution.d.ts +8 -0
  244. package/dist/node-resolution.js +237 -0
  245. package/dist/operational-insights.d.ts +19 -0
  246. package/dist/operational-insights.js +24 -0
  247. package/dist/presets.d.ts +4 -0
  248. package/dist/presets.js +95 -0
  249. package/dist/services/egress-service.d.ts +57 -0
  250. package/dist/services/egress-service.js +334 -0
  251. package/dist/services/sandbox-service.d.ts +38 -0
  252. package/dist/services/sandbox-service.js +95 -0
  253. package/dist/templates.d.ts +7 -0
  254. package/dist/templates.js +56 -0
  255. package/dist/types.d.ts +230 -0
  256. package/dist/types.js +1 -0
  257. package/dist/version.d.ts +1 -0
  258. package/dist/version.js +1 -0
  259. package/package.json +65 -0
  260. package/skills/belay/SKILL.md +52 -0
  261. package/skills/belay/belay-approve.md +7 -0
  262. package/skills/belay/belay-explain.md +11 -0
  263. package/skills/belay/belay-recover.md +13 -0
  264. package/skills/belay/belay-report.md +7 -0
  265. package/skills/belay/belay-status.md +9 -0
  266. package/skills/belay/belay-why.md +11 -0
@@ -0,0 +1,260 @@
1
+ import process from 'node:process';
2
+ import { unnormalizedGateVerdict } from '../../core/gate-contract.js';
3
+ import { claudeLayout } from '../layouts/claude.js';
4
+ import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateVerdictToClaudePreToolUseResponse, gateVerdictToClaudeUserPromptResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
5
+ import { findRepoRoot } from '../shared/repo-root.js';
6
+ async function readStdinJson() {
7
+ const chunks = [];
8
+ for await (const chunk of process.stdin) {
9
+ chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
10
+ }
11
+ const raw = chunks.join('').trim();
12
+ if (!raw) {
13
+ return {};
14
+ }
15
+ try {
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ function jsonResponse(value) {
23
+ process.stdout.write(`${JSON.stringify(value)}\n`);
24
+ }
25
+ async function loadRuntimeContext(cwd) {
26
+ const repoRoot = findRepoRoot(cwd, claudeLayout);
27
+ const configPath = claudeLayout.configPath(repoRoot);
28
+ const deps = createDefaultGateRuntimeDeps();
29
+ const config = await resolveGateConfig({ layout: claudeLayout, repoRoot, configPath }, deps);
30
+ return { layout: claudeLayout, repoRoot, config, configPath };
31
+ }
32
+ function mapClaudeToolName(toolName) {
33
+ if (toolName === 'Bash') {
34
+ return 'shell';
35
+ }
36
+ if (toolName === 'Task') {
37
+ return 'subagent';
38
+ }
39
+ if (toolName === 'Write' ||
40
+ toolName === 'Edit' ||
41
+ toolName === 'Delete' ||
42
+ toolName === 'NotebookEdit' ||
43
+ toolName === 'MultiEdit') {
44
+ return 'tool';
45
+ }
46
+ if (toolName.startsWith('mcp__')) {
47
+ return 'mcp';
48
+ }
49
+ return null;
50
+ }
51
+ function normalizeClaudeToolPayload(toolName, payload) {
52
+ if (toolName === 'Bash') {
53
+ const toolInput = payload.tool_input;
54
+ const command = toolInput && typeof toolInput === 'object'
55
+ ? String(toolInput.command ?? '')
56
+ : '';
57
+ return {
58
+ tool_name: 'Shell',
59
+ tool_input: { command },
60
+ };
61
+ }
62
+ if (toolName === 'Task') {
63
+ return payload;
64
+ }
65
+ if (toolName === 'Edit') {
66
+ const toolInput = payload.tool_input;
67
+ const filePath = toolInput && typeof toolInput === 'object'
68
+ ? String(toolInput.file_path ?? '')
69
+ : '';
70
+ return {
71
+ tool_name: 'StrReplace',
72
+ tool_input: { path: filePath },
73
+ };
74
+ }
75
+ if (toolName === 'Write') {
76
+ const toolInput = payload.tool_input;
77
+ const filePath = toolInput && typeof toolInput === 'object'
78
+ ? String(toolInput.file_path ?? '')
79
+ : '';
80
+ return {
81
+ tool_name: 'Write',
82
+ tool_input: { path: filePath },
83
+ };
84
+ }
85
+ if (toolName === 'Delete') {
86
+ const toolInput = payload.tool_input;
87
+ const filePath = toolInput && typeof toolInput === 'object'
88
+ ? String(toolInput.path ?? '')
89
+ : '';
90
+ return {
91
+ tool_name: 'Delete',
92
+ tool_input: { path: filePath },
93
+ };
94
+ }
95
+ if (toolName === 'NotebookEdit' || toolName === 'MultiEdit') {
96
+ const toolInput = payload.tool_input;
97
+ const input = toolInput && typeof toolInput === 'object' ? toolInput : null;
98
+ const directPath = typeof input?.file_path === 'string'
99
+ ? input.file_path
100
+ : typeof input?.path === 'string'
101
+ ? input.path
102
+ : null;
103
+ if (!directPath) {
104
+ return null;
105
+ }
106
+ return {
107
+ tool_name: toolName === 'NotebookEdit' ? 'Write' : 'StrReplace',
108
+ tool_input: { path: directPath },
109
+ };
110
+ }
111
+ return null;
112
+ }
113
+ export async function runBeforeSubmitPromptHook() {
114
+ try {
115
+ const payload = await readStdinJson();
116
+ const prompt = String(payload.prompt ?? process.env.CLAUDE_USER_PROMPT ?? '');
117
+ const ctx = await loadRuntimeContext(process.cwd());
118
+ const deps = createDefaultGateRuntimeDeps();
119
+ const result = await processApprovalPrompt(ctx, deps, prompt);
120
+ jsonResponse(gateVerdictToClaudeUserPromptResponse(result));
121
+ }
122
+ catch {
123
+ jsonResponse({
124
+ hookSpecificOutput: {
125
+ hookEventName: 'UserPromptSubmit',
126
+ continue: false,
127
+ user_message: 'belay failed while processing approval state. Run belay doctor, then retry.',
128
+ },
129
+ });
130
+ }
131
+ }
132
+ export async function runShellGateHook() {
133
+ try {
134
+ const payload = await readStdinJson();
135
+ const toolInput = payload.tool_input;
136
+ const command = toolInput && typeof toolInput === 'object'
137
+ ? String(toolInput.command ?? '')
138
+ : String(payload.command ?? '');
139
+ const cwd = process.cwd();
140
+ const ctx = await loadRuntimeContext(cwd);
141
+ const deps = createDefaultGateRuntimeDeps();
142
+ const verdict = await evaluateGatedAction(ctx, deps, {
143
+ kind: 'shell',
144
+ cwd,
145
+ command,
146
+ payload,
147
+ toolName: 'Bash',
148
+ });
149
+ jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
150
+ }
151
+ catch {
152
+ jsonResponse({
153
+ hookSpecificOutput: {
154
+ hookEventName: 'PreToolUse',
155
+ permissionDecision: 'deny',
156
+ permissionDecisionReason: 'belay failed while classifying this shell command. Run belay doctor, then retry.',
157
+ },
158
+ });
159
+ }
160
+ }
161
+ export async function runToolGateHook(_eventName) {
162
+ try {
163
+ const payload = await readStdinJson();
164
+ const cwd = process.cwd();
165
+ const ctx = await loadRuntimeContext(cwd);
166
+ const deps = createDefaultGateRuntimeDeps();
167
+ const toolName = String(payload.tool_name ?? '');
168
+ const mappedKind = mapClaudeToolName(toolName);
169
+ if (mappedKind === 'mcp') {
170
+ await deps.appendAudit(ctx, {
171
+ event: 'preToolUse',
172
+ kind: 'tool',
173
+ verdict: 'deny_pending_approval',
174
+ reason: 'unsupported_mcp_tool',
175
+ mode: ctx.config.mode,
176
+ wouldBlock: true,
177
+ permission: 'deny',
178
+ summary: toolName,
179
+ });
180
+ const verdict = unnormalizedGateVerdict({
181
+ reason: 'unsupported_mcp_tool',
182
+ mode: ctx.config.mode,
183
+ user_message: 'belay blocked this MCP tool because Claude MCP payloads are not normalized safely yet.',
184
+ agent_message: 'Belay denied this MCP tool because its payload shape is unsupported.',
185
+ });
186
+ jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
187
+ return;
188
+ }
189
+ if (!mappedKind) {
190
+ await deps.appendAudit(ctx, {
191
+ event: 'preToolUse',
192
+ kind: 'tool',
193
+ verdict: 'deny_pending_approval',
194
+ reason: 'unmapped_tool',
195
+ mode: ctx.config.mode,
196
+ wouldBlock: true,
197
+ permission: 'deny',
198
+ summary: toolName,
199
+ });
200
+ const verdict = unnormalizedGateVerdict({
201
+ reason: 'unmapped_tool',
202
+ mode: ctx.config.mode,
203
+ user_message: 'belay does not recognize this tool action. Run belay doctor, then retry.',
204
+ agent_message: 'Belay denied this action because the tool could not be normalized.',
205
+ });
206
+ jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
207
+ return;
208
+ }
209
+ const normalizedPayload = normalizeClaudeToolPayload(toolName, payload);
210
+ if (!normalizedPayload) {
211
+ await deps.appendAudit(ctx, {
212
+ event: 'preToolUse',
213
+ kind: 'tool',
214
+ verdict: 'deny_pending_approval',
215
+ reason: 'normalization_failed',
216
+ mode: ctx.config.mode,
217
+ wouldBlock: true,
218
+ permission: 'deny',
219
+ summary: toolName,
220
+ });
221
+ const verdict = unnormalizedGateVerdict({
222
+ reason: 'normalization_failed',
223
+ mode: ctx.config.mode,
224
+ user_message: 'belay could not normalize this Claude tool payload. Run belay doctor, then retry.',
225
+ agent_message: 'Belay denied this action because the Claude tool payload could not be normalized.',
226
+ });
227
+ jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
228
+ return;
229
+ }
230
+ const verdict = await evaluateGatedAction(ctx, deps, {
231
+ kind: mappedKind,
232
+ cwd,
233
+ payload: normalizedPayload,
234
+ toolName,
235
+ });
236
+ jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
237
+ }
238
+ catch {
239
+ jsonResponse({
240
+ hookSpecificOutput: {
241
+ hookEventName: 'PreToolUse',
242
+ permissionDecision: 'deny',
243
+ permissionDecisionReason: 'belay failed while classifying this tool action. Run belay doctor, then retry.',
244
+ },
245
+ });
246
+ }
247
+ }
248
+ export async function runAuditHook(eventName) {
249
+ try {
250
+ const payload = await readStdinJson();
251
+ const ctx = await loadRuntimeContext(process.cwd());
252
+ const deps = createDefaultGateRuntimeDeps();
253
+ await appendObservedAudit(ctx, deps, eventName, payload);
254
+ jsonResponse({});
255
+ }
256
+ catch (error) {
257
+ console.error('belay audit hook failed:', error instanceof Error ? error.message : String(error));
258
+ jsonResponse({});
259
+ }
260
+ }
@@ -0,0 +1,7 @@
1
+ import type { BelayAdapter } from '../types.js';
2
+ export declare const codexAdapter: BelayAdapter;
3
+ export declare function codexPaths(repoRoot: string): {
4
+ config: string;
5
+ hooks: string;
6
+ runtime: string;
7
+ };
@@ -0,0 +1,73 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { doctorProject } from '../../commands/doctor.js';
5
+ import { mergeAndWriteConfig } from '../../config-io.js';
6
+ import { runtimeIntegrityFiles, writeIntegrityManifest } from '../../core/integrity.js';
7
+ import { bootstrapStateFiles, writeSkillArtifacts } from '../../installer/bootstrap.js';
8
+ import { writeRuntimeArtifacts } from '../../installer/runtime-artifacts.js';
9
+ import { applyInstallScope, resolveOperationScope } from '../../installer/scope-config.js';
10
+ import { codexLayout } from '../layouts/codex.js';
11
+ import { resolveScopedPaths } from '../layouts/scope.js';
12
+ import { getCodexManagedHookEntries, mergeCodexHooksToml } from './hooks.js';
13
+ async function loadCodexConfigToml(configTomlPath) {
14
+ if (!existsSync(configTomlPath)) {
15
+ return '';
16
+ }
17
+ return readFile(configTomlPath, 'utf8');
18
+ }
19
+ async function writeCodexHooksConfig(paths, repoRoot) {
20
+ const configTomlPath = paths.hooksSettingsPath;
21
+ const existing = await loadCodexConfigToml(configTomlPath);
22
+ const merged = mergeCodexHooksToml(existing, process.platform, paths.hooksDir, repoRoot);
23
+ await mkdir(path.dirname(configTomlPath), { recursive: true });
24
+ await writeFile(configTomlPath, merged, 'utf8');
25
+ }
26
+ async function installCodexBase(repoRoot, options) {
27
+ const scope = await resolveOperationScope(repoRoot, 'codex', options);
28
+ const paths = resolveScopedPaths(codexLayout, scope, repoRoot);
29
+ const config = await mergeAndWriteConfig(repoRoot, 'codex');
30
+ await applyInstallScope(repoRoot, 'codex', scope, config);
31
+ await writeRuntimeArtifacts('codex', paths);
32
+ await bootstrapStateFiles(repoRoot, config, paths);
33
+ if (options.withSkill) {
34
+ await writeSkillArtifacts('codex', paths);
35
+ }
36
+ await writeCodexHooksConfig(paths, repoRoot);
37
+ await writeIntegrityManifest(repoRoot, codexLayout, runtimeIntegrityFiles(codexLayout, paths));
38
+ }
39
+ export const codexAdapter = {
40
+ name: 'codex',
41
+ layout: codexLayout,
42
+ async install(repoRoot, options = {}) {
43
+ await installCodexBase(repoRoot, options);
44
+ return { repoRoot, withSkill: options.withSkill === true };
45
+ },
46
+ async upgrade(repoRoot, options = {}) {
47
+ const scope = await resolveOperationScope(repoRoot, 'codex', options);
48
+ const paths = resolveScopedPaths(codexLayout, scope, repoRoot);
49
+ const config = await mergeAndWriteConfig(repoRoot, 'codex');
50
+ await applyInstallScope(repoRoot, 'codex', scope, config);
51
+ await writeRuntimeArtifacts('codex', paths);
52
+ await writeCodexHooksConfig(paths, repoRoot);
53
+ if (options.withSkill) {
54
+ await writeSkillArtifacts('codex', paths);
55
+ }
56
+ await writeIntegrityManifest(repoRoot, codexLayout, runtimeIntegrityFiles(codexLayout, paths));
57
+ return { repoRoot };
58
+ },
59
+ async doctor(options = {}) {
60
+ return doctorProject({ ...options, adapter: 'codex' });
61
+ },
62
+ hookEvents() {
63
+ return getCodexManagedHookEntries(process.platform);
64
+ },
65
+ };
66
+ export function codexPaths(repoRoot) {
67
+ const resolved = path.resolve(repoRoot);
68
+ return {
69
+ config: codexLayout.configPath(resolved),
70
+ hooks: codexLayout.hooksSettingsPath(resolved),
71
+ runtime: path.join(codexLayout.runtimeDir(resolved), 'core.mjs'),
72
+ };
73
+ }
@@ -0,0 +1,21 @@
1
+ export declare const CODEX_HOOKS_BEGIN = "# --- BELAY MANAGED HOOKS BEGIN (managed by belay; do not edit) ---";
2
+ export declare const CODEX_HOOKS_END = "# --- BELAY MANAGED HOOKS END ---";
3
+ /**
4
+ * Render belay's Codex lifecycle hooks as a marker-delimited TOML block for `.codex/config.toml`.
5
+ * The block is replaced wholesale on re-init/upgrade (see mergeCodexHooksToml), so we avoid a
6
+ * full TOML parser while staying idempotent.
7
+ */
8
+ export declare function renderCodexHooksToml(platform: NodeJS.Platform, hooksDir: string, repoRoot: string): string;
9
+ /**
10
+ * Merge belay's managed hooks block into an existing `.codex/config.toml` body idempotently:
11
+ * strip any prior BELAY MANAGED HOOKS block, then append the freshly rendered one.
12
+ */
13
+ export declare function mergeCodexHooksToml(existing: string, platform: NodeJS.Platform, hooksDir: string, repoRoot: string): string;
14
+ export declare function getCodexManagedHookEntries(platform?: NodeJS.Platform, hooksDir?: string, repoRoot?: string): Array<{
15
+ event: string;
16
+ definition: {
17
+ command: string;
18
+ placement: 'prepend';
19
+ matcher?: string;
20
+ };
21
+ }>;
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import { buildRunnerInvocation } from '../layouts/scope.js';
3
+ export const CODEX_HOOKS_BEGIN = '# --- BELAY MANAGED HOOKS BEGIN (managed by belay; do not edit) ---';
4
+ export const CODEX_HOOKS_END = '# --- BELAY MANAGED HOOKS END ---';
5
+ const HOOK_TIMEOUT_SECONDS = 30;
6
+ const CODEX_HOOK_SPECS = [
7
+ { event: 'PreToolUse', matcher: '.*', runnerArgs: ['belay-tool-gate', 'PreToolUse'] },
8
+ { event: 'SubagentStart', runnerArgs: ['belay-tool-gate', 'SubagentStart'] },
9
+ { event: 'UserPromptSubmit', runnerArgs: ['belay-before-submit'] },
10
+ { event: 'PostToolUse', runnerArgs: ['belay-audit', 'PostToolUse'] },
11
+ ];
12
+ function tomlString(value) {
13
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
14
+ }
15
+ function runnerCommand(platform, hooksDir, repoRoot, hookName, ...args) {
16
+ return buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
17
+ }
18
+ /**
19
+ * Render belay's Codex lifecycle hooks as a marker-delimited TOML block for `.codex/config.toml`.
20
+ * The block is replaced wholesale on re-init/upgrade (see mergeCodexHooksToml), so we avoid a
21
+ * full TOML parser while staying idempotent.
22
+ */
23
+ export function renderCodexHooksToml(platform, hooksDir, repoRoot) {
24
+ const lines = [CODEX_HOOKS_BEGIN];
25
+ for (const spec of CODEX_HOOK_SPECS) {
26
+ const command = runnerCommand(platform, hooksDir, repoRoot, spec.runnerArgs[0], ...spec.runnerArgs.slice(1));
27
+ lines.push('');
28
+ lines.push(`[[hooks.${spec.event}]]`);
29
+ if (spec.matcher !== undefined) {
30
+ lines.push(`matcher = ${tomlString(spec.matcher)}`);
31
+ }
32
+ lines.push(`[[hooks.${spec.event}.hooks]]`);
33
+ lines.push('type = "command"');
34
+ lines.push(`command = ${tomlString(command)}`);
35
+ lines.push(`timeout = ${HOOK_TIMEOUT_SECONDS}`);
36
+ }
37
+ lines.push('');
38
+ lines.push(CODEX_HOOKS_END);
39
+ return `${lines.join('\n')}\n`;
40
+ }
41
+ /**
42
+ * Merge belay's managed hooks block into an existing `.codex/config.toml` body idempotently:
43
+ * strip any prior BELAY MANAGED HOOKS block, then append the freshly rendered one.
44
+ */
45
+ export function mergeCodexHooksToml(existing, platform, hooksDir, repoRoot) {
46
+ const block = renderCodexHooksToml(platform, hooksDir, repoRoot);
47
+ const stripped = stripManagedBlock(existing);
48
+ const base = stripped.replace(/\s*$/, '');
49
+ if (base.length === 0) {
50
+ return block;
51
+ }
52
+ return `${base}\n\n${block}`;
53
+ }
54
+ function stripManagedBlock(content) {
55
+ const begin = content.indexOf(CODEX_HOOKS_BEGIN);
56
+ if (begin === -1) {
57
+ return content;
58
+ }
59
+ const endMarker = content.indexOf(CODEX_HOOKS_END, begin);
60
+ if (endMarker === -1) {
61
+ // Malformed (begin without end): drop from begin to EOF to avoid leaving a broken block.
62
+ return content.slice(0, begin);
63
+ }
64
+ return content.slice(0, begin) + content.slice(endMarker + CODEX_HOOKS_END.length);
65
+ }
66
+ // Used by adapter.hookEvents() for diagnostics/parity with other adapters.
67
+ export function getCodexManagedHookEntries(platform = process.platform, hooksDir, repoRoot) {
68
+ const resolvedRepo = path.resolve(repoRoot ?? process.cwd());
69
+ const resolvedHooksDir = hooksDir ?? path.join(resolvedRepo, '.codex', 'hooks');
70
+ return CODEX_HOOK_SPECS.map((spec) => ({
71
+ event: spec.event,
72
+ definition: {
73
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, spec.runnerArgs[0], ...spec.runnerArgs.slice(1)),
74
+ placement: 'prepend',
75
+ matcher: spec.matcher,
76
+ },
77
+ }));
78
+ }
@@ -0,0 +1,4 @@
1
+ export declare function runBeforeSubmitPromptHook(): Promise<void>;
2
+ export declare function runToolGateHook(eventName: string): Promise<void>;
3
+ export declare function runShellGateHook(): Promise<void>;
4
+ export declare function runAuditHook(eventName: string): Promise<void>;
@@ -0,0 +1,237 @@
1
+ import process from 'node:process';
2
+ import { codexLayout } from '../layouts/codex.js';
3
+ import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateUnmappedToolVerdict, gateVerdictToCodexPreToolUseResponse, gateVerdictToCodexUserPromptResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
4
+ import { findRepoRoot } from '../shared/repo-root.js';
5
+ async function readStdinJson() {
6
+ const chunks = [];
7
+ for await (const chunk of process.stdin) {
8
+ chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
9
+ }
10
+ const raw = chunks.join('').trim();
11
+ if (!raw) {
12
+ return {};
13
+ }
14
+ try {
15
+ return JSON.parse(raw);
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ function jsonResponse(value) {
22
+ process.stdout.write(`${JSON.stringify(value)}\n`);
23
+ }
24
+ async function loadRuntimeContext(cwd) {
25
+ const repoRoot = findRepoRoot(cwd, codexLayout);
26
+ const configPath = codexLayout.configPath(repoRoot);
27
+ const deps = createDefaultGateRuntimeDeps();
28
+ const config = await resolveGateConfig({ layout: codexLayout, repoRoot, configPath }, deps);
29
+ return { layout: codexLayout, repoRoot, config, configPath };
30
+ }
31
+ // shell is confirmed as tool_name:"Bash" / tool_input:{command} (TUI smoke).
32
+ // Non-shell names (apply_patch / read-family / subagent variants) are still best-guess pending
33
+ // the belay-adapter TUI smoke (G-B2). Unknown names ask with pending approval (R39).
34
+ function mapCodexToolName(toolName) {
35
+ const name = toolName.toLowerCase();
36
+ if (name === 'shell' ||
37
+ name === 'bash' ||
38
+ name === 'local_shell' ||
39
+ name === 'exec_command' ||
40
+ name === 'unified_exec') {
41
+ return 'shell';
42
+ }
43
+ if (name === 'task' || name === 'spawn' || name === 'subagent') {
44
+ return 'subagent';
45
+ }
46
+ if (name === 'read' ||
47
+ name === 'grep' ||
48
+ name === 'glob' ||
49
+ name === 'ls' ||
50
+ name === 'view' ||
51
+ name === 'search' ||
52
+ name === 'apply_patch' ||
53
+ name === 'write' ||
54
+ name === 'edit' ||
55
+ name === 'multiedit' ||
56
+ name === 'patch' ||
57
+ name === 'delete' ||
58
+ name === 'strreplace' ||
59
+ name === 'str_replace') {
60
+ return 'tool';
61
+ }
62
+ return null;
63
+ }
64
+ function resolveCodexGateKind(eventName, toolName) {
65
+ if (eventName === 'SubagentStart') {
66
+ return 'subagent';
67
+ }
68
+ return mapCodexToolName(toolName);
69
+ }
70
+ function extractString(value, ...keys) {
71
+ if (!value || typeof value !== 'object') {
72
+ return '';
73
+ }
74
+ const record = value;
75
+ for (const key of keys) {
76
+ if (typeof record[key] === 'string') {
77
+ return record[key];
78
+ }
79
+ }
80
+ return '';
81
+ }
82
+ function normalizeCodexToolPayload(kind, payload) {
83
+ const toolInput = payload.tool_input;
84
+ if (kind === 'shell') {
85
+ return {
86
+ tool_name: 'Shell',
87
+ tool_input: { command: extractString(toolInput, 'command', 'cmd') },
88
+ };
89
+ }
90
+ if (kind === 'tool') {
91
+ const toolName = String(payload.tool_name ?? payload.toolName ?? '');
92
+ const lowered = toolName.toLowerCase();
93
+ if (lowered === 'write') {
94
+ return {
95
+ tool_name: 'Write',
96
+ tool_input: {
97
+ path: extractString(toolInput, 'path', 'file_path', 'filename'),
98
+ },
99
+ };
100
+ }
101
+ if (lowered === 'delete') {
102
+ return {
103
+ tool_name: 'Delete',
104
+ tool_input: {
105
+ path: extractString(toolInput, 'path', 'file_path', 'filename'),
106
+ },
107
+ };
108
+ }
109
+ if (lowered === 'edit' ||
110
+ lowered === 'multiedit' ||
111
+ lowered === 'patch' ||
112
+ lowered === 'strreplace' ||
113
+ lowered === 'str_replace') {
114
+ return {
115
+ tool_name: 'StrReplace',
116
+ tool_input: {
117
+ path: extractString(toolInput, 'path', 'file_path', 'filename'),
118
+ },
119
+ };
120
+ }
121
+ if (lowered === 'apply_patch') {
122
+ return {
123
+ tool_name: 'ApplyPatch',
124
+ tool_input: {
125
+ patch: extractString(toolInput, 'patch', 'input', 'text'),
126
+ },
127
+ };
128
+ }
129
+ return {
130
+ tool_name: toolName,
131
+ tool_input: typeof toolInput === 'object' && toolInput ? toolInput : {},
132
+ };
133
+ }
134
+ return payload;
135
+ }
136
+ export async function runBeforeSubmitPromptHook() {
137
+ try {
138
+ const payload = await readStdinJson();
139
+ const prompt = String(payload.prompt ?? payload.user_message ?? '');
140
+ const ctx = await loadRuntimeContext(process.cwd());
141
+ const deps = createDefaultGateRuntimeDeps();
142
+ const result = await processApprovalPrompt(ctx, deps, prompt);
143
+ jsonResponse(gateVerdictToCodexUserPromptResponse(result));
144
+ }
145
+ catch {
146
+ jsonResponse({
147
+ decision: 'block',
148
+ reason: 'belay failed while processing approval state. Run belay doctor, then retry.',
149
+ });
150
+ }
151
+ }
152
+ // Codex routes all PreToolUse through this unified handler (matcher ".*"), since the exact
153
+ // shell tool name is not yet confirmed. SubagentStart is also routed to this handler.
154
+ // Unmapped tools/events ask with pending approval (R39) to avoid silent bypass without hard block.
155
+ export async function runToolGateHook(eventName) {
156
+ try {
157
+ const payload = await readStdinJson();
158
+ const cwd = process.cwd();
159
+ const toolName = String(payload.tool_name ?? payload.toolName ?? '');
160
+ const kind = resolveCodexGateKind(eventName, toolName);
161
+ const ctx = await loadRuntimeContext(cwd);
162
+ const deps = createDefaultGateRuntimeDeps();
163
+ if (!kind) {
164
+ // Unmapped Codex tool. Policy-driven: default 'deny' asks with pending approval (R39).
165
+ // 'allow' is the opt-out — pass the tool but record it to audit for vocabulary learning.
166
+ const policy = ctx.config.policy?.codexUnmappedTool ?? 'deny';
167
+ if (policy === 'allow') {
168
+ await appendObservedAudit(ctx, deps, eventName, payload);
169
+ jsonResponse({});
170
+ return;
171
+ }
172
+ const verdict = await gateUnmappedToolVerdict(ctx, deps, toolName, payload);
173
+ jsonResponse(gateVerdictToCodexPreToolUseResponse(verdict));
174
+ return;
175
+ }
176
+ const normalizedPayload = normalizeCodexToolPayload(kind, payload);
177
+ const verdict = await evaluateGatedAction(ctx, deps, {
178
+ kind,
179
+ cwd,
180
+ command: kind === 'shell' ? extractString(normalizedPayload.tool_input, 'command') : undefined,
181
+ payload: normalizedPayload,
182
+ toolName,
183
+ });
184
+ jsonResponse(gateVerdictToCodexPreToolUseResponse(verdict));
185
+ }
186
+ catch {
187
+ // Fail-closed: deny on classifier failure (belay is a floor).
188
+ jsonResponse({
189
+ hookSpecificOutput: {
190
+ hookEventName: 'PreToolUse',
191
+ permissionDecision: 'deny',
192
+ permissionDecisionReason: 'belay failed while classifying this tool action. Run belay doctor, then retry.',
193
+ },
194
+ });
195
+ }
196
+ }
197
+ // Provided for symmetry with the shared templates; not wired by default (Codex routes shell
198
+ // through the unified PreToolUse handler above).
199
+ export async function runShellGateHook() {
200
+ try {
201
+ const payload = await readStdinJson();
202
+ const command = extractString(payload.tool_input, 'command') || String(payload.command ?? '');
203
+ const cwd = process.cwd();
204
+ const ctx = await loadRuntimeContext(cwd);
205
+ const deps = createDefaultGateRuntimeDeps();
206
+ const verdict = await evaluateGatedAction(ctx, deps, {
207
+ kind: 'shell',
208
+ cwd,
209
+ command,
210
+ payload,
211
+ toolName: 'Shell',
212
+ });
213
+ jsonResponse(gateVerdictToCodexPreToolUseResponse(verdict));
214
+ }
215
+ catch {
216
+ jsonResponse({
217
+ hookSpecificOutput: {
218
+ hookEventName: 'PreToolUse',
219
+ permissionDecision: 'deny',
220
+ permissionDecisionReason: 'belay failed while classifying this shell command. Run belay doctor, then retry.',
221
+ },
222
+ });
223
+ }
224
+ }
225
+ export async function runAuditHook(eventName) {
226
+ try {
227
+ const payload = await readStdinJson();
228
+ const ctx = await loadRuntimeContext(process.cwd());
229
+ const deps = createDefaultGateRuntimeDeps();
230
+ await appendObservedAudit(ctx, deps, eventName, payload);
231
+ jsonResponse({});
232
+ }
233
+ catch (error) {
234
+ console.error('belay audit hook failed:', error instanceof Error ? error.message : String(error));
235
+ jsonResponse({});
236
+ }
237
+ }