Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 179 additions & 3 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,178 @@ jobs:
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

build-sandbox:
name: Build Sandbox
needs: [version]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ghcr.io/aureliolo/synthorg-sandbox
tags: |
type=raw,value=${{ needs.version.outputs.app_version }},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=sha,prefix=sha-

# Build locally first, scan, then push only if scans pass
- name: Build image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: docker/sandbox/Dockerfile
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64

# SHA tag always exists (version/semver tags only on v* tags)
- name: Compute scan image ref
id: scan-ref
run: echo "ref=ghcr.io/aureliolo/synthorg-sandbox:sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

# Single Trivy run for CRITICAL + HIGH.
- name: Trivy scan
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: ${{ steps.scan-ref.outputs.ref }}
format: json
output: trivy-sandbox.json
exit-code: "0"
severity: CRITICAL,HIGH
trivyignores: .github/.trivyignore.yaml

- name: Evaluate Trivy results
run: |
if [ ! -f trivy-sandbox.json ]; then
echo "::error::trivy-sandbox.json not found — Trivy scan may have failed to produce output"
exit 1
fi
echo "## Trivy Scan — Sandbox"
TOTAL=$(jq '[.Results[]?.Vulnerabilities[]?] | length' trivy-sandbox.json)
if [ "$TOTAL" -eq 0 ]; then
echo "No CRITICAL or HIGH vulnerabilities found."
echo "## Trivy Scan — Sandbox: No CRITICAL or HIGH vulnerabilities found." >> "$GITHUB_STEP_SUMMARY"
else
TABLE=$(jq -r '
["SEVERITY","CVE","PACKAGE","VERSION","TITLE"],
(.Results[]?.Vulnerabilities[]? |
[.Severity, .VulnerabilityID, .PkgName, .InstalledVersion, (.Title // "")[0:60]]) |
@tsv
' trivy-sandbox.json | column -t -s$'\t')
echo "$TABLE"
printf '## Trivy Scan — Sandbox\n```\n%s\n```\n' "$TABLE" >> "$GITHUB_STEP_SUMMARY"
fi

CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' trivy-sandbox.json)
HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' trivy-sandbox.json)
echo "Critical: $CRITICAL, High: $HIGH"
echo "**Critical: $CRITICAL, High: $HIGH**" >> "$GITHUB_STEP_SUMMARY"
if [ "$HIGH" -gt 0 ]; then
echo "::warning::Found $HIGH HIGH severity vulnerabilities"
fi
if [ "$CRITICAL" -gt 0 ]; then
echo "::error::Found $CRITICAL CRITICAL vulnerabilities — failing build"
exit 1
fi

- name: Upload Trivy report (sandbox)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: trivy-sandbox-report
path: trivy-sandbox.json
retention-days: 30

- name: Grype scan
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
with:
image: ${{ steps.scan-ref.outputs.ref }}
fail-build: true
severity-cutoff: critical
config: .github/.grype.yaml

- name: CIS Docker Benchmark (sandbox)
continue-on-error: true
env:
IMAGE_REF: ${{ steps.scan-ref.outputs.ref }}
run: |
command -v trivy >/dev/null 2>&1 || { echo "::warning::trivy not on PATH, skipping CIS scan"; exit 0; }
trivy image --compliance docker-cis-1.6.0 --format table "$IMAGE_REF"

# Push only after scans pass — prevents publishing vulnerable images.
# NOTE: Separate build invocation; GHA cache ensures deterministic layers,
# but manifest digest differs due to SBOM + provenance attestation layers.
- name: Push image
if: github.event_name != 'pull_request'
id: push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: docker/sandbox/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
platforms: linux/amd64
sbom: true
provenance: true

- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0

- name: Sign image
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
if [ -z "$DIGEST" ]; then
echo "::error::Push step did not produce a digest — cannot sign image"
exit 1
fi
cosign sign --yes ghcr.io/aureliolo/synthorg-sandbox@${DIGEST}

- name: Attest build provenance (SLSA Level 3)
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ghcr.io/aureliolo/synthorg-sandbox
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

# Append container image references to the GitHub Release (version tags only)
update-release:
name: Update Release Notes
if: startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request'
needs: [version, build-backend, build-web]
needs: [version, build-backend, build-web, build-sandbox]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -408,12 +575,13 @@ jobs:
VERSION: ${{ needs.version.outputs.app_version }}
BACKEND_DIGEST: ${{ needs.build-backend.outputs.digest }}
WEB_DIGEST: ${{ needs.build-web.outputs.digest }}
SANDBOX_DIGEST: ${{ needs.build-sandbox.outputs.digest }}
run: |
set -euo pipefail

# Validate required inputs
if [ -z "$VERSION" ] || [ -z "$BACKEND_DIGEST" ] || [ -z "$WEB_DIGEST" ]; then
echo "::error::Missing required values: VERSION='$VERSION' BACKEND_DIGEST='$BACKEND_DIGEST' WEB_DIGEST='$WEB_DIGEST'"
if [ -z "$VERSION" ] || [ -z "$BACKEND_DIGEST" ] || [ -z "$WEB_DIGEST" ] || [ -z "$SANDBOX_DIGEST" ]; then
echo "::error::Missing required values: VERSION='$VERSION' BACKEND_DIGEST='$BACKEND_DIGEST' WEB_DIGEST='$WEB_DIGEST' SANDBOX_DIGEST='$SANDBOX_DIGEST'"
exit 1
fi

Expand Down Expand Up @@ -441,10 +609,12 @@ jobs:
|-------|------|
| Backend | `docker pull ghcr.io/aureliolo/synthorg-backend:VERSION_PH` |
| Web | `docker pull ghcr.io/aureliolo/synthorg-web:VERSION_PH` |
| Sandbox | `docker pull ghcr.io/aureliolo/synthorg-sandbox:VERSION_PH` |

**Digests** (for pinning):
- Backend: `ghcr.io/aureliolo/synthorg-backend@BACKEND_DIGEST_PH`
- Web: `ghcr.io/aureliolo/synthorg-web@WEB_DIGEST_PH`
- Sandbox: `ghcr.io/aureliolo/synthorg-sandbox@SANDBOX_DIGEST_PH`

<!-- CONTAINER_VERIFICATION_DATA
```bash
Expand All @@ -455,11 +625,16 @@ jobs:
cosign verify ghcr.io/aureliolo/synthorg-web@WEB_DIGEST_PH \
--certificate-identity-regexp='github\.com/Aureliolo/synthorg' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'

cosign verify ghcr.io/aureliolo/synthorg-sandbox@SANDBOX_DIGEST_PH \
--certificate-identity-regexp='github\.com/Aureliolo/synthorg' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'
```

```bash
gh attestation verify oci://ghcr.io/aureliolo/synthorg-backend@BACKEND_DIGEST_PH -R Aureliolo/synthorg
gh attestation verify oci://ghcr.io/aureliolo/synthorg-web@WEB_DIGEST_PH -R Aureliolo/synthorg
gh attestation verify oci://ghcr.io/aureliolo/synthorg-sandbox@SANDBOX_DIGEST_PH -R Aureliolo/synthorg
```
CONTAINER_VERIFICATION_DATA -->
BLOCK
Expand All @@ -468,6 +643,7 @@ jobs:
IMAGES=${IMAGES//VERSION_PH/$VERSION}
IMAGES=${IMAGES//BACKEND_DIGEST_PH/$BACKEND_DIGEST}
IMAGES=${IMAGES//WEB_DIGEST_PH/$WEB_DIGEST}
IMAGES=${IMAGES//SANDBOX_DIGEST_PH/$SANDBOX_DIGEST}

# Idempotent: strip existing Container Images section before appending.
# Uses awk to delete only this section (up to the next ## heading), not to EOF.
Expand Down
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ curl http://localhost:3000/api/v1/health # backend (via web proxy)

- **Backend**: 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened
- **Web**: `nginxinc/nginx-unprivileged`, Vue 3 SPA (PrimeVue + Tailwind CSS), SPA routing, API/WebSocket proxy to backend
- **Sandbox**: `synthorg-sandbox` — Python 3.14 + Node.js + git, non-root (UID 10001), agent code execution sandbox
- **Config**: all Docker files in `docker/` — Dockerfiles, compose, `.env.example`
- **CI**: `.github/workflows/docker.yml` — build → scan → push to GHCR + cosign sign + SLSA L3 provenance via `attest-build-provenance` (images only pushed after Trivy/Grype scans pass)
- **Build context**: single root `.dockerignore` (both images build with `context: .`)
- **Build context**: single root `.dockerignore` (all images build with `context: .`)
- **Tags**: CI tags images with version from `pyproject.toml` (`[tool.commitizen].version`), semver, and SHA
- **Dependabot**: auto-updates Docker image digests and versions daily

Expand Down Expand Up @@ -152,6 +153,7 @@ cli/ # Go CLI binary (cross-platform, manages Docker lifecycle)
health/ # Health check polling with retry + timeout
diagnostics/ # System info collection for bug reports
selfupdate/ # GitHub Releases self-update + binary replacement
ui/ # Styled CLI output (lipgloss-based: logo, status icons, key-value display)
scripts/ # Install scripts (install.sh, install.ps1)
testdata/ # Golden files for compose generation tests
.goreleaser.yml # GoReleaser config (cross-compile, checksums)
Expand Down Expand Up @@ -257,7 +259,7 @@ site/ # Astro landing page (synthorg.io)
- Build job runs regardless (catches build failures); deploy job skips on fork PRs (same-repo check via job output)
- Cleanup job deletes preview comment and Cloudflare deployments on PR close (pull_request events only)
- Concurrency group cancels stale builds on rapid pushes
- **Docker**: `.github/workflows/docker.yml` — builds backend + web images, pushes to GHCR, signs with cosign. SLSA L3 provenance attestations via `actions/attest-build-provenance` (SHA-pinned, Sigstore-signed, pushed to registry). Scans: Trivy (CRITICAL = hard fail, HIGH = warn-only) + Grype (critical cutoff) + CIS Docker Benchmark v1.6.0 compliance (informational). CVE triage via `.github/.trivyignore.yaml` and `.github/.grype.yaml`. Images only pushed after scans pass. Triggers on push to main and version tags (`v*`).
- **Docker**: `.github/workflows/docker.yml` — builds backend + web + sandbox images, pushes to GHCR, signs with cosign. SLSA L3 provenance attestations via `actions/attest-build-provenance` (SHA-pinned, Sigstore-signed, pushed to registry). Scans: Trivy (CRITICAL = hard fail, HIGH = warn-only) + Grype (critical cutoff) + CIS Docker Benchmark v1.6.0 compliance (informational). CVE triage via `.github/.trivyignore.yaml` and `.github/.grype.yaml`. Images only pushed after scans pass. Triggers on push to main and version tags (`v*`).
- **Matrix**: Python 3.14
- **CLI**: `.github/workflows/cli.yml` — Go lint (`golangci-lint` + `go vet`) + test (`-race -coverprofile`) + build (cross-compile matrix: linux/darwin/windows × amd64/arm64) + vulnerability check (`govulncheck`) + fuzz testing (main-only, 30s/target, `continue-on-error`, matrix over 4 packages) on `cli/**` changes. `cli-pass` gate includes fuzz result as informational warning. GoReleaser release on `v*` tags (attaches assets to the draft Release Please release). Cosign keyless signing of `checksums.txt` (`.sig` + `.pem` attached to release). SLSA L3 provenance attestations via `actions/attest-build-provenance` (SHA-pinned, Sigstore-signed) for individual binaries and checksums file. Sigstore provenance bundle (`.sigstore.json`) attached to release for OpenSSF Scorecard signed-releases scoring. Post-release step appends install instructions + checksum table + cosign verification + provenance verification instructions to the draft release notes (while still in draft — before finalize-release publishes).
- **Dependabot**: daily uv + github-actions + npm + pre-commit + docker + gomod updates, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging
Expand All @@ -282,4 +284,4 @@ site/ # Astro landing page (synthorg.io)
- **Required**: `mem0ai` (Mem0 memory backend — the default and currently only backend)
- **Install**: `uv sync` installs everything (dev group is default)
- **Web dashboard**: Node.js 20+, dependencies in `web/package.json` (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc)
- **CLI**: Go 1.26+, dependencies in `cli/go.mod` (Cobra, charmbracelet/huh)
- **CLI**: Go 1.26+, dependencies in `cli/go.mod` (Cobra, charmbracelet/huh, charmbracelet/lipgloss)
23 changes: 13 additions & 10 deletions cli/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/Aureliolo/synthorg/cli/internal/compose"
"github.com/Aureliolo/synthorg/cli/internal/config"
"github.com/Aureliolo/synthorg/cli/internal/ui"
"github.com/Aureliolo/synthorg/cli/internal/version"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -43,13 +44,12 @@ func runInit(cmd *cobra.Command, _ []string) error {
return err
}

// Warn if re-initializing over existing config (JWT secret will change).
// Warn if re-initializing over existing config (JWT secret will change).
// isInteractive() is already checked at function entry, so prompt is safe.
if existing := config.StatePath(state.DataDir); fileExists(existing) {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(),
"Warning: existing config at %s will be overwritten.\n"+
"A new JWT secret will be generated — running containers will need a restart.\n", existing)
errOut := ui.NewUI(cmd.ErrOrStderr())
errOut.Warn("Existing config at " + existing + " will be overwritten.")
errOut.Warn("A new JWT secret will be generated — running containers will need a restart.")
var proceed bool
form := huh.NewForm(huh.NewGroup(
huh.NewConfirm().Title("Overwrite existing configuration?").Value(&proceed),
Expand All @@ -67,12 +67,14 @@ func runInit(cmd *cobra.Command, _ []string) error {
return err
}

composePath := filepath.Join(safeDir, "compose.yml")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nSynthOrg initialized in %s\n", safeDir)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " Compose file: %s\n", composePath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " Config: %s\n", config.StatePath(safeDir))
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nKeep compose.yml and config.json private — they contain your JWT secret.\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Run 'synthorg start' to launch.\n")
out := ui.NewUI(cmd.OutOrStdout())
out.Logo(version.Version)
out.Success("SynthOrg initialized")
out.KeyValue("Data dir", safeDir)
out.KeyValue("Compose file", filepath.Join(safeDir, "compose.yml"))
out.KeyValue("Config", config.StatePath(safeDir))
out.Warn("Keep compose.yml and config.json private — they contain your JWT secret.")
out.Hint("Run 'synthorg start' to launch.")

return nil
}
Expand All @@ -94,6 +96,7 @@ func runSetupForm() (setupAnswers, error) {
dir: defaults.DataDir,
backendPortStr: fmt.Sprintf("%d", defaults.BackendPort),
webPortStr: fmt.Sprintf("%d", defaults.WebPort),
sandbox: defaults.Sandbox,
dockerSock: defaultDockerSock(),
logLevel: defaults.LogLevel,
genJWT: true,
Expand Down
Loading
Loading