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,43 @@
1
+ # Releasing the local agent
2
+
3
+ Two artefacts are released: the npm package `deepdebug-local-agent` and the
4
+ public BYOC image `local-agent-full`. Nothing here runs on a normal push.
5
+
6
+ ## npm package
7
+
8
+ `.github/workflows/npm-publish.yml` runs on every push to `main` that touches
9
+ `src/**`, `package.json` or `bin/**`, and bumps the **patch** version
10
+ automatically (from the highest of the latest `v*` tag and `package.json`).
11
+ That is right for routine fixes and wrong for a minor release: with
12
+ `package.json` at `1.1.0` the automatic bump would publish `1.1.1`.
13
+
14
+ To publish an exact version (for example `1.1.0`):
15
+
16
+ 1. Leave `package.json` as it is. Do **not** bump it in a PR (a push to `main`
17
+ would trigger the automatic patch bump).
18
+ 2. Actions → **Publish NPM Package** → *Run workflow* on `main`, input
19
+ `version` = `1.1.0`. The workflow validates `X.Y.Z`, requires it to be
20
+ greater than the highest existing tag and `package.json`, runs
21
+ `npm version 1.1.0`, publishes with provenance, then pushes the bump commit
22
+ and the `v1.1.0` tag.
23
+ 3. Left empty, the input keeps the old behaviour (automatic patch bump).
24
+
25
+ Prerequisite (owner of the npm package): Trusted Publisher configured for
26
+ `VISIONHAMMER/deepdebug-local-agent`, workflow `npm-publish.yml` (see the
27
+ header of the workflow). Without it the publish step fails and no version is
28
+ burned.
29
+
30
+ Heads-up: merging any PR that changes `src/**` to `main` starts an automatic
31
+ patch publish (for example `1.0.39`) once the Trusted Publisher exists. To
32
+ release `1.1.0` as the next version, dispatch it before merging further
33
+ `src/**` changes, or merge with `[skip ci]` in the commit message.
34
+
35
+ ## BYOC image (`local-agent-full`)
36
+
37
+ Tags pushed by `npm-publish.yml` use `GITHUB_TOKEN` and do not trigger other
38
+ workflows, so after the npm release run Actions → **Build BYOC Agent Image**
39
+ on `main` with `version` = `1.1.0`
40
+ (see `docs/byoc-agent-image.md`). Tick `supply_chain` to also sign the image
41
+ (cosign keyless), attach the SBOM and the provenance, and verify them.
42
+ For `v*` tag and release runs the same job runs when the repository variable
43
+ `BYOC_SUPPLY_CHAIN` is `true`. Default: off.
@@ -0,0 +1,163 @@
1
+ # Signed and allow-listed commands — operator notes
2
+
3
+ The platform (vh-api-gateway, visionhammer-agents) signs every command it sends
4
+ to a local agent with an Ed25519 key, and the agent runs shell commands only
5
+ from an allowlist. Both checks have three modes, `off | warn | enforce`. The
6
+ default is `enforce` (signature: since 1.1.0; exec policy: since 1.0.35), and an
7
+ unrecognised value also means `enforce`. `warn` verifies and logs without
8
+ rejecting; use it only as an explicit, temporary opt-out
9
+ (`LOCAL_AGENT_SIGNATURE_MODE=warn`) for an agent whose platform side does not
10
+ sign yet. Without a listen address the agent also binds `127.0.0.1` unless a
11
+ token (`LOCAL_AGENT_TOKEN` / `AGENT_SHARED_SECRET`) or Cloud Run is present.
12
+
13
+ ## Protocol v1
14
+
15
+ HTTP headers on every platform → agent request:
16
+
17
+ | Header | Value |
18
+ |---|---|
19
+ | `X-VH-Key-Id` | id of the signing key, for example `vh-2026-09` |
20
+ | `X-VH-Timestamp` | unix epoch **seconds** |
21
+ | `X-VH-Nonce` | 16–128 chars `[A-Za-z0-9_-]`, unique per request |
22
+ | `X-VH-Tenant-Id` / `X-VH-Workspace-Id` | the tenant and workspace the platform acts for (`""` when unknown) |
23
+ | `X-VH-Signature` | base64 Ed25519 signature over the canonical string |
24
+
25
+ Canonical string (UTF-8, `\n`-joined, no trailing newline):
26
+
27
+ ```
28
+ VH-AGENT-SIG-V1
29
+ <METHOD upper-case>
30
+ <raw path + "?" + raw query, as sent>
31
+ <hex sha256 of the exact body bytes (sha256("") when empty)>
32
+ <tenantId>
33
+ <workspaceId>
34
+ <timestamp>
35
+ <nonce>
36
+ ```
37
+
38
+ Golden vector (all three implementations test against it): method `post`, path
39
+ `/workspace/exec?x=1`, body `{}`, tenant `t`, workspace `w`, timestamp
40
+ `1790000000`, nonce `abcdefghijklmnop` →
41
+ `VH-AGENT-SIG-V1\nPOST\n/workspace/exec?x=1\n44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\nt\nw\n1790000000\nabcdefghijklmnop`.
42
+
43
+ WebSocket tunnel: the envelope gains `paramsRaw` (the exact JSON the platform
44
+ signed) and `sig: {keyId, timestamp, nonce, tenantId, workspaceId, signature}`.
45
+ Method is `WS`, path is the command name, body is `paramsRaw`. A verified
46
+ envelope runs `JSON.parse(paramsRaw)`; the unsigned `params` copy is used only
47
+ by older agents, or in `warn`/`off` mode when the envelope is not signed.
48
+
49
+ The agent rejects (in `enforce`) with 401 when:
50
+
51
+ * the request has no signature, or the key id is unknown;
52
+ * the signature does not verify;
53
+ * `|now − timestamp| > 60 s`;
54
+ * the nonce was already used (in-memory LRU, TTL 120 s, 100k entries);
55
+ * the tenant does not match the agent's identity (`LOCAL_AGENT_TENANT_ID` →
56
+ `TENANT_ID` → `~/.deepdebug/config.json`). If the signed workspace is
57
+ non-empty, it must match `LOCAL_AGENT_WORKSPACE_ID` / `WORKSPACE_ID` (set
58
+ `LOCAL_AGENT_REQUIRE_WORKSPACE=true` to require it on every call). A shared
59
+ operator agent with no identity accepts any signed tenant.
60
+
61
+ `X-Agent-Token` (`LOCAL_AGENT_TOKEN`) is still checked first, as the second
62
+ factor. `GET /health` and `/metrics` stay public.
63
+
64
+ ## Exec allowlist
65
+
66
+ `/workspace/exec`, `/workspace/run`, `/mcp/execute-command` and
67
+ `/setup/run-command` accept a command only when every segment (`&&`, `||`,
68
+ `;`, `|`) belongs to one of these families: read-only git; package-manager
69
+ install, test, build or lint for an ecosystem detected in the workspace (npm,
70
+ yarn, pnpm, mvn/mvnw, gradle/gradlew, pip, poetry, uv, go, cargo, dotnet); test
71
+ runners; linters; file read/write inside the workspace root; or `cd` inside
72
+ the root. `$VAR`, `$(...)`, backticks, subshells, `&`, here-docs, process
73
+ substitution and paths outside the root are refused.
74
+
75
+ Hardening rules:
76
+
77
+ * **Wrappers are validated recursively.** `env`, `time`, `nice`, `timeout`,
78
+ `c8`, `nyc`, `npx`, `npm/pnpm exec`, `poetry run` and `uv run` wrap another
79
+ command, and that command must pass the same policy. `xargs` is refused.
80
+ Interpreter eval (`node -e/-p`, `python -c`, `sh/bash -c`) is never
81
+ allowed. Test runners refuse inline JSON config.
82
+ * **Paths are realpath-resolved.** Every non-flag token that is path-like or
83
+ exists on disk, every `--opt=value`, every redirect target and every
84
+ `cd`/`git -C` directory is resolved with symlinks followed and must stay
85
+ inside the root. An unquoted glob may use wildcards only in its last segment,
86
+ and none of the symlinks it can match may point outside.
87
+ * **Protected files.** A redirect, `tee`, `cp`, `mv`, `rm`, `touch`, `chmod`
88
+ or `sort -o` is refused on these paths: `.git/**`, hook dirs (`.husky`,
89
+ `.githooks`), and build/test config that executes code. That includes
90
+ `package.json`, `.npmrc`, lockfiles, `Makefile`, `pom.xml`, `*.gradle*`,
91
+ `gradle/wrapper`, `setup.py`, `pyproject.toml`, `conftest.py`, and
92
+ `*.config.js` for jest, vitest, eslint and similar tools. Those files change
93
+ only through the signed apply-patch tool.
94
+ * **No write-then-execute.** A command string that writes files may not run a
95
+ package-manager, test or lint tool in a later segment. The source of truth is
96
+ `src/utils/exec-policy.js`; the reason is returned in the 403 body.
97
+
98
+ Two exceptions:
99
+
100
+ 1. **Human approval (REQUIRE_HUMAN).** An `X-VH-Approval` header (or
101
+ `approvalToken` in the body) holding a PolicyGate token
102
+ `vhapr1.<keyId>.<b64url payload>.<b64url sig>`. It is signed with the same
103
+ platform key over `VH-HUMAN-APPROVAL-V1\n<keyId>\n<payload segment>`. The
104
+ token is bound to the SHA-256 of the exact command, the tenant and the
105
+ workspace. It lives at most 15 minutes and can be used once.
106
+ 2. **Platform-authored tools.** A body with `commandOrigin: "platform"` and a
107
+ known `tool` (`create_branch`, `git_commit`, `git_push`, `close_pr`,
108
+ `delete_branch`, `publish_scope`, `current_branch`). It is honoured only
109
+ when the request signature verified. The model's free-form `run_command`
110
+ never carries it.
111
+
112
+ ## Rollout
113
+
114
+ 1. **Generate the keypair** on a trusted workstation. Keep the private key
115
+ off disk after upload.
116
+ ```sh
117
+ openssl genpkey -algorithm ed25519 -out local-agent-signing-key.pem
118
+ openssl pkey -in local-agent-signing-key.pem -pubout -outform DER | base64 # SPKI, 60 chars
119
+ ```
120
+ 2. **Store the private key** in Secret Manager as `local-agent-signing-key`
121
+ (PKCS#8 PEM) in `visionhammer`, and grant `secretAccessor` to the gateway
122
+ and agent2 runtime service accounts.
123
+ ```sh
124
+ gcloud secrets create local-agent-signing-key --replication-policy=automatic --data-file=local-agent-signing-key.pem
125
+ ```
126
+ 3. **Mount it on the signers.** On gateway and software-engineer (agent2)
127
+ Cloud Run, set
128
+ `--set-secrets=LOCAL_AGENT_SIGNING_KEY=local-agent-signing-key:latest` and
129
+ `--update-env-vars=LOCAL_AGENT_SIGNING_KEY_ID=vh-2026-09`. Without the key
130
+ they log once and send unsigned requests, as before. Remember
131
+ `--vpc-egress=private-ranges-only` on `run services update`.
132
+ 4. **Give the agents the public key:**
133
+ * Image default: add `{"keyId":"vh-2026-09","publicKey":"<SPKI b64>"}` to
134
+ `src/config/trusted-signing-keys.json` and cut a new image.
135
+ * BYOC provisioning: set `LOCAL_AGENT_SIGNING_PUBLIC_KEYS=vh-2026-09=<SPKI b64>`
136
+ on the gateway. `CloudEngineerService` copies it, along with
137
+ `LOCAL_AGENT_SIGNATURE_MODE` and `LOCAL_AGENT_EXEC_POLICY_MODE`, into
138
+ every ACI/Cloud Run agent it provisions.
139
+ * Operator QA agent: set the same env on `deepdebug-local-agent-qa`.
140
+ 5. **Soak in `warn`.** Watch the logs for `[agent-sig] WARN` and
141
+ `[exec-policy] WARN`. The logs are rate-limited, one line per reason and
142
+ path per minute. Expect zero signature warnings from updated platform
143
+ callers. Every exec warning is a command that `enforce` would block.
144
+ 6. **Flip to enforce** per agent: `LOCAL_AGENT_SIGNATURE_MODE=enforce` once
145
+ the signature warnings are zero. Set the gateway env to `enforce` so new
146
+ BYOC agents start enforced. Roll back by setting `warn` again, with no
147
+ redeploy of the platform.
148
+
149
+ Since 1.0.35 (security review 2026-10-11) the **exec policy defaults to
150
+ `enforce`**: a command outside the allowlist is refused unless it carries a
151
+ PolicyGate approval token or is a signed platform tool command. Set
152
+ `LOCAL_AGENT_EXEC_POLICY_MODE=warn` explicitly on an agent that still needs
153
+ the soak. In the same release the HTTP perimeter stopped defaulting to
154
+ `compat`: `AGENT_SHARED_SECRET` is enforced as `X-Agent-Token`, and an agent
155
+ with no token outside Cloud Run only answers loopback callers (the reverse
156
+ WS tunnel). `LOCAL_AGENT_AUTH=compat` is the explicit, logged opt-out.
157
+
158
+ ### Key rotation
159
+
160
+ Add the new public key next to the old one (`kid1=...,kid2=...` / two file
161
+ entries) on every agent. Then switch `LOCAL_AGENT_SIGNING_KEY` and
162
+ `LOCAL_AGENT_SIGNING_KEY_ID` on the signers to the new version. After a day,
163
+ remove the old public key. Approval tokens use the same keys.
package/env.example CHANGED
@@ -39,3 +39,33 @@ LOG_LEVEL=info
39
39
 
40
40
  # For on-premise DeepDebug server
41
41
  # DEEPDEBUG_API_URL=https://deepdebug.internal.company.com
42
+
43
+ # ─── Security (audit 2026-09) ───
44
+ # LOCAL_AGENT_TOKEN: when set, every route except GET /health and /metrics
45
+ # requires the X-Agent-Token header. Set it only once every caller (gateway,
46
+ # agents) sends the header. AGENT_SHARED_SECRET (injected by the gateway on
47
+ # BYOC ACI agents) is enforced the same way when LOCAL_AGENT_TOKEN is not set.
48
+ # With neither (and outside Cloud Run) the agent is loopback-only: it binds
49
+ # 127.0.0.1 and refuses non-loopback callers. LOCAL_AGENT_BIND_HOST overrides.
50
+ # LOCAL_AGENT_TOKEN=
51
+ # Extra browser origins allowed by CORS (comma-separated, exact match).
52
+ # LOCAL_AGENT_CORS_ORIGINS=
53
+ # Re-enable /setup/run-command, /setup/install-tool, /setup/run-auth-command.
54
+ # LOCAL_AGENT_ENABLE_SETUP_COMMANDS=false
55
+ # LOCAL_AGENT_AUTH=loopback -> accept loopback callers only (laptop + WS tunnel)
56
+ # LOCAL_AGENT_AUTH=off -> emergency opt-out (NOT recommended)
57
+
58
+ # ─── Signed + allow-listed commands (docs/signed-commands.md) ───
59
+ # Trusted platform public keys (Ed25519), keyId=base64 (raw 32-byte or SPKI DER).
60
+ # Several entries = rotation window. The image default lives in
61
+ # src/config/trusted-signing-keys.json.
62
+ # LOCAL_AGENT_SIGNING_PUBLIC_KEYS=vh-2026-09=MCowBQYDK2VwAyEA...
63
+ # off | warn | enforce (default) — verify X-VH-Signature on every command.
64
+ # Set warn only as a loud, temporary opt-out for a platform that does not sign yet.
65
+ # LOCAL_AGENT_SIGNATURE_MODE=enforce
66
+ # off | warn | enforce (default) — shell allowlist on exec endpoints.
67
+ # LOCAL_AGENT_EXEC_POLICY_MODE=enforce
68
+ # Identity the signature must match (defaults: TENANT_ID / WORKSPACE_ID).
69
+ # LOCAL_AGENT_TENANT_ID=
70
+ # LOCAL_AGENT_WORKSPACE_ID=
71
+ # LOCAL_AGENT_REQUIRE_WORKSPACE=false
package/helm/Chart.yaml CHANGED
@@ -2,8 +2,8 @@ apiVersion: v2
2
2
  name: deepdebug-agent
3
3
  description: DeepDebug Local Agent for enterprise code debugging and analysis
4
4
  type: application
5
- version: 1.0.0
6
- appVersion: "1.0.0"
5
+ version: 1.1.0
6
+ appVersion: "1.1.0"
7
7
  keywords:
8
8
  - debugging
9
9
  - ai
@@ -38,14 +38,24 @@ spec:
38
38
  value: "production"
39
39
  - name: PORT
40
40
  value: "5055"
41
- - name: DEEPDEBUG_API_URL
41
+ # Names aligned to what src/server.js actually reads (server.js frozen):
42
+ # GATEWAY_URL (server.js:73), TENANT_ID (:7505), WORKSPACE_ROOT (:637).
43
+ - name: GATEWAY_URL
42
44
  value: {{ .Values.config.apiUrl | quote }}
43
- - name: DEEPDEBUG_TENANT_ID
45
+ - name: TENANT_ID
44
46
  valueFrom:
45
47
  secretKeyRef:
46
48
  name: {{ include "deepdebug-agent.fullname" . }}-secret
47
49
  key: tenant-id
48
- - name: DEEPDEBUG_WORKSPACE_PATH
50
+ # Without a token the agent is loopback-only (bind 127.0.0.1) and the
51
+ # kubelet probes would fail; with it, every route but /health and
52
+ # /metrics needs X-Agent-Token.
53
+ - name: LOCAL_AGENT_TOKEN
54
+ valueFrom:
55
+ secretKeyRef:
56
+ name: {{ include "deepdebug-agent.fullname" . }}-secret
57
+ key: agent-token
58
+ - name: WORKSPACE_ROOT
49
59
  value: {{ .Values.config.workspacePath | quote }}
50
60
  - name: LOG_LEVEL
51
61
  value: {{ .Values.config.logLevel | quote }}
@@ -0,0 +1,22 @@
1
+ {{- if .Values.networkPolicy.enabled }}
2
+ apiVersion: networking.k8s.io/v1
3
+ kind: NetworkPolicy
4
+ metadata:
5
+ name: {{ include "deepdebug-agent.fullname" . }}
6
+ labels:
7
+ {{- include "deepdebug-agent.labels" . | nindent 4 }}
8
+ spec:
9
+ podSelector:
10
+ matchLabels:
11
+ {{- include "deepdebug-agent.selectorLabels" . | nindent 6 }}
12
+ policyTypes:
13
+ - Ingress
14
+ {{- with .Values.networkPolicy.allowFrom }}
15
+ ingress:
16
+ - from:
17
+ {{- toYaml . | nindent 8 }}
18
+ ports:
19
+ - protocol: TCP
20
+ port: 5055
21
+ {{- end }}
22
+ {{- end }}
@@ -7,3 +7,4 @@ metadata:
7
7
  type: Opaque
8
8
  data:
9
9
  tenant-id: {{ .Values.config.tenantId | b64enc | quote }}
10
+ agent-token: {{ required "config.agentToken is required (LOCAL_AGENT_TOKEN: the agent refuses remote callers without it)" .Values.config.agentToken | b64enc | quote }}
package/helm/values.yaml CHANGED
@@ -27,6 +27,9 @@ replicaCount: 1
27
27
  # ─────────────────────────────────────────
28
28
  config:
29
29
  tenantId: "" # Required: Your tenant ID
30
+ # Required: shared secret for X-Agent-Token (LOCAL_AGENT_TOKEN). Pass it with
31
+ # --set config.agentToken=$(openssl rand -hex 32); never commit it.
32
+ agentToken: ""
30
33
  apiUrl: "https://api.deepdebug.ai"
31
34
  logLevel: "info"
32
35
 
@@ -117,20 +120,6 @@ readinessProbe:
117
120
  timeoutSeconds: 5
118
121
  failureThreshold: 3
119
122
 
120
- # ─────────────────────────────────────────
121
- # Network Policies (Enterprise)
122
- # ─────────────────────────────────────────
123
- networkPolicy:
124
- enabled: true
125
- # Allow egress only to DeepDebug API
126
- egressRules:
127
- - to:
128
- - ipBlock:
129
- cidr: 0.0.0.0/0
130
- ports:
131
- - protocol: TCP
132
- port: 443
133
-
134
123
  # ─────────────────────────────────────────
135
124
  # Pod Disruption Budget
136
125
  # ─────────────────────────────────────────
@@ -160,3 +149,13 @@ podAnnotations:
160
149
  prometheus.io/scrape: "true"
161
150
  prometheus.io/port: "5055"
162
151
  prometheus.io/path: "/metrics"
152
+
153
+ # ─────────────────────────────────────────
154
+ # Network Policy (security review 2026-10-11, C1)
155
+ # ─────────────────────────────────────────
156
+ # The agent reaches the platform over an OUTBOUND tunnel, so it needs no
157
+ # inbound traffic except kubelet probes. Default: deny all ingress except the
158
+ # sources listed in allowFrom (NetworkPolicy peers, e.g. a podSelector).
159
+ networkPolicy:
160
+ enabled: true
161
+ allowFrom: []
package/mcp-server.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  ListToolsRequestSchema,
7
7
  } from "@modelcontextprotocol/sdk/types.js";
