oxe-cc 1.0.0 → 1.3.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 (322) hide show
  1. package/.cursor/commands/oxe-ask.md +3 -3
  2. package/.cursor/commands/oxe-capabilities.md +3 -3
  3. package/.cursor/commands/oxe-checkpoint.md +3 -3
  4. package/.cursor/commands/oxe-compact.md +3 -3
  5. package/.cursor/commands/oxe-dashboard.md +3 -3
  6. package/.cursor/commands/oxe-debug.md +3 -3
  7. package/.cursor/commands/oxe-discuss.md +3 -3
  8. package/.cursor/commands/oxe-execute.md +7 -4
  9. package/.cursor/commands/oxe-forensics.md +3 -3
  10. package/.cursor/commands/oxe-help.md +3 -3
  11. package/.cursor/commands/oxe-loop.md +3 -3
  12. package/.cursor/commands/oxe-milestone.md +3 -3
  13. package/.cursor/commands/oxe-next.md +3 -3
  14. package/.cursor/commands/oxe-obs.md +3 -3
  15. package/.cursor/commands/oxe-plan-agent.md +3 -3
  16. package/.cursor/commands/oxe-plan.md +3 -3
  17. package/.cursor/commands/oxe-project.md +3 -3
  18. package/.cursor/commands/oxe-quick.md +3 -3
  19. package/.cursor/commands/oxe-research.md +3 -3
  20. package/.cursor/commands/oxe-retro.md +3 -3
  21. package/.cursor/commands/oxe-review-pr.md +3 -3
  22. package/.cursor/commands/oxe-route.md +3 -3
  23. package/.cursor/commands/oxe-scan.md +3 -3
  24. package/.cursor/commands/oxe-security.md +3 -3
  25. package/.cursor/commands/oxe-session.md +4 -4
  26. package/.cursor/commands/oxe-ship.md +45 -0
  27. package/.cursor/commands/oxe-skill.md +3 -3
  28. package/.cursor/commands/oxe-spec.md +3 -3
  29. package/.cursor/commands/oxe-ui-review.md +3 -3
  30. package/.cursor/commands/oxe-ui-spec.md +3 -3
  31. package/.cursor/commands/oxe-update.md +3 -3
  32. package/.cursor/commands/oxe-validate-gaps.md +3 -3
  33. package/.cursor/commands/oxe-verify.md +6 -3
  34. package/.cursor/commands/oxe-workstream.md +3 -3
  35. package/.cursor/commands/oxe.md +6 -6
  36. package/.github/copilot-instructions.md +94 -4
  37. package/.github/prompts/oxe-ask.prompt.md +3 -3
  38. package/.github/prompts/oxe-capabilities.prompt.md +3 -3
  39. package/.github/prompts/oxe-checkpoint.prompt.md +3 -3
  40. package/.github/prompts/oxe-compact.prompt.md +3 -3
  41. package/.github/prompts/oxe-dashboard.prompt.md +3 -3
  42. package/.github/prompts/oxe-debug.prompt.md +3 -3
  43. package/.github/prompts/oxe-discuss.prompt.md +3 -3
  44. package/.github/prompts/oxe-execute.prompt.md +7 -4
  45. package/.github/prompts/oxe-forensics.prompt.md +3 -3
  46. package/.github/prompts/oxe-help.prompt.md +3 -3
  47. package/.github/prompts/oxe-loop.prompt.md +3 -3
  48. package/.github/prompts/oxe-milestone.prompt.md +3 -3
  49. package/.github/prompts/oxe-next.prompt.md +3 -3
  50. package/.github/prompts/oxe-obs.prompt.md +3 -3
  51. package/.github/prompts/oxe-plan-agent.prompt.md +3 -3
  52. package/.github/prompts/oxe-plan.prompt.md +3 -3
  53. package/.github/prompts/oxe-project.prompt.md +3 -3
  54. package/.github/prompts/oxe-quick.prompt.md +3 -3
  55. package/.github/prompts/oxe-research.prompt.md +3 -3
  56. package/.github/prompts/oxe-retro.prompt.md +3 -3
  57. package/.github/prompts/oxe-review-pr.prompt.md +3 -3
  58. package/.github/prompts/oxe-route.prompt.md +3 -3
  59. package/.github/prompts/oxe-scan.prompt.md +3 -3
  60. package/.github/prompts/oxe-security.prompt.md +3 -3
  61. package/.github/prompts/oxe-session.prompt.md +4 -4
  62. package/.github/prompts/oxe-ship.prompt.md +45 -0
  63. package/.github/prompts/oxe-skill.prompt.md +3 -3
  64. package/.github/prompts/oxe-spec.prompt.md +3 -3
  65. package/.github/prompts/oxe-ui-review.prompt.md +3 -3
  66. package/.github/prompts/oxe-ui-spec.prompt.md +3 -3
  67. package/.github/prompts/oxe-update.prompt.md +3 -3
  68. package/.github/prompts/oxe-validate-gaps.prompt.md +3 -3
  69. package/.github/prompts/oxe-verify.prompt.md +6 -3
  70. package/.github/prompts/oxe-workstream.prompt.md +3 -3
  71. package/.github/prompts/oxe.prompt.md +5 -5
  72. package/AGENTS.md +43 -28
  73. package/CHANGELOG.md +193 -0
  74. package/README.md +610 -529
  75. package/bin/banner.txt +1 -1
  76. package/bin/lib/oxe-agent-install.cjs +69 -69
  77. package/bin/lib/oxe-azure.cjs +1445 -1445
  78. package/bin/lib/oxe-context-engine.cjs +867 -867
  79. package/bin/lib/oxe-dashboard.cjs +76 -28
  80. package/bin/lib/oxe-operational.cjs +2144 -1340
  81. package/bin/lib/oxe-project-health.cjs +483 -1
  82. package/bin/lib/oxe-runtime-semantics.cjs +12 -0
  83. package/bin/oxe-cc.js +554 -152
  84. package/commands/oxe/ask.md +7 -3
  85. package/commands/oxe/capabilities.md +2 -2
  86. package/commands/oxe/checkpoint.md +3 -3
  87. package/commands/oxe/compact.md +3 -3
  88. package/commands/oxe/dashboard.md +2 -2
  89. package/commands/oxe/debug.md +3 -3
  90. package/commands/oxe/discuss.md +2 -2
  91. package/commands/oxe/execute.md +7 -4
  92. package/commands/oxe/forensics.md +3 -3
  93. package/commands/oxe/help.md +2 -2
  94. package/commands/oxe/loop.md +3 -3
  95. package/commands/oxe/milestone.md +3 -3
  96. package/commands/oxe/next.md +3 -3
  97. package/commands/oxe/obs.md +3 -3
  98. package/commands/oxe/oxe.md +5 -5
  99. package/commands/oxe/plan-agent.md +2 -2
  100. package/commands/oxe/plan.md +2 -2
  101. package/commands/oxe/project.md +3 -3
  102. package/commands/oxe/quick.md +2 -2
  103. package/commands/oxe/research.md +3 -3
  104. package/commands/oxe/retro.md +3 -3
  105. package/commands/oxe/review-pr.md +3 -3
  106. package/commands/oxe/route.md +3 -3
  107. package/commands/oxe/scan.md +3 -3
  108. package/commands/oxe/security.md +3 -3
  109. package/commands/oxe/session.md +4 -4
  110. package/commands/oxe/ship.md +49 -0
  111. package/commands/oxe/skill.md +2 -2
  112. package/commands/oxe/spec.md +4 -4
  113. package/commands/oxe/ui-review.md +3 -3
  114. package/commands/oxe/ui-spec.md +3 -3
  115. package/commands/oxe/update.md +2 -2
  116. package/commands/oxe/validate-gaps.md +3 -3
  117. package/commands/oxe/verify.md +7 -4
  118. package/commands/oxe/workstream.md +3 -3
  119. package/lib/runtime/audit/audit-trail.d.ts +71 -0
  120. package/lib/runtime/audit/audit-trail.js +154 -0
  121. package/lib/runtime/audit/index.d.ts +2 -0
  122. package/lib/runtime/audit/index.js +18 -0
  123. package/lib/runtime/audit/policy-pack.d.ts +15 -0
  124. package/lib/runtime/audit/policy-pack.js +57 -0
  125. package/lib/runtime/context/context-pack-builder.d.ts +15 -0
  126. package/lib/runtime/context/context-pack-builder.js +42 -0
  127. package/lib/runtime/context/context-pack-store.d.ts +38 -0
  128. package/lib/runtime/context/context-pack-store.js +142 -0
  129. package/lib/runtime/context/context-profiles.d.ts +11 -0
  130. package/lib/runtime/context/context-profiles.js +51 -0
  131. package/lib/runtime/context/index.d.ts +2 -0
  132. package/lib/runtime/context/index.js +2 -0
  133. package/lib/runtime/decision/decision-engine.d.ts +43 -0
  134. package/lib/runtime/decision/decision-engine.js +127 -0
  135. package/lib/runtime/decision/decision-memo.d.ts +53 -0
  136. package/lib/runtime/decision/decision-memo.js +173 -0
  137. package/lib/runtime/decision/index.d.ts +2 -0
  138. package/lib/runtime/decision/index.js +18 -0
  139. package/lib/runtime/delivery/branch-manager.d.ts +1 -0
  140. package/lib/runtime/delivery/branch-manager.js +7 -0
  141. package/lib/runtime/delivery/ci-checks.js +34 -1
  142. package/lib/runtime/delivery/delivery-records.d.ts +34 -0
  143. package/lib/runtime/delivery/delivery-records.js +48 -0
  144. package/lib/runtime/delivery/index.d.ts +2 -0
  145. package/lib/runtime/delivery/index.js +2 -0
  146. package/lib/runtime/delivery/promotion-pipeline.d.ts +63 -0
  147. package/lib/runtime/delivery/promotion-pipeline.js +224 -0
  148. package/lib/runtime/gate/gate-manager.d.ts +41 -0
  149. package/lib/runtime/gate/gate-manager.js +108 -1
  150. package/lib/runtime/index.d.ts +5 -2
  151. package/lib/runtime/index.js +7 -1
  152. package/lib/runtime/models/gate-decision.d.ts +4 -1
  153. package/lib/runtime/models/workspace.d.ts +3 -0
  154. package/lib/runtime/plugins/capability-adapter.d.ts +12 -0
  155. package/lib/runtime/plugins/capability-adapter.js +204 -0
  156. package/lib/runtime/plugins/capability-matrix.d.ts +25 -0
  157. package/lib/runtime/plugins/capability-matrix.js +90 -0
  158. package/lib/runtime/plugins/index.d.ts +3 -0
  159. package/lib/runtime/plugins/index.js +3 -0
  160. package/lib/runtime/plugins/plugin-abi.d.ts +2 -0
  161. package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
  162. package/lib/runtime/plugins/plugin-manifest.js +95 -0
  163. package/lib/runtime/plugins/plugin-registry.d.ts +46 -0
  164. package/lib/runtime/plugins/plugin-registry.js +84 -2
  165. package/lib/runtime/policy/policy-engine.d.ts +47 -1
  166. package/lib/runtime/policy/policy-engine.js +172 -9
  167. package/lib/runtime/projection/projection-engine.d.ts +9 -1
  168. package/lib/runtime/projection/projection-engine.js +73 -3
  169. package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
  170. package/lib/runtime/reducers/run-state-reducer.js +117 -1
  171. package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
  172. package/lib/runtime/scheduler/agent-registry.js +96 -0
  173. package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
  174. package/lib/runtime/scheduler/agent-roles.js +62 -0
  175. package/lib/runtime/scheduler/index.d.ts +3 -0
  176. package/lib/runtime/scheduler/index.js +3 -0
  177. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +45 -1
  178. package/lib/runtime/scheduler/multi-agent-coordinator.js +234 -35
  179. package/lib/runtime/scheduler/run-journal.d.ts +18 -0
  180. package/lib/runtime/scheduler/run-journal.js +54 -0
  181. package/lib/runtime/scheduler/scheduler.d.ts +29 -1
  182. package/lib/runtime/scheduler/scheduler.js +387 -14
  183. package/lib/runtime/verification/index.d.ts +1 -0
  184. package/lib/runtime/verification/index.js +1 -0
  185. package/lib/runtime/verification/verification-compiler.d.ts +43 -0
  186. package/lib/runtime/verification/verification-compiler.js +137 -0
  187. package/lib/runtime/verification/verification-manifest.d.ts +67 -0
  188. package/lib/runtime/verification/verification-manifest.js +179 -0
  189. package/lib/runtime/workspace/strategies/ephemeral-container.d.ts +1 -0
  190. package/lib/runtime/workspace/strategies/ephemeral-container.js +4 -0
  191. package/lib/runtime/workspace/strategies/git-worktree.d.ts +1 -0
  192. package/lib/runtime/workspace/strategies/git-worktree.js +2 -0
  193. package/lib/runtime/workspace/strategies/inplace.d.ts +1 -0
  194. package/lib/runtime/workspace/strategies/inplace.js +2 -0
  195. package/lib/runtime/workspace/workspace-manager.d.ts +2 -1
  196. package/lib/sdk/README.md +9 -9
  197. package/lib/sdk/index.cjs +33 -24
  198. package/lib/sdk/index.d.ts +149 -14
  199. package/oxe/templates/ACTIVE-RUN.template.json +32 -32
  200. package/oxe/templates/CAPABILITIES.template.md +7 -7
  201. package/oxe/templates/CAPABILITY.template.md +45 -45
  202. package/oxe/templates/CHECKPOINTS.template.md +7 -7
  203. package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -68
  204. package/oxe/templates/HYPOTHESES.template.md +33 -33
  205. package/oxe/templates/LESSONS-METRICS.template.json +13 -13
  206. package/oxe/templates/NOTES.template.md +16 -16
  207. package/oxe/templates/PLAN-REVIEW.template.md +31 -31
  208. package/oxe/templates/SESSION.template.md +34 -34
  209. package/oxe/templates/SKILL.template.md +26 -26
  210. package/oxe/templates/STATE.md +55 -55
  211. package/oxe/templates/WORKFLOW_AUTHORING.md +18 -18
  212. package/oxe/workflows/ask.md +96 -92
  213. package/oxe/workflows/capabilities.md +25 -25
  214. package/oxe/workflows/checkpoint.md +14 -10
  215. package/oxe/workflows/dashboard.md +33 -33
  216. package/oxe/workflows/debug.md +19 -15
  217. package/oxe/workflows/discuss.md +12 -12
  218. package/oxe/workflows/execute.md +44 -2
  219. package/oxe/workflows/forensics.md +13 -9
  220. package/oxe/workflows/help.md +352 -304
  221. package/oxe/workflows/loop.md +17 -13
  222. package/oxe/workflows/next.md +22 -22
  223. package/oxe/workflows/obs.md +4 -0
  224. package/oxe/workflows/oxe.md +64 -31
  225. package/oxe/workflows/plan-agent.md +9 -9
  226. package/oxe/workflows/project.md +6 -1
  227. package/oxe/workflows/quick.md +10 -10
  228. package/oxe/workflows/references/reasoning-discovery.md +28 -28
  229. package/oxe/workflows/references/reasoning-execution.md +29 -29
  230. package/oxe/workflows/references/reasoning-planning.md +32 -32
  231. package/oxe/workflows/references/reasoning-review.md +29 -29
  232. package/oxe/workflows/references/reasoning-status.md +24 -24
  233. package/oxe/workflows/references/robustness-elevation.md +295 -295
  234. package/oxe/workflows/references/workflow-runtime-contracts.json +952 -907
  235. package/oxe/workflows/research.md +32 -28
  236. package/oxe/workflows/retro.md +4 -0
  237. package/oxe/workflows/review-pr.md +15 -11
  238. package/oxe/workflows/route.md +16 -16
  239. package/oxe/workflows/scan.md +4 -0
  240. package/oxe/workflows/security.md +14 -10
  241. package/oxe/workflows/session.md +213 -197
  242. package/oxe/workflows/ship.md +142 -0
  243. package/oxe/workflows/skill.md +44 -44
  244. package/oxe/workflows/spec.md +15 -0
  245. package/oxe/workflows/ui-review.md +20 -16
  246. package/oxe/workflows/ui-spec.md +7 -3
  247. package/oxe/workflows/validate-gaps.md +13 -9
  248. package/oxe/workflows/verify-audit.md +73 -73
  249. package/oxe/workflows/verify.md +52 -3
  250. package/package.json +92 -92
  251. package/packages/runtime/package.json +17 -17
  252. package/packages/runtime/src/audit/audit-trail.ts +243 -0
  253. package/packages/runtime/src/audit/index.ts +2 -0
  254. package/packages/runtime/src/audit/policy-pack.ts +62 -0
  255. package/packages/runtime/src/compiler/graph-compiler.ts +245 -245
  256. package/packages/runtime/src/compiler/index.ts +1 -1
  257. package/packages/runtime/src/context/context-pack-builder.ts +259 -193
  258. package/packages/runtime/src/context/context-pack-store.ts +197 -0
  259. package/packages/runtime/src/context/context-profiles.ts +60 -0
  260. package/packages/runtime/src/context/index.ts +3 -1
  261. package/packages/runtime/src/decision/decision-engine.ts +174 -0
  262. package/packages/runtime/src/decision/decision-memo.ts +211 -0
  263. package/packages/runtime/src/decision/index.ts +2 -0
  264. package/packages/runtime/src/delivery/branch-manager.ts +91 -84
  265. package/packages/runtime/src/delivery/ci-checks.ts +285 -252
  266. package/packages/runtime/src/delivery/delivery-records.ts +75 -0
  267. package/packages/runtime/src/delivery/index.ts +5 -3
  268. package/packages/runtime/src/delivery/pr-manager.ts +112 -112
  269. package/packages/runtime/src/delivery/promotion-pipeline.ts +334 -0
  270. package/packages/runtime/src/events/bus.ts +92 -92
  271. package/packages/runtime/src/events/catalog.ts +29 -29
  272. package/packages/runtime/src/events/envelope.ts +14 -14
  273. package/packages/runtime/src/events/index.ts +3 -3
  274. package/packages/runtime/src/evidence/evidence-store.ts +130 -130
  275. package/packages/runtime/src/evidence/index.ts +1 -1
  276. package/packages/runtime/src/gate/gate-manager.ts +289 -137
  277. package/packages/runtime/src/gate/index.ts +1 -1
  278. package/packages/runtime/src/index.ts +41 -32
  279. package/packages/runtime/src/models/attempt.ts +19 -19
  280. package/packages/runtime/src/models/evidence.ts +21 -21
  281. package/packages/runtime/src/models/gate-decision.ts +25 -21
  282. package/packages/runtime/src/models/index.ts +8 -8
  283. package/packages/runtime/src/models/run.ts +24 -24
  284. package/packages/runtime/src/models/session.ts +11 -11
  285. package/packages/runtime/src/models/verification-result.ts +10 -10
  286. package/packages/runtime/src/models/work-item.ts +25 -25
  287. package/packages/runtime/src/models/workspace.ts +31 -28
  288. package/packages/runtime/src/plugins/capability-adapter.ts +206 -0
  289. package/packages/runtime/src/plugins/capability-matrix.ts +126 -0
  290. package/packages/runtime/src/plugins/index.ts +5 -2
  291. package/packages/runtime/src/plugins/plugin-abi.ts +97 -95
  292. package/packages/runtime/src/plugins/plugin-manifest.ts +118 -0
  293. package/packages/runtime/src/plugins/plugin-registry.ts +232 -119
  294. package/packages/runtime/src/policy/index.ts +1 -1
  295. package/packages/runtime/src/policy/policy-engine.ts +330 -113
  296. package/packages/runtime/src/projection/index.ts +1 -1
  297. package/packages/runtime/src/projection/projection-engine.ts +328 -249
  298. package/packages/runtime/src/reducers/debug-reducer.ts +36 -36
  299. package/packages/runtime/src/reducers/index.ts +2 -2
  300. package/packages/runtime/src/reducers/run-state-reducer.ts +269 -127
  301. package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
  302. package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
  303. package/packages/runtime/src/scheduler/index.ts +4 -1
  304. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +521 -231
  305. package/packages/runtime/src/scheduler/run-journal.ts +62 -0
  306. package/packages/runtime/src/scheduler/scheduler.ts +722 -281
  307. package/packages/runtime/src/verification/index.ts +2 -1
  308. package/packages/runtime/src/verification/verification-compiler.ts +436 -225
  309. package/packages/runtime/src/verification/verification-manifest.ts +252 -0
  310. package/packages/runtime/src/workspace/index.ts +5 -5
  311. package/packages/runtime/src/workspace/strategies/ephemeral-container.ts +126 -121
  312. package/packages/runtime/src/workspace/strategies/git-worktree.ts +79 -77
  313. package/packages/runtime/src/workspace/strategies/inplace.ts +38 -35
  314. package/packages/runtime/src/workspace/workspace-manager.ts +16 -15
  315. package/packages/runtime/tsconfig.json +17 -17
  316. package/vscode-extension/.vscodeignore +7 -7
  317. package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
  318. package/vscode-extension/package.json +185 -185
  319. package/vscode-extension/src/extension.js +310 -310
  320. package/vscode-extension/src/shared/contextLoader.js +137 -137
  321. package/vscode-extension/src/shared/contractBuilder.js +159 -159
  322. package/vscode-extension/src/shared/stateReader.js +101 -101
