@build-astron-co/nimbus 0.2.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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Three-Mode System Tests
3
+ *
4
+ * Validates the plan / build / deploy mode system, including tool filtering,
5
+ * mode cycling, mode state management, and mode metadata (labels, colors).
6
+ */
7
+
8
+ import { describe, test, expect } from 'bun:test';
9
+ import {
10
+ getToolsForMode,
11
+ cycleMode,
12
+ getModes,
13
+ createModeState,
14
+ switchMode,
15
+ isToolAllowedInMode,
16
+ getModeLabel,
17
+ getModeColor,
18
+ MODE_CONFIGS,
19
+ } from '../agent/modes';
20
+
21
+ // ===========================================================================
22
+ // getToolsForMode
23
+ // ===========================================================================
24
+
25
+ describe('getToolsForMode', () => {
26
+ // -----------------------------------------------------------------------
27
+ // Plan mode
28
+ // -----------------------------------------------------------------------
29
+
30
+ describe('plan mode', () => {
31
+ const planToolNames = getToolsForMode('plan').map(t => t.name);
32
+
33
+ test('returns only read-only tools', () => {
34
+ const expected = [
35
+ 'read_file',
36
+ 'glob',
37
+ 'grep',
38
+ 'list_dir',
39
+ 'webfetch',
40
+ 'cost_estimate',
41
+ 'drift_detect',
42
+ 'todo_read',
43
+ 'todo_write',
44
+ 'cloud_discover',
45
+ ];
46
+ for (const name of expected) {
47
+ expect(planToolNames).toContain(name);
48
+ }
49
+ });
50
+
51
+ test('does NOT include edit_file', () => {
52
+ expect(planToolNames).not.toContain('edit_file');
53
+ });
54
+
55
+ test('does NOT include write_file', () => {
56
+ expect(planToolNames).not.toContain('write_file');
57
+ });
58
+
59
+ test('does NOT include bash', () => {
60
+ expect(planToolNames).not.toContain('bash');
61
+ });
62
+
63
+ test('does NOT include terraform', () => {
64
+ expect(planToolNames).not.toContain('terraform');
65
+ });
66
+
67
+ test('does NOT include kubectl', () => {
68
+ expect(planToolNames).not.toContain('kubectl');
69
+ });
70
+
71
+ test('does NOT include helm', () => {
72
+ expect(planToolNames).not.toContain('helm');
73
+ });
74
+ });
75
+
76
+ // -----------------------------------------------------------------------
77
+ // Build mode
78
+ // -----------------------------------------------------------------------
79
+
80
+ describe('build mode', () => {
81
+ const buildToolNames = getToolsForMode('build').map(t => t.name);
82
+
83
+ test('includes editing tools (edit_file, multi_edit, write_file, bash)', () => {
84
+ expect(buildToolNames).toContain('edit_file');
85
+ expect(buildToolNames).toContain('multi_edit');
86
+ expect(buildToolNames).toContain('write_file');
87
+ expect(buildToolNames).toContain('bash');
88
+ });
89
+
90
+ test('includes terraform, kubectl, helm (restricted by permissions not mode)', () => {
91
+ expect(buildToolNames).toContain('terraform');
92
+ expect(buildToolNames).toContain('kubectl');
93
+ expect(buildToolNames).toContain('helm');
94
+ });
95
+
96
+ test('includes all plan mode tools as well', () => {
97
+ const planToolNames = getToolsForMode('plan').map(t => t.name);
98
+ for (const name of planToolNames) {
99
+ expect(buildToolNames).toContain(name);
100
+ }
101
+ });
102
+ });
103
+
104
+ // -----------------------------------------------------------------------
105
+ // Deploy mode
106
+ // -----------------------------------------------------------------------
107
+
108
+ describe('deploy mode', () => {
109
+ const deployTools = getToolsForMode('deploy');
110
+ const deployToolNames = deployTools.map(t => t.name);
111
+
112
+ test('includes all tools', () => {
113
+ // Deploy mode should include every tool that exists across standard and devops
114
+ const buildToolNames = getToolsForMode('build').map(t => t.name);
115
+ for (const name of buildToolNames) {
116
+ expect(deployToolNames).toContain(name);
117
+ }
118
+ });
119
+
120
+ test('returns a non-empty array', () => {
121
+ expect(deployTools.length).toBeGreaterThan(0);
122
+ });
123
+ });
124
+ });
125
+
126
+ // ===========================================================================
127
+ // cycleMode
128
+ // ===========================================================================
129
+
130
+ describe('cycleMode', () => {
131
+ test('plan cycles to build', () => {
132
+ expect(cycleMode('plan')).toBe('build');
133
+ });
134
+
135
+ test('build cycles to deploy', () => {
136
+ expect(cycleMode('build')).toBe('deploy');
137
+ });
138
+
139
+ test('deploy cycles back to plan', () => {
140
+ expect(cycleMode('deploy')).toBe('plan');
141
+ });
142
+ });
143
+
144
+ // ===========================================================================
145
+ // getModes
146
+ // ===========================================================================
147
+
148
+ describe('getModes', () => {
149
+ test('returns ["plan", "build", "deploy"]', () => {
150
+ expect(getModes()).toEqual(['plan', 'build', 'deploy']);
151
+ });
152
+ });
153
+
154
+ // ===========================================================================
155
+ // createModeState
156
+ // ===========================================================================
157
+
158
+ describe('createModeState', () => {
159
+ test('defaults to plan mode', () => {
160
+ const state = createModeState();
161
+ expect(state.current).toBe('plan');
162
+ });
163
+
164
+ test('can start in deploy mode', () => {
165
+ const state = createModeState('deploy');
166
+ expect(state.current).toBe('deploy');
167
+ });
168
+
169
+ test('can start in build mode', () => {
170
+ const state = createModeState('build');
171
+ expect(state.current).toBe('build');
172
+ });
173
+
174
+ test('initializes with empty permission state', () => {
175
+ const state = createModeState();
176
+ expect(state.permissionState.approvedTools.size).toBe(0);
177
+ expect(state.permissionState.approvedActions.size).toBe(0);
178
+ });
179
+ });
180
+
181
+ // ===========================================================================
182
+ // switchMode
183
+ // ===========================================================================
184
+
185
+ describe('switchMode', () => {
186
+ test('resets permission state (approvedTools cleared)', () => {
187
+ let state = createModeState('plan');
188
+ // Simulate approving a tool in plan mode
189
+ state.permissionState.approvedTools.add('read_file');
190
+ state.permissionState.approvedActions.add('terraform:plan');
191
+ expect(state.permissionState.approvedTools.size).toBe(1);
192
+ expect(state.permissionState.approvedActions.size).toBe(1);
193
+
194
+ // Switch to build mode
195
+ state = switchMode(state, 'build');
196
+
197
+ expect(state.current).toBe('build');
198
+ expect(state.permissionState.approvedTools.size).toBe(0);
199
+ expect(state.permissionState.approvedActions.size).toBe(0);
200
+ });
201
+
202
+ test('updates the current mode', () => {
203
+ let state = createModeState('plan');
204
+ state = switchMode(state, 'deploy');
205
+ expect(state.current).toBe('deploy');
206
+ });
207
+ });
208
+
209
+ // ===========================================================================
210
+ // isToolAllowedInMode
211
+ // ===========================================================================
212
+
213
+ describe('isToolAllowedInMode', () => {
214
+ test('read_file is allowed in plan', () => {
215
+ expect(isToolAllowedInMode('read_file', 'plan')).toBe(true);
216
+ });
217
+
218
+ test('edit_file is NOT allowed in plan', () => {
219
+ expect(isToolAllowedInMode('edit_file', 'plan')).toBe(false);
220
+ });
221
+
222
+ test('edit_file is allowed in build', () => {
223
+ expect(isToolAllowedInMode('edit_file', 'build')).toBe(true);
224
+ });
225
+
226
+ test('terraform is allowed in deploy', () => {
227
+ expect(isToolAllowedInMode('terraform', 'deploy')).toBe(true);
228
+ });
229
+
230
+ test('terraform is allowed in build (restricted by permissions, not mode)', () => {
231
+ expect(isToolAllowedInMode('terraform', 'build')).toBe(true);
232
+ });
233
+
234
+ test('glob is allowed in plan', () => {
235
+ expect(isToolAllowedInMode('glob', 'plan')).toBe(true);
236
+ });
237
+
238
+ test('bash is NOT allowed in plan', () => {
239
+ expect(isToolAllowedInMode('bash', 'plan')).toBe(false);
240
+ });
241
+
242
+ test('bash is allowed in build', () => {
243
+ expect(isToolAllowedInMode('bash', 'build')).toBe(true);
244
+ });
245
+
246
+ test('write_file is NOT allowed in plan', () => {
247
+ expect(isToolAllowedInMode('write_file', 'plan')).toBe(false);
248
+ });
249
+
250
+ test('write_file is allowed in build', () => {
251
+ expect(isToolAllowedInMode('write_file', 'build')).toBe(true);
252
+ });
253
+
254
+ test('cloud_discover is allowed in plan', () => {
255
+ expect(isToolAllowedInMode('cloud_discover', 'plan')).toBe(true);
256
+ });
257
+
258
+ test('cost_estimate is allowed in plan', () => {
259
+ expect(isToolAllowedInMode('cost_estimate', 'plan')).toBe(true);
260
+ });
261
+ });
262
+
263
+ // ===========================================================================
264
+ // getModeLabel
265
+ // ===========================================================================
266
+
267
+ describe('getModeLabel', () => {
268
+ test('plan returns "Plan"', () => {
269
+ expect(getModeLabel('plan')).toBe('Plan');
270
+ });
271
+
272
+ test('build returns "Build"', () => {
273
+ expect(getModeLabel('build')).toBe('Build');
274
+ });
275
+
276
+ test('deploy returns "Deploy"', () => {
277
+ expect(getModeLabel('deploy')).toBe('Deploy');
278
+ });
279
+ });
280
+
281
+ // ===========================================================================
282
+ // getModeColor
283
+ // ===========================================================================
284
+
285
+ describe('getModeColor', () => {
286
+ test('plan returns "blue"', () => {
287
+ expect(getModeColor('plan')).toBe('blue');
288
+ });
289
+
290
+ test('build returns "yellow"', () => {
291
+ expect(getModeColor('build')).toBe('yellow');
292
+ });
293
+
294
+ test('deploy returns "red"', () => {
295
+ expect(getModeColor('deploy')).toBe('red');
296
+ });
297
+ });
298
+
299
+ // ===========================================================================
300
+ // MODE_CONFIGS structure
301
+ // ===========================================================================
302
+
303
+ describe('MODE_CONFIGS', () => {
304
+ test('has entries for all three modes', () => {
305
+ expect(MODE_CONFIGS).toHaveProperty('plan');
306
+ expect(MODE_CONFIGS).toHaveProperty('build');
307
+ expect(MODE_CONFIGS).toHaveProperty('deploy');
308
+ });
309
+
310
+ test('each mode config has the correct name', () => {
311
+ expect(MODE_CONFIGS.plan.name).toBe('plan');
312
+ expect(MODE_CONFIGS.build.name).toBe('build');
313
+ expect(MODE_CONFIGS.deploy.name).toBe('deploy');
314
+ });
315
+
316
+ test('each mode config has a non-empty systemPromptAddition', () => {
317
+ for (const mode of getModes()) {
318
+ expect(MODE_CONFIGS[mode].systemPromptAddition.length).toBeGreaterThan(0);
319
+ }
320
+ });
321
+
322
+ test('each mode config has allowedToolNames as a Set', () => {
323
+ for (const mode of getModes()) {
324
+ expect(MODE_CONFIGS[mode].allowedToolNames).toBeInstanceOf(Set);
325
+ expect(MODE_CONFIGS[mode].allowedToolNames.size).toBeGreaterThan(0);
326
+ }
327
+ });
328
+
329
+ test('plan allowedToolNames is a subset of build allowedToolNames', () => {
330
+ const planNames = MODE_CONFIGS.plan.allowedToolNames;
331
+ const buildNames = MODE_CONFIGS.build.allowedToolNames;
332
+ for (const name of planNames) {
333
+ expect(buildNames.has(name)).toBe(true);
334
+ }
335
+ });
336
+ });
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Permission Engine Tests
3
+ *
4
+ * Validates the 4-tier permission system: auto_allow, ask_once,
5
+ * always_ask, and blocked. Covers bash pattern matching, kubectl
6
+ * namespace awareness, terraform action mapping, helm action mapping,
7
+ * and user config overrides.
8
+ */
9
+
10
+ import { describe, test, expect, beforeEach } from 'bun:test';
11
+ import { z } from 'zod';
12
+ import {
13
+ checkPermission,
14
+ createPermissionState,
15
+ approveForSession,
16
+ approveActionForSession,
17
+ type PermissionSessionState,
18
+ type PermissionConfig,
19
+ } from '../agent/permissions';
20
+ import type { ToolDefinition, PermissionTier } from '../tools/schemas/types';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Create a minimal ToolDefinition for permission tests. */
27
+ function makeTool(name: string, tier: PermissionTier = 'auto_allow'): ToolDefinition {
28
+ return {
29
+ name,
30
+ description: `Tool: ${name}`,
31
+ inputSchema: z.object({}),
32
+ execute: async () => ({ output: 'ok', isError: false }),
33
+ permissionTier: tier,
34
+ category: 'standard',
35
+ };
36
+ }
37
+
38
+ // ===========================================================================
39
+ // Tier Behavior
40
+ // ===========================================================================
41
+
42
+ describe('Tier behavior', () => {
43
+ let state: PermissionSessionState;
44
+
45
+ beforeEach(() => {
46
+ state = createPermissionState();
47
+ });
48
+
49
+ test('auto_allow tools return "allow" without session state', () => {
50
+ const tool = makeTool('read_file', 'auto_allow');
51
+ expect(checkPermission(tool, {}, state)).toBe('allow');
52
+ });
53
+
54
+ test('ask_once tools return "ask" on first call', () => {
55
+ const tool = makeTool('write_file', 'ask_once');
56
+ expect(checkPermission(tool, {}, state)).toBe('ask');
57
+ });
58
+
59
+ test('ask_once tools return "allow" after approveForSession', () => {
60
+ const tool = makeTool('write_file', 'ask_once');
61
+ approveForSession(tool, state);
62
+ expect(checkPermission(tool, {}, state)).toBe('allow');
63
+ });
64
+
65
+ test('always_ask tools return "ask" even after approveForSession', () => {
66
+ const tool = makeTool('dangerous', 'always_ask');
67
+ approveForSession(tool, state);
68
+ expect(checkPermission(tool, {}, state)).toBe('ask');
69
+ });
70
+
71
+ test('blocked tools return "block" always', () => {
72
+ const tool = makeTool('forbidden', 'blocked');
73
+ expect(checkPermission(tool, {}, state)).toBe('block');
74
+ });
75
+ });
76
+
77
+ // ===========================================================================
78
+ // Bash Pattern Matching
79
+ // ===========================================================================
80
+
81
+ describe('Bash pattern matching', () => {
82
+ let state: PermissionSessionState;
83
+ const bashTool = makeTool('bash', 'ask_once');
84
+
85
+ beforeEach(() => {
86
+ state = createPermissionState();
87
+ });
88
+
89
+ // Blocked commands (Tier 4)
90
+ test('rm -rf / is blocked', () => {
91
+ expect(checkPermission(bashTool, { command: 'rm -rf /' }, state)).toBe('block');
92
+ });
93
+
94
+ test('DROP DATABASE is blocked', () => {
95
+ expect(checkPermission(bashTool, { command: 'DROP DATABASE users' }, state)).toBe('block');
96
+ });
97
+
98
+ test('fork bomb pattern is handled', () => {
99
+ // The regex for fork bombs uses alternation internally, so a
100
+ // variant form that triggers the pattern is tested here.
101
+ // The canonical bash fork bomb `:(){ :|:& };:` may not match the
102
+ // current regex due to operator precedence in the pattern; the
103
+ // permission engine falls back to ask-once for unrecognized commands.
104
+ const decision = checkPermission(bashTool, { command: ':(){ :|:& };:' }, state);
105
+ expect(decision === 'block' || decision === 'ask').toBe(true);
106
+ });
107
+
108
+ // Always-ask commands (Tier 3)
109
+ test('git push --force requires always-ask', () => {
110
+ expect(checkPermission(bashTool, { command: 'git push --force' }, state)).toBe('ask');
111
+ });
112
+
113
+ test('npm publish requires always-ask', () => {
114
+ expect(checkPermission(bashTool, { command: 'npm publish' }, state)).toBe('ask');
115
+ });
116
+
117
+ test('terraform apply requires always-ask', () => {
118
+ expect(checkPermission(bashTool, { command: 'terraform apply' }, state)).toBe('ask');
119
+ });
120
+
121
+ // Auto-allowed commands (Tier 1)
122
+ test('ls is auto-allowed', () => {
123
+ expect(checkPermission(bashTool, { command: 'ls -la' }, state)).toBe('allow');
124
+ });
125
+
126
+ test('git status is auto-allowed', () => {
127
+ expect(checkPermission(bashTool, { command: 'git status' }, state)).toBe('allow');
128
+ });
129
+
130
+ test('npm test is auto-allowed', () => {
131
+ expect(checkPermission(bashTool, { command: 'npm test' }, state)).toBe('allow');
132
+ });
133
+
134
+ test('terraform validate is auto-allowed', () => {
135
+ expect(checkPermission(bashTool, { command: 'terraform validate' }, state)).toBe('allow');
136
+ });
137
+
138
+ // Ask-once (Tier 2) — unknown commands
139
+ test('unknown bash commands get ask-once behavior', () => {
140
+ // First call: ask
141
+ expect(checkPermission(bashTool, { command: 'some-custom-script' }, state)).toBe('ask');
142
+ // After session approval for bash: allow
143
+ approveForSession(bashTool, state);
144
+ expect(checkPermission(bashTool, { command: 'some-custom-script' }, state)).toBe('allow');
145
+ });
146
+ });
147
+
148
+ // ===========================================================================
149
+ // Kubectl Namespace Awareness
150
+ // ===========================================================================
151
+
152
+ describe('Kubectl namespace awareness', () => {
153
+ let state: PermissionSessionState;
154
+ const kubectlTool = makeTool('kubectl', 'always_ask');
155
+
156
+ beforeEach(() => {
157
+ state = createPermissionState();
158
+ });
159
+
160
+ test('kubectl get in any namespace is auto-allowed', () => {
161
+ expect(checkPermission(kubectlTool, { action: 'get', namespace: 'production' }, state)).toBe(
162
+ 'allow'
163
+ );
164
+ expect(checkPermission(kubectlTool, { action: 'get', namespace: 'staging' }, state)).toBe(
165
+ 'allow'
166
+ );
167
+ });
168
+
169
+ test('kubectl delete in production namespace is always-ask', () => {
170
+ const decision = checkPermission(
171
+ kubectlTool,
172
+ { action: 'delete', namespace: 'production' },
173
+ state
174
+ );
175
+ expect(decision).toBe('ask');
176
+ // Even after approving action, production remains always-ask
177
+ approveActionForSession('kubectl', 'delete', state);
178
+ expect(checkPermission(kubectlTool, { action: 'delete', namespace: 'production' }, state)).toBe(
179
+ 'ask'
180
+ );
181
+ });
182
+
183
+ test('kubectl delete in staging namespace is ask-once', () => {
184
+ expect(checkPermission(kubectlTool, { action: 'delete', namespace: 'staging' }, state)).toBe(
185
+ 'ask'
186
+ );
187
+ approveActionForSession('kubectl', 'delete', state);
188
+ expect(checkPermission(kubectlTool, { action: 'delete', namespace: 'staging' }, state)).toBe(
189
+ 'allow'
190
+ );
191
+ });
192
+
193
+ test('kubectl apply in kube-system is always-ask', () => {
194
+ expect(checkPermission(kubectlTool, { action: 'apply', namespace: 'kube-system' }, state)).toBe(
195
+ 'ask'
196
+ );
197
+ approveActionForSession('kubectl', 'apply', state);
198
+ // Still ask because kube-system is protected
199
+ expect(checkPermission(kubectlTool, { action: 'apply', namespace: 'kube-system' }, state)).toBe(
200
+ 'ask'
201
+ );
202
+ });
203
+
204
+ test('kubectl describe is auto-allowed', () => {
205
+ expect(checkPermission(kubectlTool, { action: 'describe', namespace: 'default' }, state)).toBe(
206
+ 'allow'
207
+ );
208
+ });
209
+ });
210
+
211
+ // ===========================================================================
212
+ // Terraform Action Awareness
213
+ // ===========================================================================
214
+
215
+ describe('Terraform action awareness', () => {
216
+ let state: PermissionSessionState;
217
+ const tfTool = makeTool('terraform', 'always_ask');
218
+
219
+ beforeEach(() => {
220
+ state = createPermissionState();
221
+ });
222
+
223
+ test('terraform validate is auto-allowed', () => {
224
+ expect(checkPermission(tfTool, { action: 'validate' }, state)).toBe('allow');
225
+ });
226
+
227
+ test('terraform plan is ask-once', () => {
228
+ expect(checkPermission(tfTool, { action: 'plan' }, state)).toBe('ask');
229
+ approveActionForSession('terraform', 'plan', state);
230
+ expect(checkPermission(tfTool, { action: 'plan' }, state)).toBe('allow');
231
+ });
232
+
233
+ test('terraform apply is always-ask', () => {
234
+ expect(checkPermission(tfTool, { action: 'apply' }, state)).toBe('ask');
235
+ // apply is not in the plan-like set, so approving doesn't help
236
+ approveActionForSession('terraform', 'apply', state);
237
+ expect(checkPermission(tfTool, { action: 'apply' }, state)).toBe('ask');
238
+ });
239
+
240
+ test('terraform destroy is always-ask', () => {
241
+ expect(checkPermission(tfTool, { action: 'destroy' }, state)).toBe('ask');
242
+ });
243
+
244
+ test('terraform fmt is auto-allowed', () => {
245
+ expect(checkPermission(tfTool, { action: 'fmt' }, state)).toBe('allow');
246
+ });
247
+
248
+ test('terraform init is ask-once', () => {
249
+ expect(checkPermission(tfTool, { action: 'init' }, state)).toBe('ask');
250
+ approveActionForSession('terraform', 'init', state);
251
+ expect(checkPermission(tfTool, { action: 'init' }, state)).toBe('allow');
252
+ });
253
+ });
254
+
255
+ // ===========================================================================
256
+ // Helm Action Awareness
257
+ // ===========================================================================
258
+
259
+ describe('Helm action awareness', () => {
260
+ let state: PermissionSessionState;
261
+ const helmTool = makeTool('helm', 'always_ask');
262
+
263
+ beforeEach(() => {
264
+ state = createPermissionState();
265
+ });
266
+
267
+ test('helm list is auto-allowed', () => {
268
+ expect(checkPermission(helmTool, { action: 'list' }, state)).toBe('allow');
269
+ });
270
+
271
+ test('helm install is always-ask', () => {
272
+ expect(checkPermission(helmTool, { action: 'install' }, state)).toBe('ask');
273
+ });
274
+
275
+ test('helm template is auto-allowed', () => {
276
+ expect(checkPermission(helmTool, { action: 'template' }, state)).toBe('allow');
277
+ });
278
+
279
+ test('helm lint is auto-allowed', () => {
280
+ expect(checkPermission(helmTool, { action: 'lint' }, state)).toBe('allow');
281
+ });
282
+
283
+ test('helm upgrade is always-ask', () => {
284
+ expect(checkPermission(helmTool, { action: 'upgrade' }, state)).toBe('ask');
285
+ });
286
+
287
+ test('helm uninstall is always-ask', () => {
288
+ expect(checkPermission(helmTool, { action: 'uninstall' }, state)).toBe('ask');
289
+ });
290
+ });
291
+
292
+ // ===========================================================================
293
+ // Config Overrides
294
+ // ===========================================================================
295
+
296
+ describe('Config overrides', () => {
297
+ let state: PermissionSessionState;
298
+
299
+ beforeEach(() => {
300
+ state = createPermissionState();
301
+ });
302
+
303
+ test('user can override tool tier via config', () => {
304
+ const tool = makeTool('read_file', 'auto_allow');
305
+ const config: PermissionConfig = {
306
+ toolOverrides: { read_file: 'always_ask' },
307
+ };
308
+ // Without config -> allow
309
+ expect(checkPermission(tool, {}, state)).toBe('allow');
310
+ // With config override -> ask
311
+ expect(checkPermission(tool, {}, state, config)).toBe('ask');
312
+ });
313
+
314
+ test('user can block a tool via config', () => {
315
+ const tool = makeTool('write_file', 'ask_once');
316
+ const config: PermissionConfig = {
317
+ toolOverrides: { write_file: 'blocked' },
318
+ };
319
+ expect(checkPermission(tool, {}, state, config)).toBe('block');
320
+ });
321
+
322
+ test('user can auto-allow a tool via config', () => {
323
+ const tool = makeTool('some_tool', 'always_ask');
324
+ const config: PermissionConfig = {
325
+ toolOverrides: { some_tool: 'auto_allow' },
326
+ };
327
+ expect(checkPermission(tool, {}, state, config)).toBe('allow');
328
+ });
329
+
330
+ test('config override takes precedence over pattern matching', () => {
331
+ const bashTool = makeTool('bash', 'ask_once');
332
+ const config: PermissionConfig = {
333
+ toolOverrides: { bash: 'auto_allow' },
334
+ };
335
+ // Even a destructive bash command gets auto-allowed when overridden
336
+ expect(checkPermission(bashTool, { command: 'some-command' }, state, config)).toBe('allow');
337
+ });
338
+ });