Skip to content

feat(figma): Figma in API endpoints, screenshot hosting (Phase F — T4F.1, T4F.2)#47

Open
canonical-muhammadbassiony wants to merge 5 commits into
feat/phase-5-auth-securityfrom
feat/figma-phase-f-api
Open

feat(figma): Figma in API endpoints, screenshot hosting (Phase F — T4F.1, T4F.2)#47
canonical-muhammadbassiony wants to merge 5 commits into
feat/phase-5-auth-securityfrom
feat/figma-phase-f-api

Conversation

@canonical-muhammadbassiony

Copy link
Copy Markdown
Collaborator

Summary

Wires Figma support into the API endpoints and introduces a screenshot hosting interface, completing Phase F of spec 002. Also includes review fixes from the post-implementation audit.

Tasks Implemented

  • T4F.1: POST /api/v1/workflows now accepts optional figma_url field — piped through APIRequestWorkflowInput → orchestrator config. POST /api/v1/issues already had support.
  • T4F.2: ScreenshotHost interface (internal/artifacts/hosting.go) with three implementations:
    • LocalFileServer — serves screenshots from artifacts dir (local dev)
    • NopHost — no-op when no hosting configured
    • S3Host — stub for future S3 integration
    • HostFromEnv factory selects backend from BAUER_STATIC_BASE_URL / BAUER_S3_BUCKET
  • Static file server conditionally mounted at /static/ when BAUER_STATIC_BASE_URL is set.

Review Fixes (post-implementation audit)

  • Restored fs.Usage closure in cmd/bauer/main.go (was printing help unconditionally)
  • Added nil-agent guard in cmd/app/v1/jira.go (prevented panic)
  • Made OIDC middleware fail-closed in internal/auth/middleware.go (was silently bypassing auth)
  • Added path traversal guard in internal/artifacts/hosting.go (rejected .. prefix paths)

Files Changed

  • internal/workflow/api.goFigmaURL in APIRequest
  • internal/workflow/workflow.goFigmaURL/FigmaToken in WorkflowInput
  • internal/artifacts/hosting.goScreenshotHost interface + implementations + path traversal guard
  • cmd/app/v1/issues.goformatIssueBodyWithHosting
  • cmd/app/main.go/static/ file server route
  • cmd/bauer/main.gofs.Usage fix
  • cmd/app/v1/jira.go — nil-agent guard
  • internal/auth/middleware.go — fail-closed fix
  • .env.exampleBAUER_STATIC_BASE_URL, BAUER_S3_BUCKET, BAUER_S3_REGION

Part of the Bauer v2 stacked PR series (Branch 12 of 12).

Bauer Agent added 4 commits May 20, 2026 14:16
…nterface

T4F.1: IssueRequest and APIRequest/WorkflowRequest accept figma_url (optional)
T4F.1: handlers set cfg.FigmaURL and cfg.FigmaToken from BAUER_FIGMA_TOKEN env var
T4F.1: BAUER_FIGMA_TOKEN documented in .env.example
T4F.2: ScreenshotHost interface in internal/artifacts/hosting.go
T4F.2: LocalFileServer, NopHost, S3Host (stub) implementations
T4F.2: HostFromEnv selects backend from BAUER_STATIC_BASE_URL / BAUER_S3_BUCKET
T4F.2: IssuesHandler uses HostFromEnv; warns when no hosting configured with figma_url
T4F.2: /static/ route serves artifact dir when BAUER_STATIC_BASE_URL is configured
- cmd/bauer/main.go: restore fs.Usage closure (help text was printing unconditionally)
- cmd/app/v1/jira.go: add nil-agent guard (prevent panic on NewClient failure)
- internal/auth/middleware.go: fail-closed when OIDC configured but JWKS fetch fails
- internal/artifacts/hosting.go: reject path traversal in LocalFileServer.Host()

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes Phase F of spec 002 by wiring optional Figma context into the workflow API endpoint and introducing a screenshot hosting abstraction to support public screenshot links in generated issue bodies (plus several post-audit reliability/security fixes).

Changes:

  • Add optional figma_url support to POST /api/v1/workflows and plumb Figma URL/token through workflow input into the orchestrator config.
  • Introduce internal/artifacts/hosting.go (ScreenshotHost, LocalFileServer, NopHost, S3Host stub) and select hosting backend from env.
  • Conditionally mount a /static/ file server when BAUER_STATIC_BASE_URL is set; harden auth middleware to fail closed when JWKS resolution/fetch fails; add nil-agent guard in Jira webhook handler.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
