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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ Requirements can reference figma.com design URLs; vxd builds the UI to match the
- **Interactive-once auth, communicated up front:** Figma needs an operator credential, so the FIRST Figma-referencing run breaks fire-and-forget. `vxd req` detects design URLs (`figma.ParseURLs` — design/file/proto/board forms, node-id canonicalised to `12:345`) and fails fast BEFORE any LLM spend when no credential exists, naming the interactive step: `vxd figma auth` (prints the settings URL — never auto-opens a browser — reads the token, validates via `/v1/me`, stores at `<state_dir>/figma.token` 0600) or `FIGMA_TOKEN` env. After that single session, runs are autonomous again.
- **Design pull (`figma.BuildDesignContext`):** fetches referenced nodes (`/v1/files/:key/nodes`, depth 3) + 2x PNG renders (`/v1/images/:key`) into `<repo>/.vxd-design/` — `DESIGN_CONTEXT.md` (frame inventory with dimensions, text styles, solid fills as hex) + the rendered PNGs. Per-ref failures degrade to a note; a deleted frame never strands the requirement.
- **Pipeline flow:** the planner appends the context inside `<design-reference>` data tags (instructing token derivation FROM the design); frontend stories get it in their goal prompt after `FrontendDesignBrief` (the reference overrides generic design guidance) and `copyDesignDir` puts the PNGs in each frontend worktree so agents can open them. `loadDesignContext` injection-scans the markdown (Figma layer names are third-party data — `sanitize.MatchInjectionPattern` hit drops the context loudly) and caps it at 16 KB.
- **Hygiene:** `.vxd-design/` is gitignored (adapter + repo patterns) and stripped from story branches (`stripVXDArtifactsFromBranch`) — design pulls are working material, not deliverables.
- **Hygiene:** `.vxd-design/` is gitignored (adapter + repo patterns) and stripped from story branches (`stripVXDArtifactsFromBranch`) — design pulls are working material, not deliverables. The directory is owner-only (`0o700` dir, `0o600` files, repaired on reuse) in both the repo root and worktree copies, because design refs map back to internal design files and must not be readable by other users on a shared dispatch host (`TestFigmaBuildContext_FilePermsOwnerOnly`).
- **Tests:** `internal/figma/*_test.go` (URL parse/dedupe, token resolution chain incl. the interactive-step error text, owner-only perms, httptest client + context extraction with hex/typography assertions), `engine/design_context_test.go` (load/injection-drop/size-cap/copy + planner prompt injection), `cli/figma_test.go` (auth validates-then-stores, invalid token not stored, status paths). **NXD: not ported — Figma is a cloud API; NXD stays offline-first.**