8
8
  import path from "path";
9
+ import { createAuthMiddleware } from "./src/utils/agent-auth.js";
9
10
  import { spawn } from "child_process";
10
11
  import { exists, listRecursive, readFile, writeFile, stat } from "./fs-utils.js";
11
12
  import { detectProject, readText } from "./detectors.js";
@@ -454,6 +455,8 @@ function errorResponse(message) {
454
455
  export async function startMCPHttpServer(wsManager, port = 5056) {
455
456
  const express = (await import("express")).default;
456
457
  const app = express();
458
+ // SECURITY: execute_command runs `sh -c`; same auth perimeter as the main API.
459
+ app.use(createAuthMiddleware());
457
460
 
458
461
  const server = createMCPServer(wsManager);
459
462
 
@@ -522,7 +525,7 @@ export async function startMCPHttpServer(wsManager, port = 5056) {
522
525
  }
523
526
  });
524
527
 
525
- app.listen(port, () => {
528
+ app.listen(port, process.env.MCP_BIND_HOST || "127.0.0.1", () => {
526
529
  console.log(`🧠 DeepDebug MCP Server listening on http://localhost:${port}`);
527
530
  console.log(` SSE: http://localhost:${port}/sse`);
528
531
  console.log(` REST: http://localhost:${port}/tool/{toolName}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepdebug-local-agent",
3
- "version": "1.0.23",
3
+ "version": "1.1.0",
4
4
  "description": "DeepDebug Local Agent - AI-powered code debugging assistant",
5
5
  "private": false,
6
6
  "type": "module",
@@ -41,9 +41,14 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "pg": "^8.20.0",
43
43
  "pidusage": "^3.0.2",
44
+ "pino": "^10.3.1",
45
+ "prom-client": "^15.1.3",
44
46
  "properties-reader": "^2.3.0",
45
47
  "ssh2": "^1.16.0",
46
48
  "strip-ansi": "^7.1.0",
49
+ "tree-sitter": "^0.22.4",
50
+ "tree-sitter-swift": "^0.7.1",
51
+ "typescript": "^5.9.3",
47
52
  "unidiff": "^1.0.2",
48
53
  "ws": "^8.18.0"
49
54
  },
@@ -75,10 +80,10 @@
75
80
  "license": "Proprietary",
76
81
  "repository": {
77
82
  "type": "git",
78
- "url": "https://github.com/williambella/deepdebug-local-agent.git"
83
+ "url": "git+https://github.com/VISIONHAMMER/deepdebug-local-agent.git"
79
84
  },
80
- "homepage": "https://deepdebug.ai",
85
+ "homepage": "https://visionhammer.io",
81
86
  "bugs": {
82
- "url": "https://github.com/williambella/deepdebug-local-agent/issues"
87
+ "url": "https://github.com/VISIONHAMMER/deepdebug-local-agent/issues"
83
88
  }
84
89
  }
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env bash
2
+ # Upgrade an installed DeepDebug client observability bundle so that:
3
+ # 1. Alloy also pushes the Docker log stream to VisionHammer
4
+ # (OTLP/HTTP JSON -> <gateway>/api/v1/webhooks/logs/otlp, Bearer <trigger secret>),
5
+ # which feeds Live logs and the services list;
6
+ # 2. Alertmanager sends the trigger secret as a Bearer header instead of ?s= in the URL.
7
+ #
8
+ # Safe to re-run (idempotent). Backs up both files, validates the new configs with the
9
+ # same images the stack runs, restarts only alloy + alertmanager, and rolls back on failure.
10
+ #
11
+ # Usage (on the server, as a user that can run docker):
12
+ # sudo bash upgrade-observability-push.sh # apply
13
+ # sudo bash upgrade-observability-push.sh --dry-run # show what would change
14
+ # Optional env:
15
+ # INSTALL_DIR=/path/to/deepdebug-observability (auto-detected otherwise)
16
+ # GATEWAY_URL=https://api-qa.visionhammer.io (read from alertmanager.yml otherwise)
17
+ set -euo pipefail
18
+
19
+ DRY_RUN=0
20
+ [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
21
+
22
+ log() { printf '\033[1;34m[upgrade]\033[0m %s\n' "$*"; }
23
+ warn() { printf '\033[1;33m[upgrade]\033[0m %s\n' "$*" >&2; }
24
+ die() { printf '\033[1;31m[upgrade]\033[0m %s\n' "$*" >&2; exit 1; }
25
+ mask() { local s="$1"; printf '%s...%s' "${s:0:8}" "${s: -4}"; }
26
+
27
+ command -v docker >/dev/null || die "docker not found"
28
+ docker compose version >/dev/null 2>&1 || die "docker compose plugin not found"
29
+ command -v curl >/dev/null || die "curl not found"
30
+
31
+ # ---------------------------------------------------------------- locate the stack
32
+ find_install_dir() {
33
+ if [[ -n "${INSTALL_DIR:-}" ]]; then echo "$INSTALL_DIR"; return; fi
34
+ # 1) the running compose project
35
+ local cfg
36
+ cfg=$(docker compose ls --all --format json 2>/dev/null \
37
+ | tr '{' '\n' | grep '"Name":"deepdebug-observability"' \
38
+ | sed -n 's/.*"ConfigFiles":"\([^",]*\).*/\1/p' | head -1 || true)
39
+ if [[ -n "$cfg" && -f "$cfg" ]]; then dirname "$cfg"; return; fi
40
+ # 2) default provision paths
41
+ local d
42
+ for d in "$HOME/.deepdebug/observability/deepdebug-observability" \
43
+ /root/.deepdebug/observability/deepdebug-observability \
44
+ /home/*/.deepdebug/observability/deepdebug-observability; do
45
+ [[ -f "$d/docker-compose.yml" ]] && { echo "$d"; return; }
46
+ done
47
+ }
48
+
49
+ DIR=$(find_install_dir || true)
50
+ [[ -n "$DIR" && -f "$DIR/docker-compose.yml" ]] \
51
+ || die "observability bundle not found; set INSTALL_DIR=/path/to/deepdebug-observability"
52
+ ALLOY="$DIR/alloy/config.alloy"
53
+ AM="$DIR/alertmanager/alertmanager.yml"
54
+ [[ -f "$ALLOY" ]] || die "missing $ALLOY"
55
+ [[ -f "$AM" ]] || die "missing $AM"
56
+ log "bundle: $DIR"
57
+
58
+ # ---------------------------------------------------------------- secret + gateway
59
+ SECRET=$(grep -oE 'vht_[A-Za-z0-9._-]+' "$AM" | head -1 || true)
60
+ [[ -n "$SECRET" ]] || die "no vht_ trigger secret found in $AM"
61
+ if [[ -z "${GATEWAY_URL:-}" ]]; then
62
+ GATEWAY_URL=$(grep -oE "https?://[^/'\" ]+" "$AM" | head -1 || true)
63
+ fi
64
+ GATEWAY_URL="${GATEWAY_URL%/}"
65
+ [[ "$GATEWAY_URL" =~ ^https?://[A-Za-z0-9.:-]+$ ]] || die "could not determine GATEWAY_URL (set it explicitly)"
66
+ log "gateway: $GATEWAY_URL secret: $(mask "$SECRET")"
67
+ case "$GATEWAY_URL" in
68
+ https://api-qa.visionhammer.io|https://api.visionhammer.io) ;;
69
+ *) warn "gateway is not api-qa/api.visionhammer.io; the push endpoint only exists on the current gateway." ;;
70
+ esac
71
+
72
+ # ---------------------------------------------------------------- 1. probe the endpoint
73
+ # An empty OTLP batch authenticates and registers the SELF_HOSTED log source without
74
+ # writing any log line.
75
+ code=$(curl -sS -o /tmp/vh-otlp-probe.$$ -w '%{http_code}' -m 20 \
76
+ -X POST "$GATEWAY_URL/api/v1/webhooks/logs/otlp" \
77
+ -H "Authorization: Bearer $SECRET" -H 'Content-Type: application/json' \
78
+ --data '{"resourceLogs":[]}' || echo 000)
79
+ case "$code" in
80
+ 200) log "push endpoint OK (200)" ;;
81
+ 401) die "push endpoint rejected the secret (401): the trigger source is disabled or the secret changed" ;;
82
+ 404) die "push endpoint not deployed yet on $GATEWAY_URL (404); retry after the gateway deploy" ;;
83
+ *) die "push endpoint probe failed (HTTP $code): $(head -c 300 /tmp/vh-otlp-probe.$$ 2>/dev/null)" ;;
84
+ esac
85
+ rm -f /tmp/vh-otlp-probe.$$
86
+
87
+ # ---------------------------------------------------------------- 2. build new configs
88
+ TMP=$(mktemp -d)
89
+ trap 'rm -rf "$TMP"' EXIT
90
+ cp "$ALLOY" "$TMP/config.alloy"
91
+ cp "$AM" "$TMP/alertmanager.yml"
92
+
93
+ alloy_changed=0
94
+ if grep -q 'otelcol.exporter.otlphttp "visionhammer"' "$TMP/config.alloy"; then
95
+ log "alloy: push already configured"
96
+ else
97
+ # loki.process "default" must also forward to the OTLP receiver.
98
+ n=$(grep -c 'forward_to = \[loki.write.default.receiver\]' "$TMP/config.alloy" || true)
99
+ [[ "$n" == "1" ]] || die "alloy config does not look like the stock bundle (expected one 'forward_to = [loki.write.default.receiver]', found $n); edit $ALLOY by hand"
100
+ sed -i 's|forward_to = \[loki.write.default.receiver\]|forward_to = [loki.write.default.receiver, otelcol.receiver.loki.visionhammer.receiver]|' "$TMP/config.alloy"
101
+ cat >> "$TMP/config.alloy" <<EOF
102
+
103
+ // ---- push to VisionHammer: Loki entries -> OTLP logs (labels become attributes) ----
104
+ otelcol.receiver.loki "visionhammer" {
105
+ output {
106
+ logs = [otelcol.processor.batch.visionhammer.input]
107
+ }
108
+ }
109
+
110
+ otelcol.processor.batch "visionhammer" {
111
+ send_batch_size = 500
112
+ send_batch_max_size = 2000
113
+ timeout = "5s"
114
+ output {
115
+ logs = [otelcol.exporter.otlphttp.visionhammer.input]
116
+ }
117
+ }
118
+
119
+ otelcol.exporter.otlphttp "visionhammer" {
120
+ client {
121
+ endpoint = "$GATEWAY_URL"
122
+ compression = "gzip"
123
+ headers = { "Authorization" = "Bearer $SECRET" }
124
+ }
125
+ encoding = "json"
126
+ logs_endpoint = "$GATEWAY_URL/api/v1/webhooks/logs/otlp"
127
+ }
128
+ EOF
129
+ alloy_changed=1
130
+ log "alloy: push blocks added"
131
+ fi
132
+
133
+ am_changed=0
134
+ if grep -q 'webhooks/alertmanager?s=' "$TMP/alertmanager.yml"; then
135
+ # Drop ?s=... from the URL and add http_config.authorization at the same indent
136
+ # as the url key of that list item.
137
+ # Each receiver keeps ITS OWN secret (a bundle may post to QA and PROD gateways,
138
+ # each registered with a different vht_ secret).
139
+ awk '
140
+ /webhooks\/alertmanager\?s=/ {
141
+ match($0, /vht_[A-Za-z0-9._-]+/)
142
+ secret = substr($0, RSTART, RLENGTH)
143
+ line = $0
144
+ sub(/\?s=[^'"'"'" ]*/, "", line)
145
+ print line
146
+ match($0, /^[ ]*(- )?/)
147
+ ind = RLENGTH
148
+ pad = sprintf("%" ind "s", "")
149
+ print pad "http_config:"
150
+ print pad " authorization:"
151
+ print pad " type: Bearer"
152
+ print pad " credentials: '"'"'" secret "'"'"'"
153
+ next
154
+ }
155
+ { print }
156
+ ' "$TMP/alertmanager.yml" > "$TMP/alertmanager.yml.new"
157
+ mv "$TMP/alertmanager.yml.new" "$TMP/alertmanager.yml"
158
+ am_changed=1
159
+ log "alertmanager: secret moved from ?s= to Authorization header"
160
+ else
161
+ log "alertmanager: already uses header auth (or a custom config); left unchanged"
162
+ fi
163
+
164
+ if [[ $alloy_changed == 0 && $am_changed == 0 ]]; then
165
+ log "nothing to do"; exit 0
166
+ fi
167
+
168
+ # Mask every vht_ secret in printed output, not only the first one.
169
+ mask_all() { sed -E 's/(vht_[A-Za-z0-9]{4})[A-Za-z0-9._-]*([A-Za-z0-9._-]{4})/\1...\2/g'; }
170
+ show_diff() { diff -u "$1" "$2" | mask_all || true; }
171
+ [[ $alloy_changed == 1 ]] && show_diff "$ALLOY" "$TMP/config.alloy"
172
+ [[ $am_changed == 1 ]] && show_diff "$AM" "$TMP/alertmanager.yml"
173
+
174
+ if [[ $DRY_RUN == 1 ]]; then log "dry run: nothing written"; exit 0; fi
175
+
176
+ # ---------------------------------------------------------------- 3. validate with the stack's images
177
+ image_of() { grep -E "^\s*image:\s*.*$1" "$DIR/docker-compose.yml" | head -1 | awk '{print $2}'; }
178
+ ALLOY_IMG=$(image_of 'grafana/alloy'); ALLOY_IMG=${ALLOY_IMG:-grafana/alloy:v1.3.1}
179
+ AM_IMG=$(image_of 'prom/alertmanager'); AM_IMG=${AM_IMG:-prom/alertmanager:v0.27.0}
180
+
181
+ if [[ $alloy_changed == 1 ]]; then
182
+ docker run --rm -v "$TMP/config.alloy:/c/config.alloy:ro" "$ALLOY_IMG" fmt /c/config.alloy >/dev/null \
183
+ || die "new alloy config failed validation; nothing changed"
184
+ log "alloy config valid ($ALLOY_IMG)"
185
+ fi
186
+ if [[ $am_changed == 1 ]]; then
187
+ docker run --rm --entrypoint amtool -v "$TMP/alertmanager.yml:/c/am.yml:ro" "$AM_IMG" check-config /c/am.yml >/dev/null \
188
+ || die "new alertmanager config failed validation; nothing changed"
189
+ log "alertmanager config valid ($AM_IMG)"
190
+ fi
191
+
192
+ # ---------------------------------------------------------------- 4. apply + restart
193
+ STAMP=$(date +%Y%m%d-%H%M%S)
194
+ cp -p "$ALLOY" "$ALLOY.bak-$STAMP"
195
+ cp -p "$AM" "$AM.bak-$STAMP"
196
+ log "backups: *.bak-$STAMP"
197
+ # Overwrite in place (keeps owner/permissions and the bind-mounted inode semantics on restart).
198
+ cat "$TMP/config.alloy" > "$ALLOY"
199
+ cat "$TMP/alertmanager.yml" > "$AM"
200
+
201
+ rollback() {
202
+ warn "rolling back to *.bak-$STAMP"
203
+ cat "$ALLOY.bak-$STAMP" > "$ALLOY"
204
+ cat "$AM.bak-$STAMP" > "$AM"
205
+ (cd "$DIR" && docker compose restart alloy alertmanager >/dev/null 2>&1) || true
206
+ die "$1"
207
+ }
208
+
209
+ (cd "$DIR" && docker compose restart alloy alertmanager) || rollback "restart failed"
210
+
211
+ # ---------------------------------------------------------------- 5. verify
212
+ log "waiting 30s for alloy to ship its first batch..."
213
+ sleep 30
214
+ running=$(cd "$DIR" && docker compose ps --status running --services 2>/dev/null || true)
215
+ grep -qx alloy <<<"$running" || rollback "alloy is not running after the restart"
216
+ grep -qx alertmanager <<<"$running" || rollback "alertmanager is not running after the restart"
217
+
218
+ errs=$(cd "$DIR" && docker compose logs --since 40s alloy 2>&1 \
219
+ | grep -iE 'level=error|Exporting failed|permanent error' | grep -i 'visionhammer\|otlphttp' || true)
220
+ if [[ -n "$errs" ]]; then
221
+ warn "alloy reported export errors:"
222
+ mask_all <<<"$errs" | tail -5 >&2
223
+ rollback "alloy cannot push to $GATEWAY_URL"
224
+ fi
225
+
226
+ log "done. Logs should appear in VisionHammer > Live logs within ~1-2 minutes."
227
+ log "to undo: cp $ALLOY.bak-$STAMP $ALLOY; cp $AM.bak-$STAMP $AM; (cd $DIR && docker compose restart alloy alertmanager)"