feat(org-templates): Phase 4 — atomize each role to <role>/workspace.yaml
Part 4 of 4 — terminal step of the org.yaml scalability refactor. Each role in the molecule-dev template now owns its own workspace.yaml file, colocated with the existing system-prompt.md / initial-prompt.md / idle-prompt.md / schedules/*.md. Team files shrink to a leader's own definition plus a list of !include refs. ## Platform change `resolveYAMLIncludes` now uses a TWO-ROOT model: - Path resolution is relative to the INCLUDING file's directory (natural sibling + cousin refs, C-include / Sass @import convention). - Security bound is the ORIGINAL org root (`rootDir`), preserved across all recursion depths. Sibling-dir refs like `../my-role/workspace.yaml` from a team file are now allowed (they stay inside the org template); refs that escape the root still error. Regression coverage: new `TestResolveYAMLIncludes_SiblingDirAccess` reproduces the Phase 4 pattern (team file at `teams/x.yaml` referencing `../<role>/workspace.yaml`) — fails without the fix, passes with. ## Template change Atomized 15 child workspaces across 3 team files: - `teams/research.yaml`: 58 → 30 lines; 3 children now !include refs - `teams/dev.yaml`: 222 → 38 lines; 6 children now !include refs - `teams/marketing.yaml`: 143 → 28 lines; 6 children now !include refs Each role now has `<role>/workspace.yaml` colocated with its prompts. Example `frontend-engineer/` directory: frontend-engineer/ ├── workspace.yaml (24 lines — name/role/tier/canvas/plugins/...) ├── system-prompt.md (from earlier phases) ├── initial-prompt.md ├── idle-prompt.md └── (no schedules for this role — but if added, schedules/<slug>.md) ## File-size progression across all 4 phases | State | org.yaml | total `.yaml` in tree | |---|---:|---:| | Before (main) | 1801 lines / 108 KB | 1801 / 108 KB (one file) | | After Phase 1 (#389) | 1687 | 1687 / 101 KB | | After Phase 2 (#390) | 676 | 676 / 35 KB | | After Phase 3 (#393) | 114 | 683 (1 + 6 teams) / 33 KB | | **After this PR** | **114** | **~698** (1 + 6 + 15 workspace) / 35 KB | Aggregate size is flat — the decrease came from prompt externalization in Phases 1/2; Phases 3/4 reorganize structure without adding content. The win is readability and ownership: - Every individual file fits on 1-2 screens. - Adding a new role is now: create `<role>/` dir, add `workspace.yaml` + `system-prompt.md` + prompts, add ONE `!include` line to the team file. No touching of aggregated mega-YAML. - Team files can be reviewed + merged independently. ## Tests All 10 `TestResolveYAMLIncludes_*` tests pass, including the real-template integration test (`TestResolveYAMLIncludes_RealMoleculeDev`) which now walks org.yaml → teams/pm.yaml → teams/research.yaml → ../market-analyst/ workspace.yaml and validates the full 21-role tree unmarshals cleanly. Plus all existing `TestResolvePromptRef` + `TestOrgYAML` + `TestInitialPrompt` suites stay green. ## Ops followup After merging all 4 phases and deploying, the `POST /org/import` endpoint should produce a workspace tree byte-identical to the pre-refactor state. Verify with: diff <(curl POST /org/import before) <(curl POST /org/import after) or by spot-checking: - `/configs/config.yaml` bodies across all 21 workspaces - `workspace_schedules.prompt` row values The externalization is lossless — YAML literal to file and back recovers the same string modulo trailing-whitespace normalization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2abb267d97
commit
40a69d6f87
29
org-templates/molecule-dev/backend-engineer/workspace.yaml
Normal file
29
org-templates/molecule-dev/backend-engineer/workspace.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
name: Backend Engineer
|
||||
role: >-
|
||||
Owns the Go/Gin platform layer: REST handlers, WebSocket hub,
|
||||
workspace provisioner, and A2A proxy. Manages Postgres schema,
|
||||
migrations, and parameterized query safety; Redis pub/sub,
|
||||
heartbeat TTLs, and per-workspace key cleanup. Enforces access
|
||||
control on every endpoint and structured error handling across
|
||||
all platform/ code. Primary reviewer for any platform-layer PR.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: backend-engineer
|
||||
# #266: HITL gate — Backend Engineer's scope includes destructive
|
||||
# DB migrations + runtime config changes; the @requires_approval
|
||||
# decorator stops an unattended agent from shipping a prod
|
||||
# schema mutation without a human click. UNION with defaults.
|
||||
# #280: molecule-skill-code-review — self-review rubric before
|
||||
# raising a PR (same rubric Dev Lead applies in review).
|
||||
# #303: molecule-security-scan — CVE gate at dev time, not
|
||||
# just at Security Auditor's 12h cron. Catches supply-chain
|
||||
# deps + secret patterns before they reach PR review.
|
||||
# #310: molecule-skill-llm-judge — self-gate before PR review.
|
||||
# #322: molecule-compliance — OA-03 excessive-agency cap; Backend
|
||||
# Engineer is the highest tool-call-volume role (platform PRs,
|
||||
# migrations, API changes) so a hard cap is a concrete guard
|
||||
# against runaway loops during large refactors.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-security-scan, molecule-skill-llm-judge, molecule-compliance]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
19
org-templates/molecule-dev/community-manager/workspace.yaml
Normal file
19
org-templates/molecule-dev/community-manager/workspace.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
name: Community Manager
|
||||
role: >-
|
||||
Voice-of-the-user. Triages every inbound question
|
||||
(GH Discussions, Discord, Slack), routes technical
|
||||
ones to DevRel, feature requests to PM, vulnerability
|
||||
reports to Security Auditor. Owns response-time SLAs
|
||||
and user-feedback capture.
|
||||
tier: 2
|
||||
files_dir: community-manager
|
||||
canvas: {x: 1150, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly unanswered sweep
|
||||
cron_expr: "12 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-unanswered-sweep.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
@ -0,0 +1,7 @@
|
||||
name: Competitive Intelligence
|
||||
role: Competitor tracking and feature comparison
|
||||
files_dir: competitive-intelligence
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (sibling to Market Analyst).
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
20
org-templates/molecule-dev/content-marketer/workspace.yaml
Normal file
20
org-templates/molecule-dev/content-marketer/workspace.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
name: Content Marketer
|
||||
role: >-
|
||||
Writes the blog posts, tutorials, launch write-ups,
|
||||
and case studies that drive organic traffic and
|
||||
credibility. Partners with DevRel on technical
|
||||
narratives and SEO Analyst on keyword briefs. Never
|
||||
invents benchmarks — only quotes merged PR measurements
|
||||
or labels a number as design intent.
|
||||
tier: 2
|
||||
files_dir: content-marketer
|
||||
canvas: {x: 1300, y: 250}
|
||||
plugins: [molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly topic queue refresh
|
||||
cron_expr: "41 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-topic-queue-refresh.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
44
org-templates/molecule-dev/devops-engineer/workspace.yaml
Normal file
44
org-templates/molecule-dev/devops-engineer/workspace.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
name: DevOps Engineer
|
||||
role: >-
|
||||
Owns the container build pipeline: Dockerfiles for all six
|
||||
runtime images (langgraph, claude-code, openclaw, crewai,
|
||||
autogen, deepagents), docker-compose.infra.yml for the local
|
||||
dev stack, and build-all.sh hygiene. Manages GitHub Actions
|
||||
CI (platform-build, canvas-build, python-lint,
|
||||
mcp-server-build), coverage thresholds, and secrets hygiene
|
||||
in the pipeline. Keeps infra/scripts/setup.sh and nuke.sh
|
||||
in sync whenever migrations or services change. Escalates to
|
||||
Backend Engineer for schema/runtime-config changes and to
|
||||
Frontend Engineer for canvas build failures. "Done" means:
|
||||
all CI jobs green, all images buildable from a clean checkout,
|
||||
no *.log or .env files leaked into image layers.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devops-engineer
|
||||
# #266: HITL gate — DevOps Engineer's scope covers fly deploys,
|
||||
# registry pushes, CI pipeline mutations. Any of these going
|
||||
# wrong affects every tenant; @requires_approval before
|
||||
# destructive infra ops is the point.
|
||||
# #280: molecule-skill-code-review — self-review rubric for
|
||||
# Dockerfiles, CI workflows, infra scripts before PR.
|
||||
# #322: molecule-freeze-scope — lock edits to infra/** during
|
||||
# risky operations (CI migrations, fly secret rotations, image
|
||||
# rebuilds). Plugin was an orphan for 3 weekly audits; DevOps
|
||||
# is the natural home.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-freeze-scope]
|
||||
# #247: notify on build-break — DevOps routes CI failures + infra
|
||||
# alerts via Telegram so they're not invisible until morning review.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly channel expansion survey
|
||||
cron_expr: "47 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-channel-expansion-survey.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
22
org-templates/molecule-dev/devrel-engineer/workspace.yaml
Normal file
22
org-templates/molecule-dev/devrel-engineer/workspace.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
name: DevRel Engineer
|
||||
role: >-
|
||||
Developer-facing voice of Molecule AI. Owns the code
|
||||
samples, runnable tutorials, and talk-track that turn
|
||||
"I've heard of this" into "I can run it". Partners with
|
||||
Content Marketer for blog narratives and with PMM for
|
||||
positioning. Never ships a tutorial that doesn't run
|
||||
green against the current main. On every feat: PR merge,
|
||||
produces a 20-line demo within 24 hours.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devrel-engineer
|
||||
canvas: {x: 1000, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly sample-coverage audit
|
||||
cron_expr: "18 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-sample-coverage-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
24
org-templates/molecule-dev/frontend-engineer/workspace.yaml
Normal file
24
org-templates/molecule-dev/frontend-engineer/workspace.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
name: Frontend Engineer
|
||||
role: >-
|
||||
Owns the Next.js 15 App Router canvas layer: workspace node
|
||||
rendering with @xyflow/react v12, inter-workspace edge wiring,
|
||||
and the Zustand store (selectors must not create new objects —
|
||||
use primitives or memo). Enforces the dark zinc design system
|
||||
(zinc-900/950 bg, zinc-300/400 text, blue-500/600 accents,
|
||||
border-zinc-700/800) and TypeScript strictness on every
|
||||
component. Adds 'use client' to any .tsx that uses hooks; gates
|
||||
every commit with npm run build passing clean. Escalates to
|
||||
Backend Engineer for API shape questions — never guesses.
|
||||
"Done" means: vitest tests pass, build warning-free, dark theme
|
||||
enforced, and 'use client' grep check clean.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: frontend-engineer
|
||||
# #280: self-review rubric before raising a PR. Dev Lead uses
|
||||
# the same rubric, so catching issues here cuts the review loop.
|
||||
# #310: molecule-skill-llm-judge — gate own PR against issue body
|
||||
# before requesting review ("shipped the wrong thing" early catch).
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
9
org-templates/molecule-dev/market-analyst/workspace.yaml
Normal file
9
org-templates/molecule-dev/market-analyst/workspace.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
name: Market Analyst
|
||||
role: Market sizing, trends, user research
|
||||
files_dir: market-analyst
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (#216 → #285 → #304 validated on Technical
|
||||
# Researcher 2026-04-16 02:40 UTC). Market Analyst gets the same
|
||||
# reflection-on-completion pattern tuned for market research work.
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
@ -0,0 +1,22 @@
|
||||
name: Product Marketing Manager
|
||||
role: >-
|
||||
Owns positioning, messaging, and competitive framing.
|
||||
Every piece of copy from marketing roots back to a
|
||||
PMM positioning decision. Maintains docs/marketing/
|
||||
positioning.md + competitors.md as single-source-of-
|
||||
truth. For every feat: PR merge, writes the launch
|
||||
brief within 24 hours. Pulls competitor diffs from
|
||||
ecosystem-watch.md hourly.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: product-marketing-manager
|
||||
canvas: {x: 1150, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly competitor diff
|
||||
cron_expr: "33 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-competitor-diff.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
18
org-templates/molecule-dev/qa-engineer/workspace.yaml
Normal file
18
org-templates/molecule-dev/qa-engineer/workspace.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
name: QA Engineer
|
||||
role: Testing, quality assurance, test automation
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: qa-engineer
|
||||
# QA reviews test coverage + runs llm-judge on whether test
|
||||
# deliverables actually match acceptance criteria. Issue #133.
|
||||
# #322: molecule-compliance — OA-01 prompt-injection detection
|
||||
# (in detect mode, not block) catches adversarial test payloads
|
||||
# before they slip into production. OA-03 excessive-agency caps
|
||||
# prevent runaway test loops.
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge, molecule-compliance]
|
||||
schedules:
|
||||
- name: Code quality audit (every 12h)
|
||||
cron_expr: "0 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/code-quality-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
56
org-templates/molecule-dev/security-auditor/workspace.yaml
Normal file
56
org-templates/molecule-dev/security-auditor/workspace.yaml
Normal file
@ -0,0 +1,56 @@
|
||||
name: Security Auditor
|
||||
role: >-
|
||||
Owns security posture across the full stack: Go/Gin handlers
|
||||
(SQL injection, path traversal, command injection, missing access
|
||||
control), Python workspace-template (RCE via subprocess, secrets
|
||||
in env/logs), Canvas (XSS in user-rendered content), and
|
||||
infrastructure (Docker socket exposure, secrets in images).
|
||||
Runs SAST via `gosec ./...` on every PR-touching Go file and
|
||||
`bandit -r .` on Python. Performs DAST checks against the running
|
||||
platform (`POST /workspaces/:id/a2a` CanCommunicate bypass
|
||||
attempts, CORS header validation, rate-limit enforcement).
|
||||
Escalates to Dev Lead immediately for: any SQL injection or RCE
|
||||
vector, leaked secrets in committed code, missing auth on a new
|
||||
endpoint. Files weekly summary to memory key
|
||||
`security-audit-latest`. Definition of done: every changed file
|
||||
reviewed, gosec/bandit clean (or false-positives annotated),
|
||||
no open critical findings without a linked issue.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: security-auditor
|
||||
# Security Auditor adds security-critical skills on top of defaults:
|
||||
# - molecule-skill-code-review: multi-criteria review for security-relevant PRs
|
||||
# - molecule-skill-cross-vendor-review: adversarial second opinion via non-Claude model
|
||||
# (use ONLY for noteworthy PRs — auth, billing, data)
|
||||
# - molecule-skill-llm-judge: cheap gate that catches "wrong thing shipped"
|
||||
# - molecule-security-scan (#275): supply-chain CVE gate via Snyk/pip-audit; wraps
|
||||
# builtin_tools/security_scan.py — gosec/bandit/etc
|
||||
# - molecule-hitl (#266): @requires_approval before filing critical issues
|
||||
# so false-positives don't spam the tracker
|
||||
# - molecule-compliance (#322): OWASP Top 10 for Agentic Applications — active
|
||||
# enforcement on Security Auditor's own tool calls
|
||||
# - molecule-audit (#322): immutable JSON-Lines audit log (EU AI Act Art 12/13/17)
|
||||
# — Security Auditor owns the report generation path
|
||||
plugins:
|
||||
- molecule-skill-code-review
|
||||
- molecule-skill-cross-vendor-review
|
||||
- molecule-skill-llm-judge
|
||||
- molecule-security-scan
|
||||
- molecule-hitl
|
||||
- molecule-compliance
|
||||
- molecule-audit
|
||||
# #246: notify on critical findings — Security Auditor pushes HIGH+
|
||||
# severity alerts via Telegram so they're not invisible until next
|
||||
# manual memory check.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Security audit (every 12h)
|
||||
cron_expr: "7 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/security-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
19
org-templates/molecule-dev/seo-growth-analyst/workspace.yaml
Normal file
19
org-templates/molecule-dev/seo-growth-analyst/workspace.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
name: SEO Growth Analyst
|
||||
role: >-
|
||||
Owns organic search visibility and funnel conversion.
|
||||
Metrics: keyword rank, search impressions, CTR, time-
|
||||
on-page, signup conversion. Writes SEO briefs for every
|
||||
Content post; audits Lighthouse + Core Web Vitals daily;
|
||||
proposes A/B tests for weakest funnel step.
|
||||
tier: 2
|
||||
files_dir: seo-growth-analyst
|
||||
canvas: {x: 1000, y: 400}
|
||||
plugins: [browser-automation]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Daily Lighthouse + keyword audit
|
||||
cron_expr: "23 8 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/daily-lighthouse-keyword-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
19
org-templates/molecule-dev/social-media-brand/workspace.yaml
Normal file
19
org-templates/molecule-dev/social-media-brand/workspace.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
name: Social Media Brand
|
||||
role: >-
|
||||
Owns Molecule AI's voice on X + LinkedIn and the visual
|
||||
identity across marketing surfaces. 1-2 X posts + 3-5
|
||||
replies/day; LinkedIn 2-3 posts/week. Maintains brand
|
||||
guidelines (zinc dark, blue accents, system-mono code).
|
||||
Every launch gets a 3-post thread within 24h.
|
||||
tier: 2
|
||||
files_dir: social-media-brand
|
||||
canvas: {x: 1300, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly mention monitor
|
||||
cron_expr: "27 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-mention-monitor.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
@ -29,194 +29,10 @@ schedules:
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-template-fitness-audit.md
|
||||
children:
|
||||
- name: Frontend Engineer
|
||||
role: >-
|
||||
Owns the Next.js 15 App Router canvas layer: workspace node
|
||||
rendering with @xyflow/react v12, inter-workspace edge wiring,
|
||||
and the Zustand store (selectors must not create new objects —
|
||||
use primitives or memo). Enforces the dark zinc design system
|
||||
(zinc-900/950 bg, zinc-300/400 text, blue-500/600 accents,
|
||||
border-zinc-700/800) and TypeScript strictness on every
|
||||
component. Adds 'use client' to any .tsx that uses hooks; gates
|
||||
every commit with npm run build passing clean. Escalates to
|
||||
Backend Engineer for API shape questions — never guesses.
|
||||
"Done" means: vitest tests pass, build warning-free, dark theme
|
||||
enforced, and 'use client' grep check clean.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: frontend-engineer
|
||||
# #280: self-review rubric before raising a PR. Dev Lead uses
|
||||
# the same rubric, so catching issues here cuts the review loop.
|
||||
# #310: molecule-skill-llm-judge — gate own PR against issue body
|
||||
# before requesting review ("shipped the wrong thing" early catch).
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Backend Engineer
|
||||
role: >-
|
||||
Owns the Go/Gin platform layer: REST handlers, WebSocket hub,
|
||||
workspace provisioner, and A2A proxy. Manages Postgres schema,
|
||||
migrations, and parameterized query safety; Redis pub/sub,
|
||||
heartbeat TTLs, and per-workspace key cleanup. Enforces access
|
||||
control on every endpoint and structured error handling across
|
||||
all platform/ code. Primary reviewer for any platform-layer PR.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: backend-engineer
|
||||
# #266: HITL gate — Backend Engineer's scope includes destructive
|
||||
# DB migrations + runtime config changes; the @requires_approval
|
||||
# decorator stops an unattended agent from shipping a prod
|
||||
# schema mutation without a human click. UNION with defaults.
|
||||
# #280: molecule-skill-code-review — self-review rubric before
|
||||
# raising a PR (same rubric Dev Lead applies in review).
|
||||
# #303: molecule-security-scan — CVE gate at dev time, not
|
||||
# just at Security Auditor's 12h cron. Catches supply-chain
|
||||
# deps + secret patterns before they reach PR review.
|
||||
# #310: molecule-skill-llm-judge — self-gate before PR review.
|
||||
# #322: molecule-compliance — OA-03 excessive-agency cap; Backend
|
||||
# Engineer is the highest tool-call-volume role (platform PRs,
|
||||
# migrations, API changes) so a hard cap is a concrete guard
|
||||
# against runaway loops during large refactors.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-security-scan, molecule-skill-llm-judge, molecule-compliance]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: DevOps Engineer
|
||||
role: >-
|
||||
Owns the container build pipeline: Dockerfiles for all six
|
||||
runtime images (langgraph, claude-code, openclaw, crewai,
|
||||
autogen, deepagents), docker-compose.infra.yml for the local
|
||||
dev stack, and build-all.sh hygiene. Manages GitHub Actions
|
||||
CI (platform-build, canvas-build, python-lint,
|
||||
mcp-server-build), coverage thresholds, and secrets hygiene
|
||||
in the pipeline. Keeps infra/scripts/setup.sh and nuke.sh
|
||||
in sync whenever migrations or services change. Escalates to
|
||||
Backend Engineer for schema/runtime-config changes and to
|
||||
Frontend Engineer for canvas build failures. "Done" means:
|
||||
all CI jobs green, all images buildable from a clean checkout,
|
||||
no *.log or .env files leaked into image layers.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devops-engineer
|
||||
# #266: HITL gate — DevOps Engineer's scope covers fly deploys,
|
||||
# registry pushes, CI pipeline mutations. Any of these going
|
||||
# wrong affects every tenant; @requires_approval before
|
||||
# destructive infra ops is the point.
|
||||
# #280: molecule-skill-code-review — self-review rubric for
|
||||
# Dockerfiles, CI workflows, infra scripts before PR.
|
||||
# #322: molecule-freeze-scope — lock edits to infra/** during
|
||||
# risky operations (CI migrations, fly secret rotations, image
|
||||
# rebuilds). Plugin was an orphan for 3 weekly audits; DevOps
|
||||
# is the natural home.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-freeze-scope]
|
||||
# #247: notify on build-break — DevOps routes CI failures + infra
|
||||
# alerts via Telegram so they're not invisible until morning review.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly channel expansion survey
|
||||
cron_expr: "47 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-channel-expansion-survey.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Security Auditor
|
||||
role: >-
|
||||
Owns security posture across the full stack: Go/Gin handlers
|
||||
(SQL injection, path traversal, command injection, missing access
|
||||
control), Python workspace-template (RCE via subprocess, secrets
|
||||
in env/logs), Canvas (XSS in user-rendered content), and
|
||||
infrastructure (Docker socket exposure, secrets in images).
|
||||
Runs SAST via `gosec ./...` on every PR-touching Go file and
|
||||
`bandit -r .` on Python. Performs DAST checks against the running
|
||||
platform (`POST /workspaces/:id/a2a` CanCommunicate bypass
|
||||
attempts, CORS header validation, rate-limit enforcement).
|
||||
Escalates to Dev Lead immediately for: any SQL injection or RCE
|
||||
vector, leaked secrets in committed code, missing auth on a new
|
||||
endpoint. Files weekly summary to memory key
|
||||
`security-audit-latest`. Definition of done: every changed file
|
||||
reviewed, gosec/bandit clean (or false-positives annotated),
|
||||
no open critical findings without a linked issue.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: security-auditor
|
||||
# Security Auditor adds security-critical skills on top of defaults:
|
||||
# - molecule-skill-code-review: multi-criteria review for security-relevant PRs
|
||||
# - molecule-skill-cross-vendor-review: adversarial second opinion via non-Claude model
|
||||
# (use ONLY for noteworthy PRs — auth, billing, data)
|
||||
# - molecule-skill-llm-judge: cheap gate that catches "wrong thing shipped"
|
||||
# - molecule-security-scan (#275): supply-chain CVE gate via Snyk/pip-audit; wraps
|
||||
# builtin_tools/security_scan.py — gosec/bandit/etc
|
||||
# - molecule-hitl (#266): @requires_approval before filing critical issues
|
||||
# so false-positives don't spam the tracker
|
||||
# - molecule-compliance (#322): OWASP Top 10 for Agentic Applications — active
|
||||
# enforcement on Security Auditor's own tool calls
|
||||
# - molecule-audit (#322): immutable JSON-Lines audit log (EU AI Act Art 12/13/17)
|
||||
# — Security Auditor owns the report generation path
|
||||
plugins:
|
||||
- molecule-skill-code-review
|
||||
- molecule-skill-cross-vendor-review
|
||||
- molecule-skill-llm-judge
|
||||
- molecule-security-scan
|
||||
- molecule-hitl
|
||||
- molecule-compliance
|
||||
- molecule-audit
|
||||
# #246: notify on critical findings — Security Auditor pushes HIGH+
|
||||
# severity alerts via Telegram so they're not invisible until next
|
||||
# manual memory check.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Security audit (every 12h)
|
||||
cron_expr: "7 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/security-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: QA Engineer
|
||||
role: Testing, quality assurance, test automation
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: qa-engineer
|
||||
# QA reviews test coverage + runs llm-judge on whether test
|
||||
# deliverables actually match acceptance criteria. Issue #133.
|
||||
# #322: molecule-compliance — OA-01 prompt-injection detection
|
||||
# (in detect mode, not block) catches adversarial test payloads
|
||||
# before they slip into production. OA-03 excessive-agency caps
|
||||
# prevent runaway test loops.
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge, molecule-compliance]
|
||||
schedules:
|
||||
- name: Code quality audit (every 12h)
|
||||
cron_expr: "0 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/code-quality-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: UIUX Designer
|
||||
role: User flow design, visual design review, interaction patterns, accessibility
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: uiux-designer
|
||||
# browser-automation for live canvas screenshots via Puppeteer
|
||||
# (Chrome CDP path; recipe in the cron prompt below).
|
||||
plugins: [browser-automation]
|
||||
schedules:
|
||||
- name: Hourly UI/UX audit with live screenshots
|
||||
# #306: was "5,20,35,50 * * * *" (every 15 min — 96
|
||||
# ticks/day × 8 screenshots × vision = runaway cost).
|
||||
# Hourly matches the schedule name and is sufficient
|
||||
# because the canvas UI only changes on deploys.
|
||||
cron_expr: "5 * * * *"
|
||||
enabled: true
|
||||
|
||||
prompt_file: schedules/hourly-ui-ux-audit-with-live-screenshots.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- !include ../frontend-engineer/workspace.yaml
|
||||
- !include ../backend-engineer/workspace.yaml
|
||||
- !include ../devops-engineer/workspace.yaml
|
||||
- !include ../security-auditor/workspace.yaml
|
||||
- !include ../qa-engineer/workspace.yaml
|
||||
- !include ../uiux-designer/workspace.yaml
|
||||
initial_prompt_file: initial-prompt.md
|
||||
|
||||
@ -19,125 +19,10 @@ schedules:
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
children:
|
||||
- name: DevRel Engineer
|
||||
role: >-
|
||||
Developer-facing voice of Molecule AI. Owns the code
|
||||
samples, runnable tutorials, and talk-track that turn
|
||||
"I've heard of this" into "I can run it". Partners with
|
||||
Content Marketer for blog narratives and with PMM for
|
||||
positioning. Never ships a tutorial that doesn't run
|
||||
green against the current main. On every feat: PR merge,
|
||||
produces a 20-line demo within 24 hours.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devrel-engineer
|
||||
canvas: {x: 1000, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly sample-coverage audit
|
||||
cron_expr: "18 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-sample-coverage-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Product Marketing Manager
|
||||
role: >-
|
||||
Owns positioning, messaging, and competitive framing.
|
||||
Every piece of copy from marketing roots back to a
|
||||
PMM positioning decision. Maintains docs/marketing/
|
||||
positioning.md + competitors.md as single-source-of-
|
||||
truth. For every feat: PR merge, writes the launch
|
||||
brief within 24 hours. Pulls competitor diffs from
|
||||
ecosystem-watch.md hourly.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: product-marketing-manager
|
||||
canvas: {x: 1150, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly competitor diff
|
||||
cron_expr: "33 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-competitor-diff.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Content Marketer
|
||||
role: >-
|
||||
Writes the blog posts, tutorials, launch write-ups,
|
||||
and case studies that drive organic traffic and
|
||||
credibility. Partners with DevRel on technical
|
||||
narratives and SEO Analyst on keyword briefs. Never
|
||||
invents benchmarks — only quotes merged PR measurements
|
||||
or labels a number as design intent.
|
||||
tier: 2
|
||||
files_dir: content-marketer
|
||||
canvas: {x: 1300, y: 250}
|
||||
plugins: [molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly topic queue refresh
|
||||
cron_expr: "41 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-topic-queue-refresh.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Community Manager
|
||||
role: >-
|
||||
Voice-of-the-user. Triages every inbound question
|
||||
(GH Discussions, Discord, Slack), routes technical
|
||||
ones to DevRel, feature requests to PM, vulnerability
|
||||
reports to Security Auditor. Owns response-time SLAs
|
||||
and user-feedback capture.
|
||||
tier: 2
|
||||
files_dir: community-manager
|
||||
canvas: {x: 1150, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly unanswered sweep
|
||||
cron_expr: "12 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-unanswered-sweep.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: SEO Growth Analyst
|
||||
role: >-
|
||||
Owns organic search visibility and funnel conversion.
|
||||
Metrics: keyword rank, search impressions, CTR, time-
|
||||
on-page, signup conversion. Writes SEO briefs for every
|
||||
Content post; audits Lighthouse + Core Web Vitals daily;
|
||||
proposes A/B tests for weakest funnel step.
|
||||
tier: 2
|
||||
files_dir: seo-growth-analyst
|
||||
canvas: {x: 1000, y: 400}
|
||||
plugins: [browser-automation]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Daily Lighthouse + keyword audit
|
||||
cron_expr: "23 8 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/daily-lighthouse-keyword-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Social Media Brand
|
||||
role: >-
|
||||
Owns Molecule AI's voice on X + LinkedIn and the visual
|
||||
identity across marketing surfaces. 1-2 X posts + 3-5
|
||||
replies/day; LinkedIn 2-3 posts/week. Maintains brand
|
||||
guidelines (zinc dark, blue accents, system-mono code).
|
||||
Every launch gets a 3-post thread within 24h.
|
||||
tier: 2
|
||||
files_dir: social-media-brand
|
||||
canvas: {x: 1300, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly mention monitor
|
||||
cron_expr: "27 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-mention-monitor.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- !include ../devrel-engineer/workspace.yaml
|
||||
- !include ../product-marketing-manager/workspace.yaml
|
||||
- !include ../content-marketer/workspace.yaml
|
||||
- !include ../community-manager/workspace.yaml
|
||||
- !include ../seo-growth-analyst/workspace.yaml
|
||||
- !include ../social-media-brand/workspace.yaml
|
||||
initial_prompt_file: initial-prompt.md
|
||||
|
||||
@ -24,35 +24,7 @@ schedules:
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-ecosystem-watch.md
|
||||
children:
|
||||
- name: Market Analyst
|
||||
role: Market sizing, trends, user research
|
||||
files_dir: market-analyst
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (#216 → #285 → #304 validated on Technical
|
||||
# Researcher 2026-04-16 02:40 UTC). Market Analyst gets the same
|
||||
# reflection-on-completion pattern tuned for market research work.
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Technical Researcher
|
||||
role: AI frameworks and protocol evaluation
|
||||
files_dir: technical-researcher
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop pilot (#205) — Technical Researcher is the first workspace
|
||||
# to opt in to the reflection-on-completion pattern. Measure
|
||||
# activity_logs delta over 24h, then roll to the rest of the research
|
||||
# team if it produces useful backlog-pull dispatches.
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly plugin curation
|
||||
cron_expr: "22 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-plugin-curation.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Competitive Intelligence
|
||||
role: Competitor tracking and feature comparison
|
||||
files_dir: competitive-intelligence
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (sibling to Market Analyst).
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- !include ../market-analyst/workspace.yaml
|
||||
- !include ../technical-researcher/workspace.yaml
|
||||
- !include ../competitive-intelligence/workspace.yaml
|
||||
initial_prompt_file: initial-prompt.md
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
name: Technical Researcher
|
||||
role: AI frameworks and protocol evaluation
|
||||
files_dir: technical-researcher
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop pilot (#205) — Technical Researcher is the first workspace
|
||||
# to opt in to the reflection-on-completion pattern. Measure
|
||||
# activity_logs delta over 24h, then roll to the rest of the research
|
||||
# team if it produces useful backlog-pull dispatches.
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly plugin curation
|
||||
cron_expr: "22 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-plugin-curation.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
19
org-templates/molecule-dev/uiux-designer/workspace.yaml
Normal file
19
org-templates/molecule-dev/uiux-designer/workspace.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
name: UIUX Designer
|
||||
role: User flow design, visual design review, interaction patterns, accessibility
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: uiux-designer
|
||||
# browser-automation for live canvas screenshots via Puppeteer
|
||||
# (Chrome CDP path; recipe in the cron prompt below).
|
||||
plugins: [browser-automation]
|
||||
schedules:
|
||||
- name: Hourly UI/UX audit with live screenshots
|
||||
# #306: was "5,20,35,50 * * * *" (every 15 min — 96
|
||||
# ticks/day × 8 screenshots × vision = runaway cost).
|
||||
# Hourly matches the schedule name and is sufficient
|
||||
# because the canvas UI only changes on deploys.
|
||||
cron_expr: "5 * * * *"
|
||||
enabled: true
|
||||
|
||||
prompt_file: schedules/hourly-ui-ux-audit-with-live-screenshots.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@ -21,8 +22,14 @@ const maxIncludeDepth = 16
|
||||
// Semantics:
|
||||
// - A scalar node tagged `!include` with a string value is replaced by
|
||||
// the parsed content of the referenced file.
|
||||
// - Paths are resolved relative to `baseDir` and must stay inside it
|
||||
// (same traversal defense as resolveInsideRoot).
|
||||
// - Paths resolve relative to the INCLUDING file's directory (natural
|
||||
// sibling/cousin refs, matches C-include / Sass @import convention).
|
||||
// When the including file is the top-level org.yaml, that's baseDir.
|
||||
// When it's a nested team file, that's the team file's own dir.
|
||||
// - Security: every resolved absolute path must stay inside `rootDir`
|
||||
// (the original baseDir from the top-level call). This allows natural
|
||||
// `../sibling-dir/file.yaml` refs while still blocking traversal
|
||||
// outside the org template root.
|
||||
// - Includes may be nested (a team file can !include a role file).
|
||||
// Cycles are detected via a visited set keyed on absolute path;
|
||||
// `maxIncludeDepth` caps total recursion depth as a belt-and-braces
|
||||
@ -39,7 +46,9 @@ func resolveYAMLIncludes(data []byte, baseDir string) ([]byte, error) {
|
||||
}
|
||||
|
||||
visited := map[string]bool{}
|
||||
if err := expandNode(&root, baseDir, visited, 0); err != nil {
|
||||
// At the top-level call, the "including file's dir" and the "security
|
||||
// root" are the same. They diverge as we descend into nested includes.
|
||||
if err := expandNode(&root, baseDir, baseDir, visited, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -52,9 +61,10 @@ func resolveYAMLIncludes(data []byte, baseDir string) ([]byte, error) {
|
||||
|
||||
// expandNode walks the yaml.Node tree in-place and replaces any
|
||||
// `!include`-tagged scalar with the parsed content of the referenced
|
||||
// file. Compound nodes (document / mapping / sequence) recurse; alias
|
||||
// nodes are left alone (the yaml parser already resolves them pre-tag).
|
||||
func expandNode(n *yaml.Node, baseDir string, visited map[string]bool, depth int) error {
|
||||
// file. `currentDir` is the dir of the file currently being processed
|
||||
// (used for path resolution); `rootDir` is the original org base dir
|
||||
// (used to bound the security check).
|
||||
func expandNode(n *yaml.Node, currentDir, rootDir string, visited map[string]bool, depth int) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
@ -63,11 +73,11 @@ func expandNode(n *yaml.Node, baseDir string, visited map[string]bool, depth int
|
||||
}
|
||||
|
||||
if n.Kind == yaml.ScalarNode && n.Tag == "!include" {
|
||||
return resolveIncludeScalar(n, baseDir, visited, depth)
|
||||
return resolveIncludeScalar(n, currentDir, rootDir, visited, depth)
|
||||
}
|
||||
|
||||
for _, child := range n.Content {
|
||||
if err := expandNode(child, baseDir, visited, depth); err != nil {
|
||||
if err := expandNode(child, currentDir, rootDir, visited, depth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -75,24 +85,39 @@ func expandNode(n *yaml.Node, baseDir string, visited map[string]bool, depth int
|
||||
}
|
||||
|
||||
// resolveIncludeScalar replaces an `!include <path>` scalar with the
|
||||
// parsed content of the referenced file. The replacement happens by
|
||||
// mutating *n to take on the included file's root kind/content/tag.
|
||||
func resolveIncludeScalar(n *yaml.Node, baseDir string, visited map[string]bool, depth int) error {
|
||||
// parsed content of the referenced file.
|
||||
func resolveIncludeScalar(n *yaml.Node, currentDir, rootDir string, visited map[string]bool, depth int) error {
|
||||
rel := n.Value
|
||||
if rel == "" {
|
||||
return fmt.Errorf("!include at line %d: empty path", n.Line)
|
||||
}
|
||||
if baseDir == "" {
|
||||
if rootDir == "" {
|
||||
return fmt.Errorf("!include %q at line %d requires a dir-based org template (no baseDir in inline-template mode)", rel, n.Line)
|
||||
}
|
||||
abs, err := resolveInsideRoot(baseDir, rel)
|
||||
|
||||
// Resolve relative to the including file's dir. Result must stay
|
||||
// inside the original rootDir — sibling dirs (../foo/bar.yaml) are
|
||||
// fine as long as they don't escape the template root.
|
||||
abs := filepath.Clean(filepath.Join(currentDir, rel))
|
||||
absRoot, err := filepath.Abs(rootDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("!include %q at line %d: %w", rel, n.Line, err)
|
||||
return fmt.Errorf("!include %q at line %d: cannot abs rootDir: %w", rel, n.Line, err)
|
||||
}
|
||||
if visited[abs] {
|
||||
absTarget, err := filepath.Abs(abs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("!include %q at line %d: cannot abs target: %w", rel, n.Line, err)
|
||||
}
|
||||
// Ensure target is inside root. `filepath.Rel` returns "../..." if target
|
||||
// is outside; we reject that.
|
||||
rel2, err := filepath.Rel(absRoot, absTarget)
|
||||
if err != nil || strings.HasPrefix(rel2, "..") || rel2 == ".." {
|
||||
return fmt.Errorf("!include %q at line %d: path escapes root", rel, n.Line)
|
||||
}
|
||||
|
||||
if visited[absTarget] {
|
||||
return fmt.Errorf("!include cycle detected at %q (line %d)", rel, n.Line)
|
||||
}
|
||||
data, err := os.ReadFile(abs)
|
||||
data, err := os.ReadFile(absTarget)
|
||||
if err != nil {
|
||||
return fmt.Errorf("!include %q at line %d: %w", rel, n.Line, err)
|
||||
}
|
||||
@ -108,24 +133,19 @@ func resolveIncludeScalar(n *yaml.Node, baseDir string, visited map[string]bool,
|
||||
root = root.Content[0]
|
||||
}
|
||||
|
||||
// Mark visited for the whole descent through this file, then recurse
|
||||
// so nested !includes inside the included file resolve too. Each file
|
||||
// gets its own baseDir (the directory containing it) so paths like
|
||||
// `!include role-a/initial.yaml` inside `teams/dev.yaml` resolve
|
||||
// relative to the team file's directory.
|
||||
visited[abs] = true
|
||||
defer delete(visited, abs)
|
||||
// Mark visited for the whole descent through this file, then recurse.
|
||||
// Relative refs inside the included file resolve against THAT file's
|
||||
// dir (subDir), but security stays bounded by the original rootDir.
|
||||
visited[absTarget] = true
|
||||
defer delete(visited, absTarget)
|
||||
|
||||
subDir := filepath.Dir(abs)
|
||||
if err := expandNode(root, subDir, visited, depth+1); err != nil {
|
||||
subDir := filepath.Dir(absTarget)
|
||||
if err := expandNode(root, subDir, rootDir, visited, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace the !include scalar with the resolved content in-place.
|
||||
*n = *root
|
||||
// Clear the !include tag (root's Tag is whatever kind it actually is —
|
||||
// !!map / !!seq / !!str — after unmarshal, which is correct).
|
||||
// If somehow root.Tag is still !include (shouldn't happen), drop it.
|
||||
if n.Tag == "!include" {
|
||||
n.Tag = ""
|
||||
}
|
||||
|
||||
@ -149,6 +149,44 @@ func TestResolveYAMLIncludes_InlineTemplateErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_SiblingDirAccess(t *testing.T) {
|
||||
// Phase 4 pattern: a team file at `teams/<x>.yaml` refers to a role
|
||||
// file at `<role>/workspace.yaml` via `../<role>/workspace.yaml`.
|
||||
// The ref escapes the team file's own dir but stays inside the org
|
||||
// root — this must be allowed.
|
||||
tmp := t.TempDir()
|
||||
teamsDir := filepath.Join(tmp, "teams")
|
||||
roleDir := filepath.Join(tmp, "my-role")
|
||||
if err := os.MkdirAll(teamsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(roleDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(roleDir, "workspace.yaml"), []byte("name: Cousin\ntier: 2\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(teamsDir, "parent.yaml"), []byte("name: Parent\nchildren:\n - !include ../my-role/workspace.yaml\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := []byte("workspaces:\n - !include teams/parent.yaml\n")
|
||||
out, err := resolveYAMLIncludes(src, tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("sibling-dir !include should work: %v", err)
|
||||
}
|
||||
var tmpl OrgTemplate
|
||||
if err := yaml.Unmarshal(out, &tmpl); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(tmpl.Workspaces) != 1 {
|
||||
t.Fatalf("workspaces: %d", len(tmpl.Workspaces))
|
||||
}
|
||||
kids := tmpl.Workspaces[0].Children
|
||||
if len(kids) != 1 || kids[0].Name != "Cousin" {
|
||||
t.Fatalf("children: %+v", kids)
|
||||
}
|
||||
}
|
||||
|
||||
// Integration check: after Phase 3 split, the real molecule-dev/org.yaml
|
||||
// resolves cleanly via !include and unmarshal into OrgTemplate produces
|
||||
// the full workspace tree. Guards against split regressions landing on
|
||||
|
||||
Loading…
Reference in New Issue
Block a user