deepdebug-local-agent 1.0.23 → 1.2.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.
- package/.claude/context.md +76 -0
- package/.claude/skills/README.md +25 -0
- package/.claude/skills/tdd-debugging/SKILL.md +53 -0
- package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
- package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
- package/.claude/standards/README.md +51 -0
- package/.claude/standards/clean-code.md +75 -0
- package/.claude/standards/code-review-checklist.md +61 -0
- package/.claude/standards/layered-architecture.md +163 -0
- package/.claude/standards/node.md +45 -0
- package/.claude/standards/security-owasp.md +114 -0
- package/.claude/standards/simplicity.md +42 -0
- package/.claude/standards/solid.md +64 -0
- package/.claude/standards/testing.md +59 -0
- package/.cursor/rules/standards.mdc +17 -0
- package/.github/workflows/build-byoc-image.yml +315 -0
- package/.github/workflows/deploy-gcp.yml +102 -0
- package/.github/workflows/npm-publish.yml +76 -8
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +76 -0
- package/CLAUDE.md +13 -0
- package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
- package/Dockerfile +4 -0
- package/Dockerfile.full +55 -3
- package/bin/install.js +16 -5
- package/cloudbuild-full.yaml +57 -0
- package/docker-compose.yml +5 -3
- package/docs/OBSERVABILITY.md +164 -0
- package/docs/byoc-agent-image.md +70 -0
- package/docs/dlp.md +78 -0
- package/docs/graphify-packaging-plan.md +133 -0
- package/docs/graphify-validation-run-2026-10-28.md +243 -0
- package/docs/releasing.md +37 -0
- package/docs/signed-commands.md +163 -0
- package/env.example +34 -0
- package/helm/Chart.yaml +2 -2
- package/helm/templates/deployment.yaml +13 -3
- package/helm/templates/networkpolicy.yaml +22 -0
- package/helm/templates/secret.yaml +1 -0
- package/helm/values.yaml +13 -14
- package/mcp-server.js +4 -1
- package/package.json +9 -4
- package/scripts/next-version.mjs +75 -0
- package/scripts/upgrade-observability-push.sh +227 -0
- package/src/ai-engine.js +22 -22
- package/src/config/trusted-signing-keys.json +3 -0
- package/src/detectors/build-tool-detector.js +50 -0
- package/src/detectors/multi-project-detector.js +22 -22
- package/src/detectors/service-detector.js +24 -24
- package/src/dlp/engine.js +111 -0
- package/src/dlp/index.js +137 -0
- package/src/dlp/rules.js +97 -0
- package/src/dlp/validators.js +123 -0
- package/src/exec-utils.js +26 -4
- package/src/git/base-git-provider.js +117 -117
- package/src/git/bitbucket-provider.js +2 -2
- package/src/git/git-provider-registry.js +18 -18
- package/src/git/github-provider.js +13 -13
- package/src/git/multi-repo-pr.js +161 -0
- package/src/mcp-http-server.js +32 -16
- package/src/observability/bundle-files.js +418 -0
- package/src/observability/detect.js +151 -0
- package/src/observability/provision.js +212 -0
- package/src/observability/render-cli.js +59 -0
- package/src/observability/routes.js +88 -0
- package/src/patch/patch-engine.js +28 -28
- package/src/patch-manager.js +85 -85
- package/src/patch_bkp.js +14 -14
- package/src/ports.js +4 -4
- package/src/routes/workspace.route.js +47 -16
- package/src/runtimes/base-runtime.js +75 -75
- package/src/runtimes/java/gradle-runtime.js +17 -17
- package/src/runtimes/java/java-integrations.js +25 -25
- package/src/runtimes/java/maven-runtime.js +34 -34
- package/src/runtimes/node/node-integrations.js +1 -1
- package/src/runtimes/node/npm-runtime.js +14 -14
- package/src/runtimes/node/yarn-runtime.js +4 -4
- package/src/runtimes/runtime-registry.js +25 -25
- package/src/server-local.js +4 -4
- package/src/server.js +1366 -333
- package/src/siblings/burst-suppressor.js +89 -0
- package/src/siblings/capabilities.js +273 -0
- package/src/siblings/env-compat.cjs +58 -0
- package/src/siblings/incident-forwarder.js +322 -0
- package/src/siblings/logger.js +15 -0
- package/src/siblings/metrics.js +94 -0
- package/src/utils/agent-auth.js +184 -0
- package/src/utils/approval-token.js +56 -0
- package/src/utils/bind-host.js +29 -0
- package/src/utils/branch-refspec.js +98 -0
- package/src/utils/child-env.js +34 -0
- package/src/utils/environment-diagnostics.js +55 -55
- package/src/utils/exec-policy.js +962 -0
- package/src/utils/exec-utils.js +15 -15
- package/src/utils/file-count.js +46 -0
- package/src/utils/fs-utils.js +10 -10
- package/src/utils/multi-repo-open.js +132 -0
- package/src/utils/open-gate.js +183 -0
- package/src/utils/ranged-read.js +65 -0
- package/src/utils/request-guards.js +77 -0
- package/src/utils/request-signing.js +342 -0
- package/src/utils/root-recovery.js +244 -0
- package/src/utils/safe-exec-args.js +58 -0
- package/src/utils/sandbox.js +182 -0
- package/src/utils/search-outcome.js +56 -0
- package/src/utils/workspace-root.js +80 -0
- package/src/validators/syntax-validator.js +149 -0
- package/src/workspace/detect-port.js +16 -16
- package/src/workspace/file-reader.js +1 -1
- package/src/workspace/process-manager.js +46 -46
- package/src/workspace-manager.js +86 -24
- package/test/adapters/registry.test.js +9 -2
- package/test/agent-auth.test.js +194 -0
- package/test/bind-host.test.js +26 -0
- package/test/branch-refspec.test.js +224 -0
- package/test/burst-suppressor.test.js +60 -0
- package/test/capabilities-access.test.js +47 -0
- package/test/capabilities.test.js +87 -0
- package/test/child-env.test.js +23 -0
- package/test/dlp/engine.test.js +173 -0
- package/test/dlp/fixtures.js +126 -0
- package/test/dlp/index.test.js +166 -0
- package/test/env-compat.test.js +71 -0
- package/test/exec-policy.test.js +463 -0
- package/test/file-count.test.js +64 -0
- package/test/incident-forwarder.test.js +231 -0
- package/test/multi-repo-open.test.js +180 -0
- package/test/multi-repo-pr.test.js +129 -0
- package/test/next-version.test.js +48 -0
- package/test/observability-detect.test.js +199 -0
- package/test/open-gate.test.js +341 -0
- package/test/ranged-read.test.js +60 -0
- package/test/request-guards.test.js +91 -0
- package/test/request-signing.test.js +284 -0
- package/test/root-recovery-sidecar.test.js +171 -0
- package/test/root-recovery.test.js +274 -0
- package/test/safe-exec-args.test.js +58 -0
- package/test/sandbox.test.js +98 -0
- package/test/search-outcome.test.js +50 -0
- package/test/workspace-root.test.js +64 -0
- package/tools/graphify-validation/README.md +92 -0
- package/tools/graphify-validation/config/questions.json +31 -0
- package/tools/graphify-validation/config/repos.json +9 -0
- package/tools/graphify-validation/config/symbols.json +6 -0
- package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
- package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
- package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
- package/tools/graphify-validation/lib/graph_metrics.py +96 -0
- package/tools/graphify-validation/lib/gv_common.py +130 -0
- package/tools/graphify-validation/lib/licenses.py +47 -0
- package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
- package/tools/graphify-validation/lib/recall.py +115 -0
- package/tools/graphify-validation/lib/report.py +226 -0
- package/tools/graphify-validation/lib/run_all.py +237 -0
- package/tools/graphify-validation/lib/run_graphify.py +144 -0
- package/tools/graphify-validation/lib/rusage_helper.py +17 -0
- package/tools/graphify-validation/lib/sample_edges.py +246 -0
- package/tools/graphify-validation/lib/spring_fixture.py +81 -0
- package/tools/graphify-validation/requirements-py311.lock +508 -0
- package/tools/graphify-validation/requirements-tools.in +1 -0
- package/tools/graphify-validation/requirements-tools.lock +371 -0
- package/tools/graphify-validation/requirements.in +1 -0
- package/tools/graphify-validation/requirements.lock +502 -0
- package/tools/graphify-validation/run.sh +43 -0
- package/tools/graphify-validation/tests/test_harness.py +151 -0
- package/cloudbuild-agent-qa.yaml +0 -43
- package/cloudbuild-agent-staging.yaml +0 -40
- package/cloudbuild.yaml.deprecated +0 -42
|
@@ -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"
|