deepdebug-local-agent 1.0.22 → 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 (188) 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/README.md +81 -0
  26. package/bin/install.js +16 -5
  27. package/cloudbuild-full.yaml +57 -0
  28. package/docker-compose.yml +5 -3
  29. package/docs/OBSERVABILITY.md +164 -0
  30. package/docs/byoc-agent-image.md +70 -0
  31. package/docs/graphify-packaging-plan.md +133 -0
  32. package/docs/graphify-validation-run-2026-10-28.md +243 -0
  33. package/docs/legacy-connectors.md +268 -0
  34. package/docs/releasing.md +43 -0
  35. package/docs/signed-commands.md +163 -0
  36. package/env.example +30 -0
  37. package/helm/Chart.yaml +2 -2
  38. package/helm/templates/deployment.yaml +13 -3
  39. package/helm/templates/networkpolicy.yaml +22 -0
  40. package/helm/templates/secret.yaml +1 -0
  41. package/helm/values.yaml +13 -14
  42. package/mcp-server.js +4 -1
  43. package/package.json +12 -4
  44. package/scripts/sync-fixtures.mjs +246 -0
  45. package/scripts/upgrade-observability-push.sh +227 -0
  46. package/src/adapters/as400/as400-parser.js +12 -0
  47. package/src/adapters/as400/index.js +134 -0
  48. package/src/adapters/cobol/cobol-parser.js +37 -0
  49. package/src/adapters/cobol/index.js +151 -0
  50. package/src/adapters/ibmmq/ibmmq-parser.js +23 -0
  51. package/src/adapters/ibmmq/index.js +137 -0
  52. package/src/adapters/index.js +79 -0
  53. package/src/adapters/oracle-forms/index.js +145 -0
  54. package/src/adapters/oracle-forms/oracle-forms-parser.js +11 -0
  55. package/src/adapters/sap/index.js +146 -0
  56. package/src/adapters/sap/sap-parser.js +41 -0
  57. package/src/ai-engine.js +22 -22
  58. package/src/config/trusted-signing-keys.json +3 -0
  59. package/src/detectors/build-tool-detector.js +50 -0
  60. package/src/detectors/multi-project-detector.js +22 -22
  61. package/src/detectors/service-detector.js +24 -24
  62. package/src/exec-utils.js +26 -4
  63. package/src/git/base-git-provider.js +117 -117
  64. package/src/git/bitbucket-provider.js +2 -2
  65. package/src/git/git-provider-registry.js +18 -18
  66. package/src/git/github-provider.js +13 -13
  67. package/src/git/multi-repo-pr.js +161 -0
  68. package/src/mcp-http-server.js +32 -16
  69. package/src/observability/bundle-files.js +418 -0
  70. package/src/observability/detect.js +151 -0
  71. package/src/observability/provision.js +212 -0
  72. package/src/observability/render-cli.js +59 -0
  73. package/src/observability/routes.js +88 -0
  74. package/src/patch/patch-engine.js +28 -28
  75. package/src/patch-manager.js +85 -85
  76. package/src/patch_bkp.js +14 -14
  77. package/src/ports.js +4 -4
  78. package/src/routes/workspace.route.js +47 -16
  79. package/src/runtimes/base-runtime.js +75 -75
  80. package/src/runtimes/java/gradle-runtime.js +17 -17
  81. package/src/runtimes/java/java-integrations.js +25 -25
  82. package/src/runtimes/java/maven-runtime.js +34 -34
  83. package/src/runtimes/node/node-integrations.js +1 -1
  84. package/src/runtimes/node/npm-runtime.js +14 -14
  85. package/src/runtimes/node/yarn-runtime.js +4 -4
  86. package/src/runtimes/runtime-registry.js +25 -25
  87. package/src/server-local.js +4 -4
  88. package/src/server.js +1412 -335
  89. package/src/siblings/burst-suppressor.js +89 -0
  90. package/src/siblings/capabilities.js +273 -0
  91. package/src/siblings/env-compat.cjs +58 -0
  92. package/src/siblings/incident-forwarder.js +320 -0
  93. package/src/siblings/logger.js +15 -0
  94. package/src/siblings/metrics.js +94 -0
  95. package/src/utils/agent-auth.js +184 -0
  96. package/src/utils/approval-token.js +56 -0
  97. package/src/utils/bind-host.js +29 -0
  98. package/src/utils/branch-refspec.js +98 -0
  99. package/src/utils/child-env.js +34 -0
  100. package/src/utils/environment-diagnostics.js +55 -55
  101. package/src/utils/exec-policy.js +962 -0
  102. package/src/utils/exec-utils.js +15 -15
  103. package/src/utils/file-count.js +46 -0
  104. package/src/utils/fs-utils.js +10 -10
  105. package/src/utils/multi-repo-open.js +132 -0
  106. package/src/utils/open-gate.js +183 -0
  107. package/src/utils/ranged-read.js +65 -0
  108. package/src/utils/request-guards.js +77 -0
  109. package/src/utils/request-signing.js +342 -0
  110. package/src/utils/root-recovery.js +244 -0
  111. package/src/utils/safe-exec-args.js +58 -0
  112. package/src/utils/sandbox.js +182 -0
  113. package/src/utils/search-outcome.js +56 -0
  114. package/src/utils/workspace-root.js +80 -0
  115. package/src/validators/syntax-validator.js +149 -0
  116. package/src/workspace/detect-port.js +16 -16
  117. package/src/workspace/file-reader.js +1 -1
  118. package/src/workspace/process-manager.js +46 -46
  119. package/src/workspace-manager.js +86 -24
  120. package/test/adapters/as400/as400.test.js +121 -0
  121. package/test/adapters/cobol/cobol.test.js +217 -0
  122. package/test/adapters/ibmmq/ibmmq.test.js +122 -0
  123. package/test/adapters/oracle-forms/oracle-forms.test.js +180 -0
  124. package/test/adapters/registry.test.js +108 -0
  125. package/test/adapters/sap/sap.test.js +142 -0
  126. package/test/agent-auth.test.js +194 -0
  127. package/test/bind-host.test.js +26 -0
  128. package/test/branch-refspec.test.js +224 -0
  129. package/test/burst-suppressor.test.js +60 -0
  130. package/test/capabilities-access.test.js +47 -0
  131. package/test/capabilities.test.js +87 -0
  132. package/test/child-env.test.js +23 -0
  133. package/test/env-compat.test.js +71 -0
  134. package/test/exec-policy.test.js +463 -0
  135. package/test/file-count.test.js +64 -0
  136. package/test/incident-forwarder.test.js +231 -0
  137. package/test/multi-repo-open.test.js +180 -0
  138. package/test/multi-repo-pr.test.js +129 -0
  139. package/test/observability-detect.test.js +199 -0
  140. package/test/open-gate.test.js +341 -0
  141. package/test/ranged-read.test.js +60 -0
  142. package/test/request-guards.test.js +91 -0
  143. package/test/request-signing.test.js +284 -0
  144. package/test/root-recovery-sidecar.test.js +171 -0
  145. package/test/root-recovery.test.js +274 -0
  146. package/test/safe-exec-args.test.js +58 -0
  147. package/test/sandbox.test.js +98 -0
  148. package/test/search-outcome.test.js +50 -0
  149. package/test/workspace-root.test.js +64 -0
  150. package/tools/graphify-validation/README.md +92 -0
  151. package/tools/graphify-validation/config/questions.json +31 -0
  152. package/tools/graphify-validation/config/repos.json +9 -0
  153. package/tools/graphify-validation/config/symbols.json +6 -0
  154. package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
  155. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
  156. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
  157. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
  158. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
  159. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
  160. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
  161. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
  162. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
  163. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
  164. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
  165. package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
  166. package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
  167. package/tools/graphify-validation/lib/graph_metrics.py +96 -0
  168. package/tools/graphify-validation/lib/gv_common.py +130 -0
  169. package/tools/graphify-validation/lib/licenses.py +47 -0
  170. package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
  171. package/tools/graphify-validation/lib/recall.py +115 -0
  172. package/tools/graphify-validation/lib/report.py +226 -0
  173. package/tools/graphify-validation/lib/run_all.py +237 -0
  174. package/tools/graphify-validation/lib/run_graphify.py +144 -0
  175. package/tools/graphify-validation/lib/rusage_helper.py +17 -0
  176. package/tools/graphify-validation/lib/sample_edges.py +246 -0
  177. package/tools/graphify-validation/lib/spring_fixture.py +81 -0
  178. package/tools/graphify-validation/requirements-py311.lock +508 -0
  179. package/tools/graphify-validation/requirements-tools.in +1 -0
  180. package/tools/graphify-validation/requirements-tools.lock +371 -0
  181. package/tools/graphify-validation/requirements.in +1 -0
  182. package/tools/graphify-validation/requirements.lock +502 -0
  183. package/tools/graphify-validation/run.sh +43 -0
  184. package/tools/graphify-validation/tests/test_harness.py +151 -0
  185. package/cloudbuild-agent-qa.yaml +0 -43
  186. package/cloudbuild-agent-staging.yaml +0 -40
  187. package/cloudbuild.yaml.deprecated +0 -42
  188. package/src/local-agent-source.tar.gz +0 -0