Taskfile.yml Fix formatting/indentation for docker-run and clean tasks.
internal/workflow/workflow.go Add FigmaURL/FigmaToken to WorkflowInput and forward into Bauer config.
internal/workflow/api.go Add figma_url to workflow API request and pass through to workflow input.
internal/prompt/templates/figma-context.md Adjust template formatting/whitespace around Figma context sections.
internal/copilotcli/client.go Import grouping/formatting change.
internal/auth/middleware.go Fail-closed behavior when JWKS discovery/fetch fails (return 503).
internal/artifacts/hosting.go New screenshot hosting interface + implementations + path traversal guard.
docs/implementation-log.md Update branch chain + add post-implementation review notes.
cmd/bauer/main.go Restore fs.Usage closure; minor formatting adjustments.
cmd/app/v1/jira.go Add nil-agent guard when creating Copilot client.
cmd/app/v1/issues.go Add hosting selection + stub formatIssueBodyWithHosting and warning when hosting is not configured.
cmd/app/main.go Conditionally mount /static/ file server when BAUER_STATIC_BASE_URL is set.
.env.example Document screenshot hosting env vars.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/bauer/main.go Outdated
@@ -32,6 +32,9 @@ func main() {
branchPrefix := fs.String("branch-prefix", "", "Prefix for created branches (default: bauer)")
githubRepo := fs.String("github-repo", "", "GitHub repository in owner/repo format (required for --open-pr and --open-issue)")
figmaURL := fs.String("figma-url", "", "Figma file or design URL for design reference (requires BAUER_FIGMA_TOKEN)")
Comment thread internal/workflow/api.go Outdated
Model: firstNonEmpty(req.Model, os.Getenv("BAUER_MODEL"), "gpt-5-mini-high"),
DryRun: req.DryRun,
FigmaURL: req.FigmaURL,
FigmaToken: os.Getenv("BAUER_FIGMA_TOKEN"),
Comment thread internal/artifacts/hosting.go Outdated
Comment on lines +26 to +33
rel, err := filepath.Rel(s.ServeDir, localPath)
if err != nil {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
if strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
return s.BaseURL + "/" + filepath.ToSlash(rel), nil
Comment on lines +26 to +31
rel, err := filepath.Rel(s.ServeDir, localPath)
if err != nil {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
if strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
Comment thread cmd/app/main.go Outdated
Comment on lines +72 to +78
artsDir := os.Getenv("BAUER_ARTIFACTS_DIR")
if artsDir == "" {
artsDir = "./bauer-artifacts"
}
if baseURL := os.Getenv("BAUER_STATIC_BASE_URL"); baseURL != "" {
slog.Info("Serving artifact screenshots at /static/", slog.String("base_url", baseURL))
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(artsDir))))
Comment thread cmd/app/main.go
Comment on lines +76 to +79
if baseURL := os.Getenv("BAUER_STATIC_BASE_URL"); baseURL != "" {
slog.Info("Serving artifact screenshots at /static/", slog.String("base_url", baseURL))
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(artsDir))))
}
Comment on lines +8 to +14
{{range .Anchors}}

- **{{.NodeName}}** (node: `{{.NodeID}}`)
{{- end}}
{{end}}
{{if .Screenshots}}
{{- end}}
{{end}}
{{if .Screenshots}}

Comment on lines +25 to +32
func (s *LocalFileServer) Host(_ context.Context, localPath string) (string, error) {
rel, err := filepath.Rel(s.ServeDir, localPath)
if err != nil {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
if strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — adding unit tests for the hosting layer is a good follow-up. Out of scope for this PR which focuses on wiring the integration. Tracked as a TODO for a dedicated testing PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 10 comments.

Comment thread cmd/bauer/main.go Outdated
@@ -32,6 +32,9 @@ func main() {
branchPrefix := fs.String("branch-prefix", "", "Prefix for created branches (default: bauer)")
githubRepo := fs.String("github-repo", "", "GitHub repository in owner/repo format (required for --open-pr and --open-issue)")
figmaURL := fs.String("figma-url", "", "Figma file or design URL for design reference (requires BAUER_FIGMA_TOKEN)")
Comment thread cmd/app/main.go Outdated
Comment on lines +71 to +78
// Serve artifact screenshots at /static/ when BAUER_STATIC_BASE_URL is configured.
artsDir := os.Getenv("BAUER_ARTIFACTS_DIR")
if artsDir == "" {
artsDir = "./bauer-artifacts"
}
if baseURL := os.Getenv("BAUER_STATIC_BASE_URL"); baseURL != "" {
slog.Info("Serving artifact screenshots at /static/", slog.String("base_url", baseURL))
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(artsDir))))
Comment thread cmd/app/main.go
Comment on lines +76 to +79
if baseURL := os.Getenv("BAUER_STATIC_BASE_URL"); baseURL != "" {
slog.Info("Serving artifact screenshots at /static/", slog.String("base_url", baseURL))
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(artsDir))))
}
Comment thread cmd/app/v1/issues.go Outdated
Comment on lines +99 to +105
artsDir := firstNonEmpty(os.Getenv("BAUER_ARTIFACTS_DIR"), "./bauer-artifacts")
host := artifacts.HostFromEnv(artsDir)
if _, isNop := host.(*artifacts.NopHost); isNop && req.FigmaURL != "" {
slog.Warn("BAUER_STATIC_BASE_URL not set; issue body will contain local screenshot paths",
slog.String("run_id", result.RunID))
}
body := formatIssueBodyWithHosting(r.Context(), result, req.DocID, host)
Comment thread cmd/app/v1/issues.go
Comment on lines +124 to +132
// When a real hosting backend is configured, screenshot paths embedded in the
// issue body can be rewritten to public URLs here. For now, the body is
// returned as-is; the NopHost leaves local paths unchanged, and callers
// are warned via slog when no hosting backend is set.
_ = host
_ = ctx
return body
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional — it is a placeholder for the next phase where screenshot paths in the issue body will be rewritten to public URLs via the host backend. The function signature is in place so callers do not need to change when the rewriting logic lands.

