@event4u/agent-config 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -5
- package/CHANGELOG.md +167 -440
- package/README.md +3 -3
- package/dist/agent-src/commands/agent-handoff.md +5 -4
- package/dist/agent-src/commands/agent-status.md +1 -0
- package/dist/agent-src/commands/agents/audit.md +1 -0
- package/dist/agent-src/commands/agents/init.md +3 -0
- package/dist/agent-src/commands/agents/optimize.md +1 -0
- package/dist/agent-src/commands/agents/user/accept.md +1 -0
- package/dist/agent-src/commands/agents/user/init.md +1 -0
- package/dist/agent-src/commands/agents/user/review.md +1 -0
- package/dist/agent-src/commands/agents/user/show.md +1 -0
- package/dist/agent-src/commands/agents/user/update.md +1 -0
- package/dist/agent-src/commands/agents/user.md +1 -0
- package/dist/agent-src/commands/agents.md +1 -0
- package/dist/agent-src/commands/analytics/prune.md +3 -2
- package/dist/agent-src/commands/analytics/show.md +3 -2
- package/dist/agent-src/commands/analytics.md +3 -2
- package/dist/agent-src/commands/analyze-reference-repo.md +1 -0
- package/dist/agent-src/commands/bug-fix.md +1 -0
- package/dist/agent-src/commands/bug-investigate.md +1 -0
- package/dist/agent-src/commands/challenge-me/vision.md +3 -2
- package/dist/agent-src/commands/challenge-me/with-docs.md +3 -2
- package/dist/agent-src/commands/challenge-me.md +3 -2
- package/dist/agent-src/commands/chat-history/import.md +9 -9
- package/dist/agent-src/commands/chat-history.md +32 -30
- package/dist/agent-src/commands/check-current-md.md +1 -0
- package/dist/agent-src/commands/commit/in-chunks.md +1 -0
- package/dist/agent-src/commands/commit.md +1 -0
- package/dist/agent-src/commands/condense.md +1 -0
- package/dist/agent-src/commands/context/create.md +1 -0
- package/dist/agent-src/commands/context/refactor.md +1 -0
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/cost-report.md +5 -4
- package/dist/agent-src/commands/council/analysis.md +3 -2
- package/dist/agent-src/commands/council/debate.md +5 -4
- package/dist/agent-src/commands/council/default.md +3 -2
- package/dist/agent-src/commands/council/design.md +3 -2
- package/dist/agent-src/commands/council/optimize.md +3 -2
- package/dist/agent-src/commands/council/pr.md +3 -2
- package/dist/agent-src/commands/council.md +4 -3
- package/dist/agent-src/commands/e2e-heal.md +1 -0
- package/dist/agent-src/commands/e2e-plan.md +1 -0
- package/dist/agent-src/commands/estimate-ticket.md +1 -0
- package/dist/agent-src/commands/feature/dev.md +1 -0
- package/dist/agent-src/commands/feature/explore.md +1 -0
- package/dist/agent-src/commands/feature/plan.md +6 -6
- package/dist/agent-src/commands/feature/refactor.md +1 -0
- package/dist/agent-src/commands/feature/roadmap.md +1 -0
- package/dist/agent-src/commands/feature.md +1 -0
- package/dist/agent-src/commands/fix/ci.md +1 -0
- package/dist/agent-src/commands/fix/portability.md +1 -0
- package/dist/agent-src/commands/fix/pr-comments.md +147 -15
- package/dist/agent-src/commands/fix/refs.md +1 -0
- package/dist/agent-src/commands/fix/seeder.md +1 -0
- package/dist/agent-src/commands/fix.md +8 -8
- package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
- package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
- package/dist/agent-src/commands/ghostwriter/list.md +1 -0
- package/dist/agent-src/commands/ghostwriter/show.md +1 -0
- package/dist/agent-src/commands/ghostwriter/write.md +1 -0
- package/dist/agent-src/commands/ghostwriter.md +1 -0
- package/dist/agent-src/commands/grill-me.md +3 -2
- package/dist/agent-src/commands/image/analyse.md +1 -0
- package/dist/agent-src/commands/image/create.md +1 -0
- package/dist/agent-src/commands/image/verify.md +1 -0
- package/dist/agent-src/commands/image.md +1 -0
- package/dist/agent-src/commands/implement-ticket.md +1 -0
- package/dist/agent-src/commands/jira-ticket.md +1 -0
- package/dist/agent-src/commands/judge/on-diff.md +1 -0
- package/dist/agent-src/commands/judge/solo.md +1 -0
- package/dist/agent-src/commands/judge/steps.md +1 -0
- package/dist/agent-src/commands/judge.md +1 -0
- package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
- package/dist/agent-src/commands/knowledge/forget.md +1 -0
- package/dist/agent-src/commands/knowledge/ingest.md +1 -0
- package/dist/agent-src/commands/knowledge/list.md +1 -0
- package/dist/agent-src/commands/knowledge.md +1 -0
- package/dist/agent-src/commands/memory/add.md +8 -6
- package/dist/agent-src/commands/memory/learn-low-impact.md +3 -2
- package/dist/agent-src/commands/memory/load.md +7 -7
- package/dist/agent-src/commands/memory/mine-session.md +39 -12
- package/dist/agent-src/commands/memory/promote.md +3 -2
- package/dist/agent-src/commands/memory/propose.md +7 -6
- package/dist/agent-src/commands/memory.md +3 -2
- package/dist/agent-src/commands/mode.md +1 -0
- package/dist/agent-src/commands/module/create.md +1 -0
- package/dist/agent-src/commands/module/explore.md +1 -0
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
- package/dist/agent-src/commands/optimize/augmentignore.md +1 -0
- package/dist/agent-src/commands/optimize/rtk.md +1 -0
- package/dist/agent-src/commands/optimize/skills.md +1 -0
- package/dist/agent-src/commands/optimize-prompt.md +1 -0
- package/dist/agent-src/commands/optimize.md +1 -0
- package/dist/agent-src/commands/orchestrate.md +1 -0
- package/dist/agent-src/commands/override/create.md +1 -0
- package/dist/agent-src/commands/override/manage.md +1 -0
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/package-reset.md +1 -0
- package/dist/agent-src/commands/package-test.md +1 -0
- package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
- package/dist/agent-src/commands/post-as/me.md +1 -0
- package/dist/agent-src/commands/post-as.md +1 -0
- package/dist/agent-src/commands/pr/create/description-only.md +1 -0
- package/dist/agent-src/commands/pr/create.md +25 -0
- package/dist/agent-src/commands/prediction-pool.md +1 -0
- package/dist/agent-src/commands/prepare-for-review.md +1 -0
- package/dist/agent-src/commands/profile/activate.md +1 -0
- package/dist/agent-src/commands/profile/deactivate.md +1 -0
- package/dist/agent-src/commands/profile/show.md +1 -0
- package/dist/agent-src/commands/profile.md +1 -0
- package/dist/agent-src/commands/project-analyze.md +1 -0
- package/dist/agent-src/commands/project-health.md +1 -0
- package/dist/agent-src/commands/quality-fix.md +1 -0
- package/dist/agent-src/commands/refine-ticket.md +1 -0
- package/dist/agent-src/commands/research/deep.md +1 -0
- package/dist/agent-src/commands/research/report.md +1 -0
- package/dist/agent-src/commands/research.md +1 -0
- package/dist/agent-src/commands/review-changes.md +1 -0
- package/dist/agent-src/commands/review-routing.md +1 -0
- package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
- package/dist/agent-src/commands/roadmap/create.md +1 -0
- package/dist/agent-src/commands/roadmap/process-full.md +1 -0
- package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
- package/dist/agent-src/commands/roadmap/process-step.md +1 -0
- package/dist/agent-src/commands/roadmap.md +1 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +1 -0
- package/dist/agent-src/commands/security-audit-config.md +84 -0
- package/dist/agent-src/commands/set-cost-profile.md +1 -0
- package/dist/agent-src/commands/skill/preview.md +1 -0
- package/dist/agent-src/commands/skill.md +1 -0
- package/dist/agent-src/commands/skills/discover.md +1 -0
- package/dist/agent-src/commands/skills.md +1 -0
- package/dist/agent-src/commands/sync-agent-settings.md +1 -0
- package/dist/agent-src/commands/sync-gitignore/fix.md +1 -0
- package/dist/agent-src/commands/sync-gitignore.md +1 -0
- package/dist/agent-src/commands/tests/create.md +1 -0
- package/dist/agent-src/commands/tests/execute.md +1 -0
- package/dist/agent-src/commands/tests.md +1 -0
- package/dist/agent-src/commands/threat-model.md +1 -0
- package/dist/agent-src/commands/update-form-request-messages.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +1 -0
- package/dist/agent-src/commands/video/from-script.md +1 -0
- package/dist/agent-src/commands/video/from-song.md +1 -0
- package/dist/agent-src/commands/video/scene.md +1 -0
- package/dist/agent-src/commands/video/stitch.md +1 -0
- package/dist/agent-src/commands/video/storyboard.md +1 -0
- package/dist/agent-src/commands/video.md +1 -0
- package/dist/agent-src/commands/work.md +1 -0
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +2 -2
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -6
- package/dist/agent-src/contexts/contracts/consumer-agents-md-guide.md +2 -2
- package/dist/agent-src/contexts/execution/rdp-gate.md +75 -0
- package/dist/agent-src/contexts/subagent-configuration.md +1 -0
- package/dist/agent-src/personas/advisors/contrarian.md +1 -1
- package/dist/agent-src/personas/advisors/executor.md +1 -1
- package/dist/agent-src/personas/advisors/expansionist.md +1 -1
- package/dist/agent-src/personas/advisors/first-principles.md +1 -1
- package/dist/agent-src/personas/advisors/outsider.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +12 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +47 -1
- package/dist/agent-src/rules/improve-before-implement.md +12 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +80 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -4
- package/dist/agent-src/rules/notes-first-reasoning.md +71 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +48 -31
- package/dist/agent-src/rules/security-sensitive-stop.md +14 -1
- package/dist/agent-src/rules/source-confidentiality.md +97 -0
- package/dist/agent-src/rules/think-before-action.md +9 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +76 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.py +171 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +14 -0
- package/dist/agent-src/skills/agent-security-review/SKILL.md +113 -0
- package/dist/agent-src/skills/agent-security-review/evals/triggers.json +51 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +3 -3
- package/dist/agent-src/skills/async-python-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +12 -11
- package/dist/agent-src/skills/command-routing/SKILL.md +1 -1
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +96 -0
- package/dist/agent-src/skills/complexity-first-planning/evals/triggers.json +16 -0
- package/dist/agent-src/skills/copilot-config/SKILL.md +3 -4
- package/dist/agent-src/skills/defense-in-depth/SKILL.md +1 -1
- package/dist/agent-src/skills/developer-like-execution/SKILL.md +5 -4
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/feature-planning/SKILL.md +2 -2
- package/dist/agent-src/skills/mcp-builder/SKILL.md +1 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +63 -17
- package/dist/agent-src/skills/prompt-engineering-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/readme-writing-package/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +119 -0
- package/dist/agent-src/skills/reasoning-orchestrator/evals/triggers.json +16 -0
- package/dist/agent-src/skills/receiving-code-review/SKILL.md +6 -6
- package/dist/agent-src/skills/refine-prompt/SKILL.md +1 -1
- package/dist/agent-src/skills/refine-ticket/SKILL.md +1 -1
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/secrets-management/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +10 -3
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +1 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
- package/dist/agent-src/templates/agents/.gitattributes.fragment +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +4 -4
- package/dist/agent-src/templates/scripts/check_memory.py +1 -2
- package/dist/agent-src/templates/scripts/check_memory_proposal.py +1 -1
- package/dist/agent-src/templates/scripts/memory_lookup.py +148 -289
- package/dist/agent-src/templates/scripts/memory_report.py +132 -2
- package/dist/agent-src/templates/scripts/memory_signal.py +7 -9
- package/dist/agent-src/templates/scripts/memory_status.py +25 -206
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/memory.py +6 -6
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +3 -3
- package/dist/agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +0 -1
- package/dist/cli/agent-config.js +31 -300
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/commands/commands.js +10 -5
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/discovery/loadManifest.js.map +1 -1
- package/dist/cli/main.js +309 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +645 -342
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +8 -5
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +149 -37
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +61 -36
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/server/routes/wizard.js +4 -3
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +18 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/adrs/cost/0001-hard-stop-hook.md +5 -5
- package/docs/adrs/memory/0001-consumer-side-snapshot.md +15 -7
- package/docs/adrs/memory/README.md +6 -5
- package/docs/adrs/router/0001-three-tier-routing.md +2 -2
- package/docs/adrs/schema/0001-json-schema-frontmatter.md +2 -2
- package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +5 -5
- package/docs/adrs/telegraph/0001-default-off-until-bench.md +3 -3
- package/docs/architecture.md +9 -9
- package/docs/archive/CHANGELOG-pre-2.2.0.md +30 -30
- package/docs/archive/CHANGELOG-pre-2.25.0.md +1 -1
- package/docs/archive/CHANGELOG-pre-4.5.0.md +1 -1
- package/docs/archive/CHANGELOG-pre-6.0.0.md +473 -0
- package/docs/benchmark.md +54 -53
- package/docs/benchmarks.md +2 -2
- package/docs/case-studies/{frontend-design-vs-ui-ux-pro-max.md → frontend-design-positioning.md} +4 -4
- package/docs/catalog.md +20 -13
- package/docs/command-flows.md +90 -92
- package/docs/contracts/adr-layout.md +2 -3
- package/docs/contracts/adr-level-6-productization.md +1 -1
- package/docs/contracts/ai-council-config.md +42 -7
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/cost-enforcement.md +1 -1
- package/docs/contracts/cost-summary-schema.md +1 -1
- package/docs/contracts/daily-workspace.md +1 -0
- package/docs/contracts/discovery-manifest.schema.json +4 -2
- package/docs/contracts/explain-modes.md +1 -1
- package/docs/contracts/implement-ticket-flow.md +6 -7
- package/docs/contracts/mcp-tool-inventory.md +10 -10
- package/docs/contracts/measurement-baseline.md +1 -1
- package/docs/contracts/memory-visibility-v1.md +1 -5
- package/docs/contracts/namespace.md +1 -1
- package/docs/contracts/persona-schema.md +1 -1
- package/docs/contracts/rule-interactions.md +1 -1
- package/docs/contracts/smoke-contracts.md +1 -1
- package/docs/contracts/universal-skills.md +0 -1
- package/docs/contracts/workspace-boundary.md +84 -0
- package/docs/customization.md +3 -3
- package/docs/decisions/ADR-009-event4u-namespace.md +1 -1
- package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +1 -1
- package/docs/decisions/ADR-026-explain-mode-translation.md +1 -1
- package/docs/decisions/ADR-088-no-external-runtime-federation.md +26 -27
- package/docs/decisions/ADR-090-visibility-command-frontmatter-field.md +95 -0
- package/docs/decisions/ADR-091-split-meta-capability-packs.md +113 -0
- package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +93 -0
- package/docs/decisions/ADR-093-ai-council-config-user-global.md +111 -0
- package/docs/decisions/ADR-094-agent-memory-layer-removal.md +94 -0
- package/docs/decisions/ADR-095-workspace-boundary-contract.md +108 -0
- package/docs/decisions/INDEX.md +6 -0
- package/docs/development.md +5 -7
- package/docs/getting-started.md +4 -4
- package/docs/guidelines/agent-infra/5w2h-analysis.md +1 -1
- package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
- package/docs/guidelines/agent-infra/corpus-grounding-authoring.md +1 -1
- package/docs/guidelines/agent-infra/critical-thinking.md +1 -1
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +1 -5
- package/docs/guidelines/agent-infra/first-principles.md +1 -1
- package/docs/guidelines/agent-infra/frontier-reasoning-operating-profile.md +164 -0
- package/docs/guidelines/agent-infra/inversion-thinking.md +1 -1
- package/docs/guidelines/agent-infra/ios-simulator-guide.md +9 -14
- package/docs/guidelines/agent-infra/mcp-request-signing.md +19 -22
- package/docs/guidelines/agent-infra/memory-access.md +25 -31
- package/docs/guidelines/agent-infra/mental-models.md +1 -1
- package/docs/guidelines/agent-infra/model-recommendation.md +29 -0
- package/docs/guidelines/agent-infra/scqa-framework.md +3 -3
- package/docs/guidelines/agent-infra/security-lint-containment.md +81 -0
- package/docs/guidelines/agent-infra/six-hats.md +1 -1
- package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +72 -0
- package/docs/installation.md +1 -1
- package/docs/mcp.md +2 -2
- package/docs/parity/{bench-ruflo.json → bench-external.json} +10 -10
- package/docs/parity/{ruflo.md → external-runtime.md} +9 -9
- package/docs/quality.md +3 -3
- package/docs/safety.md +3 -3
- package/docs/skills-catalog.md +4 -1
- package/llms.txt +3 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +65 -3
- package/src/config/discovery/packs.yml +29 -0
- package/src/config/discovery/workspaces.yml +3 -1
- package/src/config/gitignore-block.txt +6 -0
- package/src/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/src/scripts/_cli/cmd_doctor.py +99 -13
- package/src/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.py +227 -0
- package/src/scripts/_lib/global_deploy_inventory.py +39 -9
- package/src/scripts/_lib/link_crypto.py +206 -0
- package/src/scripts/_lib/security_lint.py +228 -0
- package/src/scripts/ai_council/clients.py +2 -2
- package/src/scripts/ai_council/config.py +55 -0
- package/src/scripts/audit_adr_coverage.py +0 -2
- package/src/scripts/audit_command_surface.py +18 -5
- package/src/scripts/audit_mcp_tools.py +2 -2
- package/src/scripts/audit_skill_descriptions.py +2 -2
- package/src/scripts/bench_ab_clone.py +62 -12
- package/src/scripts/bench_ab_task_runner.py +475 -30
- package/src/scripts/bench_ab_v2_run.py +247 -0
- package/src/scripts/bench_ab_v2_stats.py +347 -0
- package/src/scripts/bench_run.py +1 -1
- package/src/scripts/build_discovery_manifest.py +10 -0
- package/src/scripts/check_bite_sized_granularity.py +1 -2
- package/src/scripts/check_memory.py +49 -63
- package/src/scripts/check_memory_proposal.py +1 -1
- package/src/scripts/check_no_external_sources.py +101 -0
- package/src/scripts/check_references.py +2 -0
- package/src/scripts/cost_by_conversation.py +1 -1
- package/src/scripts/council_cli.py +28 -14
- package/src/scripts/external_sources_denylist.json +91 -0
- package/src/scripts/hook_manifest.yaml +14 -6
- package/src/scripts/injection_scan_hook.py +145 -0
- package/src/scripts/install-hooks.sh +11 -0
- package/src/scripts/install.py +88 -13
- package/src/scripts/lint_agent_security.py +112 -0
- package/src/scripts/lint_bench_ab.py +5 -4
- package/src/scripts/lint_command_tiers.py +63 -22
- package/src/scripts/lint_discovery_vocabulary.py +2 -0
- package/src/scripts/lint_empty_roadmaps.py +80 -0
- package/src/scripts/lint_hidden_unicode.py +132 -0
- package/src/scripts/lint_instruction_smuggling.py +107 -0
- package/src/scripts/lint_marketplace.py +1 -1
- package/src/scripts/lint_mcp_config_security.py +124 -0
- package/src/scripts/lint_skill_frontmatter_safety.py +144 -0
- package/src/scripts/lint_workspace_boundary.py +122 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +2 -3
- package/src/scripts/mcp_server/tools.py +8 -32
- package/src/scripts/memory_lookup.py +27 -296
- package/src/scripts/memory_report.py +1 -23
- package/src/scripts/memory_signal.py +6 -53
- package/src/scripts/memory_status.py +25 -206
- package/src/scripts/mine_session.py +118 -41
- package/src/scripts/pack_dependency_allowlist.json +2 -2
- package/src/scripts/render_benchmark_md.py +141 -52
- package/src/scripts/schemas/command.schema.json +6 -1
- package/src/scripts/security_audit_config.py +153 -0
- package/dist/agent-src/commands/chat-history/learn.md +0 -184
- package/dist/agent-src/commands/chat-history/show.md +0 -113
- package/dist/agent-src/commands/fix/pr-bot-comments.md +0 -157
- package/dist/agent-src/commands/fix/pr-developer-comments.md +0 -163
- package/dist/agent-src/templates/agents/memory/architecture-decisions.example.yml +0 -95
- package/docs/contracts/agent-memory-contract.md +0 -159
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hard-Gate linter: no empty roadmap files under ``agents/roadmaps/``.
|
|
3
|
+
|
|
4
|
+
A roadmap ``.md`` that is 0 bytes (or only whitespace) is never valid — it
|
|
5
|
+
carries no goal, no phases, no content. Empty roadmaps have been introduced
|
|
6
|
+
twice by an external ``chore: add uncomitted roadmaps`` auto-commit that
|
|
7
|
+
staged 0-byte placeholders (2026-06-13: ``road-to-6.0.0-final-readiness.md``
|
|
8
|
+
and ``road-to-reaping-catches-pre-inventory-orphans.md``). The local
|
|
9
|
+
pre-commit dashboard check did not catch them — the dashboard generator
|
|
10
|
+
silently skips empty files — and the commits bypassed it anyway. This linter
|
|
11
|
+
is the authoritative backstop: it fails CI (and the pre-commit hook) so an
|
|
12
|
+
empty roadmap can never reach ``main`` again, regardless of how it was staged.
|
|
13
|
+
|
|
14
|
+
Scope: every ``*.md`` under ``agents/roadmaps/`` (active, ``archive/``,
|
|
15
|
+
``skipped/``, ``stubs/``, ``later/``) — empty is invalid everywhere. The
|
|
16
|
+
``.gitkeep`` placeholders are not ``.md`` and are ignored.
|
|
17
|
+
|
|
18
|
+
Cap: ≤ 120 LOC, stdlib only. Hooked into ``task ci`` / ``task ci-fast`` via
|
|
19
|
+
``task lint-empty-roadmaps`` and into the pre-commit hook.
|
|
20
|
+
|
|
21
|
+
Exit codes: 0 = clean, 1 = at least one empty roadmap found.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
QUIET = "--quiet" in sys.argv
|
|
29
|
+
|
|
30
|
+
ROADMAP_DIR = Path("agents/roadmaps")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _repo_root() -> Path:
|
|
34
|
+
"""Walk up from CWD until a dir containing ``agents/roadmaps`` is found."""
|
|
35
|
+
here = Path.cwd()
|
|
36
|
+
for candidate in (here, *here.parents):
|
|
37
|
+
if (candidate / ROADMAP_DIR).is_dir():
|
|
38
|
+
return candidate
|
|
39
|
+
return here
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_empty_roadmaps(root: Path) -> list[Path]:
|
|
43
|
+
base = root / ROADMAP_DIR
|
|
44
|
+
if not base.is_dir():
|
|
45
|
+
return []
|
|
46
|
+
empties: list[Path] = []
|
|
47
|
+
for md in sorted(base.rglob("*.md")):
|
|
48
|
+
try:
|
|
49
|
+
text = md.read_text(encoding="utf-8")
|
|
50
|
+
except (OSError, UnicodeDecodeError):
|
|
51
|
+
# Unreadable / binary -> not an empty-text file; leave to other gates.
|
|
52
|
+
continue
|
|
53
|
+
if text.strip() == "":
|
|
54
|
+
empties.append(md.relative_to(root))
|
|
55
|
+
return empties
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> int:
|
|
59
|
+
root = _repo_root()
|
|
60
|
+
empties = find_empty_roadmaps(root)
|
|
61
|
+
|
|
62
|
+
if not empties:
|
|
63
|
+
if not QUIET:
|
|
64
|
+
print("✅ lint-empty-roadmaps: no empty roadmap files.")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
print("❌ lint-empty-roadmaps: empty (0-byte / whitespace-only) roadmap file(s):")
|
|
68
|
+
for rel in empties:
|
|
69
|
+
print(f" {rel}")
|
|
70
|
+
print()
|
|
71
|
+
print(" A roadmap with no content is invalid. Either:")
|
|
72
|
+
print(" • restore the intended content, or")
|
|
73
|
+
print(" • delete the file (if its content lives in agents/roadmaps/archive/).")
|
|
74
|
+
print(" Empty roadmap stubs are usually an artefact of an auto-commit that")
|
|
75
|
+
print(" staged a 0-byte placeholder — remove it; do not commit it.")
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""P1.1 — hidden-Unicode / smuggling-codepoint linter (road-to-security-pillar.md).
|
|
3
|
+
|
|
4
|
+
Detects the invisible-character class used by the "rules-file backdoor" attack:
|
|
5
|
+
instructions a human reviewer cannot see but the model reads. The codepoint set
|
|
6
|
+
covers bidi controls (Trojan Source), zero-width / format chars, the Unicode Tag
|
|
7
|
+
block, variation-selector runs, Private Use Area, and stray C0/C1 controls.
|
|
8
|
+
|
|
9
|
+
Scope: every `.md` under src/{skills,rules,agent-src,domains} + frontmatter.
|
|
10
|
+
Containment: a real teaching doc never needs the *actual* invisible char (it
|
|
11
|
+
writes ``U+200B`` as text), so this linter scans even inside ordinary code
|
|
12
|
+
fences; only a ```security-example fence or a `security-lint: allow
|
|
13
|
+
hidden-unicode` pragma exempts a file/region.
|
|
14
|
+
|
|
15
|
+
Exit 0 clean, 1 on any blocking finding. ``--fix`` writes an NFKC-normalised,
|
|
16
|
+
zero-width-stripped sibling ``<file>.sanitized`` for human review (never
|
|
17
|
+
auto-applied).
|
|
18
|
+
|
|
19
|
+
Usage: python3 src/scripts/lint_hidden_unicode.py [--json] [--fix]
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import sys
|
|
25
|
+
import unicodedata
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
29
|
+
from _lib import security_lint as sl # noqa: E402
|
|
30
|
+
|
|
31
|
+
CHECK = "hidden-unicode"
|
|
32
|
+
|
|
33
|
+
# (name, predicate over a single codepoint) — ordered by specificity.
|
|
34
|
+
_BIDI = {0x202A, 0x202B, 0x202C, 0x202D, 0x202E, 0x2066, 0x2067, 0x2068, 0x2069,
|
|
35
|
+
0x200E, 0x200F, 0x061C}
|
|
36
|
+
_ZERO_WIDTH = {0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF, 0x00AD}
|
|
37
|
+
_DEPRECATED = {0x206A, 0x206B, 0x206C, 0x206D, 0x206E, 0x206F, 0xFFF9, 0xFFFA, 0xFFFB}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _classify(cp: int) -> str | None:
|
|
41
|
+
if cp in _BIDI:
|
|
42
|
+
return "bidi-control"
|
|
43
|
+
if cp in _ZERO_WIDTH:
|
|
44
|
+
return "zero-width"
|
|
45
|
+
if 0xE0000 <= cp <= 0xE007F:
|
|
46
|
+
return "unicode-tag"
|
|
47
|
+
if cp in _DEPRECATED:
|
|
48
|
+
return "deprecated-format"
|
|
49
|
+
if 0xE000 <= cp <= 0xF8FF or 0xF0000 <= cp <= 0xFFFFD or 0x100000 <= cp <= 0x10FFFD:
|
|
50
|
+
return "private-use-area"
|
|
51
|
+
# C0/C1 controls except tab/newline/CR
|
|
52
|
+
if (0x00 <= cp <= 0x1F or 0x7F <= cp <= 0x9F) and cp not in (0x09, 0x0A, 0x0D):
|
|
53
|
+
return "control-char"
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Variation selectors flagged only in runs of >=3 on one line (steganography).
|
|
58
|
+
# Restricted to the SUPPLEMENTARY block (U+E0100–E01EF): the standard selectors
|
|
59
|
+
# U+FE00–FE0F are legitimate emoji/text presentation (e.g. ❌️ ✅️ ⚠️) and runs
|
|
60
|
+
# of them are normal, so they are NOT a steganography signal.
|
|
61
|
+
_VS = set(range(0xE0100, 0xE01F0))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _scan(sf: sl.ScannedFile) -> list[sl.Finding]:
|
|
65
|
+
if sf.pragma_allows(CHECK):
|
|
66
|
+
return []
|
|
67
|
+
out: list[sl.Finding] = []
|
|
68
|
+
for lineno, text in sf.iter_lines(skip_example_fence=True):
|
|
69
|
+
vs_run = 0
|
|
70
|
+
for ch in text:
|
|
71
|
+
cp = ord(ch)
|
|
72
|
+
if cp in _VS:
|
|
73
|
+
vs_run += 1
|
|
74
|
+
continue
|
|
75
|
+
kind = _classify(cp)
|
|
76
|
+
if kind:
|
|
77
|
+
name = unicodedata.name(ch, "<unnamed>")
|
|
78
|
+
out.append(sl.Finding(
|
|
79
|
+
path=sf.rel, line=lineno, check=CHECK, severity="HIGH",
|
|
80
|
+
message=f"{kind} U+{cp:04X} ({name})",
|
|
81
|
+
weight=sf.weight,
|
|
82
|
+
))
|
|
83
|
+
if vs_run >= 3:
|
|
84
|
+
out.append(sl.Finding(
|
|
85
|
+
path=sf.rel, line=lineno, check=CHECK, severity="HIGH",
|
|
86
|
+
message=f"variation-selector run x{vs_run} (steganography signature)",
|
|
87
|
+
weight=sf.weight,
|
|
88
|
+
))
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sanitize(path: Path) -> Path:
|
|
93
|
+
raw = path.read_text(encoding="utf-8", errors="surrogatepass")
|
|
94
|
+
cleaned = "".join(
|
|
95
|
+
ch for ch in raw
|
|
96
|
+
if _classify(ord(ch)) is None and ord(ch) not in _VS
|
|
97
|
+
)
|
|
98
|
+
cleaned = unicodedata.normalize("NFKC", cleaned)
|
|
99
|
+
out = path.with_suffix(path.suffix + ".sanitized")
|
|
100
|
+
out.write_text(cleaned, encoding="utf-8")
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
ap = argparse.ArgumentParser(description=__doc__, epilog=sl.GUIDELINE_EPILOG)
|
|
106
|
+
ap.add_argument("--json", action="store_true")
|
|
107
|
+
ap.add_argument("--fix", action="store_true",
|
|
108
|
+
help="write a sanitised sibling for each flagged file (review only)")
|
|
109
|
+
args = ap.parse_args()
|
|
110
|
+
|
|
111
|
+
findings: list[sl.Finding] = []
|
|
112
|
+
flagged: set[Path] = set()
|
|
113
|
+
for sf in sl.iter_corpus():
|
|
114
|
+
hits = _scan(sf)
|
|
115
|
+
findings.extend(hits)
|
|
116
|
+
if hits:
|
|
117
|
+
flagged.add(sf.path)
|
|
118
|
+
|
|
119
|
+
if args.fix:
|
|
120
|
+
for p in sorted(flagged):
|
|
121
|
+
print(f" fixed → {_sanitize(p).relative_to(sl.ROOT)}")
|
|
122
|
+
|
|
123
|
+
if args.json:
|
|
124
|
+
import json
|
|
125
|
+
print(json.dumps([f.__dict__ for f in findings], indent=2))
|
|
126
|
+
return 1 if any(f.is_fail for f in findings) else 0
|
|
127
|
+
|
|
128
|
+
return sl.report(findings, check_label="hidden-unicode")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""P1.2 — instruction-smuggling / suppression-phrase linter (road-to-security-pillar.md).
|
|
3
|
+
|
|
4
|
+
Detects instructions smuggled into rule/skill/command bodies (and MCP tool
|
|
5
|
+
descriptions) that try to steer or silence the agent — the prose half of the
|
|
6
|
+
"rules-file backdoor" + MCP "tool-poisoning" attack classes.
|
|
7
|
+
|
|
8
|
+
Precision over recall (false-positive containment, P1.5): the corpus
|
|
9
|
+
*legitimately* quotes attack strings as teaching material, so this linter only
|
|
10
|
+
treats a pattern as a **blocking (HIGH)** smuggle when it appears as live
|
|
11
|
+
**prose** — outside any code fence and outside inline `backtick` spans, where a
|
|
12
|
+
teaching doc would put a quoted example. Shell / secret-path signatures are
|
|
13
|
+
common in legitimate security docs, so they are **MED warnings**, never blocking.
|
|
14
|
+
|
|
15
|
+
- HIGH (fail): disclosure-suppression imperative ("do not mention/tell the
|
|
16
|
+
user", "without explaining") or an imperative injection tag (`<IMPORTANT>`,
|
|
17
|
+
"ignore previous instructions", "you are now …") in live prose.
|
|
18
|
+
- MED (warn): pipe-to-shell, reverse-shell, or secret-path read in live prose.
|
|
19
|
+
|
|
20
|
+
Scope: every `.md` under src/{skills,rules,agent-src,domains}. Exempt: a
|
|
21
|
+
```security-example fence or a `security-lint: allow instruction-smuggling` pragma.
|
|
22
|
+
|
|
23
|
+
Usage: python3 src/scripts/lint_instruction_smuggling.py [--json]
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
33
|
+
from _lib import security_lint as sl # noqa: E402
|
|
34
|
+
|
|
35
|
+
CHECK = "instruction-smuggling"
|
|
36
|
+
|
|
37
|
+
_INLINE_CODE = re.compile(r"`[^`]*`")
|
|
38
|
+
|
|
39
|
+
# HIGH — disclosure suppression as an imperative addressed to the agent.
|
|
40
|
+
_SUPPRESS = re.compile(
|
|
41
|
+
r"\b(do not|don'?t|never)\s+(mention|tell|inform|disclose|reveal|notify)\b"
|
|
42
|
+
r"[^.]{0,40}\b(the )?(user|human|reviewer|operator|owner)\b"
|
|
43
|
+
r"|without (telling|informing|notifying|explaining to) (the )?(user|them|anyone)",
|
|
44
|
+
re.IGNORECASE,
|
|
45
|
+
)
|
|
46
|
+
# HIGH — imperative injection / role-takeover tags and phrases.
|
|
47
|
+
_INJECT = re.compile(
|
|
48
|
+
r"<\s*(important|system|admin|secret|critical)\s*>"
|
|
49
|
+
r"|ignore (all |the )?(previous|prior|above) (instructions|prompts|rules)"
|
|
50
|
+
r"|disregard (all |the )?(previous|prior|above)"
|
|
51
|
+
r"|you are now (a|an|the)\b"
|
|
52
|
+
r"|new system prompt",
|
|
53
|
+
re.IGNORECASE,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# MED — execution / exfil signatures (common in legit security docs → warn only).
|
|
57
|
+
_MED = [
|
|
58
|
+
(re.compile(r"\bcurl\b[^\n|]*\|\s*(ba|z|fi)?sh\b", re.IGNORECASE), "pipe-to-shell (curl|sh)"),
|
|
59
|
+
(re.compile(r"\bwget\b[^\n|]*\|\s*(ba|z|fi)?sh\b", re.IGNORECASE), "pipe-to-shell (wget|sh)"),
|
|
60
|
+
(re.compile(r"\b(socat|nc)\b[^\n]*\b(exec|-e)\b|/dev/tcp/", re.IGNORECASE), "reverse-shell signature"),
|
|
61
|
+
(re.compile(r"(~/\.ssh/id_[rd]sa|/etc/shadow|\.aws/credentials)"), "secret-path read"),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _strip_inline_code(text: str) -> str:
|
|
66
|
+
"""Blank out inline `code` spans so quoted examples don't trip prose checks."""
|
|
67
|
+
return _INLINE_CODE.sub(lambda m: " " * len(m.group(0)), text)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _scan(sf: sl.ScannedFile) -> list[sl.Finding]:
|
|
71
|
+
if sf.pragma_allows(CHECK):
|
|
72
|
+
return []
|
|
73
|
+
out: list[sl.Finding] = []
|
|
74
|
+
# prose = lines outside ANY fence; inline-code spans blanked.
|
|
75
|
+
for lineno, text in sf.iter_lines(skip_example_fence=True, skip_any_fence=True):
|
|
76
|
+
prose = _strip_inline_code(text)
|
|
77
|
+
if _SUPPRESS.search(prose):
|
|
78
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "HIGH",
|
|
79
|
+
"disclosure-suppression imperative in prose", sf.weight))
|
|
80
|
+
if _INJECT.search(prose):
|
|
81
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "HIGH",
|
|
82
|
+
"injection / role-takeover phrase in prose", sf.weight))
|
|
83
|
+
for rx, label in _MED:
|
|
84
|
+
if rx.search(prose):
|
|
85
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "MED",
|
|
86
|
+
f"{label} in prose (verify intent)", sf.weight))
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> int:
|
|
91
|
+
ap = argparse.ArgumentParser(description=__doc__, epilog=sl.GUIDELINE_EPILOG)
|
|
92
|
+
ap.add_argument("--json", action="store_true")
|
|
93
|
+
args = ap.parse_args()
|
|
94
|
+
|
|
95
|
+
findings: list[sl.Finding] = []
|
|
96
|
+
for sf in sl.iter_corpus():
|
|
97
|
+
findings.extend(_scan(sf))
|
|
98
|
+
|
|
99
|
+
if args.json:
|
|
100
|
+
import json
|
|
101
|
+
print(json.dumps([f.__dict__ for f in findings], indent=2))
|
|
102
|
+
return 1 if any(f.is_fail for f in findings) else 0
|
|
103
|
+
return sl.report(findings, check_label="instruction-smuggling")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
raise SystemExit(main())
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Lint .claude-plugin/marketplace.json for the event4u/agent-config package.
|
|
4
4
|
|
|
5
5
|
Validates the Claude Code Plugin Marketplace manifest against the canonical
|
|
6
|
-
shape
|
|
6
|
+
manifest shape:
|
|
7
7
|
|
|
8
8
|
- Required top-level fields: name, owner, metadata, plugins
|
|
9
9
|
- owner must have name + email
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""P1.3 — MCP-config security linter (road-to-security-pillar.md). OWASP ASI04.
|
|
3
|
+
|
|
4
|
+
Scans shipped MCP configuration — named config files (``*.mcp.json``,
|
|
5
|
+
``mcp.json``, ``claude_desktop_config.json*``) and fenced ```json blocks that
|
|
6
|
+
declare ``mcpServers`` — for the supply-chain smells behind MCP tool-poisoning
|
|
7
|
+
and rug-pull attacks.
|
|
8
|
+
|
|
9
|
+
- HIGH (fail): a **real inline secret value** in a shipped config (an actual
|
|
10
|
+
key, not the bare prefix used as documentation). Secrets belong in
|
|
11
|
+
``${env:VAR}``.
|
|
12
|
+
- MED (warn): ``npx -y`` auto-install, unpinned server version, ``autoApprove``
|
|
13
|
+
/ ``enableAllProjectMcpServers``, ``0.0.0.0`` binding, shell metacharacters in
|
|
14
|
+
args, omnibus scopes (``*`` / ``all`` / ``full-access``), ``*_BASE_URL`` in a
|
|
15
|
+
project-scoped env. These are smells, not leaks — templates legitimately show
|
|
16
|
+
them, so they warn (and weight 0.25x in example/template files per P1.5).
|
|
17
|
+
|
|
18
|
+
Usage: python3 src/scripts/lint_mcp_config_security.py [--json]
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
28
|
+
from _lib import security_lint as sl # noqa: E402
|
|
29
|
+
|
|
30
|
+
CHECK = "mcp-config-security"
|
|
31
|
+
|
|
32
|
+
_NAME_HINTS = re.compile(r"(^|/)(\.mcp\.json|mcp\.json|claude_desktop_config\.json)")
|
|
33
|
+
|
|
34
|
+
# Real secret VALUES (prefix + enough key chars to be a live credential).
|
|
35
|
+
_SECRET = re.compile(
|
|
36
|
+
r"sk-ant-[A-Za-z0-9_\-]{20,}"
|
|
37
|
+
r"|sk-proj-[A-Za-z0-9_\-]{20,}"
|
|
38
|
+
r"|AKIA[0-9A-Z]{16}"
|
|
39
|
+
r"|AIza[0-9A-Za-z_\-]{35}"
|
|
40
|
+
r"|ghp_[0-9A-Za-z]{36}"
|
|
41
|
+
r"|eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Line-level smells (match on a single line).
|
|
45
|
+
_MED = [
|
|
46
|
+
(re.compile(r"\bautoApprove\b|\benableAllProjectMcpServers\b", re.IGNORECASE), "auto-approve / auto-enable bypasses consent"),
|
|
47
|
+
(re.compile(r"0\.0\.0\.0"), "0.0.0.0 bind (exposed beyond localhost)"),
|
|
48
|
+
(re.compile(r'"[^"]*_BASE_URL"\s*:'), "*_BASE_URL in config (request-redirect / token-exfil vector)"),
|
|
49
|
+
(re.compile(r'"(scopes?|permissions?)"\s*:\s*(\[[^\]]*"(\*|all|full-access)"|"(\*|all|full-access)")', re.IGNORECASE), "omnibus scope (* / all / full-access)"),
|
|
50
|
+
(re.compile(r'"args"\s*:\s*\[[^\]]*(&&|\|\||;|`)'), "shell metacharacters in args"),
|
|
51
|
+
]
|
|
52
|
+
# Chunk-level smells (span multiple lines in pretty-printed JSON).
|
|
53
|
+
_NPX = re.compile(r'"command"\s*:\s*"(npx|uvx)"', re.IGNORECASE)
|
|
54
|
+
_NPX_YES = re.compile(r'"\s*(-y|--yes)\s*"')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _candidate_chunks(sf: sl.ScannedFile):
|
|
58
|
+
"""Yield (start_lineno, [lines]) for MCP-config regions in this file."""
|
|
59
|
+
if _NAME_HINTS.search(sf.rel):
|
|
60
|
+
yield 1, list(enumerate(sf.lines, start=1))
|
|
61
|
+
return
|
|
62
|
+
if sf.path.suffix != ".md":
|
|
63
|
+
return
|
|
64
|
+
# fenced ```json / ```jsonc blocks that mention mcpServers / command
|
|
65
|
+
in_block, start, buf = False, 0, []
|
|
66
|
+
for i, text in enumerate(sf.lines, start=1):
|
|
67
|
+
st = text.strip()
|
|
68
|
+
if not in_block and re.match(r"`{3,}(json[c5]?|jsonc)\b", st):
|
|
69
|
+
in_block, start, buf = True, i, []
|
|
70
|
+
continue
|
|
71
|
+
if in_block and re.match(r"`{3,}\s*$", st):
|
|
72
|
+
joined = "\n".join(t for _, t in buf)
|
|
73
|
+
if "mcpServers" in joined or '"command"' in joined:
|
|
74
|
+
yield start, buf
|
|
75
|
+
in_block, buf = False, []
|
|
76
|
+
continue
|
|
77
|
+
if in_block:
|
|
78
|
+
buf.append((i, text))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _scan(sf: sl.ScannedFile):
|
|
82
|
+
if sf.pragma_allows(CHECK):
|
|
83
|
+
return []
|
|
84
|
+
out = []
|
|
85
|
+
for start, numbered in _candidate_chunks(sf):
|
|
86
|
+
for lineno, text in numbered:
|
|
87
|
+
if _SECRET.search(text):
|
|
88
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "HIGH",
|
|
89
|
+
"inline secret value in MCP config — use ${env:VAR}",
|
|
90
|
+
sf.weight))
|
|
91
|
+
for rx, label in _MED:
|
|
92
|
+
if rx.search(text):
|
|
93
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "MED", label, sf.weight))
|
|
94
|
+
# chunk-level: npx/uvx auto-install spans command + args lines
|
|
95
|
+
npx_line = next((ln for ln, t in numbered if _NPX.search(t)), start)
|
|
96
|
+
chunk = "\n".join(t for _, t in numbered)
|
|
97
|
+
if _NPX.search(chunk) and _NPX_YES.search(chunk):
|
|
98
|
+
out.append(sl.Finding(sf.rel, npx_line, CHECK, "MED",
|
|
99
|
+
"npx/uvx -y auto-install (supply-chain risk; pin + pre-install)",
|
|
100
|
+
sf.weight))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
ap = argparse.ArgumentParser(description=__doc__, epilog=sl.GUIDELINE_EPILOG)
|
|
106
|
+
ap.add_argument("--json", action="store_true")
|
|
107
|
+
args = ap.parse_args()
|
|
108
|
+
|
|
109
|
+
findings = []
|
|
110
|
+
# scan .md (fenced examples) under the default roots PLUS named MCP configs
|
|
111
|
+
# under src/templates (where the shipped claude_desktop_config template lives).
|
|
112
|
+
roots = (*sl.DEFAULT_SCAN_ROOTS, "src/templates")
|
|
113
|
+
for sf in sl.iter_corpus(roots=roots, exts=(".md", ".json", ".template")):
|
|
114
|
+
findings.extend(_scan(sf))
|
|
115
|
+
|
|
116
|
+
if args.json:
|
|
117
|
+
import json
|
|
118
|
+
print(json.dumps([f.__dict__ for f in findings], indent=2))
|
|
119
|
+
return 1 if any(f.is_fail for f in findings) else 0
|
|
120
|
+
return sl.report(findings, check_label="mcp-config-security")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""P1.4 — dangerous-frontmatter linter (road-to-security-pillar.md).
|
|
3
|
+
|
|
4
|
+
Enforces the execution-safety contract from the ``runtime-safety`` rule at the
|
|
5
|
+
frontmatter layer, and flags the consumer-format consent-bypass headers
|
|
6
|
+
(``permissionMode: bypassPermissions``, wildcard ``allowed-tools``) that the
|
|
7
|
+
"skill supply-chain" attack class abuses.
|
|
8
|
+
|
|
9
|
+
Checks (skill / command / persona / agent frontmatter under src/):
|
|
10
|
+
|
|
11
|
+
- HIGH: ``execution.type: automated`` but the runtime-safety floor is not met —
|
|
12
|
+
``handler`` is ``none``/missing, ``safety_mode`` is not ``strict``, or no
|
|
13
|
+
``allowed_tools`` key is declared.
|
|
14
|
+
- HIGH: ``allowed_tools`` (or consumer ``allowed-tools``) grants a wildcard —
|
|
15
|
+
``*``, ``Bash(*)``, or bare ``Bash`` — an over-broad tool grant.
|
|
16
|
+
- HIGH: ``permissionMode: bypassPermissions`` (consent bypass).
|
|
17
|
+
|
|
18
|
+
Reconciles with ``validate_frontmatter.py`` (schema fill) by checking only
|
|
19
|
+
*safety semantics*, never re-reporting shape/required-key errors.
|
|
20
|
+
|
|
21
|
+
Usage: python3 src/scripts/lint_skill_frontmatter_safety.py [--json]
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
31
|
+
from _lib import security_lint as sl # noqa: E402
|
|
32
|
+
|
|
33
|
+
CHECK = "dangerous-frontmatter"
|
|
34
|
+
|
|
35
|
+
# Always over-broad: a star or Bash(*) wildcard grant.
|
|
36
|
+
_WILDCARD_TOOL = re.compile(r"(^|[\s,\[\"'])(\*|Bash\(\*\))(\s|,|\]|\"|'|$)")
|
|
37
|
+
# Bare `Bash` (full shell) — over-broad only on a NON-execution skill.
|
|
38
|
+
_BARE_BASH = re.compile(r"(^|[\s,\[\"'])Bash(\s|,|\]|\"|'|$)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _frontmatter(sf: sl.ScannedFile) -> tuple[list[tuple[int, str]], int] | None:
|
|
42
|
+
"""Return [(lineno, text)] of the frontmatter body + its end line, or None."""
|
|
43
|
+
if not sf.lines or sf.lines[0].strip() != "---":
|
|
44
|
+
return None
|
|
45
|
+
body = []
|
|
46
|
+
for i in range(1, len(sf.lines)):
|
|
47
|
+
if sf.lines[i].strip() == "---":
|
|
48
|
+
return body, i + 1
|
|
49
|
+
body.append((i + 1, sf.lines[i]))
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _exec_block(body: list[tuple[int, str]]) -> dict[str, tuple[int, str]]:
|
|
54
|
+
"""Extract execution.* sub-keys as {key: (lineno, value)} (one-level block)."""
|
|
55
|
+
out: dict[str, tuple[int, str]] = {}
|
|
56
|
+
in_exec = False
|
|
57
|
+
base_indent = 0
|
|
58
|
+
for lineno, text in body:
|
|
59
|
+
if re.match(r"^execution:\s*$", text):
|
|
60
|
+
in_exec = True
|
|
61
|
+
base_indent = len(text) - len(text.lstrip())
|
|
62
|
+
continue
|
|
63
|
+
if in_exec:
|
|
64
|
+
indent = len(text) - len(text.lstrip())
|
|
65
|
+
if text.strip() and indent <= base_indent:
|
|
66
|
+
in_exec = False
|
|
67
|
+
continue
|
|
68
|
+
m = re.match(r"^\s+([\w-]+):\s*(.*)$", text)
|
|
69
|
+
if m:
|
|
70
|
+
out[m.group(1)] = (lineno, m.group(2).strip())
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _scan(sf: sl.ScannedFile) -> list[sl.Finding]:
|
|
75
|
+
if sf.pragma_allows(CHECK):
|
|
76
|
+
return []
|
|
77
|
+
fm = _frontmatter(sf)
|
|
78
|
+
if not fm:
|
|
79
|
+
return []
|
|
80
|
+
body, _end = fm
|
|
81
|
+
out: list[sl.Finding] = []
|
|
82
|
+
|
|
83
|
+
ex = _exec_block(body)
|
|
84
|
+
handler = ex.get("handler", (0, ""))[1].strip("'\"")
|
|
85
|
+
is_execution_skill = bool(ex) and handler not in ("", "none")
|
|
86
|
+
|
|
87
|
+
# consumer consent-bypass header + wildcard `allowed-tools` (hyphen = Claude
|
|
88
|
+
# format; the underscore source key is covered by the execution block below).
|
|
89
|
+
for lineno, text in body:
|
|
90
|
+
if re.match(r"\s*permissionMode:\s*['\"]?bypassPermissions", text):
|
|
91
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "HIGH",
|
|
92
|
+
"permissionMode: bypassPermissions (consent bypass)", sf.weight))
|
|
93
|
+
m = re.match(r"\s*allowed-tools:\s*(.+)$", text)
|
|
94
|
+
if m and (_WILDCARD_TOOL.search(m.group(1))
|
|
95
|
+
or (_BARE_BASH.search(m.group(1)) and not is_execution_skill)):
|
|
96
|
+
out.append(sl.Finding(sf.rel, lineno, CHECK, "HIGH",
|
|
97
|
+
"wildcard / bare-Bash tool grant (over-broad)", sf.weight))
|
|
98
|
+
|
|
99
|
+
if not ex:
|
|
100
|
+
return out
|
|
101
|
+
etype = ex.get("type", (0, ""))[1].strip("'\"")
|
|
102
|
+
safety = ex.get("safety_mode", (0, ""))[1].strip("'\"")
|
|
103
|
+
exec_line = ex.get("type", ex.get("handler", (0, "")))[0]
|
|
104
|
+
|
|
105
|
+
if etype == "automated":
|
|
106
|
+
if handler in ("", "none"):
|
|
107
|
+
out.append(sl.Finding(sf.rel, exec_line, CHECK, "HIGH",
|
|
108
|
+
"automated execution with handler none/missing (runtime-safety)", sf.weight))
|
|
109
|
+
if safety != "strict":
|
|
110
|
+
out.append(sl.Finding(sf.rel, exec_line, CHECK, "HIGH",
|
|
111
|
+
"automated execution without safety_mode: strict (runtime-safety)", sf.weight))
|
|
112
|
+
if "allowed_tools" not in ex:
|
|
113
|
+
out.append(sl.Finding(sf.rel, exec_line, CHECK, "HIGH",
|
|
114
|
+
"automated execution without an explicit allowed_tools declaration", sf.weight))
|
|
115
|
+
|
|
116
|
+
at_line, at_val = ex.get("allowed_tools", (0, ""))
|
|
117
|
+
if at_val and _WILDCARD_TOOL.search(at_val):
|
|
118
|
+
out.append(sl.Finding(sf.rel, at_line, CHECK, "HIGH",
|
|
119
|
+
"execution.allowed_tools wildcard (* / Bash(*)) grant", sf.weight))
|
|
120
|
+
elif at_val and _BARE_BASH.search(at_val) and not is_execution_skill:
|
|
121
|
+
out.append(sl.Finding(sf.rel, at_line, CHECK, "HIGH",
|
|
122
|
+
"bare Bash grant on a non-execution skill (handler none/missing)", sf.weight))
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> int:
|
|
127
|
+
ap = argparse.ArgumentParser(description=__doc__, epilog=sl.GUIDELINE_EPILOG)
|
|
128
|
+
ap.add_argument("--json", action="store_true")
|
|
129
|
+
args = ap.parse_args()
|
|
130
|
+
|
|
131
|
+
findings: list[sl.Finding] = []
|
|
132
|
+
roots = ("src/skills", "src/agent-src", "src/domains")
|
|
133
|
+
for sf in sl.iter_corpus(roots=roots, exts=(".md",)):
|
|
134
|
+
findings.extend(_scan(sf))
|
|
135
|
+
|
|
136
|
+
if args.json:
|
|
137
|
+
import json
|
|
138
|
+
print(json.dumps([f.__dict__ for f in findings], indent=2))
|
|
139
|
+
return 1 if any(f.is_fail for f in findings) else 0
|
|
140
|
+
return sl.report(findings, check_label="dangerous-frontmatter")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
raise SystemExit(main())
|