@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,147 @@
1
+ import { inferWouldBlock, isGateRecord, parseTimestamp } from './audit-query.js';
2
+ const WRAPPER_TERMS = ['bash -c', 'sh -c', 'eval ', 'source ', 'node -e', '| bash', '| sh'];
3
+ function normalizeSummary(summary) {
4
+ return summary.replace(/\s+/g, ' ').trim().toLowerCase();
5
+ }
6
+ function tokenOverlap(left, right) {
7
+ const leftTokens = new Set(normalizeSummary(left).split(' ').filter(Boolean));
8
+ const rightTokens = new Set(normalizeSummary(right).split(' ').filter(Boolean));
9
+ if (leftTokens.size === 0 || rightTokens.size === 0) {
10
+ return 0;
11
+ }
12
+ let overlap = 0;
13
+ for (const token of leftTokens) {
14
+ if (rightTokens.has(token)) {
15
+ overlap += 1;
16
+ }
17
+ }
18
+ return overlap / Math.max(leftTokens.size, rightTokens.size);
19
+ }
20
+ function hasWrapperPattern(summary) {
21
+ const normalized = normalizeSummary(summary);
22
+ return WRAPPER_TERMS.some((term) => normalized.includes(term));
23
+ }
24
+ export function detectBypassAttempts(records, windowMs = 5 * 60_000) {
25
+ const attempts = [];
26
+ const recentDenies = [];
27
+ for (const record of records) {
28
+ const timestampMs = parseTimestamp(record.timestamp) ?? 0;
29
+ if (isGateRecord(record) && inferWouldBlock(record) && record.fingerprint) {
30
+ recentDenies.push({ timestampMs, record });
31
+ continue;
32
+ }
33
+ if (!isGateRecord(record) || inferWouldBlock(record)) {
34
+ continue;
35
+ }
36
+ const summary = record.summary ?? '';
37
+ const fingerprint = record.fingerprint ?? '';
38
+ for (const deny of recentDenies) {
39
+ if (timestampMs - deny.timestampMs > windowMs) {
40
+ continue;
41
+ }
42
+ if (deny.record.fingerprint === fingerprint) {
43
+ continue;
44
+ }
45
+ const denySummary = deny.record.summary ?? '';
46
+ const similarity = tokenOverlap(denySummary, summary);
47
+ if (similarity >= 0.6 || hasWrapperPattern(summary)) {
48
+ attempts.push({
49
+ afterDenyTimestamp: deny.record.timestamp ?? '',
50
+ denyFingerprint: deny.record.fingerprint ?? '',
51
+ denySummary,
52
+ attemptTimestamp: record.timestamp ?? '',
53
+ attemptSummary: summary,
54
+ attemptFingerprint: fingerprint,
55
+ signal: hasWrapperPattern(summary) ? 'wrapper_pattern' : 'similar_command',
56
+ });
57
+ }
58
+ }
59
+ }
60
+ for (const record of records) {
61
+ const signals = record.assessment?.signals ?? [];
62
+ if (!signals.includes('agent_assessment_mismatch')) {
63
+ continue;
64
+ }
65
+ attempts.push({
66
+ afterDenyTimestamp: record.timestamp ?? '',
67
+ denyFingerprint: record.fingerprint ?? '',
68
+ denySummary: record.summary ?? '',
69
+ attemptTimestamp: record.timestamp ?? '',
70
+ attemptSummary: record.summary ?? '',
71
+ attemptFingerprint: record.fingerprint ?? '',
72
+ signal: 'agent_assessment_mismatch',
73
+ });
74
+ }
75
+ return attempts;
76
+ }
77
+ export function detectNoisyRules(records, roundTrips, minDenies = 2) {
78
+ const denyByReason = new Map();
79
+ const approvedByReason = new Map();
80
+ for (const record of records) {
81
+ if (!isGateRecord(record) || !inferWouldBlock(record)) {
82
+ continue;
83
+ }
84
+ const reason = record.reason ?? 'unknown';
85
+ denyByReason.set(reason, (denyByReason.get(reason) ?? 0) + 1);
86
+ }
87
+ for (const trip of roundTrips) {
88
+ if (!trip.approvalTimestamp) {
89
+ continue;
90
+ }
91
+ approvedByReason.set(trip.reason, (approvedByReason.get(trip.reason) ?? 0) + 1);
92
+ }
93
+ const candidates = [];
94
+ for (const [reason, denyCount] of denyByReason) {
95
+ if (denyCount < minDenies) {
96
+ continue;
97
+ }
98
+ const approvedCount = approvedByReason.get(reason) ?? 0;
99
+ const approvalRate = denyCount > 0 ? approvedCount / denyCount : 0;
100
+ if (approvalRate >= 0.5) {
101
+ candidates.push({ reason, denyCount, approvedCount, approvalRate });
102
+ }
103
+ }
104
+ return candidates.sort((left, right) => right.approvalRate - left.approvalRate);
105
+ }
106
+ export function computeApprovalLatencyStats(roundTrips) {
107
+ const latencies = roundTrips
108
+ .map((trip) => trip.approvalLatencyMs)
109
+ .filter((value) => typeof value === 'number' && value >= 0)
110
+ .sort((left, right) => left - right);
111
+ if (latencies.length === 0) {
112
+ return { count: 0, medianMs: null, p95Ms: null };
113
+ }
114
+ const medianIndex = Math.floor(latencies.length / 2);
115
+ const p95Index = Math.min(latencies.length - 1, Math.ceil(latencies.length * 0.95) - 1);
116
+ return {
117
+ count: latencies.length,
118
+ medianMs: latencies[medianIndex] ?? null,
119
+ p95Ms: latencies[p95Index] ?? null,
120
+ };
121
+ }
122
+ export function bucketGateEventsByDay(records) {
123
+ const buckets = {};
124
+ for (const record of records) {
125
+ if (!isGateRecord(record)) {
126
+ continue;
127
+ }
128
+ const timestampMs = parseTimestamp(record.timestamp);
129
+ if (timestampMs === null) {
130
+ continue;
131
+ }
132
+ const day = new Date(timestampMs).toISOString().slice(0, 10);
133
+ buckets[day] = (buckets[day] ?? 0) + 1;
134
+ }
135
+ return buckets;
136
+ }
137
+ export function countVerdicts(records) {
138
+ const counts = {};
139
+ for (const record of records) {
140
+ if (!isGateRecord(record)) {
141
+ continue;
142
+ }
143
+ const verdict = record.verdict ?? 'unknown';
144
+ counts[verdict] = (counts[verdict] ?? 0) + 1;
145
+ }
146
+ return counts;
147
+ }
@@ -0,0 +1,51 @@
1
+ import { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord } from './audit-query.js';
2
+ /** Minimum gate events before recommending enforce with zero would-block rate. */
3
+ export declare const MIN_GATE_EVENTS_FOR_ENFORCE = 20;
4
+ export interface AuditMetricsReport {
5
+ schemaVersion: number;
6
+ auditLogPath: string;
7
+ totalLines: number;
8
+ parsedRecords: number;
9
+ gateEvents: number;
10
+ wouldBlockCount: number;
11
+ wouldBlockRate: number;
12
+ byReason: Record<string, number>;
13
+ byKind: Record<string, number>;
14
+ byVerdict: Record<string, number>;
15
+ byLocation: Record<string, number>;
16
+ byOpacity: Record<string, number>;
17
+ byEffect: Record<string, number>;
18
+ byConfidence: Record<string, number>;
19
+ approvalRecordedCount: number;
20
+ topWouldBlockSummaries: Array<{
21
+ summary: string;
22
+ reason: string;
23
+ count: number;
24
+ }>;
25
+ approvalLatency: {
26
+ count: number;
27
+ medianMs: number | null;
28
+ p95Ms: number | null;
29
+ };
30
+ gateEventsByDay: Record<string, number>;
31
+ bypassAttemptCount: number;
32
+ noisyRuleCandidates: Array<{
33
+ reason: string;
34
+ denyCount: number;
35
+ approvedCount: number;
36
+ approvalRate: number;
37
+ }>;
38
+ dogfood: {
39
+ mode: string | null;
40
+ unknownLocalEffect: string | null;
41
+ readyForEnforce: boolean;
42
+ notes: string[];
43
+ };
44
+ }
45
+ export declare function parseAuditNdjson(raw: string): Record<string, unknown>[];
46
+ export declare function computeAuditMetrics(records: Record<string, unknown>[], options?: {
47
+ auditLogPath?: string;
48
+ mode?: string;
49
+ unknownLocalEffect?: string;
50
+ }): AuditMetricsReport;
51
+ export { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord };
@@ -0,0 +1,155 @@
1
+ import { bucketGateEventsByDay, computeApprovalLatencyStats, countVerdicts, detectBypassAttempts, detectNoisyRules, } from './audit-analysis.js';
2
+ import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, toAuditRecord, } from './audit-query.js';
3
+ import { AUDIT_METRICS_SCHEMA_VERSION, GATE_EVENTS } from './audit-types.js';
4
+ /** Minimum gate events before recommending enforce with zero would-block rate. */
5
+ export const MIN_GATE_EVENTS_FOR_ENFORCE = 20;
6
+ export function parseAuditNdjson(raw) {
7
+ const records = [];
8
+ for (const line of raw.split('\n')) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed) {
11
+ continue;
12
+ }
13
+ try {
14
+ records.push(JSON.parse(trimmed));
15
+ }
16
+ catch {
17
+ // skip malformed lines
18
+ }
19
+ }
20
+ return records;
21
+ }
22
+ function increment(bucket, key) {
23
+ bucket[key] = (bucket[key] ?? 0) + 1;
24
+ }
25
+ export function computeAuditMetrics(records, options = {}) {
26
+ const auditRecords = records.map(toAuditRecord);
27
+ const byReason = {};
28
+ const byKind = {};
29
+ const byLocation = {};
30
+ const byOpacity = {};
31
+ const byEffect = {};
32
+ const byConfidence = {};
33
+ const summaryCounts = new Map();
34
+ let gateEvents = 0;
35
+ let wouldBlockCount = 0;
36
+ let approvalRecordedCount = 0;
37
+ for (const record of auditRecords) {
38
+ const event = typeof record.event === 'string' ? record.event : '';
39
+ if (isApprovalRecorded(record)) {
40
+ approvalRecordedCount += 1;
41
+ continue;
42
+ }
43
+ if (!GATE_EVENTS.has(event)) {
44
+ continue;
45
+ }
46
+ gateEvents += 1;
47
+ const reason = typeof record.reason === 'string' ? record.reason : 'unknown';
48
+ const kind = typeof record.kind === 'string' ? record.kind : 'unknown';
49
+ increment(byReason, reason);
50
+ increment(byKind, kind);
51
+ if (typeof record.location === 'string') {
52
+ increment(byLocation, record.location);
53
+ }
54
+ if (typeof record.opacity === 'string') {
55
+ increment(byOpacity, record.opacity);
56
+ }
57
+ if (typeof record.effect === 'string') {
58
+ increment(byEffect, record.effect);
59
+ }
60
+ if (typeof record.confidence === 'string') {
61
+ increment(byConfidence, record.confidence);
62
+ }
63
+ if (inferWouldBlock(record)) {
64
+ wouldBlockCount += 1;
65
+ const summary = typeof record.summary === 'string' ? record.summary : '';
66
+ const key = `${reason}::${summary}`;
67
+ const existing = summaryCounts.get(key);
68
+ if (existing) {
69
+ existing.count += 1;
70
+ }
71
+ else {
72
+ summaryCounts.set(key, { summary, reason, count: 1 });
73
+ }
74
+ }
75
+ }
76
+ const byVerdict = countVerdicts(auditRecords);
77
+ const roundTrips = buildApprovalRoundTrips(auditRecords);
78
+ const approvalLatency = computeApprovalLatencyStats(roundTrips);
79
+ const bypassAttempts = detectBypassAttempts(auditRecords);
80
+ const noisyRuleCandidates = detectNoisyRules(auditRecords, roundTrips);
81
+ const wouldBlockRate = gateEvents > 0 ? wouldBlockCount / gateEvents : 0;
82
+ const topWouldBlockSummaries = [...summaryCounts.values()]
83
+ .sort((left, right) => right.count - left.count)
84
+ .slice(0, 10);
85
+ const mode = options.mode ?? null;
86
+ const unknownLocalEffect = options.unknownLocalEffect ?? null;
87
+ const notes = [];
88
+ let readyForEnforce = false;
89
+ if (mode === 'audit' && unknownLocalEffect === 'deny') {
90
+ notes.push('Dogfood config detected: audit mode with fail-closed shell policy.');
91
+ if (gateEvents === 0) {
92
+ notes.push('No gate events yet — run normal agent work, then re-check metrics.');
93
+ }
94
+ else if (wouldBlockRate === 0) {
95
+ if (gateEvents >= MIN_GATE_EVENTS_FOR_ENFORCE) {
96
+ readyForEnforce = true;
97
+ notes.push('No would-block events recorded — safe to try mode: "enforce".');
98
+ }
99
+ else {
100
+ notes.push(`Only ${gateEvents} gate event(s) recorded — collect at least ${MIN_GATE_EVENTS_FOR_ENFORCE} before enforce.`);
101
+ }
102
+ }
103
+ else {
104
+ notes.push(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic). Review top summaries and add overrides.allow where appropriate.`);
105
+ if (approvalRecordedCount > 0) {
106
+ notes.push(`${approvalRecordedCount} approval(s) recorded — these likely indicate actions operators wanted.`);
107
+ }
108
+ else {
109
+ notes.push('Review top would-block summaries and add overrides.allow for legitimate commands before switching to enforce.');
110
+ }
111
+ if (wouldBlockRate < 0.05 && gateEvents >= 20) {
112
+ readyForEnforce = true;
113
+ notes.push('Would-block rate is below 5% with sufficient sample size — consider enforce mode.');
114
+ }
115
+ }
116
+ }
117
+ else if (mode !== 'audit') {
118
+ notes.push('Config is not in audit mode — metrics show enforce-time behavior.');
119
+ }
120
+ else {
121
+ notes.push('Set policy.unknownLocalEffect to "deny" to dogfood fail-closed defaults.');
122
+ }
123
+ if (noisyRuleCandidates.length > 0) {
124
+ notes.push(`${noisyRuleCandidates.length} noisy rule candidate(s) — high deny-then-approve rate.`);
125
+ }
126
+ return {
127
+ schemaVersion: AUDIT_METRICS_SCHEMA_VERSION,
128
+ auditLogPath: options.auditLogPath ?? 'belay/audit.ndjson',
129
+ totalLines: records.length,
130
+ parsedRecords: records.length,
131
+ gateEvents,
132
+ wouldBlockCount,
133
+ wouldBlockRate,
134
+ byReason,
135
+ byKind,
136
+ byVerdict,
137
+ byLocation,
138
+ byOpacity,
139
+ byEffect,
140
+ byConfidence,
141
+ approvalRecordedCount,
142
+ topWouldBlockSummaries,
143
+ approvalLatency,
144
+ gateEventsByDay: bucketGateEventsByDay(auditRecords),
145
+ bypassAttemptCount: bypassAttempts.length,
146
+ noisyRuleCandidates,
147
+ dogfood: {
148
+ mode,
149
+ unknownLocalEffect,
150
+ readyForEnforce,
151
+ notes,
152
+ },
153
+ };
154
+ }
155
+ export { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord };
@@ -0,0 +1,11 @@
1
+ import type { ApprovalRoundTrip, AuditFilter, AuditRecord } from './audit-types.js';
2
+ export declare function toAuditRecord(value: Record<string, unknown>): AuditRecord;
3
+ export declare function parseTimestamp(value?: string): number | null;
4
+ export declare function isGateRecord(record: AuditRecord): boolean;
5
+ export declare function isApprovalRecorded(record: AuditRecord): boolean;
6
+ export declare function inferWouldBlock(record: AuditRecord): boolean;
7
+ export declare function recordStringField(record: AuditRecord, field: 'effect' | 'reason' | 'permission' | 'verdict' | 'mode' | 'summary' | 'fingerprint' | 'location'): string;
8
+ export declare function isSilentPassRecord(record: AuditRecord): boolean;
9
+ export declare function filterAuditRecords(records: AuditRecord[], filter?: AuditFilter): AuditRecord[];
10
+ export declare function buildApprovalRoundTrips(records: AuditRecord[]): ApprovalRoundTrip[];
11
+ export declare function summarizeRoundTrips(trips: ApprovalRoundTrip[]): string[];
@@ -0,0 +1,142 @@
1
+ import { GATE_EVENTS } from './audit-types.js';
2
+ export function toAuditRecord(value) {
3
+ return value;
4
+ }
5
+ export function parseTimestamp(value) {
6
+ if (!value) {
7
+ return null;
8
+ }
9
+ const parsed = Date.parse(value);
10
+ return Number.isNaN(parsed) ? null : parsed;
11
+ }
12
+ export function isGateRecord(record) {
13
+ return typeof record.event === 'string' && GATE_EVENTS.has(record.event);
14
+ }
15
+ export function isApprovalRecorded(record) {
16
+ return ((record.event === 'approval' ||
17
+ (record.event === 'beforeSubmitPrompt' && record.reason === 'approval_recorded')) &&
18
+ record.reason === 'approval_recorded');
19
+ }
20
+ export function inferWouldBlock(record) {
21
+ if (typeof record.wouldBlock === 'boolean') {
22
+ return record.wouldBlock;
23
+ }
24
+ return record.verdict === 'deny_pending_approval';
25
+ }
26
+ export function recordStringField(record, field) {
27
+ const value = record[field];
28
+ return typeof value === 'string' ? value : '';
29
+ }
30
+ export function isSilentPassRecord(record) {
31
+ const permission = recordStringField(record, 'permission');
32
+ const verdict = recordStringField(record, 'verdict');
33
+ return (permission === 'allow' ||
34
+ permission === 'allow_flagged' ||
35
+ verdict === 'allow' ||
36
+ verdict === 'allow_flagged');
37
+ }
38
+ export function filterAuditRecords(records, filter = {}) {
39
+ const sinceMs = parseTimestamp(filter.since);
40
+ const untilMs = parseTimestamp(filter.until);
41
+ let filtered = records.filter((record) => {
42
+ const timestampMs = parseTimestamp(record.timestamp);
43
+ if (sinceMs !== null && (timestampMs === null || timestampMs < sinceMs)) {
44
+ return false;
45
+ }
46
+ if (untilMs !== null && (timestampMs === null || timestampMs > untilMs)) {
47
+ return false;
48
+ }
49
+ if (filter.verdict && record.verdict !== filter.verdict) {
50
+ return false;
51
+ }
52
+ if (filter.reason && record.reason !== filter.reason) {
53
+ return false;
54
+ }
55
+ if (filter.kind && record.kind !== filter.kind) {
56
+ return false;
57
+ }
58
+ if (filter.fingerprint && record.fingerprint !== filter.fingerprint) {
59
+ return false;
60
+ }
61
+ if (filter.event && record.event !== filter.event) {
62
+ return false;
63
+ }
64
+ if (filter.location && record.location !== filter.location) {
65
+ return false;
66
+ }
67
+ if (filter.opacity && record.opacity !== filter.opacity) {
68
+ return false;
69
+ }
70
+ if (filter.effect && record.effect !== filter.effect) {
71
+ return false;
72
+ }
73
+ if (filter.confidence && record.confidence !== filter.confidence) {
74
+ return false;
75
+ }
76
+ return true;
77
+ });
78
+ if (typeof filter.limit === 'number' && filter.limit > 0) {
79
+ filtered = filtered.slice(-filter.limit);
80
+ }
81
+ return filtered;
82
+ }
83
+ export function buildApprovalRoundTrips(records) {
84
+ const trips = [];
85
+ const pendingByApprovalId = new Map();
86
+ const pendingByFingerprint = new Map();
87
+ for (const record of records) {
88
+ const timestamp = record.timestamp ?? '';
89
+ if (isGateRecord(record) && inferWouldBlock(record) && record.fingerprint) {
90
+ const trip = {
91
+ denyTimestamp: timestamp,
92
+ fingerprint: record.fingerprint,
93
+ reason: record.reason ?? 'unknown',
94
+ summary: record.summary ?? '',
95
+ kind: record.kind ?? 'unknown',
96
+ approvalId: record.approvalId,
97
+ };
98
+ trips.push(trip);
99
+ if (record.approvalId) {
100
+ pendingByApprovalId.set(record.approvalId, trip);
101
+ }
102
+ pendingByFingerprint.set(record.fingerprint, trip);
103
+ continue;
104
+ }
105
+ if (isApprovalRecorded(record) && record.approvalId) {
106
+ const trip = pendingByApprovalId.get(record.approvalId);
107
+ if (trip) {
108
+ trip.approvalTimestamp = timestamp;
109
+ const denyMs = parseTimestamp(trip.denyTimestamp);
110
+ const approvalMs = parseTimestamp(timestamp);
111
+ if (denyMs !== null && approvalMs !== null) {
112
+ trip.approvalLatencyMs = approvalMs - denyMs;
113
+ }
114
+ }
115
+ continue;
116
+ }
117
+ if (isGateRecord(record) &&
118
+ record.reason === 'approved_once' &&
119
+ record.fingerprint &&
120
+ record.permission === 'allow') {
121
+ const trip = pendingByFingerprint.get(record.fingerprint);
122
+ if (trip) {
123
+ trip.executeTimestamp = timestamp;
124
+ }
125
+ }
126
+ }
127
+ return trips;
128
+ }
129
+ export function summarizeRoundTrips(trips) {
130
+ return trips.map((trip) => {
131
+ const parts = [
132
+ `[${trip.kind}] ${trip.summary}`,
133
+ `denied(${trip.reason})`,
134
+ trip.approvalTimestamp ? 'approved' : 'pending-approval',
135
+ trip.executeTimestamp ? 'executed' : 'not-retried',
136
+ ];
137
+ if (typeof trip.approvalLatencyMs === 'number') {
138
+ parts.push(`${Math.round(trip.approvalLatencyMs / 1000)}s latency`);
139
+ }
140
+ return parts.join(' → ');
141
+ });
142
+ }
@@ -0,0 +1,33 @@
1
+ import type { AuditFilter, AuditRecord } from './audit-types.js';
2
+ export type AuditTier = 'Tier0' | 'Tier1' | 'deterministic';
3
+ export interface RecentAskEntry {
4
+ timestamp?: string;
5
+ summary: string;
6
+ reason: string;
7
+ tier: AuditTier;
8
+ }
9
+ export interface AuditVisibilitySummary {
10
+ gateEvents: number;
11
+ askCount: number;
12
+ enforceAskCount: number;
13
+ auditAskCount: number;
14
+ unknownModeAskCount: number;
15
+ flagCount: number;
16
+ allowCount: number;
17
+ silentPassRate: number;
18
+ recentAsks: RecentAskEntry[];
19
+ }
20
+ export declare const DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
21
+ export declare const MIN_GATE_EVENTS_FOR_FENCE_DRIFT = 20;
22
+ export interface FenceDriftOptions {
23
+ threshold?: number;
24
+ }
25
+ export declare function inferAuditTier(record: AuditRecord): AuditTier;
26
+ export declare function formatAskBreakdown(summary: Pick<AuditVisibilitySummary, 'askCount' | 'enforceAskCount' | 'auditAskCount' | 'unknownModeAskCount'>, indent?: string): string[];
27
+ export declare function summarizeAuditVisibility(records: AuditRecord[], filter?: AuditFilter, options?: {
28
+ recentAskLimit?: number;
29
+ }): AuditVisibilitySummary;
30
+ export declare function detectFenceDrift(summary: Pick<AuditVisibilitySummary, 'gateEvents' | 'silentPassRate'>, options?: FenceDriftOptions): {
31
+ warnings: string[];
32
+ notes: string[];
33
+ };
@@ -0,0 +1,111 @@
1
+ import { filterAuditRecords, inferWouldBlock, isApprovalRecorded, isGateRecord, parseTimestamp, recordStringField, } from './audit-query.js';
2
+ export const DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
3
+ export const MIN_GATE_EVENTS_FOR_FENCE_DRIFT = 20;
4
+ function isTier0Reason(reason) {
5
+ return reason.startsWith('tier0_') || reason === 'external_effect';
6
+ }
7
+ export function inferAuditTier(record) {
8
+ const savedConfidence = typeof record.confidence === 'string' ? record.confidence : '';
9
+ const reason = typeof record.reason === 'string' ? record.reason : '';
10
+ if (savedConfidence === 'llm') {
11
+ return 'Tier1';
12
+ }
13
+ if (savedConfidence === 'deterministic') {
14
+ return isTier0Reason(reason) ? 'Tier0' : 'deterministic';
15
+ }
16
+ if (isTier0Reason(reason)) {
17
+ return 'Tier0';
18
+ }
19
+ if (reason === 'unknown_local_effect') {
20
+ return 'Tier1';
21
+ }
22
+ return 'deterministic';
23
+ }
24
+ export function formatAskBreakdown(summary, indent = '') {
25
+ const lines = [
26
+ `${indent}Ask (would-block): ${summary.askCount}`,
27
+ `${indent} enforce (blocked): ${summary.enforceAskCount}`,
28
+ `${indent} audit (would-block only): ${summary.auditAskCount}`,
29
+ ];
30
+ if (summary.unknownModeAskCount > 0) {
31
+ lines.push(`${indent} mode unknown (legacy): ${summary.unknownModeAskCount}`);
32
+ }
33
+ return lines;
34
+ }
35
+ function isGateEventRecord(record) {
36
+ return isGateRecord(record) && !isApprovalRecorded(record);
37
+ }
38
+ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
39
+ const filtered = filterAuditRecords(records, filter);
40
+ const gateRecords = filtered.filter(isGateEventRecord);
41
+ const recentAskLimit = options.recentAskLimit ?? 10;
42
+ let askCount = 0;
43
+ let enforceAskCount = 0;
44
+ let auditAskCount = 0;
45
+ let unknownModeAskCount = 0;
46
+ let flagCount = 0;
47
+ let allowCount = 0;
48
+ const recentAsks = [];
49
+ for (const record of gateRecords) {
50
+ if (inferWouldBlock(record)) {
51
+ askCount += 1;
52
+ const recordMode = recordStringField(record, 'mode');
53
+ if (recordMode === 'enforce') {
54
+ enforceAskCount += 1;
55
+ }
56
+ else if (recordMode === 'audit') {
57
+ auditAskCount += 1;
58
+ }
59
+ else {
60
+ unknownModeAskCount += 1;
61
+ }
62
+ recentAsks.push({
63
+ timestamp: record.timestamp,
64
+ summary: typeof record.summary === 'string' ? record.summary : '',
65
+ reason: typeof record.reason === 'string' ? record.reason : 'unknown',
66
+ tier: inferAuditTier(record),
67
+ });
68
+ }
69
+ if (record.verdict === 'allow_flagged') {
70
+ flagCount += 1;
71
+ }
72
+ if (record.verdict === 'allow') {
73
+ allowCount += 1;
74
+ }
75
+ }
76
+ recentAsks.sort((left, right) => {
77
+ const leftMs = parseTimestamp(left.timestamp) ?? 0;
78
+ const rightMs = parseTimestamp(right.timestamp) ?? 0;
79
+ return rightMs - leftMs;
80
+ });
81
+ const gateEvents = gateRecords.length;
82
+ const silentPassRate = gateEvents > 0 ? (allowCount + flagCount) / gateEvents : 0;
83
+ return {
84
+ gateEvents,
85
+ askCount,
86
+ enforceAskCount,
87
+ auditAskCount,
88
+ unknownModeAskCount,
89
+ flagCount,
90
+ allowCount,
91
+ silentPassRate,
92
+ recentAsks: recentAsks.slice(0, recentAskLimit),
93
+ };
94
+ }
95
+ export function detectFenceDrift(summary, options = {}) {
96
+ const threshold = options.threshold ?? DEFAULT_SILENT_PASS_THRESHOLD;
97
+ const warnings = [];
98
+ const notes = [];
99
+ if (summary.gateEvents === 0) {
100
+ return { warnings, notes };
101
+ }
102
+ if (summary.gateEvents < MIN_GATE_EVENTS_FOR_FENCE_DRIFT) {
103
+ notes.push(`Fence drift check deferred: only ${summary.gateEvents} gate event(s) recorded (need at least ${MIN_GATE_EVENTS_FOR_FENCE_DRIFT} for a reliable silent-pass rate).`);
104
+ return { warnings, notes };
105
+ }
106
+ if (summary.silentPassRate < threshold) {
107
+ warnings.push(`Silent-pass rate is ${(summary.silentPassRate * 100).toFixed(1)}% (below ${(threshold * 100).toFixed(0)}% threshold). ` +
108
+ 'This may indicate over-blocking (fence-like behavior). Use belay explain on recent asks to check for false positives.');
109
+ }
110
+ return { warnings, notes };
111
+ }