### External-audit response 2026-07-05 (docs/audit-findings-2026-07-05.md)
Expand Down
6 changes: 4 additions & 2 deletions internal/engine/design_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ func copyDesignDir(repoDir, worktreePath string) {
if err != nil {
return
}
// Owner-only, matching figma.BuildDesignContext — design context must not
// be readable by other local users on a shared dispatch host.
dst := filepath.Join(worktreePath, figma.DirName)
if err := os.MkdirAll(dst, 0o755); err != nil {
if err := os.MkdirAll(dst, 0o700); err != nil {
log.Printf("[figma] create %s in worktree: %v", figma.DirName, err)
return
}
Expand All @@ -63,7 +65,7 @@ func copyDesignDir(repoDir, worktreePath string) {
log.Printf("[figma] copy %s: %v", e.Name(), err)
continue
}
if err := os.WriteFile(filepath.Join(dst, e.Name()), data, 0o644); err != nil {
if err := os.WriteFile(filepath.Join(dst, e.Name()), data, 0o600); err != nil {
log.Printf("[figma] write %s to worktree: %v", e.Name(), err)
}
}
Expand Down
50 changes: 50 additions & 0 deletions internal/figma/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,53 @@ func TestResolveToken_UnreadableFileIsDistinguished(t *testing.T) {
t.Errorf("permission failure must be named, got %v", rerr)
}
}

// TestFigmaBuildContext_FilePermsOwnerOnly pins owner-only permissions on the
// design-context artifacts: the .vxd-design dir is 0o700 and every file in it
// (markdown + PNG renders) is 0o600. Design refs map back to internal design
// files and must not be readable by other users on a shared dispatch host.
// A pre-existing dir with looser perms is repaired.
func TestFigmaBuildContext_FilePermsOwnerOnly(t *testing.T) {
srv := newFixtureServer(t)
c := NewClient("good-token")
c.BaseURL = srv.URL

outDir := filepath.Join(t.TempDir(), DirName)
// Pre-create with loose perms — BuildDesignContext must tighten, not trust.
if err := os.MkdirAll(outDir, 0o755); err != nil {
t.Fatal(err)
}

dc, err := BuildDesignContext(t.Context(), c, []Ref{{FileKey: "KEY1", NodeID: "12:345", RawURL: "https://figma.com/design/KEY1?node-id=12-345"}}, outDir)
if err != nil {
t.Fatalf("BuildDesignContext: %v", err)
}
if dc == nil || len(dc.Images) == 0 {
t.Fatal("fixture should produce a design context with a render")
}

info, err := os.Stat(outDir)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != 0o700 {
t.Errorf("design dir perms = %o, want 700", got)
}

entries, err := os.ReadDir(outDir)
if err != nil {
t.Fatal(err)
}
if len(entries) == 0 {
t.Fatal("expected files under design dir")
}
for _, e := range entries {
fi, err := e.Info()
if err != nil {
t.Fatal(err)
}
if got := fi.Mode().Perm(); got != 0o600 {
t.Errorf("%s perms = %o, want 600", e.Name(), got)
}
}
}
13 changes: 10 additions & 3 deletions internal/figma/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@ func BuildDesignContext(ctx context.Context, c *Client, refs []Ref, outDir strin
if len(refs) == 0 {
return nil, nil
}
if err := os.MkdirAll(outDir, 0o755); err != nil {
// Owner-only: the design context carries node IDs + token metadata that
// map back to internal design files — on a shared dispatch host it must
// not be readable by other local users. Chmod repairs a pre-existing dir
// left looser by an older VXD.
if err := os.MkdirAll(outDir, 0o700); err != nil {
return nil, fmt.Errorf("create design dir: %w", err)
}
if err := os.Chmod(outDir, 0o700); err != nil {
return nil, fmt.Errorf("tighten design dir perms: %w", err)
}

var md strings.Builder
md.WriteString("## DESIGN REFERENCE (pulled from Figma)\n\n")
Expand Down Expand Up @@ -78,7 +85,7 @@ func BuildDesignContext(ctx context.Context, c *Client, refs []Ref, outDir strin
continue
}
name := fmt.Sprintf("%s-%s.png", ref.FileKey, strings.ReplaceAll(nodeID, ":", "-"))
if err := os.WriteFile(filepath.Join(outDir, name), data, 0o644); err != nil {
if err := os.WriteFile(filepath.Join(outDir, name), data, 0o600); err != nil {
log.Printf("[figma] write render %s: %v", name, err)
continue
}
Expand All @@ -95,7 +102,7 @@ func BuildDesignContext(ctx context.Context, c *Client, refs []Ref, outDir strin

// Persist the markdown next to the renders so the executor can copy the
// whole directory into worktrees.
if err := os.WriteFile(filepath.Join(outDir, ContextFileName), []byte(dc.Markdown), 0o644); err != nil {
if err := os.WriteFile(filepath.Join(outDir, ContextFileName), []byte(dc.Markdown), 0o600); err != nil {
return nil, fmt.Errorf("write design context: %w", err)
}
return dc, nil
Expand Down
Loading