@litfamily/lithermes 1.0.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/LICENSE +21 -0
- package/README.md +186 -0
- package/README_Ko-KR.md +188 -0
- package/assets/lithermes-plugin/NOTICE.md +46 -0
- package/assets/lithermes-plugin/README.md +390 -0
- package/assets/lithermes-plugin/__init__.py +589 -0
- package/assets/lithermes-plugin/auto_update.py +338 -0
- package/assets/lithermes-plugin/bounded_work.py +1120 -0
- package/assets/lithermes-plugin/bounded_work_tools.py +115 -0
- package/assets/lithermes-plugin/core.py +1813 -0
- package/assets/lithermes-plugin/core_commands.py +243 -0
- package/assets/lithermes-plugin/core_contexts.py +458 -0
- package/assets/lithermes-plugin/core_contract.py +456 -0
- package/assets/lithermes-plugin/core_plans.py +418 -0
- package/assets/lithermes-plugin/core_reader_args.py +34 -0
- package/assets/lithermes-plugin/core_recap_command.py +141 -0
- package/assets/lithermes-plugin/core_recap_state.py +175 -0
- package/assets/lithermes-plugin/core_review.py +198 -0
- package/assets/lithermes-plugin/core_routing.py +641 -0
- package/assets/lithermes-plugin/core_runs.py +283 -0
- package/assets/lithermes-plugin/core_runtime.py +251 -0
- package/assets/lithermes-plugin/core_workflow_commands.py +207 -0
- package/assets/lithermes-plugin/deliverable_hedge_guard.py +562 -0
- package/assets/lithermes-plugin/diagnostics.py +468 -0
- package/assets/lithermes-plugin/handoff.py +97 -0
- package/assets/lithermes-plugin/knowledge.py +920 -0
- package/assets/lithermes-plugin/knowledge_tools.py +130 -0
- package/assets/lithermes-plugin/lit_mark.py +140 -0
- package/assets/lithermes-plugin/lit_mark_rows.json +1363 -0
- package/assets/lithermes-plugin/litgoal/__init__.py +10 -0
- package/assets/lithermes-plugin/litgoal/cli.py +207 -0
- package/assets/lithermes-plugin/litgoal/hook.py +81 -0
- package/assets/lithermes-plugin/litgoal/model.py +203 -0
- package/assets/lithermes-plugin/litgoal/runtime.py +522 -0
- package/assets/lithermes-plugin/litgoal/store.py +367 -0
- package/assets/lithermes-plugin/litgoal/tools.py +300 -0
- package/assets/lithermes-plugin/output-styles/asd-ste100-ko.md +41 -0
- package/assets/lithermes-plugin/output-styles/asd-ste100.md +40 -0
- package/assets/lithermes-plugin/output-styles/eli5-ko.md +13 -0
- package/assets/lithermes-plugin/output-styles/eli5.md +11 -0
- package/assets/lithermes-plugin/output_styles.py +62 -0
- package/assets/lithermes-plugin/payload-version.json +2107 -0
- package/assets/lithermes-plugin/plugin.yaml +16 -0
- package/assets/lithermes-plugin/provider_metrics.py +161 -0
- package/assets/lithermes-plugin/redaction.py +148 -0
- package/assets/lithermes-plugin/rules/__init__.py +55 -0
- package/assets/lithermes-plugin/rules/bundled-rules/baseline-discipline.md +20 -0
- package/assets/lithermes-plugin/rules/bundled-rules/build-decision-gate.md +19 -0
- package/assets/lithermes-plugin/rules/bundled-rules/windows-git-bash.md +17 -0
- package/assets/lithermes-plugin/rules/constants.py +122 -0
- package/assets/lithermes-plugin/rules/discovery.py +276 -0
- package/assets/lithermes-plugin/rules/engine.py +447 -0
- package/assets/lithermes-plugin/rules/frontmatter.py +149 -0
- package/assets/lithermes-plugin/rules/globmatch.py +333 -0
- package/assets/lithermes-plugin/scientific_visualization.py +158 -0
- package/assets/lithermes-plugin/session_context.py +8 -0
- package/assets/lithermes-plugin/skill_loop.py +2044 -0
- package/assets/lithermes-plugin/skill_observer.py +1778 -0
- package/assets/lithermes-plugin/skills/autoconference/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/autoconference/ORIGIN.json +11 -0
- package/assets/lithermes-plugin/skills/autoconference/SKILL.md +163 -0
- package/assets/lithermes-plugin/skills/autoconference/assets/conference_template.md +76 -0
- package/assets/lithermes-plugin/skills/autoconference/assets/report_template.md +53 -0
- package/assets/lithermes-plugin/skills/autoconference/assets/synthesis_template.md +39 -0
- package/assets/lithermes-plugin/skills/autoconference/dependencies/autoresearch.md +14 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/analyze/SKILL.md +269 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/core/SKILL.md +134 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/core/convergence-guide.md +162 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/core/crash-recovery.md +63 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/debate/SKILL.md +383 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/plan/SKILL.md +304 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/resume/SKILL.md +82 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/ship/SKILL.md +78 -0
- package/assets/lithermes-plugin/skills/autoconference/modes/survey/SKILL.md +376 -0
- package/assets/lithermes-plugin/skills/autoconference/references/agent-prompts.md +110 -0
- package/assets/lithermes-plugin/skills/autoconference/references/conference-protocol.md +90 -0
- package/assets/lithermes-plugin/skills/autoconference/references/core-principles.md +78 -0
- package/assets/lithermes-plugin/skills/autoconference/references/results-logging.md +165 -0
- package/assets/lithermes-plugin/skills/autoconference/references/visualization-guide.md +99 -0
- package/assets/lithermes-plugin/skills/autoconference/scripts/check_conference.sh +197 -0
- package/assets/lithermes-plugin/skills/autoconference/scripts/init_conference.py +489 -0
- package/assets/lithermes-plugin/skills/autoconference/scripts/style_presets.py +104 -0
- package/assets/lithermes-plugin/skills/autoconference/templates/code-performance.md +59 -0
- package/assets/lithermes-plugin/skills/autoconference/templates/debate-mode.md +50 -0
- package/assets/lithermes-plugin/skills/autoconference/templates/prompt-optimization.md +58 -0
- package/assets/lithermes-plugin/skills/autoconference/templates/quick-conference.md +48 -0
- package/assets/lithermes-plugin/skills/autoconference/templates/research-synthesis.md +56 -0
- package/assets/lithermes-plugin/skills/autoconference/templates/survey-mode.md +63 -0
- package/assets/lithermes-plugin/skills/autoresearch/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/autoresearch/ORIGIN.json +11 -0
- package/assets/lithermes-plugin/skills/autoresearch/SKILL.md +175 -0
- package/assets/lithermes-plugin/skills/autoresearch/assets/report_template.md +50 -0
- package/assets/lithermes-plugin/skills/autoresearch/assets/research_template.md +38 -0
- package/assets/lithermes-plugin/skills/autoresearch/assets/results_template.tsv +2 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/core/SKILL.md +204 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/core/evaluator-contract.md +108 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/core/stuck-detection.md +102 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/debug/SKILL.md +240 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/debug/investigation-techniques.md +254 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/fix/SKILL.md +176 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/learn/SKILL.md +96 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/plan/SKILL.md +291 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/predict/SKILL.md +271 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/predict/persona-templates.md +227 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/reason/SKILL.md +174 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/scenario/SKILL.md +168 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/scenario/dimensions.md +173 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/security/SKILL.md +287 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/security/owasp-checklist.md +156 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/security/stride-model.md +191 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/ship/SKILL.md +99 -0
- package/assets/lithermes-plugin/skills/autoresearch/modes/ship/type-checklists.md +158 -0
- package/assets/lithermes-plugin/skills/autoresearch/references/core-principles.md +75 -0
- package/assets/lithermes-plugin/skills/autoresearch/references/results-logging.md +55 -0
- package/assets/lithermes-plugin/skills/autoresearch/references/visualization-guide.md +90 -0
- package/assets/lithermes-plugin/skills/autoresearch/scripts/check_progress.sh +139 -0
- package/assets/lithermes-plugin/skills/autoresearch/scripts/init_research.py +219 -0
- package/assets/lithermes-plugin/skills/autoresearch/scripts/style_presets.py +104 -0
- package/assets/lithermes-plugin/skills/browser-drive/SKILL.md +147 -0
- package/assets/lithermes-plugin/skills/browser-drive/scripts/capability_probe.py +528 -0
- package/assets/lithermes-plugin/skills/comment-checker/SKILL.md +162 -0
- package/assets/lithermes-plugin/skills/debugging/SKILL.md +271 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/00-setup.md +108 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/02-investigate.md +122 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/04-oracle-triple.md +136 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/06-fix.md +116 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/08-qa.md +94 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/partial-runtime-evidence.md +229 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/go.md +252 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/node.md +260 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/python.md +248 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/rust.md +234 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/ghidra.md +212 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/pwndbg.md +263 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/pwntools.md +265 -0
- package/assets/lithermes-plugin/skills/deep-interview/SKILL.md +419 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +49 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/.gitattributes +2 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/README.md +248 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/_INDEX.md +191 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/airbnb.md +393 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/airtable.md +92 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/apple.md +250 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/aside.md +209 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/binance.md +348 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/bmw.md +183 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/brutalist-skill.md +92 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/bugatti.md +271 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/cal.md +262 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/claude.md +315 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/clay.md +307 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/clickhouse.md +284 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/clone-from-url.md +65 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/cohere.md +269 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/coinbase.md +132 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/composio.md +310 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/cursor.md +312 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/design-system-architecture.md +244 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/elevenlabs.md +268 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/expo.md +284 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/ferrari.md +317 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/figma.md +223 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/framer.md +249 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/gpt-tasteskill.md +74 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/hashicorp.md +281 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/ibm.md +335 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/image-to-code-skill.md +1228 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/imagegen-brandkit.md +798 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/imagegen-frontend-mobile.md +1465 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/imagegen-frontend-web.md +987 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/intercom.md +149 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/kraken.md +128 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/lamborghini.md +291 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/layout-skill.md +107 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/lazyweb.md +77 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/linear.app.md +370 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/lovable.md +301 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/mastercard.md +368 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/meta.md +369 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/minimalist-skill.md +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/minimax.md +260 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/mintlify.md +329 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/miro.md +111 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/mistral.ai.md +264 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/mongodb.md +269 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/nike.md +366 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/notion.md +312 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/nvidia.md +296 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/ollama.md +270 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/opencode.ai.md +284 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/output-skill.md +49 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/pinterest.md +233 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/playstation.md +367 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/posthog.md +259 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/raycast.md +271 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/react-dev-tooling-skill.md +230 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/redesign-skill.md +178 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/renault.md +314 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/replicate.md +264 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/resend.md +306 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/revolut.md +188 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/runwayml.md +247 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/sanity.md +360 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/sentry.md +265 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/shopify.md +353 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/soft-skill.md +98 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/spacex.md +197 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/spotify.md +249 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/starbucks.md +583 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/stitch-design-example.md +121 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/stitch-skill.md +184 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/stripe.md +325 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/supabase.md +258 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/superhuman.md +255 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/taste-skill.md +1206 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/tesla.md +289 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/theverge.md +342 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/together.ai.md +266 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/uber.md +298 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/vercel.md +313 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/vodafone.md +426 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/voltagent.md +326 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/warp.md +256 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/webflow.md +95 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/wired.md +281 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/wise.md +176 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/x.ai.md +260 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/design/zapier.md +331 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/EVIDENCE.md +97 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/README.md +48 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/UPSTREAM.md +80 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/lane-a-direction.md +64 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/lane-b-execution.md +65 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/lane-c-review.md +65 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/lane-d-memory.md +83 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/orchestration.md +80 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/routing.md +79 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/accessibility-reviewer.md +83 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/content-writer.md +132 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/design-builder.md +109 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/design-critic.md +89 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/design-lead.md +113 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/design-scout.md +78 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/design-strategist.md +121 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/heuristic-evaluator.md +268 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/inspiration-scout.md +107 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/agents/motion-designer.md +120 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/accessible-content/reference.md +101 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/adaptive-interfaces/reference.md +109 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/cognitive-accessibility/reference.md +107 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-debate/reference.md +199 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-debt-tracker/reference.md +174 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-handoff/reference.md +125 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-md/reference.md +106 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-retrospective/reference.md +266 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-review/reference.md +123 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/design-system-alignment/reference.md +120 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/designpowers-critique/reference.md +164 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/heuristic-evaluation/reference.md +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/inclusive-personas/reference.md +98 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/inspiration-scouting/reference.md +165 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/interaction-design/reference.md +122 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/motion-choreography/reference.md +81 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/research-planning/reference.md +96 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/responsive-patterns/reference.md +77 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/synthetic-user-testing/reference.md +192 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/taste-feedback/reference.md +165 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/taste-report/reference.md +78 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/token-architecture/reference.md +75 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/ui-composition/reference.md +117 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/usability-testing/reference.md +78 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/verification-before-shipping/reference.md +125 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/voice-and-tone/reference.md +79 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/designpowers/vendor/skills/writing-design-plans/reference.md +119 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/perfection/README.md +160 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/perfection/react-perf-tooling.md +127 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/README.md +659 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/charts.csv +26 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/colors.csv +162 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/icons.csv +106 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/landing.csv +35 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/products.csv +162 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/react-performance.csv +45 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/astro.csv +54 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/flutter.csv +53 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/html-tailwind.csv +56 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/jetpack-compose.csv +53 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/nextjs.csv +53 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/nuxt-ui.csv +51 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/nuxtjs.csv +59 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/react-native.csv +52 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/react.csv +54 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/shadcn.csv +61 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/svelte.csv +54 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/swiftui.csv +51 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/vue.csv +50 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/styles.csv +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/typography.csv +74 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/ui-reasoning.csv +162 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/ux-guidelines.csv +100 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/web-interface.csv +31 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/scripts/core.py +262 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/scripts/design_system.py +1148 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/scripts/search.py +114 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/legal/ATTRIBUTION.md +217 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/legal/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/legal/LICENSE-Apache-2.0.txt +201 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/manifest.json +876 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/adaptive-layout.md +81 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/brand-and-imagery.md +84 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +457 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/composition.md +89 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/creative-directions.md +81 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/evidence-review.md +113 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/implementation-platforms.md +92 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/inclusive-interface.md +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/interaction-motion.md +87 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/operating-lanes.md +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/performance-delivery.md +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/product-direction.md +86 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/redesign-playbook.md +91 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/system-foundations.md +82 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/taste-direction.md +86 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/visual-language.md +85 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/visual-reconstruction.md +94 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/PROVENANCE.json +38 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/THIRD-PARTY-NOTICE.txt +14 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/design-intelligence.json +1 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/import-manifest.json +1060 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/schemas/design-contract-v1alpha1.schema.json +264 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/schemas/design-contract-v1beta1.schema.json +268 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/schemas/design-contract-v1beta2.schema.json +280 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/design_contract_validation.py +996 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/design_intelligence.py +128 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/design_runtime.py +98 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/import_design_intelligence.py +56 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/source_import.py +144 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/source_manifest.py +65 -0
- package/assets/lithermes-plugin/skills/lit-burnoff/SKILL.md +448 -0
- package/assets/lithermes-plugin/skills/lit-burnoff-file/SKILL.md +254 -0
- package/assets/lithermes-plugin/skills/lit-code/SKILL.md +609 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/README.md +90 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/backend-stack.md +641 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/bootstrap.md +328 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/bubbletea-v2.md +359 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/cobra-stack.md +468 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/concurrency.md +362 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/data-modeling.md +329 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/error-handling.md +359 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/golangci-strict.md +236 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/grpc-connect.md +375 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/libraries.md +337 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/one-liners.md +202 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/sqlc-pgx.md +471 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/testing.md +467 -0
- package/assets/lithermes-plugin/skills/lit-code/references/go/type-patterns.md +298 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/README.md +314 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/async-anyio.md +442 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/data-modeling.md +233 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/data-processing.md +133 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/error-handling.md +218 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/fastapi-stack.md +316 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/httpx2-optimization.md +360 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/libraries.md +307 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/one-liners.md +268 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/orjson-stack.md +378 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/pydantic-ai.md +285 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/pyproject-strict.md +232 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/textual-tui.md +201 -0
- package/assets/lithermes-plugin/skills/lit-code/references/python/type-patterns.md +176 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/README.md +317 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/async-tokio.md +299 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/axum-stack.md +467 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/cargo-strict.md +317 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/clap-stack.md +409 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/concurrency.md +375 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/libraries.md +439 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/one-liners.md +291 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/proptest-insta.md +429 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/type-state.md +354 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/unsafe-discipline.md +250 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust/zero-cost-safety.md +527 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust-ub/README.md +289 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/assets/lithermes-plugin/skills/lit-code/references/rust-ub/ub-taxonomy.md +269 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/README.md +195 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/backend-hono.md +672 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/bootstrap.md +199 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/data-modeling.md +202 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/error-handling.md +169 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/tsconfig-strict.md +152 -0
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/type-patterns.md +196 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/check-no-excuse-rules.sh +173 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/new-project.py +138 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/.editorconfig +13 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/.golangci.yml +95 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/README.md.tmpl +12 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/Taskfile.yml +40 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/ci.yml +37 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/config.go +24 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/gitignore +15 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/main.go.tmpl +22 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/go/templates/run.go +15 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/python/check-no-excuse-rules.py +687 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/python/new-project.py +172 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/python/new-script.py +116 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/rust/check-no-excuse-rules.py +296 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/rust/new-project.py +175 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/assets/lithermes-plugin/skills/lit-code/scripts/typescript/new-project.ts +177 -0
- package/assets/lithermes-plugin/skills/lit-commit/SKILL.md +242 -0
- package/assets/lithermes-plugin/skills/lit-comprehend/SKILL.md +259 -0
- package/assets/lithermes-plugin/skills/lit-comprehend/assets/explainer-scaffold.html +123 -0
- package/assets/lithermes-plugin/skills/lit-comprehend/references/artifact-template.md +205 -0
- package/assets/lithermes-plugin/skills/lit-comprehend/references/micro-worlds.md +139 -0
- package/assets/lithermes-plugin/skills/lit-comprehend/scripts/verify-explainer.py +324 -0
- package/assets/lithermes-plugin/skills/lit-crucible/SKILL.md +315 -0
- package/assets/lithermes-plugin/skills/lit-handoff/ORIGIN.json +23 -0
- package/assets/lithermes-plugin/skills/lit-handoff/SKILL.md +145 -0
- package/assets/lithermes-plugin/skills/lit-init/SKILL.md +329 -0
- package/assets/lithermes-plugin/skills/lit-korean/SKILL.md +237 -0
- package/assets/lithermes-plugin/skills/lit-korean/references/quick-rules.md +24 -0
- package/assets/lithermes-plugin/skills/lit-korean/references/safety-checklist.md +23 -0
- package/assets/lithermes-plugin/skills/lit-plan/SKILL.md +596 -0
- package/assets/lithermes-plugin/skills/lit-recap/SKILL.md +183 -0
- package/assets/lithermes-plugin/skills/lit-scientific-visualization/ORIGIN.json +32 -0
- package/assets/lithermes-plugin/skills/lit-scientific-visualization/SKILL.md +170 -0
- package/assets/lithermes-plugin/skills/litgoal/.gitkeep +0 -0
- package/assets/lithermes-plugin/skills/litgoal/SKILL.md +372 -0
- package/assets/lithermes-plugin/skills/litresearch/ATTRIBUTION.md +50 -0
- package/assets/lithermes-plugin/skills/litresearch/SKILL.md +593 -0
- package/assets/lithermes-plugin/skills/litwork/SKILL.md +419 -0
- package/assets/lithermes-plugin/skills/lsp/SKILL.md +165 -0
- package/assets/lithermes-plugin/skills/lsp-setup/SKILL.md +265 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/bash/README.md +68 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/c-cpp/README.md +78 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/csharp/README.md +90 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/dart/README.md +59 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/elixir/README.md +61 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/go/README.md +63 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/haskell/README.md +69 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/java/README.md +70 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/julia/README.md +70 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/kotlin/README.md +72 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/lua/README.md +68 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/php/README.md +61 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/python/README.md +73 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/ruby/README.md +67 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/rust/README.md +65 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/swift/README.md +64 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/terraform/README.md +66 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/typescript/README.md +82 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/yaml/README.md +65 -0
- package/assets/lithermes-plugin/skills/lsp-setup/references/zig/README.md +59 -0
- package/assets/lithermes-plugin/skills/lsp-setup/scripts/detect-lsp.ts +221 -0
- package/assets/lithermes-plugin/skills/lsp-setup/scripts/lsp-server-table.ts +146 -0
- package/assets/lithermes-plugin/skills/lsp-setup/scripts/tsconfig.json +18 -0
- package/assets/lithermes-plugin/skills/lsp-setup/scripts/verify-lsp.ts +252 -0
- package/assets/lithermes-plugin/skills/refactor/SKILL.md +896 -0
- package/assets/lithermes-plugin/skills/review-work/SKILL.md +450 -0
- package/assets/lithermes-plugin/skills/rules/SKILL.md +194 -0
- package/assets/lithermes-plugin/skills/skill-observer/SKILL.md +190 -0
- package/assets/lithermes-plugin/skills/skill-observer/references/review-contract.md +77 -0
- package/assets/lithermes-plugin/skills/start-work/SKILL.md +550 -0
- package/assets/lithermes-plugin/skills/structural-search/SKILL.md +274 -0
- package/assets/lithermes-plugin/skills/visual-qa/SKILL.md +48 -0
- package/assets/lithermes-plugin/skills/visual-qa/references/capture-playbook.md +133 -0
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +522 -0
- package/assets/lithermes-plugin/skills/visual-qa/schemas/evidence-manifest-v1alpha1.schema.json +135 -0
- package/assets/lithermes-plugin/skills/visual-qa/schemas/evidence-manifest-v1beta1.schema.json +110 -0
- package/assets/lithermes-plugin/skills/visual-qa/schemas/review-receipt-v1alpha1.schema.json +49 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/contract_checks.py +71 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/evidence_contract.py +288 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/png_runtime.py +379 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/review_contract.py +151 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/tier_checks.py +190 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/tier_contract.py +103 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/tui_runtime.py +170 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/visual_contracts.py +31 -0
- package/assets/lithermes-plugin/skills/visual-qa/scripts/visual_qa.py +79 -0
- package/assets/lithermes-plugin/skills/wikify/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/wikify/ORIGIN.json +12 -0
- package/assets/lithermes-plugin/skills/wikify/SKILL.md +192 -0
- package/assets/lithermes-plugin/skills/wikify/assets/home-template.md +44 -0
- package/assets/lithermes-plugin/skills/wikify/assets/maintenance-report-template.md +46 -0
- package/assets/lithermes-plugin/skills/wikify/assets/paper-source-note-template.md +138 -0
- package/assets/lithermes-plugin/skills/wikify/assets/source-note-template.md +45 -0
- package/assets/lithermes-plugin/skills/wikify/assets/wiki-rules-template.md +117 -0
- package/assets/lithermes-plugin/skills/wikify/modes/ingest/SKILL.md +24 -0
- package/assets/lithermes-plugin/skills/wikify/modes/init/SKILL.md +26 -0
- package/assets/lithermes-plugin/skills/wikify/modes/lint/SKILL.md +30 -0
- package/assets/lithermes-plugin/skills/wikify/modes/query/SKILL.md +22 -0
- package/assets/lithermes-plugin/skills/wikify/modes/save/SKILL.md +32 -0
- package/assets/lithermes-plugin/skills/wikify/references/full-contract.md +761 -0
- package/assets/lithermes-plugin/uiux_runtime_common.py +117 -0
- package/assets/lithermes-plugin/vendor/NOTICE.md +21 -0
- package/assets/lithermes-plugin/vendor/handoff/SKILL.md +199 -0
- package/assets/lithermes-plugin/vendor/handoff/evals/evals.json +154 -0
- package/assets/lithermes-plugin/vendor/handoff/examples/HANDOFF-example-generic-auth-refactor.md +97 -0
- package/assets/lithermes-plugin/vendor/handoff/templates/HANDOFF.md +121 -0
- package/assets/lithermes-plugin/vendor/licenses/022_handoff-MIT.txt +21 -0
- package/assets/lithermes-plugin/vendor/licenses/045_scientific-visualization-MIT.txt +21 -0
- package/assets/lithermes-plugin/vendor/provenance/022_handoff.md +16 -0
- package/assets/lithermes-plugin/vendor/provenance/045_scientific-visualization.md +18 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/SKILL.md +283 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/assets/color_palettes.py +197 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/assets/nature.mplstyle +75 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/assets/presentation.mplstyle +74 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/assets/publication.mplstyle +78 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/evals/evals.json +158 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/references/color_palettes.md +380 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/references/journal_requirements.md +359 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/references/matplotlib_examples.md +608 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/references/mdanalysis_martini_visualization.md +85 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/references/publication_guidelines.md +217 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/references/seaborn_for_publications.md +293 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/scripts/figure_export.py +238 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/scripts/style_presets.py +467 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/tests/test_figure_export.py +51 -0
- package/assets/lithermes-plugin/vendor/scientific-visualization/tests/test_style_presets.py +114 -0
- package/bin/lithermes.js +8 -0
- package/package.json +58 -0
- package/readme-assets/badge-license.svg +1 -0
- package/readme-assets/badge-version.svg +1 -0
- package/readme-assets/cover.webp +0 -0
- package/readme-assets/lithermes-clay-icon.png +0 -0
- package/readme-assets/lithermes-wordmark.svg +5 -0
- package/src/cli.js +396 -0
- package/src/lib/autoUpdate.js +4 -0
- package/src/lib/canonicalCorpus.js +436 -0
- package/src/lib/check.js +200 -0
- package/src/lib/config.js +229 -0
- package/src/lib/files.js +126 -0
- package/src/lib/hermesDiscovery.js +122 -0
- package/src/lib/hud.js +135 -0
- package/src/lib/install.js +559 -0
- package/src/lib/litMark.js +68 -0
- package/src/lib/modelConfig.js +431 -0
- package/src/lib/modelConfigBoundary.js +206 -0
- package/src/lib/modelPrompt.js +201 -0
- package/src/lib/modelRoutePolicy.js +379 -0
- package/src/lib/patch.js +153 -0
- package/src/lib/skillLoop.js +62 -0
- package/src/lib/skillPayload.js +306 -0
- package/src/lib/skins.js +274 -0
- package/src/lib/spinner.js +193 -0
- package/src/lib/updateNotifier.js +1133 -0
- package/src/lib/updateNotifierWorker.js +9 -0
|
@@ -0,0 +1,1813 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from .bounded_work import *
|
|
12
|
+
from .core_commands import *
|
|
13
|
+
from .core_commands import _command_lit_dispatch, _escape_command_content, _join_positional
|
|
14
|
+
from .core_contexts import *
|
|
15
|
+
from .core_contexts import (
|
|
16
|
+
BROWSER_IDENTITY_BLOCKER,
|
|
17
|
+
_SKILL_ROUTE_CONTRACTS,
|
|
18
|
+
_probe_browser_driver_report as _browser_drive_probe_report,
|
|
19
|
+
)
|
|
20
|
+
from .core_contract import *
|
|
21
|
+
from .core_contract import _post_edit_skill_names, _skill_body, _skill_body_block
|
|
22
|
+
from .core_plans import *
|
|
23
|
+
from .core_plans import _CRITERION_PATTERN
|
|
24
|
+
from .core_recap_command import *
|
|
25
|
+
from .core_recap_command import _recap_render_options, _recap_safe_value
|
|
26
|
+
from .core_recap_state import *
|
|
27
|
+
from .core_recap_state import _recap_item, _recap_run_completions, _recap_tech_kind
|
|
28
|
+
from .core_review import *
|
|
29
|
+
from .core_review import _run_git
|
|
30
|
+
from .core_routing import *
|
|
31
|
+
from .core_routing import _after_mode_word, _after_start_work, _extract_bind_goal, _extract_run_context_task, _normalized_exact_phrase
|
|
32
|
+
from .core_runs import *
|
|
33
|
+
from .core_runs import _same_plan_path
|
|
34
|
+
from .core_runtime import *
|
|
35
|
+
from .core_runtime import _clamp_task, _redact_local_only_preview
|
|
36
|
+
from .core_workflow_commands import *
|
|
37
|
+
from .session_context import is_delegate_child_platform
|
|
38
|
+
except (ImportError, ModuleNotFoundError):
|
|
39
|
+
from bounded_work import *
|
|
40
|
+
from core_commands import *
|
|
41
|
+
from core_commands import _command_lit_dispatch, _escape_command_content, _join_positional
|
|
42
|
+
from core_contexts import *
|
|
43
|
+
from core_contexts import (
|
|
44
|
+
BROWSER_IDENTITY_BLOCKER,
|
|
45
|
+
_SKILL_ROUTE_CONTRACTS,
|
|
46
|
+
_probe_browser_driver_report as _browser_drive_probe_report,
|
|
47
|
+
)
|
|
48
|
+
from core_contract import *
|
|
49
|
+
from core_contract import _post_edit_skill_names, _skill_body, _skill_body_block
|
|
50
|
+
from core_plans import *
|
|
51
|
+
from core_plans import _CRITERION_PATTERN
|
|
52
|
+
from core_recap_command import *
|
|
53
|
+
from core_recap_command import _recap_render_options, _recap_safe_value
|
|
54
|
+
from core_recap_state import *
|
|
55
|
+
from core_recap_state import _recap_item, _recap_run_completions, _recap_tech_kind
|
|
56
|
+
from core_review import *
|
|
57
|
+
from core_review import _run_git
|
|
58
|
+
from core_routing import *
|
|
59
|
+
from core_routing import _after_mode_word, _after_start_work, _extract_bind_goal, _extract_run_context_task, _normalized_exact_phrase
|
|
60
|
+
from core_runs import *
|
|
61
|
+
from core_runs import _same_plan_path
|
|
62
|
+
from core_runtime import *
|
|
63
|
+
from core_runtime import _clamp_task, _redact_local_only_preview
|
|
64
|
+
from core_workflow_commands import *
|
|
65
|
+
from session_context import is_delegate_child_platform
|
|
66
|
+
|
|
67
|
+
# The rules engine is a subpackage, so it is imported separately from the flat
|
|
68
|
+
# star-imports above: it must not leak its module names into this facade.
|
|
69
|
+
try:
|
|
70
|
+
from . import rules as _rules
|
|
71
|
+
except (ImportError, ModuleNotFoundError):
|
|
72
|
+
import rules as _rules
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from . import auto_update as _auto_update
|
|
76
|
+
except (ImportError, ModuleNotFoundError):
|
|
77
|
+
import auto_update as _auto_update
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
from . import output_styles as _output_styles
|
|
81
|
+
except (ImportError, ModuleNotFoundError):
|
|
82
|
+
import output_styles as _output_styles
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
from . import provider_metrics as _provider_metrics
|
|
86
|
+
except (ImportError, ModuleNotFoundError):
|
|
87
|
+
import provider_metrics as _provider_metrics
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
from . import deliverable_hedge_guard as _deliverable_hedges
|
|
91
|
+
except (ImportError, ModuleNotFoundError):
|
|
92
|
+
import deliverable_hedge_guard as _deliverable_hedges
|
|
93
|
+
|
|
94
|
+
# Current-turn state stays in the compatibility facade because tests and the
|
|
95
|
+
# plugin register surface intentionally inspect it directly.
|
|
96
|
+
_PENDING_IGNITE: set[str] = set()
|
|
97
|
+
_PENDING_DISCIPLINE: dict[str, str] = {}
|
|
98
|
+
_PENDING_BLOCK: dict[str, str] = {}
|
|
99
|
+
# Mutated paths observed by post_tool_call, keyed by session, drained by the next
|
|
100
|
+
# pre_llm_call. Bounded so an abandoned or delegate-child session cannot grow it.
|
|
101
|
+
_PENDING_POST_EDIT: dict[str, list[str]] = {}
|
|
102
|
+
_MAX_POST_EDIT_SESSIONS = 64
|
|
103
|
+
_MAX_POST_EDIT_PATHS = 40
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def post_tool_call(**kwargs: Any) -> None:
|
|
107
|
+
"""Record the paths a completed tool call mutated, for event-shaped routing.
|
|
108
|
+
|
|
109
|
+
Hermes treats ``post_tool_call`` as an observer: ``model_tools.
|
|
110
|
+
_emit_post_tool_call_hook`` discards whatever a handler returns, so this hook
|
|
111
|
+
structurally cannot put text in front of the model. It is still the correct
|
|
112
|
+
event source — it is the only hook that sees a *completed* mutation — so it
|
|
113
|
+
buffers the routing input here and ``pre_llm_call``, the one hook whose return
|
|
114
|
+
value Hermes uses, renders and delivers it on the next turn.
|
|
115
|
+
|
|
116
|
+
Side-effect-free by design: no ledger write, because this fires once per tool
|
|
117
|
+
call and an events.jsonl line per edit would be pure noise.
|
|
118
|
+
"""
|
|
119
|
+
if str(kwargs.get("status") or "ok") != "ok":
|
|
120
|
+
return None
|
|
121
|
+
paths = mutated_tool_paths(str(kwargs.get("tool_name") or ""), kwargs.get("args"))
|
|
122
|
+
if not paths:
|
|
123
|
+
return None
|
|
124
|
+
key = str(kwargs.get("session_id") or "") or str(kwargs.get("task_id") or "")
|
|
125
|
+
if not key:
|
|
126
|
+
return None
|
|
127
|
+
_deliverable_hedges.observe_completed_paths(
|
|
128
|
+
key,
|
|
129
|
+
paths,
|
|
130
|
+
workspace=kwargs.get("workspace"),
|
|
131
|
+
)
|
|
132
|
+
pending = _PENDING_POST_EDIT.setdefault(key, [])
|
|
133
|
+
for path in paths:
|
|
134
|
+
if path in pending:
|
|
135
|
+
pending.remove(path)
|
|
136
|
+
pending.append(path)
|
|
137
|
+
del pending[:-_MAX_POST_EDIT_PATHS]
|
|
138
|
+
while len(_PENDING_POST_EDIT) > _MAX_POST_EDIT_SESSIONS:
|
|
139
|
+
_PENDING_POST_EDIT.pop(next(iter(_PENDING_POST_EDIT)), None)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def post_api_request(**kwargs: Any) -> None:
|
|
144
|
+
"""Record Hermes' numeric provider usage receipt in the local ledger.
|
|
145
|
+
|
|
146
|
+
The provider observer payload also carries request/response compatibility
|
|
147
|
+
fields, but the measurement module intentionally ignores them. Returning
|
|
148
|
+
``None`` preserves the observer-only hook contract.
|
|
149
|
+
"""
|
|
150
|
+
return _provider_metrics.post_api_request(**kwargs)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def consume_post_edit_skill_context(session_id: Any) -> str:
|
|
154
|
+
"""Drain this session's buffered mutations into a model-facing route block."""
|
|
155
|
+
paths = _PENDING_POST_EDIT.pop(str(session_id or ""), None)
|
|
156
|
+
return conditional_post_edit_skill_blocks(paths) if paths else ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def consume_post_edit_context(session_id: Any, compacted: bool = False) -> str:
|
|
160
|
+
"""Drain buffered mutations into BOTH event-shaped lanes.
|
|
161
|
+
|
|
162
|
+
The skill route and the dynamic rules lane read the same buffer, so it is
|
|
163
|
+
popped exactly once here rather than by each consumer.
|
|
164
|
+
"""
|
|
165
|
+
paths = _PENDING_POST_EDIT.pop(str(session_id or ""), None)
|
|
166
|
+
if not paths:
|
|
167
|
+
return ""
|
|
168
|
+
blocks = [
|
|
169
|
+
conditional_post_edit_skill_blocks(paths),
|
|
170
|
+
_rules.dynamic_rules_block(paths, session_id, compacted=compacted),
|
|
171
|
+
]
|
|
172
|
+
return "\n\n".join(block for block in blocks if block)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def on_session_start(**kwargs: Any) -> None:
|
|
176
|
+
"""Initialize session-scoped rule state for a brand-new session.
|
|
177
|
+
|
|
178
|
+
Hermes ignores this hook's return value, so it cannot inject the static rule
|
|
179
|
+
block itself; it resets the dedup ledger so a recycled session id cannot
|
|
180
|
+
inherit "already injected" from a previous session. `pre_llm_call` renders
|
|
181
|
+
the static lane on the first turn.
|
|
182
|
+
"""
|
|
183
|
+
release_browser_drive_state(kwargs.get("session_id"))
|
|
184
|
+
_deliverable_hedges.release_session(kwargs.get("session_id"))
|
|
185
|
+
_rules.begin_session(str(kwargs.get("session_id") or ""))
|
|
186
|
+
_print_agents_nudge(str(kwargs.get("platform") or ""))
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _print_agents_nudge(platform: str) -> None:
|
|
191
|
+
"""Point interactive classic-CLI sessions at Hermes' native /agents view.
|
|
192
|
+
|
|
193
|
+
ADR-001: the /agents overlay (alias /tasks) is the host-owned surface for
|
|
194
|
+
live delegate_task visibility; LitHermes only nudges, it renders nothing.
|
|
195
|
+
Gated to platform "cli" (cli.py fires the hook with platform="cli") AND a
|
|
196
|
+
real stdout tty: the TUI/gateway/telegram surfaces and test runs stay
|
|
197
|
+
byte-identical, and a gateway process under a pseudo-tty never prints
|
|
198
|
+
because its platform is not "cli".
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
if platform == "cli" and sys.stdout.isatty():
|
|
202
|
+
print("helper agents: type /agents")
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def consume_rules_context(**kwargs: Any) -> str:
|
|
208
|
+
"""Render both rule lanes plus the post-edit skill route for this turn.
|
|
209
|
+
|
|
210
|
+
One owner, because all three share state that must be consumed exactly once:
|
|
211
|
+
the compaction budget and the mutated-path buffer.
|
|
212
|
+
|
|
213
|
+
Static lane runs on the first turn of a session and once more after a context
|
|
214
|
+
compaction. Hermes stamps its compaction summary message with
|
|
215
|
+
`_compressed_summary`, so the reopen is detected structurally rather than
|
|
216
|
+
guessed from token counts. The reopen also shrinks both budgets and clears the
|
|
217
|
+
dedup ledger, since the compacted history no longer holds the earlier copy.
|
|
218
|
+
"""
|
|
219
|
+
session_id = str(kwargs.get("session_id") or "")
|
|
220
|
+
reopened = False
|
|
221
|
+
if _rules.history_was_compacted(kwargs.get("conversation_history")):
|
|
222
|
+
reopened = _rules.consume_compaction_budget(session_id)
|
|
223
|
+
|
|
224
|
+
blocks = []
|
|
225
|
+
if kwargs.get("is_first_turn") or reopened:
|
|
226
|
+
try:
|
|
227
|
+
blocks.append(_rules.static_rules_block(Path.cwd(), session_id, compacted=reopened))
|
|
228
|
+
except OSError:
|
|
229
|
+
pass
|
|
230
|
+
style_block = _output_styles.output_style_block()
|
|
231
|
+
if style_block:
|
|
232
|
+
blocks.append(style_block)
|
|
233
|
+
blocks.append(_deliverable_hedges.consume_context(session_id))
|
|
234
|
+
blocks.append(consume_post_edit_context(session_id, compacted=reopened))
|
|
235
|
+
return "\n\n".join(block for block in blocks if block)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def release_rules_session(session_id: Any) -> None:
|
|
239
|
+
_rules.end_session(str(session_id or ""))
|
|
240
|
+
_deliverable_hedges.release_session(session_id)
|
|
241
|
+
|
|
242
|
+
def ignite(result: str | dict[str, str], route: str = "litwork") -> str | dict[str, str]:
|
|
243
|
+
"""Draw a command acknowledgement and require a separate model probe."""
|
|
244
|
+
mark = acknowledgement(route, color=True)
|
|
245
|
+
if isinstance(result, dict):
|
|
246
|
+
updated = dict(result)
|
|
247
|
+
display = result.get("display")
|
|
248
|
+
if display and not str(display).startswith(mark):
|
|
249
|
+
updated["display"] = f"{mark}\n{display}"
|
|
250
|
+
if result.get("agent_message"):
|
|
251
|
+
updated["agent_message"] = f"{probe_contract(route)}\n\n{result['agent_message']}"
|
|
252
|
+
return updated
|
|
253
|
+
if isinstance(result, str) and result.strip() and not result.startswith(mark):
|
|
254
|
+
return f"{mark}\n{result}"
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
# browser-drive substitution guard.
|
|
258
|
+
#
|
|
259
|
+
# The route already measures the driver and states the verdict in context. On this
|
|
260
|
+
# host that was not enough: the model read `status: unavailable` and an explicit
|
|
261
|
+
# prohibition, then fetched the page anyway in two separate toolset configurations.
|
|
262
|
+
# A contract the host can overrule is a suggestion. So when the route fires and the
|
|
263
|
+
# driver is absent, the substitution is refused at the tool boundary, where the
|
|
264
|
+
# answer is a fact rather than a request.
|
|
265
|
+
#
|
|
266
|
+
# Scope is deliberately narrow. Only page-retrieval is refused: exact browser/web
|
|
267
|
+
# tools, URL-bearing aliases, and a terminal command that performs a fetch. Ordinary
|
|
268
|
+
# terminal and file work in the same turn is untouched.
|
|
269
|
+
_BROWSER_DRIVE_GUARD: set[str] = set()
|
|
270
|
+
_BROWSER_DRIVE_REPORTS: dict[str, dict[str, Any]] = {}
|
|
271
|
+
# Keep state-loss blocking tied to the sessions that actually lost their entry.
|
|
272
|
+
_BROWSER_DRIVE_EVICTED_SESSIONS: set[str] = set()
|
|
273
|
+
|
|
274
|
+
_FETCH_TOOL_NAMES = frozenset(
|
|
275
|
+
{
|
|
276
|
+
"browser_navigate",
|
|
277
|
+
"browser_open",
|
|
278
|
+
"browser_snapshot",
|
|
279
|
+
"fetch",
|
|
280
|
+
"fetch_url",
|
|
281
|
+
"get_url",
|
|
282
|
+
"http.client",
|
|
283
|
+
"http.get",
|
|
284
|
+
"http.request",
|
|
285
|
+
"http_client",
|
|
286
|
+
"open_url",
|
|
287
|
+
"page_fetch",
|
|
288
|
+
"page_open",
|
|
289
|
+
"url_fetch",
|
|
290
|
+
"web_fetch",
|
|
291
|
+
"web_open",
|
|
292
|
+
"web_search",
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
_FETCH_TOOL_ALIASES = frozenset({"browse", "get", "navigate", "open", "request", "retrieve"})
|
|
296
|
+
_FETCH_COMMAND_NAMES = frozenset(
|
|
297
|
+
{
|
|
298
|
+
"curl",
|
|
299
|
+
"fetch",
|
|
300
|
+
"http",
|
|
301
|
+
"http.client",
|
|
302
|
+
"httpie",
|
|
303
|
+
"lynx",
|
|
304
|
+
"nc",
|
|
305
|
+
"netcat",
|
|
306
|
+
"open_url",
|
|
307
|
+
"socat",
|
|
308
|
+
"telnet",
|
|
309
|
+
"wget",
|
|
310
|
+
"w3m",
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
_SHELL_COMMAND_NAMES = frozenset({"ash", "bash", "dash", "fish", "ksh", "sh", "zsh"})
|
|
314
|
+
_HOST_BROWSER_TOOL = re.compile(
|
|
315
|
+
r"(?:^|[^a-z0-9])(?:browser|web|computer[_\-.]?use)(?:$|[^a-z0-9])",
|
|
316
|
+
re.IGNORECASE,
|
|
317
|
+
)
|
|
318
|
+
_SHELL_NETWORK_ACCESS = re.compile(r"/dev/(?:tcp|udp)/", re.IGNORECASE)
|
|
319
|
+
_URL_INERT_COMMAND_NAMES = frozenset(
|
|
320
|
+
{
|
|
321
|
+
"awk",
|
|
322
|
+
"cat",
|
|
323
|
+
"cut",
|
|
324
|
+
"echo",
|
|
325
|
+
"grep",
|
|
326
|
+
"head",
|
|
327
|
+
"git",
|
|
328
|
+
"less",
|
|
329
|
+
"ls",
|
|
330
|
+
"more",
|
|
331
|
+
"printf",
|
|
332
|
+
"pwd",
|
|
333
|
+
"rg",
|
|
334
|
+
"sed",
|
|
335
|
+
"sort",
|
|
336
|
+
"tail",
|
|
337
|
+
"tr",
|
|
338
|
+
"true",
|
|
339
|
+
"false",
|
|
340
|
+
"uniq",
|
|
341
|
+
"wc",
|
|
342
|
+
"bun",
|
|
343
|
+
"deno",
|
|
344
|
+
"node",
|
|
345
|
+
"nodejs",
|
|
346
|
+
"perl",
|
|
347
|
+
"php",
|
|
348
|
+
"python",
|
|
349
|
+
"python2",
|
|
350
|
+
"python3",
|
|
351
|
+
"ruby",
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
_GIT_NETWORK_SUBCOMMANDS = frozenset({"clone", "fetch", "pull", "push", "ls-remote"})
|
|
355
|
+
_PYTHON_COMMAND_NAMES = frozenset({"python", "python2", "python3"})
|
|
356
|
+
_NODE_COMMAND_NAMES = frozenset({"bun", "deno", "node", "nodejs"})
|
|
357
|
+
_COMMAND_FIELDS = ("command", "cmd", "script", "code", "input")
|
|
358
|
+
_MAX_URL_ARGUMENT_DEPTH = 16
|
|
359
|
+
_MAX_URL_ARGUMENT_NODES = 256
|
|
360
|
+
_HTTP_URL = re.compile(r"(?<![A-Za-z0-9+.-])https?://[^\s'\"`<>()|;&]+", re.IGNORECASE)
|
|
361
|
+
_SHELL_SEPARATOR_NAMES = frozenset({";", "&", "|", "&&", "||"})
|
|
362
|
+
_SHELL_ASSIGNMENT = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)(\+?=)(.*)$")
|
|
363
|
+
_SHELL_CONTROL_WORD_NAMES = frozenset(
|
|
364
|
+
{
|
|
365
|
+
"!",
|
|
366
|
+
"if",
|
|
367
|
+
"while",
|
|
368
|
+
"until",
|
|
369
|
+
"then",
|
|
370
|
+
"do",
|
|
371
|
+
"else",
|
|
372
|
+
"elif",
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
_SHELL_PARAMETER_COMMAND_START = re.compile(
|
|
376
|
+
r'(?m)(?:^|[;&|])\s*(?:(?:if|while|until|then|do|else|elif|!)\s+)?'
|
|
377
|
+
r'"?(\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*)'
|
|
378
|
+
)
|
|
379
|
+
_SHELL_PARAMETER = re.compile(r"\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))")
|
|
380
|
+
_OPAQUE_SHELL_VALUE = "\x00lithermes-opaque-shell-value\x00"
|
|
381
|
+
_NODE_REQUEST = re.compile(
|
|
382
|
+
r"(?:\bfetch\s*\()"
|
|
383
|
+
r"|(?:\b(?:http|https|node:http|node:https)\s*\.\s*(?:get|request)\s*\()"
|
|
384
|
+
r"|(?:\brequire\s*\(\s*['\"]?(?:node:)?https?['\"]?\s*\)\s*\.\s*(?:get|request)\s*\()"
|
|
385
|
+
r"|(?:\brequire\s*\(\s*['\"](?:node:)?https?['\"]\s*\)[^\n]{0,512}\b(?:get|request)\s*\()"
|
|
386
|
+
r"|(?:\brequire\s*\([^)]{1,128}\)[^\n]{0,512}\b(?:get|request)\s*\()"
|
|
387
|
+
r"|(?:\[['\"](?:fetch|get|request)['\"]\]\s*\()"
|
|
388
|
+
r"|(?:\b(?:axios|undici)\s*\.\s*(?:fetch|get|request)\s*\()",
|
|
389
|
+
re.IGNORECASE,
|
|
390
|
+
)
|
|
391
|
+
_PYTHON_REQUEST = re.compile(
|
|
392
|
+
r"(?:\b(?:requests|httpx|urllib(?:\.request)?|urllib3|aiohttp)\s*\.\s*"
|
|
393
|
+
r"(?:delete|get|patch|post|put|request|urlopen|urlretrieve)\s*\()"
|
|
394
|
+
r"|(?:\b(?:urlopen|urlretrieve)\s*\()"
|
|
395
|
+
r"|(?:\bhttp\s*\.\s*client\s*\.\s*(?:HTTPConnection|HTTPSConnection|request)\s*\()"
|
|
396
|
+
r"|(?:\b(?:HTTPConnection|HTTPSConnection)\s*\()"
|
|
397
|
+
r"|(?:\bgetattr\s*\(\s*http\s*\.\s*client\b[^)]{0,160}\)\s*\()"
|
|
398
|
+
r"|(?:\bgetattr\s*\([^,()]{1,80},\s*['\"](?:request|get|connect|urlopen|create_connection|create_server|socket|HTTPConnection|HTTPSConnection)['\"]\s*\)\s*\()"
|
|
399
|
+
r"|(?:\bsocket\s*\.\s*(?:create_connection|create_server|socket)\s*\()"
|
|
400
|
+
r"|(?:\b(?:create_connection|create_server)\s*\()"
|
|
401
|
+
r"|(?:\b(?:import|from)\s+(?:requests|httpx|urllib3|aiohttp|socket)\b[^\n]{0,256}\b[A-Za-z_]\w*\s*\.\s*(?:delete|get|patch|post|put|request|urlopen|urlretrieve|create_connection|create_server)\s*\()",
|
|
402
|
+
re.IGNORECASE,
|
|
403
|
+
)
|
|
404
|
+
_BASE64_DECODE = re.compile(
|
|
405
|
+
r"(?:\bbase64\s+(?:-[A-Za-z]*d|--decode)\b"
|
|
406
|
+
r"|\b(?:base64\.)?(?:b64decode|urlsafe_b64decode)\s*\("
|
|
407
|
+
r"|\bBuffer\s*\.\s*from\s*\([^\n]{0,8192}['\"]base64['\"])",
|
|
408
|
+
re.IGNORECASE,
|
|
409
|
+
)
|
|
410
|
+
_BASE64_EXECUTION = re.compile(
|
|
411
|
+
r"(?:"
|
|
412
|
+
r"\|\s*(?:(?:/[A-Za-z0-9_.-]+)*/)?(?:env\s+)?(?:(?:/[A-Za-z0-9_.-]+)*/)?(?:busybox\s+)?(?:ash|bash|dash|fish|ksh|node|perl|php|python|python3|ruby|sh|zsh)\b"
|
|
413
|
+
r"|\b(?:ash|bash|dash|fish|ksh|node|python|python3|sh|zsh)\s+-c\b"
|
|
414
|
+
r"|\b(?:exec|eval|popen|source|system)\s*\(?"
|
|
415
|
+
r"|\b(?:new\s+Function|os\s*\.\s*system|subprocess\s*\.\s*"
|
|
416
|
+
r"(?:call|Popen|run))\s*\("
|
|
417
|
+
r")",
|
|
418
|
+
re.IGNORECASE,
|
|
419
|
+
)
|
|
420
|
+
_MAX_GUARD_TEXT = 8192
|
|
421
|
+
_MAX_COMMAND_DEPTH = 8
|
|
422
|
+
_MAX_BROWSER_DRIVE_SESSIONS = 64
|
|
423
|
+
_MAX_BROWSER_SESSION_ID_LENGTH = 256
|
|
424
|
+
_MAX_BROWSER_REPORT_FIELD_LENGTH = 512
|
|
425
|
+
_BLOCKER_STATE = "BLOCKED_BROWSER_DRIVER_STATE_UNAVAILABLE"
|
|
426
|
+
_BROWSER_DRIVER_VERSION = re.compile(
|
|
427
|
+
r"^agent-browser\s+v?\d+(?:\.\d+)+$", re.IGNORECASE
|
|
428
|
+
)
|
|
429
|
+
_RUBY_NETWORK_REQUEST = re.compile(r"\b(?:URI\s*\.\s*open|Net\s*::\s*HTTP|open-uri)\b", re.IGNORECASE)
|
|
430
|
+
_PHP_NETWORK_REQUEST = re.compile(
|
|
431
|
+
r"\b(?:file_get_contents|fopen|curl_exec|fsockopen)\s*\(", re.IGNORECASE
|
|
432
|
+
)
|
|
433
|
+
_PYTHON_NETWORK_MEMBERS = frozenset(
|
|
434
|
+
{
|
|
435
|
+
"create_connection",
|
|
436
|
+
"create_server",
|
|
437
|
+
"delete",
|
|
438
|
+
"get",
|
|
439
|
+
"patch",
|
|
440
|
+
"post",
|
|
441
|
+
"put",
|
|
442
|
+
"request",
|
|
443
|
+
"socket",
|
|
444
|
+
"urlopen",
|
|
445
|
+
"urlretrieve",
|
|
446
|
+
"HTTPConnection",
|
|
447
|
+
"HTTPSConnection",
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
_PYTHON_NETWORK_MODULES = frozenset(
|
|
451
|
+
{"aiohttp", "http", "http.client", "httpx", "requests", "socket", "urllib", "urllib.request", "urllib3"}
|
|
452
|
+
)
|
|
453
|
+
_PYTHON_NETWORK_IMPORT = re.compile(
|
|
454
|
+
r"(?:\bimport\s+(?:requests|httpx|urllib(?:\.request)?|urllib3|aiohttp|http(?:\.client)?|socket)\b"
|
|
455
|
+
r"|\bfrom\s+(?:requests|httpx|urllib(?:\.request)?|urllib3|aiohttp|http(?:\.client)?|socket)\s+import\b"
|
|
456
|
+
r"|(?:^|[^A-Za-z0-9_])(?:__import__|importlib\s*\.\s*import_module)\s*\(\s*['\"]"
|
|
457
|
+
r"(?:requests|httpx|urllib(?:\.request)?|urllib3|aiohttp|http(?:\.client)?|socket)['\"]\s*\))",
|
|
458
|
+
re.IGNORECASE,
|
|
459
|
+
)
|
|
460
|
+
_PYTHON_MEMBER_ASSIGNMENT = re.compile(
|
|
461
|
+
r"\b([A-Za-z_]\w*)\s*=\s*[A-Za-z_]\w*\s*\.\s*"
|
|
462
|
+
r"(?:delete|get|patch|post|put|request|urlopen|urlretrieve|create_connection|create_server|socket|"
|
|
463
|
+
r"HTTPConnection|HTTPSConnection)\b",
|
|
464
|
+
re.IGNORECASE,
|
|
465
|
+
)
|
|
466
|
+
_PYTHON_ALIAS_IMPORT = re.compile(
|
|
467
|
+
r"\bfrom\s+(?:requests|httpx|urllib(?:\.request)?|urllib3|aiohttp|http(?:\.client)?|socket)\s+import\b[^;]*"
|
|
468
|
+
r"\bas\s+([A-Za-z_]\w*)\b",
|
|
469
|
+
re.IGNORECASE,
|
|
470
|
+
)
|
|
471
|
+
_PYTHON_MEMBER_CALL = re.compile(
|
|
472
|
+
r"\b[A-Za-z_]\w*\s*\.\s*(?:delete|get|patch|post|put|request|urlopen|urlretrieve|"
|
|
473
|
+
r"create_connection|create_server|socket|HTTPConnection|HTTPSConnection)\s*\(",
|
|
474
|
+
re.IGNORECASE,
|
|
475
|
+
)
|
|
476
|
+
_PYTHON_DYNAMIC_ALIAS = re.compile(r"\b([A-Za-z_]\w*)\s*=\s*getattr\s*\([^;]{1,256}\)", re.IGNORECASE)
|
|
477
|
+
_PYTHON_FROM_REQUEST = re.compile(
|
|
478
|
+
r"\bfrom\s+(?:requests|httpx|urllib(?:\.request)?|urllib3|aiohttp|http(?:\.client)?|socket)\s+import\b"
|
|
479
|
+
r"[^\n]{0,256}\b(?:delete|get|patch|post|put|request|urlopen|urlretrieve|create_connection|create_server|socket|"
|
|
480
|
+
r"HTTPConnection|HTTPSConnection)\s*\(",
|
|
481
|
+
re.IGNORECASE,
|
|
482
|
+
)
|
|
483
|
+
_NODE_NETWORK_MARKER = re.compile(
|
|
484
|
+
r"(?:\bfetch\b|\b(?:node:)?https?\b|\b(?:axios|undici)\b|"
|
|
485
|
+
r"\brequire\s*\(\s*['\"](?:node:)?https?['\"]\s*\))",
|
|
486
|
+
re.IGNORECASE,
|
|
487
|
+
)
|
|
488
|
+
_NODE_GLOBAL_FETCH_ALIAS = re.compile(
|
|
489
|
+
r"\b(?:const|let|var)?\s*([A-Za-z_]\w*)\s*=\s*globalThis\s*\[\s*['\"]fetch['\"]\s*\]",
|
|
490
|
+
re.IGNORECASE,
|
|
491
|
+
)
|
|
492
|
+
_NODE_REQUIRE_ALIAS = re.compile(
|
|
493
|
+
r"\b(?:const|let|var)?\s*([A-Za-z_]\w*)\s*=\s*require\s*\(\s*['\"](?:node:)?https?['\"]\s*\)\s*\.\s*(get|request)\b",
|
|
494
|
+
re.IGNORECASE,
|
|
495
|
+
)
|
|
496
|
+
_NODE_IMPORT_ALIASES = re.compile(
|
|
497
|
+
r"\bimport\s*\{([^}]*)\}\s*from\s*['\"](?:node:)?https?['\"]",
|
|
498
|
+
re.IGNORECASE,
|
|
499
|
+
)
|
|
500
|
+
_NODE_REQUIRE_DESTRUCTURED = re.compile(
|
|
501
|
+
r"\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['\"](?:node:)?https?['\"]\s*\)",
|
|
502
|
+
re.IGNORECASE,
|
|
503
|
+
)
|
|
504
|
+
_NODE_SIMPLE_ALIAS = re.compile(
|
|
505
|
+
r"\b(?:const|let|var)?\s*([A-Za-z_]\w*)\s*=\s*(?![=])([A-Za-z_]\w*)(?:\s*\.\s*([A-Za-z_]\w*))?",
|
|
506
|
+
re.IGNORECASE,
|
|
507
|
+
)
|
|
508
|
+
_NODE_MEMBER_ASSIGNMENT = re.compile(
|
|
509
|
+
r"\b([A-Za-z_]\w*)\s*=\s*(?:[A-Za-z_]\w*\s*\.\s*)?"
|
|
510
|
+
r"(?:fetch|get|request)\b|\b([A-Za-z_]\w*)\s*=\s*"
|
|
511
|
+
r"(?:require\s*\([^)]{0,128}\)|[A-Za-z_]\w*)\s*\.\s*(?:get|request)\b",
|
|
512
|
+
re.IGNORECASE,
|
|
513
|
+
)
|
|
514
|
+
_NODE_DIRECT_ALIAS = re.compile(r"\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:globalThis\s*\.\s*)?fetch\b", re.IGNORECASE)
|
|
515
|
+
_NODE_NAMED_IMPORT_ALIAS = re.compile(
|
|
516
|
+
r"\bimport\s*\{[^}]*\b(?:fetch|get|request)\s+as\s+([A-Za-z_]\w*)\b",
|
|
517
|
+
re.IGNORECASE,
|
|
518
|
+
)
|
|
519
|
+
_NODE_DESTRUCTURED_ALIAS = re.compile(
|
|
520
|
+
r"\b(?:const|let|var)\s*\{[^}]*\b(?:fetch|get|request)\s*:\s*([A-Za-z_]\w*)\b",
|
|
521
|
+
re.IGNORECASE,
|
|
522
|
+
)
|
|
523
|
+
_SHELL_EXECUTION_NAMES = frozenset({".", "eval", "source", "xargs"})
|
|
524
|
+
_SHELL_PREFIX_WRAPPER_NAMES = frozenset(
|
|
525
|
+
{"command", "exec", "env", "sudo", "time", "nice", "nohup"}
|
|
526
|
+
)
|
|
527
|
+
_SHELL_EXECUTION_WRAPPER_NAMES = frozenset(
|
|
528
|
+
{"command", "exec", "env", "sudo", "time", "nice", "nohup"}
|
|
529
|
+
) | _SHELL_EXECUTION_NAMES
|
|
530
|
+
_SHELL_SCRIPT_EXECUTION_NAMES = frozenset({".", "eval", "source"})
|
|
531
|
+
_SHELL_WRAPPER_OPTION_VALUES = {
|
|
532
|
+
"exec": frozenset({"-a"}),
|
|
533
|
+
"env": frozenset({"-u", "--unset", "-C", "--chdir"}),
|
|
534
|
+
"nice": frozenset({"-n", "--adjustment"}),
|
|
535
|
+
"sudo": frozenset({"-u", "--user", "-g", "--group", "-h", "--host", "-C", "--chdir"}),
|
|
536
|
+
"xargs": frozenset({"-a", "--arg-file", "-d", "--delimiter", "-I", "--replace", "-P", "--max-procs"}),
|
|
537
|
+
}
|
|
538
|
+
_RUBY_COMMAND_NAMES = frozenset({"ruby"})
|
|
539
|
+
_PHP_COMMAND_NAMES = frozenset({"php"})
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class _NormalisedBrowserDriverReport(dict[str, Any]):
|
|
543
|
+
pass
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _default_browser_driver_report() -> dict[str, Any]:
|
|
547
|
+
return _NormalisedBrowserDriverReport(
|
|
548
|
+
{
|
|
549
|
+
"status": "unavailable",
|
|
550
|
+
"command": None,
|
|
551
|
+
"version": None,
|
|
552
|
+
"blocker": "BLOCKED_BROWSER_DRIVER_UNAVAILABLE",
|
|
553
|
+
"detail": "the browser driver was not measured",
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _browser_session_id(value: Any) -> str | None:
|
|
559
|
+
if (
|
|
560
|
+
isinstance(value, str)
|
|
561
|
+
and value.strip()
|
|
562
|
+
and len(value) <= _MAX_BROWSER_SESSION_ID_LENGTH
|
|
563
|
+
and not any(ord(character) < 32 or ord(character) == 127 for character in value)
|
|
564
|
+
):
|
|
565
|
+
return value
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _bounded_report_value(value: Any) -> str | None:
|
|
570
|
+
if not isinstance(value, str) or not value:
|
|
571
|
+
return None
|
|
572
|
+
return value[:_MAX_BROWSER_REPORT_FIELD_LENGTH]
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _normalise_browser_driver_report(report: Any) -> dict[str, Any]:
|
|
576
|
+
if isinstance(report, _NormalisedBrowserDriverReport):
|
|
577
|
+
return report
|
|
578
|
+
if not isinstance(report, dict):
|
|
579
|
+
return _default_browser_driver_report()
|
|
580
|
+
status = report.get("status")
|
|
581
|
+
command = _bounded_report_value(report.get("command"))
|
|
582
|
+
version = _bounded_report_value(report.get("version"))
|
|
583
|
+
blocker = report.get("blocker")
|
|
584
|
+
detail = _bounded_report_value(report.get("detail")) or "the browser driver was not measured"
|
|
585
|
+
if status == "available":
|
|
586
|
+
command_name = command.replace("\\", "/").rsplit("/", 1)[-1].casefold() if command else None
|
|
587
|
+
if (
|
|
588
|
+
command_name not in {"agent-browser", "agent-browser.exe"}
|
|
589
|
+
or version is None
|
|
590
|
+
or _BROWSER_DRIVER_VERSION.fullmatch(version) is None
|
|
591
|
+
or blocker is not None
|
|
592
|
+
):
|
|
593
|
+
status = "unverified-identity"
|
|
594
|
+
blocker = "BLOCKED_BROWSER_DRIVER_IDENTITY_UNVERIFIED"
|
|
595
|
+
else:
|
|
596
|
+
blocker = None
|
|
597
|
+
elif status not in {"unavailable", "unverified-identity", "unknown"}:
|
|
598
|
+
status = "unknown"
|
|
599
|
+
blocker = "BLOCKED_BROWSER_DRIVER_UNAVAILABLE"
|
|
600
|
+
elif blocker not in {
|
|
601
|
+
"BLOCKED_BROWSER_DRIVER_UNAVAILABLE",
|
|
602
|
+
"BLOCKED_BROWSER_DRIVER_IDENTITY_UNVERIFIED",
|
|
603
|
+
BROWSER_IDENTITY_BLOCKER,
|
|
604
|
+
None,
|
|
605
|
+
}:
|
|
606
|
+
blocker = (
|
|
607
|
+
"BLOCKED_BROWSER_DRIVER_IDENTITY_UNVERIFIED"
|
|
608
|
+
if status == "unverified-identity"
|
|
609
|
+
else "BLOCKED_BROWSER_DRIVER_UNAVAILABLE"
|
|
610
|
+
)
|
|
611
|
+
return _NormalisedBrowserDriverReport(
|
|
612
|
+
{
|
|
613
|
+
"status": status,
|
|
614
|
+
"command": command,
|
|
615
|
+
"version": version,
|
|
616
|
+
"blocker": blocker,
|
|
617
|
+
"detail": detail,
|
|
618
|
+
}
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _evict_browser_drive_state() -> None:
|
|
623
|
+
while len(_BROWSER_DRIVE_REPORTS) > _MAX_BROWSER_DRIVE_SESSIONS:
|
|
624
|
+
stale = next(iter(_BROWSER_DRIVE_REPORTS))
|
|
625
|
+
_BROWSER_DRIVE_GUARD.discard(stale)
|
|
626
|
+
_BROWSER_DRIVE_REPORTS.pop(stale, None)
|
|
627
|
+
_BROWSER_DRIVE_EVICTED_SESSIONS.add(stale)
|
|
628
|
+
while len(_BROWSER_DRIVE_GUARD) > _MAX_BROWSER_DRIVE_SESSIONS:
|
|
629
|
+
stale = next(iter(_BROWSER_DRIVE_GUARD))
|
|
630
|
+
_BROWSER_DRIVE_GUARD.discard(stale)
|
|
631
|
+
_BROWSER_DRIVE_REPORTS.pop(stale, None)
|
|
632
|
+
_BROWSER_DRIVE_EVICTED_SESSIONS.add(stale)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def arm_browser_drive_guard(session_id: Any, report: dict[str, Any] | None = None) -> None:
|
|
636
|
+
session = _browser_session_id(session_id)
|
|
637
|
+
if session is not None:
|
|
638
|
+
_BROWSER_DRIVE_EVICTED_SESSIONS.discard(session)
|
|
639
|
+
_BROWSER_DRIVE_GUARD.add(session)
|
|
640
|
+
normalised = (
|
|
641
|
+
report
|
|
642
|
+
if isinstance(report, _NormalisedBrowserDriverReport)
|
|
643
|
+
else _normalise_browser_driver_report(report)
|
|
644
|
+
)
|
|
645
|
+
_BROWSER_DRIVE_REPORTS[session] = dict(normalised)
|
|
646
|
+
_evict_browser_drive_state()
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def release_browser_drive_guard(session_id: Any) -> None:
|
|
650
|
+
session = _browser_session_id(session_id)
|
|
651
|
+
if session is None:
|
|
652
|
+
return None
|
|
653
|
+
_BROWSER_DRIVE_GUARD.discard(session)
|
|
654
|
+
_BROWSER_DRIVE_REPORTS.pop(session, None)
|
|
655
|
+
_BROWSER_DRIVE_EVICTED_SESSIONS.discard(session)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def release_browser_drive_state(session_id: Any) -> None:
|
|
659
|
+
session = _browser_session_id(session_id)
|
|
660
|
+
if session is None:
|
|
661
|
+
return None
|
|
662
|
+
release_browser_drive_guard(session)
|
|
663
|
+
_PENDING_IGNITE.discard(session)
|
|
664
|
+
_PENDING_DISCIPLINE.pop(session, None)
|
|
665
|
+
_PENDING_BLOCK.pop(session, None)
|
|
666
|
+
_PENDING_POST_EDIT.pop(session, None)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _evict_pending_browser_state() -> None:
|
|
670
|
+
while len(_PENDING_IGNITE) > _MAX_BROWSER_DRIVE_SESSIONS:
|
|
671
|
+
stale = next(iter(_PENDING_IGNITE))
|
|
672
|
+
_PENDING_IGNITE.discard(stale)
|
|
673
|
+
_PENDING_DISCIPLINE.pop(stale, None)
|
|
674
|
+
while len(_PENDING_BLOCK) > _MAX_BROWSER_DRIVE_SESSIONS:
|
|
675
|
+
_PENDING_BLOCK.pop(next(iter(_PENDING_BLOCK)), None)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _has_http_url(value: str) -> bool:
|
|
679
|
+
for match in _HTTP_URL.finditer(value):
|
|
680
|
+
candidate = match.group(0).rstrip(".,!?]}")
|
|
681
|
+
try:
|
|
682
|
+
parsed = urlsplit(candidate)
|
|
683
|
+
except ValueError:
|
|
684
|
+
continue
|
|
685
|
+
if parsed.scheme.lower() in {"http", "https"} and parsed.netloc:
|
|
686
|
+
return True
|
|
687
|
+
return False
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _shell_tokens(command: str) -> list[str]:
|
|
691
|
+
try:
|
|
692
|
+
lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|()<>")
|
|
693
|
+
lexer.whitespace_split = True
|
|
694
|
+
lexer.commenters = ""
|
|
695
|
+
return list(lexer)
|
|
696
|
+
except (TypeError, ValueError):
|
|
697
|
+
return []
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _split_unquoted_newlines(command: str) -> list[str]:
|
|
701
|
+
parts: list[str] = []
|
|
702
|
+
start = 0
|
|
703
|
+
quote: str | None = None
|
|
704
|
+
escaped = False
|
|
705
|
+
index = 0
|
|
706
|
+
while index < len(command):
|
|
707
|
+
character = command[index]
|
|
708
|
+
if escaped:
|
|
709
|
+
escaped = False
|
|
710
|
+
elif character == "\\" and quote != "'":
|
|
711
|
+
escaped = True
|
|
712
|
+
elif quote is not None:
|
|
713
|
+
if character == quote:
|
|
714
|
+
quote = None
|
|
715
|
+
elif character in {"'", '"'}:
|
|
716
|
+
quote = character
|
|
717
|
+
elif character in {"\r", "\n"}:
|
|
718
|
+
parts.append(command[start:index])
|
|
719
|
+
if character == "\r" and index + 1 < len(command) and command[index + 1] == "\n":
|
|
720
|
+
index += 1
|
|
721
|
+
start = index + 1
|
|
722
|
+
index += 1
|
|
723
|
+
parts.append(command[start:])
|
|
724
|
+
return parts
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _command_segments(tokens: list[str]) -> list[list[str]]:
|
|
728
|
+
segments: list[list[str]] = []
|
|
729
|
+
current: list[str] = []
|
|
730
|
+
for token in tokens:
|
|
731
|
+
if token in _SHELL_SEPARATOR_NAMES:
|
|
732
|
+
if current:
|
|
733
|
+
segments.append(current)
|
|
734
|
+
current = []
|
|
735
|
+
continue
|
|
736
|
+
current.append(token)
|
|
737
|
+
if current:
|
|
738
|
+
segments.append(current)
|
|
739
|
+
return segments
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _shell_substitutions(command: str) -> list[str]:
|
|
743
|
+
if len(command) > _MAX_GUARD_TEXT:
|
|
744
|
+
return []
|
|
745
|
+
substitutions: list[str] = []
|
|
746
|
+
index = 0
|
|
747
|
+
quote: str | None = None
|
|
748
|
+
while index < len(command):
|
|
749
|
+
character = command[index]
|
|
750
|
+
if quote == "'":
|
|
751
|
+
if character == "'":
|
|
752
|
+
quote = None
|
|
753
|
+
index += 1
|
|
754
|
+
continue
|
|
755
|
+
if character == "\\":
|
|
756
|
+
index += 2
|
|
757
|
+
continue
|
|
758
|
+
if quote == '"':
|
|
759
|
+
if character == '"':
|
|
760
|
+
quote = None
|
|
761
|
+
index += 1
|
|
762
|
+
continue
|
|
763
|
+
elif character in {'"', "'"}:
|
|
764
|
+
quote = character
|
|
765
|
+
index += 1
|
|
766
|
+
continue
|
|
767
|
+
if character == "`":
|
|
768
|
+
end = index + 1
|
|
769
|
+
escaped = False
|
|
770
|
+
while end < len(command):
|
|
771
|
+
nested = command[end]
|
|
772
|
+
if escaped:
|
|
773
|
+
escaped = False
|
|
774
|
+
elif nested == "\\":
|
|
775
|
+
escaped = True
|
|
776
|
+
elif nested == "`":
|
|
777
|
+
substitutions.append(command[index + 1 : end])
|
|
778
|
+
index = end + 1
|
|
779
|
+
break
|
|
780
|
+
end += 1
|
|
781
|
+
else:
|
|
782
|
+
return substitutions
|
|
783
|
+
continue
|
|
784
|
+
if character == "$" and index + 1 < len(command) and command[index + 1] == "(":
|
|
785
|
+
end = index + 2
|
|
786
|
+
depth = 1
|
|
787
|
+
nested_quote: str | None = None
|
|
788
|
+
escaped = False
|
|
789
|
+
while end < len(command):
|
|
790
|
+
nested = command[end]
|
|
791
|
+
if escaped:
|
|
792
|
+
escaped = False
|
|
793
|
+
elif nested == "\\":
|
|
794
|
+
escaped = True
|
|
795
|
+
elif nested_quote == "'":
|
|
796
|
+
if nested == "'":
|
|
797
|
+
nested_quote = None
|
|
798
|
+
elif nested_quote == '"':
|
|
799
|
+
if nested == '"':
|
|
800
|
+
nested_quote = None
|
|
801
|
+
elif nested in {'"', "'"}:
|
|
802
|
+
nested_quote = nested
|
|
803
|
+
elif nested == "(":
|
|
804
|
+
depth += 1
|
|
805
|
+
elif nested == ")":
|
|
806
|
+
depth -= 1
|
|
807
|
+
if depth == 0:
|
|
808
|
+
substitutions.append(command[index + 2 : end])
|
|
809
|
+
index = end + 1
|
|
810
|
+
break
|
|
811
|
+
end += 1
|
|
812
|
+
else:
|
|
813
|
+
return substitutions
|
|
814
|
+
continue
|
|
815
|
+
index += 1
|
|
816
|
+
return substitutions
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _shell_substitution_is_opaque(command: str) -> bool:
|
|
820
|
+
if len(command) > _MAX_GUARD_TEXT and re.search(r"(?:\$\(|`)", command):
|
|
821
|
+
return True
|
|
822
|
+
word: list[str] = []
|
|
823
|
+
has_substitution = False
|
|
824
|
+
quote: str | None = None
|
|
825
|
+
expect_command = True
|
|
826
|
+
redirection = False
|
|
827
|
+
control_mode: str | None = None
|
|
828
|
+
execution_wrapper: str | None = None
|
|
829
|
+
wrapper_option_value = False
|
|
830
|
+
wrapper_options_ended = False
|
|
831
|
+
find_command = False
|
|
832
|
+
|
|
833
|
+
def flush_word() -> bool:
|
|
834
|
+
nonlocal control_mode, execution_wrapper, expect_command, find_command
|
|
835
|
+
nonlocal has_substitution, redirection, wrapper_option_value, wrapper_options_ended
|
|
836
|
+
if not word:
|
|
837
|
+
return False
|
|
838
|
+
token = "".join(word)
|
|
839
|
+
word.clear()
|
|
840
|
+
token_has_substitution = has_substitution
|
|
841
|
+
has_substitution = False
|
|
842
|
+
lowered = token.casefold()
|
|
843
|
+
if execution_wrapper is not None:
|
|
844
|
+
if token_has_substitution:
|
|
845
|
+
return True
|
|
846
|
+
if wrapper_option_value:
|
|
847
|
+
wrapper_option_value = False
|
|
848
|
+
return False
|
|
849
|
+
if not wrapper_options_ended and lowered == "--":
|
|
850
|
+
wrapper_options_ended = True
|
|
851
|
+
return False
|
|
852
|
+
option_values = _SHELL_WRAPPER_OPTION_VALUES.get(execution_wrapper, frozenset())
|
|
853
|
+
if not wrapper_options_ended and lowered in option_values:
|
|
854
|
+
wrapper_option_value = True
|
|
855
|
+
return False
|
|
856
|
+
if not wrapper_options_ended and lowered.startswith("-"):
|
|
857
|
+
return False
|
|
858
|
+
if execution_wrapper == "env" and _SHELL_ASSIGNMENT.fullmatch(token) is not None:
|
|
859
|
+
return False
|
|
860
|
+
if lowered in _SHELL_EXECUTION_WRAPPER_NAMES:
|
|
861
|
+
execution_wrapper = lowered
|
|
862
|
+
wrapper_option_value = False
|
|
863
|
+
wrapper_options_ended = False
|
|
864
|
+
return False
|
|
865
|
+
if execution_wrapper in _SHELL_SCRIPT_EXECUTION_NAMES:
|
|
866
|
+
return False
|
|
867
|
+
find_command = lowered == "find"
|
|
868
|
+
execution_wrapper = None
|
|
869
|
+
wrapper_option_value = False
|
|
870
|
+
wrapper_options_ended = False
|
|
871
|
+
expect_command = False
|
|
872
|
+
return False
|
|
873
|
+
if redirection:
|
|
874
|
+
redirection = False
|
|
875
|
+
return False
|
|
876
|
+
if control_mode == "for_name":
|
|
877
|
+
control_mode = "for_in"
|
|
878
|
+
return False
|
|
879
|
+
if control_mode == "for_in":
|
|
880
|
+
control_mode = "for_list" if lowered == "in" else None
|
|
881
|
+
return False
|
|
882
|
+
if control_mode == "for_list":
|
|
883
|
+
if lowered == "do":
|
|
884
|
+
control_mode = None
|
|
885
|
+
expect_command = True
|
|
886
|
+
return False
|
|
887
|
+
if control_mode == "case_expr":
|
|
888
|
+
if lowered == "in":
|
|
889
|
+
control_mode = "case_pattern"
|
|
890
|
+
return False
|
|
891
|
+
if control_mode == "case_pattern":
|
|
892
|
+
return False
|
|
893
|
+
if control_mode == "case_body" and lowered == "esac":
|
|
894
|
+
control_mode = None
|
|
895
|
+
expect_command = False
|
|
896
|
+
return False
|
|
897
|
+
if token_has_substitution and expect_command and _SHELL_ASSIGNMENT.fullmatch(token) is None:
|
|
898
|
+
return True
|
|
899
|
+
if expect_command and lowered == "for":
|
|
900
|
+
control_mode = "for_name"
|
|
901
|
+
expect_command = False
|
|
902
|
+
return False
|
|
903
|
+
if expect_command and lowered == "case":
|
|
904
|
+
control_mode = "case_expr"
|
|
905
|
+
expect_command = False
|
|
906
|
+
return False
|
|
907
|
+
if find_command and lowered in {"-exec", "-execdir"}:
|
|
908
|
+
execution_wrapper = "find-exec"
|
|
909
|
+
wrapper_option_value = False
|
|
910
|
+
wrapper_options_ended = False
|
|
911
|
+
find_command = False
|
|
912
|
+
return False
|
|
913
|
+
if expect_command and lowered in _SHELL_EXECUTION_WRAPPER_NAMES:
|
|
914
|
+
execution_wrapper = lowered
|
|
915
|
+
wrapper_option_value = False
|
|
916
|
+
wrapper_options_ended = False
|
|
917
|
+
return False
|
|
918
|
+
if expect_command and lowered in _SHELL_CONTROL_WORD_NAMES:
|
|
919
|
+
return False
|
|
920
|
+
if expect_command and _SHELL_ASSIGNMENT.fullmatch(token) is not None:
|
|
921
|
+
return False
|
|
922
|
+
if expect_command and lowered == "find":
|
|
923
|
+
find_command = True
|
|
924
|
+
expect_command = False
|
|
925
|
+
return False
|
|
926
|
+
|
|
927
|
+
index = 0
|
|
928
|
+
while index < len(command):
|
|
929
|
+
character = command[index]
|
|
930
|
+
if quote == "'":
|
|
931
|
+
word.append(character)
|
|
932
|
+
if character == "'":
|
|
933
|
+
quote = None
|
|
934
|
+
index += 1
|
|
935
|
+
continue
|
|
936
|
+
if quote == '"':
|
|
937
|
+
if character == "\\" and index + 1 < len(command):
|
|
938
|
+
word.extend((character, command[index + 1]))
|
|
939
|
+
index += 2
|
|
940
|
+
continue
|
|
941
|
+
if character == '"':
|
|
942
|
+
word.append(character)
|
|
943
|
+
quote = None
|
|
944
|
+
index += 1
|
|
945
|
+
continue
|
|
946
|
+
if character not in {"$", "`"}:
|
|
947
|
+
word.append(character)
|
|
948
|
+
index += 1
|
|
949
|
+
continue
|
|
950
|
+
elif character in {'"', "'"}:
|
|
951
|
+
word.append(character)
|
|
952
|
+
quote = character
|
|
953
|
+
index += 1
|
|
954
|
+
continue
|
|
955
|
+
if character == "\\":
|
|
956
|
+
word.append(character)
|
|
957
|
+
if index + 1 < len(command):
|
|
958
|
+
word.append(command[index + 1])
|
|
959
|
+
index += 2
|
|
960
|
+
else:
|
|
961
|
+
index += 1
|
|
962
|
+
continue
|
|
963
|
+
if character == "$" and index + 1 < len(command) and command[index + 1] == "(":
|
|
964
|
+
word.append("x")
|
|
965
|
+
has_substitution = True
|
|
966
|
+
index += 2
|
|
967
|
+
depth = 1
|
|
968
|
+
nested_quote: str | None = None
|
|
969
|
+
nested_backtick = False
|
|
970
|
+
escaped = False
|
|
971
|
+
while index < len(command):
|
|
972
|
+
nested = command[index]
|
|
973
|
+
if escaped:
|
|
974
|
+
escaped = False
|
|
975
|
+
elif nested == "\\":
|
|
976
|
+
escaped = True
|
|
977
|
+
elif nested_backtick:
|
|
978
|
+
if nested == "`":
|
|
979
|
+
nested_backtick = False
|
|
980
|
+
elif nested_quote == "'":
|
|
981
|
+
if nested == "'":
|
|
982
|
+
nested_quote = None
|
|
983
|
+
elif nested_quote == '"':
|
|
984
|
+
if nested == '"':
|
|
985
|
+
nested_quote = None
|
|
986
|
+
elif nested in {'"', "'"}:
|
|
987
|
+
nested_quote = nested
|
|
988
|
+
elif nested == "`":
|
|
989
|
+
nested_backtick = True
|
|
990
|
+
elif nested == "(":
|
|
991
|
+
depth += 1
|
|
992
|
+
elif nested == ")":
|
|
993
|
+
depth -= 1
|
|
994
|
+
if depth == 0:
|
|
995
|
+
index += 1
|
|
996
|
+
break
|
|
997
|
+
index += 1
|
|
998
|
+
continue
|
|
999
|
+
if character == "`":
|
|
1000
|
+
word.append("x")
|
|
1001
|
+
has_substitution = True
|
|
1002
|
+
index += 1
|
|
1003
|
+
escaped = False
|
|
1004
|
+
while index < len(command):
|
|
1005
|
+
nested = command[index]
|
|
1006
|
+
if escaped:
|
|
1007
|
+
escaped = False
|
|
1008
|
+
elif nested == "\\":
|
|
1009
|
+
escaped = True
|
|
1010
|
+
elif nested == "`":
|
|
1011
|
+
index += 1
|
|
1012
|
+
break
|
|
1013
|
+
index += 1
|
|
1014
|
+
continue
|
|
1015
|
+
if character.isspace():
|
|
1016
|
+
if flush_word():
|
|
1017
|
+
return True
|
|
1018
|
+
if character in {"\r", "\n"}:
|
|
1019
|
+
expect_command = True
|
|
1020
|
+
execution_wrapper = None
|
|
1021
|
+
wrapper_option_value = False
|
|
1022
|
+
wrapper_options_ended = False
|
|
1023
|
+
find_command = False
|
|
1024
|
+
index += 1
|
|
1025
|
+
continue
|
|
1026
|
+
if character == "#" and not word:
|
|
1027
|
+
newline = command.find("\n", index)
|
|
1028
|
+
if newline == -1:
|
|
1029
|
+
break
|
|
1030
|
+
expect_command = True
|
|
1031
|
+
execution_wrapper = None
|
|
1032
|
+
wrapper_option_value = False
|
|
1033
|
+
wrapper_options_ended = False
|
|
1034
|
+
find_command = False
|
|
1035
|
+
index = newline + 1
|
|
1036
|
+
continue
|
|
1037
|
+
if character in ";&|":
|
|
1038
|
+
if flush_word():
|
|
1039
|
+
return True
|
|
1040
|
+
if control_mode != "case_pattern":
|
|
1041
|
+
expect_command = True
|
|
1042
|
+
execution_wrapper = None
|
|
1043
|
+
wrapper_option_value = False
|
|
1044
|
+
wrapper_options_ended = False
|
|
1045
|
+
find_command = False
|
|
1046
|
+
index += 1
|
|
1047
|
+
continue
|
|
1048
|
+
if character in "(){}":
|
|
1049
|
+
if flush_word():
|
|
1050
|
+
return True
|
|
1051
|
+
if character == "(" and control_mode != "case_pattern":
|
|
1052
|
+
expect_command = True
|
|
1053
|
+
elif character == ")" and control_mode == "case_pattern":
|
|
1054
|
+
control_mode = "case_body"
|
|
1055
|
+
expect_command = True
|
|
1056
|
+
elif character == ")":
|
|
1057
|
+
expect_command = False
|
|
1058
|
+
elif character == "{":
|
|
1059
|
+
expect_command = True
|
|
1060
|
+
else:
|
|
1061
|
+
expect_command = False
|
|
1062
|
+
index += 1
|
|
1063
|
+
continue
|
|
1064
|
+
if character in "<>":
|
|
1065
|
+
if flush_word():
|
|
1066
|
+
return True
|
|
1067
|
+
redirection = True
|
|
1068
|
+
index += 1
|
|
1069
|
+
if index < len(command) and command[index] == character:
|
|
1070
|
+
index += 1
|
|
1071
|
+
continue
|
|
1072
|
+
word.append(character)
|
|
1073
|
+
index += 1
|
|
1074
|
+
return flush_word()
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _runtime_reads_code_from_stdin(tokens: list[str], aliases: dict[str, str]) -> bool:
|
|
1078
|
+
for index, token in enumerate(tokens):
|
|
1079
|
+
if token != "|":
|
|
1080
|
+
continue
|
|
1081
|
+
end = index + 1
|
|
1082
|
+
while end < len(tokens) and tokens[end] not in _SHELL_SEPARATOR_NAMES:
|
|
1083
|
+
end += 1
|
|
1084
|
+
segment = tokens[index + 1 : end]
|
|
1085
|
+
if not segment:
|
|
1086
|
+
continue
|
|
1087
|
+
name = _resolve_shell_alias(_command_name(segment), aliases)
|
|
1088
|
+
command_index = next(
|
|
1089
|
+
(
|
|
1090
|
+
candidate
|
|
1091
|
+
for candidate, value in enumerate(segment)
|
|
1092
|
+
if _resolve_shell_alias(
|
|
1093
|
+
value[1:] if value.startswith("$") else value.rsplit("/", 1)[-1], aliases
|
|
1094
|
+
)
|
|
1095
|
+
== name
|
|
1096
|
+
),
|
|
1097
|
+
0,
|
|
1098
|
+
)
|
|
1099
|
+
arguments = segment[command_index + 1 :]
|
|
1100
|
+
if name in _PYTHON_COMMAND_NAMES:
|
|
1101
|
+
if "-" in arguments or not any(not argument.startswith("-") for argument in arguments):
|
|
1102
|
+
return True
|
|
1103
|
+
if name in _NODE_COMMAND_NAMES:
|
|
1104
|
+
has_runtime_option = any(
|
|
1105
|
+
argument in {"-e", "--eval", "-p", "--print", "--check"}
|
|
1106
|
+
or argument.startswith(("-e", "--eval=", "-p", "--print="))
|
|
1107
|
+
for argument in arguments
|
|
1108
|
+
)
|
|
1109
|
+
has_script_argument = any(not argument.startswith("-") for argument in arguments)
|
|
1110
|
+
if not has_runtime_option and not has_script_argument:
|
|
1111
|
+
return True
|
|
1112
|
+
return False
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _shell_prefix_command_name(tokens: list[str]) -> str:
|
|
1116
|
+
index = 0
|
|
1117
|
+
while index < len(tokens):
|
|
1118
|
+
token = tokens[index]
|
|
1119
|
+
lowered = token.casefold()
|
|
1120
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*=.*", token):
|
|
1121
|
+
index += 1
|
|
1122
|
+
continue
|
|
1123
|
+
if lowered not in _SHELL_PREFIX_WRAPPER_NAMES:
|
|
1124
|
+
if token.startswith("$"):
|
|
1125
|
+
return token.casefold()
|
|
1126
|
+
return token.rsplit("/", 1)[-1].casefold()
|
|
1127
|
+
option_values = _SHELL_WRAPPER_OPTION_VALUES.get(lowered, frozenset())
|
|
1128
|
+
index += 1
|
|
1129
|
+
while index < len(tokens):
|
|
1130
|
+
option = tokens[index]
|
|
1131
|
+
if option == "--":
|
|
1132
|
+
index += 1
|
|
1133
|
+
break
|
|
1134
|
+
if lowered == "env" and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*=.*", option):
|
|
1135
|
+
index += 1
|
|
1136
|
+
continue
|
|
1137
|
+
if option in option_values:
|
|
1138
|
+
index += 2
|
|
1139
|
+
continue
|
|
1140
|
+
if option.startswith("-"):
|
|
1141
|
+
index += 1
|
|
1142
|
+
continue
|
|
1143
|
+
break
|
|
1144
|
+
return ""
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _command_name(tokens: list[str]) -> str:
|
|
1148
|
+
return _shell_prefix_command_name(tokens)
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _shell_aliases(command: str) -> dict[str, str]:
|
|
1152
|
+
aliases: dict[str, str] = {}
|
|
1153
|
+
for token in _shell_tokens(command):
|
|
1154
|
+
match = _SHELL_ASSIGNMENT.fullmatch(token)
|
|
1155
|
+
if match is None:
|
|
1156
|
+
continue
|
|
1157
|
+
alias, operator, target = match.groups()
|
|
1158
|
+
alias = alias.casefold()
|
|
1159
|
+
if operator == "+=":
|
|
1160
|
+
target = aliases.get(alias, "") + target
|
|
1161
|
+
aliases[alias] = (
|
|
1162
|
+
_OPAQUE_SHELL_VALUE
|
|
1163
|
+
if _shell_substitutions(target) or "`" in target
|
|
1164
|
+
else target.casefold()
|
|
1165
|
+
)
|
|
1166
|
+
for match in re.finditer(
|
|
1167
|
+
r'(?<![A-Za-z0-9_])([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?=[^;&|\n]*(?:\$\(|`))', command
|
|
1168
|
+
):
|
|
1169
|
+
substitutions = _shell_substitutions(command[match.start() :])
|
|
1170
|
+
if substitutions or re.search(r"(?:\$\(|`)", command[match.start() :]):
|
|
1171
|
+
aliases[match.group(1).casefold()] = _OPAQUE_SHELL_VALUE
|
|
1172
|
+
for match in re.finditer(
|
|
1173
|
+
r"(?m)(?:^|[;&|{}])\s*for\s+([A-Za-z_][A-Za-z0-9_]*)\s+in\s+([^\n]*?)\s*(?:;|\n)\s*do\b",
|
|
1174
|
+
command,
|
|
1175
|
+
):
|
|
1176
|
+
words = match.group(2)
|
|
1177
|
+
source_is_opaque = bool(_shell_substitutions(words) or re.search(r"(?:\$\(|`)", words))
|
|
1178
|
+
resolved_sources: list[str] = []
|
|
1179
|
+
if not source_is_opaque:
|
|
1180
|
+
for parameter in _SHELL_PARAMETER.finditer(words):
|
|
1181
|
+
resolved = _resolve_shell_alias(parameter.group(0), aliases)
|
|
1182
|
+
if resolved == _OPAQUE_SHELL_VALUE:
|
|
1183
|
+
source_is_opaque = True
|
|
1184
|
+
break
|
|
1185
|
+
if resolved:
|
|
1186
|
+
resolved_sources.append(resolved)
|
|
1187
|
+
if source_is_opaque:
|
|
1188
|
+
aliases[match.group(1).casefold()] = _OPAQUE_SHELL_VALUE
|
|
1189
|
+
elif resolved_sources and len(set(resolved_sources)) == 1:
|
|
1190
|
+
aliases[match.group(1).casefold()] = resolved_sources[0]
|
|
1191
|
+
return aliases
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _resolve_shell_alias(name: str, aliases: dict[str, str]) -> str:
|
|
1195
|
+
resolved = name.casefold()
|
|
1196
|
+
seen: set[str] = set()
|
|
1197
|
+
for _ in range(_MAX_COMMAND_DEPTH):
|
|
1198
|
+
if _OPAQUE_SHELL_VALUE in resolved:
|
|
1199
|
+
return _OPAQUE_SHELL_VALUE
|
|
1200
|
+
single_parameter = re.fullmatch(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}", resolved)
|
|
1201
|
+
if single_parameter:
|
|
1202
|
+
resolved = single_parameter.group(1)
|
|
1203
|
+
expanded = _SHELL_PARAMETER.sub(
|
|
1204
|
+
lambda match: aliases.get(
|
|
1205
|
+
(match.group(1) or match.group(2)).casefold(), match.group(0)
|
|
1206
|
+
),
|
|
1207
|
+
resolved,
|
|
1208
|
+
)
|
|
1209
|
+
if expanded != resolved:
|
|
1210
|
+
resolved = expanded
|
|
1211
|
+
continue
|
|
1212
|
+
if resolved not in aliases or resolved in seen:
|
|
1213
|
+
break
|
|
1214
|
+
seen.add(resolved)
|
|
1215
|
+
resolved = aliases[resolved]
|
|
1216
|
+
if _OPAQUE_SHELL_VALUE in resolved:
|
|
1217
|
+
return _OPAQUE_SHELL_VALUE
|
|
1218
|
+
return resolved.strip().split(None, 1)[0].rsplit("/", 1)[-1] if resolved.strip() else ""
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _inline_shell_commands(tokens: list[str]) -> list[str]:
|
|
1222
|
+
if _command_name(tokens) not in _SHELL_COMMAND_NAMES:
|
|
1223
|
+
return []
|
|
1224
|
+
commands: list[str] = []
|
|
1225
|
+
for index, token in enumerate(tokens[:-1]):
|
|
1226
|
+
if token in {"-c", "--command"} or (token.startswith("-") and "c" in token[1:]):
|
|
1227
|
+
commands.append(tokens[index + 1])
|
|
1228
|
+
return commands
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def _find_exec_shell_commands(tokens: list[str]) -> list[str]:
|
|
1232
|
+
commands: list[str] = []
|
|
1233
|
+
index = 0
|
|
1234
|
+
while index < len(tokens):
|
|
1235
|
+
if tokens[index] not in {"-exec", "-execdir"}:
|
|
1236
|
+
index += 1
|
|
1237
|
+
continue
|
|
1238
|
+
end = index + 1
|
|
1239
|
+
while end < len(tokens) and tokens[end] not in {"+", ";"}:
|
|
1240
|
+
end += 1
|
|
1241
|
+
commands.extend(_inline_shell_commands(tokens[index + 1 : end]))
|
|
1242
|
+
index = end + 1 if end < len(tokens) else end
|
|
1243
|
+
return commands
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _base64_followed_by_execution(command: str) -> bool:
|
|
1247
|
+
bounded = command[:_MAX_GUARD_TEXT]
|
|
1248
|
+
if _BASE64_DECODE.search(bounded) and _BASE64_EXECUTION.search(bounded):
|
|
1249
|
+
return True
|
|
1250
|
+
if len(command) > _MAX_GUARD_TEXT:
|
|
1251
|
+
return bool(_BASE64_DECODE.search(command) or _BASE64_EXECUTION.search(command))
|
|
1252
|
+
return False
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _alias_is_called(command: str, alias: str) -> bool:
|
|
1256
|
+
return re.search(rf"\b{re.escape(alias)}\s*\(", command) is not None
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def _python_aliases(command: str) -> dict[str, str]:
|
|
1260
|
+
aliases: dict[str, str] = {}
|
|
1261
|
+
for match in re.finditer(
|
|
1262
|
+
r"\bimport\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)(?:\s+as\s+([A-Za-z_]\w*))?",
|
|
1263
|
+
command,
|
|
1264
|
+
re.IGNORECASE,
|
|
1265
|
+
):
|
|
1266
|
+
module, alias = match.groups()
|
|
1267
|
+
if module.casefold() in _PYTHON_NETWORK_MODULES:
|
|
1268
|
+
aliases[(alias or module.rsplit(".", 1)[-1]).casefold()] = module
|
|
1269
|
+
for match in re.finditer(
|
|
1270
|
+
r"\bfrom\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)\s+import\s+([^;\n]+)",
|
|
1271
|
+
command,
|
|
1272
|
+
re.IGNORECASE,
|
|
1273
|
+
):
|
|
1274
|
+
module, imported_names = match.groups()
|
|
1275
|
+
if module.casefold() not in _PYTHON_NETWORK_MODULES:
|
|
1276
|
+
continue
|
|
1277
|
+
for imported in imported_names.split(","):
|
|
1278
|
+
parts = imported.strip().split()
|
|
1279
|
+
if not parts or parts[0] == "*":
|
|
1280
|
+
continue
|
|
1281
|
+
name = parts[0]
|
|
1282
|
+
alias = parts[2] if len(parts) >= 3 and parts[1].casefold() == "as" else name
|
|
1283
|
+
aliases[alias.casefold()] = f"{module}.{name}"
|
|
1284
|
+
for match in re.finditer(
|
|
1285
|
+
r"\b([A-Za-z_]\w*)\s*=(?!=)\s*([A-Za-z_]\w*)(?:\s*\.\s*([A-Za-z_]\w*))?",
|
|
1286
|
+
command,
|
|
1287
|
+
re.IGNORECASE,
|
|
1288
|
+
):
|
|
1289
|
+
alias, base, member = match.groups()
|
|
1290
|
+
aliases[alias.casefold()] = f"{base}.{member}" if member else base
|
|
1291
|
+
return aliases
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def _python_target_is_network(target: str, aliases: dict[str, str], seen: set[str] | None = None) -> bool:
|
|
1295
|
+
current = target.strip()
|
|
1296
|
+
lowered = current.casefold()
|
|
1297
|
+
if lowered in _PYTHON_NETWORK_MODULES:
|
|
1298
|
+
return True
|
|
1299
|
+
seen = set() if seen is None else seen
|
|
1300
|
+
if lowered in seen:
|
|
1301
|
+
return False
|
|
1302
|
+
seen.add(lowered)
|
|
1303
|
+
if "." in current:
|
|
1304
|
+
base, member = current.rsplit(".", 1)
|
|
1305
|
+
if member.casefold() in {name.casefold() for name in _PYTHON_NETWORK_MEMBERS}:
|
|
1306
|
+
if _python_target_is_network(base, aliases, seen):
|
|
1307
|
+
return True
|
|
1308
|
+
alias_target = aliases.get(lowered)
|
|
1309
|
+
return bool(alias_target and _python_target_is_network(alias_target, aliases, seen))
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def _python_request_shaped(command: str) -> bool:
|
|
1313
|
+
if _PYTHON_REQUEST.search(command):
|
|
1314
|
+
return True
|
|
1315
|
+
compact = re.sub(r"\s+", " ", command)
|
|
1316
|
+
if _PYTHON_NETWORK_IMPORT.search(compact) is None:
|
|
1317
|
+
return False
|
|
1318
|
+
for match in _PYTHON_MEMBER_ASSIGNMENT.finditer(compact):
|
|
1319
|
+
if _alias_is_called(compact, match.group(1)):
|
|
1320
|
+
return True
|
|
1321
|
+
for match in _PYTHON_ALIAS_IMPORT.finditer(compact):
|
|
1322
|
+
if _alias_is_called(compact, match.group(1)):
|
|
1323
|
+
return True
|
|
1324
|
+
for match in _PYTHON_DYNAMIC_ALIAS.finditer(compact):
|
|
1325
|
+
if _alias_is_called(compact, match.group(1)):
|
|
1326
|
+
return True
|
|
1327
|
+
aliases = _python_aliases(compact)
|
|
1328
|
+
for match in re.finditer(r"\b([A-Za-z_]\w*)\s*\(", compact):
|
|
1329
|
+
if _python_target_is_network(match.group(1), aliases):
|
|
1330
|
+
return True
|
|
1331
|
+
return bool(
|
|
1332
|
+
_PYTHON_MEMBER_CALL.search(compact)
|
|
1333
|
+
or _PYTHON_FROM_REQUEST.search(compact)
|
|
1334
|
+
or _PYTHON_REQUEST.search(compact)
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _node_request_shaped(command: str) -> bool:
|
|
1339
|
+
if _NODE_REQUEST.search(command):
|
|
1340
|
+
return True
|
|
1341
|
+
compact = re.sub(r"\s+", " ", command)
|
|
1342
|
+
if _NODE_NETWORK_MARKER.search(compact) is None:
|
|
1343
|
+
return False
|
|
1344
|
+
aliases: dict[str, str] = {}
|
|
1345
|
+
for match in _NODE_SIMPLE_ALIAS.finditer(compact):
|
|
1346
|
+
alias, base, member = match.groups()
|
|
1347
|
+
aliases[alias.casefold()] = f"{base}.{member}" if member else base
|
|
1348
|
+
for match in _NODE_GLOBAL_FETCH_ALIAS.finditer(compact):
|
|
1349
|
+
aliases[match.group(1).casefold()] = "fetch"
|
|
1350
|
+
for match in _NODE_REQUIRE_ALIAS.finditer(compact):
|
|
1351
|
+
aliases[match.group(1).casefold()] = f"https.{match.group(2)}"
|
|
1352
|
+
for match in _NODE_IMPORT_ALIASES.finditer(compact):
|
|
1353
|
+
for imported in match.group(1).split(","):
|
|
1354
|
+
parts = imported.strip().split()
|
|
1355
|
+
if not parts:
|
|
1356
|
+
continue
|
|
1357
|
+
name = parts[0]
|
|
1358
|
+
alias = parts[2] if len(parts) >= 3 and parts[1].casefold() == "as" else name
|
|
1359
|
+
aliases[alias.casefold()] = f"https.{name}"
|
|
1360
|
+
for match in _NODE_REQUIRE_DESTRUCTURED.finditer(compact):
|
|
1361
|
+
for imported in match.group(1).split(","):
|
|
1362
|
+
parts = imported.strip().split(":", 1)
|
|
1363
|
+
name = parts[0].strip()
|
|
1364
|
+
alias = parts[1].strip() if len(parts) == 2 else name
|
|
1365
|
+
if name in {"get", "request"}:
|
|
1366
|
+
aliases[alias.casefold()] = f"https.{name}"
|
|
1367
|
+
for match in _NODE_MEMBER_ASSIGNMENT.finditer(compact):
|
|
1368
|
+
member_aliases = [alias for alias in match.groups() if alias]
|
|
1369
|
+
if any(_alias_is_called(compact, alias) for alias in member_aliases):
|
|
1370
|
+
return True
|
|
1371
|
+
for match in _NODE_DIRECT_ALIAS.finditer(compact):
|
|
1372
|
+
if _alias_is_called(compact, match.group(1)):
|
|
1373
|
+
return True
|
|
1374
|
+
for pattern in (_NODE_NAMED_IMPORT_ALIAS, _NODE_DESTRUCTURED_ALIAS):
|
|
1375
|
+
for match in pattern.finditer(compact):
|
|
1376
|
+
if _alias_is_called(compact, match.group(1)):
|
|
1377
|
+
return True
|
|
1378
|
+
def target_is_network(target: str, seen: set[str] | None = None) -> bool:
|
|
1379
|
+
current = target.casefold()
|
|
1380
|
+
if current == "fetch" or re.fullmatch(r"(?:node:)?https?\.(?:get|request)", current):
|
|
1381
|
+
return True
|
|
1382
|
+
seen = set() if seen is None else seen
|
|
1383
|
+
if current in seen:
|
|
1384
|
+
return False
|
|
1385
|
+
seen.add(current)
|
|
1386
|
+
alias_target = aliases.get(current)
|
|
1387
|
+
return bool(alias_target and target_is_network(alias_target, seen))
|
|
1388
|
+
|
|
1389
|
+
for match in re.finditer(r"\b([A-Za-z_]\w*)\s*\(", compact):
|
|
1390
|
+
if target_is_network(match.group(1)):
|
|
1391
|
+
return True
|
|
1392
|
+
return bool(re.search(r"\b(?:fetch|get|request)\s*\(", compact))
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _runtime_code_is_opaque(tokens: list[str]) -> bool:
|
|
1396
|
+
for index, token in enumerate(tokens[:-1]):
|
|
1397
|
+
if token in {"-c", "-e", "--eval", "--command"} and tokens[index + 1].startswith(("$", "<")):
|
|
1398
|
+
return True
|
|
1399
|
+
return False
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def _git_network_command(tokens: list[str]) -> bool:
|
|
1403
|
+
if not tokens or _command_name(tokens) != "git":
|
|
1404
|
+
return False
|
|
1405
|
+
return any(token.casefold() in _GIT_NETWORK_SUBCOMMANDS for token in tokens[1:])
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _url_is_retrieval_shaped(tokens: list[str], command: str, name: str) -> bool:
|
|
1409
|
+
if not _has_http_url(command):
|
|
1410
|
+
return False
|
|
1411
|
+
if name in _URL_INERT_COMMAND_NAMES and name != "git":
|
|
1412
|
+
return False
|
|
1413
|
+
if name == "git":
|
|
1414
|
+
return _git_network_command(tokens)
|
|
1415
|
+
return True
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def _command_is_fetch_shaped(
|
|
1419
|
+
command: str, _depth: int = 0, _aliases: dict[str, str] | None = None
|
|
1420
|
+
) -> bool:
|
|
1421
|
+
if not isinstance(command, str):
|
|
1422
|
+
return False
|
|
1423
|
+
if _SHELL_NETWORK_ACCESS.search(command):
|
|
1424
|
+
return True
|
|
1425
|
+
if _shell_substitution_is_opaque(command):
|
|
1426
|
+
return True
|
|
1427
|
+
aliases = _aliases if _aliases is not None else _shell_aliases(command)
|
|
1428
|
+
if any(
|
|
1429
|
+
_resolve_shell_alias(match.group(1), aliases) in (_FETCH_COMMAND_NAMES | {_OPAQUE_SHELL_VALUE})
|
|
1430
|
+
for match in _SHELL_PARAMETER_COMMAND_START.finditer(command)
|
|
1431
|
+
):
|
|
1432
|
+
return True
|
|
1433
|
+
if any(_command_is_fetch_shaped(value, _depth + 1, aliases) for value in _shell_substitutions(command)):
|
|
1434
|
+
return True
|
|
1435
|
+
lines = _split_unquoted_newlines(command)
|
|
1436
|
+
if len(lines) > 1:
|
|
1437
|
+
return any(_command_is_fetch_shaped(line, _depth, aliases) for line in lines)
|
|
1438
|
+
if command.lstrip().startswith("#"):
|
|
1439
|
+
return False
|
|
1440
|
+
if _depth >= _MAX_COMMAND_DEPTH:
|
|
1441
|
+
tokens = _shell_tokens(command)
|
|
1442
|
+
names = {_command_name(segment) for segment in _command_segments(tokens)}
|
|
1443
|
+
return bool(
|
|
1444
|
+
names & (_SHELL_COMMAND_NAMES | _FETCH_COMMAND_NAMES | _NODE_COMMAND_NAMES | _PYTHON_COMMAND_NAMES)
|
|
1445
|
+
or _has_http_url(command)
|
|
1446
|
+
or _base64_followed_by_execution(command)
|
|
1447
|
+
)
|
|
1448
|
+
if _base64_followed_by_execution(command):
|
|
1449
|
+
return True
|
|
1450
|
+
tokens = _shell_tokens(command)
|
|
1451
|
+
if not tokens:
|
|
1452
|
+
return _has_http_url(command)
|
|
1453
|
+
if _runtime_reads_code_from_stdin(tokens, aliases):
|
|
1454
|
+
return True
|
|
1455
|
+
for segment in _command_segments(tokens):
|
|
1456
|
+
name = _command_name(segment)
|
|
1457
|
+
effective_name = _resolve_shell_alias(name, aliases)
|
|
1458
|
+
if effective_name == _OPAQUE_SHELL_VALUE:
|
|
1459
|
+
return True
|
|
1460
|
+
if effective_name == "find":
|
|
1461
|
+
nested = _find_exec_shell_commands(segment)
|
|
1462
|
+
if nested and any(_command_is_fetch_shaped(value, _depth + 1, aliases) for value in nested):
|
|
1463
|
+
return True
|
|
1464
|
+
if effective_name in _SHELL_COMMAND_NAMES:
|
|
1465
|
+
nested = _inline_shell_commands(segment)
|
|
1466
|
+
if nested and any(_command_is_fetch_shaped(value, _depth + 1, aliases) for value in nested):
|
|
1467
|
+
return True
|
|
1468
|
+
if nested and any(value.lstrip().startswith(("$", "<")) for value in nested):
|
|
1469
|
+
return True
|
|
1470
|
+
if nested:
|
|
1471
|
+
continue
|
|
1472
|
+
if effective_name in _FETCH_COMMAND_NAMES:
|
|
1473
|
+
return True
|
|
1474
|
+
if _git_network_command(segment):
|
|
1475
|
+
return True
|
|
1476
|
+
body = " ".join(segment)
|
|
1477
|
+
if effective_name in _SHELL_EXECUTION_NAMES:
|
|
1478
|
+
if any(token.startswith(("$", "<")) for token in segment[1:]):
|
|
1479
|
+
return True
|
|
1480
|
+
if re.search(
|
|
1481
|
+
r"\b(?:curl|fetch|http|http\.client|httpie|lynx|open_url|wget|w3m)\b",
|
|
1482
|
+
body,
|
|
1483
|
+
re.IGNORECASE,
|
|
1484
|
+
):
|
|
1485
|
+
return True
|
|
1486
|
+
if effective_name in _NODE_COMMAND_NAMES and _node_request_shaped(body):
|
|
1487
|
+
return True
|
|
1488
|
+
if effective_name in _PYTHON_COMMAND_NAMES and _python_request_shaped(body):
|
|
1489
|
+
return True
|
|
1490
|
+
if effective_name in _RUBY_COMMAND_NAMES and _RUBY_NETWORK_REQUEST.search(body):
|
|
1491
|
+
return True
|
|
1492
|
+
if effective_name in _PHP_COMMAND_NAMES and _PHP_NETWORK_REQUEST.search(body):
|
|
1493
|
+
return True
|
|
1494
|
+
if effective_name in (_NODE_COMMAND_NAMES | _PYTHON_COMMAND_NAMES) and _runtime_code_is_opaque(segment):
|
|
1495
|
+
return True
|
|
1496
|
+
if any(token.startswith("$") for token in segment) and (
|
|
1497
|
+
_node_request_shaped(body) or _python_request_shaped(body)
|
|
1498
|
+
):
|
|
1499
|
+
return True
|
|
1500
|
+
if _base64_followed_by_execution(body):
|
|
1501
|
+
return True
|
|
1502
|
+
if _url_is_retrieval_shaped(segment, body, effective_name):
|
|
1503
|
+
return True
|
|
1504
|
+
return False
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def _url_argument_present(args: Any) -> bool:
|
|
1508
|
+
seen: set[int] = set()
|
|
1509
|
+
nodes = 0
|
|
1510
|
+
|
|
1511
|
+
def visit(value: Any, depth: int) -> bool:
|
|
1512
|
+
nonlocal nodes
|
|
1513
|
+
if isinstance(value, str):
|
|
1514
|
+
return _has_http_url(value)
|
|
1515
|
+
if not isinstance(value, (dict, list, tuple)):
|
|
1516
|
+
return False
|
|
1517
|
+
marker = id(value)
|
|
1518
|
+
if marker in seen:
|
|
1519
|
+
return False
|
|
1520
|
+
if depth > _MAX_URL_ARGUMENT_DEPTH or nodes >= _MAX_URL_ARGUMENT_NODES:
|
|
1521
|
+
return True
|
|
1522
|
+
seen.add(marker)
|
|
1523
|
+
nodes += 1
|
|
1524
|
+
children = value.items() if isinstance(value, dict) else value
|
|
1525
|
+
for child in children:
|
|
1526
|
+
if isinstance(value, dict):
|
|
1527
|
+
key, item = child
|
|
1528
|
+
if visit(key, depth + 1) or visit(item, depth + 1):
|
|
1529
|
+
return True
|
|
1530
|
+
elif visit(child, depth + 1):
|
|
1531
|
+
return True
|
|
1532
|
+
return False
|
|
1533
|
+
|
|
1534
|
+
return visit(args, 0)
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
def _is_fetch_shaped(tool_name: str, args: Any) -> bool:
|
|
1538
|
+
raw_tool_name = tool_name.strip() if isinstance(tool_name, str) else str(tool_name or "")
|
|
1539
|
+
lowered = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", raw_tool_name).casefold()
|
|
1540
|
+
if lowered in _FETCH_TOOL_NAMES or lowered.startswith(("browser_", "web_")) or _HOST_BROWSER_TOOL.search(lowered):
|
|
1541
|
+
return True
|
|
1542
|
+
if lowered in _FETCH_TOOL_ALIASES and _url_argument_present(args):
|
|
1543
|
+
return True
|
|
1544
|
+
if not isinstance(args, dict):
|
|
1545
|
+
return False
|
|
1546
|
+
for key in _COMMAND_FIELDS:
|
|
1547
|
+
value = args.get(key)
|
|
1548
|
+
if isinstance(value, str) and _command_is_fetch_shaped(value):
|
|
1549
|
+
return True
|
|
1550
|
+
return False
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def browser_drive_tool_guard(
|
|
1554
|
+
tool_name: str = "", args: Any = None, session_id: Any = "", **_: Any
|
|
1555
|
+
) -> dict[str, str] | None:
|
|
1556
|
+
session = _browser_session_id(session_id)
|
|
1557
|
+
tool = str(tool_name or "")
|
|
1558
|
+
fetch_shaped = _is_fetch_shaped(tool, args)
|
|
1559
|
+
if session is None:
|
|
1560
|
+
return None
|
|
1561
|
+
if session not in _BROWSER_DRIVE_GUARD:
|
|
1562
|
+
if not fetch_shaped:
|
|
1563
|
+
return None
|
|
1564
|
+
if session in _BROWSER_DRIVE_EVICTED_SESSIONS:
|
|
1565
|
+
return {
|
|
1566
|
+
"action": "block",
|
|
1567
|
+
"message": (
|
|
1568
|
+
f"{_BLOCKER_STATE}: the browser-drive route state exceeded its bounded session limit. "
|
|
1569
|
+
"Release the tracked session before retrieving a page."
|
|
1570
|
+
),
|
|
1571
|
+
}
|
|
1572
|
+
return None
|
|
1573
|
+
if not fetch_shaped:
|
|
1574
|
+
return None
|
|
1575
|
+
report = _BROWSER_DRIVE_REPORTS.get(session, _default_browser_driver_report())
|
|
1576
|
+
if report.get("status") == "available":
|
|
1577
|
+
return None
|
|
1578
|
+
blocker = report.get("blocker")
|
|
1579
|
+
if blocker not in {
|
|
1580
|
+
"BLOCKED_BROWSER_DRIVER_IDENTITY_UNVERIFIED",
|
|
1581
|
+
"BLOCKED_BROWSER_DRIVER_UNAVAILABLE",
|
|
1582
|
+
BROWSER_IDENTITY_BLOCKER,
|
|
1583
|
+
}:
|
|
1584
|
+
blocker = "BLOCKED_BROWSER_DRIVER_UNAVAILABLE"
|
|
1585
|
+
# The harness's canonical denial payload is {"action": "block", "message": ...}.
|
|
1586
|
+
# An invented shape is silently ignored, which is how the first attempt at this
|
|
1587
|
+
# guard passed its own tests and changed nothing on the host.
|
|
1588
|
+
return {
|
|
1589
|
+
"action": "block",
|
|
1590
|
+
"message": (
|
|
1591
|
+
f"{blocker}: this turn routed to lithermes:browser-drive and the "
|
|
1592
|
+
"capability probe measured no verified driver. Retrieving the page by another means is the "
|
|
1593
|
+
"substitution this contract exists to prevent. Report the blocker and mark what could not "
|
|
1594
|
+
"be established as [UNVERIFIED]."
|
|
1595
|
+
),
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
def pre_llm_call(**kwargs: Any) -> dict[str, str] | None:
|
|
1600
|
+
# Hermes identifies delegate_task children with platform="subagent".
|
|
1601
|
+
# Child goals may quote words such as "lit" or "lit-crucible" as inert task
|
|
1602
|
+
# context; re-running top-level activation here would contaminate the lane.
|
|
1603
|
+
if is_delegate_child_platform(str(kwargs.get("platform") or "")):
|
|
1604
|
+
release_browser_drive_state(kwargs.get("session_id"))
|
|
1605
|
+
return None
|
|
1606
|
+
# A first interactive top-level turn is the only plugin lifecycle point
|
|
1607
|
+
# where the Python payload can safely foreground an exact npm update. The
|
|
1608
|
+
# bridge owns its own recursion guard, timeout, transaction, rollback, and
|
|
1609
|
+
# receipt; it never contributes text to the model-facing context.
|
|
1610
|
+
try:
|
|
1611
|
+
_auto_update.pre_llm_call(**kwargs)
|
|
1612
|
+
except _auto_update.UnknownStateError:
|
|
1613
|
+
# A failed restore is not an advisory miss: continuing would let the
|
|
1614
|
+
# model operate against a profile whose installed payload is unknown.
|
|
1615
|
+
raise
|
|
1616
|
+
except Exception:
|
|
1617
|
+
# Ordinary registry/installer failures remain fail-open for this turn;
|
|
1618
|
+
# the transaction receipt records the redacted reason.
|
|
1619
|
+
pass
|
|
1620
|
+
user_message = str(kwargs.get("user_message") or "")
|
|
1621
|
+
requested_reader_mode = reader_facing_mode_from_user_request(user_message)
|
|
1622
|
+
reader_contract = reader_facing_contract_block(
|
|
1623
|
+
requested_reader_mode,
|
|
1624
|
+
authority="current_user_request" if requested_reader_mode is not None else None,
|
|
1625
|
+
)
|
|
1626
|
+
|
|
1627
|
+
def with_reader_contract(context: str, *, compact: bool = False) -> dict[str, str]:
|
|
1628
|
+
contract = (
|
|
1629
|
+
reader_facing_contract_block(
|
|
1630
|
+
requested_reader_mode,
|
|
1631
|
+
authority=(
|
|
1632
|
+
"current_user_request" if requested_reader_mode is not None else None
|
|
1633
|
+
),
|
|
1634
|
+
compact=True,
|
|
1635
|
+
)
|
|
1636
|
+
if compact
|
|
1637
|
+
else reader_contract
|
|
1638
|
+
)
|
|
1639
|
+
return {"context": f"{context.rstrip()}\n\n{contract}"}
|
|
1640
|
+
|
|
1641
|
+
session_id = _browser_session_id(kwargs.get("session_id")) or ""
|
|
1642
|
+
if session_id:
|
|
1643
|
+
# The guard belongs to this turn's route. Clear a previous browser route
|
|
1644
|
+
# before a new message can select another mode.
|
|
1645
|
+
release_browser_drive_guard(session_id)
|
|
1646
|
+
# Clear any stale ignite flag from an interrupted previous turn so it can't
|
|
1647
|
+
# leak the banner onto this turn's response. Re-added below only if THIS turn
|
|
1648
|
+
# is a keyword-lit turn — keeping the flag scoped to the current turn.
|
|
1649
|
+
_PENDING_IGNITE.discard(session_id)
|
|
1650
|
+
_PENDING_DISCIPLINE.pop(session_id, None)
|
|
1651
|
+
_PENDING_BLOCK.pop(session_id, None)
|
|
1652
|
+
# /litgoal & /lit-plan carry a legacy bind-goal marker. It remains a bounded
|
|
1653
|
+
# route envelope only; native /goal is user-managed and never mutated here.
|
|
1654
|
+
bind_obj = _extract_bind_goal(user_message)
|
|
1655
|
+
if bind_obj:
|
|
1656
|
+
return None
|
|
1657
|
+
# /lit and /lit-loop inject a run-context message. Durable run state already
|
|
1658
|
+
# exists, so skip re-injecting context without touching native /goal state.
|
|
1659
|
+
if "<lithermes-run-context>" in user_message:
|
|
1660
|
+
return None
|
|
1661
|
+
# Read-only recap route runs FIRST so `lit recap` never falls through to
|
|
1662
|
+
# the run-writing litwork path. It never binds a goal or writes run state.
|
|
1663
|
+
recap_args = detect_recap(user_message)
|
|
1664
|
+
if recap_args is not None:
|
|
1665
|
+
record_event(
|
|
1666
|
+
"litwork_trigger",
|
|
1667
|
+
session_id=session_id,
|
|
1668
|
+
platform=str(kwargs.get("platform") or ""),
|
|
1669
|
+
mode="lit-recap",
|
|
1670
|
+
)
|
|
1671
|
+
if session_id:
|
|
1672
|
+
_PENDING_IGNITE.add(session_id)
|
|
1673
|
+
_PENDING_DISCIPLINE[session_id] = "lit-recap"
|
|
1674
|
+
_evict_pending_browser_state()
|
|
1675
|
+
return {"context": command_lit_recap(recap_args)["agent_message"]}
|
|
1676
|
+
route = detect_lit_mode(user_message)
|
|
1677
|
+
if route is None:
|
|
1678
|
+
return None
|
|
1679
|
+
_deliverable_hedges.activate_skill(session_id, route.mode)
|
|
1680
|
+
|
|
1681
|
+
record_event(
|
|
1682
|
+
"litwork_trigger",
|
|
1683
|
+
session_id=session_id,
|
|
1684
|
+
platform=str(kwargs.get("platform") or ""),
|
|
1685
|
+
mode=route.mode,
|
|
1686
|
+
)
|
|
1687
|
+
if session_id:
|
|
1688
|
+
_PENDING_IGNITE.add(session_id)
|
|
1689
|
+
_PENDING_DISCIPLINE[session_id] = discipline(route.mode)
|
|
1690
|
+
_evict_pending_browser_state()
|
|
1691
|
+
|
|
1692
|
+
if route.blocked:
|
|
1693
|
+
if session_id:
|
|
1694
|
+
_PENDING_BLOCK[session_id] = route.block_message
|
|
1695
|
+
_evict_pending_browser_state()
|
|
1696
|
+
return with_reader_contract(build_natural_mode_context(route), compact=True)
|
|
1697
|
+
|
|
1698
|
+
browser_report = None
|
|
1699
|
+
if route.mode == "browser-drive":
|
|
1700
|
+
# Measure once. The same structured report goes to the context and the guard.
|
|
1701
|
+
browser_report = _normalise_browser_driver_report(_browser_drive_probe_report())
|
|
1702
|
+
if session_id:
|
|
1703
|
+
arm_browser_drive_guard(session_id, browser_report)
|
|
1704
|
+
if route.mode == "lit-plan" and route.objective:
|
|
1705
|
+
planned = command_lit_plan(shlex.quote(route.objective))
|
|
1706
|
+
return with_reader_contract(
|
|
1707
|
+
f"{build_natural_mode_context(route)}\n\n"
|
|
1708
|
+
f"Durable plan: {planned['plan']}",
|
|
1709
|
+
compact=True,
|
|
1710
|
+
)
|
|
1711
|
+
if route.mode != "litwork":
|
|
1712
|
+
if route.mode in _SKILL_ROUTE_CONTRACTS:
|
|
1713
|
+
compact_contract = reader_facing_contract_block(
|
|
1714
|
+
requested_reader_mode,
|
|
1715
|
+
authority=(
|
|
1716
|
+
"current_user_request" if requested_reader_mode is not None else None
|
|
1717
|
+
),
|
|
1718
|
+
compact=True,
|
|
1719
|
+
)
|
|
1720
|
+
return {
|
|
1721
|
+
"context": build_natural_mode_context(
|
|
1722
|
+
route,
|
|
1723
|
+
browser_report=browser_report,
|
|
1724
|
+
reader_contract=compact_contract,
|
|
1725
|
+
)
|
|
1726
|
+
}
|
|
1727
|
+
return with_reader_contract(
|
|
1728
|
+
build_natural_mode_context(route, browser_report=browser_report),
|
|
1729
|
+
compact=True,
|
|
1730
|
+
)
|
|
1731
|
+
|
|
1732
|
+
direct = DIRECT_LIT_PATTERN.match(route.visible_message)
|
|
1733
|
+
run_context = ""
|
|
1734
|
+
if direct:
|
|
1735
|
+
task = route.objective or _clamp_task(direct.group("task"))
|
|
1736
|
+
if task:
|
|
1737
|
+
workspace = Path.cwd().resolve()
|
|
1738
|
+
run_dir = write_run_state(
|
|
1739
|
+
workspace,
|
|
1740
|
+
task=task,
|
|
1741
|
+
command="lit",
|
|
1742
|
+
)
|
|
1743
|
+
run_context = "\n\n" + build_run_agent_message(
|
|
1744
|
+
load_run_state(run_dir), include_reader_contract=False
|
|
1745
|
+
)
|
|
1746
|
+
if requested_reader_mode is None:
|
|
1747
|
+
if run_context:
|
|
1748
|
+
return {
|
|
1749
|
+
"context": (
|
|
1750
|
+
f"{LIT_CONTEXT}{run_context}\n\n"
|
|
1751
|
+
"#contract.parent_projection_final: reader disclosure still governs this parent reply; detailed run/evidence/DoneClaim clauses above remain internal unless requested or materially necessary."
|
|
1752
|
+
)
|
|
1753
|
+
}
|
|
1754
|
+
return {"context": LIT_CONTEXT}
|
|
1755
|
+
return with_reader_contract(LIT_CONTEXT_BASE + run_context)
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def transform_llm_output(**kwargs: Any) -> str | None:
|
|
1759
|
+
"""Draw the route mark once, without forging or normalizing a model probe.
|
|
1760
|
+
|
|
1761
|
+
Natural routes have no native display return channel before inference.
|
|
1762
|
+
Hermes applies this transform when the reply arrives. A blocked route still
|
|
1763
|
+
replaces unsafe output with its measured denial, independently of the probe.
|
|
1764
|
+
"""
|
|
1765
|
+
session_id = str(kwargs.get("session_id") or "")
|
|
1766
|
+
block = _PENDING_BLOCK.pop(session_id, "")
|
|
1767
|
+
route = _PENDING_DISCIPLINE.pop(session_id, "litwork")
|
|
1768
|
+
pending = session_id in _PENDING_IGNITE
|
|
1769
|
+
_PENDING_IGNITE.discard(session_id)
|
|
1770
|
+
if not pending and not block:
|
|
1771
|
+
return None
|
|
1772
|
+
response = block or str(kwargs.get("response_text") or "")
|
|
1773
|
+
return acknowledge_reply(route, response)
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def subagent_stop(**kwargs: Any) -> None:
|
|
1777
|
+
"""Record each delegate_task child (e.g. a review lane) to the LitHermes ledger.
|
|
1778
|
+
|
|
1779
|
+
Hermes fires this once per child after delegate_task finishes. Observer-only:
|
|
1780
|
+
the return value is ignored.
|
|
1781
|
+
"""
|
|
1782
|
+
record_event(
|
|
1783
|
+
"subagent_stop",
|
|
1784
|
+
parent_session_id=str(kwargs.get("parent_session_id") or ""),
|
|
1785
|
+
child_role=str(kwargs.get("child_role") or ""),
|
|
1786
|
+
child_status=str(kwargs.get("child_status") or ""),
|
|
1787
|
+
duration_ms=kwargs.get("duration_ms"),
|
|
1788
|
+
)
|
|
1789
|
+
return None
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
try:
|
|
1793
|
+
from . import diagnostics as _diagnostics
|
|
1794
|
+
except (ImportError, ModuleNotFoundError):
|
|
1795
|
+
import diagnostics as _diagnostics
|
|
1796
|
+
|
|
1797
|
+
rules = _rules
|
|
1798
|
+
|
|
1799
|
+
PLUGIN_ROOT = _diagnostics.PLUGIN_ROOT
|
|
1800
|
+
HOOKS = _diagnostics.HOOKS
|
|
1801
|
+
SLASH_COMMANDS = _diagnostics.SLASH_COMMANDS
|
|
1802
|
+
GOAL_TOOLS = _diagnostics.GOAL_TOOLS
|
|
1803
|
+
plugin_version = _diagnostics.plugin_version
|
|
1804
|
+
version_line = _diagnostics.version_line
|
|
1805
|
+
hermes_host_version = _diagnostics.hermes_host_version
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def status_report() -> str:
|
|
1809
|
+
return _diagnostics.status_report(hermes_host_version())
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
def doctor_report() -> tuple[list[str], int]:
|
|
1813
|
+
return _diagnostics.doctor_report(hermes_host_version())
|