Comment on lines 27 to +45
if err != nil {
slog.Error("Failed to resolve JWKS URL from OIDC discovery; JWT validation bypassed",
slog.Error("Failed to resolve JWKS URL from OIDC discovery; all protected endpoints will return 503",
slog.String("issuer", issuer),
slog.String("error", err.Error()),
)
return next
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "authentication service unavailable", http.StatusServiceUnavailable)
})
}

keySet, err := jwk.Fetch(context.Background(), jwksURL)
if err != nil {
slog.Error("Failed to fetch JWKS; JWT validation bypassed",
slog.Error("Failed to fetch JWKS; all protected endpoints will return 503",
slog.String("jwks_url", jwksURL),
slog.String("error", err.Error()),
)
return next
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "authentication service unavailable", http.StatusServiceUnavailable)
})
Comment on lines +26 to +32
rel, err := filepath.Rel(s.ServeDir, localPath)
if err != nil {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
if strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
Comment thread internal/artifacts/hosting.go Outdated
if strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path %q is not under serve directory %q", localPath, s.ServeDir)
}
return s.BaseURL + "/" + filepath.ToSlash(rel), nil
Comment thread .env.example Outdated
Comment on lines +46 to +49
# --- Screenshot hosting (T4F.2) ---
BAUER_STATIC_BASE_URL= # Public URL prefix for serving screenshots (e.g. https://bauer.example.com/static)
BAUER_S3_BUCKET= # S3 bucket for screenshot hosting (not yet implemented)
BAUER_S3_REGION= # AWS region for S3 bucket
Comment on lines +55 to +65
// HostFromEnv returns a ScreenshotHost configured from environment variables.
// Priority: BAUER_STATIC_BASE_URL → BAUER_S3_BUCKET → NopHost.
func HostFromEnv(serveDir string) ScreenshotHost {
if baseURL := os.Getenv("BAUER_STATIC_BASE_URL"); baseURL != "" {
return &LocalFileServer{BaseURL: baseURL, ServeDir: serveDir}
}
// S3 stub: if BAUER_S3_BUCKET is set, return S3Host (not yet functional)
if bucket := os.Getenv("BAUER_S3_BUCKET"); bucket != "" {
return &S3Host{Bucket: bucket, Region: os.Getenv("BAUER_S3_REGION")}
}
return &NopHost{}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 19 changed files in this pull request and generated 3 comments.

Comment thread cmd/app/main.go
Comment on lines +72 to +76
// Serve artifact screenshots at /static/ when BAUER_STATIC_BASE_URL is configured.
if baseURL := os.Getenv("BAUER_STATIC_BASE_URL"); baseURL != "" {
screenshotsDir := filepath.Join(cfg.ArtifactsDir, "screenshots")
slog.Info("Serving artifact screenshots at /static/", slog.String("base_url", baseURL), slog.String("dir", screenshotsDir))
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(screenshotsDir))))
Comment thread internal/github/pr.go
@@ -77,7 +77,7 @@ func CreatePR(owner, repo string, opts CreatePROptions) (string, error) {
} else {
logger.Debug("GH_TOKEN is set for PR creation", "token_prefix", ghToken[:10])
Comment on lines 36 to +38
- If the design shows a spacing or typography token, check whether an equivalent exists in the codebase.
{{if .FigmaURL}}
{{if .FigmaURL}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants