deepdebug-local-agent 1.0.23 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.claude/context.md +76 -0
  2. package/.claude/skills/README.md +25 -0
  3. package/.claude/skills/tdd-debugging/SKILL.md +53 -0
  4. package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
  5. package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
  6. package/.claude/standards/README.md +51 -0
  7. package/.claude/standards/clean-code.md +75 -0
  8. package/.claude/standards/code-review-checklist.md +61 -0
  9. package/.claude/standards/layered-architecture.md +163 -0
  10. package/.claude/standards/node.md +45 -0
  11. package/.claude/standards/security-owasp.md +114 -0
  12. package/.claude/standards/simplicity.md +42 -0
  13. package/.claude/standards/solid.md +64 -0
  14. package/.claude/standards/testing.md +59 -0
  15. package/.cursor/rules/standards.mdc +17 -0
  16. package/.github/workflows/build-byoc-image.yml +315 -0
  17. package/.github/workflows/deploy-gcp.yml +102 -0
  18. package/.github/workflows/npm-publish.yml +69 -8
  19. package/AGENTS.md +21 -0
  20. package/CHANGELOG.md +69 -0
  21. package/CLAUDE.md +13 -0
  22. package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
  23. package/Dockerfile +4 -0
  24. package/Dockerfile.full +55 -3
  25. package/bin/install.js +16 -5
  26. package/cloudbuild-full.yaml +57 -0
  27. package/docker-compose.yml +5 -3
  28. package/docs/OBSERVABILITY.md +164 -0
  29. package/docs/byoc-agent-image.md +70 -0
  30. package/docs/graphify-packaging-plan.md +133 -0
  31. package/docs/graphify-validation-run-2026-10-28.md +243 -0
  32. package/docs/releasing.md +43 -0
  33. package/docs/signed-commands.md +163 -0
  34. package/env.example +30 -0
  35. package/helm/Chart.yaml +2 -2
  36. package/helm/templates/deployment.yaml +13 -3
  37. package/helm/templates/networkpolicy.yaml +22 -0
  38. package/helm/templates/secret.yaml +1 -0
  39. package/helm/values.yaml +13 -14
  40. package/mcp-server.js +4 -1
  41. package/package.json +9 -4
  42. package/scripts/upgrade-observability-push.sh +227 -0
  43. package/src/ai-engine.js +22 -22
  44. package/src/config/trusted-signing-keys.json +3 -0
  45. package/src/detectors/build-tool-detector.js +50 -0
  46. package/src/detectors/multi-project-detector.js +22 -22
  47. package/src/detectors/service-detector.js +24 -24
  48. package/src/exec-utils.js +26 -4
  49. package/src/git/base-git-provider.js +117 -117
  50. package/src/git/bitbucket-provider.js +2 -2
  51. package/src/git/git-provider-registry.js +18 -18
  52. package/src/git/github-provider.js +13 -13
  53. package/src/git/multi-repo-pr.js +161 -0
  54. package/src/mcp-http-server.js +32 -16
  55. package/src/observability/bundle-files.js +418 -0
  56. package/src/observability/detect.js +151 -0
  57. package/src/observability/provision.js +212 -0
  58. package/src/observability/render-cli.js +59 -0
  59. package/src/observability/routes.js +88 -0
  60. package/src/patch/patch-engine.js +28 -28
  61. package/src/patch-manager.js +85 -85
  62. package/src/patch_bkp.js +14 -14
  63. package/src/ports.js +4 -4
  64. package/src/routes/workspace.route.js +47 -16
  65. package/src/runtimes/base-runtime.js +75 -75
  66. package/src/runtimes/java/gradle-runtime.js +17 -17
  67. package/src/runtimes/java/java-integrations.js +25 -25
  68. package/src/runtimes/java/maven-runtime.js +34 -34
  69. package/src/runtimes/node/node-integrations.js +1 -1
  70. package/src/runtimes/node/npm-runtime.js +14 -14
  71. package/src/runtimes/node/yarn-runtime.js +4 -4
  72. package/src/runtimes/runtime-registry.js +25 -25
  73. package/src/server-local.js +4 -4
  74. package/src/server.js +1366 -333
  75. package/src/siblings/burst-suppressor.js +89 -0
  76. package/src/siblings/capabilities.js +273 -0
  77. package/src/siblings/env-compat.cjs +58 -0
  78. package/src/siblings/incident-forwarder.js +320 -0
  79. package/src/siblings/logger.js +15 -0
  80. package/src/siblings/metrics.js +94 -0
  81. package/src/utils/agent-auth.js +184 -0
  82. package/src/utils/approval-token.js +56 -0
  83. package/src/utils/bind-host.js +29 -0
  84. package/src/utils/branch-refspec.js +98 -0
  85. package/src/utils/child-env.js +34 -0
  86. package/src/utils/environment-diagnostics.js +55 -55
  87. package/src/utils/exec-policy.js +962 -0
  88. package/src/utils/exec-utils.js +15 -15
  89. package/src/utils/file-count.js +46 -0
  90. package/src/utils/fs-utils.js +10 -10
  91. package/src/utils/multi-repo-open.js +132 -0
  92. package/src/utils/open-gate.js +183 -0
  93. package/src/utils/ranged-read.js +65 -0
  94. package/src/utils/request-guards.js +77 -0
  95. package/src/utils/request-signing.js +342 -0
  96. package/src/utils/root-recovery.js +244 -0
  97. package/src/utils/safe-exec-args.js +58 -0
  98. package/src/utils/sandbox.js +182 -0
  99. package/src/utils/search-outcome.js +56 -0
  100. package/src/utils/workspace-root.js +80 -0
  101. package/src/validators/syntax-validator.js +149 -0
  102. package/src/workspace/detect-port.js +16 -16
  103. package/src/workspace/file-reader.js +1 -1
  104. package/src/workspace/process-manager.js +46 -46
  105. package/src/workspace-manager.js +86 -24
  106. package/test/adapters/registry.test.js +9 -2
  107. package/test/agent-auth.test.js +194 -0
  108. package/test/bind-host.test.js +26 -0
  109. package/test/branch-refspec.test.js +224 -0
  110. package/test/burst-suppressor.test.js +60 -0
  111. package/test/capabilities-access.test.js +47 -0
  112. package/test/capabilities.test.js +87 -0
  113. package/test/child-env.test.js +23 -0
  114. package/test/env-compat.test.js +71 -0
  115. package/test/exec-policy.test.js +463 -0
  116. package/test/file-count.test.js +64 -0
  117. package/test/incident-forwarder.test.js +231 -0
  118. package/test/multi-repo-open.test.js +180 -0
  119. package/test/multi-repo-pr.test.js +129 -0
  120. package/test/observability-detect.test.js +199 -0
  121. package/test/open-gate.test.js +341 -0
  122. package/test/ranged-read.test.js +60 -0
  123. package/test/request-guards.test.js +91 -0
  124. package/test/request-signing.test.js +284 -0
  125. package/test/root-recovery-sidecar.test.js +171 -0
  126. package/test/root-recovery.test.js +274 -0
  127. package/test/safe-exec-args.test.js +58 -0
  128. package/test/sandbox.test.js +98 -0
  129. package/test/search-outcome.test.js +50 -0
  130. package/test/workspace-root.test.js +64 -0
  131. package/tools/graphify-validation/README.md +92 -0
  132. package/tools/graphify-validation/config/questions.json +31 -0
  133. package/tools/graphify-validation/config/repos.json +9 -0
  134. package/tools/graphify-validation/config/symbols.json +6 -0
  135. package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
  136. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
  137. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
  138. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
  139. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
  140. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
  141. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
  142. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
  143. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
  144. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
  145. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
  146. package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
  147. package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
  148. package/tools/graphify-validation/lib/graph_metrics.py +96 -0
  149. package/tools/graphify-validation/lib/gv_common.py +130 -0
  150. package/tools/graphify-validation/lib/licenses.py +47 -0
  151. package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
  152. package/tools/graphify-validation/lib/recall.py +115 -0
  153. package/tools/graphify-validation/lib/report.py +226 -0
  154. package/tools/graphify-validation/lib/run_all.py +237 -0
  155. package/tools/graphify-validation/lib/run_graphify.py +144 -0
  156. package/tools/graphify-validation/lib/rusage_helper.py +17 -0
  157. package/tools/graphify-validation/lib/sample_edges.py +246 -0
  158. package/tools/graphify-validation/lib/spring_fixture.py +81 -0
  159. package/tools/graphify-validation/requirements-py311.lock +508 -0
  160. package/tools/graphify-validation/requirements-tools.in +1 -0
  161. package/tools/graphify-validation/requirements-tools.lock +371 -0
  162. package/tools/graphify-validation/requirements.in +1 -0
  163. package/tools/graphify-validation/requirements.lock +502 -0
  164. package/tools/graphify-validation/run.sh +43 -0
  165. package/tools/graphify-validation/tests/test_harness.py +151 -0
  166. package/cloudbuild-agent-qa.yaml +0 -43
  167. package/cloudbuild-agent-staging.yaml +0 -40
  168. package/cloudbuild.yaml.deprecated +0 -42
