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,89 @@
1
+ /**
2
+ * Local burst suppressor (queue-admission burst control).
3
+ *
4
+ * NOT deduplication. The gateway performs the authoritative dedup on ITS hash
5
+ * (error message + first stacktrace line). The adapter 6-key event carries no
6
+ * stacktrace (message + file only), so that hash is not computable agent-side —
7
+ * we deliberately do NOT approximate it. Instead this is a short-window burst
8
+ * limiter that collapses a log storm of the SAME error into one forward, while
9
+ * letting the error recur ACROSS windows (that recurrence is the signal the
10
+ * gateway needs to promote a KB entry unverified -> verified).
11
+ *
12
+ * The admission key is a SHA-256 signature over adapter | normalize(message) |
13
+ * file. It is never sent to the gateway, never persisted, and is deliberately
14
+ * never called "hash" anywhere (that word is reserved for the gateway's dedup
15
+ * key). Normalization is essential: without it, rotating ids make every storm
16
+ * line unique and nothing is suppressed.
17
+ */
18
+ import crypto from 'crypto';
19
+
20
+ /**
21
+ * Replace volatile tokens with fixed placeholders BEFORE signing, so repeats of
22
+ * the same logical error collapse to one signature.
23
+ */
24
+ export function normalize(message) {
25
+ let s = String(message == null ? '' : message);
26
+ // ISO-8601 timestamps (with optional millis + Z/offset)
27
+ s = s.replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g, '#TS');
28
+ // UUIDs
29
+ s = s.replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, '#UUID');
30
+ // 0x addresses
31
+ s = s.replace(/0x[0-9a-fA-F]+/g, '#ADDR');
32
+ // Path segments that look like identifiers (contain a digit or a long hex run)
33
+ s = s.replace(/\/[A-Za-z0-9._-]*(?:\d|[0-9a-fA-F]{8,})[A-Za-z0-9._-]*/g, '/#SEG');
34
+ // Standalone hex runs >= 8
35
+ s = s.replace(/\b[0-9a-fA-F]{8,}\b/g, '#HEX');
36
+ // Digit runs >= 4
37
+ s = s.replace(/\d{4,}/g, '#NUM');
38
+ return s;
39
+ }
40
+
41
+ /** SHA-256 signature over adapter | normalize(message) | file. */
42
+ export function suppressionKey(evt) {
43
+ const adapter = evt && evt.adapter != null ? String(evt.adapter) : '';
44
+ const file = evt && evt.file != null ? String(evt.file) : '';
45
+ const msg = normalize(evt && evt.message);
46
+ return crypto.createHash('sha256').update(`${adapter}|${msg}|${file}`).digest('hex');
47
+ }
48
+
49
+ export class BurstSuppressor {
50
+ constructor(opts = {}) {
51
+ this.ttlMs = opts.ttlMs || Number(process.env.ADAPTER_SUPPRESS_TTL_MS) || 45000;
52
+ this.maxKeys = opts.maxKeys || 5000;
53
+ this.now = opts.now || (() => Date.now());
54
+ // key -> admission timestamp. Map preserves insertion order (LRU).
55
+ this.seen = new Map();
56
+ }
57
+
58
+ /**
59
+ * @returns {boolean} true if the event is within an active window for its
60
+ * signature and should be SUPPRESSED; false if it passes (and is recorded).
61
+ * The stored timestamp is the ADMISSION time and is NOT refreshed on
62
+ * suppressed hits, so the same error recurs (passes) once the TTL elapses.
63
+ */
64
+ shouldSuppress(evt) {
65
+ const key = suppressionKey(evt);
66
+ const t = this.now();
67
+ const first = this.seen.get(key);
68
+ if (first !== undefined && (t - first) < this.ttlMs) {
69
+ // Keep the admission timestamp; only refresh LRU recency.
70
+ this.seen.delete(key);
71
+ this.seen.set(key, first);
72
+ return true;
73
+ }
74
+ // New admission (or window expired): record and pass.
75
+ this.seen.delete(key);
76
+ this.seen.set(key, t);
77
+ this._evict();
78
+ return false;
79
+ }
80
+
81
+ _evict() {
82
+ while (this.seen.size > this.maxKeys) {
83
+ const oldest = this.seen.keys().next().value;
84
+ this.seen.delete(oldest);
85
+ }
86
+ }
87
+ }
88
+
89
+ export default BurstSuppressor;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Boot-time capability preflight (sibling module).
3
+ *
4
+ * Reuses the existing (previously dead) EnvironmentDiagnostics (host toolchain
5
+ * probe) and RuntimeRegistry (registered build runtimes) to emit a capability
6
+ * manifest and derive EXACTLY ONE operating mode:
7
+ *
8
+ * FULL_LOCAL source on disk AND host can build AND test locally.
9
+ * SPLIT_PLANE partial: can do some work (clone/detect/partial build) and
10
+ * offload the rest (git present, or some toolchain, or egress).
11
+ * DETECT_ONLY can only observe/detect — OR detection was inconclusive.
12
+ *
13
+ * Safety rule: never assume FULL_LOCAL. If detection is inconclusive we degrade
14
+ * to DETECT_ONLY and record why.
15
+ *
16
+ * buildManifest/deriveMode are pure and injectable so the mode logic is unit
17
+ * tested against fixtures without shelling out.
18
+ */
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import crypto from 'crypto';
22
+ import { EnvironmentDiagnostics } from '../utils/environment-diagnostics.js';
23
+ import { getRuntimeRegistry, initializeRuntimes } from '../runtimes/runtime-registry.js';
24
+ import { incidentForwarder } from './incident-forwarder.js';
25
+ import { logger } from './logger.js';
26
+
27
+ const TOOLCHAIN_LABEL = { python: 'python3' };
28
+
29
+ /** Derive exactly one mode from an assembled manifest (+ hasGit hint). Pure. */
30
+ export function deriveMode(m) {
31
+ if (m.conclusive === false) {
32
+ return {
33
+ mode: 'DETECT_ONLY',
34
+ reason: 'capability detection inconclusive; refusing to assume FULL_LOCAL',
35
+ };
36
+ }
37
+ if (m.source_available && m.build_capable && m.test_capable) {
38
+ return { mode: 'FULL_LOCAL', reason: 'source present and host can build + test locally' };
39
+ }
40
+ const canOffload = m.hasGit || m.build_capable || (m.egress && m.egress.gateway);
41
+ if (canOffload) {
42
+ const why = [];
43
+ if (!m.source_available) why.push('no source working tree on disk');
44
+ if (!m.build_capable) why.push('host not fully build-capable');
45
+ else if (!m.test_capable) why.push('host not test-capable');
46
+ return {
47
+ mode: 'SPLIT_PLANE',
48
+ reason: `partial local capability (${why.join(', ') || 'offload available'})`,
49
+ };
50
+ }
51
+ return { mode: 'DETECT_ONLY', reason: 'no source, no build toolchain, no git/egress to offload' };
52
+ }
53
+
54
+ /** Assemble the manifest from raw inputs and stamp the derived mode. Pure. */
55
+ export function buildManifest({
56
+ diag, sourceAvailable, dockerSock, containerRuntime, egress, runtimeIds, conclusive,
57
+ } = {}) {
58
+ const tools = (diag && diag.tools) || {};
59
+ const has = (t) => Boolean(tools[t] && tools[t].installed);
60
+ const toolchains = Object.keys(tools).filter(has).map((t) => TOOLCHAIN_LABEL[t] || t);
61
+
62
+ const buildCapable =
63
+ (has('java') && (has('maven') || has('gradle'))) ||
64
+ (has('node') && has('npm')) ||
65
+ has('go') || has('dotnet') ||
66
+ (has('python') && has('pip'));
67
+ // Test capability is narrower: python needs a runner we do not probe, so be
68
+ // conservative and tie it to the JVM/node/go/dotnet chains.
69
+ const testCapable =
70
+ (has('java') && (has('maven') || has('gradle'))) ||
71
+ (has('node') && has('npm')) ||
72
+ has('go') || has('dotnet');
73
+
74
+ const manifest = {
75
+ source_available: Boolean(sourceAvailable),
76
+ toolchains,
77
+ container_runtime: containerRuntime || null,
78
+ docker_sock: Boolean(dockerSock),
79
+ egress: egress || {},
80
+ build_capable: buildCapable,
81
+ test_capable: testCapable,
82
+ runtimes: runtimeIds || [],
83
+ conclusive: conclusive !== false,
84
+ };
85
+ const { mode, reason } = deriveMode({ ...manifest, hasGit: has('git') });
86
+ manifest.mode = mode;
87
+ manifest.reason = reason;
88
+ return manifest;
89
+ }
90
+
91
+ /** Is there a git working tree on disk at workspaceRoot? */
92
+ function probeSource(workspaceRoot) {
93
+ try {
94
+ if (!workspaceRoot) return false;
95
+ return fs.existsSync(workspaceRoot) && fs.existsSync(path.join(workspaceRoot, '.git'));
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ function probeDockerSock() {
102
+ try { return fs.existsSync('/var/run/docker.sock'); } catch { return false; }
103
+ }
104
+
105
+ function probeContainerRuntime() {
106
+ try {
107
+ if (fs.existsSync('/.dockerenv')) return 'docker';
108
+ const cg = fs.readFileSync('/proc/1/cgroup', 'utf-8');
109
+ if (/docker/.test(cg)) return 'docker';
110
+ if (/containerd/.test(cg)) return 'containerd';
111
+ if (/kubepods/.test(cg)) return 'kubernetes';
112
+ return null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ async function reachable(url, timeoutMs = 2500) {
119
+ if (!url) return false;
120
+ try {
121
+ const ctrl = new AbortController();
122
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
123
+ try {
124
+ await fetch(url, { method: 'HEAD', signal: ctrl.signal });
125
+ return true; // any HTTP response means egress works
126
+ } finally {
127
+ clearTimeout(timer);
128
+ }
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ async function probeEgress(gatewayUrl) {
135
+ const [gateway, github] = await Promise.all([
136
+ reachable(gatewayUrl),
137
+ reachable('https://github.com'),
138
+ ]);
139
+ return { gateway, github };
140
+ }
141
+
142
+ /**
143
+ * Run the real preflight (host probes) and produce the manifest. All probe
144
+ * inputs are injectable via `probes` for tests.
145
+ */
146
+ export async function computeCapabilityManifest({
147
+ workspaceRoot, gatewayUrl, diagnostics, runtimeRegistry, probes,
148
+ } = {}) {
149
+ const diagInst = diagnostics || new EnvironmentDiagnostics();
150
+ let diag;
151
+ let conclusive = true;
152
+ try {
153
+ diag = await diagInst.diagnose();
154
+ if (!diag || !diag.tools) conclusive = false;
155
+ } catch (err) {
156
+ diag = { tools: {} };
157
+ conclusive = false;
158
+ logger.warn({ err: err.message }, '[capabilities] diagnostics failed; degrading to DETECT_ONLY');
159
+ }
160
+
161
+ let runtimeIds = [];
162
+ try {
163
+ const reg = runtimeRegistry || getRuntimeRegistry();
164
+ if (reg.runtimes && reg.runtimes.size === 0) await initializeRuntimes();
165
+ const reg2 = runtimeRegistry || getRuntimeRegistry();
166
+ runtimeIds = reg2.runtimes ? [...reg2.runtimes.keys()] : [];
167
+ } catch (err) {
168
+ logger.warn({ err: err.message }, '[capabilities] runtime registry init failed');
169
+ }
170
+
171
+ const p = probes || {};
172
+ const sourceAvailable = p.sourceAvailable != null ? p.sourceAvailable : probeSource(workspaceRoot);
173
+ const dockerSock = p.dockerSock != null ? p.dockerSock : probeDockerSock();
174
+ const containerRuntime = p.containerRuntime !== undefined ? p.containerRuntime : probeContainerRuntime();
175
+ const egress = p.egress != null ? p.egress : await probeEgress(gatewayUrl);
176
+
177
+ return buildManifest({ diag, sourceAvailable, dockerSock, containerRuntime, egress, runtimeIds, conclusive });
178
+ }
179
+
180
+ /** Constant-time bearer check against the configured agent API key. */
181
+ export function isAuthorized(req, authToken) {
182
+ if (!authToken) return false; // fail closed: no key configured => never expose inventory
183
+ const h = req && req.headers ? (req.headers.authorization || req.headers.Authorization) : null;
184
+ if (!h || !/^Bearer\s+/i.test(h)) return false;
185
+ const presented = h.replace(/^Bearer\s+/i, '').trim();
186
+ const a = Buffer.from(presented);
187
+ const b = Buffer.from(String(authToken));
188
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
189
+ }
190
+
191
+ /**
192
+ * UNAUTHENTICATED view: operational health only. Whoever runs the box must be
193
+ * able to see the agent is broken (mode + the C1 contract signal) WITHOUT holding
194
+ * credentials. No host/topology inventory.
195
+ */
196
+ export function publicCapabilities(manifest, forwarderStatus, uptimeS) {
197
+ const cp = forwarderStatus && forwarderStatus.contract_preflight;
198
+ return {
199
+ authenticated: false,
200
+ mode: manifest.mode,
201
+ reason: manifest.reason,
202
+ contract: {
203
+ ok: forwarderStatus ? forwarderStatus.contract_ok : null,
204
+ preflight_reason: cp ? cp.reason : null, // coarse: e.g. missing_internal_token / auth_rejected
205
+ },
206
+ uptime_s: uptimeS,
207
+ };
208
+ }
209
+
210
+ /** AUTHENTICATED view: adds the client-infra topology inventory. */
211
+ export function fullCapabilities(manifest, forwarderStatus, uptimeS) {
212
+ return {
213
+ ...publicCapabilities(manifest, forwarderStatus, uptimeS),
214
+ authenticated: true,
215
+ source_available: manifest.source_available,
216
+ toolchains: manifest.toolchains,
217
+ container_runtime: manifest.container_runtime,
218
+ docker_sock: manifest.docker_sock,
219
+ egress: manifest.egress,
220
+ build_capable: manifest.build_capable,
221
+ test_capable: manifest.test_capable,
222
+ runtimes: manifest.runtimes,
223
+ forwarder: forwarderStatus,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Compute the manifest, log the derived mode at startup, and expose it on
229
+ * GET /capabilities (registered on the main app — same 5055 process).
230
+ */
231
+ export async function startCapabilityPreflight(app, { metrics, workspaceRoot, gatewayUrl, authToken } = {}) {
232
+ let manifest;
233
+ try {
234
+ manifest = await computeCapabilityManifest({ workspaceRoot, gatewayUrl });
235
+ } catch (err) {
236
+ // Absolute backstop: never crash, never assume FULL_LOCAL.
237
+ manifest = buildManifest({ diag: { tools: {} }, conclusive: false, egress: {} });
238
+ logger.error({ err: err.message }, '[capabilities] preflight crashed; forcing DETECT_ONLY');
239
+ }
240
+
241
+ logger.info(
242
+ {
243
+ mode: manifest.mode,
244
+ reason: manifest.reason,
245
+ source_available: manifest.source_available,
246
+ toolchains: manifest.toolchains,
247
+ build_capable: manifest.build_capable,
248
+ test_capable: manifest.test_capable,
249
+ container_runtime: manifest.container_runtime,
250
+ docker_sock: manifest.docker_sock,
251
+ egress: manifest.egress,
252
+ },
253
+ `[capabilities] derived mode = ${manifest.mode}`,
254
+ );
255
+
256
+ if (app && typeof app.get === 'function') {
257
+ // Built per-request: live forwarder/contract status (B2) + access control
258
+ // (B4). Unauthenticated callers see health only; the agent API key unlocks
259
+ // the client-infra inventory.
260
+ app.get('/capabilities', (req, res) => {
261
+ const status = incidentForwarder.getStatus();
262
+ const uptimeS = Math.round(process.uptime());
263
+ const view = isAuthorized(req, authToken)
264
+ ? fullCapabilities(manifest, status, uptimeS)
265
+ : publicCapabilities(manifest, status, uptimeS);
266
+ res.json(view);
267
+ });
268
+ }
269
+ if (metrics && typeof metrics.setMode === 'function') {
270
+ metrics.setMode(manifest.mode);
271
+ }
272
+ return manifest;
273
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * env-compat preload (CommonJS).
3
+ *
4
+ * Maps legacy manifest env names to the canonical names src/server.js actually
5
+ * reads, ONLY when the canonical is unset. This must run BEFORE server.js is
6
+ * evaluated because the canonical names are read at module-eval time
7
+ * (GATEWAY_URL at server.js:73, WORKSPACE_ROOT at :637). server.js stays frozen.
8
+ *
9
+ * Wiring: NODE_OPTIONS="--require .../env-compat.cjs" (set in the Dockerfiles).
10
+ * Chosen over editing the Docker CMD/entrypoint because:
11
+ * - --require runs before the entry module's evaluation (satisfies the
12
+ * "before module-eval" requirement) without changing how the app is launched;
13
+ * - it is CommonJS, which --require loads directly (ESM needs --import + is
14
+ * evaluated later), so the mapping is guaranteed in place first;
15
+ * - it composes with `node src/server.js`, `npm start`, and the pkg binaries
16
+ * alike — no per-entrypoint edits.
17
+ *
18
+ * Backward-compatible: existing deployments that still set the legacy names keep
19
+ * working (with a deprecation warning); new deployments set the canonical names
20
+ * and this is a no-op.
21
+ */
22
+ 'use strict';
23
+
24
+ const LEGACY_MAP = Object.freeze([
25
+ ['DEEPDEBUG_API_URL', 'GATEWAY_URL'],
26
+ ['DEEPDEBUG_TENANT_ID', 'TENANT_ID'],
27
+ ['DEEPDEBUG_WORKSPACE_PATH', 'WORKSPACE_ROOT'],
28
+ ]);
29
+
30
+ const isSet = (v) => v !== undefined && v !== null && v !== '';
31
+
32
+ /**
33
+ * Apply the legacy->canonical mapping to `env`. For each pair, map only when the
34
+ * canonical is unset and the legacy is set; emit exactly one deprecation warning
35
+ * per mapping actually applied (naming the legacy variable). Pure w.r.t. the
36
+ * passed-in env + warn function, so it is unit-testable without a subprocess.
37
+ *
38
+ * @returns {string[]} the canonical names that were populated from a legacy var.
39
+ */
40
+ function applyEnvCompat(env, warn) {
41
+ const mapped = [];
42
+ for (const [legacy, canonical] of LEGACY_MAP) {
43
+ if (!isSet(env[canonical]) && isSet(env[legacy])) {
44
+ env[canonical] = env[legacy];
45
+ mapped.push(canonical);
46
+ if (typeof warn === 'function') {
47
+ warn(`[env-compat] DEPRECATION: ${legacy} is deprecated and will be removed; `
48
+ + `use ${canonical}. Mapped ${legacy} -> ${canonical} for this run.\n`);
49
+ }
50
+ }
51
+ }
52
+ return mapped;
53
+ }
54
+
55
+ // Auto-apply to the real environment when preloaded (--require) or required.
56
+ applyEnvCompat(process.env, (msg) => process.stderr.write(msg));
57
+
58
+ module.exports = { applyEnvCompat, LEGACY_MAP };