PRD-71: Unified Skills Architecture
Version: 1.0 Status: Draft — Awaiting Review Priority: P1 — High Author: Gar Kavanagh + Auto CTO Created: 2026-03-04 Updated: 2026-03-04 Dependencies: PRD-22 (Skills / Git-Backed Skill Loading), PRD-42 (Plugin Marketplace), PRD-02 (Agent Factory) Branch: fix/pentest-remediation-70 (will move to dedicated branch before implementation)
Executive Summary
PRD-22 introduced Skills — Git-backed SKILL.md files that inject methodology into agent system prompts. PRD-42 introduced Plugins — zip packages uploaded to a Marketplace with security scanning and admin approval. Both systems do the same thing: teach agents HOW to do tasks by injecting content into system prompts. Having two systems creates bugs, confusion, and maintenance overhead.
This PRD unifies them into one system.
The Problem in One Sentence
An agent with a "workspace-git" skill AND an "email" plugin silently loses ALL skill content because the codebase has a mutual exclusion bug: if has_plugins → skip skills. - NO NOTHING TO DO WITH THE PROBLEM
What Changes
Two concepts: Skills + Plugins
One concept: Skills
Two distribution channels: Git import + Marketplace upload
One channel: Marketplace (admin uploads, security scans, user enables)
Three injection points: agent_factory, chatbot/service, recipe_executor
One injection point: Agent Factory (at activation time)
Runtime keyword matching to select which assigned skills to load
One rule: Load ALL assigned skills
Two security standards: 8 patterns (skills) vs 42 + LLM (plugins)
One standard: 42 patterns + LLM for everything
Skills and plugins mutually exclusive
Both always load
What Stays the Same
MarketplacePluginmodel — still the distribution/governance/approval layerPluginSecurityScanmodel — security scanning at upload timePluginContentCache— S3/Redis caching for plugin contentSkillLoader— progressive disclosure engine (used by agent_factory to load SKILL.md content)All existing Skills API endpoints (browse, assign, list)
All existing Admin Plugin API endpoints (upload, approve, reject, scan)
Marketplace UI — users browse and enable skills
Capabilities tab — users assign skills to agents
1. Problem Statement
1.1 Mutual Exclusion Bug (P0)
Severity: Production bug — silent data loss
Three files contain identical mutual exclusion logic:
Pattern in all three:
Impact: Any agent with at least one plugin assigned gets zero skill content. The user assigns skills, sees them in the UI, but the agent never receives them. No error, no warning, no log message.
1.2 Three Divergent Injection Points
Skills are loaded independently in three places, each with different behavior:
agent_factory.py
Queries agent.skills relationship, runs keyword matching + context window budgeting, loads via SkillLoader
Over-engineered selection logic
chatbot/service.py
Queries Skill table by name, reads skill.content["tools_schema"]
Bug: content column doesn't exist on Skill model
recipe_executor.py
Queries agent.skills relationship, loads via SkillLoader
Duplicates agent_factory logic
Having three injection points means three places to maintain, three places where bugs can hide, and three places that drift apart over time.
1.3 Over-Engineered Runtime Selection
The current agent_factory contains three methods that run at every agent activation:
_select_relevant_skills()— scores each assigned skill against the task using keyword matching_calculate_max_skills()— determines how many skills fit in the context window_estimate_skill_tokens()— estimates token count per skill using a hardcoded lookup table
This means: the user assigns skills to an agent, then at runtime the system second-guesses that decision and may not load some of them. It's like hiring a Java developer and then quizzing them on Java every morning to decide if they should use Java today.
The fix is simple: Load ALL assigned skills. If there are too many, warn at assignment time (Phase 4), not at runtime.
1.4 Two Security Standards
Skills (Git import via PRD-22)
scan_for_dangerous_patterns() in skill_loader.py
8 regex patterns
No
Plugins (Marketplace via PRD-42)
static_scan() + llm_security_scan() in plugin_security_scanner.py
42 patterns (code + network + filesystem + prompt injection)
Yes (Claude Haiku)
Same type of content (text injected into agent prompts), wildly different security treatment. A prompt injection attack that would be caught in a plugin sails through the skill scanner.
Have text on the assign page, advising user to just assign the skill that this agent needs, a Test Agent needs Tester Skill, JIra Admin needs the JIRA skill etc... Don't over load agent with skills they dont need.
1.5 Recipe Prompts Are Too Long
The Bug Fixer recipe is 220+ lines of step-by-step instructions that essentially "program" the LLM because there's no clean way to give agents reusable methodology. With skills properly working, the recipe says WHAT to do and the skills provide HOW.
2. Goals
G1
Eliminate mutual exclusion bug
Agent with skills AND plugins gets ALL content in system prompt
G2
Single injection point
Only agent_factory.py loads skills — down from 3 files
G3
Load ALL assigned skills
No runtime keyword matching or selection logic
G4
Unified security scanning
All skills scanned with 42 patterns + LLM before reaching marketplace
G5
Skills distributed via Marketplace
Admin uploads, security scans, user enables — one pipeline
G6
Token budget awareness
Warn users at assignment time if combined skill tokens are high
3. Architecture Overview
3.1 Current Data Flow (Broken)
3.2 Target Data Flow
3.3 Key Design Decisions
D1: No new SkillInjectionService. The previous attempt created a new service called from all 3 injection points. That's wrong — it preserves the 3-injection-point problem. Instead, fix agent_factory._build_agent_system_prompt() directly and remove skill loading from the other two files.
D2: No origin_type column. A skill is a skill regardless of whether it came from a git import, marketplace upload, or seed data. The package_slug column is enough to trace marketplace origin when needed.
D3: Plugins still load via PluginContextService. We're not deleting the plugin loading code from agent_factory — we're removing the mutual exclusion. Both skills AND plugins load. Over time, plugins will be migrated to skills via the materializer, but backward compatibility is maintained.
D4: No runtime selection logic. If a user assigns 5 skills, the agent gets all 5. Token budget warnings happen at assignment time (UI), not at runtime.
4. User Journeys
J1: Admin Publishes a Skill Package
J2: User Enables and Assigns Skills
J3: Agent Uses Skills at Runtime
5. Implementation Phases
Phase 1: Fix Agent Factory — Single Injection Point
Goal: Eliminate the mutual exclusion bug. One place loads skills.
1.1 Modify agent_factory.py — _build_agent_system_prompt()
agent_factory.py — _build_agent_system_prompt()What to delete:
_select_relevant_skills()method (~90 lines) — keyword matching, scoring, fallback_calculate_max_skills()method (~30 lines) — runtime context window budgeting_estimate_skill_tokens()method (~10 lines) — hardcoded token lookupenable_smart_skill_selectionparameter from method signaturehas_plugins/if not has_pluginsbranching (~95 lines)
What to replace it with:
File: orchestrator/modules/agents/factory/agent_factory.py
1.2 Modify chatbot/service.py — _load_agent_context()
chatbot/service.py — _load_agent_context()What to delete: The entire skill/plugin loading block (lines ~1179-1226):
has_pluginsflagPlugin loading via
PluginContextServiceSkill querying via
SkillmodelTool schema extraction from
skill.content(the buggy line)
What to replace it with: Return empty extra_context and empty skill_tools. The agent's system prompt (built by agent_factory) already contains all skill + plugin content. The chatbot doesn't need to duplicate it.
The persona and description loading stays — those are used by the chatbot for identity injection separate from skills.
File: orchestrator/consumers/chatbot/service.py
1.3 Modify recipe_executor.py — _build_system_prompt()
recipe_executor.py — _build_system_prompt()What to delete: The entire skill/plugin loading block (lines ~426-470):
has_pluginsflagPlugin loading via
PluginContextServiceSkill loading via
SkillLoader
What to replace it with: Same approach — the recipe executor should use the agent's pre-built system prompt from agent_factory rather than building its own. If it must build a prompt (for cases where agent_factory wasn't called first), include skills loading identical to agent_factory's new logic (no mutual exclusion, load all).
File: orchestrator/api/recipe_executor.py
1.4 Delete dead code
api/community_skills.py— router not registered, referencesscan_content()which doesn't exist
Phase 2: Plugin-to-Skill Materialization
Goal: When admin approves a marketplace plugin, its SKILL.md files become Skill DB records. At runtime, the unified skill loading path handles everything.
2.1 Create SkillMaterializer service
SkillMaterializer serviceNew file: orchestrator/core/services/skill_materializer.py
Called by the approve_plugin endpoint after approval succeeds:
Fetch plugin's files from S3 (already uploaded and scanned)
Find all
SKILL.mdfiles in the packageParse YAML frontmatter + markdown body (reuse
parse_yaml_frontmatterfrom skill_loader.py)For each SKILL.md, create or update a
Skillrecord:name= from frontmatterdescription= from frontmatterprompt_template= markdown bodytools_schema= from frontmattertoolskeypackage_slug= plugin.slug (new column — traces marketplace origin)workspace_id= from the plugin's context or a system workspaceis_active= True
Store created skill IDs on
plugin.materialized_skill_ids(new JSONB column)
Security scanning already happened at upload time — no re-scanning during materialization.
2.2 Modify admin_plugins.py — approve endpoint
admin_plugins.py — approve endpointAfter plugin.approval_status = "approved" and before db.commit(), call:
2.3 Modify agent_plugins.py — auto-assign materialized skills
agent_plugins.py — auto-assign materialized skillsWhen update_agent_plugins (PUT endpoint) assigns plugins to an agent, also create agent_skills junction entries for each plugin's materialized skills. This ensures the agent gets skill content through the normal skill loading path in agent_factory.
2.4 DB Migration
No origin_type column. package_slug is enough.
Phase 3: Unified Security Scanning
Goal: All skills — regardless of import method — get the same 42-pattern + LLM security treatment. Scanning happens at import/upload time only, never at runtime.
3.1 Add quick_scan() to plugin_security_scanner.py
quick_scan() to plugin_security_scanner.pyA synchronous, single-content scanner using all 42 patterns from ALL_PATTERN_GROUPS. Returns a list of findings. No LLM call (that happens during the full marketplace scan).
Used by SkillLoader when git repos are imported via the admin git import flow.
3.2 Modify skill_loader.py — upgrade scanner
skill_loader.py — upgrade scannerReplace the 8 hardcoded DANGEROUS_PATTERNS in SkillLoaderConfig and the scan_for_dangerous_patterns() function with a call to quick_scan().
Current behavior (weak): Skills that match 8 patterns are silently skipped during indexing. New behavior: Skills that match any of the 42 patterns get is_active = False + audit log entry explaining why.
This only runs during git import/re-index — never at runtime.
Phase 4: Token Budget Warning at Assignment Time
Goal: Help users make informed decisions about how many skills to assign. This is a UI/API enhancement, not a runtime blocker.
4.1 Backend: Add token estimates to skill API responses
When listing skills for an agent, include estimated token counts:
When total exceeds threshold (e.g., 15K tokens):
Token estimation: len(prompt_template) / 4 (rough chars-to-tokens approximation).
4.2 Frontend: Show warning in Capabilities tab
Yellow warning banner when total exceeds threshold. Not a blocker — informational only.
Phase 5: Example Skills (Prove the Pattern)
Goal: Create real SKILL.md files that demonstrate skills replacing verbose recipe prompts.
Skills to create:
workspace-git
Git operations: branch naming (fix/{issue_key}), commit format, PR creation
Bug Fixer, QA Engineer
jira-admin
Jira: read/create/transition tickets, priority mapping, comment format
Bug Fixer, QA Engineer
bug-fixer
Methodology: ticket → code → failing test → fix → verify → PR
Bug Fixer recipe
qa-engineer
Test running, failure classification (P0-P3), report format
QA Engineer recipe
These live in a separate skills repository (not embedded in the platform codebase). Admins import via git import or upload as a marketplace package.
Recipe Transformation Example
Before — Bug Fixer recipe (220 lines):
After — Bug Fixer recipe (~5 lines):
The agent's assigned skills (bug-fixer, workspace-git, jira-admin) provide the HOW.
6. Files Changed
modules/agents/factory/agent_factory.py
MODIFY
Remove mutual exclusion, load ALL skills + plugins, delete selection logic (3 methods)
consumers/chatbot/service.py
MODIFY
Remove independent skill/plugin loading from _load_agent_context()
api/recipe_executor.py
MODIFY
Remove independent skill/plugin loading from _build_system_prompt()
core/services/skill_materializer.py
CREATE
Convert approved plugin SKILL.md files into Skill DB records
core/services/plugin_security_scanner.py
MODIFY
Add quick_scan() synchronous single-content scanner
modules/agents/services/skill_loader.py
MODIFY
Replace 8-pattern scanner with quick_scan() (42 patterns)
api/admin_plugins.py
MODIFY
Call SkillMaterializer after plugin approval
api/agent_plugins.py
MODIFY
Auto-assign materialized skills when plugin assigned to agent
core/models/core.py
MODIFY
Add package_slug column to Skill model
core/models/marketplace_plugins.py
MODIFY
Add materialized_skill_ids column to MarketplacePlugin model
Alembic migration
CREATE
Add package_slug and materialized_skill_ids columns
api/community_skills.py
DELETE
Dead code — router not registered, references non-existent function
7. What Gets Deleted
_select_relevant_skills()
agent_factory.py
Replaced by "load all assigned"
_calculate_max_skills()
agent_factory.py
Runtime budgeting → assignment-time warning instead
_estimate_skill_tokens()
agent_factory.py
Moved to API response (Phase 4)
enable_smart_skill_selection param
agent_factory.py
No longer needed
has_plugins / if not has_plugins branching
agent_factory.py, chatbot/service.py, recipe_executor.py
The mutual exclusion bug
Skill/plugin loading block
chatbot/service.py:1179-1226
Duplicated what agent_factory does
Skill/plugin loading block
recipe_executor.py:426-470
Duplicated what agent_factory does
DANGEROUS_PATTERNS (8 patterns)
skill_loader.py
Replaced by quick_scan() (42 patterns)
scan_for_dangerous_patterns()
skill_loader.py
Replaced by quick_scan()
api/community_skills.py
entire file
Dead code
8. What Does NOT Change
MarketplacePluginmodel (except addingmaterialized_skill_ids)PluginSecurityScanmodelPluginContentCache(S3/Redis caching)SkillLoaderclass (progressive disclosure engine — still used by agent_factory)All existing Skills API endpoints
All existing Admin Plugin API endpoints
Marketplace UI
Capabilities tab UI (except adding token warning)
Agent activation flow in agent_factory (just the skill/plugin section changes)
PluginContextService(still loads plugin content — backwards compat)
9. Risks and Mitigations
Removing skill loading from chatbot/service breaks skill_tools
Tool schemas from skills stop working in chatbot
Verify agent_factory's skill_tool_schemas are passed through AgentRuntime to chatbot
recipe_executor expects its own system prompt
Recipe steps may not have agent_factory-built prompt available
Keep skill loading in recipe_executor but use the same logic as agent_factory (no mutual exclusion)
Large skill content bloats system prompts
Slower responses, higher costs
Phase 4 token warning; future: skill summarization
SkillMaterializer fails silently
Approved plugins have no materialized skills
Log errors, return materialization status in approve response
10. Testing Strategy
Unit Tests
agent_factory._build_agent_system_prompt()loads ALL assigned skills (no filtering)agent_factory._build_agent_system_prompt()loads skills AND plugin content together (mutual exclusion bug gone)SkillMaterializer.materialize_plugin()creates correct Skill records from SKILL.md filesquick_scan()detects all 42 pattern categoriesquick_scan()returns empty findings for clean contentToken estimation returns reasonable values for known skill sizes
Integration Tests
Agent with 3 skills assigned → system prompt contains all 3 skill sections
Agent with skills AND plugins → both present in system prompt
Plugin approval → SKILL.md materialized → Skill records created → available for assignment
Git import → skill scanned with 42 patterns → flagged skills get
is_active = FalsePlugin assigned to agent → materialized skills auto-assigned via agent_skills junction
Manual Validation
Create Bug Fixer agent with bug-fixer + workspace-git + jira-admin skills
Assign an email plugin to the same agent
Start a chat → verify system prompt contains BOTH skill content and plugin content
Run simplified recipe prompt → verify agent follows methodology from skills
Assign >15K tokens of skills → verify warning appears in Capabilities tab
11. Future Extensions (Not In Scope)
User-uploaded skills — once security flow is proven, allow workspace users to upload
Inline SKILL.md editor — create/edit skills in the UI
Skill versioning — pin to specific version, rollback
Skill usage analytics — which skills get loaded, how often, token cost
Frontend unification — merge skills/plugins tabs into single "Skills" view
Skill summarization — for agents with many skills, generate condensed summaries to save tokens
12. Open Questions
recipe_executor prompt source: Does recipe_executor always have access to an agent_factory-built prompt, or does it sometimes build from scratch? If from scratch, it needs its own skill loading (matching agent_factory logic, not duplicating old behavior).
Workspace ID for materialized skills: When SkillMaterializer creates Skill records from a marketplace plugin, which
workspace_idshould they get? Options: (a) a system/global workspace, (b) created per-workspace when a user enables the plugin, (c) nullable for marketplace skills.Plugin content vs skill content: After materialization, should agent_factory load BOTH the materialized skills AND the plugin's PluginContextService content? Or only the skills? Loading both would double the content. Recommendation: load skills only; stop loading plugin context once a plugin has been fully materialized.
Migration of existing plugins: Should we run SkillMaterializer retroactively on already-approved plugins? Or only for newly approved ones going forward?
Last updated