@@ -0,0 +1,418 @@
1
+ /**
2
+ * observability/bundle-files.js
3
+ *
4
+ * The client-side observability bundle, embedded as templated strings so it
5
+ * ships inside the pkg-bundled agent binary (no loose asset files to lose).
6
+ *
7
+ * This is the CLIENT delivery of the same stack we run for ourselves in
8
+ * `vh-observability` (Grafana + Loki + Alloy + Alertmanager). The difference is
9
+ * only in how logs get in and where chunks live:
10
+ *
11
+ * vh-observability (ours, Cloud Run): logs = Cloud Logging -> Pub/Sub (gcplog)
12
+ * storage = GCS
13
+ * client bundle (this, VPS): logs = docker.sock (every container)
14
+ * storage = local filesystem volume
15
+ *
16
+ * Everything downstream is identical: Loki stores + rules-evaluates, Alertmanager
17
+ * posts firing alerts to the gateway webhook (tenant resolved from the ?s= trigger
18
+ * secret, exactly like ours), and Grafana serves a logs dashboard whose "Open in
19
+ * Workbench" data link hits /api/v1/webhooks/from-log.
20
+ *
21
+ * Placeholders replaced by renderBundle():
22
+ * __GATEWAY_URL__ e.g. https://api.visionhammer.io (no trailing slash)
23
+ * __TRIGGER_SECRET__ the tenant's vht_... alertmanager trigger secret
24
+ * __ENVIRONMENT__ static env label for this client's logs (e.g. production)
25
+ */
26
+
27
+ // ---- Loki: filesystem backend (no GCS); rules + ruler -> Alertmanager ----
28
+ const LOKI_CONFIG = `# DeepDebug client observability - Loki (docker-compose, filesystem backend).
29
+ auth_enabled: false
30
+
31
+ server:
32
+ http_listen_port: 3100
33
+ grpc_listen_port: 9095
34
+ log_level: warn
35
+
36
+ common:
37
+ instance_addr: 127.0.0.1
38
+ path_prefix: /loki
39
+ replication_factor: 1
40
+ ring:
41
+ kvstore:
42
+ store: inmemory
43
+ storage:
44
+ filesystem:
45
+ chunks_directory: /loki/chunks
46
+ rules_directory: /loki/rules-store
47
+
48
+ schema_config:
49
+ configs:
50
+ - from: 2024-01-01
51
+ store: tsdb
52
+ object_store: filesystem
53
+ schema: v13
54
+ index:
55
+ prefix: index_
56
+ period: 24h
57
+
58
+ limits_config:
59
+ retention_period: 336h # 14 days
60
+ reject_old_samples: true
61
+ # Wide enough to accept a host's existing docker log backlog on first start.
62
+ # loki.source.docker replays each container's full history oldest-first; a
63
+ # tight window (e.g. 168h) 400-rejects that backlog and queues fresh lines
64
+ # behind it. 720h clears the backlog cleanly; retention above still caps disk.
65
+ reject_old_samples_max_age: 720h
66
+ volume_enabled: true
67
+ allow_structured_metadata: true
68
+
69
+ compactor:
70
+ working_directory: /loki/compactor
71
+ retention_enabled: true
72
+ delete_request_store: filesystem
73
+
74
+ # Ruler evaluates the LogQL error rule and pushes firing alerts to Alertmanager.
75
+ ruler:
76
+ storage:
77
+ type: local
78
+ local:
79
+ directory: /etc/loki/rules
80
+ rule_path: /loki/rules-tmp
81
+ alertmanager_url: http://alertmanager:9093
82
+ # Alertmanager v0.27 removed the legacy v1 push API; Loki's ruler defaults to
83
+ # POSTing /api/v1/alerts, which that Alertmanager rejects, so alerts silently
84
+ # never arrive. Force the v2 endpoint.
85
+ enable_alertmanager_v2: true
86
+ enable_api: true
87
+ ring:
88
+ kvstore:
89
+ store: inmemory
90
+ `;
91
+
92
+ // ---- Loki rule: error rate by service -> critical alert ----
93
+ const LOKI_RULE = `# DeepDebug client error alerts. Labels service/severity/environment flow into
94
+ # the gateway incident (AlertmanagerIngestService reads them).
95
+ groups:
96
+ - name: client-errors
97
+ rules:
98
+ - alert: ClientServiceErrors
99
+ expr: |
100
+ sum by (service, environment) (rate({job="docker", service!~"deepdebug-observability-.*|vh-obs-.*|visionhammer-agent"} |~ \`(?i)\\bERROR\\b\` [5m])) > 0
101
+ for: 2m
102
+ labels:
103
+ severity: critical
104
+ origin: deepdebug-observability
105
+ annotations:
106
+ summary: "errors on {{ $labels.service }} ({{ $labels.environment }})"
107
+ `;
108
+
109
+ // ---- Alloy: tail every container via docker.sock, label by container name ----
110
+ const ALLOY_CONFIG = `// DeepDebug client observability - Alloy.
111
+ // Tails every Docker container on the host (docker.sock) and ships to Loki.
112
+ // The same stream is also pushed to VisionHammer (OTLP/HTTP JSON, outbound only)
113
+ // so Live logs and the services list see this server; auth is the tenant's
114
+ // trigger secret, the same one the Alertmanager below uses.
115
+ // service = container name; environment = the static label this client was
116
+ // provisioned with; level is parsed from the line for quick filtering.
117
+
118
+ discovery.docker "containers" {
119
+ host = "unix:///var/run/docker.sock"
120
+ }
121
+
122
+ discovery.relabel "containers" {
123
+ targets = discovery.docker.containers.targets
124
+
125
+ rule {
126
+ source_labels = ["__meta_docker_container_name"]
127
+ regex = "/?(.*)"
128
+ target_label = "service"
129
+ }
130
+ rule {
131
+ source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
132
+ target_label = "project"
133
+ }
134
+ }
135
+
136
+ loki.source.docker "default" {
137
+ host = "unix:///var/run/docker.sock"
138
+ targets = discovery.relabel.containers.output
139
+ labels = { job = "docker" }
140
+ forward_to = [loki.process.default.receiver]
141
+ }
142
+
143
+ loki.process "default" {
144
+ forward_to = [loki.write.default.receiver, otelcol.receiver.loki.visionhammer.receiver]
145
+
146
+ stage.regex {
147
+ expression = "(?i)(?P<level>ERROR|WARNING|WARN|INFO|DEBUG|SEVERE|FATAL|CRITICAL)"
148
+ }
149
+ stage.labels {
150
+ values = { level = "" }
151
+ }
152
+ stage.static_labels {
153
+ values = { environment = "__ENVIRONMENT__" }
154
+ }
155
+ }
156
+
157
+ loki.write "default" {
158
+ endpoint {
159
+ url = "http://loki:3100/loki/api/v1/push"
160
+ }
161
+ }
162
+
163
+ // ---- push to VisionHammer: Loki entries -> OTLP logs (labels become attributes) ----
164
+ otelcol.receiver.loki "visionhammer" {
165
+ output {
166
+ logs = [otelcol.processor.batch.visionhammer.input]
167
+ }
168
+ }
169
+
170
+ otelcol.processor.batch "visionhammer" {
171
+ send_batch_size = 500
172
+ send_batch_max_size = 2000
173
+ timeout = "5s"
174
+ output {
175
+ logs = [otelcol.exporter.otlphttp.visionhammer.input]
176
+ }
177
+ }
178
+
179
+ otelcol.exporter.otlphttp "visionhammer" {
180
+ client {
181
+ endpoint = "__GATEWAY_URL__"
182
+ compression = "gzip"
183
+ headers = { "Authorization" = "Bearer __TRIGGER_SECRET__" }
184
+ }
185
+ encoding = "json"
186
+ logs_endpoint = "__GATEWAY_URL__/api/v1/webhooks/logs/otlp"
187
+ }
188
+ `;
189
+
190
+ // ---- Alertmanager: firing alerts -> gateway webhook (tenant from ?s=) ----
191
+ const ALERTMANAGER_CONFIG = `route:
192
+ receiver: deepdebug-gateway
193
+ group_by: ['service']
194
+ group_wait: 30s
195
+ group_interval: 5m
196
+ repeat_interval: 4h
197
+
198
+ receivers:
199
+ - name: deepdebug-gateway
200
+ webhook_configs:
201
+ - url: '__GATEWAY_URL__/api/v1/webhooks/alertmanager'
202
+ send_resolved: false
203
+ http_config:
204
+ authorization:
205
+ type: Bearer
206
+ credentials: '__TRIGGER_SECRET__'
207
+ `;
208
+
209
+ // ---- Grafana provisioning: Loki datasource + dashboard provider ----
210
+ const GRAFANA_DATASOURCE = `apiVersion: 1
211
+ datasources:
212
+ - name: Loki
213
+ uid: ddo-loki
214
+ type: loki
215
+ access: proxy
216
+ url: http://loki:3100
217
+ isDefault: true
218
+ jsonData:
219
+ maxLines: 1000
220
+ `;
221
+
222
+ const GRAFANA_DASH_PROVIDER = `apiVersion: 1
223
+ providers:
224
+ - name: deepdebug
225
+ type: file
226
+ disableDeletion: false
227
+ updateIntervalSeconds: 30
228
+ options:
229
+ path: /etc/grafana/dashboards
230
+ foldersFromFilesStructure: false
231
+ `;
232
+
233
+ // ---- Grafana dashboard: logs + error stats, "Open in Workbench" data link ----
234
+ const DASHBOARD = JSON.stringify({
235
+ uid: 'ddo-logs',
236
+ title: 'DeepDebug - Observability',
237
+ editable: true,
238
+ schemaVersion: 39,
239
+ style: 'dark',
240
+ tags: ['deepdebug', 'logs'],
241
+ time: { from: 'now-1h', to: 'now' },
242
+ refresh: '30s',
243
+ templating: {
244
+ list: [
245
+ { name: 'service', label: 'Service', type: 'query',
246
+ datasource: { type: 'loki', uid: 'ddo-loki' },
247
+ query: 'label_values(service)', refresh: 2, multi: true, includeAll: true,
248
+ current: { text: 'All', value: '$__all' } },
249
+ { name: 'search', label: 'Search', type: 'textbox', current: { text: '', value: '' } },
250
+ ],
251
+ },
252
+ panels: [
253
+ { id: 1, title: 'Errors (last 5m)', type: 'stat',
254
+ datasource: { type: 'loki', uid: 'ddo-loki' },
255
+ gridPos: { h: 5, w: 6, x: 0, y: 0 },
256
+ options: { colorMode: 'background', graphMode: 'area', reduceOptions: { calcs: ['lastNotNull'] } },
257
+ fieldConfig: { defaults: { color: { mode: 'thresholds' },
258
+ thresholds: { steps: [ { color: 'green', value: null }, { color: 'orange', value: 1 }, { color: 'red', value: 10 } ] } } },
259
+ targets: [ { refId: 'A', expr: 'sum(count_over_time({service=~"$service"} |~ `(?i)error` [5m]))' } ] },
260
+ { id: 2, title: 'Warnings (last 5m)', type: 'stat',
261
+ datasource: { type: 'loki', uid: 'ddo-loki' },
262
+ gridPos: { h: 5, w: 6, x: 6, y: 0 },
263
+ options: { colorMode: 'background', graphMode: 'area', reduceOptions: { calcs: ['lastNotNull'] } },
264
+ fieldConfig: { defaults: { color: { mode: 'thresholds' },
265
+ thresholds: { steps: [ { color: 'green', value: null }, { color: 'yellow', value: 1 } ] } } },
266
+ targets: [ { refId: 'A', expr: 'sum(count_over_time({service=~"$service"} |~ `(?i)warn` [5m]))' } ] },
267
+ { id: 3, title: 'Error rate by service', type: 'timeseries',
268
+ datasource: { type: 'loki', uid: 'ddo-loki' },
269
+ gridPos: { h: 5, w: 12, x: 12, y: 0 },
270
+ fieldConfig: { defaults: { custom: { drawStyle: 'line', fillOpacity: 20, showPoints: 'never' }, color: { mode: 'palette-classic' } } },
271
+ targets: [ { refId: 'A', legendFormat: '{{service}}', expr: 'sum by (service) (rate({service=~"$service"} |~ `(?i)error` [5m]))' } ] },
272
+ { id: 4, title: 'Logs', type: 'logs',
273
+ datasource: { type: 'loki', uid: 'ddo-loki' },
274
+ gridPos: { h: 12, w: 24, x: 0, y: 5 },
275
+ options: { showTime: true, wrapLogMessage: true, enableLogDetails: true, dedupStrategy: 'none', sortOrder: 'Descending' },
276
+ targets: [ { refId: 'A', expr: '{service=~"$service"} |~ `(?i)$search`' } ],
277
+ fieldConfig: { defaults: { links: [ {
278
+ title: 'Open in DeepDebug Workbench',
279
+ url: '__GATEWAY_URL__/api/v1/webhooks/from-log?s=__TRIGGER_SECRET__&service=${service}&environment=__ENVIRONMENT__&severity=critical&message=${__value.raw}',
280
+ targetBlank: true,
281
+ } ] } } },
282
+ ],
283
+ }, null, 2);
284
+
285
+ // ---- docker-compose: the four containers, Grafana bound to loopback only ----
286
+ const DOCKER_COMPOSE = `name: deepdebug-observability
287
+ services:
288
+ loki:
289
+ image: grafana/loki:3.1.1
290
+ command: -config.file=/etc/loki/loki-config.yaml
291
+ volumes:
292
+ - ./loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
293
+ - ./loki/rules:/etc/loki/rules:ro
294
+ - ddo-loki:/loki
295
+ restart: unless-stopped
296
+
297
+ alloy:
298
+ image: grafana/alloy:v1.3.1
299
+ command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
300
+ volumes:
301
+ - ./alloy/config.alloy:/etc/alloy/config.alloy:ro
302
+ - /var/run/docker.sock:/var/run/docker.sock:ro
303
+ - ddo-alloy:/var/lib/alloy/data
304
+ depends_on:
305
+ - loki
306
+ restart: unless-stopped
307
+
308
+ grafana:
309
+ image: grafana/grafana:11.1.0
310
+ environment:
311
+ - GF_SECURITY_ADMIN_USER=\${GF_ADMIN_USER:-admin}
312
+ - GF_SECURITY_ADMIN_PASSWORD=\${GF_ADMIN_PASSWORD:-deepdebug}
313
+ - GF_USERS_ALLOW_SIGN_UP=false
314
+ - GF_USERS_DEFAULT_THEME=dark
315
+ - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/dashboards/ddo-logs.json
316
+ ports:
317
+ # Loopback only: reach it over an SSH tunnel, never expose to the internet.
318
+ - "127.0.0.1:3000:3000"
319
+ volumes:
320
+ - ./grafana/provisioning:/etc/grafana/provisioning:ro
321
+ - ./grafana/dashboards:/etc/grafana/dashboards:ro
322
+ - ddo-grafana:/var/lib/grafana
323
+ depends_on:
324
+ - loki
325
+ restart: unless-stopped
326
+
327
+ alertmanager:
328
+ image: prom/alertmanager:v0.27.0
329
+ command:
330
+ - --config.file=/etc/alertmanager/alertmanager.yml
331
+ - --storage.path=/tmp/alertmanager
332
+ volumes:
333
+ - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
334
+ restart: unless-stopped
335
+
336
+ volumes:
337
+ ddo-loki:
338
+ ddo-alloy:
339
+ ddo-grafana:
340
+ `;
341
+
342
+ // ---- Tunnel overlay (opt-in): expose Loki to the gateway, bearer-gated ----
343
+ // Caddy sits in front of Loki and 403s anything without the exact bearer token,
344
+ // so the Cloudflare quick tunnel never exposes an unauthenticated Loki. The
345
+ // gateway's loki-logging config stores this same token.
346
+ const CADDY_CONFIG = `# DeepDebug observability - bearer-auth gate in front of Loki.
347
+ :8080 {
348
+ @unauthorized not header Authorization "Bearer __LOKI_TUNNEL_TOKEN__"
349
+ respond @unauthorized "forbidden" 403
350
+ reverse_proxy loki:3100
351
+ }
352
+ `;
353
+
354
+ // Opt-in overlay compose. Start alongside the base file:
355
+ // docker compose -f docker-compose.yml -f docker-compose.tunnel.yml up -d
356
+ // then read the public URL:
357
+ // docker compose logs cloudflared | grep -o 'https://[^ ]*trycloudflare.com'
358
+ const TUNNEL_COMPOSE = `name: deepdebug-observability
359
+ services:
360
+ loki-auth:
361
+ image: caddy:2.8
362
+ volumes:
363
+ - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
364
+ depends_on:
365
+ - loki
366
+ restart: unless-stopped
367
+
368
+ cloudflared:
369
+ image: cloudflare/cloudflared:2024.8.3
370
+ command: tunnel --no-autoupdate --url http://loki-auth:8080
371
+ depends_on:
372
+ - loki-auth
373
+ restart: unless-stopped
374
+ `;
375
+
376
+ /** The compose project name; also used to recognise OUR own stack in detection. */
377
+ export const COMPOSE_PROJECT = 'deepdebug-observability';
378
+
379
+ /** Raw templated file map (relative path -> content, placeholders unresolved). */
380
+ export const BUNDLE_TEMPLATES = {
381
+ 'docker-compose.yml': DOCKER_COMPOSE,
382
+ 'loki/loki-config.yaml': LOKI_CONFIG,
383
+ // Loki's local ruler reads rules from <rules_dir>/<tenant>/; with
384
+ // auth_enabled=false the tenant is the literal "fake". A file placed directly
385
+ // under rules/ (no tenant subdir) is silently ignored and no alert ever fires.
386
+ 'loki/rules/fake/client.yaml': LOKI_RULE,
387
+ 'alloy/config.alloy': ALLOY_CONFIG,
388
+ 'alertmanager/alertmanager.yml': ALERTMANAGER_CONFIG,
389
+ 'grafana/provisioning/datasources/loki.yaml': GRAFANA_DATASOURCE,
390
+ 'grafana/provisioning/dashboards/provider.yaml': GRAFANA_DASH_PROVIDER,
391
+ 'grafana/dashboards/ddo-logs.json': DASHBOARD,
392
+ // Opt-in tunnel overlay (see renderBundle lokiTunnelToken).
393
+ 'caddy/Caddyfile': CADDY_CONFIG,
394
+ 'docker-compose.tunnel.yml': TUNNEL_COMPOSE,
395
+ };
396
+
397
+ /**
398
+ * Resolve the bundle for a given client. Pure: same inputs -> same file map.
399
+ * @param {{gatewayUrl:string, triggerSecret:string, environment?:string,
400
+ * lokiTunnelToken?:string}} opts
401
+ * @returns {Record<string,string>} relative path -> final file content
402
+ */
403
+ export function renderBundle({ gatewayUrl, triggerSecret, environment = 'production',
404
+ lokiTunnelToken = '' } = {}) {
405
+ if (!gatewayUrl || !triggerSecret) {
406
+ throw new Error('renderBundle requires gatewayUrl and triggerSecret');
407
+ }
408
+ const url = String(gatewayUrl).replace(/\/+$/, ''); // strip trailing slashes
409
+ const out = {};
410
+ for (const [path, tmpl] of Object.entries(BUNDLE_TEMPLATES)) {
411
+ out[path] = tmpl
412
+ .split('__GATEWAY_URL__').join(url)
413
+ .split('__TRIGGER_SECRET__').join(triggerSecret)
414
+ .split('__ENVIRONMENT__').join(environment)
415
+ .split('__LOKI_TUNNEL_TOKEN__').join(lokiTunnelToken);
416
+ }
417
+ return out;
418
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * observability/detect.js
3
+ *
4
+ * "Does this client already have observability?" If not, the agent offers to
5
+ * provision the DeepDebug bundle. The classifier is a PURE function over the
6
+ * host's running containers + processes so it can be unit-tested without Docker.
7
+ *
8
+ * We only ever recommend provisioning when we find NOTHING that already ships
9
+ * logs/metrics somewhere. Finding a real stack (Datadog, ELK, an existing
10
+ * Grafana, an otel-collector, ...) means the client has observability and we
11
+ * must NOT bulldoze it -- we report what we found and recommend 'skip'.
12
+ */
13
+
14
+ import { exec } from 'child_process';
15
+ import { promisify } from 'util';
16
+ import { COMPOSE_PROJECT } from './bundle-files.js';
17
+
18
+ const execAsync = promisify(exec);
19
+
20
+ /**
21
+ * Known observability signals. Matched (case-insensitively, as substrings)
22
+ * against container image/name AND process command lines. Deliberately broad:
23
+ * a false "they have it" is safe (we skip); a false "they have nothing" is not
24
+ * (we would deploy on top of an existing stack).
25
+ */
26
+ export const OBSERVABILITY_SIGNALS = [
27
+ // log shippers / agents
28
+ 'promtail', 'fluent-bit', 'fluentbit', 'fluentd', 'vector', 'filebeat',
29
+ 'logstash', 'otelcol', 'otel-collector', 'opentelemetry', 'telegraf',
30
+ // stores / backends
31
+ 'loki', 'elasticsearch', 'opensearch', 'victoriametrics', 'victoria-metrics',
32
+ 'prometheus', 'thanos', 'mimir', 'signoz', 'clickhouse-observability',
33
+ // UIs
34
+ 'grafana', 'kibana',
35
+ // SaaS agents
36
+ 'datadog', 'dd-agent', 'newrelic', 'new-relic', 'splunk', 'dynatrace',
37
+ 'sematext', 'sumologic', 'sumo-logic', 'instana',
38
+ ];
39
+
40
+ /**
41
+ * Classify whether the host already has observability.
42
+ *
43
+ * @param {{containers?: Array<{name?:string,image?:string,project?:string}>,
44
+ * processes?: string[]}} input
45
+ * @returns {{hasObservability:boolean, found:string[], ours:string[],
46
+ * recommendation:'provision'|'skip'}}
47
+ */
48
+ export function classifyObservability({ containers = [], processes = [] } = {}) {
49
+ const found = new Set();
50
+ const ours = new Set();
51
+
52
+ const scan = (haystack, sourceLabel) => {
53
+ if (!haystack) return;
54
+ const h = String(haystack).toLowerCase();
55
+ for (const sig of OBSERVABILITY_SIGNALS) {
56
+ if (h.includes(sig)) found.add(sig);
57
+ }
58
+ };
59
+
60
+ for (const c of containers) {
61
+ // Our own stack must never count as "the client already has observability".
62
+ if (c && c.project && c.project === COMPOSE_PROJECT) {
63
+ if (c.name) ours.add(c.name);
64
+ continue;
65
+ }
66
+ scan(c && c.name);
67
+ scan(c && c.image);
68
+ }
69
+ for (const p of processes) scan(p);
70
+
71
+ const hits = [...found];
72
+ return {
73
+ hasObservability: hits.length > 0,
74
+ found: hits.sort(),
75
+ ours: [...ours].sort(),
76
+ recommendation: hits.length > 0 ? 'skip' : 'provision',
77
+ };
78
+ }
79
+
80
+ /** True when OUR compose stack is already running on the host. */
81
+ export function ownStackRunning(containers = []) {
82
+ return containers.some((c) => c && c.project === COMPOSE_PROJECT);
83
+ }
84
+
85
+ /**
86
+ * Read running containers via `docker ps`. Returns [] (not throw) when Docker
87
+ * is absent/unreachable, so detection degrades to "process scan only".
88
+ */
89
+ export async function listContainers() {
90
+ try {
91
+ // Tab-separated so names/images with spaces stay parseable.
92
+ const fmt = '{{.Names}}\t{{.Image}}\t{{.Label "com.docker.compose.project"}}';
93
+ const { stdout } = await execAsync(`docker ps --no-trunc --format '${fmt}'`, { timeout: 15000 });
94
+ return stdout
95
+ .split('\n')
96
+ .map((l) => l.trim())
97
+ .filter(Boolean)
98
+ .map((line) => {
99
+ const [name = '', image = '', project = ''] = line.split('\t');
100
+ return { name, image, project };
101
+ });
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ /** Best-effort process list (Linux/macOS). Returns [] on failure. */
108
+ export async function listProcesses() {
109
+ try {
110
+ const { stdout } = await execAsync('ps -eo args=', { timeout: 10000 });
111
+ return stdout.split('\n').map((l) => l.trim()).filter(Boolean);
112
+ } catch {
113
+ return [];
114
+ }
115
+ }
116
+
117
+ /** Whether `docker` (and the compose plugin) are usable on this host. */
118
+ export async function dockerAvailable() {
119
+ try {
120
+ await execAsync('docker version --format "{{.Server.Version}}"', { timeout: 15000 });
121
+ } catch {
122
+ return { docker: false, compose: false };
123
+ }
124
+ let compose = false;
125
+ try {
126
+ await execAsync('docker compose version', { timeout: 15000 });
127
+ compose = true;
128
+ } catch {
129
+ compose = false;
130
+ }
131
+ return { docker: true, compose };
132
+ }
133
+
134
+ /**
135
+ * Full host observability probe: containers + processes + docker availability,
136
+ * classified. This is what the /observability/status route returns.
137
+ */
138
+ export async function detectObservability() {
139
+ const [containers, processes, docker] = await Promise.all([
140
+ listContainers(),
141
+ listProcesses(),
142
+ dockerAvailable(),
143
+ ]);
144
+ const classification = classifyObservability({ containers, processes });
145
+ return {
146
+ ...classification,
147
+ ownStackRunning: ownStackRunning(containers),
148
+ docker,
149
+ containersScanned: containers.length,
150
+ };
151
+ }