@@ -0,0 +1,114 @@
1
+ # Security (OWASP Top 10 and ASVS basics, applied)
2
+
3
+ Default posture: **deny by default, fail closed, trust nothing the client sends, and prove it
4
+ on the deployed artefact.**
5
+
6
+ ## 1. Tenant isolation and access control (A01)
7
+
8
+ This is the risk that matters most for us. One missing check leaks one customer's data to
9
+ another.
10
+
11
+ - **The tenant is always the signed JWT claim.** In the gateway, resolve it with
12
+ `TenantAccessGuard.requireTenant(user)`. A `tenantId` in the body, query, path or header
13
+ can at most be *compared* against the claim; it is never used on its own. Enforced by
14
+ `ControllerTenantSourceArchTest`. Frontend: never send `tenantId`.
15
+ - **Ownership on every lookup by id.** Use `TenantAccessGuard.requireOwnedWorkspace`,
16
+ `IncidentAccessGuard` for incidents, and `TenantRepoOwnership` before acting on a repo
17
+ named in a payload. Every SQL query on tenant data has `tenant_id = :tenantId`.
18
+ - **Unknown and foreign both answer 404.** Answering 403 confirms the id exists and makes
19
+ ids enumerable.
20
+ - **Check the tenant first**, then authentication details, then role or plan (RBAC). Never
21
+ run a permission check that reveals whether a resource exists before the tenant check.
22
+ - **Root and admin routes** use the dedicated guards (for example `RootAdminGuard`). Root
23
+ users do not call tenant-scoped `/api/v2/admin/*` from the FE.
24
+ - **Internal endpoints** (`/api/v1/internal/**`, `/internal/**`) use the internal bearer
25
+ checked by `InternalTokenAuthenticator`: constant-time compare
26
+ (`MessageDigest.isEqual`), and reject everything when the token isn't configured. A
27
+ tenant id in an internal payload still has to be bound to the resource
28
+ (`TenantRepoOwnership`).
29
+ - **Per-purpose credentials.** The codescan callback uses a per-scan HMAC token (with
30
+ expiry and rotation via `_PREVIOUS`), never the shared internal token. Each service has
31
+ its own LiteLLM virtual key.
32
+ - **Redis keys and channels** that carry tenant data include the tenant id.
33
+ - **Automation and merge.** Anything that applies a patch goes through
34
+ `AutomationGateService` (`AutomationGateArchTest`). Nothing merges on its own
35
+ (`NoAutonomousMergeArchitectureTest`). A model verdict is evidence for a human reviewer,
36
+ never permission to merge.
37
+ - **A new check on an existing resource:** find **every** read path to that resource, not
38
+ just the new one. A check that a parallel path bypasses is worse than none.
39
+
40
+ ## 2. Secrets (A02, A05, ASVS V6/V14)
41
+
42
+ - No secret in code, YAML defaults, Dockerfiles, `manifest.yml`, docs, tests, PR text or
43
+ commit messages. Secrets come from Secret Manager, env vars or Forge variables at
44
+ runtime.
45
+ - **Never print a secret.** Not in logs, errors, `toString()`, SSE frames, CLI output, or
46
+ your own terminal while debugging. To check a secret, compare hashes or lengths, or run
47
+ `grep -c` against the real log.
48
+ - **Provider keys (Anthropic, Gemini, OpenAI, Voyage) exist only in vh-litellm.** Any
49
+ other service holding one is a bug (`NoDirectProviderCallsGuardTest`,
50
+ `LlmAgnosticRatchetTest`, `test_no_direct_provider.py`).
51
+ - Cloud Run: `--update-env-vars` / `--update-secrets`, never `--set-env-vars`.
52
+ - If a secret leaks into git or logs, rotate it. Deleting the line is not enough.
53
+ - Encryption: AES-GCM with a random IV, no ECB, no "decrypt failed, so return the
54
+ plaintext" (`NoEcbEncryptionArchTest`). Hash passwords with BCrypt or Argon2.
55
+
56
+ ## 3. Input validation and injection (A03)
57
+
58
+ - Validate at the boundary with allowlists: `@Valid` plus jakarta constraints on DTOs,
59
+ zod or explicit checks in TS, pydantic or explicit checks in Python. Reject
60
+ input; don't "clean" it.
61
+ - SQL: bound parameters only (`:param` in `@Query` or `DatabaseClient.bind`). Dynamic sort
62
+ or column names go through an allowlist map.
63
+ - Shell and git: pass argument arrays (`ProcessBuilder(List)`, `spawn(cmd, args)`,
64
+ `subprocess.run([...])`), never a concatenated string and never `shell=True`. Refs,
65
+ branch names and paths from users are validated against a strict pattern.
66
+ - Paths: normalize, then check the result is inside the workspace root. Reject `..`,
67
+ absolute paths and symlink escapes.
68
+ - **Prompt injection.** Ticket descriptions, repo files, logs and PR comments are untrusted
69
+ data. Pass them as delimited data, never as instructions, and never let them pick tools,
70
+ URLs or credentials.
71
+ - Frontend: no `dangerouslySetInnerHTML` with server or user text; encode for the sink.
72
+
73
+ ## 4. SSRF and egress (A10)
74
+
75
+ - Any URL a tenant supplies (webhooks, `localAgentUrl`, repo URLs, callback URLs) goes
76
+ through an egress guard (`EgressUrlGuard`). Block loopback, link-local and the metadata
77
+ server (`169.254.169.254`, `metadata.google.internal`), and block private ranges for
78
+ third-party targets. Re-check after DNS resolution and on every redirect.
79
+ - Callbacks go only to allowlisted hosts (for example codescan's `CALLBACK_ALLOWED_HOSTS`).
80
+ - Forge app egress allowlist = the gateway domain only.
81
+ - Open redirects: redirect only to relative paths (`/x`, not `//x`) or an exact allowlist.
82
+
83
+ ## 5. Logging without PII or secrets (A09)
84
+
85
+ - Log events with actor and context ids (`tenantId`, `workspaceId`, `scanId`, correlation
86
+ id). Don't log payloads.
87
+ - Never log tokens, passwords, API keys, OAuth codes, cookies, `Authorization` headers,
88
+ URLs with credentials in them (`https://oauth2:<token>@...`), raw prompts, source code,
89
+ ticket descriptions or personal data (email, names) beyond an id.
90
+ - Redact credentials in URLs before logging (the shared sanitizer, pinned by
91
+ `SecretsNeverReachLogsTest`). Framework loggers can leak query strings too. Keep keys
92
+ out of query strings.
93
+ - Security events (auth failure, cross-tenant attempt, guard rejection) are logged at WARN
94
+ with the reason and no secrets.
95
+ - Error responses carry no stack traces and no internal hosts.
96
+
97
+ ## 6. Other Top 10 items, briefly
98
+
99
+ - **A04 insecure design:** new flows that act on customer systems need a threat note in the
100
+ PR: who can trigger it, what it can touch, and how it fails.
101
+ - **A05 misconfiguration:** QA-only beans are `@Profile("qa")` (`QaProfileArchTest`).
102
+ Security headers in `vercel.json` / nginx are fixed. No runtime DDL.
103
+ - **A06 components:** pin versions. codescan's trivy/gitleaks and Dependabot findings with
104
+ CVSS >= 7 block release. The LiteLLM image is pinned to an exact tag, never `latest`.
105
+ - **A07 auth:** short-lived JWTs with refresh; OAuth `state` bound to the session and
106
+ single-use; tokens never in URLs (FE `authenticatedDownload`).
107
+ - **A08 integrity:** verify webhook signatures (GitHub HMAC, Forge FIT via JWKS, Slack
108
+ signing) before parsing.
109
+
110
+ ## 7. Verify on the real artefact
111
+
112
+ Code review proves what we control. Leaks happen in what we don't. Before you say "no
113
+ secret, PII or egress", grep the real revision's logs, the real HTTP response and the
114
+ built JS bundle.
@@ -0,0 +1,42 @@
1
+ # Simplicity (KISS, YAGNI)
2
+
3
+ The cheapest code to own is code that doesn't exist. Before adding anything, ask:
4
+
5
+ 1. Does the current requirement need it? If the answer starts with "we might", don't add it.
6
+ 2. Does the standard library, the framework or code already in the repo solve it? Search first.
7
+ 3. Could a new teammate understand it in 30 seconds?
8
+
9
+ ## Rules
10
+
11
+ - **No speculative generality.** No extension points, config options, feature flags,
12
+ generic type parameters or plugin systems without a second real use case today.
13
+ - **No interface with a single implementation** unless it is a port that crosses a layer
14
+ or process boundary (see `solid.md`).
15
+ - **Flags are for rollout, not for design.** A feature flag guards a risky change until
16
+ the soak is over; then either the old path or the flag gets removed. Every flag has an
17
+ owner and an exit condition in the PR.
18
+ - **No new dependency** (Maven artifact, npm package, pip package) without saying in the
19
+ PR why the existing stack can't do it. Check its licence and maintenance. Never add a
20
+ provider LLM SDK (ADR-001).
21
+ - **Prefer boring technology** that is already in the stack: WebFlux, R2DBC, Postgres,
22
+ Redis, React Query, shadcn/ui, pytest. Don't add a second library for the same job.
23
+ - **Small PRs.** One intent per PR. Keep mechanical moves and renames separate from
24
+ behaviour changes. Keep removals in their own PR (gateway CLAUDE.md rule 1).
25
+ - **Delete as you go.** When a flag has soaked, remove the dead branch in a follow-up PR
26
+ and make the ratchet baseline shrink.
27
+
28
+ ## Smells
29
+
30
+ | Smell | Simpler alternative |
31
+ |---|---|
32
+ | `AbstractBaseGenericHandler<T, R, C>` with one subclass | the one concrete class |
33
+ | a config property nobody sets to anything but the default | a constant |
34
+ | a 5-step builder for a 3-field object | a record constructor |
35
+ | a Reactor chain with `zip` over one publisher, or `flatMap(Mono::just)` | the plain operator (`map`) |
36
+ | a React context for state used by one component | local `useState` |
37
+ | a retry, cache or queue "for performance" with no measurement | measure first |
38
+
39
+ ## Simple is not naive
40
+
41
+ Simplicity never justifies skipping tenant checks, input validation, error handling or
42
+ tests. Those are part of the requirement.
@@ -0,0 +1,64 @@
1
+ # SOLID, applied to our code
2
+
3
+ Use SOLID to decide structure: where a responsibility lives and what depends on what. Name
4
+ the principle, point at the concrete smell, and apply the smallest fix. Don't bring in a
5
+ pattern that is heavier than the problem (see `simplicity.md`).
6
+
7
+ ## S: single responsibility
8
+
9
+ A class has one reason to change, meaning one actor or one policy.
10
+
11
+ - Smell: a 2,000-line `*Service` that does HTTP mapping, SQL, prompt building and
12
+ notifications. `AgenticChatService` is the cautionary example.
13
+ - Fix: one use case per application class, such as `IncidentFixRunner` and
14
+ `OrchestratedImplementRunner`. Adapters live in `infrastructure/`.
15
+ - Rule of thumb: if describing the class needs "and", it probably has two jobs.
16
+
17
+ ## O: open/closed
18
+
19
+ Add behaviour by adding code, not by editing a switch that keeps growing.
20
+
21
+ - Smell: `if (provider.equals("github")) ... else if ("gitlab") ... else if ("bitbucket")`
22
+ copied into several services.
23
+ - Fix: one strategy or adapter per variant behind a port, chosen once in a map or registry.
24
+ For a closed set, a Java 21 `sealed` interface with a `switch` is fine and simpler.
25
+ - A single `switch` in one place is not a violation. Wait for it to be duplicated.
26
+
27
+ ## L: Liskov substitution
28
+
29
+ A subtype must work wherever its parent does, with no surprises.
30
+
31
+ - Smell: an override that throws `UnsupportedOperationException`, or a caller doing
32
+ `instanceof` checks to avoid a subtype.
33
+ - Fix: composition over inheritance; split the interface (see ISP).
34
+ - Exception: we do deliberately make a dead autonomous path throw (for example the retired
35
+ `autoApproveAndMerge`) so it cannot run. That is a kill switch, not a design to copy.
36
+
37
+ ## I: interface segregation
38
+
39
+ Callers depend only on what they use.
40
+
41
+ - The gateway's `agenttask` feature has three ports, `AgentTaskPort`, `AgentRunPort` and
42
+ `AgentReviewPort`, instead of one "OrchestratorClient" with every method. Follow that
43
+ shape.
44
+ - Smell: a port whose implementers leave half the methods empty.
45
+
46
+ ## D: dependency inversion
47
+
48
+ High-level policy doesn't depend on low-level detail. Both depend on an abstraction owned
49
+ by the high level.
50
+
51
+ - Ports (interfaces) live in `domain/` (or `application/`) of the feature that needs them.
52
+ Adapters in `infrastructure/` implement them. Examples: `AgentTaskPort` is implemented by
53
+ `OrchestratorAgentTaskClient`, and `LlmPort` by `LiteLlmChatAdapter`.
54
+ - Use constructor injection only. No field injection, no `new` of collaborators inside
55
+ business code, no static service locators.
56
+ - Don't create an interface for a class that has one implementation and no boundary to
57
+ cross. A port is justified because it crosses a layer or process boundary, not "for
58
+ testability" alone.
59
+
60
+ ## Order of work
61
+
62
+ 1. SOLID for structure (split responsibilities, invert dependencies).
63
+ 2. Clean code for local readability.
64
+ 3. A named design pattern only if variation is real and recurring.
@@ -0,0 +1,59 @@
1
+ # Testing
2
+
3
+ A test proves a **behaviour** a user or caller depends on. A green suite is evidence about
4
+ the code; the deployed artefact is evidence about the product. We need both.
5
+
6
+ ## Rules
7
+
8
+ - **Test behaviour, not choreography.** Assert on outputs, persisted state, emitted events
9
+ and HTTP status. Use `verify(...)` only when the interaction *is* the contract (for
10
+ example "the merge endpoint is never called").
11
+ - **One concept per test**, named for the behaviour:
12
+ `crossTenantWorkspaceAnswers404`, `callbackRejectsExpiredHmac`.
13
+ - **Cover the scenarios, not the lines:** happy path; boundaries (0, 1, max); invalid, null
14
+ or empty input; error path (downstream fails, times out, returns garbage);
15
+ **authorization (no token, wrong tenant, wrong role)**; idempotency and retries;
16
+ ordering and concurrency where it applies.
17
+ - **Every tenant-scoped endpoint or use case has a cross-tenant test** that expects 404 (or
18
+ an empty result) and proves nothing of the other tenant leaks.
19
+ - **Mock only real boundaries**: HTTP clients, ports to other services, the clock, and
20
+ external CLIs. Use real objects for the unit under test, value objects, DTOs, records and
21
+ pure functions.
22
+ - **Mocks hide types.** A mocked repository accepts a `String` that Postgres will reject
23
+ as `jsonb`, `uuid[]`, an enum or `timestamptz`. Any new write to a column must be
24
+ exercised against real PostgreSQL (Testcontainers) or a real call in QA before it
25
+ counts as tested. The same goes for JSON contracts between services: test against the
26
+ real DTO or schema, not a hand-written map.
27
+ - **Tests are deterministic**: no real network, no sleeps (use virtual time or fake
28
+ clocks), no order dependence, no shared mutable fixtures.
29
+ - **Don't edit an existing test to make it pass.** If a correct test fails, the code is
30
+ wrong. If the test is wrong, fix it in a separate, explained commit.
31
+
32
+ ## Bug fixes: RED, then GREEN
33
+
34
+ 1. Reproduce: write a failing test that shows the bug (or point to an existing failing
35
+ one). Commit it or keep it in the PR.
36
+ 2. Fix the production code until it passes.
37
+ 3. Run the module suite, then the full suite.
38
+ 4. In the PR: scenario, the proof test, the fix and the results.
39
+
40
+ ## Guard tests are tests too
41
+
42
+ Architecture and guard tests (ArchUnit or source scans) protect invariants that no
43
+ behavioural test will catch next year. See the table in `README.md`. When you fix a class
44
+ of bug (not one instance), add or extend a guard so the *next* instance fails the build.
45
+
46
+ ## Per stack
47
+
48
+ | Stack | Unit | Slice / integration | Notes |
49
+ |---|---|---|---|
50
+ | Java WebFlux | JUnit 5 + AssertJ + Mockito; `StepVerifier` for every `Mono`/`Flux` (never `.block()` in tests) | `@WebFluxTest` + `WebTestClient`; Testcontainers Postgres for R2DBC and migrations | `mvn -q -Dtest=ClassName test` for targeted runs; JDK 21 locally (Lombok breaks on 25) |
51
+ | TypeScript/React | Vitest + Testing Library; test the hook/component behaviour the user sees | MSW or mocked `lib/api` at the HTTP edge | `npm run typecheck && npm run lint && npm test` |
52
+ | Python | pytest; pure functions (`scoring`, `normalization`, `smart_router_policy`) without I/O | subprocess engines faked at the boundary; schema tests on JSON contracts | `pytest -q` |
53
+ | Node (local-agent) | `node --test` | fixtures under `test/fixtures` | `npm test` |
54
+
55
+ ## Verify by use
56
+
57
+ After a merge deploys QA, verify the behaviour on the deployed revision: `curl` the
58
+ endpoint, read that revision's logs (the severity is in the text, not in the metadata),
59
+ and grep the built bundle. Compare startup with startup. "Tests pass" is not "it works".
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: VisionHammer v2 context and engineering standards (shared across repos)
3
+ alwaysApply: true
4
+ ---
5
+
6
+ Before writing or reviewing code in this repo, read and follow:
7
+
8
+ - @.claude/context.md (what this repo is, boundaries, run/test/deploy, gotchas)
9
+ - @.claude/standards/README.md (index and the guard tests that enforce the standards)
10
+ - @.claude/standards/security-owasp.md
11
+ - @.claude/standards/layered-architecture.md
12
+ - @.claude/standards/node.md
13
+ - @AGENTS.md
14
+
15
+ The other standards (`clean-code.md`, `solid.md`, `simplicity.md`, `testing.md`,
16
+ `code-review-checklist.md`) live in `.claude/standards/`. Repo rules in `CLAUDE.md` and
17
+ `AGENTS.md` win over the generic standards.
@@ -0,0 +1,315 @@
1
+ # Build + push the BYOC "full" local-agent image (Dockerfile.full: Node + JDK 17
2
+ # + Maven + Python + Swift) to the PUBLIC Artifact Registry repo that customer
3
+ # clouds pull from without credentials:
4
+ #
5
+ # europe-west1-docker.pkg.dev/visionhammer/visionhammer-docker-public-eu/local-agent-full:<semver>
6
+ # europe-west1-docker.pkg.dev/visionhammer/visionhammer-docker-public-eu/local-agent-full:<git-sha>
7
+ #
8
+ # Consumers: vh-api-gateway CloudEngineerService (BYOC GCP Cloud Run + BYOC Azure
9
+ # ACI) via the `deepdebug.byoc.agent-image` property. The gateway pins a version
10
+ # (ideally `:<semver>@sha256:<digest>`); this workflow never moves a floating
11
+ # tag (no :latest, no :v3) and refuses to overwrite an existing :<semver>.
12
+ #
13
+ # Triggers:
14
+ # - push of a v* tag / published GitHub release (tag must be on main).
15
+ # NOTE: tags pushed by npm-publish.yml's auto-bump use GITHUB_TOKEN and so do
16
+ # NOT trigger other workflows; run this manually (workflow_dispatch) for those.
17
+ # - workflow_dispatch from main (version defaults to package.json).
18
+ #
19
+ # Supply chain (ADR-0017 D12), OFF by default: the `supply-chain` job signs the
20
+ # pushed image with cosign (keyless, GitHub OIDC), attaches an SPDX SBOM (syft)
21
+ # and a SLSA v1 provenance attestation, then verifies all three. It runs only
22
+ # when the dispatch input `supply_chain` is true or the repository variable
23
+ # BYOC_SUPPLY_CHAIN is 'true' (for v* tag / release runs). Never on branch pushes
24
+ # (this workflow has no branch trigger). See docs/byoc-agent-image.md.
25
+ name: Build BYOC Agent Image
26
+
27
+ on:
28
+ push:
29
+ tags: ['v*']
30
+ release:
31
+ types: [published]
32
+ workflow_dispatch:
33
+ inputs:
34
+ version:
35
+ description: 'Semver tag to publish (default: package.json version, e.g. 1.0.31)'
36
+ required: false
37
+ type: string
38
+ supply_chain:
39
+ description: 'Also sign (cosign keyless), attach SBOM + provenance and verify'
40
+ required: false
41
+ default: false
42
+ type: boolean
43
+
44
+ concurrency:
45
+ group: build-byoc-image-${{ github.sha }}
46
+ cancel-in-progress: false
47
+
48
+ permissions:
49
+ contents: read
50
+ id-token: write
51
+
52
+ env:
53
+ GCP_PROJECT: visionhammer
54
+ WIF_PROVIDER: projects/1009366917257/locations/global/workloadIdentityPools/github-pool/providers/github-provider
55
+ DEPLOYER_SA: github-deployer@visionhammer.iam.gserviceaccount.com
56
+ AR_HOST: europe-west1-docker.pkg.dev
57
+ AR_IMAGE: europe-west1-docker.pkg.dev/visionhammer/visionhammer-docker-public-eu/local-agent-full
58
+
59
+ jobs:
60
+ build:
61
+ runs-on: ubuntu-latest
62
+ timeout-minutes: 75
63
+ outputs:
64
+ version: ${{ steps.vars.outputs.version }}
65
+ sha: ${{ steps.vars.outputs.sha }}
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ with:
69
+ fetch-depth: 0
70
+
71
+ - name: Resolve version and verify source is on main
72
+ id: vars
73
+ env:
74
+ INPUT_VERSION: ${{ inputs.version }}
75
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
76
+ EVENT: ${{ github.event_name }}
77
+ run: |
78
+ set -euo pipefail
79
+ case "$EVENT" in
80
+ workflow_dispatch)
81
+ if [ "$GITHUB_REF" != "refs/heads/main" ]; then
82
+ echo "::error::workflow_dispatch must run from main (got $GITHUB_REF)"; exit 1
83
+ fi
84
+ VERSION="${INPUT_VERSION:-$(node -p "require('./package.json').version")}" ;;
85
+ # Untrusted-ish input: read from env, never interpolated into the script.
86
+ release) VERSION="${RELEASE_TAG}" ;;
87
+ *) VERSION="${GITHUB_REF_NAME}" ;;
88
+ esac
89
+ VERSION="${VERSION#v}"
90
+ if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
91
+ echo "::error::'$VERSION' is not a semver version"; exit 1
92
+ fi
93
+ git fetch --quiet origin main
94
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
95
+ echo "::error::$GITHUB_SHA is not on main; BYOC images are built from main only"; exit 1
96
+ fi
97
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
98
+ echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
99
+
100
+ - name: Authenticate to GCP (WIF)
101
+ uses: google-github-actions/auth@v2
102
+ with:
103
+ workload_identity_provider: ${{ env.WIF_PROVIDER }}
104
+ service_account: ${{ env.DEPLOYER_SA }}
105
+
106
+ - name: Set up gcloud
107
+ uses: google-github-actions/setup-gcloud@v2
108
+
109
+ - name: Configure Docker for Artifact Registry
110
+ run: gcloud auth configure-docker "$AR_HOST" --quiet
111
+
112
+ - name: Refuse to overwrite an existing version tag
113
+ id: exists
114
+ run: |
115
+ V="${AR_IMAGE}:${{ steps.vars.outputs.version }}"
116
+ S="${AR_IMAGE}:${{ steps.vars.outputs.sha }}"
117
+ if gcloud artifacts docker images describe "$V" --project "$GCP_PROJECT" >/dev/null 2>&1; then
118
+ VD=$(gcloud artifacts docker images describe "$V" --project "$GCP_PROJECT" --format='value(image_summary.digest)')
119
+ SD=$(gcloud artifacts docker images describe "$S" --project "$GCP_PROJECT" --format='value(image_summary.digest)' 2>/dev/null || true)
120
+ if [ -n "$SD" ] && [ "$VD" = "$SD" ]; then
121
+ # Same commit already published (e.g. tag push + release both fired).
122
+ echo "::notice::$V already published from this commit ($VD); nothing to do"
123
+ echo "skip=true" >> "$GITHUB_OUTPUT"
124
+ exit 0
125
+ fi
126
+ echo "::error::$V already exists from a different build; bump the version instead of overwriting"
127
+ exit 1
128
+ fi
129
+ echo "skip=false" >> "$GITHUB_OUTPUT"
130
+
131
+ - name: Free runner disk (image is ~3 GB with the Swift toolchain)
132
+ if: steps.exists.outputs.skip != 'true'
133
+ run: |
134
+ sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true
135
+ df -h /
136
+
137
+ - name: Build image (Dockerfile.full)
138
+ if: steps.exists.outputs.skip != 'true'
139
+ run: |
140
+ docker build --platform=linux/amd64 -f Dockerfile.full \
141
+ --label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \
142
+ --label org.opencontainers.image.revision="${{ steps.vars.outputs.sha }}" \
143
+ --label org.opencontainers.image.version="${{ steps.vars.outputs.version }}" \
144
+ -t "${AR_IMAGE}:${{ steps.vars.outputs.version }}" \
145
+ -t "${AR_IMAGE}:${{ steps.vars.outputs.sha }}" \
146
+ .
147
+
148
+ - name: Smoke test (boots, /health public, API requires X-Agent-Token)
149
+ if: steps.exists.outputs.skip != 'true'
150
+ run: |
151
+ set -euo pipefail
152
+ docker run -d --name smoke -p 5055:5055 \
153
+ -e LOCAL_AGENT_TOKEN=smoke-token -e DISABLE_WS_TUNNEL=true \
154
+ "${AR_IMAGE}:${{ steps.vars.outputs.version }}"
155
+ for i in $(seq 1 60); do
156
+ curl -fsS http://127.0.0.1:5055/health >/dev/null 2>&1 && break
157
+ sleep 2
158
+ done
159
+ curl -fsS http://127.0.0.1:5055/health
160
+ code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5055/workspace/info)
161
+ [ "$code" = "401" ] || { echo "::error::expected 401 without X-Agent-Token, got $code"; docker logs smoke; exit 1; }
162
+ code=$(curl -s -o /dev/null -w '%{http_code}' -H 'X-Agent-Token: smoke-token' http://127.0.0.1:5055/workspace/info)
163
+ [ "$code" != "401" ] || { echo "::error::valid X-Agent-Token was rejected"; docker logs smoke; exit 1; }
164
+ docker exec smoke sh -c 'java -version && mvn -version >/dev/null && swift --version >/dev/null && git --version'
165
+ docker rm -f smoke
166
+
167
+ - name: Push :<semver> and :<git-sha>
168
+ if: steps.exists.outputs.skip != 'true'
169
+ id: push
170
+ run: |
171
+ set -euo pipefail
172
+ docker push "${AR_IMAGE}:${{ steps.vars.outputs.version }}"
173
+ # :<git-sha> is immutable too: the same commit may be rebuilt under a new
174
+ # semver (e.g. dispatch 1.0.31, later tag v1.0.32) and a rebuild is not
175
+ # byte-reproducible, so never move an existing :<git-sha> to a new digest -
176
+ # it keeps pointing at the first image built from that commit.
177
+ if gcloud artifacts docker images describe "${AR_IMAGE}:${{ steps.vars.outputs.sha }}" \
178
+ --project "$GCP_PROJECT" >/dev/null 2>&1; then
179
+ echo "::notice::${AR_IMAGE}:${{ steps.vars.outputs.sha }} already exists; not moving it (pin by semver@digest)"
180
+ else
181
+ docker push "${AR_IMAGE}:${{ steps.vars.outputs.sha }}"
182
+ fi
183
+ DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "${AR_IMAGE}:${{ steps.vars.outputs.version }}" | sed 's/.*@//')
184
+ echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
185
+
186
+ - name: Summary
187
+ if: steps.exists.outputs.skip != 'true'
188
+ run: |
189
+ {
190
+ echo "### BYOC agent image published"
191
+ echo ""
192
+ echo "- \`${AR_IMAGE}:${{ steps.vars.outputs.version }}\`"
193
+ echo "- \`${AR_IMAGE}:${{ steps.vars.outputs.sha }}\`"
194
+ echo "- digest: \`${{ steps.push.outputs.digest }}\`"
195
+ echo ""
196
+ echo "Pin it in the gateway (vh-api-gateway, \`deepdebug.byoc.agent-image\` / env \`BYOC_AGENT_IMAGE\`):"
197
+ echo ""
198
+ echo "\`${AR_IMAGE}:${{ steps.vars.outputs.version }}@${{ steps.push.outputs.digest }}\`"
199
+ } >> "$GITHUB_STEP_SUMMARY"
200
+
201
+ # Cosign keyless signature + SBOM + provenance for the image pushed above.
202
+ # Flag: input `supply_chain` (dispatch) or repo variable BYOC_SUPPLY_CHAIN=='true'.
203
+ # Needs github-deployer to write signatures next to the image (same repository,
204
+ # roles/artifactregistry.writer, already required to push).
205
+ supply-chain:
206
+ needs: build
207
+ if: ${{ inputs.supply_chain == true || vars.BYOC_SUPPLY_CHAIN == 'true' }}
208
+ runs-on: ubuntu-latest
209
+ timeout-minutes: 20
210
+ permissions:
211
+ contents: read
212
+ id-token: write # keyless signing identity (Fulcio) + WIF
213
+ env:
214
+ VERSION: ${{ needs.build.outputs.version }}
215
+ SOURCE_SHA: ${{ needs.build.outputs.sha }}
216
+ steps:
217
+ - name: Authenticate to GCP (WIF)
218
+ uses: google-github-actions/auth@v2
219
+ with:
220
+ workload_identity_provider: ${{ env.WIF_PROVIDER }}
221
+ service_account: ${{ env.DEPLOYER_SA }}
222
+
223
+ - name: Set up gcloud
224
+ uses: google-github-actions/setup-gcloud@v2
225
+
226
+ - name: Configure Docker for Artifact Registry
227
+ run: gcloud auth configure-docker "$AR_HOST" --quiet
228
+
229
+ - name: Resolve the image digest
230
+ id: digest
231
+ run: |
232
+ set -euo pipefail
233
+ D=$(gcloud artifacts docker images describe "${AR_IMAGE}:${VERSION}" \
234
+ --project "$GCP_PROJECT" --format='value(image_summary.digest)')
235
+ case "$D" in sha256:*) ;; *) echo "::error::could not resolve digest of ${AR_IMAGE}:${VERSION}"; exit 1;; esac
236
+ echo "digest=$D" >> "$GITHUB_OUTPUT"
237
+ echo "ref=${AR_IMAGE}@${D}" >> "$GITHUB_OUTPUT"
238
+
239
+ - name: Install cosign
240
+ uses: sigstore/cosign-installer@v3
241
+ with:
242
+ cosign-release: 'v2.4.1'
243
+
244
+ - name: Sign the image (keyless, OIDC)
245
+ env:
246
+ REF: ${{ steps.digest.outputs.ref }}
247
+ run: cosign sign --yes "$REF"
248
+
249
+ - name: SBOM (syft, SPDX JSON)
250
+ uses: anchore/sbom-action@v0
251
+ with:
252
+ image: ${{ steps.digest.outputs.ref }}
253
+ format: spdx-json
254
+ output-file: sbom.spdx.json
255
+ upload-artifact: true
256
+ artifact-name: local-agent-full-${{ needs.build.outputs.version }}.sbom.spdx.json
257
+
258
+ - name: Attach the SBOM as a signed attestation
259
+ env:
260
+ REF: ${{ steps.digest.outputs.ref }}
261
+ run: cosign attest --yes --type spdxjson --predicate sbom.spdx.json "$REF"
262
+
263
+ - name: Build and attach the provenance attestation (SLSA v1)
264
+ env:
265
+ REF: ${{ steps.digest.outputs.ref }}
266
+ run: |
267
+ set -euo pipefail
268
+ node -e '
269
+ const e = process.env;
270
+ const repoUrl = `https://github.com/${e.GITHUB_REPOSITORY}`;
271
+ const p = {
272
+ buildDefinition: {
273
+ buildType: `${repoUrl}/.github/workflows/build-byoc-image.yml`,
274
+ externalParameters: {
275
+ workflow: { ref: e.GITHUB_REF, repository: repoUrl, path: ".github/workflows/build-byoc-image.yml" },
276
+ dockerfile: "Dockerfile.full",
277
+ version: e.VERSION
278
+ },
279
+ resolvedDependencies: [{ uri: `git+${repoUrl}@${e.GITHUB_REF}`, digest: { gitCommit: e.SOURCE_SHA } }]
280
+ },
281
+ runDetails: {
282
+ builder: { id: `${repoUrl}/actions/runner/github-hosted` },
283
+ metadata: { invocationId: `${repoUrl}/actions/runs/${e.GITHUB_RUN_ID}/attempts/${e.GITHUB_RUN_ATTEMPT}` }
284
+ }
285
+ };
286
+ require("fs").writeFileSync("provenance.json", JSON.stringify(p, null, 2));
287
+ '
288
+ cosign attest --yes --type slsaprovenance1 --predicate provenance.json "$REF"
289
+
290
+ - name: Verify signature and attestations
291
+ env:
292
+ REF: ${{ steps.digest.outputs.ref }}
293
+ run: |
294
+ set -euo pipefail
295
+ ID="https://github.com/${GITHUB_REPOSITORY}/.github/workflows/build-byoc-image.yml@${GITHUB_REF}"
296
+ ISS="https://token.actions.githubusercontent.com"
297
+ cosign verify --certificate-identity "$ID" --certificate-oidc-issuer "$ISS" "$REF" >/dev/null
298
+ for T in spdxjson slsaprovenance1; do
299
+ cosign verify-attestation --type "$T" --certificate-identity "$ID" --certificate-oidc-issuer "$ISS" "$REF" >/dev/null
300
+ done
301
+
302
+ - name: Summary
303
+ env:
304
+ REF: ${{ steps.digest.outputs.ref }}
305
+ run: |
306
+ {
307
+ echo "### BYOC agent image signed"
308
+ echo ""
309
+ echo "- \`$REF\`"
310
+ echo "- cosign keyless signature, SPDX SBOM and SLSA v1 provenance attached and verified"
311
+ echo ""
312
+ echo "Verify:"
313
+ echo ""
314
+ echo "\`cosign verify --certificate-identity-regexp '^https://github.com/${GITHUB_REPOSITORY}/' --certificate-oidc-issuer https://token.actions.githubusercontent.com $REF\`"
315
+ } >> "$GITHUB_STEP_SUMMARY"