@@ -1,1445 +1,1445 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
- const operational = require('./oxe-operational.cjs');
7
-
8
- const MIN_AZURE_CLI_MAJOR = 2;
9
- const AZURE_CAPABILITY_IDS = [
10
- 'azure-auth',
11
- 'azure-resource-graph',
12
- 'azure-servicebus',
13
- 'azure-eventgrid',
14
- 'azure-sql-admin',
15
- ];
16
-
17
- const DEFAULT_AZURE_PROFILE = {
18
- cloud: 'AzureCloud',
19
- tenant_id: null,
20
- subscription_id: null,
21
- subscription_name: null,
22
- auth_mode: 'unknown',
23
- default_resource_group: '',
24
- preferred_locations: [],
25
- last_auth_check: null,
26
- resource_graph_enabled: false,
27
- };
28
-
29
- const RESOURCE_GRAPH_QUERY = [
30
- 'Resources',
31
- '| project',
32
- 'id,',
33
- 'name,',
34
- 'type,',
35
- 'resourceGroup,',
36
- 'location,',
37
- 'subscriptionId,',
38
- 'tags,',
39
- 'sku=tostring(sku.name)',
40
- ].join(' ');
41
-
42
- function ensureDir(dirPath) {
43
- fs.mkdirSync(dirPath, { recursive: true });
44
- }
45
-
46
- function ensureDirForFile(filePath) {
47
- ensureDir(path.dirname(filePath));
48
- }
49
-
50
- function readTextIfExists(filePath) {
51
- try {
52
- return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- function readJsonIfExists(filePath, fallback = null) {
59
- const raw = readTextIfExists(filePath);
60
- if (!raw) return fallback;
61
- try {
62
- return JSON.parse(raw);
63
- } catch {
64
- return fallback;
65
- }
66
- }
67
-
68
- function writeJson(filePath, value) {
69
- ensureDirForFile(filePath);
70
- fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
71
- }
72
-
73
- function writeText(filePath, value) {
74
- ensureDirForFile(filePath);
75
- fs.writeFileSync(filePath, value, 'utf8');
76
- }
77
-
78
- function azurePaths(projectRoot) {
79
- const root = path.join(projectRoot, '.oxe', 'cloud', 'azure');
80
- return {
81
- root,
82
- profile: path.join(root, 'profile.json'),
83
- authStatus: path.join(root, 'auth-status.json'),
84
- inventory: path.join(root, 'inventory.json'),
85
- inventoryMd: path.join(root, 'INVENTORY.md'),
86
- serviceBusMd: path.join(root, 'SERVICEBUS.md'),
87
- eventGridMd: path.join(root, 'EVENTGRID.md'),
88
- sqlMd: path.join(root, 'SQL.md'),
89
- operationsDir: path.join(root, 'operations'),
90
- };
91
- }
92
-
93
- function ensureAzureArtifacts(projectRoot) {
94
- const p = azurePaths(projectRoot);
95
- ensureDir(p.root);
96
- ensureDir(p.operationsDir);
97
- return p;
98
- }
99
-
100
- function isAzureContextEnabled(projectRoot, config = {}) {
101
- const p = azurePaths(projectRoot);
102
- const capsRoot = path.join(projectRoot, '.oxe', 'capabilities');
103
- const azureConfig = config && typeof config.azure === 'object' ? config.azure : null;
104
- return Boolean(
105
- (azureConfig && azureConfig.enabled) ||
106
- fs.existsSync(p.root) ||
107
- fs.existsSync(p.profile) ||
108
- fs.existsSync(p.inventory) ||
109
- AZURE_CAPABILITY_IDS.some((id) => fs.existsSync(path.join(capsRoot, id, 'CAPABILITY.md')))
110
- );
111
- }
112
-
113
- function redactString(value, fallback = '[redacted]') {
114
- if (value == null || value === '') return null;
115
- return fallback;
116
- }
117
-
118
- function redactObject(value) {
119
- if (Array.isArray(value)) return value.map((item) => redactObject(item));
120
- if (!value || typeof value !== 'object') return value;
121
- const out = {};
122
- for (const [key, item] of Object.entries(value)) {
123
- if (/(token|secret|password|connection.?string|access.?key|primary.?key|secondary.?key)/i.test(key)) {
124
- out[key] = redactString(item);
125
- } else {
126
- out[key] = redactObject(item);
127
- }
128
- }
129
- return out;
130
- }
131
-
132
- function runAz(args, options = {}) {
133
- if (typeof options.runner === 'function') {
134
- return options.runner(args, options);
135
- }
136
- const spawnOptions = {
137
- cwd: options.cwd || process.cwd(),
138
- env: { ...process.env, ...(options.env || {}) },
139
- encoding: 'utf8',
140
- shell: false,
141
- timeout: options.timeoutMs || 30000,
142
- stdio: options.inherit ? 'inherit' : 'pipe',
143
- };
144
- let result;
145
- if (process.platform === 'win32') {
146
- const quoteForCmd = (value) => {
147
- const raw = String(value == null ? '' : value);
148
- if (!raw.length) return '""';
149
- if (!/[ \t"&|<>^]/.test(raw)) return raw;
150
- return `"${raw.replace(/"/g, '""')}"`;
151
- };
152
- const commandLine = ['az', ...args].map((item) => quoteForCmd(item)).join(' ');
153
- result = spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', commandLine], spawnOptions);
154
- } else {
155
- result = spawnSync('az', args, spawnOptions);
156
- }
157
- return {
158
- status: typeof result.status === 'number' ? result.status : 1,
159
- stdout: typeof result.stdout === 'string' ? result.stdout : '',
160
- stderr: typeof result.stderr === 'string' ? result.stderr : '',
161
- error: result.error || null,
162
- };
163
- }
164
-
165
- function parseJsonOutput(raw, fallback = null) {
166
- try {
167
- return JSON.parse(String(raw || '').trim() || 'null');
168
- } catch {
169
- return fallback;
170
- }
171
- }
172
-
173
- function detectAzureCli(projectRoot, options = {}) {
174
- const result = runAz(['version', '--output', 'json'], { cwd: projectRoot, ...options });
175
- if (result.error) {
176
- return {
177
- installed: false,
178
- version: null,
179
- major: null,
180
- okVersion: false,
181
- message: result.error.message || 'Azure CLI não encontrada.',
182
- raw: null,
183
- };
184
- }
185
- if (result.status !== 0) {
186
- return {
187
- installed: false,
188
- version: null,
189
- major: null,
190
- okVersion: false,
191
- message: (result.stderr || result.stdout || 'Azure CLI não encontrada.').trim(),
192
- raw: null,
193
- };
194
- }
195
- const parsed = parseJsonOutput(result.stdout, {});
196
- const version = String((parsed && parsed['azure-cli']) || '').trim() || null;
197
- const major = version ? parseInt(version.split('.')[0], 10) : null;
198
- return {
199
- installed: true,
200
- version,
201
- major,
202
- okVersion: Number.isInteger(major) && major >= MIN_AZURE_CLI_MAJOR,
203
- message: null,
204
- raw: parsed,
205
- };
206
- }
207
-
208
- function normalizeAuthMode(account) {
209
- const userType = String(account && account.user && account.user.type || '').toLowerCase();
210
- if (userType === 'serviceprincipal') return 'service_principal';
211
- if (userType === 'user') return 'user_mfa';
212
- if (userType === 'managedidentity') return 'managed_identity';
213
- return 'unknown';
214
- }
215
-
216
- function normalizeAzureProfile(account, cloud, existingProfile = {}) {
217
- const profile = {
218
- ...DEFAULT_AZURE_PROFILE,
219
- ...(existingProfile && typeof existingProfile === 'object' ? existingProfile : {}),
220
- };
221
- if (!account || typeof account !== 'object') return profile;
222
- return {
223
- ...profile,
224
- cloud: String((cloud && cloud.name) || profile.cloud || 'AzureCloud'),
225
- tenant_id: account.tenantId || profile.tenant_id || null,
226
- subscription_id: account.id || profile.subscription_id || null,
227
- subscription_name: account.name || profile.subscription_name || null,
228
- auth_mode: normalizeAuthMode(account),
229
- last_auth_check: new Date().toISOString(),
230
- };
231
- }
232
-
233
- function loadAzureProfile(projectRoot) {
234
- return {
235
- ...DEFAULT_AZURE_PROFILE,
236
- ...(readJsonIfExists(azurePaths(projectRoot).profile, {}) || {}),
237
- };
238
- }
239
-
240
- function loadAzureAuthStatus(projectRoot) {
241
- return readJsonIfExists(azurePaths(projectRoot).authStatus, null);
242
- }
243
-
244
- function loadAzureInventory(projectRoot) {
245
- return readJsonIfExists(azurePaths(projectRoot).inventory, null);
246
- }
247
-
248
- function deriveServiceFamily(type) {
249
- const value = String(type || '').toLowerCase();
250
- if (value.startsWith('microsoft.servicebus/')) return 'servicebus';
251
- if (value.startsWith('microsoft.eventgrid/')) return 'eventgrid';
252
- if (value.startsWith('microsoft.sql/')) return 'sql';
253
- return 'other';
254
- }
255
-
256
- function summarizeInventory(items) {
257
- const summary = {
258
- total: 0,
259
- servicebus: 0,
260
- eventgrid: 0,
261
- sql: 0,
262
- other: 0,
263
- };
264
- for (const item of items || []) {
265
- const family = deriveServiceFamily(item.type || item.service_family);
266
- summary.total += 1;
267
- summary[family] = (summary[family] || 0) + 1;
268
- }
269
- return summary;
270
- }
271
-
272
- function normalizeInventoryItem(item = {}) {
273
- const normalized = {
274
- id: item.id || '',
275
- name: item.name || '',
276
- type: item.type || '',
277
- resourceGroup: item.resourceGroup || item.resource_group || '',
278
- location: item.location || '',
279
- subscriptionId: item.subscriptionId || item.subscription_id || '',
280
- tags: item.tags && typeof item.tags === 'object' ? item.tags : {},
281
- sku: item.sku || '',
282
- };
283
- normalized.service_family = deriveServiceFamily(normalized.type);
284
- return normalized;
285
- }
286
-
287
- function renderInventoryMarkdown(title, profile, authStatus, items, syncedAt) {
288
- const summary = summarizeInventory(items);
289
- const lines = [
290
- `# OXE — ${title}`,
291
- '',
292
- '> Inventário Azure materializado pelo provider nativo do OXE via Azure CLI.',
293
- '',
294
- `- **Cloud:** ${profile.cloud || '—'}`,
295
- `- **Tenant:** ${profile.tenant_id || '—'}`,
296
- `- **Subscription:** ${profile.subscription_name || profile.subscription_id || '—'}`,
297
- `- **Auth mode:** ${profile.auth_mode || 'unknown'}`,
298
- `- **Último check de auth:** ${profile.last_auth_check || authStatus && authStatus.checked_at || '—'}`,
299
- `- **Último sync:** ${syncedAt || '—'}`,
300
- '',
301
- '## Resumo',
302
- '',
303
- `- **Total:** ${summary.total}`,
304
- `- **Service Bus:** ${summary.servicebus}`,
305
- `- **Event Grid:** ${summary.eventgrid}`,
306
- `- **Azure SQL:** ${summary.sql}`,
307
- `- **Outros:** ${summary.other}`,
308
- '',
309
- '| Nome | Tipo | Família | Resource Group | Location | SKU |',
310
- '|------|------|---------|----------------|----------|-----|',
311
- ];
312
- if (!items.length) {
313
- lines.push('| (vazio) | — | — | — | — | — |');
314
- } else {
315
- for (const item of items) {
316
- lines.push(
317
- `| ${item.name || '—'} | ${item.type || '—'} | ${item.service_family || 'other'} | ${item.resourceGroup || '—'} | ${item.location || '—'} | ${item.sku || '—'} |`
318
- );
319
- }
320
- }
321
- lines.push('');
322
- return lines.join('\n');
323
- }
324
-
325
- function listAzureOperations(projectRoot) {
326
- const p = azurePaths(projectRoot);
327
- if (!fs.existsSync(p.operationsDir)) return [];
328
- return fs
329
- .readdirSync(p.operationsDir)
330
- .filter((name) => name.endsWith('.json'))
331
- .map((name) => readJsonIfExists(path.join(p.operationsDir, name), null))
332
- .filter(Boolean)
333
- .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
334
- }
335
-
336
- function writeAzureAuthArtifacts(projectRoot, payload) {
337
- const p = ensureAzureArtifacts(projectRoot);
338
- if (payload.profile) writeJson(p.profile, payload.profile);
339
- if (payload.authStatus) writeJson(p.authStatus, redactObject(payload.authStatus));
340
- return p;
341
- }
342
-
343
- function getAzureContext(projectRoot, options = {}) {
344
- const cli = detectAzureCli(projectRoot, options);
345
- const existingProfile = loadAzureProfile(projectRoot);
346
- if (!cli.installed) {
347
- const authStatus = {
348
- checked_at: new Date().toISOString(),
349
- installed: false,
350
- version: null,
351
- login_active: false,
352
- subscription_selected: false,
353
- tenant_id: null,
354
- subscription_id: existingProfile.subscription_id || null,
355
- subscription_name: existingProfile.subscription_name || null,
356
- cloud: existingProfile.cloud || 'AzureCloud',
357
- auth_mode: existingProfile.auth_mode || 'unknown',
358
- user: null,
359
- user_type: null,
360
- resource_graph_enabled: false,
361
- warnings: [cli.message || 'Azure CLI não instalada.'],
362
- };
363
- if (options.write !== false) {
364
- writeAzureAuthArtifacts(projectRoot, { profile: existingProfile, authStatus });
365
- }
366
- return { cli, profile: existingProfile, authStatus, account: null, cloud: null, extension: null };
367
- }
368
-
369
- const accountResult = runAz(['account', 'show', '--output', 'json'], { cwd: projectRoot, ...options });
370
- if (accountResult.status !== 0) {
371
- const authStatus = {
372
- checked_at: new Date().toISOString(),
373
- installed: true,
374
- version: cli.version,
375
- login_active: false,
376
- subscription_selected: Boolean(existingProfile.subscription_id),
377
- tenant_id: existingProfile.tenant_id || null,
378
- subscription_id: existingProfile.subscription_id || null,
379
- subscription_name: existingProfile.subscription_name || null,
380
- cloud: existingProfile.cloud || 'AzureCloud',
381
- auth_mode: existingProfile.auth_mode || 'unknown',
382
- user: null,
383
- user_type: null,
384
- resource_graph_enabled: false,
385
- warnings: ['Azure CLI instalada, mas sem sessão ativa. Execute "oxe-cc azure auth login".'],
386
- };
387
- if (options.write !== false) {
388
- writeAzureAuthArtifacts(projectRoot, { profile: existingProfile, authStatus });
389
- }
390
- return { cli, profile: existingProfile, authStatus, account: null, cloud: null, extension: null };
391
- }
392
-
393
- const account = parseJsonOutput(accountResult.stdout, {});
394
- const cloud = parseJsonOutput(runAz(['cloud', 'show', '--output', 'json'], { cwd: projectRoot, ...options }).stdout, {});
395
- const extension = parseJsonOutput(
396
- runAz(['extension', 'show', '--name', 'resource-graph', '--output', 'json'], { cwd: projectRoot, ...options }).stdout,
397
- null
398
- );
399
- const profile = normalizeAzureProfile(account, cloud, existingProfile);
400
- profile.resource_graph_enabled = Boolean(extension);
401
- const authStatus = {
402
- checked_at: new Date().toISOString(),
403
- installed: true,
404
- version: cli.version,
405
- login_active: true,
406
- subscription_selected: Boolean(account.id),
407
- tenant_id: account.tenantId || null,
408
- subscription_id: account.id || null,
409
- subscription_name: account.name || null,
410
- cloud: (cloud && cloud.name) || profile.cloud || 'AzureCloud',
411
- auth_mode: normalizeAuthMode(account),
412
- user: account.user && account.user.name ? account.user.name : null,
413
- user_type: account.user && account.user.type ? account.user.type : null,
414
- resource_graph_enabled: Boolean(extension),
415
- warnings: [],
416
- };
417
- if (options.write !== false) {
418
- writeAzureAuthArtifacts(projectRoot, { profile, authStatus });
419
- }
420
- return { cli, profile, authStatus, account, cloud, extension };
421
- }
422
-
423
- function ensureResourceGraphExtension(projectRoot, options = {}) {
424
- const show = runAz(['extension', 'show', '--name', 'resource-graph', '--output', 'json'], { cwd: projectRoot, ...options });
425
- if (show.status === 0) {
426
- return {
427
- ok: true,
428
- installed: true,
429
- changed: false,
430
- extension: parseJsonOutput(show.stdout, {}),
431
- };
432
- }
433
- if (options.autoInstall === false) {
434
- return {
435
- ok: false,
436
- installed: false,
437
- changed: false,
438
- extension: null,
439
- message: 'Extensão resource-graph ausente.',
440
- };
441
- }
442
- const add = runAz(['extension', 'add', '--name', 'resource-graph', '--upgrade', '--only-show-errors'], {
443
- cwd: projectRoot,
444
- ...options,
445
- });
446
- if (add.status !== 0) {
447
- return {
448
- ok: false,
449
- installed: false,
450
- changed: false,
451
- extension: null,
452
- message: (add.stderr || add.stdout || 'Falha ao instalar resource-graph.').trim(),
453
- };
454
- }
455
- const installed = parseJsonOutput(
456
- runAz(['extension', 'show', '--name', 'resource-graph', '--output', 'json'], { cwd: projectRoot, ...options }).stdout,
457
- {}
458
- );
459
- return {
460
- ok: true,
461
- installed: true,
462
- changed: true,
463
- extension: installed,
464
- };
465
- }
466
-
467
- function syncAzureInventory(projectRoot, options = {}) {
468
- const p = ensureAzureArtifacts(projectRoot);
469
- const previousInventory = options.diff ? loadAzureInventory(projectRoot) : null;
470
- const context = getAzureContext(projectRoot, options);
471
- if (!context.cli.installed) {
472
- throw new Error('Azure CLI não instalada.');
473
- }
474
- if (!context.authStatus.login_active) {
475
- throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
476
- }
477
- const extension = ensureResourceGraphExtension(projectRoot, options);
478
- if (!extension.ok) {
479
- throw new Error(extension.message || 'Extensão resource-graph ausente.');
480
- }
481
- const args = ['graph', 'query', '-q', RESOURCE_GRAPH_QUERY, '--first', '1000', '--output', 'json'];
482
- if (context.profile.subscription_id) {
483
- args.push('--subscriptions', context.profile.subscription_id);
484
- }
485
- const result = runAz(args, { cwd: projectRoot, ...options });
486
- if (result.status !== 0) {
487
- throw new Error((result.stderr || result.stdout || 'Falha ao executar az graph query.').trim());
488
- }
489
- const parsed = parseJsonOutput(result.stdout, {});
490
- const items = Array.isArray(parsed.data) ? parsed.data.map(normalizeInventoryItem) : [];
491
- items.sort((a, b) => String(a.service_family).localeCompare(String(b.service_family)) || String(a.name).localeCompare(String(b.name)));
492
- const syncedAt = new Date().toISOString();
493
- const inventory = {
494
- oxeAzureInventorySchema: 1,
495
- synced_at: syncedAt,
496
- query: RESOURCE_GRAPH_QUERY,
497
- cloud: context.profile.cloud,
498
- tenant_id: context.profile.tenant_id,
499
- subscription_id: context.profile.subscription_id,
500
- subscription_name: context.profile.subscription_name,
501
- summary: summarizeInventory(items),
502
- items,
503
- };
504
- writeJson(p.inventory, inventory);
505
- writeText(p.inventoryMd, renderInventoryMarkdown('Azure Inventory', context.profile, context.authStatus, items, syncedAt));
506
- writeText(
507
- p.serviceBusMd,
508
- renderInventoryMarkdown(
509
- 'Service Bus',
510
- context.profile,
511
- context.authStatus,
512
- items.filter((item) => item.service_family === 'servicebus'),
513
- syncedAt
514
- )
515
- );
516
- writeText(
517
- p.eventGridMd,
518
- renderInventoryMarkdown(
519
- 'Event Grid',
520
- context.profile,
521
- context.authStatus,
522
- items.filter((item) => item.service_family === 'eventgrid'),
523
- syncedAt
524
- )
525
- );
526
- writeText(
527
- p.sqlMd,
528
- renderInventoryMarkdown(
529
- 'Azure SQL',
530
- context.profile,
531
- context.authStatus,
532
- items.filter((item) => item.service_family === 'sql'),
533
- syncedAt
534
- )
535
- );
536
- const nextProfile = {
537
- ...context.profile,
538
- resource_graph_enabled: true,
539
- last_auth_check: context.authStatus.checked_at,
540
- };
541
- writeAzureAuthArtifacts(projectRoot, {
542
- profile: nextProfile,
543
- authStatus: {
544
- ...context.authStatus,
545
- checked_at: syncedAt,
546
- resource_graph_enabled: true,
547
- last_sync: syncedAt,
548
- },
549
- });
550
- const syncResult = { paths: p, profile: nextProfile, authStatus: context.authStatus, inventory };
551
- if (options.diff && previousInventory) {
552
- syncResult.diff = diffInventory(previousInventory.items || [], items);
553
- }
554
- return syncResult;
555
- }
556
-
557
- function searchAzureInventory(projectRoot, query, filters = {}) {
558
- const inventory = loadAzureInventory(projectRoot);
559
- if (!inventory || !Array.isArray(inventory.items)) return [];
560
- let items = inventory.items;
561
- if (filters.type) {
562
- const ft = String(filters.type).toLowerCase();
563
- items = items.filter((item) =>
564
- String(item.type || '').toLowerCase().includes(ft) ||
565
- String(item.service_family || '').toLowerCase().includes(ft)
566
- );
567
- }
568
- if (filters.resourceGroup) {
569
- const frg = String(filters.resourceGroup).toLowerCase();
570
- items = items.filter((item) => String(item.resourceGroup || '').toLowerCase() === frg);
571
- }
572
- const q = String(query || '').trim().toLowerCase();
573
- if (!q) return items;
574
- return items.filter((item) => {
575
- const haystack = [
576
- item.name,
577
- item.type,
578
- item.resourceGroup,
579
- item.location,
580
- item.subscriptionId,
581
- item.service_family,
582
- ...Object.keys(item.tags || {}),
583
- ...Object.values(item.tags || {}),
584
- ]
585
- .join(' ')
586
- .toLowerCase();
587
- return haystack.includes(q);
588
- });
589
- }
590
-
591
- function diffInventory(previousItems, currentItems) {
592
- const prevMap = new Map((previousItems || []).map((item) => [item.id, item]));
593
- const currMap = new Map((currentItems || []).map((item) => [item.id, item]));
594
- const added = (currentItems || []).filter((item) => !prevMap.has(item.id));
595
- const removed = (previousItems || []).filter((item) => !currMap.has(item.id));
596
- return { added, removed, unchanged: (currentItems || []).length - added.length };
597
- }
598
-
599
- function statusAzure(projectRoot, config = {}, options = {}) {
600
- const context = getAzureContext(projectRoot, { ...options, write: false });
601
- const inventory = loadAzureInventory(projectRoot);
602
- const azureCfg = config && typeof config.azure === 'object' ? config.azure : {};
603
- const maxAgeHours = azureCfg.inventory_max_age_hours != null ? Number(azureCfg.inventory_max_age_hours) : 24;
604
- const syncedAtMs = inventory ? Date.parse(String(inventory.synced_at || '')) : null;
605
- const ageHours = syncedAtMs && !Number.isNaN(syncedAtMs) ? Math.floor((Date.now() - syncedAtMs) / (1000 * 60 * 60)) : null;
606
- const stale = ageHours !== null && maxAgeHours > 0 && ageHours > maxAgeHours;
607
- const pendingOps = listAzureOperations(projectRoot).filter((op) => op.phase === 'waiting_approval');
608
- return {
609
- cliInstalled: context.cli.installed,
610
- cliVersion: context.cli.version || null,
611
- loginActive: context.authStatus.login_active,
612
- subscription: context.profile.subscription_name || context.profile.subscription_id || null,
613
- cloud: context.profile.cloud || 'AzureCloud',
614
- resourceGraphEnabled: Boolean(context.authStatus.resource_graph_enabled),
615
- inventoryPresent: Boolean(inventory),
616
- inventoryStale: stale,
617
- inventoryAgeHours: ageHours,
618
- inventorySummary: inventory && inventory.summary ? inventory.summary : null,
619
- pendingOperations: pendingOps.length,
620
- pendingOperationIds: pendingOps.map((op) => op.operation_id),
621
- vpnRequired: Boolean(azureCfg.vpn_required),
622
- };
623
- }
624
-
625
- function renderCapabilityManifest(manifest) {
626
- return [
627
- '---',
628
- 'oxe_capability: true',
629
- `id: ${manifest.id}`,
630
- 'version: 1',
631
- 'type: script',
632
- 'status: active',
633
- `scope: ${manifest.scope}`,
634
- `entrypoint: "${manifest.entrypoint}"`,
635
- `approval_policy: ${manifest.approval_policy}`,
636
- `side_effects: [${manifest.side_effects.map((item) => item).join(', ')}]`,
637
- `requires_env: [${manifest.requires_env.map((item) => item).join(', ')}]`,
638
- `evidence_outputs: [${manifest.evidence_outputs.map((item) => item).join(', ')}]`,
639
- 'session_compatibility: [legacy, session]',
640
- '---',
641
- '',
642
- `# OXE — Capability ${manifest.id}`,
643
- '',
644
- '## Objetivo',
645
- '',
646
- `- ${manifest.summary}`,
647
- '',
648
- '## Escopo OXE',
649
- '',
650
- `- ${manifest.scope}`,
651
- '',
652
- '## Operações',
653
- '',
654
- ...manifest.operations.map((op) => `- ${op}`),
655
- '',
656
- '## Entradas e saídas',
657
- '',
658
- '- Entradas resolvidas pelo provider Azure e pelo runtime do OXE.',
659
- '- Saídas persistidas em `.oxe/cloud/azure/operations/` e no trace operacional.',
660
- '',
661
- '## Requisitos',
662
- '',
663
- '- Azure CLI instalada localmente.',
664
- '- Sessão Azure válida ou contexto autenticado compatível.',
665
- '',
666
- '## Evidência e segurança',
667
- '',
668
- '- Toda mutação gera plano, checkpoint e evidência redacted.',
669
- '- Segredos não são persistidos em `.oxe/`.',
670
- '',
671
- ].join('\n');
672
- }
673
-
674
- function ensureAzureCapabilities(projectRoot) {
675
- const capsDir = path.join(projectRoot, '.oxe', 'capabilities');
676
- ensureDir(capsDir);
677
- const manifests = [
678
- {
679
- id: 'azure-auth',
680
- scope: 'ask',
681
- entrypoint: 'oxe-cc azure auth <login|whoami|set-subscription>',
682
- approval_policy: 'always_allow',
683
- side_effects: [],
684
- requires_env: [],
685
- evidence_outputs: ['.oxe/cloud/azure/auth-status.json', '.oxe/cloud/azure/profile.json'],
686
- summary: 'Autenticação e contexto Azure corporativo via Azure CLI.',
687
- operations: ['auth login', 'auth whoami', 'auth set-subscription'],
688
- },
689
- {
690
- id: 'azure-resource-graph',
691
- scope: 'research',
692
- entrypoint: 'oxe-cc azure sync',
693
- approval_policy: 'always_allow',
694
- side_effects: [],
695
- requires_env: [],
696
- evidence_outputs: ['.oxe/cloud/azure/inventory.json', '.oxe/cloud/azure/INVENTORY.md'],
697
- summary: 'Discovery determinístico de recursos Azure via Resource Graph.',
698
- operations: ['sync inventory', 'find resource'],
699
- },
700
- {
701
- id: 'azure-servicebus',
702
- scope: 'execute',
703
- entrypoint: 'oxe-cc azure servicebus <list|show|plan|apply>',
704
- approval_policy: 'require_approval_if_external_side_effect',
705
- side_effects: ['azure_resource_mutation'],
706
- requires_env: [],
707
- evidence_outputs: ['.oxe/cloud/azure/operations/*.json', '.oxe/cloud/azure/SERVICEBUS.md'],
708
- summary: 'Gestão assistida de namespaces, queues, topics e subscriptions do Azure Service Bus.',
709
- operations: ['list', 'show', 'create namespace', 'create queue', 'create topic', 'create subscription'],
710
- },
711
- {
712
- id: 'azure-eventgrid',
713
- scope: 'execute',
714
- entrypoint: 'oxe-cc azure eventgrid <list|show|plan|apply>',
715
- approval_policy: 'require_approval_if_external_side_effect',
716
- side_effects: ['azure_resource_mutation'],
717
- requires_env: [],
718
- evidence_outputs: ['.oxe/cloud/azure/operations/*.json', '.oxe/cloud/azure/EVENTGRID.md'],
719
- summary: 'Gestão assistida de topics, system topics e event subscriptions do Azure Event Grid.',
720
- operations: ['list', 'show', 'create topic', 'create event subscription'],
721
- },
722
- {
723
- id: 'azure-sql-admin',
724
- scope: 'execute',
725
- entrypoint: 'oxe-cc azure sql <list|show|plan|apply>',
726
- approval_policy: 'require_approval_if_external_side_effect',
727
- side_effects: ['azure_resource_mutation'],
728
- requires_env: ['AZURE_SQL_ADMIN_PASSWORD'],
729
- evidence_outputs: ['.oxe/cloud/azure/operations/*.json', '.oxe/cloud/azure/SQL.md'],
730
- summary: 'Gestão assistida de servers, databases e firewall rules do Azure SQL.',
731
- operations: ['list', 'show', 'create server', 'create database', 'create firewall rule'],
732
- },
733
- ];
734
- for (const manifest of manifests) {
735
- const filePath = path.join(capsDir, manifest.id, 'CAPABILITY.md');
736
- if (!fs.existsSync(filePath)) {
737
- ensureDir(path.dirname(filePath));
738
- writeText(filePath, renderCapabilityManifest(manifest));
739
- }
740
- }
741
- return manifests.map((manifest) => manifest.id);
742
- }
743
-
744
- function makeOperationId(domain) {
745
- return `azure-${domain}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
746
- }
747
-
748
- function makeCheckpointId() {
749
- return `CP-AZ-${Date.now().toString(36).toUpperCase()}`;
750
- }
751
-
752
- function ipRangeTooWide(start, end) {
753
- const s = String(start || '').trim();
754
- const e = String(end || '').trim();
755
- return (s === '0.0.0.0' && e === '255.255.255.255') || (s === '*' && e === '*');
756
- }
757
-
758
- function requireField(input, key, label) {
759
- const value = input[key];
760
- if (value == null || value === '') {
761
- throw new Error(`Informe ${label}.`);
762
- }
763
- return String(value);
764
- }
765
-
766
- function buildReadCommand(domain, verb, input) {
767
- const resourceGroup = input.resourceGroup ? ['--resource-group', String(input.resourceGroup)] : [];
768
- if (domain === 'servicebus') {
769
- const kind = String(input.kind || 'namespace');
770
- if (verb === 'list') {
771
- if (kind === 'namespace') return ['servicebus', 'namespace', 'list', ...resourceGroup, '--output', 'json'];
772
- if (kind === 'queue') return ['servicebus', 'queue', 'list', '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
773
- if (kind === 'topic') return ['servicebus', 'topic', 'list', '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
774
- if (kind === 'subscription') {
775
- return [
776
- 'servicebus',
777
- 'topic',
778
- 'subscription',
779
- 'list',
780
- '--namespace-name',
781
- requireField(input, 'namespace', '--namespace'),
782
- '--topic-name',
783
- requireField(input, 'topicName', '--topic-name'),
784
- ...resourceGroup,
785
- '--output',
786
- 'json',
787
- ];
788
- }
789
- }
790
- if (verb === 'show') {
791
- if (kind === 'namespace') return ['servicebus', 'namespace', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
792
- if (kind === 'queue') return ['servicebus', 'queue', 'show', '--name', requireField(input, 'name', '--name'), '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
793
- if (kind === 'topic') return ['servicebus', 'topic', 'show', '--name', requireField(input, 'name', '--name'), '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
794
- if (kind === 'subscription') {
795
- return [
796
- 'servicebus',
797
- 'topic',
798
- 'subscription',
799
- 'show',
800
- '--name',
801
- requireField(input, 'subscriptionName', '--subscription-name'),
802
- '--namespace-name',
803
- requireField(input, 'namespace', '--namespace'),
804
- '--topic-name',
805
- requireField(input, 'topicName', '--topic-name'),
806
- ...resourceGroup,
807
- '--output',
808
- 'json',
809
- ];
810
- }
811
- }
812
- }
813
- if (domain === 'eventgrid') {
814
- const kind = String(input.kind || 'topic');
815
- if (verb === 'list') {
816
- if (kind === 'topic') return ['eventgrid', 'topic', 'list', ...resourceGroup, '--output', 'json'];
817
- if (kind === 'system-topic') return ['eventgrid', 'system-topic', 'list', ...resourceGroup, '--output', 'json'];
818
- if (kind === 'event-subscription') {
819
- return [
820
- 'eventgrid',
821
- 'event-subscription',
822
- 'list',
823
- '--source-resource-id',
824
- requireField(input, 'sourceResourceId', '--source-resource-id'),
825
- '--output',
826
- 'json',
827
- ];
828
- }
829
- }
830
- if (verb === 'show') {
831
- if (kind === 'topic') return ['eventgrid', 'topic', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
832
- if (kind === 'system-topic') return ['eventgrid', 'system-topic', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
833
- if (kind === 'event-subscription') {
834
- return [
835
- 'eventgrid',
836
- 'event-subscription',
837
- 'show',
838
- '--name',
839
- requireField(input, 'name', '--name'),
840
- '--source-resource-id',
841
- requireField(input, 'sourceResourceId', '--source-resource-id'),
842
- '--output',
843
- 'json',
844
- ];
845
- }
846
- }
847
- }
848
- if (domain === 'sql') {
849
- const kind = String(input.kind || 'server');
850
- if (verb === 'list') {
851
- if (kind === 'server') return ['sql', 'server', 'list', ...resourceGroup, '--output', 'json'];
852
- if (kind === 'database') return ['sql', 'db', 'list', '--server', requireField(input, 'server', '--server'), ...resourceGroup, '--output', 'json'];
853
- if (kind === 'firewall-rule') {
854
- return [
855
- 'sql',
856
- 'server',
857
- 'firewall-rule',
858
- 'list',
859
- '--server',
860
- requireField(input, 'server', '--server'),
861
- ...resourceGroup,
862
- '--output',
863
- 'json',
864
- ];
865
- }
866
- }
867
- if (verb === 'show') {
868
- if (kind === 'server') return ['sql', 'server', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
869
- if (kind === 'database') {
870
- return ['sql', 'db', 'show', '--name', requireField(input, 'name', '--name'), '--server', requireField(input, 'server', '--server'), ...resourceGroup, '--output', 'json'];
871
- }
872
- if (kind === 'firewall-rule') {
873
- return [
874
- 'sql',
875
- 'server',
876
- 'firewall-rule',
877
- 'show',
878
- '--name',
879
- requireField(input, 'name', '--name'),
880
- '--server',
881
- requireField(input, 'server', '--server'),
882
- ...resourceGroup,
883
- '--output',
884
- 'json',
885
- ];
886
- }
887
- }
888
- }
889
- throw new Error(`Combinação ${domain}/${verb} ainda não suportada.`);
890
- }
891
-
892
- function buildMutationPlan(domain, input) {
893
- const kind = String(input.kind || '').toLowerCase();
894
- const resourceGroup = requireField(input, 'resourceGroup', '--resource-group');
895
- const location = input.location ? String(input.location) : null;
896
- const operation = {
897
- operation_id: makeOperationId(domain),
898
- created_at: new Date().toISOString(),
899
- updated_at: new Date().toISOString(),
900
- domain,
901
- phase: 'planned',
902
- kind,
903
- action: 'create',
904
- mutate: true,
905
- approval_policy: 'require_approval_if_external_side_effect',
906
- checkpoint_id: makeCheckpointId(),
907
- resource_group: resourceGroup,
908
- location,
909
- resource_refs: [],
910
- evidence_outputs: [],
911
- blocked: false,
912
- blocked_reason: null,
913
- summary: '',
914
- command_args: [],
915
- command_display: '',
916
- command_display_redacted: '',
917
- metadata: {},
918
- };
919
- if (domain === 'servicebus') {
920
- const namespace = input.namespace ? String(input.namespace) : null;
921
- if (kind === 'namespace') {
922
- const name = requireField(input, 'name', '--name');
923
- operation.command_args = ['servicebus', 'namespace', 'create', '--name', name, '--resource-group', resourceGroup];
924
- if (location) operation.command_args.push('--location', location);
925
- operation.summary = `Criar namespace Service Bus ${name}`;
926
- operation.resource_refs.push({ kind: 'namespace', name, resourceGroup });
927
- } else if (kind === 'queue') {
928
- const name = requireField(input, 'name', '--name');
929
- operation.command_args = ['servicebus', 'queue', 'create', '--name', name, '--namespace-name', requireField(input, 'namespace', '--namespace'), '--resource-group', resourceGroup];
930
- operation.summary = `Criar queue Service Bus ${name} no namespace ${namespace}`;
931
- operation.resource_refs.push({ kind: 'queue', name, namespace, resourceGroup });
932
- } else if (kind === 'topic') {
933
- const name = requireField(input, 'name', '--name');
934
- operation.command_args = ['servicebus', 'topic', 'create', '--name', name, '--namespace-name', requireField(input, 'namespace', '--namespace'), '--resource-group', resourceGroup];
935
- operation.summary = `Criar topic Service Bus ${name} no namespace ${namespace}`;
936
- operation.resource_refs.push({ kind: 'topic', name, namespace, resourceGroup });
937
- } else if (kind === 'subscription') {
938
- const name = requireField(input, 'subscriptionName', '--subscription-name');
939
- const topicName = requireField(input, 'topicName', '--topic-name');
940
- operation.command_args = ['servicebus', 'topic', 'subscription', 'create', '--name', name, '--namespace-name', requireField(input, 'namespace', '--namespace'), '--topic-name', topicName, '--resource-group', resourceGroup];
941
- operation.summary = `Criar subscription ${name} no topic ${topicName}`;
942
- operation.resource_refs.push({ kind: 'subscription', name, namespace, topicName, resourceGroup });
943
- } else {
944
- throw new Error('Service Bus suporta kind namespace | queue | topic | subscription.');
945
- }
946
- } else if (domain === 'eventgrid') {
947
- if (kind === 'topic') {
948
- const name = requireField(input, 'name', '--name');
949
- operation.command_args = ['eventgrid', 'topic', 'create', '--name', name, '--resource-group', resourceGroup, '--location', location || 'eastus'];
950
- operation.summary = `Criar topic Event Grid ${name}`;
951
- operation.resource_refs.push({ kind: 'topic', name, resourceGroup });
952
- } else if (kind === 'event-subscription') {
953
- const name = requireField(input, 'name', '--name');
954
- const sourceResourceId = requireField(input, 'sourceResourceId', '--source-resource-id');
955
- const endpoint = requireField(input, 'endpoint', '--endpoint');
956
- operation.command_args = ['eventgrid', 'event-subscription', 'create', '--name', name, '--source-resource-id', sourceResourceId, '--endpoint', endpoint];
957
- operation.summary = `Criar event subscription ${name}`;
958
- operation.resource_refs.push({ kind: 'event-subscription', name, sourceResourceId });
959
- operation.metadata.endpoint = endpoint;
960
- } else {
961
- throw new Error('Event Grid suporta kind topic | event-subscription para mutação na v1.');
962
- }
963
- } else if (domain === 'sql') {
964
- if (kind === 'server') {
965
- const name = requireField(input, 'name', '--name');
966
- const adminUser = requireField(input, 'adminUser', '--admin-user');
967
- const passwordEnv = requireField(input, 'adminPasswordEnv', '--admin-password-env');
968
- const passwordValue = process.env[passwordEnv] || (input.env && input.env[passwordEnv]) || null;
969
- if (!passwordValue) {
970
- throw new Error(`A variável de ambiente ${passwordEnv} não está definida.`);
971
- }
972
- operation.command_args = ['sql', 'server', 'create', '--name', name, '--resource-group', resourceGroup, '--location', location || 'eastus', '--admin-user', adminUser, '--admin-password', passwordValue];
973
- operation.summary = `Criar Azure SQL server ${name}`;
974
- operation.resource_refs.push({ kind: 'server', name, resourceGroup });
975
- operation.metadata.admin_user = adminUser;
976
- operation.metadata.admin_password_env = passwordEnv;
977
- operation.command_display_redacted = `az sql server create --name ${name} --resource-group ${resourceGroup} --location ${location || 'eastus'} --admin-user ${adminUser} --admin-password \${${passwordEnv}}`;
978
- } else if (kind === 'database') {
979
- const name = requireField(input, 'name', '--name');
980
- const server = requireField(input, 'server', '--server');
981
- const serviceObjective = input.serviceObjective ? String(input.serviceObjective) : 'S0';
982
- operation.command_args = ['sql', 'db', 'create', '--name', name, '--resource-group', resourceGroup, '--server', server, '--service-objective', serviceObjective];
983
- operation.summary = `Criar Azure SQL database ${name} no server ${server}`;
984
- operation.resource_refs.push({ kind: 'database', name, server, resourceGroup });
985
- operation.metadata.service_objective = serviceObjective;
986
- } else if (kind === 'firewall-rule') {
987
- const name = requireField(input, 'name', '--name');
988
- const server = requireField(input, 'server', '--server');
989
- const startIp = requireField(input, 'startIpAddress', '--start-ip-address');
990
- const endIp = requireField(input, 'endIpAddress', '--end-ip-address');
991
- operation.command_args = ['sql', 'server', 'firewall-rule', 'create', '--name', name, '--resource-group', resourceGroup, '--server', server, '--start-ip-address', startIp, '--end-ip-address', endIp];
992
- operation.summary = `Criar firewall rule ${name} no Azure SQL server ${server}`;
993
- operation.resource_refs.push({ kind: 'firewall-rule', name, server, resourceGroup });
994
- operation.metadata.start_ip_address = startIp;
995
- operation.metadata.end_ip_address = endIp;
996
- if (ipRangeTooWide(startIp, endIp)) {
997
- operation.approval_policy = 'deny_unless_overridden';
998
- operation.blocked = true;
999
- operation.blocked_reason = 'Faixa de firewall ampla bloqueada por política.';
1000
- }
1001
- } else {
1002
- throw new Error('Azure SQL suporta kind server | database | firewall-rule.');
1003
- }
1004
- } else {
1005
- throw new Error(`Domínio Azure desconhecido: ${domain}`);
1006
- }
1007
- if (!operation.command_display_redacted) {
1008
- operation.command_display_redacted = `az ${operation.command_args.join(' ')}`;
1009
- }
1010
- operation.command_display = operation.command_display_redacted;
1011
- operation.evidence_outputs = [
1012
- `.oxe/cloud/azure/operations/${operation.operation_id}.json`,
1013
- `.oxe/cloud/azure/operations/${operation.operation_id}.md`,
1014
- ];
1015
- return operation;
1016
- }
1017
-
1018
- function renderAzureOperationMarkdown(operation) {
1019
- return [
1020
- `# OXE — Azure Operation ${operation.operation_id}`,
1021
- '',
1022
- `- **Domínio:** ${operation.domain}`,
1023
- `- **Kind:** ${operation.kind}`,
1024
- `- **Ação:** ${operation.action}`,
1025
- `- **Fase:** ${operation.phase}`,
1026
- `- **Mutação:** ${operation.mutate ? 'sim' : 'não'}`,
1027
- `- **Política:** ${operation.approval_policy}`,
1028
- `- **Checkpoint:** ${operation.checkpoint_id || '—'}`,
1029
- `- **Resumo:** ${operation.summary || '—'}`,
1030
- `- **Criado em:** ${operation.created_at}`,
1031
- `- **Atualizado em:** ${operation.updated_at}`,
1032
- '',
1033
- '## Comando',
1034
- '',
1035
- '```bash',
1036
- operation.command_display_redacted || operation.command_display || '',
1037
- '```',
1038
- '',
1039
- '## Recursos alvo',
1040
- '',
1041
- ...(operation.resource_refs || []).map((ref) => `- ${JSON.stringify(ref)}`),
1042
- '',
1043
- '## Evidência',
1044
- '',
1045
- ...(operation.evidence_outputs || []).map((item) => `- ${item}`),
1046
- '',
1047
- ].join('\n');
1048
- }
1049
-
1050
- function persistAzureOperation(projectRoot, operation) {
1051
- const p = ensureAzureArtifacts(projectRoot);
1052
- const jsonPath = path.join(p.operationsDir, `${operation.operation_id}.json`);
1053
- const mdPath = path.join(p.operationsDir, `${operation.operation_id}.md`);
1054
- writeJson(jsonPath, redactObject(operation));
1055
- writeText(mdPath, renderAzureOperationMarkdown(redactObject(operation)));
1056
- return { jsonPath, mdPath };
1057
- }
1058
-
1059
- function checkpointIndexPath(projectRoot, activeSession) {
1060
- return operational.operationalPaths(projectRoot, activeSession).checkpoints;
1061
- }
1062
-
1063
- function readCheckpointIndex(projectRoot, activeSession) {
1064
- return readTextIfExists(checkpointIndexPath(projectRoot, activeSession)) || '';
1065
- }
1066
-
1067
- function writeCheckpointIndex(projectRoot, activeSession, rows) {
1068
- const target = checkpointIndexPath(projectRoot, activeSession);
1069
- const lines = [
1070
- '# OXE — Checkpoints',
1071
- '',
1072
- '> Índice de checkpoints formais do ciclo atual. Usado para aprovações humanas e gates sensíveis.',
1073
- '',
1074
- '| ID | Tipo | Fase | Escopo | Estado | Política | Decisão | Override | Criado em | Resolvido em | Notas |',
1075
- '|----|------|------|--------|--------|----------|---------|----------|-----------|--------------|-------|',
1076
- ...rows.map((row) => `| ${row.id} | ${row.type} | ${row.phase} | ${row.scope} | ${row.status} | ${row.policy} | ${row.decision} | ${row.override} | ${row.created_at} | ${row.resolved_at} | ${row.notes} |`),
1077
- '',
1078
- ];
1079
- writeText(target, lines.join('\n'));
1080
- }
1081
-
1082
- function upsertCheckpoint(projectRoot, activeSession, checkpoint) {
1083
- const text = readCheckpointIndex(projectRoot, activeSession);
1084
- const rows = [];
1085
- if (text) {
1086
- for (const line of text.split(/\r?\n/)) {
1087
- const match = line.match(/^\|\s*(CP-[^|]+)\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|/i);
1088
- if (!match) continue;
1089
- rows.push({
1090
- id: match[1].trim(),
1091
- type: match[2].trim(),
1092
- phase: match[3].trim(),
1093
- scope: match[4].trim(),
1094
- status: match[5].trim(),
1095
- policy: match[6].trim(),
1096
- decision: match[7].trim(),
1097
- override: match[8].trim(),
1098
- created_at: match[9].trim(),
1099
- resolved_at: match[10].trim(),
1100
- notes: match[11].trim(),
1101
- });
1102
- }
1103
- }
1104
- const idx = rows.findIndex((row) => row.id === checkpoint.id);
1105
- if (idx === -1) rows.unshift(checkpoint);
1106
- else rows[idx] = checkpoint;
1107
- writeCheckpointIndex(projectRoot, activeSession, rows);
1108
- return checkpoint;
1109
- }
1110
-
1111
- function syncRunProviderContext(projectRoot, activeSession, input) {
1112
- const current = operational.readRunState(projectRoot, activeSession);
1113
- const now = new Date().toISOString();
1114
- const providerContext = {
1115
- ...(current && current.provider_context && typeof current.provider_context === 'object' ? current.provider_context : {}),
1116
- azure: {
1117
- ...(((current && current.provider_context) || {}).azure || {}),
1118
- ...(input.provider_context || {}),
1119
- },
1120
- };
1121
- const next = current
1122
- ? operational.writeRunState(projectRoot, activeSession, {
1123
- ...current,
1124
- updated_at: now,
1125
- status: input.status || current.status,
1126
- pending_checkpoints: input.pending_checkpoints || current.pending_checkpoints || [],
1127
- provider_context: providerContext,
1128
- evidence: input.evidence || current.evidence || [],
1129
- })
1130
- : operational.writeRunState(projectRoot, activeSession, {
1131
- run_id: input.run_id || operational.makeRunId(),
1132
- created_at: now,
1133
- updated_at: now,
1134
- status: input.status || 'planned',
1135
- current_wave: null,
1136
- cursor: { wave: null, task: null, mode: 'provider-azure' },
1137
- active_tasks: [],
1138
- pending_checkpoints: input.pending_checkpoints || [],
1139
- evidence: input.evidence || [],
1140
- provider_context: providerContext,
1141
- });
1142
- return next;
1143
- }
1144
-
1145
- function attachAzureContextToRun(projectRoot, activeSession, context, operation, status, pendingCheckpoints, evidence) {
1146
- const summary = summarizeInventory((loadAzureInventory(projectRoot) || {}).items || []);
1147
- return syncRunProviderContext(projectRoot, activeSession, {
1148
- status,
1149
- pending_checkpoints: pendingCheckpoints,
1150
- evidence,
1151
- provider_context: {
1152
- enabled: true,
1153
- cloud: context.profile && context.profile.cloud ? context.profile.cloud : 'AzureCloud',
1154
- tenant_id: context.profile && context.profile.tenant_id ? context.profile.tenant_id : null,
1155
- subscription_id: context.profile && context.profile.subscription_id ? context.profile.subscription_id : null,
1156
- auth_mode: context.profile && context.profile.auth_mode ? context.profile.auth_mode : 'unknown',
1157
- inventory_summary: summary,
1158
- last_sync: (loadAzureInventory(projectRoot) || {}).synced_at || null,
1159
- last_operation: operation
1160
- ? {
1161
- operation_id: operation.operation_id,
1162
- domain: operation.domain,
1163
- kind: operation.kind,
1164
- action: operation.action,
1165
- phase: operation.phase,
1166
- summary: operation.summary,
1167
- checkpoint_id: operation.checkpoint_id || null,
1168
- capability_id: `azure-${operation.domain === 'sql' ? 'sql-admin' : operation.domain}`,
1169
- resource_refs: operation.resource_refs || [],
1170
- }
1171
- : null,
1172
- },
1173
- });
1174
- }
1175
-
1176
- function planAzureOperation(projectRoot, activeSession, domain, input, options = {}) {
1177
- const context = getAzureContext(projectRoot, options);
1178
- if (!context.cli.installed) throw new Error('Azure CLI não instalada.');
1179
- if (!context.authStatus.login_active) throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
1180
- const operation = buildMutationPlan(domain, input);
1181
- operation.phase = 'planned';
1182
- operation.updated_at = new Date().toISOString();
1183
- const files = persistAzureOperation(projectRoot, operation);
1184
- attachAzureContextToRun(projectRoot, activeSession, context, operation, 'planned', [], []);
1185
- operational.appendEvent(projectRoot, activeSession, {
1186
- type: 'azure_operation_planned',
1187
- payload: {
1188
- domain,
1189
- operation_id: operation.operation_id,
1190
- checkpoint_id: operation.checkpoint_id,
1191
- summary: operation.summary,
1192
- files,
1193
- },
1194
- });
1195
- return { context, operation, files };
1196
- }
1197
-
1198
- function applyAzureOperation(projectRoot, activeSession, domain, input, options = {}) {
1199
- const context = getAzureContext(projectRoot, options);
1200
- if (!context.cli.installed) throw new Error('Azure CLI não instalada.');
1201
- if (!context.authStatus.login_active) throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
1202
- if (options.vpnRequired && !options.vpnConfirmed) {
1203
- throw new Error('Esta operação requer VPN conforme configuração do projeto. Execute com --vpn-confirmed para confirmar a conexão.');
1204
- }
1205
- const operation = buildMutationPlan(domain, input);
1206
- if (options.dryRun) {
1207
- return {
1208
- approved: false,
1209
- dryRun: true,
1210
- operation: { ...operation, phase: 'dry_run' },
1211
- commandPreview: `az ${operation.command_args.join(' ')}`,
1212
- message: '[dry-run] Validação OK. Nenhuma alteração foi feita.',
1213
- };
1214
- }
1215
- if (operation.blocked && !options.overridePolicy) {
1216
- persistAzureOperation(projectRoot, operation);
1217
- throw new Error(operation.blocked_reason || 'Operação Azure bloqueada por política.');
1218
- }
1219
- if (!options.approve) {
1220
- operation.phase = 'waiting_approval';
1221
- operation.updated_at = new Date().toISOString();
1222
- const files = persistAzureOperation(projectRoot, operation);
1223
- upsertCheckpoint(projectRoot, activeSession, {
1224
- id: operation.checkpoint_id,
1225
- type: 'approval',
1226
- phase: 'execution',
1227
- scope: `azure:${domain}:${operation.kind}`,
1228
- status: 'pending_approval',
1229
- policy: operation.approval_policy,
1230
- decision: '—',
1231
- override: '—',
1232
- created_at: operation.created_at.slice(0, 10),
1233
- resolved_at: '—',
1234
- notes: operation.summary,
1235
- });
1236
- attachAzureContextToRun(projectRoot, activeSession, context, operation, 'waiting_approval', [operation.checkpoint_id], []);
1237
- operational.appendEvent(projectRoot, activeSession, {
1238
- type: 'azure_checkpoint_opened',
1239
- payload: {
1240
- domain,
1241
- operation_id: operation.operation_id,
1242
- checkpoint_id: operation.checkpoint_id,
1243
- summary: operation.summary,
1244
- files,
1245
- },
1246
- });
1247
- return {
1248
- approved: false,
1249
- operation,
1250
- files,
1251
- checkpoint_id: operation.checkpoint_id,
1252
- message: 'Mutação Azure planejada. Reexecute com --approve para aplicar.',
1253
- };
1254
- }
1255
-
1256
- operation.phase = 'running';
1257
- operation.updated_at = new Date().toISOString();
1258
- upsertCheckpoint(projectRoot, activeSession, {
1259
- id: operation.checkpoint_id,
1260
- type: 'approval',
1261
- phase: 'execution',
1262
- scope: `azure:${domain}:${operation.kind}`,
1263
- status: 'approved',
1264
- policy: operation.approval_policy,
1265
- decision: 'approved',
1266
- override: options.overridePolicy ? 'explicit' : '—',
1267
- created_at: operation.created_at.slice(0, 10),
1268
- resolved_at: operation.updated_at.slice(0, 10),
1269
- notes: operation.summary,
1270
- });
1271
- attachAzureContextToRun(projectRoot, activeSession, context, operation, 'running', [], []);
1272
- const result = runAz([...operation.command_args, '--output', 'json'], {
1273
- cwd: projectRoot,
1274
- env: options.env || {},
1275
- runner: options.runner,
1276
- });
1277
- if (result.status !== 0) {
1278
- operation.phase = 'failed';
1279
- operation.updated_at = new Date().toISOString();
1280
- operation.error = (result.stderr || result.stdout || 'Falha ao aplicar operação Azure.').trim();
1281
- const files = persistAzureOperation(projectRoot, operation);
1282
- attachAzureContextToRun(projectRoot, activeSession, context, operation, 'failed', [], []);
1283
- operational.appendEvent(projectRoot, activeSession, {
1284
- type: 'azure_operation_failed',
1285
- payload: {
1286
- domain,
1287
- operation_id: operation.operation_id,
1288
- checkpoint_id: operation.checkpoint_id,
1289
- error: operation.error,
1290
- files,
1291
- },
1292
- });
1293
- throw new Error(operation.error);
1294
- }
1295
- operation.phase = 'applied';
1296
- operation.updated_at = new Date().toISOString();
1297
- operation.result = redactObject(parseJsonOutput(result.stdout, {}));
1298
- const files = persistAzureOperation(projectRoot, operation);
1299
- attachAzureContextToRun(projectRoot, activeSession, context, operation, 'completed', [], [files.mdPath]);
1300
- operational.appendEvent(projectRoot, activeSession, {
1301
- type: 'azure_operation_applied',
1302
- payload: {
1303
- domain,
1304
- operation_id: operation.operation_id,
1305
- checkpoint_id: operation.checkpoint_id,
1306
- files,
1307
- resources: operation.resource_refs,
1308
- },
1309
- });
1310
- return { approved: true, operation, files, result: operation.result };
1311
- }
1312
-
1313
- function executeAzureRead(projectRoot, activeSession, domain, verb, input, options = {}) {
1314
- const context = getAzureContext(projectRoot, options);
1315
- if (!context.cli.installed) throw new Error('Azure CLI não instalada.');
1316
- if (!context.authStatus.login_active) throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
1317
- const args = buildReadCommand(domain, verb, input);
1318
- const result = runAz(args, { cwd: projectRoot, ...options });
1319
- if (result.status !== 0) {
1320
- throw new Error((result.stderr || result.stdout || 'Falha ao consultar recurso Azure.').trim());
1321
- }
1322
- const parsed = parseJsonOutput(result.stdout, []);
1323
- attachAzureContextToRun(projectRoot, activeSession, context, null, 'completed', [], []);
1324
- operational.appendEvent(projectRoot, activeSession, {
1325
- type: 'azure_resource_found',
1326
- payload: {
1327
- domain,
1328
- verb,
1329
- kind: input.kind || null,
1330
- query: redactObject(input),
1331
- },
1332
- });
1333
- return parsed;
1334
- }
1335
-
1336
- function loginAzure(projectRoot, options = {}) {
1337
- const cli = detectAzureCli(projectRoot, options);
1338
- if (!cli.installed) throw new Error('Azure CLI não instalada.');
1339
- operational.appendEvent(projectRoot, null, { type: 'azure_login_started' });
1340
- const azArgs = ['login', '--output', 'json'];
1341
- if (options.tenant) azArgs.push('--tenant', String(options.tenant));
1342
- const login = runAz(azArgs, { cwd: projectRoot, inherit: Boolean(options.inherit), ...options });
1343
- if (login.status !== 0) {
1344
- throw new Error((login.stderr || login.stdout || 'Falha no az login.').trim());
1345
- }
1346
- operational.appendEvent(projectRoot, null, { type: 'azure_login_succeeded' });
1347
- // getAzureContext must capture output — strip inherit so it pipes stdout
1348
- const { inherit: _inherit, ...getCtxOptions } = options;
1349
- return getAzureContext(projectRoot, getCtxOptions);
1350
- }
1351
-
1352
- function setAzureSubscription(projectRoot, subscription, options = {}) {
1353
- if (!subscription) throw new Error('Informe a subscription.');
1354
- const cli = detectAzureCli(projectRoot, options);
1355
- if (!cli.installed) throw new Error('Azure CLI não instalada.');
1356
- const set = runAz(['account', 'set', '--subscription', String(subscription)], { cwd: projectRoot, ...options });
1357
- if (set.status !== 0) {
1358
- throw new Error((set.stderr || set.stdout || 'Falha ao selecionar subscription.').trim());
1359
- }
1360
- const context = getAzureContext(projectRoot, options);
1361
- operational.appendEvent(projectRoot, null, {
1362
- type: 'azure_subscription_selected',
1363
- payload: {
1364
- subscription: context.profile.subscription_id,
1365
- subscription_name: context.profile.subscription_name,
1366
- },
1367
- });
1368
- return context;
1369
- }
1370
-
1371
- function azureDoctor(projectRoot, config = {}, options = {}) {
1372
- const p = options.write === false ? azurePaths(projectRoot) : ensureAzureArtifacts(projectRoot);
1373
- const context = getAzureContext(projectRoot, options);
1374
- const inventory = loadAzureInventory(projectRoot);
1375
- const capsRoot = path.join(projectRoot, '.oxe', 'capabilities');
1376
- const missingCapabilities = AZURE_CAPABILITY_IDS.filter((id) => !fs.existsSync(path.join(capsRoot, id, 'CAPABILITY.md')));
1377
- const warnings = [];
1378
- if (!context.cli.installed) warnings.push('Azure CLI não instalada.');
1379
- else if (!context.cli.okVersion) warnings.push(`Azure CLI ${context.cli.version || 'desconhecida'} abaixo do mínimo suportado.`);
1380
- if (context.cli.installed && !context.authStatus.login_active) warnings.push('Sessão Azure ausente.');
1381
- if (context.authStatus.login_active && !context.profile.subscription_id) warnings.push('Subscription Azure não selecionada.');
1382
- if (!context.authStatus.resource_graph_enabled) warnings.push('Extensão resource-graph ausente ou não habilitada.');
1383
- if (!inventory) warnings.push('Inventário Azure ausente. Execute "oxe-cc azure sync".');
1384
- if (inventory && config && typeof config.azure === 'object') {
1385
- const maxAgeHours = Number(config.azure.inventory_max_age_hours || 24);
1386
- const syncedAt = Date.parse(String(inventory.synced_at || ''));
1387
- if (!Number.isNaN(syncedAt) && maxAgeHours > 0) {
1388
- const ageHours = Math.floor((Date.now() - syncedAt) / (1000 * 60 * 60));
1389
- if (ageHours > maxAgeHours) warnings.push(`Inventário Azure stale (${ageHours}h > ${maxAgeHours}h).`);
1390
- }
1391
- }
1392
- if (missingCapabilities.length) warnings.push(`Capabilities Azure ausentes: ${missingCapabilities.join(', ')}`);
1393
- const operations = listAzureOperations(projectRoot);
1394
- const pendingOperation = operations.find((operation) => operation.phase === 'waiting_approval');
1395
- if (pendingOperation) warnings.push(`Operação Azure pendente sem apply final: ${pendingOperation.operation_id}`);
1396
- const authStatus = {
1397
- ...(context.authStatus || {}),
1398
- checked_at: new Date().toISOString(),
1399
- warnings,
1400
- };
1401
- if (options.write !== false) {
1402
- writeAzureAuthArtifacts(projectRoot, { profile: context.profile || DEFAULT_AZURE_PROFILE, authStatus });
1403
- }
1404
- return {
1405
- healthy: warnings.length === 0,
1406
- warnings,
1407
- profile: context.profile,
1408
- authStatus,
1409
- inventory,
1410
- inventorySummary: inventory && inventory.summary ? inventory.summary : summarizeInventory([]),
1411
- paths: p,
1412
- };
1413
- }
1414
-
1415
- module.exports = {
1416
- MIN_AZURE_CLI_MAJOR,
1417
- AZURE_CAPABILITY_IDS,
1418
- RESOURCE_GRAPH_QUERY,
1419
- DEFAULT_AZURE_PROFILE,
1420
- azurePaths,
1421
- ensureAzureArtifacts,
1422
- isAzureContextEnabled,
1423
- detectAzureCli,
1424
- loadAzureProfile,
1425
- loadAzureAuthStatus,
1426
- loadAzureInventory,
1427
- listAzureOperations,
1428
- summarizeInventory,
1429
- normalizeInventoryItem,
1430
- searchAzureInventory,
1431
- diffInventory,
1432
- statusAzure,
1433
- ensureAzureCapabilities,
1434
- getAzureContext,
1435
- loginAzure,
1436
- setAzureSubscription,
1437
- ensureResourceGraphExtension,
1438
- syncAzureInventory,
1439
- executeAzureRead,
1440
- planAzureOperation,
1441
- applyAzureOperation,
1442
- azureDoctor,
1443
- redactObject,
1444
- runAz,
1445
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const operational = require('./oxe-operational.cjs');
7
+
8
+ const MIN_AZURE_CLI_MAJOR = 2;
9
+ const AZURE_CAPABILITY_IDS = [
10
+ 'azure-auth',
11
+ 'azure-resource-graph',
12
+ 'azure-servicebus',
13
+ 'azure-eventgrid',
14
+ 'azure-sql-admin',
15
+ ];
16
+
17
+ const DEFAULT_AZURE_PROFILE = {
18
+ cloud: 'AzureCloud',
19
+ tenant_id: null,
20
+ subscription_id: null,
21
+ subscription_name: null,
22
+ auth_mode: 'unknown',
23
+ default_resource_group: '',
24
+ preferred_locations: [],
25
+ last_auth_check: null,
26
+ resource_graph_enabled: false,
27
+ };
28
+
29
+ const RESOURCE_GRAPH_QUERY = [
30
+ 'Resources',
31
+ '| project',
32
+ 'id,',
33
+ 'name,',
34
+ 'type,',
35
+ 'resourceGroup,',
36
+ 'location,',
37
+ 'subscriptionId,',
38
+ 'tags,',
39
+ 'sku=tostring(sku.name)',
40
+ ].join(' ');
41
+
42
+ function ensureDir(dirPath) {
43
+ fs.mkdirSync(dirPath, { recursive: true });
44
+ }
45
+
46
+ function ensureDirForFile(filePath) {
47
+ ensureDir(path.dirname(filePath));
48
+ }
49
+
50
+ function readTextIfExists(filePath) {
51
+ try {
52
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function readJsonIfExists(filePath, fallback = null) {
59
+ const raw = readTextIfExists(filePath);
60
+ if (!raw) return fallback;
61
+ try {
62
+ return JSON.parse(raw);
63
+ } catch {
64
+ return fallback;
65
+ }
66
+ }
67
+
68
+ function writeJson(filePath, value) {
69
+ ensureDirForFile(filePath);
70
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
71
+ }
72
+
73
+ function writeText(filePath, value) {
74
+ ensureDirForFile(filePath);
75
+ fs.writeFileSync(filePath, value, 'utf8');
76
+ }
77
+
78
+ function azurePaths(projectRoot) {
79
+ const root = path.join(projectRoot, '.oxe', 'cloud', 'azure');
80
+ return {
81
+ root,
82
+ profile: path.join(root, 'profile.json'),
83
+ authStatus: path.join(root, 'auth-status.json'),
84
+ inventory: path.join(root, 'inventory.json'),
85
+ inventoryMd: path.join(root, 'INVENTORY.md'),
86
+ serviceBusMd: path.join(root, 'SERVICEBUS.md'),
87
+ eventGridMd: path.join(root, 'EVENTGRID.md'),
88
+ sqlMd: path.join(root, 'SQL.md'),
89
+ operationsDir: path.join(root, 'operations'),
90
+ };
91
+ }
92
+
93
+ function ensureAzureArtifacts(projectRoot) {
94
+ const p = azurePaths(projectRoot);
95
+ ensureDir(p.root);
96
+ ensureDir(p.operationsDir);
97
+ return p;
98
+ }
99
+
100
+ function isAzureContextEnabled(projectRoot, config = {}) {
101
+ const p = azurePaths(projectRoot);
102
+ const capsRoot = path.join(projectRoot, '.oxe', 'capabilities');
103
+ const azureConfig = config && typeof config.azure === 'object' ? config.azure : null;
104
+ return Boolean(
105
+ (azureConfig && azureConfig.enabled) ||
106
+ fs.existsSync(p.root) ||
107
+ fs.existsSync(p.profile) ||
108
+ fs.existsSync(p.inventory) ||
109
+ AZURE_CAPABILITY_IDS.some((id) => fs.existsSync(path.join(capsRoot, id, 'CAPABILITY.md')))
110
+ );
111
+ }
112
+
113
+ function redactString(value, fallback = '[redacted]') {
114
+ if (value == null || value === '') return null;
115
+ return fallback;
116
+ }
117
+
118
+ function redactObject(value) {
119
+ if (Array.isArray(value)) return value.map((item) => redactObject(item));
120
+ if (!value || typeof value !== 'object') return value;
121
+ const out = {};
122
+ for (const [key, item] of Object.entries(value)) {
123
+ if (/(token|secret|password|connection.?string|access.?key|primary.?key|secondary.?key)/i.test(key)) {
124
+ out[key] = redactString(item);
125
+ } else {
126
+ out[key] = redactObject(item);
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+
132
+ function runAz(args, options = {}) {
133
+ if (typeof options.runner === 'function') {
134
+ return options.runner(args, options);
135
+ }
136
+ const spawnOptions = {
137
+ cwd: options.cwd || process.cwd(),
138
+ env: { ...process.env, ...(options.env || {}) },
139
+ encoding: 'utf8',
140
+ shell: false,
141
+ timeout: options.timeoutMs || 30000,
142
+ stdio: options.inherit ? 'inherit' : 'pipe',
143
+ };
144
+ let result;
145
+ if (process.platform === 'win32') {
146
+ const quoteForCmd = (value) => {
147
+ const raw = String(value == null ? '' : value);
148
+ if (!raw.length) return '""';
149
+ if (!/[ \t"&|<>^]/.test(raw)) return raw;
150
+ return `"${raw.replace(/"/g, '""')}"`;
151
+ };
152
+ const commandLine = ['az', ...args].map((item) => quoteForCmd(item)).join(' ');
153
+ result = spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', commandLine], spawnOptions);
154
+ } else {
155
+ result = spawnSync('az', args, spawnOptions);
156
+ }
157
+ return {
158
+ status: typeof result.status === 'number' ? result.status : 1,
159
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
160
+ stderr: typeof result.stderr === 'string' ? result.stderr : '',
161
+ error: result.error || null,
162
+ };
163
+ }
164
+
165
+ function parseJsonOutput(raw, fallback = null) {
166
+ try {
167
+ return JSON.parse(String(raw || '').trim() || 'null');
168
+ } catch {
169
+ return fallback;
170
+ }
171
+ }
172
+
173
+ function detectAzureCli(projectRoot, options = {}) {
174
+ const result = runAz(['version', '--output', 'json'], { cwd: projectRoot, ...options });
175
+ if (result.error) {
176
+ return {
177
+ installed: false,
178
+ version: null,
179
+ major: null,
180
+ okVersion: false,
181
+ message: result.error.message || 'Azure CLI não encontrada.',
182
+ raw: null,
183
+ };
184
+ }
185
+ if (result.status !== 0) {
186
+ return {
187
+ installed: false,
188
+ version: null,
189
+ major: null,
190
+ okVersion: false,
191
+ message: (result.stderr || result.stdout || 'Azure CLI não encontrada.').trim(),
192
+ raw: null,
193
+ };
194
+ }
195
+ const parsed = parseJsonOutput(result.stdout, {});
196
+ const version = String((parsed && parsed['azure-cli']) || '').trim() || null;
197
+ const major = version ? parseInt(version.split('.')[0], 10) : null;
198
+ return {
199
+ installed: true,
200
+ version,
201
+ major,
202
+ okVersion: Number.isInteger(major) && major >= MIN_AZURE_CLI_MAJOR,
203
+ message: null,
204
+ raw: parsed,
205
+ };
206
+ }
207
+
208
+ function normalizeAuthMode(account) {
209
+ const userType = String(account && account.user && account.user.type || '').toLowerCase();
210
+ if (userType === 'serviceprincipal') return 'service_principal';
211
+ if (userType === 'user') return 'user_mfa';
212
+ if (userType === 'managedidentity') return 'managed_identity';
213
+ return 'unknown';
214
+ }
215
+
216
+ function normalizeAzureProfile(account, cloud, existingProfile = {}) {
217
+ const profile = {
218
+ ...DEFAULT_AZURE_PROFILE,
219
+ ...(existingProfile && typeof existingProfile === 'object' ? existingProfile : {}),
220
+ };
221
+ if (!account || typeof account !== 'object') return profile;
222
+ return {
223
+ ...profile,
224
+ cloud: String((cloud && cloud.name) || profile.cloud || 'AzureCloud'),
225
+ tenant_id: account.tenantId || profile.tenant_id || null,
226
+ subscription_id: account.id || profile.subscription_id || null,
227
+ subscription_name: account.name || profile.subscription_name || null,
228
+ auth_mode: normalizeAuthMode(account),
229
+ last_auth_check: new Date().toISOString(),
230
+ };
231
+ }
232
+
233
+ function loadAzureProfile(projectRoot) {
234
+ return {
235
+ ...DEFAULT_AZURE_PROFILE,
236
+ ...(readJsonIfExists(azurePaths(projectRoot).profile, {}) || {}),
237
+ };
238
+ }
239
+
240
+ function loadAzureAuthStatus(projectRoot) {
241
+ return readJsonIfExists(azurePaths(projectRoot).authStatus, null);
242
+ }
243
+
244
+ function loadAzureInventory(projectRoot) {
245
+ return readJsonIfExists(azurePaths(projectRoot).inventory, null);
246
+ }
247
+
248
+ function deriveServiceFamily(type) {
249
+ const value = String(type || '').toLowerCase();
250
+ if (value.startsWith('microsoft.servicebus/')) return 'servicebus';
251
+ if (value.startsWith('microsoft.eventgrid/')) return 'eventgrid';
252
+ if (value.startsWith('microsoft.sql/')) return 'sql';
253
+ return 'other';
254
+ }
255
+
256
+ function summarizeInventory(items) {
257
+ const summary = {
258
+ total: 0,
259
+ servicebus: 0,
260
+ eventgrid: 0,
261
+ sql: 0,
262
+ other: 0,
263
+ };
264
+ for (const item of items || []) {
265
+ const family = deriveServiceFamily(item.type || item.service_family);
266
+ summary.total += 1;
267
+ summary[family] = (summary[family] || 0) + 1;
268
+ }
269
+ return summary;
270
+ }
271
+
272
+ function normalizeInventoryItem(item = {}) {
273
+ const normalized = {
274
+ id: item.id || '',
275
+ name: item.name || '',
276
+ type: item.type || '',
277
+ resourceGroup: item.resourceGroup || item.resource_group || '',
278
+ location: item.location || '',
279
+ subscriptionId: item.subscriptionId || item.subscription_id || '',
280
+ tags: item.tags && typeof item.tags === 'object' ? item.tags : {},
281
+ sku: item.sku || '',
282
+ };
283
+ normalized.service_family = deriveServiceFamily(normalized.type);
284
+ return normalized;
285
+ }
286
+
287
+ function renderInventoryMarkdown(title, profile, authStatus, items, syncedAt) {
288
+ const summary = summarizeInventory(items);
289
+ const lines = [
290
+ `# OXE — ${title}`,
291
+ '',
292
+ '> Inventário Azure materializado pelo provider nativo do OXE via Azure CLI.',
293
+ '',
294
+ `- **Cloud:** ${profile.cloud || '—'}`,
295
+ `- **Tenant:** ${profile.tenant_id || '—'}`,
296
+ `- **Subscription:** ${profile.subscription_name || profile.subscription_id || '—'}`,
297
+ `- **Auth mode:** ${profile.auth_mode || 'unknown'}`,
298
+ `- **Último check de auth:** ${profile.last_auth_check || authStatus && authStatus.checked_at || '—'}`,
299
+ `- **Último sync:** ${syncedAt || '—'}`,
300
+ '',
301
+ '## Resumo',
302
+ '',
303
+ `- **Total:** ${summary.total}`,
304
+ `- **Service Bus:** ${summary.servicebus}`,
305
+ `- **Event Grid:** ${summary.eventgrid}`,
306
+ `- **Azure SQL:** ${summary.sql}`,
307
+ `- **Outros:** ${summary.other}`,
308
+ '',
309
+ '| Nome | Tipo | Família | Resource Group | Location | SKU |',
310
+ '|------|------|---------|----------------|----------|-----|',
311
+ ];
312
+ if (!items.length) {
313
+ lines.push('| (vazio) | — | — | — | — | — |');
314
+ } else {
315
+ for (const item of items) {
316
+ lines.push(
317
+ `| ${item.name || '—'} | ${item.type || '—'} | ${item.service_family || 'other'} | ${item.resourceGroup || '—'} | ${item.location || '—'} | ${item.sku || '—'} |`
318
+ );
319
+ }
320
+ }
321
+ lines.push('');
322
+ return lines.join('\n');
323
+ }
324
+
325
+ function listAzureOperations(projectRoot) {
326
+ const p = azurePaths(projectRoot);
327
+ if (!fs.existsSync(p.operationsDir)) return [];
328
+ return fs
329
+ .readdirSync(p.operationsDir)
330
+ .filter((name) => name.endsWith('.json'))
331
+ .map((name) => readJsonIfExists(path.join(p.operationsDir, name), null))
332
+ .filter(Boolean)
333
+ .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
334
+ }
335
+
336
+ function writeAzureAuthArtifacts(projectRoot, payload) {
337
+ const p = ensureAzureArtifacts(projectRoot);
338
+ if (payload.profile) writeJson(p.profile, payload.profile);
339
+ if (payload.authStatus) writeJson(p.authStatus, redactObject(payload.authStatus));
340
+ return p;
341
+ }
342
+
343
+ function getAzureContext(projectRoot, options = {}) {
344
+ const cli = detectAzureCli(projectRoot, options);
345
+ const existingProfile = loadAzureProfile(projectRoot);
346
+ if (!cli.installed) {
347
+ const authStatus = {
348
+ checked_at: new Date().toISOString(),
349
+ installed: false,
350
+ version: null,
351
+ login_active: false,
352
+ subscription_selected: false,
353
+ tenant_id: null,
354
+ subscription_id: existingProfile.subscription_id || null,
355
+ subscription_name: existingProfile.subscription_name || null,
356
+ cloud: existingProfile.cloud || 'AzureCloud',
357
+ auth_mode: existingProfile.auth_mode || 'unknown',
358
+ user: null,
359
+ user_type: null,
360
+ resource_graph_enabled: false,
361
+ warnings: [cli.message || 'Azure CLI não instalada.'],
362
+ };
363
+ if (options.write !== false) {
364
+ writeAzureAuthArtifacts(projectRoot, { profile: existingProfile, authStatus });
365
+ }
366
+ return { cli, profile: existingProfile, authStatus, account: null, cloud: null, extension: null };
367
+ }
368
+
369
+ const accountResult = runAz(['account', 'show', '--output', 'json'], { cwd: projectRoot, ...options });
370
+ if (accountResult.status !== 0) {
371
+ const authStatus = {
372
+ checked_at: new Date().toISOString(),
373
+ installed: true,
374
+ version: cli.version,
375
+ login_active: false,
376
+ subscription_selected: Boolean(existingProfile.subscription_id),
377
+ tenant_id: existingProfile.tenant_id || null,
378
+ subscription_id: existingProfile.subscription_id || null,
379
+ subscription_name: existingProfile.subscription_name || null,
380
+ cloud: existingProfile.cloud || 'AzureCloud',
381
+ auth_mode: existingProfile.auth_mode || 'unknown',
382
+ user: null,
383
+ user_type: null,
384
+ resource_graph_enabled: false,
385
+ warnings: ['Azure CLI instalada, mas sem sessão ativa. Execute "oxe-cc azure auth login".'],
386
+ };
387
+ if (options.write !== false) {
388
+ writeAzureAuthArtifacts(projectRoot, { profile: existingProfile, authStatus });
389
+ }
390
+ return { cli, profile: existingProfile, authStatus, account: null, cloud: null, extension: null };
391
+ }
392
+
393
+ const account = parseJsonOutput(accountResult.stdout, {});
394
+ const cloud = parseJsonOutput(runAz(['cloud', 'show', '--output', 'json'], { cwd: projectRoot, ...options }).stdout, {});
395
+ const extension = parseJsonOutput(
396
+ runAz(['extension', 'show', '--name', 'resource-graph', '--output', 'json'], { cwd: projectRoot, ...options }).stdout,
397
+ null
398
+ );
399
+ const profile = normalizeAzureProfile(account, cloud, existingProfile);
400
+ profile.resource_graph_enabled = Boolean(extension);
401
+ const authStatus = {
402
+ checked_at: new Date().toISOString(),
403
+ installed: true,
404
+ version: cli.version,
405
+ login_active: true,
406
+ subscription_selected: Boolean(account.id),
407
+ tenant_id: account.tenantId || null,
408
+ subscription_id: account.id || null,
409
+ subscription_name: account.name || null,
410
+ cloud: (cloud && cloud.name) || profile.cloud || 'AzureCloud',
411
+ auth_mode: normalizeAuthMode(account),
412
+ user: account.user && account.user.name ? account.user.name : null,
413
+ user_type: account.user && account.user.type ? account.user.type : null,
414
+ resource_graph_enabled: Boolean(extension),
415
+ warnings: [],
416
+ };
417
+ if (options.write !== false) {
418
+ writeAzureAuthArtifacts(projectRoot, { profile, authStatus });
419
+ }
420
+ return { cli, profile, authStatus, account, cloud, extension };
421
+ }
422
+
423
+ function ensureResourceGraphExtension(projectRoot, options = {}) {
424
+ const show = runAz(['extension', 'show', '--name', 'resource-graph', '--output', 'json'], { cwd: projectRoot, ...options });
425
+ if (show.status === 0) {
426
+ return {
427
+ ok: true,
428
+ installed: true,
429
+ changed: false,
430
+ extension: parseJsonOutput(show.stdout, {}),
431
+ };
432
+ }
433
+ if (options.autoInstall === false) {
434
+ return {
435
+ ok: false,
436
+ installed: false,
437
+ changed: false,
438
+ extension: null,
439
+ message: 'Extensão resource-graph ausente.',
440
+ };
441
+ }
442
+ const add = runAz(['extension', 'add', '--name', 'resource-graph', '--upgrade', '--only-show-errors'], {
443
+ cwd: projectRoot,
444
+ ...options,
445
+ });
446
+ if (add.status !== 0) {
447
+ return {
448
+ ok: false,
449
+ installed: false,
450
+ changed: false,
451
+ extension: null,
452
+ message: (add.stderr || add.stdout || 'Falha ao instalar resource-graph.').trim(),
453
+ };
454
+ }
455
+ const installed = parseJsonOutput(
456
+ runAz(['extension', 'show', '--name', 'resource-graph', '--output', 'json'], { cwd: projectRoot, ...options }).stdout,
457
+ {}
458
+ );
459
+ return {
460
+ ok: true,
461
+ installed: true,
462
+ changed: true,
463
+ extension: installed,
464
+ };
465
+ }
466
+
467
+ function syncAzureInventory(projectRoot, options = {}) {
468
+ const p = ensureAzureArtifacts(projectRoot);
469
+ const previousInventory = options.diff ? loadAzureInventory(projectRoot) : null;
470
+ const context = getAzureContext(projectRoot, options);
471
+ if (!context.cli.installed) {
472
+ throw new Error('Azure CLI não instalada.');
473
+ }
474
+ if (!context.authStatus.login_active) {
475
+ throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
476
+ }
477
+ const extension = ensureResourceGraphExtension(projectRoot, options);
478
+ if (!extension.ok) {
479
+ throw new Error(extension.message || 'Extensão resource-graph ausente.');
480
+ }
481
+ const args = ['graph', 'query', '-q', RESOURCE_GRAPH_QUERY, '--first', '1000', '--output', 'json'];
482
+ if (context.profile.subscription_id) {
483
+ args.push('--subscriptions', context.profile.subscription_id);
484
+ }
485
+ const result = runAz(args, { cwd: projectRoot, ...options });
486
+ if (result.status !== 0) {
487
+ throw new Error((result.stderr || result.stdout || 'Falha ao executar az graph query.').trim());
488
+ }
489
+ const parsed = parseJsonOutput(result.stdout, {});
490
+ const items = Array.isArray(parsed.data) ? parsed.data.map(normalizeInventoryItem) : [];
491
+ items.sort((a, b) => String(a.service_family).localeCompare(String(b.service_family)) || String(a.name).localeCompare(String(b.name)));
492
+ const syncedAt = new Date().toISOString();
493
+ const inventory = {
494
+ oxeAzureInventorySchema: 1,
495
+ synced_at: syncedAt,
496
+ query: RESOURCE_GRAPH_QUERY,
497
+ cloud: context.profile.cloud,
498
+ tenant_id: context.profile.tenant_id,
499
+ subscription_id: context.profile.subscription_id,
500
+ subscription_name: context.profile.subscription_name,
501
+ summary: summarizeInventory(items),
502
+ items,
503
+ };
504
+ writeJson(p.inventory, inventory);
505
+ writeText(p.inventoryMd, renderInventoryMarkdown('Azure Inventory', context.profile, context.authStatus, items, syncedAt));
506
+ writeText(
507
+ p.serviceBusMd,
508
+ renderInventoryMarkdown(
509
+ 'Service Bus',
510
+ context.profile,
511
+ context.authStatus,
512
+ items.filter((item) => item.service_family === 'servicebus'),
513
+ syncedAt
514
+ )
515
+ );
516
+ writeText(
517
+ p.eventGridMd,
518
+ renderInventoryMarkdown(
519
+ 'Event Grid',
520
+ context.profile,
521
+ context.authStatus,
522
+ items.filter((item) => item.service_family === 'eventgrid'),
523
+ syncedAt
524
+ )
525
+ );
526
+ writeText(
527
+ p.sqlMd,
528
+ renderInventoryMarkdown(
529
+ 'Azure SQL',
530
+ context.profile,
531
+ context.authStatus,
532
+ items.filter((item) => item.service_family === 'sql'),
533
+ syncedAt
534
+ )
535
+ );
536
+ const nextProfile = {
537
+ ...context.profile,
538
+ resource_graph_enabled: true,
539
+ last_auth_check: context.authStatus.checked_at,
540
+ };
541
+ writeAzureAuthArtifacts(projectRoot, {
542
+ profile: nextProfile,
543
+ authStatus: {
544
+ ...context.authStatus,
545
+ checked_at: syncedAt,
546
+ resource_graph_enabled: true,
547
+ last_sync: syncedAt,
548
+ },
549
+ });
550
+ const syncResult = { paths: p, profile: nextProfile, authStatus: context.authStatus, inventory };
551
+ if (options.diff && previousInventory) {
552
+ syncResult.diff = diffInventory(previousInventory.items || [], items);
553
+ }
554
+ return syncResult;
555
+ }
556
+
557
+ function searchAzureInventory(projectRoot, query, filters = {}) {
558
+ const inventory = loadAzureInventory(projectRoot);
559
+ if (!inventory || !Array.isArray(inventory.items)) return [];
560
+ let items = inventory.items;
561
+ if (filters.type) {
562
+ const ft = String(filters.type).toLowerCase();
563
+ items = items.filter((item) =>
564
+ String(item.type || '').toLowerCase().includes(ft) ||
565
+ String(item.service_family || '').toLowerCase().includes(ft)
566
+ );
567
+ }
568
+ if (filters.resourceGroup) {
569
+ const frg = String(filters.resourceGroup).toLowerCase();
570
+ items = items.filter((item) => String(item.resourceGroup || '').toLowerCase() === frg);
571
+ }
572
+ const q = String(query || '').trim().toLowerCase();
573
+ if (!q) return items;
574
+ return items.filter((item) => {
575
+ const haystack = [
576
+ item.name,
577
+ item.type,
578
+ item.resourceGroup,
579
+ item.location,
580
+ item.subscriptionId,
581
+ item.service_family,
582
+ ...Object.keys(item.tags || {}),
583
+ ...Object.values(item.tags || {}),
584
+ ]
585
+ .join(' ')
586
+ .toLowerCase();
587
+ return haystack.includes(q);
588
+ });
589
+ }
590
+
591
+ function diffInventory(previousItems, currentItems) {
592
+ const prevMap = new Map((previousItems || []).map((item) => [item.id, item]));
593
+ const currMap = new Map((currentItems || []).map((item) => [item.id, item]));
594
+ const added = (currentItems || []).filter((item) => !prevMap.has(item.id));
595
+ const removed = (previousItems || []).filter((item) => !currMap.has(item.id));
596
+ return { added, removed, unchanged: (currentItems || []).length - added.length };
597
+ }
598
+
599
+ function statusAzure(projectRoot, config = {}, options = {}) {
600
+ const context = getAzureContext(projectRoot, { ...options, write: false });
601
+ const inventory = loadAzureInventory(projectRoot);
602
+ const azureCfg = config && typeof config.azure === 'object' ? config.azure : {};
603
+ const maxAgeHours = azureCfg.inventory_max_age_hours != null ? Number(azureCfg.inventory_max_age_hours) : 24;
604
+ const syncedAtMs = inventory ? Date.parse(String(inventory.synced_at || '')) : null;
605
+ const ageHours = syncedAtMs && !Number.isNaN(syncedAtMs) ? Math.floor((Date.now() - syncedAtMs) / (1000 * 60 * 60)) : null;
606
+ const stale = ageHours !== null && maxAgeHours > 0 && ageHours > maxAgeHours;
607
+ const pendingOps = listAzureOperations(projectRoot).filter((op) => op.phase === 'waiting_approval');
608
+ return {
609
+ cliInstalled: context.cli.installed,
610
+ cliVersion: context.cli.version || null,
611
+ loginActive: context.authStatus.login_active,
612
+ subscription: context.profile.subscription_name || context.profile.subscription_id || null,
613
+ cloud: context.profile.cloud || 'AzureCloud',
614
+ resourceGraphEnabled: Boolean(context.authStatus.resource_graph_enabled),
615
+ inventoryPresent: Boolean(inventory),
616
+ inventoryStale: stale,
617
+ inventoryAgeHours: ageHours,
618
+ inventorySummary: inventory && inventory.summary ? inventory.summary : null,
619
+ pendingOperations: pendingOps.length,
620
+ pendingOperationIds: pendingOps.map((op) => op.operation_id),
621
+ vpnRequired: Boolean(azureCfg.vpn_required),
622
+ };
623
+ }
624
+
625
+ function renderCapabilityManifest(manifest) {
626
+ return [
627
+ '---',
628
+ 'oxe_capability: true',
629
+ `id: ${manifest.id}`,
630
+ 'version: 1',
631
+ 'type: script',
632
+ 'status: active',
633
+ `scope: ${manifest.scope}`,
634
+ `entrypoint: "${manifest.entrypoint}"`,
635
+ `approval_policy: ${manifest.approval_policy}`,
636
+ `side_effects: [${manifest.side_effects.map((item) => item).join(', ')}]`,
637
+ `requires_env: [${manifest.requires_env.map((item) => item).join(', ')}]`,
638
+ `evidence_outputs: [${manifest.evidence_outputs.map((item) => item).join(', ')}]`,
639
+ 'session_compatibility: [legacy, session]',
640
+ '---',
641
+ '',
642
+ `# OXE — Capability ${manifest.id}`,
643
+ '',
644
+ '## Objetivo',
645
+ '',
646
+ `- ${manifest.summary}`,
647
+ '',
648
+ '## Escopo OXE',
649
+ '',
650
+ `- ${manifest.scope}`,
651
+ '',
652
+ '## Operações',
653
+ '',
654
+ ...manifest.operations.map((op) => `- ${op}`),
655
+ '',
656
+ '## Entradas e saídas',
657
+ '',
658
+ '- Entradas resolvidas pelo provider Azure e pelo runtime do OXE.',
659
+ '- Saídas persistidas em `.oxe/cloud/azure/operations/` e no trace operacional.',
660
+ '',
661
+ '## Requisitos',
662
+ '',
663
+ '- Azure CLI instalada localmente.',
664
+ '- Sessão Azure válida ou contexto autenticado compatível.',
665
+ '',
666
+ '## Evidência e segurança',
667
+ '',
668
+ '- Toda mutação gera plano, checkpoint e evidência redacted.',
669
+ '- Segredos não são persistidos em `.oxe/`.',
670
+ '',
671
+ ].join('\n');
672
+ }
673
+
674
+ function ensureAzureCapabilities(projectRoot) {
675
+ const capsDir = path.join(projectRoot, '.oxe', 'capabilities');
676
+ ensureDir(capsDir);
677
+ const manifests = [
678
+ {
679
+ id: 'azure-auth',
680
+ scope: 'ask',
681
+ entrypoint: 'oxe-cc azure auth <login|whoami|set-subscription>',
682
+ approval_policy: 'always_allow',
683
+ side_effects: [],
684
+ requires_env: [],
685
+ evidence_outputs: ['.oxe/cloud/azure/auth-status.json', '.oxe/cloud/azure/profile.json'],
686
+ summary: 'Autenticação e contexto Azure corporativo via Azure CLI.',
687
+ operations: ['auth login', 'auth whoami', 'auth set-subscription'],
688
+ },
689
+ {
690
+ id: 'azure-resource-graph',
691
+ scope: 'research',
692
+ entrypoint: 'oxe-cc azure sync',
693
+ approval_policy: 'always_allow',
694
+ side_effects: [],
695
+ requires_env: [],
696
+ evidence_outputs: ['.oxe/cloud/azure/inventory.json', '.oxe/cloud/azure/INVENTORY.md'],
697
+ summary: 'Discovery determinístico de recursos Azure via Resource Graph.',
698
+ operations: ['sync inventory', 'find resource'],
699
+ },
700
+ {
701
+ id: 'azure-servicebus',
702
+ scope: 'execute',
703
+ entrypoint: 'oxe-cc azure servicebus <list|show|plan|apply>',
704
+ approval_policy: 'require_approval_if_external_side_effect',
705
+ side_effects: ['azure_resource_mutation'],
706
+ requires_env: [],
707
+ evidence_outputs: ['.oxe/cloud/azure/operations/*.json', '.oxe/cloud/azure/SERVICEBUS.md'],
708
+ summary: 'Gestão assistida de namespaces, queues, topics e subscriptions do Azure Service Bus.',
709
+ operations: ['list', 'show', 'create namespace', 'create queue', 'create topic', 'create subscription'],
710
+ },
711
+ {
712
+ id: 'azure-eventgrid',
713
+ scope: 'execute',
714
+ entrypoint: 'oxe-cc azure eventgrid <list|show|plan|apply>',
715
+ approval_policy: 'require_approval_if_external_side_effect',
716
+ side_effects: ['azure_resource_mutation'],
717
+ requires_env: [],
718
+ evidence_outputs: ['.oxe/cloud/azure/operations/*.json', '.oxe/cloud/azure/EVENTGRID.md'],
719
+ summary: 'Gestão assistida de topics, system topics e event subscriptions do Azure Event Grid.',
720
+ operations: ['list', 'show', 'create topic', 'create event subscription'],
721
+ },
722
+ {
723
+ id: 'azure-sql-admin',
724
+ scope: 'execute',
725
+ entrypoint: 'oxe-cc azure sql <list|show|plan|apply>',
726
+ approval_policy: 'require_approval_if_external_side_effect',
727
+ side_effects: ['azure_resource_mutation'],
728
+ requires_env: ['AZURE_SQL_ADMIN_PASSWORD'],
729
+ evidence_outputs: ['.oxe/cloud/azure/operations/*.json', '.oxe/cloud/azure/SQL.md'],
730
+ summary: 'Gestão assistida de servers, databases e firewall rules do Azure SQL.',
731
+ operations: ['list', 'show', 'create server', 'create database', 'create firewall rule'],
732
+ },
733
+ ];
734
+ for (const manifest of manifests) {
735
+ const filePath = path.join(capsDir, manifest.id, 'CAPABILITY.md');
736
+ if (!fs.existsSync(filePath)) {
737
+ ensureDir(path.dirname(filePath));
738
+ writeText(filePath, renderCapabilityManifest(manifest));
739
+ }
740
+ }
741
+ return manifests.map((manifest) => manifest.id);
742
+ }
743
+
744
+ function makeOperationId(domain) {
745
+ return `azure-${domain}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
746
+ }
747
+
748
+ function makeCheckpointId() {
749
+ return `CP-AZ-${Date.now().toString(36).toUpperCase()}`;
750
+ }
751
+
752
+ function ipRangeTooWide(start, end) {
753
+ const s = String(start || '').trim();
754
+ const e = String(end || '').trim();
755
+ return (s === '0.0.0.0' && e === '255.255.255.255') || (s === '*' && e === '*');
756
+ }
757
+
758
+ function requireField(input, key, label) {
759
+ const value = input[key];
760
+ if (value == null || value === '') {
761
+ throw new Error(`Informe ${label}.`);
762
+ }
763
+ return String(value);
764
+ }
765
+
766
+ function buildReadCommand(domain, verb, input) {
767
+ const resourceGroup = input.resourceGroup ? ['--resource-group', String(input.resourceGroup)] : [];
768
+ if (domain === 'servicebus') {
769
+ const kind = String(input.kind || 'namespace');
770
+ if (verb === 'list') {
771
+ if (kind === 'namespace') return ['servicebus', 'namespace', 'list', ...resourceGroup, '--output', 'json'];
772
+ if (kind === 'queue') return ['servicebus', 'queue', 'list', '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
773
+ if (kind === 'topic') return ['servicebus', 'topic', 'list', '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
774
+ if (kind === 'subscription') {
775
+ return [
776
+ 'servicebus',
777
+ 'topic',
778
+ 'subscription',
779
+ 'list',
780
+ '--namespace-name',
781
+ requireField(input, 'namespace', '--namespace'),
782
+ '--topic-name',
783
+ requireField(input, 'topicName', '--topic-name'),
784
+ ...resourceGroup,
785
+ '--output',
786
+ 'json',
787
+ ];
788
+ }
789
+ }
790
+ if (verb === 'show') {
791
+ if (kind === 'namespace') return ['servicebus', 'namespace', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
792
+ if (kind === 'queue') return ['servicebus', 'queue', 'show', '--name', requireField(input, 'name', '--name'), '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
793
+ if (kind === 'topic') return ['servicebus', 'topic', 'show', '--name', requireField(input, 'name', '--name'), '--namespace-name', requireField(input, 'namespace', '--namespace'), ...resourceGroup, '--output', 'json'];
794
+ if (kind === 'subscription') {
795
+ return [
796
+ 'servicebus',
797
+ 'topic',
798
+ 'subscription',
799
+ 'show',
800
+ '--name',
801
+ requireField(input, 'subscriptionName', '--subscription-name'),
802
+ '--namespace-name',
803
+ requireField(input, 'namespace', '--namespace'),
804
+ '--topic-name',
805
+ requireField(input, 'topicName', '--topic-name'),
806
+ ...resourceGroup,
807
+ '--output',
808
+ 'json',
809
+ ];
810
+ }
811
+ }
812
+ }
813
+ if (domain === 'eventgrid') {
814
+ const kind = String(input.kind || 'topic');
815
+ if (verb === 'list') {
816
+ if (kind === 'topic') return ['eventgrid', 'topic', 'list', ...resourceGroup, '--output', 'json'];
817
+ if (kind === 'system-topic') return ['eventgrid', 'system-topic', 'list', ...resourceGroup, '--output', 'json'];
818
+ if (kind === 'event-subscription') {
819
+ return [
820
+ 'eventgrid',
821
+ 'event-subscription',
822
+ 'list',
823
+ '--source-resource-id',
824
+ requireField(input, 'sourceResourceId', '--source-resource-id'),
825
+ '--output',
826
+ 'json',
827
+ ];
828
+ }
829
+ }
830
+ if (verb === 'show') {
831
+ if (kind === 'topic') return ['eventgrid', 'topic', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
832
+ if (kind === 'system-topic') return ['eventgrid', 'system-topic', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
833
+ if (kind === 'event-subscription') {
834
+ return [
835
+ 'eventgrid',
836
+ 'event-subscription',
837
+ 'show',
838
+ '--name',
839
+ requireField(input, 'name', '--name'),
840
+ '--source-resource-id',
841
+ requireField(input, 'sourceResourceId', '--source-resource-id'),
842
+ '--output',
843
+ 'json',
844
+ ];
845
+ }
846
+ }
847
+ }
848
+ if (domain === 'sql') {
849
+ const kind = String(input.kind || 'server');
850
+ if (verb === 'list') {
851
+ if (kind === 'server') return ['sql', 'server', 'list', ...resourceGroup, '--output', 'json'];
852
+ if (kind === 'database') return ['sql', 'db', 'list', '--server', requireField(input, 'server', '--server'), ...resourceGroup, '--output', 'json'];
853
+ if (kind === 'firewall-rule') {
854
+ return [
855
+ 'sql',
856
+ 'server',
857
+ 'firewall-rule',
858
+ 'list',
859
+ '--server',
860
+ requireField(input, 'server', '--server'),
861
+ ...resourceGroup,
862
+ '--output',
863
+ 'json',
864
+ ];
865
+ }
866
+ }
867
+ if (verb === 'show') {
868
+ if (kind === 'server') return ['sql', 'server', 'show', '--name', requireField(input, 'name', '--name'), ...resourceGroup, '--output', 'json'];
869
+ if (kind === 'database') {
870
+ return ['sql', 'db', 'show', '--name', requireField(input, 'name', '--name'), '--server', requireField(input, 'server', '--server'), ...resourceGroup, '--output', 'json'];
871
+ }
872
+ if (kind === 'firewall-rule') {
873
+ return [
874
+ 'sql',
875
+ 'server',
876
+ 'firewall-rule',
877
+ 'show',
878
+ '--name',
879
+ requireField(input, 'name', '--name'),
880
+ '--server',
881
+ requireField(input, 'server', '--server'),
882
+ ...resourceGroup,
883
+ '--output',
884
+ 'json',
885
+ ];
886
+ }
887
+ }
888
+ }
889
+ throw new Error(`Combinação ${domain}/${verb} ainda não suportada.`);
890
+ }
891
+
892
+ function buildMutationPlan(domain, input) {
893
+ const kind = String(input.kind || '').toLowerCase();
894
+ const resourceGroup = requireField(input, 'resourceGroup', '--resource-group');
895
+ const location = input.location ? String(input.location) : null;
896
+ const operation = {
897
+ operation_id: makeOperationId(domain),
898
+ created_at: new Date().toISOString(),
899
+ updated_at: new Date().toISOString(),
900
+ domain,
901
+ phase: 'planned',
902
+ kind,
903
+ action: 'create',
904
+ mutate: true,
905
+ approval_policy: 'require_approval_if_external_side_effect',
906
+ checkpoint_id: makeCheckpointId(),
907
+ resource_group: resourceGroup,
908
+ location,
909
+ resource_refs: [],
910
+ evidence_outputs: [],
911
+ blocked: false,
912
+ blocked_reason: null,
913
+ summary: '',
914
+ command_args: [],
915
+ command_display: '',
916
+ command_display_redacted: '',
917
+ metadata: {},
918
+ };
919
+ if (domain === 'servicebus') {
920
+ const namespace = input.namespace ? String(input.namespace) : null;
921
+ if (kind === 'namespace') {
922
+ const name = requireField(input, 'name', '--name');
923
+ operation.command_args = ['servicebus', 'namespace', 'create', '--name', name, '--resource-group', resourceGroup];
924
+ if (location) operation.command_args.push('--location', location);
925
+ operation.summary = `Criar namespace Service Bus ${name}`;
926
+ operation.resource_refs.push({ kind: 'namespace', name, resourceGroup });
927
+ } else if (kind === 'queue') {
928
+ const name = requireField(input, 'name', '--name');
929
+ operation.command_args = ['servicebus', 'queue', 'create', '--name', name, '--namespace-name', requireField(input, 'namespace', '--namespace'), '--resource-group', resourceGroup];
930
+ operation.summary = `Criar queue Service Bus ${name} no namespace ${namespace}`;
931
+ operation.resource_refs.push({ kind: 'queue', name, namespace, resourceGroup });
932
+ } else if (kind === 'topic') {
933
+ const name = requireField(input, 'name', '--name');
934
+ operation.command_args = ['servicebus', 'topic', 'create', '--name', name, '--namespace-name', requireField(input, 'namespace', '--namespace'), '--resource-group', resourceGroup];
935
+ operation.summary = `Criar topic Service Bus ${name} no namespace ${namespace}`;
936
+ operation.resource_refs.push({ kind: 'topic', name, namespace, resourceGroup });
937
+ } else if (kind === 'subscription') {
938
+ const name = requireField(input, 'subscriptionName', '--subscription-name');
939
+ const topicName = requireField(input, 'topicName', '--topic-name');
940
+ operation.command_args = ['servicebus', 'topic', 'subscription', 'create', '--name', name, '--namespace-name', requireField(input, 'namespace', '--namespace'), '--topic-name', topicName, '--resource-group', resourceGroup];
941
+ operation.summary = `Criar subscription ${name} no topic ${topicName}`;
942
+ operation.resource_refs.push({ kind: 'subscription', name, namespace, topicName, resourceGroup });
943
+ } else {
944
+ throw new Error('Service Bus suporta kind namespace | queue | topic | subscription.');
945
+ }
946
+ } else if (domain === 'eventgrid') {
947
+ if (kind === 'topic') {
948
+ const name = requireField(input, 'name', '--name');
949
+ operation.command_args = ['eventgrid', 'topic', 'create', '--name', name, '--resource-group', resourceGroup, '--location', location || 'eastus'];
950
+ operation.summary = `Criar topic Event Grid ${name}`;
951
+ operation.resource_refs.push({ kind: 'topic', name, resourceGroup });
952
+ } else if (kind === 'event-subscription') {
953
+ const name = requireField(input, 'name', '--name');
954
+ const sourceResourceId = requireField(input, 'sourceResourceId', '--source-resource-id');
955
+ const endpoint = requireField(input, 'endpoint', '--endpoint');
956
+ operation.command_args = ['eventgrid', 'event-subscription', 'create', '--name', name, '--source-resource-id', sourceResourceId, '--endpoint', endpoint];
957
+ operation.summary = `Criar event subscription ${name}`;
958
+ operation.resource_refs.push({ kind: 'event-subscription', name, sourceResourceId });
959
+ operation.metadata.endpoint = endpoint;
960
+ } else {
961
+ throw new Error('Event Grid suporta kind topic | event-subscription para mutação na v1.');
962
+ }
963
+ } else if (domain === 'sql') {
964
+ if (kind === 'server') {
965
+ const name = requireField(input, 'name', '--name');
966
+ const adminUser = requireField(input, 'adminUser', '--admin-user');
967
+ const passwordEnv = requireField(input, 'adminPasswordEnv', '--admin-password-env');
968
+ const passwordValue = process.env[passwordEnv] || (input.env && input.env[passwordEnv]) || null;
969
+ if (!passwordValue) {
970
+ throw new Error(`A variável de ambiente ${passwordEnv} não está definida.`);
971
+ }
972
+ operation.command_args = ['sql', 'server', 'create', '--name', name, '--resource-group', resourceGroup, '--location', location || 'eastus', '--admin-user', adminUser, '--admin-password', passwordValue];
973
+ operation.summary = `Criar Azure SQL server ${name}`;
974
+ operation.resource_refs.push({ kind: 'server', name, resourceGroup });
975
+ operation.metadata.admin_user = adminUser;
976
+ operation.metadata.admin_password_env = passwordEnv;
977
+ operation.command_display_redacted = `az sql server create --name ${name} --resource-group ${resourceGroup} --location ${location || 'eastus'} --admin-user ${adminUser} --admin-password \${${passwordEnv}}`;
978
+ } else if (kind === 'database') {
979
+ const name = requireField(input, 'name', '--name');
980
+ const server = requireField(input, 'server', '--server');
981
+ const serviceObjective = input.serviceObjective ? String(input.serviceObjective) : 'S0';
982
+ operation.command_args = ['sql', 'db', 'create', '--name', name, '--resource-group', resourceGroup, '--server', server, '--service-objective', serviceObjective];
983
+ operation.summary = `Criar Azure SQL database ${name} no server ${server}`;
984
+ operation.resource_refs.push({ kind: 'database', name, server, resourceGroup });
985
+ operation.metadata.service_objective = serviceObjective;
986
+ } else if (kind === 'firewall-rule') {
987
+ const name = requireField(input, 'name', '--name');
988
+ const server = requireField(input, 'server', '--server');
989
+ const startIp = requireField(input, 'startIpAddress', '--start-ip-address');
990
+ const endIp = requireField(input, 'endIpAddress', '--end-ip-address');
991
+ operation.command_args = ['sql', 'server', 'firewall-rule', 'create', '--name', name, '--resource-group', resourceGroup, '--server', server, '--start-ip-address', startIp, '--end-ip-address', endIp];
992
+ operation.summary = `Criar firewall rule ${name} no Azure SQL server ${server}`;
993
+ operation.resource_refs.push({ kind: 'firewall-rule', name, server, resourceGroup });
994
+ operation.metadata.start_ip_address = startIp;
995
+ operation.metadata.end_ip_address = endIp;
996
+ if (ipRangeTooWide(startIp, endIp)) {
997
+ operation.approval_policy = 'deny_unless_overridden';
998
+ operation.blocked = true;
999
+ operation.blocked_reason = 'Faixa de firewall ampla bloqueada por política.';
1000
+ }
1001
+ } else {
1002
+ throw new Error('Azure SQL suporta kind server | database | firewall-rule.');
1003
+ }
1004
+ } else {
1005
+ throw new Error(`Domínio Azure desconhecido: ${domain}`);
1006
+ }
1007
+ if (!operation.command_display_redacted) {
1008
+ operation.command_display_redacted = `az ${operation.command_args.join(' ')}`;
1009
+ }
1010
+ operation.command_display = operation.command_display_redacted;
1011
+ operation.evidence_outputs = [
1012
+ `.oxe/cloud/azure/operations/${operation.operation_id}.json`,
1013
+ `.oxe/cloud/azure/operations/${operation.operation_id}.md`,
1014
+ ];
1015
+ return operation;
1016
+ }
1017
+
1018
+ function renderAzureOperationMarkdown(operation) {
1019
+ return [
1020
+ `# OXE — Azure Operation ${operation.operation_id}`,
1021
+ '',
1022
+ `- **Domínio:** ${operation.domain}`,
1023
+ `- **Kind:** ${operation.kind}`,
1024
+ `- **Ação:** ${operation.action}`,
1025
+ `- **Fase:** ${operation.phase}`,
1026
+ `- **Mutação:** ${operation.mutate ? 'sim' : 'não'}`,
1027
+ `- **Política:** ${operation.approval_policy}`,
1028
+ `- **Checkpoint:** ${operation.checkpoint_id || '—'}`,
1029
+ `- **Resumo:** ${operation.summary || '—'}`,
1030
+ `- **Criado em:** ${operation.created_at}`,
1031
+ `- **Atualizado em:** ${operation.updated_at}`,
1032
+ '',
1033
+ '## Comando',
1034
+ '',
1035
+ '```bash',
1036
+ operation.command_display_redacted || operation.command_display || '',
1037
+ '```',
1038
+ '',
1039
+ '## Recursos alvo',
1040
+ '',
1041
+ ...(operation.resource_refs || []).map((ref) => `- ${JSON.stringify(ref)}`),
1042
+ '',
1043
+ '## Evidência',
1044
+ '',
1045
+ ...(operation.evidence_outputs || []).map((item) => `- ${item}`),
1046
+ '',
1047
+ ].join('\n');
1048
+ }
1049
+
1050
+ function persistAzureOperation(projectRoot, operation) {
1051
+ const p = ensureAzureArtifacts(projectRoot);
1052
+ const jsonPath = path.join(p.operationsDir, `${operation.operation_id}.json`);
1053
+ const mdPath = path.join(p.operationsDir, `${operation.operation_id}.md`);
1054
+ writeJson(jsonPath, redactObject(operation));
1055
+ writeText(mdPath, renderAzureOperationMarkdown(redactObject(operation)));
1056
+ return { jsonPath, mdPath };
1057
+ }
1058
+
1059
+ function checkpointIndexPath(projectRoot, activeSession) {
1060
+ return operational.operationalPaths(projectRoot, activeSession).checkpoints;
1061
+ }
1062
+
1063
+ function readCheckpointIndex(projectRoot, activeSession) {
1064
+ return readTextIfExists(checkpointIndexPath(projectRoot, activeSession)) || '';
1065
+ }
1066
+
1067
+ function writeCheckpointIndex(projectRoot, activeSession, rows) {
1068
+ const target = checkpointIndexPath(projectRoot, activeSession);
1069
+ const lines = [
1070
+ '# OXE — Checkpoints',
1071
+ '',
1072
+ '> Índice de checkpoints formais do ciclo atual. Usado para aprovações humanas e gates sensíveis.',
1073
+ '',
1074
+ '| ID | Tipo | Fase | Escopo | Estado | Política | Decisão | Override | Criado em | Resolvido em | Notas |',
1075
+ '|----|------|------|--------|--------|----------|---------|----------|-----------|--------------|-------|',
1076
+ ...rows.map((row) => `| ${row.id} | ${row.type} | ${row.phase} | ${row.scope} | ${row.status} | ${row.policy} | ${row.decision} | ${row.override} | ${row.created_at} | ${row.resolved_at} | ${row.notes} |`),
1077
+ '',
1078
+ ];
1079
+ writeText(target, lines.join('\n'));
1080
+ }
1081
+
1082
+ function upsertCheckpoint(projectRoot, activeSession, checkpoint) {
1083
+ const text = readCheckpointIndex(projectRoot, activeSession);
1084
+ const rows = [];
1085
+ if (text) {
1086
+ for (const line of text.split(/\r?\n/)) {
1087
+ const match = line.match(/^\|\s*(CP-[^|]+)\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|/i);
1088
+ if (!match) continue;
1089
+ rows.push({
1090
+ id: match[1].trim(),
1091
+ type: match[2].trim(),
1092
+ phase: match[3].trim(),
1093
+ scope: match[4].trim(),
1094
+ status: match[5].trim(),
1095
+ policy: match[6].trim(),
1096
+ decision: match[7].trim(),
1097
+ override: match[8].trim(),
1098
+ created_at: match[9].trim(),
1099
+ resolved_at: match[10].trim(),
1100
+ notes: match[11].trim(),
1101
+ });
1102
+ }
1103
+ }
1104
+ const idx = rows.findIndex((row) => row.id === checkpoint.id);
1105
+ if (idx === -1) rows.unshift(checkpoint);
1106
+ else rows[idx] = checkpoint;
1107
+ writeCheckpointIndex(projectRoot, activeSession, rows);
1108
+ return checkpoint;
1109
+ }
1110
+
1111
+ function syncRunProviderContext(projectRoot, activeSession, input) {
1112
+ const current = operational.readRunState(projectRoot, activeSession);
1113
+ const now = new Date().toISOString();
1114
+ const providerContext = {
1115
+ ...(current && current.provider_context && typeof current.provider_context === 'object' ? current.provider_context : {}),
1116
+ azure: {
1117
+ ...(((current && current.provider_context) || {}).azure || {}),
1118
+ ...(input.provider_context || {}),
1119
+ },
1120
+ };
1121
+ const next = current
1122
+ ? operational.writeRunState(projectRoot, activeSession, {
1123
+ ...current,
1124
+ updated_at: now,
1125
+ status: input.status || current.status,
1126
+ pending_checkpoints: input.pending_checkpoints || current.pending_checkpoints || [],
1127
+ provider_context: providerContext,
1128
+ evidence: input.evidence || current.evidence || [],
1129
+ })
1130
+ : operational.writeRunState(projectRoot, activeSession, {
1131
+ run_id: input.run_id || operational.makeRunId(),
1132
+ created_at: now,
1133
+ updated_at: now,
1134
+ status: input.status || 'planned',
1135
+ current_wave: null,
1136
+ cursor: { wave: null, task: null, mode: 'provider-azure' },
1137
+ active_tasks: [],
1138
+ pending_checkpoints: input.pending_checkpoints || [],
1139
+ evidence: input.evidence || [],
1140
+ provider_context: providerContext,
1141
+ });
1142
+ return next;
1143
+ }
1144
+
1145
+ function attachAzureContextToRun(projectRoot, activeSession, context, operation, status, pendingCheckpoints, evidence) {
1146
+ const summary = summarizeInventory((loadAzureInventory(projectRoot) || {}).items || []);
1147
+ return syncRunProviderContext(projectRoot, activeSession, {
1148
+ status,
1149
+ pending_checkpoints: pendingCheckpoints,
1150
+ evidence,
1151
+ provider_context: {
1152
+ enabled: true,
1153
+ cloud: context.profile && context.profile.cloud ? context.profile.cloud : 'AzureCloud',
1154
+ tenant_id: context.profile && context.profile.tenant_id ? context.profile.tenant_id : null,
1155
+ subscription_id: context.profile && context.profile.subscription_id ? context.profile.subscription_id : null,
1156
+ auth_mode: context.profile && context.profile.auth_mode ? context.profile.auth_mode : 'unknown',
1157
+ inventory_summary: summary,
1158
+ last_sync: (loadAzureInventory(projectRoot) || {}).synced_at || null,
1159
+ last_operation: operation
1160
+ ? {
1161
+ operation_id: operation.operation_id,
1162
+ domain: operation.domain,
1163
+ kind: operation.kind,
1164
+ action: operation.action,
1165
+ phase: operation.phase,
1166
+ summary: operation.summary,
1167
+ checkpoint_id: operation.checkpoint_id || null,
1168
+ capability_id: `azure-${operation.domain === 'sql' ? 'sql-admin' : operation.domain}`,
1169
+ resource_refs: operation.resource_refs || [],
1170
+ }
1171
+ : null,
1172
+ },
1173
+ });
1174
+ }
1175
+
1176
+ function planAzureOperation(projectRoot, activeSession, domain, input, options = {}) {
1177
+ const context = getAzureContext(projectRoot, options);
1178
+ if (!context.cli.installed) throw new Error('Azure CLI não instalada.');
1179
+ if (!context.authStatus.login_active) throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
1180
+ const operation = buildMutationPlan(domain, input);
1181
+ operation.phase = 'planned';
1182
+ operation.updated_at = new Date().toISOString();
1183
+ const files = persistAzureOperation(projectRoot, operation);
1184
+ attachAzureContextToRun(projectRoot, activeSession, context, operation, 'planned', [], []);
1185
+ operational.appendEvent(projectRoot, activeSession, {
1186
+ type: 'azure_operation_planned',
1187
+ payload: {
1188
+ domain,
1189
+ operation_id: operation.operation_id,
1190
+ checkpoint_id: operation.checkpoint_id,
1191
+ summary: operation.summary,
1192
+ files,
1193
+ },
1194
+ });
1195
+ return { context, operation, files };
1196
+ }
1197
+
1198
+ function applyAzureOperation(projectRoot, activeSession, domain, input, options = {}) {
1199
+ const context = getAzureContext(projectRoot, options);
1200
+ if (!context.cli.installed) throw new Error('Azure CLI não instalada.');
1201
+ if (!context.authStatus.login_active) throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
1202
+ if (options.vpnRequired && !options.vpnConfirmed) {
1203
+ throw new Error('Esta operação requer VPN conforme configuração do projeto. Execute com --vpn-confirmed para confirmar a conexão.');
1204
+ }
1205
+ const operation = buildMutationPlan(domain, input);
1206
+ if (options.dryRun) {
1207
+ return {
1208
+ approved: false,
1209
+ dryRun: true,
1210
+ operation: { ...operation, phase: 'dry_run' },
1211
+ commandPreview: `az ${operation.command_args.join(' ')}`,
1212
+ message: '[dry-run] Validação OK. Nenhuma alteração foi feita.',
1213
+ };
1214
+ }
1215
+ if (operation.blocked && !options.overridePolicy) {
1216
+ persistAzureOperation(projectRoot, operation);
1217
+ throw new Error(operation.blocked_reason || 'Operação Azure bloqueada por política.');
1218
+ }
1219
+ if (!options.approve) {
1220
+ operation.phase = 'waiting_approval';
1221
+ operation.updated_at = new Date().toISOString();
1222
+ const files = persistAzureOperation(projectRoot, operation);
1223
+ upsertCheckpoint(projectRoot, activeSession, {
1224
+ id: operation.checkpoint_id,
1225
+ type: 'approval',
1226
+ phase: 'execution',
1227
+ scope: `azure:${domain}:${operation.kind}`,
1228
+ status: 'pending_approval',
1229
+ policy: operation.approval_policy,
1230
+ decision: '—',
1231
+ override: '—',
1232
+ created_at: operation.created_at.slice(0, 10),
1233
+ resolved_at: '—',
1234
+ notes: operation.summary,
1235
+ });
1236
+ attachAzureContextToRun(projectRoot, activeSession, context, operation, 'waiting_approval', [operation.checkpoint_id], []);
1237
+ operational.appendEvent(projectRoot, activeSession, {
1238
+ type: 'azure_checkpoint_opened',
1239
+ payload: {
1240
+ domain,
1241
+ operation_id: operation.operation_id,
1242
+ checkpoint_id: operation.checkpoint_id,
1243
+ summary: operation.summary,
1244
+ files,
1245
+ },
1246
+ });
1247
+ return {
1248
+ approved: false,
1249
+ operation,
1250
+ files,
1251
+ checkpoint_id: operation.checkpoint_id,
1252
+ message: 'Mutação Azure planejada. Reexecute com --approve para aplicar.',
1253
+ };
1254
+ }
1255
+
1256
+ operation.phase = 'running';
1257
+ operation.updated_at = new Date().toISOString();
1258
+ upsertCheckpoint(projectRoot, activeSession, {
1259
+ id: operation.checkpoint_id,
1260
+ type: 'approval',
1261
+ phase: 'execution',
1262
+ scope: `azure:${domain}:${operation.kind}`,
1263
+ status: 'approved',
1264
+ policy: operation.approval_policy,
1265
+ decision: 'approved',
1266
+ override: options.overridePolicy ? 'explicit' : '—',
1267
+ created_at: operation.created_at.slice(0, 10),
1268
+ resolved_at: operation.updated_at.slice(0, 10),
1269
+ notes: operation.summary,
1270
+ });
1271
+ attachAzureContextToRun(projectRoot, activeSession, context, operation, 'running', [], []);
1272
+ const result = runAz([...operation.command_args, '--output', 'json'], {
1273
+ cwd: projectRoot,
1274
+ env: options.env || {},
1275
+ runner: options.runner,
1276
+ });
1277
+ if (result.status !== 0) {
1278
+ operation.phase = 'failed';
1279
+ operation.updated_at = new Date().toISOString();
1280
+ operation.error = (result.stderr || result.stdout || 'Falha ao aplicar operação Azure.').trim();
1281
+ const files = persistAzureOperation(projectRoot, operation);
1282
+ attachAzureContextToRun(projectRoot, activeSession, context, operation, 'failed', [], []);
1283
+ operational.appendEvent(projectRoot, activeSession, {
1284
+ type: 'azure_operation_failed',
1285
+ payload: {
1286
+ domain,
1287
+ operation_id: operation.operation_id,
1288
+ checkpoint_id: operation.checkpoint_id,
1289
+ error: operation.error,
1290
+ files,
1291
+ },
1292
+ });
1293
+ throw new Error(operation.error);
1294
+ }
1295
+ operation.phase = 'applied';
1296
+ operation.updated_at = new Date().toISOString();
1297
+ operation.result = redactObject(parseJsonOutput(result.stdout, {}));
1298
+ const files = persistAzureOperation(projectRoot, operation);
1299
+ attachAzureContextToRun(projectRoot, activeSession, context, operation, 'completed', [], [files.mdPath]);
1300
+ operational.appendEvent(projectRoot, activeSession, {
1301
+ type: 'azure_operation_applied',
1302
+ payload: {
1303
+ domain,
1304
+ operation_id: operation.operation_id,
1305
+ checkpoint_id: operation.checkpoint_id,
1306
+ files,
1307
+ resources: operation.resource_refs,
1308
+ },
1309
+ });
1310
+ return { approved: true, operation, files, result: operation.result };
1311
+ }
1312
+
1313
+ function executeAzureRead(projectRoot, activeSession, domain, verb, input, options = {}) {
1314
+ const context = getAzureContext(projectRoot, options);
1315
+ if (!context.cli.installed) throw new Error('Azure CLI não instalada.');
1316
+ if (!context.authStatus.login_active) throw new Error('Sessão Azure ausente. Execute "oxe-cc azure auth login".');
1317
+ const args = buildReadCommand(domain, verb, input);
1318
+ const result = runAz(args, { cwd: projectRoot, ...options });
1319
+ if (result.status !== 0) {
1320
+ throw new Error((result.stderr || result.stdout || 'Falha ao consultar recurso Azure.').trim());
1321
+ }
1322
+ const parsed = parseJsonOutput(result.stdout, []);
1323
+ attachAzureContextToRun(projectRoot, activeSession, context, null, 'completed', [], []);
1324
+ operational.appendEvent(projectRoot, activeSession, {
1325
+ type: 'azure_resource_found',
1326
+ payload: {
1327
+ domain,
1328
+ verb,
1329
+ kind: input.kind || null,
1330
+ query: redactObject(input),
1331
+ },
1332
+ });
1333
+ return parsed;
1334
+ }
1335
+
1336
+ function loginAzure(projectRoot, options = {}) {
1337
+ const cli = detectAzureCli(projectRoot, options);
1338
+ if (!cli.installed) throw new Error('Azure CLI não instalada.');
1339
+ operational.appendEvent(projectRoot, null, { type: 'azure_login_started' });
1340
+ const azArgs = ['login', '--output', 'json'];
1341
+ if (options.tenant) azArgs.push('--tenant', String(options.tenant));
1342
+ const login = runAz(azArgs, { cwd: projectRoot, inherit: Boolean(options.inherit), ...options });
1343
+ if (login.status !== 0) {
1344
+ throw new Error((login.stderr || login.stdout || 'Falha no az login.').trim());
1345
+ }
1346
+ operational.appendEvent(projectRoot, null, { type: 'azure_login_succeeded' });
1347
+ // getAzureContext must capture output — strip inherit so it pipes stdout
1348
+ const { inherit: _inherit, ...getCtxOptions } = options;
1349
+ return getAzureContext(projectRoot, getCtxOptions);
1350
+ }
1351
+
1352
+ function setAzureSubscription(projectRoot, subscription, options = {}) {
1353
+ if (!subscription) throw new Error('Informe a subscription.');
1354
+ const cli = detectAzureCli(projectRoot, options);
1355
+ if (!cli.installed) throw new Error('Azure CLI não instalada.');
1356
+ const set = runAz(['account', 'set', '--subscription', String(subscription)], { cwd: projectRoot, ...options });
1357
+ if (set.status !== 0) {
1358
+ throw new Error((set.stderr || set.stdout || 'Falha ao selecionar subscription.').trim());
1359
+ }
1360
+ const context = getAzureContext(projectRoot, options);
1361
+ operational.appendEvent(projectRoot, null, {
1362
+ type: 'azure_subscription_selected',
1363
+ payload: {
1364
+ subscription: context.profile.subscription_id,
1365
+ subscription_name: context.profile.subscription_name,
1366
+ },
1367
+ });
1368
+ return context;
1369
+ }
1370
+
1371
+ function azureDoctor(projectRoot, config = {}, options = {}) {
1372
+ const p = options.write === false ? azurePaths(projectRoot) : ensureAzureArtifacts(projectRoot);
1373
+ const context = getAzureContext(projectRoot, options);
1374
+ const inventory = loadAzureInventory(projectRoot);
1375
+ const capsRoot = path.join(projectRoot, '.oxe', 'capabilities');
1376
+ const missingCapabilities = AZURE_CAPABILITY_IDS.filter((id) => !fs.existsSync(path.join(capsRoot, id, 'CAPABILITY.md')));
1377
+ const warnings = [];
1378
+ if (!context.cli.installed) warnings.push('Azure CLI não instalada.');
1379
+ else if (!context.cli.okVersion) warnings.push(`Azure CLI ${context.cli.version || 'desconhecida'} abaixo do mínimo suportado.`);
1380
+ if (context.cli.installed && !context.authStatus.login_active) warnings.push('Sessão Azure ausente.');
1381
+ if (context.authStatus.login_active && !context.profile.subscription_id) warnings.push('Subscription Azure não selecionada.');
1382
+ if (!context.authStatus.resource_graph_enabled) warnings.push('Extensão resource-graph ausente ou não habilitada.');
1383
+ if (!inventory) warnings.push('Inventário Azure ausente. Execute "oxe-cc azure sync".');
1384
+ if (inventory && config && typeof config.azure === 'object') {
1385
+ const maxAgeHours = Number(config.azure.inventory_max_age_hours || 24);
1386
+ const syncedAt = Date.parse(String(inventory.synced_at || ''));
1387
+ if (!Number.isNaN(syncedAt) && maxAgeHours > 0) {
1388
+ const ageHours = Math.floor((Date.now() - syncedAt) / (1000 * 60 * 60));
1389
+ if (ageHours > maxAgeHours) warnings.push(`Inventário Azure stale (${ageHours}h > ${maxAgeHours}h).`);
1390
+ }
1391
+ }
1392
+ if (missingCapabilities.length) warnings.push(`Capabilities Azure ausentes: ${missingCapabilities.join(', ')}`);
1393
+ const operations = listAzureOperations(projectRoot);
1394
+ const pendingOperation = operations.find((operation) => operation.phase === 'waiting_approval');
1395
+ if (pendingOperation) warnings.push(`Operação Azure pendente sem apply final: ${pendingOperation.operation_id}`);
1396
+ const authStatus = {
1397
+ ...(context.authStatus || {}),
1398
+ checked_at: new Date().toISOString(),
1399
+ warnings,
1400
+ };
1401
+ if (options.write !== false) {
1402
+ writeAzureAuthArtifacts(projectRoot, { profile: context.profile || DEFAULT_AZURE_PROFILE, authStatus });
1403
+ }
1404
+ return {
1405
+ healthy: warnings.length === 0,
1406
+ warnings,
1407
+ profile: context.profile,
1408
+ authStatus,
1409
+ inventory,
1410
+ inventorySummary: inventory && inventory.summary ? inventory.summary : summarizeInventory([]),
1411
+ paths: p,
1412
+ };
1413
+ }
1414
+
1415
+ module.exports = {
1416
+ MIN_AZURE_CLI_MAJOR,
1417
+ AZURE_CAPABILITY_IDS,
1418
+ RESOURCE_GRAPH_QUERY,
1419
+ DEFAULT_AZURE_PROFILE,
1420
+ azurePaths,
1421
+ ensureAzureArtifacts,
1422
+ isAzureContextEnabled,
1423
+ detectAzureCli,
1424
+ loadAzureProfile,
1425
+ loadAzureAuthStatus,
1426
+ loadAzureInventory,
1427
+ listAzureOperations,
1428
+ summarizeInventory,
1429
+ normalizeInventoryItem,
1430
+ searchAzureInventory,
1431
+ diffInventory,
1432
+ statusAzure,
1433
+ ensureAzureCapabilities,
1434
+ getAzureContext,
1435
+ loginAzure,
1436
+ setAzureSubscription,
1437
+ ensureResourceGraphExtension,
1438
+ syncAzureInventory,
1439
+ executeAzureRead,
1440
+ planAzureOperation,
1441
+ applyAzureOperation,
1442
+ azureDoctor,
1443
+ redactObject,
1444
+ runAz,
1445
+ };