dsh-compound-engineering 0.0.0-stage → 0.1.1
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 +160 -2
- package/README.zh.md +161 -0
- package/cordis.patch.yml +6 -0
- package/lib/frontmatter.js +167 -0
- package/lib/index.js +46 -0
- package/lib/provider.js +201 -0
- package/package.json +62 -4
- package/skills/CODING_STANDARDS.md +68 -0
- package/skills/ce-babysit-pr/SKILL.md +59 -0
- package/skills/ce-babysit-pr/references/branch-currency.md +22 -0
- package/skills/ce-babysit-pr/references/envelope.md +44 -0
- package/skills/ce-babysit-pr/references/pipeline.md +15 -0
- package/skills/ce-babysit-pr/references/report.md +20 -0
- package/skills/ce-babysit-pr/references/settle.md +37 -0
- package/skills/ce-babysit-pr/references/setup.md +33 -0
- package/skills/ce-babysit-pr/references/stack-commands.md +39 -0
- package/skills/ce-babysit-pr/references/stack.md +43 -0
- package/skills/ce-babysit-pr/references/tick.md +102 -0
- package/skills/ce-babysit-pr/references/watch-loop.md +222 -0
- package/skills/ce-babysit-pr/scripts/pr-snapshot +3656 -0
- package/skills/ce-bakeoff/SKILL.md +48 -0
- package/skills/ce-bakeoff/references/candidates.md +37 -0
- package/skills/ce-bakeoff/references/judging.md +19 -0
- package/skills/ce-bakeoff/references/output.md +11 -0
- package/skills/ce-bakeoff/references/verification.md +15 -0
- package/skills/ce-brainstorm/SKILL.md +69 -0
- package/skills/ce-brainstorm/references/agents/slack-researcher.md +127 -0
- package/skills/ce-brainstorm/references/approaches.md +57 -0
- package/skills/ce-brainstorm/references/bakeoff.md +9 -0
- package/skills/ce-brainstorm/references/blindspot-pass.md +70 -0
- package/skills/ce-brainstorm/references/brainstorm-sections.md +458 -0
- package/skills/ce-brainstorm/references/dialogue.md +89 -0
- package/skills/ce-brainstorm/references/handoff.md +187 -0
- package/skills/ce-brainstorm/references/html-rendering.md +638 -0
- package/skills/ce-brainstorm/references/interaction-rules.md +33 -0
- package/skills/ce-brainstorm/references/markdown-rendering.md +242 -0
- package/skills/ce-brainstorm/references/model-tiers.md +13 -0
- package/skills/ce-brainstorm/references/output-mode.md +26 -0
- package/skills/ce-brainstorm/references/phase-0.md +104 -0
- package/skills/ce-brainstorm/references/plan-write.md +32 -0
- package/skills/ce-brainstorm/references/product-pressure-test.md +42 -0
- package/skills/ce-brainstorm/references/reasoning-elevation.md +124 -0
- package/skills/ce-brainstorm/references/settled-decisions.md +45 -0
- package/skills/ce-brainstorm/references/synthesis-summary.md +149 -0
- package/skills/ce-brainstorm/references/universal-brainstorming.md +72 -0
- package/skills/ce-brainstorm/references/verdict-routing.md +26 -0
- package/skills/ce-brainstorm/references/visual-probes.md +164 -0
- package/skills/ce-brainstorm/scripts/elevation-dispatch.sh +288 -0
- package/skills/ce-brainstorm/scripts/light-webserver.js +1275 -0
- package/skills/ce-brainstorm/scripts/packs-resolve.py +737 -0
- package/skills/ce-brainstorm/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-code-review/SKILL.md +40 -0
- package/skills/ce-code-review/references/action-class-rubric.md +67 -0
- package/skills/ce-code-review/references/cross-model-recovery.md +38 -0
- package/skills/ce-code-review/references/cross-model-review.md +191 -0
- package/skills/ce-code-review/references/depth-paths.md +32 -0
- package/skills/ce-code-review/references/diff-scope.md +59 -0
- package/skills/ce-code-review/references/dispatch-reviewers.md +135 -0
- package/skills/ce-code-review/references/findings-schema.json +125 -0
- package/skills/ce-code-review/references/finish-input.md +118 -0
- package/skills/ce-code-review/references/finish-review.md +202 -0
- package/skills/ce-code-review/references/intent-and-plan.md +73 -0
- package/skills/ce-code-review/references/modes-and-output.md +156 -0
- package/skills/ce-code-review/references/persona-catalog.md +66 -0
- package/skills/ce-code-review/references/personas/adversarial-reviewer.md +111 -0
- package/skills/ce-code-review/references/personas/agent-native-reviewer.md +173 -0
- package/skills/ce-code-review/references/personas/api-contract-reviewer.md +44 -0
- package/skills/ce-code-review/references/personas/correctness-reviewer.md +48 -0
- package/skills/ce-code-review/references/personas/data-migration-reviewer.md +111 -0
- package/skills/ce-code-review/references/personas/deployment-verification-agent.md +157 -0
- package/skills/ce-code-review/references/personas/julik-frontend-races-reviewer.md +45 -0
- package/skills/ce-code-review/references/personas/learnings-researcher.md +260 -0
- package/skills/ce-code-review/references/personas/maintainability-reviewer.md +85 -0
- package/skills/ce-code-review/references/personas/performance-reviewer.md +45 -0
- package/skills/ce-code-review/references/personas/previous-comments-reviewer.md +59 -0
- package/skills/ce-code-review/references/personas/project-standards-reviewer.md +77 -0
- package/skills/ce-code-review/references/personas/reliability-reviewer.md +49 -0
- package/skills/ce-code-review/references/personas/security-reviewer.md +50 -0
- package/skills/ce-code-review/references/personas/swift-ios-reviewer.md +99 -0
- package/skills/ce-code-review/references/personas/testing-reviewer.md +51 -0
- package/skills/ce-code-review/references/review-output-template.md +172 -0
- package/skills/ce-code-review/references/scope.md +176 -0
- package/skills/ce-code-review/references/select-and-route.md +93 -0
- package/skills/ce-code-review/references/subagent-template.md +206 -0
- package/skills/ce-code-review/references/validator-batch-template.md +67 -0
- package/skills/ce-code-review/scripts/cross-model-adversarial-review.sh +1275 -0
- package/skills/ce-code-review/scripts/findings-mechanics.py +274 -0
- package/skills/ce-code-review/scripts/packs-resolve.py +737 -0
- package/skills/ce-code-review/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-code-review/scripts/review-scope.py +419 -0
- package/skills/ce-code-review/scripts/run-log.py +280 -0
- package/skills/ce-commit/SKILL.md +60 -0
- package/skills/ce-commit-push-pr/SKILL.md +69 -0
- package/skills/ce-commit-push-pr/references/apply-and-handoff.md +79 -0
- package/skills/ce-commit-push-pr/references/branch-creation.md +47 -0
- package/skills/ce-commit-push-pr/references/commit-and-push.md +26 -0
- package/skills/ce-commit-push-pr/references/compose.md +29 -0
- package/skills/ce-commit-push-pr/references/context.md +42 -0
- package/skills/ce-commit-push-pr/references/gh-stack-cli.md +83 -0
- package/skills/ce-commit-push-pr/references/pr-description-writing.md +208 -0
- package/skills/ce-commit-push-pr/references/stack-submit.md +81 -0
- package/skills/ce-compound/SKILL.md +79 -0
- package/skills/ce-compound/assets/resolution-template.md +96 -0
- package/skills/ce-compound/references/agents/best-practices-researcher.md +115 -0
- package/skills/ce-compound/references/agents/data-integrity-guardian.md +68 -0
- package/skills/ce-compound/references/agents/framework-docs-researcher.md +93 -0
- package/skills/ce-compound/references/agents/pattern-recognition-specialist.md +55 -0
- package/skills/ce-compound/references/agents/performance-oracle.md +108 -0
- package/skills/ce-compound/references/agents/security-sentinel.md +91 -0
- package/skills/ce-compound/references/agents/session-historian.md +82 -0
- package/skills/ce-compound/references/assembly.md +127 -0
- package/skills/ce-compound/references/concepts-vocabulary.md +98 -0
- package/skills/ce-compound/references/enhancement.md +42 -0
- package/skills/ce-compound/references/grounding-validation.md +89 -0
- package/skills/ce-compound/references/lightweight.md +56 -0
- package/skills/ce-compound/references/modes.md +13 -0
- package/skills/ce-compound/references/refresh-and-discoverability.md +96 -0
- package/skills/ce-compound/references/report.md +115 -0
- package/skills/ce-compound/references/research.md +144 -0
- package/skills/ce-compound/references/schema.yaml +236 -0
- package/skills/ce-compound/references/session-history.md +73 -0
- package/skills/ce-compound/references/yaml-schema.md +130 -0
- package/skills/ce-compound/scripts/packs-resolve.py +737 -0
- package/skills/ce-compound/scripts/session-history/__pycache__/dsh_session.cpython-314.pyc +0 -0
- package/skills/ce-compound/scripts/session-history/discover-sessions.sh +290 -0
- package/skills/ce-compound/scripts/session-history/discover_dsh.py +123 -0
- package/skills/ce-compound/scripts/session-history/dsh_session.py +225 -0
- package/skills/ce-compound/scripts/session-history/extract-errors.py +178 -0
- package/skills/ce-compound/scripts/session-history/extract-metadata.py +301 -0
- package/skills/ce-compound/scripts/session-history/extract-skeleton.py +346 -0
- package/skills/ce-compound/scripts/validate-doc-claims.py +497 -0
- package/skills/ce-compound/scripts/validate-frontmatter.py +137 -0
- package/skills/ce-compound-refresh/SKILL.md +88 -0
- package/skills/ce-compound-refresh/assets/resolution-template.md +96 -0
- package/skills/ce-compound-refresh/references/classify.md +42 -0
- package/skills/ce-compound-refresh/references/commit.md +7 -0
- package/skills/ce-compound-refresh/references/concepts-vocabulary.md +116 -0
- package/skills/ce-compound-refresh/references/discoverability.md +15 -0
- package/skills/ce-compound-refresh/references/investigate.md +23 -0
- package/skills/ce-compound-refresh/references/modes.md +27 -0
- package/skills/ce-compound-refresh/references/per-action-flows.md +142 -0
- package/skills/ce-compound-refresh/references/report.md +23 -0
- package/skills/ce-compound-refresh/references/schema.yaml +236 -0
- package/skills/ce-compound-refresh/references/scope.md +18 -0
- package/skills/ce-compound-refresh/references/worth-audit.md +35 -0
- package/skills/ce-compound-refresh/references/yaml-schema.md +130 -0
- package/skills/ce-compound-refresh/scripts/validate-doc-claims.py +497 -0
- package/skills/ce-compound-refresh/scripts/validate-frontmatter.py +137 -0
- package/skills/ce-debug/SKILL.md +122 -0
- package/skills/ce-debug/references/anti-patterns.md +91 -0
- package/skills/ce-debug/references/defense-in-depth.md +39 -0
- package/skills/ce-debug/references/fix.md +21 -0
- package/skills/ce-debug/references/investigate.md +114 -0
- package/skills/ce-debug/references/investigation-techniques.md +384 -0
- package/skills/ce-debug/references/pipeline-mode.md +78 -0
- package/skills/ce-debug/references/post-fix-handoff.md +60 -0
- package/skills/ce-debug/references/return-to-caller.md +57 -0
- package/skills/ce-doc-review/SKILL.md +75 -0
- package/skills/ce-doc-review/references/bulk-preview.md +146 -0
- package/skills/ce-doc-review/references/cross-model-review.md +207 -0
- package/skills/ce-doc-review/references/decision-primer.md +47 -0
- package/skills/ce-doc-review/references/dispatch.md +56 -0
- package/skills/ce-doc-review/references/document-intake.md +44 -0
- package/skills/ce-doc-review/references/findings-schema.json +85 -0
- package/skills/ce-doc-review/references/modes.md +20 -0
- package/skills/ce-doc-review/references/open-questions-defer.md +158 -0
- package/skills/ce-doc-review/references/persona-selection.md +41 -0
- package/skills/ce-doc-review/references/personas/adversarial-document-reviewer.md +109 -0
- package/skills/ce-doc-review/references/personas/coherence-reviewer.md +69 -0
- package/skills/ce-doc-review/references/personas/design-lens-reviewer.md +48 -0
- package/skills/ce-doc-review/references/personas/feasibility-reviewer.md +50 -0
- package/skills/ce-doc-review/references/personas/product-lens-reviewer.md +85 -0
- package/skills/ce-doc-review/references/personas/scope-guardian-reviewer.md +83 -0
- package/skills/ce-doc-review/references/personas/security-lens-reviewer.md +41 -0
- package/skills/ce-doc-review/references/personas/whole-doc-reviewer.md +25 -0
- package/skills/ce-doc-review/references/rendering-floor.md +175 -0
- package/skills/ce-doc-review/references/review-output-template.md +123 -0
- package/skills/ce-doc-review/references/subagent-template.md +188 -0
- package/skills/ce-doc-review/references/synthesis-and-presentation.md +350 -0
- package/skills/ce-doc-review/references/walkthrough.md +347 -0
- package/skills/ce-doc-review/scripts/cross-model-doc-review.sh +1270 -0
- package/skills/ce-doc-review/scripts/packs-resolve.py +737 -0
- package/skills/ce-doc-review/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-dogfood/SKILL.md +66 -0
- package/skills/ce-dogfood/agents/openai.yaml +2 -0
- package/skills/ce-dogfood/references/dogfood-report-template.md +97 -0
- package/skills/ce-dogfood/references/phases.md +167 -0
- package/skills/ce-dogfood/references/test-matrix-taxonomy.md +62 -0
- package/skills/ce-dogfood/scripts/packs-resolve.py +737 -0
- package/skills/ce-explain/SKILL.md +81 -0
- package/skills/ce-explain/references/agents/behavior-trace-scout.md +19 -0
- package/skills/ce-explain/references/agents/work-recap-scout.md +23 -0
- package/skills/ce-explain/references/check-in.md +18 -0
- package/skills/ce-explain/references/destinations.md +35 -0
- package/skills/ce-explain/references/explainer-html.md +29 -0
- package/skills/ce-explain/references/explainer-markdown.md +24 -0
- package/skills/ce-explain/references/intake.md +50 -0
- package/skills/ce-explain/references/orchestration.md +46 -0
- package/skills/ce-handoff/SKILL.md +54 -0
- package/skills/ce-handoff/references/create.md +88 -0
- package/skills/ce-handoff/references/resume.md +46 -0
- package/skills/ce-ideate/SKILL.md +75 -0
- package/skills/ce-ideate/references/agents/issue-intelligence-analyst.md +166 -0
- package/skills/ce-ideate/references/agents/learnings-researcher.md +247 -0
- package/skills/ce-ideate/references/agents/slack-researcher.md +127 -0
- package/skills/ce-ideate/references/agents/web-researcher.md +121 -0
- package/skills/ce-ideate/references/decomposition.md +39 -0
- package/skills/ce-ideate/references/divergent-ideation.md +112 -0
- package/skills/ce-ideate/references/grounding.md +138 -0
- package/skills/ce-ideate/references/html-rendering.md +638 -0
- package/skills/ce-ideate/references/ideation-sections.md +191 -0
- package/skills/ce-ideate/references/issue-intelligence.md +44 -0
- package/skills/ce-ideate/references/markdown-rendering.md +242 -0
- package/skills/ce-ideate/references/output-mode.md +34 -0
- package/skills/ce-ideate/references/post-ideation-workflow.md +169 -0
- package/skills/ce-ideate/references/scope-gates.md +148 -0
- package/skills/ce-ideate/references/universal-ideation.md +119 -0
- package/skills/ce-ideate/references/user-research-artifacts.md +28 -0
- package/skills/ce-ideate/references/web-research-cache.md +56 -0
- package/skills/ce-noslop/SKILL.md +45 -0
- package/skills/ce-noslop/references/patterns.md +61 -0
- package/skills/ce-noslop/references/terminology.md +3 -0
- package/skills/ce-optimize/SKILL.md +64 -0
- package/skills/ce-optimize/references/agents/learnings-researcher.md +247 -0
- package/skills/ce-optimize/references/agents/repo-research-analyst.md +258 -0
- package/skills/ce-optimize/references/example-expensive-benchmark-spec.yaml +88 -0
- package/skills/ce-optimize/references/example-hard-spec.yaml +64 -0
- package/skills/ce-optimize/references/example-judge-spec.yaml +83 -0
- package/skills/ce-optimize/references/example-text-target-spec.yaml +80 -0
- package/skills/ce-optimize/references/experiment-log-schema.yaml +563 -0
- package/skills/ce-optimize/references/experiment-prompt-template.md +143 -0
- package/skills/ce-optimize/references/judge-prompt-template.md +117 -0
- package/skills/ce-optimize/references/loop.md +286 -0
- package/skills/ce-optimize/references/measurement.md +168 -0
- package/skills/ce-optimize/references/optimize-spec-schema.yaml +570 -0
- package/skills/ce-optimize/references/persistence.md +93 -0
- package/skills/ce-optimize/references/spec.md +107 -0
- package/skills/ce-optimize/references/text-targets.md +51 -0
- package/skills/ce-optimize/references/usage-guide.md +147 -0
- package/skills/ce-optimize/references/wrap-up.md +54 -0
- package/skills/ce-optimize/scripts/decide.mjs +676 -0
- package/skills/ce-optimize/scripts/experiment-worktree.sh +330 -0
- package/skills/ce-optimize/scripts/measure.sh +140 -0
- package/skills/ce-optimize/scripts/parallel-probe.sh +134 -0
- package/skills/ce-plan/SKILL.md +60 -0
- package/skills/ce-plan/references/agents/agent-native-planning-strategist.md +66 -0
- package/skills/ce-plan/references/agents/architecture-strategist.md +46 -0
- package/skills/ce-plan/references/agents/best-practices-researcher.md +118 -0
- package/skills/ce-plan/references/agents/data-integrity-guardian.md +68 -0
- package/skills/ce-plan/references/agents/framework-docs-researcher.md +97 -0
- package/skills/ce-plan/references/agents/git-history-analyzer.md +40 -0
- package/skills/ce-plan/references/agents/learnings-researcher.md +268 -0
- package/skills/ce-plan/references/agents/pattern-recognition-specialist.md +55 -0
- package/skills/ce-plan/references/agents/performance-oracle.md +108 -0
- package/skills/ce-plan/references/agents/repo-research-analyst.md +262 -0
- package/skills/ce-plan/references/agents/security-sentinel.md +91 -0
- package/skills/ce-plan/references/agents/slack-researcher.md +131 -0
- package/skills/ce-plan/references/agents/spec-flow-analyzer.md +84 -0
- package/skills/ce-plan/references/agents/web-researcher.md +125 -0
- package/skills/ce-plan/references/approach-altitude.md +55 -0
- package/skills/ce-plan/references/bakeoff.md +9 -0
- package/skills/ce-plan/references/deepening-workflow.md +263 -0
- package/skills/ce-plan/references/final-review.md +134 -0
- package/skills/ce-plan/references/html-rendering.md +638 -0
- package/skills/ce-plan/references/intake.md +158 -0
- package/skills/ce-plan/references/markdown-rendering.md +242 -0
- package/skills/ce-plan/references/output-contracts.md +29 -0
- package/skills/ce-plan/references/output-mode.md +45 -0
- package/skills/ce-plan/references/plan-handoff.md +116 -0
- package/skills/ce-plan/references/plan-sections.md +489 -0
- package/skills/ce-plan/references/reasoning-elevation.md +124 -0
- package/skills/ce-plan/references/research.md +211 -0
- package/skills/ce-plan/references/resume.md +61 -0
- package/skills/ce-plan/references/settled-decisions.md +45 -0
- package/skills/ce-plan/references/structure.md +207 -0
- package/skills/ce-plan/references/synthesis-summary.md +260 -0
- package/skills/ce-plan/references/universal-planning.md +170 -0
- package/skills/ce-plan/scripts/elevation-dispatch.sh +288 -0
- package/skills/ce-plan/scripts/packs-resolve.py +737 -0
- package/skills/ce-plan/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-polish/SKILL.md +22 -0
- package/skills/ce-polish/agents/openai.yaml +2 -0
- package/skills/ce-polish/references/dev-server-astro.md +58 -0
- package/skills/ce-polish/references/dev-server-detection.md +40 -0
- package/skills/ce-polish/references/dev-server-next.md +61 -0
- package/skills/ce-polish/references/dev-server-nuxt.md +58 -0
- package/skills/ce-polish/references/dev-server-procfile.md +59 -0
- package/skills/ce-polish/references/dev-server-rails.md +50 -0
- package/skills/ce-polish/references/dev-server-remix.md +58 -0
- package/skills/ce-polish/references/dev-server-sveltekit.md +58 -0
- package/skills/ce-polish/references/dev-server-vite.md +48 -0
- package/skills/ce-polish/references/install-riffrec.md +35 -0
- package/skills/ce-polish/references/launch-json-schema.md +177 -0
- package/skills/ce-polish/references/live-loop.md +98 -0
- package/skills/ce-polish/references/live-remote.md +35 -0
- package/skills/ce-polish/references/live-start.md +93 -0
- package/skills/ce-polish/references/live-stream-contract.md +253 -0
- package/skills/ce-polish/references/run.md +67 -0
- package/skills/ce-polish/scripts/detect-project-type.sh +277 -0
- package/skills/ce-polish/scripts/detect-riffrec.sh +225 -0
- package/skills/ce-polish/scripts/live-endpoint.js +2841 -0
- package/skills/ce-polish/scripts/read-launch-json.sh +117 -0
- package/skills/ce-polish/scripts/resolve-package-manager.sh +95 -0
- package/skills/ce-polish/scripts/resolve-port.sh +307 -0
- package/skills/ce-pov/SKILL.md +64 -0
- package/skills/ce-pov/references/agents/external-evidence-researcher.md +26 -0
- package/skills/ce-pov/references/agents/pov-peer.md +43 -0
- package/skills/ce-pov/references/agents/precedent-activity-scout.md +21 -0
- package/skills/ce-pov/references/agents/project-grounding-scout.md +32 -0
- package/skills/ce-pov/references/boundaries.md +36 -0
- package/skills/ce-pov/references/cross-model-panel.md +596 -0
- package/skills/ce-pov/references/followup.md +9 -0
- package/skills/ce-pov/references/grounding.md +47 -0
- package/skills/ce-pov/references/intake.md +61 -0
- package/skills/ce-pov/references/invocation.md +47 -0
- package/skills/ce-pov/references/method.md +68 -0
- package/skills/ce-pov/references/pov-schema.json +56 -0
- package/skills/ce-pov/references/report.md +7 -0
- package/skills/ce-pov/scripts/cross-model-pov.sh +1110 -0
- package/skills/ce-pov/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-product-pulse/SKILL.md +81 -0
- package/skills/ce-product-pulse/agents/openai.yaml +2 -0
- package/skills/ce-product-pulse/references/config.md +20 -0
- package/skills/ce-product-pulse/references/interview.md +260 -0
- package/skills/ce-product-pulse/references/report-template.md +89 -0
- package/skills/ce-product-pulse/references/run.md +46 -0
- package/skills/ce-product-pulse/references/setup.md +43 -0
- package/skills/ce-promote/SKILL.md +90 -0
- package/skills/ce-promote/agents/openai.yaml +2 -0
- package/skills/ce-promote/references/spiral-cli.md +149 -0
- package/skills/ce-proof/SKILL.md +65 -0
- package/skills/ce-proof/references/api.md +176 -0
- package/skills/ce-proof/references/workflows.md +124 -0
- package/skills/ce-prototype/SKILL.md +59 -0
- package/skills/ce-prototype/assets/annotate.css +206 -0
- package/skills/ce-prototype/assets/annotate.js +837 -0
- package/skills/ce-prototype/references/annotation-loop.md +32 -0
- package/skills/ce-prototype/references/build.md +39 -0
- package/skills/ce-prototype/references/craft-floor.md +43 -0
- package/skills/ce-prototype/references/preview.md +118 -0
- package/skills/ce-prototype/references/scoping.md +38 -0
- package/skills/ce-prototype/references/write-back.md +37 -0
- package/skills/ce-prototype/scripts/light-webserver.js +1275 -0
- package/skills/ce-resolve-pr-feedback/SKILL.md +60 -0
- package/skills/ce-resolve-pr-feedback/references/agents/pr-comment-resolver.md +59 -0
- package/skills/ce-resolve-pr-feedback/references/evaluation-rubric.md +141 -0
- package/skills/ce-resolve-pr-feedback/references/full-mode.md +336 -0
- package/skills/ce-resolve-pr-feedback/references/pipeline-mode.md +23 -0
- package/skills/ce-resolve-pr-feedback/references/resume.md +60 -0
- package/skills/ce-resolve-pr-feedback/references/return-to-caller.md +111 -0
- package/skills/ce-resolve-pr-feedback/references/targeted-mode.md +48 -0
- package/skills/ce-resolve-pr-feedback/scripts/get-pr-comments +176 -0
- package/skills/ce-resolve-pr-feedback/scripts/get-thread-for-comment +84 -0
- package/skills/ce-resolve-pr-feedback/scripts/pending-feedback.py +287 -0
- package/skills/ce-resolve-pr-feedback/scripts/reply-to-pr-thread +57 -0
- package/skills/ce-resolve-pr-feedback/scripts/resolve-pr-thread +23 -0
- package/skills/ce-retune/SKILL.md +42 -0
- package/skills/ce-retune/agents/openai.yaml +2 -0
- package/skills/ce-retune/references/baseline-mining.md +122 -0
- package/skills/ce-retune/references/corpus-audit.md +128 -0
- package/skills/ce-retune/references/cut-passes.md +129 -0
- package/skills/ce-retune/references/halt-taxonomy.md +123 -0
- package/skills/ce-retune/references/noise-floor.md +131 -0
- package/skills/ce-retune/references/workflow-shapes.md +73 -0
- package/skills/ce-riffrec-feedback-analysis/SKILL.md +33 -0
- package/skills/ce-riffrec-feedback-analysis/references/analyzer.md +18 -0
- package/skills/ce-riffrec-feedback-analysis/references/compound-engineering-feedback-format.md +116 -0
- package/skills/ce-riffrec-feedback-analysis/references/extensive-analysis.md +98 -0
- package/skills/ce-riffrec-feedback-analysis/references/install-riffrec.md +27 -0
- package/skills/ce-riffrec-feedback-analysis/references/quick-bug-report.md +38 -0
- package/skills/ce-riffrec-feedback-analysis/scripts/analyze_riffrec_zip.py +1306 -0
- package/skills/ce-setup/SKILL.md +103 -0
- package/skills/ce-setup/agents/openai.yaml +2 -0
- package/skills/ce-setup/assets/compounding-directive.md +11 -0
- package/skills/ce-setup/assets/noslop-directive.md +7 -0
- package/skills/ce-setup/assets/pack-rule-template.md +11 -0
- package/skills/ce-setup/references/acpx-cache-warm.md +14 -0
- package/skills/ce-setup/references/config-template.yaml +210 -0
- package/skills/ce-setup/references/legacy-codex-tool-map.md +15 -0
- package/skills/ce-setup/references/pack-scaffold.md +45 -0
- package/skills/ce-setup/references/repo-fixes.md +121 -0
- package/skills/ce-setup/scripts/check-health +929 -0
- package/skills/ce-setup/scripts/packs-resolve.py +737 -0
- package/skills/ce-simplify-code/SKILL.md +76 -0
- package/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
- package/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
- package/skills/ce-simplify-code/references/personas/comment-reviewer.md +24 -0
- package/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
- package/skills/ce-simplify-code/references/personas/test-value-reviewer.md +23 -0
- package/skills/ce-simplify-code/references/reviewer-dispatch.md +11 -0
- package/skills/ce-simplify-code/references/standalone-report.md +25 -0
- package/skills/ce-strategy/SKILL.md +61 -0
- package/skills/ce-strategy/references/grounding.md +22 -0
- package/skills/ce-strategy/references/interview.md +179 -0
- package/skills/ce-strategy/references/strategy-template.md +94 -0
- package/skills/ce-strategy/references/update-run.md +33 -0
- package/skills/ce-sweep/SKILL.md +93 -0
- package/skills/ce-sweep/agents/openai.yaml +2 -0
- package/skills/ce-sweep/references/agents/media-analyzer.md +54 -0
- package/skills/ce-sweep/references/interview.md +208 -0
- package/skills/ce-sweep/references/model-tiers.md +13 -0
- package/skills/ce-sweep/references/plan-template.md +62 -0
- package/skills/ce-sweep/references/run.md +126 -0
- package/skills/ce-sweep/references/sources/email.md +57 -0
- package/skills/ce-sweep/references/sources/github-issues.md +59 -0
- package/skills/ce-sweep/references/sources/slack.md +61 -0
- package/skills/ce-sweep/references/state-schema.md +157 -0
- package/skills/ce-sweep/references/subagent-template.md +60 -0
- package/skills/ce-sweep/scripts/analyze_riffrec_zip.py +1306 -0
- package/skills/ce-sweep/scripts/sweep-state.py +793 -0
- package/skills/ce-test-browser/SKILL.md +50 -0
- package/skills/ce-test-browser/references/agent-browser-driver.md +47 -0
- package/skills/ce-test-browser/references/pipeline-orchestration.md +45 -0
- package/skills/ce-test-browser/references/route-and-report.md +129 -0
- package/skills/ce-test-browser/scripts/resolve-port.sh +41 -0
- package/skills/ce-test-xcode/SKILL.md +26 -0
- package/skills/ce-test-xcode/agents/openai.yaml +2 -0
- package/skills/ce-test-xcode/references/setup-and-build.md +32 -0
- package/skills/ce-test-xcode/references/test-and-report.md +64 -0
- package/skills/ce-work/SKILL.md +62 -0
- package/skills/ce-work/references/agents/figma-design-sync.md +27 -0
- package/skills/ce-work/references/agents/implementation-worker.md +17 -0
- package/skills/ce-work/references/cross-model-execution.md +191 -0
- package/skills/ce-work/references/execution-engines.md +64 -0
- package/skills/ce-work/references/execution-strategy.md +73 -0
- package/skills/ce-work/references/implementation-loop.md +107 -0
- package/skills/ce-work/references/implementation-result-schema.json +52 -0
- package/skills/ce-work/references/input-triage.md +50 -0
- package/skills/ce-work/references/non-code-execution.md +23 -0
- package/skills/ce-work/references/return-to-caller.md +33 -0
- package/skills/ce-work/references/review-findings-followup.md +82 -0
- package/skills/ce-work/references/shipping-workflow.md +89 -0
- package/skills/ce-work/references/tracker-defer.md +150 -0
- package/skills/ce-work/references/work-intake.md +51 -0
- package/skills/ce-work/references/workspace-setup.md +27 -0
- package/skills/ce-work/scripts/__pycache__/unit_workspace_jobs.cpython-314.pyc +0 -0
- package/skills/ce-work/scripts/__pycache__/unit_workspace_state.cpython-314.pyc +0 -0
- package/skills/ce-work/scripts/cross-model-work.sh +1385 -0
- package/skills/ce-work/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-work/scripts/unit-workspace.py +215 -0
- package/skills/ce-work/scripts/unit_workspace_ignored.py +93 -0
- package/skills/ce-work/scripts/unit_workspace_integration.py +677 -0
- package/skills/ce-work/scripts/unit_workspace_jobs.py +1216 -0
- package/skills/ce-work/scripts/unit_workspace_lifecycle.py +981 -0
- package/skills/ce-work/scripts/unit_workspace_state.py +1379 -0
- package/skills/ce-work/scripts/unit_workspace_transaction.py +612 -0
- package/skills/ce-worktree/SKILL.md +54 -0
- package/skills/lfg/SKILL.md +59 -0
- package/skills/lfg/references/debug-return.md +25 -0
- package/skills/lfg/references/intake.md +30 -0
- package/skills/lfg/references/next-work-handoff.md +96 -0
- package/skills/lfg/references/plan-brief.md +36 -0
- package/skills/lfg/references/review-followup.md +78 -0
- package/skills/lfg/references/shipping.md +65 -0
- package/skills/lfg/references/stage-routing.md +49 -0
- package/skills/lfg/references/task-visibility.md +11 -0
- package/skills/lfg/references/tracker-defer.md +150 -0
- package/skills/lfg/references/work-return.md +27 -0
- package/skills/wtf/SKILL.md +33 -0
- package/skills/wtf/agents/openai.yaml +2 -0
|
@@ -0,0 +1,1379 @@
|
|
|
1
|
+
"""Private, crash-recoverable workspace controller for ce-work external units.
|
|
2
|
+
|
|
3
|
+
The generic peer-job runner owns process supervision. This controller owns the
|
|
4
|
+
repository-specific transaction: one private run manifest, detached sibling
|
|
5
|
+
worktrees, complete-tree transport commits, canonical integration evidence,
|
|
6
|
+
exact restoration, retention, and explicit cleanup. It never launches a model
|
|
7
|
+
CLI and never commits a worker's output in the canonical checkout.
|
|
8
|
+
|
|
9
|
+
Every successful command prints a status word and one compact JSON document.
|
|
10
|
+
Trust failures print only ``UNREADABLE`` and an error on stderr.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import base64
|
|
17
|
+
import contextlib
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import secrets
|
|
23
|
+
import shutil
|
|
24
|
+
import stat
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
33
|
+
|
|
34
|
+
# --- platform privacy primitives ---------------------------------------------
|
|
35
|
+
# POSIX expresses "this private directory is mine and nobody else can read it"
|
|
36
|
+
# with two things this module relies on throughout: `st_uid == geteuid()` and
|
|
37
|
+
# the 0700/0600 mode bits. Windows has neither. It has no `fcntl` at all, and
|
|
38
|
+
# `os.stat().st_mode` there is a synthesized value that reflects only the
|
|
39
|
+
# read-only attribute -- a directory created with `os.mkdir(p, 0o700)` reports
|
|
40
|
+
# 0o777 and a file `os.chmod(p, 0o600)` reports 0o666, so every `_mode(st) ==
|
|
41
|
+
# 0o700` gate below would fail closed on a perfectly private directory.
|
|
42
|
+
#
|
|
43
|
+
# So on Windows the equivalent identity is the owner SID of the object as read
|
|
44
|
+
# from its security descriptor, and the equivalent of the mode bits is a DACL
|
|
45
|
+
# hardened to the current user plus the root-equivalents (SYSTEM,
|
|
46
|
+
# Administrators) with inheritance broken -- exactly the substitution
|
|
47
|
+
# peer-job-runner.py already makes. Pure stdlib via ctypes; no pywin32.
|
|
48
|
+
if IS_WINDOWS:
|
|
49
|
+
import ctypes
|
|
50
|
+
import msvcrt
|
|
51
|
+
from ctypes import wintypes
|
|
52
|
+
|
|
53
|
+
_advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
54
|
+
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
55
|
+
|
|
56
|
+
_SE_FILE_OBJECT = 1
|
|
57
|
+
_OWNER_SECURITY_INFORMATION = 0x00000001
|
|
58
|
+
_TOKEN_QUERY = 0x0008
|
|
59
|
+
_TOKEN_USER_CLASS = 1
|
|
60
|
+
_TOKEN_OWNER_CLASS = 4
|
|
61
|
+
|
|
62
|
+
_advapi32.ConvertSidToStringSidW.argtypes = [
|
|
63
|
+
ctypes.c_void_p, ctypes.POINTER(ctypes.c_wchar_p)]
|
|
64
|
+
_advapi32.ConvertSidToStringSidW.restype = wintypes.BOOL
|
|
65
|
+
_advapi32.OpenProcessToken.argtypes = [
|
|
66
|
+
wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)]
|
|
67
|
+
_advapi32.OpenProcessToken.restype = wintypes.BOOL
|
|
68
|
+
_advapi32.GetTokenInformation.argtypes = [
|
|
69
|
+
wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD,
|
|
70
|
+
ctypes.POINTER(wintypes.DWORD)]
|
|
71
|
+
_advapi32.GetTokenInformation.restype = wintypes.BOOL
|
|
72
|
+
_advapi32.GetSecurityInfo.argtypes = [
|
|
73
|
+
wintypes.HANDLE, ctypes.c_int, wintypes.DWORD,
|
|
74
|
+
ctypes.POINTER(ctypes.c_void_p), ctypes.c_void_p, ctypes.c_void_p,
|
|
75
|
+
ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]
|
|
76
|
+
_advapi32.GetSecurityInfo.restype = wintypes.DWORD
|
|
77
|
+
_advapi32.GetNamedSecurityInfoW.argtypes = [
|
|
78
|
+
wintypes.LPCWSTR, ctypes.c_int, wintypes.DWORD,
|
|
79
|
+
ctypes.POINTER(ctypes.c_void_p), ctypes.c_void_p, ctypes.c_void_p,
|
|
80
|
+
ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]
|
|
81
|
+
_advapi32.GetNamedSecurityInfoW.restype = wintypes.DWORD
|
|
82
|
+
_kernel32.GetCurrentProcess.restype = wintypes.HANDLE
|
|
83
|
+
_kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
84
|
+
_kernel32.CloseHandle.restype = wintypes.BOOL
|
|
85
|
+
_kernel32.LocalFree.argtypes = [wintypes.HGLOBAL]
|
|
86
|
+
_kernel32.LocalFree.restype = wintypes.HGLOBAL
|
|
87
|
+
|
|
88
|
+
_WIN_IDENTITY_SIDS = None
|
|
89
|
+
|
|
90
|
+
def _win_sid_to_string(psid) -> str:
|
|
91
|
+
strp = ctypes.c_wchar_p()
|
|
92
|
+
if not _advapi32.ConvertSidToStringSidW(psid, ctypes.byref(strp)):
|
|
93
|
+
raise OSError(f"ConvertSidToStringSid failed: {ctypes.get_last_error()}")
|
|
94
|
+
try:
|
|
95
|
+
return strp.value
|
|
96
|
+
finally:
|
|
97
|
+
_kernel32.LocalFree(ctypes.cast(strp, wintypes.HGLOBAL))
|
|
98
|
+
|
|
99
|
+
def _win_token_sid(token, info_class) -> str:
|
|
100
|
+
size = wintypes.DWORD(0)
|
|
101
|
+
_advapi32.GetTokenInformation(token, info_class, None, 0, ctypes.byref(size))
|
|
102
|
+
buf = (ctypes.c_byte * size.value)()
|
|
103
|
+
if not _advapi32.GetTokenInformation(
|
|
104
|
+
token, info_class, buf, size, ctypes.byref(size)
|
|
105
|
+
):
|
|
106
|
+
raise OSError(f"GetTokenInformation failed: {ctypes.get_last_error()}")
|
|
107
|
+
# TOKEN_USER / TOKEN_OWNER both begin with a PSID at offset 0.
|
|
108
|
+
sid_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_void_p))[0]
|
|
109
|
+
return _win_sid_to_string(ctypes.c_void_p(sid_ptr))
|
|
110
|
+
|
|
111
|
+
def _win_identity_sids() -> frozenset:
|
|
112
|
+
"""The SID strings this process's token creates objects as: the user SID
|
|
113
|
+
and the default-owner SID (they differ for an elevated process). Cached;
|
|
114
|
+
an object owned by any of these is treated as ours."""
|
|
115
|
+
global _WIN_IDENTITY_SIDS
|
|
116
|
+
if _WIN_IDENTITY_SIDS is not None:
|
|
117
|
+
return _WIN_IDENTITY_SIDS
|
|
118
|
+
token = wintypes.HANDLE()
|
|
119
|
+
if not _advapi32.OpenProcessToken(
|
|
120
|
+
_kernel32.GetCurrentProcess(), _TOKEN_QUERY, ctypes.byref(token)
|
|
121
|
+
):
|
|
122
|
+
raise OSError(f"OpenProcessToken failed: {ctypes.get_last_error()}")
|
|
123
|
+
try:
|
|
124
|
+
sids = {
|
|
125
|
+
_win_token_sid(token, _TOKEN_USER_CLASS),
|
|
126
|
+
_win_token_sid(token, _TOKEN_OWNER_CLASS),
|
|
127
|
+
}
|
|
128
|
+
finally:
|
|
129
|
+
_kernel32.CloseHandle(token)
|
|
130
|
+
_WIN_IDENTITY_SIDS = frozenset(s for s in sids if s)
|
|
131
|
+
return _WIN_IDENTITY_SIDS
|
|
132
|
+
|
|
133
|
+
def _win_owner_sid_handle(handle: int) -> str:
|
|
134
|
+
"""Owner SID of an OPENED handle. The SID points inside the returned
|
|
135
|
+
security descriptor, so freeing the descriptor is the only (and
|
|
136
|
+
required) cleanup -- never free the SID separately."""
|
|
137
|
+
psid = ctypes.c_void_p()
|
|
138
|
+
psd = ctypes.c_void_p()
|
|
139
|
+
err = _advapi32.GetSecurityInfo(
|
|
140
|
+
wintypes.HANDLE(handle), _SE_FILE_OBJECT, _OWNER_SECURITY_INFORMATION,
|
|
141
|
+
ctypes.byref(psid), None, None, None, ctypes.byref(psd))
|
|
142
|
+
if err != 0:
|
|
143
|
+
raise OSError(f"GetSecurityInfo failed: {err}")
|
|
144
|
+
try:
|
|
145
|
+
return _win_sid_to_string(psid)
|
|
146
|
+
finally:
|
|
147
|
+
_kernel32.LocalFree(ctypes.cast(psd, wintypes.HGLOBAL))
|
|
148
|
+
|
|
149
|
+
def win_owns_handle(handle: int) -> bool:
|
|
150
|
+
try:
|
|
151
|
+
return _win_owner_sid_handle(handle) in _win_identity_sids()
|
|
152
|
+
except OSError:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def win_owns_path(path: str) -> bool:
|
|
156
|
+
"""Owner check by PATH. Required for directories: Windows cannot open a
|
|
157
|
+
directory with os.open(), so a handle-based check cannot reach one."""
|
|
158
|
+
psid = ctypes.c_void_p()
|
|
159
|
+
psd = ctypes.c_void_p()
|
|
160
|
+
err = _advapi32.GetNamedSecurityInfoW(
|
|
161
|
+
path, _SE_FILE_OBJECT, _OWNER_SECURITY_INFORMATION,
|
|
162
|
+
ctypes.byref(psid), None, None, None, ctypes.byref(psd))
|
|
163
|
+
if err != 0:
|
|
164
|
+
return False
|
|
165
|
+
try:
|
|
166
|
+
return _win_sid_to_string(psid) in _win_identity_sids()
|
|
167
|
+
except OSError:
|
|
168
|
+
return False
|
|
169
|
+
finally:
|
|
170
|
+
_kernel32.LocalFree(ctypes.cast(psd, wintypes.HGLOBAL))
|
|
171
|
+
|
|
172
|
+
def win_harden_acl(path: str) -> bool:
|
|
173
|
+
"""Break inheritance and grant only the current user plus the
|
|
174
|
+
root-equivalents, so no world/Users grant survives -- the 0700 analog.
|
|
175
|
+
Best-effort: the owner check stays the hard gate if icacls is absent."""
|
|
176
|
+
sids = _win_identity_sids()
|
|
177
|
+
if not sids:
|
|
178
|
+
return False
|
|
179
|
+
system_root = os.environ.get("SystemRoot") or r"C:\Windows"
|
|
180
|
+
icacls = os.path.join(system_root, "System32", "icacls.exe")
|
|
181
|
+
if not os.path.isfile(icacls):
|
|
182
|
+
return False
|
|
183
|
+
inherit = "(OI)(CI)" if os.path.isdir(path) else ""
|
|
184
|
+
# Grant EVERY identity SID, not an arbitrary one from the set: an
|
|
185
|
+
# elevated token carries two (user + default owner), and picking one
|
|
186
|
+
# nondeterministically could grant the wrong principal.
|
|
187
|
+
grants = []
|
|
188
|
+
for sid in sorted(sids) + ["S-1-5-18", "S-1-5-32-544"]:
|
|
189
|
+
grants += ["/grant:r", f"*{sid}:{inherit}F"]
|
|
190
|
+
try:
|
|
191
|
+
return subprocess.run(
|
|
192
|
+
[icacls, path, "/inheritance:r"] + grants,
|
|
193
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
194
|
+
check=False,
|
|
195
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000),
|
|
196
|
+
).returncode == 0
|
|
197
|
+
except OSError:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def _lock_fd(fd: int, exclusive: bool) -> None:
|
|
201
|
+
"""Advisory whole-file lock with flock's blocking semantics.
|
|
202
|
+
|
|
203
|
+
msvcrt has no blocking shared lock and no shared mode at all, so a
|
|
204
|
+
contended acquire raises EACCES instead of waiting. flock(fd, LOCK_EX)
|
|
205
|
+
on POSIX blocks, and every caller here relies on that -- a status read
|
|
206
|
+
that raced a writer must queue, not crash. So retry on contention until
|
|
207
|
+
the lock lands, with a bounded wait so a wedged holder cannot hang the
|
|
208
|
+
command forever.
|
|
209
|
+
"""
|
|
210
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
211
|
+
deadline = time.monotonic() + LOCK_WAIT_SECS
|
|
212
|
+
delay = 0.01
|
|
213
|
+
while True:
|
|
214
|
+
try:
|
|
215
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
216
|
+
return
|
|
217
|
+
except OSError:
|
|
218
|
+
if time.monotonic() >= deadline:
|
|
219
|
+
raise TrustFailure(
|
|
220
|
+
f"timed out after {LOCK_WAIT_SECS}s waiting for the run lock"
|
|
221
|
+
)
|
|
222
|
+
time.sleep(delay)
|
|
223
|
+
delay = min(delay * 2, 0.25)
|
|
224
|
+
|
|
225
|
+
def _unlock_fd(fd: int) -> None:
|
|
226
|
+
with contextlib.suppress(OSError):
|
|
227
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
228
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
|
|
229
|
+
|
|
230
|
+
def _modeis_private(st: os.stat_result, want_dir: bool) -> bool:
|
|
231
|
+
"""Windows has no mode privacy to verify; the ACL and owner check carry
|
|
232
|
+
it. Always true so the caller's POSIX gate never fails closed here."""
|
|
233
|
+
return True
|
|
234
|
+
else:
|
|
235
|
+
import fcntl
|
|
236
|
+
|
|
237
|
+
def _lock_fd(fd: int, exclusive: bool) -> None:
|
|
238
|
+
fcntl.flock(fd, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
|
|
239
|
+
|
|
240
|
+
def _unlock_fd(fd: int) -> None:
|
|
241
|
+
with contextlib.suppress(OSError):
|
|
242
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
243
|
+
|
|
244
|
+
def _modeis_private(st: os.stat_result, want_dir: bool) -> bool:
|
|
245
|
+
expected = 0o700 if want_dir else 0o600
|
|
246
|
+
return stat.S_IMODE(st.st_mode) == expected
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
SCHEMA_VERSION = 1
|
|
250
|
+
PLAN_CHECKPOINT_MESSAGE = "docs(ce-work): checkpoint selected implementation plan"
|
|
251
|
+
_uid_getter = getattr(os, "geteuid", None) or getattr(os, "getuid", None)
|
|
252
|
+
_EFFECTIVE_UID = _uid_getter() if _uid_getter is not None else None
|
|
253
|
+
if IS_WINDOWS:
|
|
254
|
+
# No geteuid on Windows, so the per-user scratch root is derived from the
|
|
255
|
+
# user profile instead of a uid -- the same substitution peer-job-runner
|
|
256
|
+
# makes. LOCALAPPDATA is per-user and never world-readable, which is what
|
|
257
|
+
# the `/tmp/compound-engineering-<uid>` 0700 root provides on POSIX.
|
|
258
|
+
OWNER_SCRATCH_ROOT = os.path.join(
|
|
259
|
+
os.environ.get("LOCALAPPDATA") or tempfile.gettempdir(),
|
|
260
|
+
"compound-engineering",
|
|
261
|
+
)
|
|
262
|
+
elif _EFFECTIVE_UID is not None:
|
|
263
|
+
OWNER_SCRATCH_ROOT = os.path.join("/tmp", f"compound-engineering-{_EFFECTIVE_UID}")
|
|
264
|
+
else:
|
|
265
|
+
OWNER_SCRATCH_ROOT = None
|
|
266
|
+
MAX_JSON_BYTES = 2 * 1024 * 1024
|
|
267
|
+
MAX_PACKET_BYTES = 200_000
|
|
268
|
+
SAFE_ID = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
|
269
|
+
O_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
|
|
270
|
+
O_DIRECTORY = getattr(os, "O_DIRECTORY", 0)
|
|
271
|
+
# Windows CPython opens os.open() descriptors in CRT *text* mode by default:
|
|
272
|
+
# writes expand \n -> \r\n and reads stop at the first 0x1A (Ctrl-Z EOF), which
|
|
273
|
+
# would silently corrupt the manifest. O_BINARY is 0 on POSIX, so this is a
|
|
274
|
+
# no-op there and every os.open below stays byte-exact on both platforms.
|
|
275
|
+
O_BINARY = getattr(os, "O_BINARY", 0)
|
|
276
|
+
# Upper bound on waiting for a contended run lock. A legitimate holder is a
|
|
277
|
+
# sub-second manifest read/write; anything longer means a wedged process, and
|
|
278
|
+
# hanging forever is worse than a clear failure.
|
|
279
|
+
LOCK_WAIT_SECS = 30.0
|
|
280
|
+
TERMINAL_PROCESS = {"done", "failed", "timeout", "died-without-result"}
|
|
281
|
+
INTEGRATABLE_STATES = {"integration-pending", "integrated", "verified"}
|
|
282
|
+
UNIT_STATES = {
|
|
283
|
+
"queued", "authoring", "authored", "integration-pending", "integrated",
|
|
284
|
+
"restoring", "verified", "committed", "preserved", "cleaned", "native-completed",
|
|
285
|
+
}
|
|
286
|
+
GIT_LOCAL_ENV_VARS = frozenset({
|
|
287
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
288
|
+
"GIT_COMMON_DIR",
|
|
289
|
+
"GIT_CONFIG",
|
|
290
|
+
"GIT_CONFIG_COUNT",
|
|
291
|
+
"GIT_CONFIG_PARAMETERS",
|
|
292
|
+
"GIT_DIR",
|
|
293
|
+
"GIT_GRAFT_FILE",
|
|
294
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
295
|
+
"GIT_INDEX_FILE",
|
|
296
|
+
# Listed by `git rev-parse --local-env-vars` on git < 2.41 (e.g. Debian
|
|
297
|
+
# bookworm's 2.39); dropped from the list by newer git. Scrub it so an
|
|
298
|
+
# inherited value cannot leak into unit verification on old git.
|
|
299
|
+
"GIT_INTERNAL_SUPER_PREFIX",
|
|
300
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
301
|
+
"GIT_OBJECT_DIRECTORY",
|
|
302
|
+
"GIT_PREFIX",
|
|
303
|
+
"GIT_REPLACE_REF_BASE",
|
|
304
|
+
"GIT_SHALLOW_FILE",
|
|
305
|
+
"GIT_WORK_TREE",
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class Operational(Exception):
|
|
310
|
+
def __init__(self, word: str, message: str, detail: dict | None = None):
|
|
311
|
+
super().__init__(message)
|
|
312
|
+
self.word = word
|
|
313
|
+
self.detail = detail or {}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TrustFailure(Operational):
|
|
317
|
+
def __init__(self, message: str):
|
|
318
|
+
super().__init__("UNREADABLE", message)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def now_iso() -> str:
|
|
322
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_fault(point: str) -> None:
|
|
326
|
+
"""Deterministic crash-window injection for the repository test suite."""
|
|
327
|
+
configured = {value.strip() for value in os.environ.get("CE_WORK_TEST_FAULT", "").split(",") if value.strip()}
|
|
328
|
+
if point in configured:
|
|
329
|
+
raise Operational("INTERRUPTED", f"injected test interruption at {point}")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _private_root_usable(path: str) -> bool:
|
|
333
|
+
"""True when `path` is (or can now be) a directory we own and can write into.
|
|
334
|
+
|
|
335
|
+
Creation is the probe: a sandbox that denies writes under /tmp refuses the
|
|
336
|
+
mkdir, and one that lets a pre-existing root stand still fails the access
|
|
337
|
+
check, so both land on the fallback instead of failing at the first run.
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
os.mkdir(path, 0o700)
|
|
341
|
+
except FileExistsError:
|
|
342
|
+
pass
|
|
343
|
+
except OSError:
|
|
344
|
+
return False
|
|
345
|
+
try:
|
|
346
|
+
st = os.lstat(path)
|
|
347
|
+
except OSError:
|
|
348
|
+
return False
|
|
349
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
350
|
+
return False
|
|
351
|
+
if IS_WINDOWS:
|
|
352
|
+
if not win_owns_path(path):
|
|
353
|
+
return False
|
|
354
|
+
elif _EFFECTIVE_UID is not None and st.st_uid != _EFFECTIVE_UID:
|
|
355
|
+
return False
|
|
356
|
+
return os.access(path, os.W_OK)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _fallback_scratch_root() -> str:
|
|
360
|
+
if IS_WINDOWS:
|
|
361
|
+
return os.path.join(tempfile.gettempdir(), "compound-engineering")
|
|
362
|
+
return os.path.join(os.environ.get("TMPDIR") or "/tmp", f"compound-engineering-{_EFFECTIVE_UID}")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def owner_scratch_root() -> str:
|
|
366
|
+
"""The owner-private scratch root, in the same candidate order as the skills' shell preamble."""
|
|
367
|
+
if OWNER_SCRATCH_ROOT is None:
|
|
368
|
+
raise TrustFailure("effective user ID is unavailable; cannot derive the runs root")
|
|
369
|
+
if _private_root_usable(OWNER_SCRATCH_ROOT):
|
|
370
|
+
return OWNER_SCRATCH_ROOT
|
|
371
|
+
return _fallback_scratch_root()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def runs_root() -> str:
|
|
375
|
+
configured = os.environ.get("CE_WORK_RUNS_ROOT")
|
|
376
|
+
if configured:
|
|
377
|
+
return os.path.abspath(configured)
|
|
378
|
+
peer_root = os.environ.get("CE_PEER_JOBS_ROOT")
|
|
379
|
+
if peer_root:
|
|
380
|
+
return os.path.join(os.path.abspath(peer_root), "ce-work")
|
|
381
|
+
return os.path.join(owner_scratch_root(), "ce-work")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def safe_id(value: str, label: str) -> str:
|
|
385
|
+
if not SAFE_ID.fullmatch(value) or not value.strip("."):
|
|
386
|
+
raise Operational("REFUSED", f"unsafe {label}: {value!r}")
|
|
387
|
+
return value
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def digest_bytes(data: bytes) -> str:
|
|
391
|
+
return hashlib.sha256(data).hexdigest()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _valid_git_object_id(value: object) -> bool:
|
|
395
|
+
if not isinstance(value, str):
|
|
396
|
+
return False
|
|
397
|
+
try:
|
|
398
|
+
raw = bytes.fromhex(value)
|
|
399
|
+
except ValueError:
|
|
400
|
+
return False
|
|
401
|
+
return len(raw) in {20, 32} and raw.hex() == value
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _native_completion_commit(unit: dict) -> str | None:
|
|
405
|
+
attempts = unit.get("attempts")
|
|
406
|
+
if not isinstance(attempts, list) or not attempts or not isinstance(attempts[-1], dict):
|
|
407
|
+
return None
|
|
408
|
+
fallback = attempts[-1].get("fallback")
|
|
409
|
+
if not isinstance(fallback, dict):
|
|
410
|
+
return None
|
|
411
|
+
claim = fallback.get("claimed")
|
|
412
|
+
completion = fallback.get("completed")
|
|
413
|
+
if not isinstance(claim, dict) or not isinstance(completion, dict) or completion.get("claim") != claim:
|
|
414
|
+
return None
|
|
415
|
+
claim_mode = claim.get("mode")
|
|
416
|
+
if claim_mode not in {"prefer", "require"}:
|
|
417
|
+
return None
|
|
418
|
+
accepted_head = completion.get("accepted_head")
|
|
419
|
+
base = unit.get("workspace", {}).get("base")
|
|
420
|
+
snapshot = completion.get("snapshot")
|
|
421
|
+
wave = unit.get("wave", {})
|
|
422
|
+
changed_paths = completion.get("changed_paths")
|
|
423
|
+
if not (
|
|
424
|
+
_valid_git_object_id(accepted_head)
|
|
425
|
+
and _valid_git_object_id(base)
|
|
426
|
+
and completion.get("base") == base
|
|
427
|
+
and isinstance(completion.get("at"), str)
|
|
428
|
+
and bool(completion["at"])
|
|
429
|
+
and isinstance(completion.get("summary"), str)
|
|
430
|
+
and bool(completion["summary"])
|
|
431
|
+
and isinstance(completion.get("evidence_digest"), str)
|
|
432
|
+
and len(completion["evidence_digest"]) == 64
|
|
433
|
+
and _valid_git_object_id(completion["evidence_digest"])
|
|
434
|
+
and isinstance(snapshot, dict)
|
|
435
|
+
and snapshot.get("head") == accepted_head
|
|
436
|
+
and snapshot.get("status_empty") is True
|
|
437
|
+
and snapshot.get("worktree_index_empty") is True
|
|
438
|
+
and _valid_git_object_id(snapshot.get("head_tree"))
|
|
439
|
+
and snapshot.get("head_tree") == snapshot.get("index_tree")
|
|
440
|
+
and snapshot.get("status_sha256") == digest_bytes(b"")
|
|
441
|
+
and (
|
|
442
|
+
not wave.get("id")
|
|
443
|
+
or (
|
|
444
|
+
_valid_git_object_id(claim.get("canonical_head"))
|
|
445
|
+
and isinstance(changed_paths, list)
|
|
446
|
+
and all(isinstance(path, str) for path in changed_paths)
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
):
|
|
450
|
+
return None
|
|
451
|
+
return accepted_head
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def unit_accepted_commit(unit: dict) -> str | None:
|
|
455
|
+
if unit.get("state") == "native-completed":
|
|
456
|
+
return _native_completion_commit(unit)
|
|
457
|
+
if unit.get("state") != "cleaned":
|
|
458
|
+
return None
|
|
459
|
+
integration = unit.get("integration")
|
|
460
|
+
if not isinstance(integration, dict):
|
|
461
|
+
return None
|
|
462
|
+
canonical = integration.get("canonical_commit")
|
|
463
|
+
if not (
|
|
464
|
+
isinstance(canonical, dict)
|
|
465
|
+
and all(_valid_git_object_id(canonical.get(field)) for field in ("commit", "parent", "tree"))
|
|
466
|
+
and isinstance(canonical.get("at"), str)
|
|
467
|
+
and bool(canonical["at"])
|
|
468
|
+
):
|
|
469
|
+
return None
|
|
470
|
+
return canonical["commit"]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def unit_ready_for_run_verification(unit: object) -> bool:
|
|
474
|
+
return isinstance(unit, dict) and unit_accepted_commit(unit) is not None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def accepted_unit_commit_snapshot(units: object) -> dict[str, str] | None:
|
|
478
|
+
if not isinstance(units, dict):
|
|
479
|
+
return None
|
|
480
|
+
snapshot: dict[str, str] = {}
|
|
481
|
+
for unit_id in sorted(units):
|
|
482
|
+
if not isinstance(unit_id, str) or not SAFE_ID.fullmatch(unit_id):
|
|
483
|
+
return None
|
|
484
|
+
unit = units[unit_id]
|
|
485
|
+
if not isinstance(unit, dict):
|
|
486
|
+
return None
|
|
487
|
+
commit = unit_accepted_commit(unit)
|
|
488
|
+
if commit is None:
|
|
489
|
+
return None
|
|
490
|
+
snapshot[unit_id] = commit
|
|
491
|
+
return snapshot
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _mode(st: os.stat_result) -> int:
|
|
495
|
+
return stat.S_IMODE(st.st_mode)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def is_private(st: os.stat_result, want_dir: bool) -> bool:
|
|
499
|
+
"""True when the object carries the privacy this module requires: mode
|
|
500
|
+
0700/0600 on POSIX, a verified owner SID on Windows (its DACL is what
|
|
501
|
+
provides privacy there, and `_mode` cannot see it)."""
|
|
502
|
+
return _modeis_private(st, want_dir)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _owned_by_us(st: os.stat_result) -> bool:
|
|
506
|
+
if IS_WINDOWS:
|
|
507
|
+
return True # the descriptor/handle owner check already ran
|
|
508
|
+
return _euid() is None or st.st_uid == _euid()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _euid() -> int | None:
|
|
512
|
+
return _EFFECTIVE_UID
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def validate_private_dir(path: str) -> None:
|
|
516
|
+
if IS_WINDOWS:
|
|
517
|
+
# Windows cannot open a directory with os.open() (the CRT maps it to
|
|
518
|
+
# CreateFileW without FILE_FLAG_BACKUP_SEMANTICS, which the kernel
|
|
519
|
+
# rejects with ERROR_ACCESS_DENIED for any directory), so there is no
|
|
520
|
+
# descriptor to fstat here. The path-based owner check is what
|
|
521
|
+
# peer-job-runner's `_check_owned_dir` uses on Windows for the same
|
|
522
|
+
# reason, and the DACL it verifies is the privacy boundary.
|
|
523
|
+
try:
|
|
524
|
+
st = os.lstat(path)
|
|
525
|
+
except OSError as exc:
|
|
526
|
+
raise TrustFailure(f"cannot safely stat directory {path}: {exc}") from exc
|
|
527
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
528
|
+
raise TrustFailure(f"not a real directory: {path}")
|
|
529
|
+
if not win_owns_path(path):
|
|
530
|
+
raise TrustFailure(f"directory is not owned by current user: {path}")
|
|
531
|
+
return
|
|
532
|
+
try:
|
|
533
|
+
fd = os.open(path, os.O_RDONLY | O_DIRECTORY | O_NOFOLLOW)
|
|
534
|
+
except OSError as exc:
|
|
535
|
+
raise TrustFailure(f"cannot safely open directory {path}: {exc}") from exc
|
|
536
|
+
try:
|
|
537
|
+
st = os.fstat(fd)
|
|
538
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
539
|
+
raise TrustFailure(f"not a real directory: {path}")
|
|
540
|
+
if _euid() is not None and st.st_uid != _euid():
|
|
541
|
+
raise TrustFailure(f"directory is not owned by current user: {path}")
|
|
542
|
+
if _mode(st) != 0o700:
|
|
543
|
+
raise TrustFailure(f"directory mode is {_mode(st):04o}, expected 0700: {path}")
|
|
544
|
+
finally:
|
|
545
|
+
os.close(fd)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def ensure_private_dir(path: str) -> None:
|
|
549
|
+
created = False
|
|
550
|
+
try:
|
|
551
|
+
os.mkdir(path, 0o700)
|
|
552
|
+
created = True
|
|
553
|
+
except FileExistsError:
|
|
554
|
+
pass
|
|
555
|
+
if IS_WINDOWS and created:
|
|
556
|
+
win_harden_acl(path)
|
|
557
|
+
validate_private_dir(path)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _owner_root_for_runs(root: str) -> str | None:
|
|
561
|
+
if OWNER_SCRATCH_ROOT is None:
|
|
562
|
+
return None
|
|
563
|
+
for candidate in (OWNER_SCRATCH_ROOT, _fallback_scratch_root()):
|
|
564
|
+
owner_root = os.path.abspath(candidate)
|
|
565
|
+
try:
|
|
566
|
+
if os.path.commonpath([owner_root, os.path.abspath(root)]) == owner_root:
|
|
567
|
+
return owner_root
|
|
568
|
+
except ValueError: # different drives on Windows: not under this candidate
|
|
569
|
+
continue
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _ensure_owner_scratch_root(path: str) -> None:
|
|
574
|
+
try:
|
|
575
|
+
os.mkdir(path, 0o700)
|
|
576
|
+
except FileExistsError:
|
|
577
|
+
pass
|
|
578
|
+
if IS_WINDOWS:
|
|
579
|
+
# No directory descriptor on Windows (see validate_private_dir); the
|
|
580
|
+
# owner check plus the hardened ACL are the whole guarantee, and the
|
|
581
|
+
# POSIX chmod-repair has no Windows analogue -- icacls is idempotent
|
|
582
|
+
# so the repair is simply re-hardening.
|
|
583
|
+
try:
|
|
584
|
+
st = os.lstat(path)
|
|
585
|
+
except OSError as exc:
|
|
586
|
+
raise TrustFailure(f"cannot safely stat owner scratch root {path}: {exc}") from exc
|
|
587
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
588
|
+
raise TrustFailure(f"owner scratch root is not a real directory: {path}")
|
|
589
|
+
if not win_owns_path(path):
|
|
590
|
+
raise TrustFailure(f"owner scratch root is not owned by current user: {path}")
|
|
591
|
+
win_harden_acl(path)
|
|
592
|
+
return
|
|
593
|
+
try:
|
|
594
|
+
fd = os.open(path, os.O_RDONLY | O_DIRECTORY | O_NOFOLLOW)
|
|
595
|
+
except OSError as exc:
|
|
596
|
+
raise TrustFailure(f"cannot safely open owner scratch root {path}: {exc}") from exc
|
|
597
|
+
try:
|
|
598
|
+
current = os.fstat(fd)
|
|
599
|
+
if not stat.S_ISDIR(current.st_mode):
|
|
600
|
+
raise TrustFailure(f"owner scratch root is not a real directory: {path}")
|
|
601
|
+
if _euid() is not None and current.st_uid != _euid():
|
|
602
|
+
raise TrustFailure(f"owner scratch root is not owned by current user: {path}")
|
|
603
|
+
if _mode(current) != 0o700:
|
|
604
|
+
os.fchmod(fd, 0o700)
|
|
605
|
+
repaired = os.fstat(fd)
|
|
606
|
+
if repaired.st_uid != current.st_uid or _mode(repaired) != 0o700:
|
|
607
|
+
raise TrustFailure(f"could not repair owner scratch root mode to 0700: {path}")
|
|
608
|
+
finally:
|
|
609
|
+
os.close(fd)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def ensure_root() -> str:
|
|
613
|
+
return ensure_runs_root(runs_root())
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def ensure_runs_root(root: str) -> str:
|
|
617
|
+
"""Create or verify one runs root (the creation root, or the other candidate
|
|
618
|
+
root an existing run was found under) and its private lock directory."""
|
|
619
|
+
owner_root = _owner_root_for_runs(root)
|
|
620
|
+
if owner_root is not None:
|
|
621
|
+
_ensure_owner_scratch_root(owner_root)
|
|
622
|
+
parent = os.path.dirname(root)
|
|
623
|
+
# The configured root's ancestors are caller-controlled; the private root
|
|
624
|
+
# itself and everything below it are the durable confidentiality boundary.
|
|
625
|
+
os.makedirs(parent, mode=0o700, exist_ok=True)
|
|
626
|
+
ensure_private_dir(root)
|
|
627
|
+
ensure_private_dir(os.path.join(root, ".locks"))
|
|
628
|
+
return root
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def read_private(path: str, cap: int = MAX_JSON_BYTES) -> bytes:
|
|
632
|
+
try:
|
|
633
|
+
fd = os.open(path, os.O_RDONLY | O_NOFOLLOW)
|
|
634
|
+
except OSError as exc:
|
|
635
|
+
raise TrustFailure(f"cannot safely open state file {path}: {exc}") from exc
|
|
636
|
+
try:
|
|
637
|
+
st = os.fstat(fd)
|
|
638
|
+
if not stat.S_ISREG(st.st_mode):
|
|
639
|
+
raise TrustFailure(f"state is not a regular file: {path}")
|
|
640
|
+
if IS_WINDOWS:
|
|
641
|
+
if not win_owns_handle(msvcrt.get_osfhandle(fd)):
|
|
642
|
+
raise TrustFailure(f"state is not owned by current user: {path}")
|
|
643
|
+
elif _euid() is not None and st.st_uid != _euid():
|
|
644
|
+
raise TrustFailure(f"state is not owned by current user: {path}")
|
|
645
|
+
if not is_private(st, want_dir=False):
|
|
646
|
+
raise TrustFailure(f"state mode is {_mode(st):04o}, expected 0600: {path}")
|
|
647
|
+
if st.st_size > cap:
|
|
648
|
+
raise TrustFailure(f"state exceeds {cap}-byte limit: {path}")
|
|
649
|
+
out = bytearray()
|
|
650
|
+
while len(out) <= cap:
|
|
651
|
+
part = os.read(fd, min(65536, cap + 1 - len(out)))
|
|
652
|
+
if not part:
|
|
653
|
+
break
|
|
654
|
+
out.extend(part)
|
|
655
|
+
if len(out) > cap:
|
|
656
|
+
raise TrustFailure(f"state grew beyond {cap}-byte limit: {path}")
|
|
657
|
+
return bytes(out)
|
|
658
|
+
finally:
|
|
659
|
+
os.close(fd)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def stat_private_file(path: str) -> os.stat_result:
|
|
663
|
+
"""Validate a private file by descriptor without consuming its content."""
|
|
664
|
+
try:
|
|
665
|
+
fd = os.open(path, os.O_RDONLY | O_NOFOLLOW)
|
|
666
|
+
except OSError as exc:
|
|
667
|
+
raise TrustFailure(f"cannot safely open state file {path}: {exc}") from exc
|
|
668
|
+
try:
|
|
669
|
+
st = os.fstat(fd)
|
|
670
|
+
if not stat.S_ISREG(st.st_mode):
|
|
671
|
+
raise TrustFailure(f"state is not a regular file: {path}")
|
|
672
|
+
if IS_WINDOWS:
|
|
673
|
+
if not win_owns_handle(msvcrt.get_osfhandle(fd)):
|
|
674
|
+
raise TrustFailure(f"state is not owned by current user: {path}")
|
|
675
|
+
elif _euid() is not None and st.st_uid != _euid():
|
|
676
|
+
raise TrustFailure(f"state is not owned by current user: {path}")
|
|
677
|
+
if not is_private(st, want_dir=False):
|
|
678
|
+
raise TrustFailure(f"state mode is {_mode(st):04o}, expected 0600: {path}")
|
|
679
|
+
return st
|
|
680
|
+
finally:
|
|
681
|
+
os.close(fd)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def read_private_json(path: str) -> dict:
|
|
685
|
+
try:
|
|
686
|
+
value = json.loads(read_private(path))
|
|
687
|
+
except TrustFailure:
|
|
688
|
+
raise
|
|
689
|
+
except (ValueError, UnicodeDecodeError) as exc:
|
|
690
|
+
raise TrustFailure(f"malformed JSON state: {path}") from exc
|
|
691
|
+
if not isinstance(value, dict):
|
|
692
|
+
raise TrustFailure(f"JSON state is not an object: {path}")
|
|
693
|
+
return value
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def create_private(path: str, data: bytes) -> None:
|
|
697
|
+
try:
|
|
698
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | O_NOFOLLOW, 0o600)
|
|
699
|
+
except OSError as exc:
|
|
700
|
+
raise Operational("BLOCKED", f"cannot exclusively create {path}: {exc}") from exc
|
|
701
|
+
try:
|
|
702
|
+
os.write(fd, data)
|
|
703
|
+
os.fsync(fd)
|
|
704
|
+
finally:
|
|
705
|
+
os.close(fd)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def atomic_private_json(path: str, doc: dict) -> None:
|
|
709
|
+
data = (json.dumps(doc, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
|
710
|
+
if len(data) > MAX_JSON_BYTES:
|
|
711
|
+
raise Operational("BLOCKED", "manifest exceeds bounded state size")
|
|
712
|
+
parent = os.path.dirname(path)
|
|
713
|
+
fd, tmp = tempfile.mkstemp(prefix=".manifest-", dir=parent)
|
|
714
|
+
try:
|
|
715
|
+
if not IS_WINDOWS:
|
|
716
|
+
os.fchmod(fd, 0o600)
|
|
717
|
+
with os.fdopen(fd, "wb", closefd=True) as stream:
|
|
718
|
+
stream.write(data)
|
|
719
|
+
stream.flush()
|
|
720
|
+
os.fsync(stream.fileno())
|
|
721
|
+
os.replace(tmp, path)
|
|
722
|
+
if not IS_WINDOWS:
|
|
723
|
+
# Directory fsync makes the rename durable. Windows cannot open a
|
|
724
|
+
# directory for fsync and does not need it: NTFS journals the rename.
|
|
725
|
+
dfd = os.open(parent, os.O_RDONLY | O_DIRECTORY | O_NOFOLLOW)
|
|
726
|
+
try:
|
|
727
|
+
os.fsync(dfd)
|
|
728
|
+
finally:
|
|
729
|
+
os.close(dfd)
|
|
730
|
+
except BaseException:
|
|
731
|
+
with contextlib.suppress(OSError):
|
|
732
|
+
os.unlink(tmp)
|
|
733
|
+
raise
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def candidate_runs_roots() -> list:
|
|
737
|
+
"""Every root an existing run may live under (configured root alone, or the
|
|
738
|
+
/tmp root and the $TMPDIR fallback, primary first). Creation uses runs_root();
|
|
739
|
+
lookup must not depend on which root this invocation would create under."""
|
|
740
|
+
configured = os.environ.get("CE_WORK_RUNS_ROOT")
|
|
741
|
+
if configured:
|
|
742
|
+
return [os.path.abspath(configured)]
|
|
743
|
+
peer_root = os.environ.get("CE_PEER_JOBS_ROOT")
|
|
744
|
+
if peer_root:
|
|
745
|
+
return [os.path.join(os.path.abspath(peer_root), "ce-work")]
|
|
746
|
+
if OWNER_SCRATCH_ROOT is None:
|
|
747
|
+
raise TrustFailure("effective user ID is unavailable; cannot derive the runs root")
|
|
748
|
+
roots = [os.path.join(os.path.abspath(OWNER_SCRATCH_ROOT), "ce-work")]
|
|
749
|
+
fallback = os.path.join(os.path.abspath(_fallback_scratch_root()), "ce-work")
|
|
750
|
+
if fallback not in roots:
|
|
751
|
+
roots.append(fallback)
|
|
752
|
+
return roots
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def run_dir(run_id: str) -> str:
|
|
756
|
+
rid = safe_id(run_id, "run id")
|
|
757
|
+
for root in candidate_runs_roots():
|
|
758
|
+
existing = os.path.join(root, rid)
|
|
759
|
+
if os.path.isdir(existing) and not os.path.islink(existing):
|
|
760
|
+
return existing
|
|
761
|
+
return os.path.join(runs_root(), rid)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@contextlib.contextmanager
|
|
765
|
+
def locked_manifest(run_id: str, write: bool = False):
|
|
766
|
+
run_id = safe_id(run_id, "run id")
|
|
767
|
+
rd = run_dir(run_id)
|
|
768
|
+
ensure_runs_root(os.path.dirname(rd))
|
|
769
|
+
validate_private_dir(rd)
|
|
770
|
+
lock_path = os.path.join(rd, "manifest.lock")
|
|
771
|
+
try:
|
|
772
|
+
fd = os.open(lock_path, os.O_RDWR | O_NOFOLLOW)
|
|
773
|
+
except OSError as exc:
|
|
774
|
+
raise TrustFailure(f"cannot safely open manifest lock: {exc}") from exc
|
|
775
|
+
try:
|
|
776
|
+
st = os.fstat(fd)
|
|
777
|
+
if not stat.S_ISREG(st.st_mode):
|
|
778
|
+
raise TrustFailure("manifest lock is not a regular file")
|
|
779
|
+
if IS_WINDOWS:
|
|
780
|
+
if not win_owns_handle(msvcrt.get_osfhandle(fd)):
|
|
781
|
+
raise TrustFailure("manifest lock owner validation failed")
|
|
782
|
+
elif (_euid() is not None and st.st_uid != _euid()) or _mode(st) != 0o600:
|
|
783
|
+
raise TrustFailure("manifest lock owner/type/mode validation failed")
|
|
784
|
+
_lock_fd(fd, exclusive=write)
|
|
785
|
+
doc = read_private_json(os.path.join(rd, "manifest.json"))
|
|
786
|
+
if doc.get("schema_version") != SCHEMA_VERSION or doc.get("run_id") != run_id:
|
|
787
|
+
raise TrustFailure("manifest schema or run identity mismatch")
|
|
788
|
+
before = json.dumps(doc, sort_keys=True, separators=(",", ":"))
|
|
789
|
+
yield doc
|
|
790
|
+
after = json.dumps(doc, sort_keys=True, separators=(",", ":"))
|
|
791
|
+
if write and after != before:
|
|
792
|
+
doc["revision"] = int(doc.get("revision", 0)) + 1
|
|
793
|
+
doc["updated_at"] = now_iso()
|
|
794
|
+
atomic_private_json(os.path.join(rd, "manifest.json"), doc)
|
|
795
|
+
finally:
|
|
796
|
+
_unlock_fd(fd)
|
|
797
|
+
os.close(fd)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def sanitized_git_environment(overrides: dict | None = None) -> dict[str, str]:
|
|
801
|
+
process_env = {key: value for key, value in os.environ.items() if key not in GIT_LOCAL_ENV_VARS}
|
|
802
|
+
process_env.update(overrides or {})
|
|
803
|
+
return process_env
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def git(repo: str, *args: str, input_data: bytes | None = None, check: bool = True, env: dict | None = None) -> bytes:
|
|
807
|
+
proc = subprocess.run(
|
|
808
|
+
["git", "-C", repo, *args], input=input_data, capture_output=True,
|
|
809
|
+
env=sanitized_git_environment(env), check=False,
|
|
810
|
+
)
|
|
811
|
+
if check and proc.returncode != 0:
|
|
812
|
+
message = proc.stderr.decode("utf-8", "replace").strip()
|
|
813
|
+
raise Operational("BLOCKED", f"git {' '.join(args)} failed: {message}")
|
|
814
|
+
return proc.stdout
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def git_text(repo: str, *args: str, check: bool = True) -> str:
|
|
818
|
+
return git(repo, *args, check=check).decode("utf-8", "surrogateescape").strip()
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def commit_index_tree(repo: str, message: str) -> str:
|
|
822
|
+
"""Commit the verified index directly, without invoking repository hooks."""
|
|
823
|
+
if not message.strip() or "\0" in message:
|
|
824
|
+
raise Operational("REFUSED", "commit message must be non-empty and contain no NUL")
|
|
825
|
+
parent = git_text(repo, "rev-parse", "HEAD")
|
|
826
|
+
branch_ref = git_text(repo, "symbolic-ref", "-q", "HEAD")
|
|
827
|
+
tree = git_text(repo, "write-tree")
|
|
828
|
+
commit = git(
|
|
829
|
+
repo,
|
|
830
|
+
"commit-tree", tree, "-p", parent,
|
|
831
|
+
input_data=f"{message.rstrip()}\n".encode("utf-8"),
|
|
832
|
+
).decode("ascii", "strict").strip()
|
|
833
|
+
git(repo, "update-ref", branch_ref, commit, parent)
|
|
834
|
+
return commit
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def repo_info(repo: str) -> dict:
|
|
838
|
+
repo = os.path.realpath(repo)
|
|
839
|
+
top = os.path.realpath(git_text(repo, "rev-parse", "--show-toplevel"))
|
|
840
|
+
if top != repo:
|
|
841
|
+
repo = top
|
|
842
|
+
branch = git_text(repo, "symbolic-ref", "-q", "HEAD", check=False)
|
|
843
|
+
if not branch:
|
|
844
|
+
raise Operational("REFUSED", "canonical checkout must be on a branch")
|
|
845
|
+
git_dir = os.path.realpath(git_text(repo, "rev-parse", "--path-format=absolute", "--absolute-git-dir"))
|
|
846
|
+
common = os.path.realpath(git_text(repo, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
847
|
+
st = os.stat(common)
|
|
848
|
+
roots = sorted(git_text(repo, "rev-list", "--max-parents=0", "HEAD").splitlines())
|
|
849
|
+
identity = digest_bytes((common + f"\0{st.st_dev}\0{st.st_ino}\0" + "\n".join(roots)).encode())
|
|
850
|
+
return {
|
|
851
|
+
"toplevel": repo,
|
|
852
|
+
"git_dir": git_dir,
|
|
853
|
+
"common_dir": common,
|
|
854
|
+
"common_dev": st.st_dev,
|
|
855
|
+
"common_ino": st.st_ino,
|
|
856
|
+
"identity_digest": identity,
|
|
857
|
+
"branch_ref": branch,
|
|
858
|
+
"head": git_text(repo, "rev-parse", "HEAD"),
|
|
859
|
+
"head_tree": git_text(repo, "rev-parse", "HEAD^{tree}"),
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def validate_source(doc: dict) -> None:
|
|
864
|
+
source = doc.get("source")
|
|
865
|
+
if source is not None:
|
|
866
|
+
if not isinstance(source, dict):
|
|
867
|
+
raise TrustFailure("manifest source record is malformed")
|
|
868
|
+
kind = source.get("kind")
|
|
869
|
+
if kind == "prompt":
|
|
870
|
+
if source.get("storage") != "run" or source.get("path") != "source/bare-prompt.md":
|
|
871
|
+
raise TrustFailure("prompt source location is malformed")
|
|
872
|
+
if not isinstance(source.get("digest"), str) or not re.fullmatch(r"[0-9a-f]{64}", source["digest"]):
|
|
873
|
+
raise TrustFailure("prompt source digest is malformed")
|
|
874
|
+
data = read_private(os.path.join(run_dir(doc["run_id"]), source["path"]), MAX_PACKET_BYTES)
|
|
875
|
+
if digest_bytes(data) != source.get("digest"):
|
|
876
|
+
raise TrustFailure("prompt source digest does not match private content")
|
|
877
|
+
elif kind == "plan":
|
|
878
|
+
if source.get("storage") != "repository" or not isinstance(source.get("path"), str):
|
|
879
|
+
raise TrustFailure("plan source location is malformed")
|
|
880
|
+
if not isinstance(source.get("digest"), str) or not re.fullmatch(r"[0-9a-f]{64}", source["digest"]):
|
|
881
|
+
raise TrustFailure("plan source digest is malformed")
|
|
882
|
+
else:
|
|
883
|
+
raise TrustFailure("manifest source kind is invalid")
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def validate_repo(doc: dict) -> dict:
|
|
887
|
+
validate_source(doc)
|
|
888
|
+
recorded = doc["repository"]
|
|
889
|
+
current = repo_info(recorded["toplevel"])
|
|
890
|
+
for key in ("toplevel", "git_dir", "common_dir", "common_dev", "common_ino", "identity_digest"):
|
|
891
|
+
if current[key] != recorded[key]:
|
|
892
|
+
raise Operational("BLOCKED", f"canonical repository identity changed ({key})")
|
|
893
|
+
if current["branch_ref"] != doc["branch"]["ref"]:
|
|
894
|
+
raise Operational("BLOCKED", "canonical branch changed")
|
|
895
|
+
return current
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def resolve_plan(repo: str, plan: str) -> tuple[str, str]:
|
|
899
|
+
supplied = os.path.abspath(plan if os.path.isabs(plan) else os.path.join(repo, plan))
|
|
900
|
+
try:
|
|
901
|
+
st = os.lstat(supplied)
|
|
902
|
+
except OSError as exc:
|
|
903
|
+
raise Operational("REFUSED", f"selected plan is missing: {exc}") from exc
|
|
904
|
+
if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode):
|
|
905
|
+
raise Operational("REFUSED", "selected plan must be one regular non-symlink file")
|
|
906
|
+
# OS temp roots may themselves be compatibility symlinks (macOS /var ->
|
|
907
|
+
# /private/var). Reject a symlink at the selected file, then compare the
|
|
908
|
+
# resolved file against the already-resolved canonical repository.
|
|
909
|
+
absolute = os.path.realpath(supplied)
|
|
910
|
+
if os.path.commonpath([repo, absolute]) != repo:
|
|
911
|
+
raise Operational("REFUSED", "plan must be inside the canonical repository")
|
|
912
|
+
return absolute, os.path.relpath(absolute, repo)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def parse_json_arg(raw: str, label: str) -> dict:
|
|
916
|
+
try:
|
|
917
|
+
value = json.loads(raw)
|
|
918
|
+
except ValueError as exc:
|
|
919
|
+
raise Operational("REFUSED", f"invalid {label} JSON") from exc
|
|
920
|
+
if not isinstance(value, dict):
|
|
921
|
+
raise Operational("REFUSED", f"{label} must be a JSON object")
|
|
922
|
+
return value
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
ROUTE_CONTRACTS = {
|
|
926
|
+
"codex": {"target": "codex", "harness": "codex", "intermediaries": [], "default_model": "auto", "restriction_posture": "cooperative"},
|
|
927
|
+
"claude": {"target": "claude", "harness": "claude", "intermediaries": [], "default_model": "auto", "restriction_posture": "cooperative"},
|
|
928
|
+
"grok-cli": {"target": "grok", "harness": "grok", "intermediaries": [], "default_model": "auto", "restriction_posture": "cooperative"},
|
|
929
|
+
"cursor": {"target": "cursor", "harness": "cursor-agent", "intermediaries": [], "default_model": "auto", "restriction_posture": "cooperative"},
|
|
930
|
+
"composer": {"target": "composer", "harness": "cursor-agent", "intermediaries": ["cursor"], "default_model": "composer-2.5[fast=true]", "restriction_posture": "cooperative"},
|
|
931
|
+
"grok-cursor": {"target": "grok", "harness": "cursor-agent", "intermediaries": ["cursor"], "default_model": "grok-4.7[context=256k,reasoning_effort=high,fast=true]", "restriction_posture": "cooperative"},
|
|
932
|
+
"opencode": {"target": "opencode", "harness": "opencode", "intermediaries": [], "default_model": "auto", "restriction_posture": "cooperative"},
|
|
933
|
+
"antigravity": {"target": "antigravity", "harness": "antigravity", "intermediaries": [], "default_model": "gemini-4-argon", "restriction_posture": "cooperative"},
|
|
934
|
+
# DeepSeek Harness. Model ids are `provider/model` (e.g.
|
|
935
|
+
# `workbuddy/deepseek-v4.1-flash`), the same shape opencode uses, because a
|
|
936
|
+
# DSH profile can front several providers at once and the provider is part
|
|
937
|
+
# of the identity -- `deepseek-v4.1-flash` alone is ambiguous across them.
|
|
938
|
+
"dsh": {"target": "dsh", "harness": "dsh", "intermediaries": [], "default_model": "auto", "restriction_posture": "cooperative"},
|
|
939
|
+
}
|
|
940
|
+
# Cursor's ACP model ids may carry a bracketed preset; route families apply to
|
|
941
|
+
# the id before the bracket.
|
|
942
|
+
CURSOR_MODEL = re.compile(r"([A-Za-z0-9][A-Za-z0-9._:/-]*)(?:\[[A-Za-z0-9._=,:-]*\])?")
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def route_model_allowed(route: str, model: str) -> bool:
|
|
946
|
+
if route in {"cursor", "composer", "grok-cursor"}:
|
|
947
|
+
match = CURSOR_MODEL.fullmatch(model)
|
|
948
|
+
if not match:
|
|
949
|
+
return False
|
|
950
|
+
base = match.group(1)
|
|
951
|
+
if route == "composer":
|
|
952
|
+
return bool(re.fullmatch(r"composer-[A-Za-z0-9._-]+", base))
|
|
953
|
+
if route == "grok-cursor":
|
|
954
|
+
return bool(re.fullmatch(r"(?:cursor-)?grok-[A-Za-z0-9._-]+", base))
|
|
955
|
+
lowered = base.lower()
|
|
956
|
+
return not (lowered in {"composer", "grok"} or lowered.startswith(("composer-", "grok-", "cursor-grok-")))
|
|
957
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]*", model):
|
|
958
|
+
return False
|
|
959
|
+
if route == "codex":
|
|
960
|
+
return model == "auto" or bool(re.fullmatch(r"(?:gpt-[A-Za-z0-9._-]+|o[0-9][A-Za-z0-9._-]*)", model))
|
|
961
|
+
if route == "claude":
|
|
962
|
+
return model in {"auto", "fable", "opus", "sonnet", "haiku"} or bool(re.fullmatch(r"claude-[A-Za-z0-9._-]+", model))
|
|
963
|
+
if route == "grok-cli":
|
|
964
|
+
return model == "auto" or bool(re.fullmatch(r"grok-[A-Za-z0-9._-]+", model))
|
|
965
|
+
if route == "opencode":
|
|
966
|
+
return model == "auto" or bool(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9._-]+", model))
|
|
967
|
+
if route == "dsh":
|
|
968
|
+
# `auto` lets the profile's configured default model decide;
|
|
969
|
+
# `provider/model` pins one explicitly, provider first.
|
|
970
|
+
return model == "auto" or bool(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*", model))
|
|
971
|
+
if route == "antigravity":
|
|
972
|
+
# Antigravity also serves Claude and gpt-oss models; this route exists for Gemini.
|
|
973
|
+
return bool(re.fullmatch(r"gemini-[A-Za-z0-9._-]+", model))
|
|
974
|
+
return False
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
EFFORT_TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,31}")
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def fixed_route_contract(binding: dict, egress: dict, word: str = "BLOCKED") -> dict:
|
|
981
|
+
if not isinstance(binding, dict) or not isinstance(egress, dict):
|
|
982
|
+
raise Operational(word, "run binding or egress sanction is malformed")
|
|
983
|
+
expected_binding_fields = {"mode", "target", "model", "source"}
|
|
984
|
+
if set(binding) != expected_binding_fields:
|
|
985
|
+
raise Operational(word, "binding must contain exactly mode, target, model, and source")
|
|
986
|
+
if binding.get("mode") not in {"prefer", "require"}:
|
|
987
|
+
raise Operational(word, "binding mode must be 'prefer' or 'require'")
|
|
988
|
+
source = binding.get("source")
|
|
989
|
+
if not isinstance(source, str) or not source or "\0" in source or len(source.encode()) > 256:
|
|
990
|
+
raise Operational(word, "binding source must be a non-empty string of at most 256 bytes")
|
|
991
|
+
route = egress.get("route")
|
|
992
|
+
contract = ROUTE_CONTRACTS.get(route)
|
|
993
|
+
if not contract:
|
|
994
|
+
allowed = ", ".join(ROUTE_CONTRACTS)
|
|
995
|
+
raise Operational(word, f"unsupported egress route {route!r}; expected one of: {allowed}")
|
|
996
|
+
if binding.get("target") != contract["target"]:
|
|
997
|
+
raise Operational(word, "binding target does not match the sanctioned fixed route")
|
|
998
|
+
intermediaries = egress.get("intermediaries")
|
|
999
|
+
if intermediaries != contract["intermediaries"]:
|
|
1000
|
+
raise Operational(word, "egress intermediaries do not match the fixed route")
|
|
1001
|
+
model = binding.get("model")
|
|
1002
|
+
if model is not None and (not isinstance(model, str) or not model):
|
|
1003
|
+
raise Operational(word, "binding model must be null or a non-empty string")
|
|
1004
|
+
requested_model = model or contract["default_model"]
|
|
1005
|
+
if not route_model_allowed(route, requested_model):
|
|
1006
|
+
raise Operational(word, "binding model is not compatible with the sanctioned fixed route")
|
|
1007
|
+
restrictions = egress.get("restrictions", [])
|
|
1008
|
+
if not isinstance(restrictions, list) or not all(isinstance(item, str) for item in restrictions):
|
|
1009
|
+
raise Operational(word, "egress restrictions must be a string list")
|
|
1010
|
+
effort = egress.get("effort")
|
|
1011
|
+
if effort is not None and (not isinstance(effort, str) or not EFFORT_TOKEN.fullmatch(effort)):
|
|
1012
|
+
raise Operational(word, "egress effort must be a short plain token")
|
|
1013
|
+
return contract
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def attempt_authorization(
|
|
1017
|
+
doc: dict,
|
|
1018
|
+
activity_posture: str,
|
|
1019
|
+
unit_id: str,
|
|
1020
|
+
attempt_id: str,
|
|
1021
|
+
packet_digest: str,
|
|
1022
|
+
) -> dict:
|
|
1023
|
+
binding = doc.get("binding")
|
|
1024
|
+
egress = doc.get("egress")
|
|
1025
|
+
contract = fixed_route_contract(binding, egress)
|
|
1026
|
+
route = egress.get("route")
|
|
1027
|
+
intermediaries = egress.get("intermediaries")
|
|
1028
|
+
model = binding.get("model")
|
|
1029
|
+
restrictions = egress.get("restrictions", [])
|
|
1030
|
+
authorization = {
|
|
1031
|
+
"schema_version": 1,
|
|
1032
|
+
"run_id": doc["run_id"],
|
|
1033
|
+
"unit_id": unit_id,
|
|
1034
|
+
"attempt_id": attempt_id,
|
|
1035
|
+
"route": route,
|
|
1036
|
+
"target": contract["target"],
|
|
1037
|
+
"harness": contract["harness"],
|
|
1038
|
+
"intermediaries": list(contract["intermediaries"]),
|
|
1039
|
+
"model_requested": model or contract["default_model"],
|
|
1040
|
+
"restriction_posture": contract["restriction_posture"],
|
|
1041
|
+
"restrictions": list(restrictions),
|
|
1042
|
+
"activity_posture": activity_posture,
|
|
1043
|
+
"packet_digest": packet_digest,
|
|
1044
|
+
}
|
|
1045
|
+
# Present only when requested, so an unset run keeps the original key set.
|
|
1046
|
+
if egress.get("effort") is not None:
|
|
1047
|
+
authorization["effort_requested"] = egress["effort"]
|
|
1048
|
+
return authorization
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def read_external_packet(path: str, label: str = "unit packet") -> bytes:
|
|
1052
|
+
supplied = os.path.abspath(path)
|
|
1053
|
+
try:
|
|
1054
|
+
fd = os.open(supplied, os.O_RDONLY | O_NOFOLLOW)
|
|
1055
|
+
except OSError as exc:
|
|
1056
|
+
raise Operational("REFUSED", f"cannot safely open {label}: {exc}") from exc
|
|
1057
|
+
try:
|
|
1058
|
+
st = os.fstat(fd)
|
|
1059
|
+
if not stat.S_ISREG(st.st_mode):
|
|
1060
|
+
raise Operational("REFUSED", f"{label} must be one regular non-symlink file")
|
|
1061
|
+
if st.st_size > MAX_PACKET_BYTES:
|
|
1062
|
+
raise Operational("REFUSED", f"{label} exceeds {MAX_PACKET_BYTES}-byte limit")
|
|
1063
|
+
data = bytearray()
|
|
1064
|
+
while len(data) <= MAX_PACKET_BYTES:
|
|
1065
|
+
part = os.read(fd, min(65536, MAX_PACKET_BYTES + 1 - len(data)))
|
|
1066
|
+
if not part:
|
|
1067
|
+
break
|
|
1068
|
+
data.extend(part)
|
|
1069
|
+
if len(data) > MAX_PACKET_BYTES:
|
|
1070
|
+
raise Operational("REFUSED", f"{label} exceeds {MAX_PACKET_BYTES}-byte limit")
|
|
1071
|
+
return bytes(data)
|
|
1072
|
+
finally:
|
|
1073
|
+
os.close(fd)
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def event(doc: dict, kind: str, unit_id: str | None = None, detail: dict | None = None) -> None:
|
|
1077
|
+
row = {"at": now_iso(), "kind": kind}
|
|
1078
|
+
if unit_id is not None:
|
|
1079
|
+
row["unit_id"] = unit_id
|
|
1080
|
+
if detail:
|
|
1081
|
+
row["detail"] = detail
|
|
1082
|
+
doc.setdefault("events", []).append(row)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def cmd_init(args) -> tuple[str, dict]:
|
|
1086
|
+
ensure_root()
|
|
1087
|
+
rid = safe_id(args.run_id, "run id")
|
|
1088
|
+
info = repo_info(args.repo)
|
|
1089
|
+
if args.plan:
|
|
1090
|
+
if not args.plan_digest or args.prompt_digest:
|
|
1091
|
+
raise Operational("REFUSED", "plan source requires only --plan-digest")
|
|
1092
|
+
plan_abs, plan_rel = resolve_plan(info["toplevel"], args.plan)
|
|
1093
|
+
source_bytes = Path(plan_abs).read_bytes()
|
|
1094
|
+
source_kind = "plan"
|
|
1095
|
+
supplied_digest = args.plan_digest
|
|
1096
|
+
source_record = {
|
|
1097
|
+
"kind": source_kind,
|
|
1098
|
+
"storage": "repository",
|
|
1099
|
+
"path": plan_rel,
|
|
1100
|
+
"digest": digest_bytes(source_bytes),
|
|
1101
|
+
}
|
|
1102
|
+
else:
|
|
1103
|
+
if not args.prompt_digest or args.plan_digest:
|
|
1104
|
+
raise Operational("REFUSED", "prompt source requires only --prompt-digest")
|
|
1105
|
+
prompt_abs = os.path.realpath(os.path.abspath(args.prompt_brief))
|
|
1106
|
+
if os.path.commonpath([info["toplevel"], prompt_abs]) == info["toplevel"]:
|
|
1107
|
+
raise Operational("REFUSED", "prompt brief must be outside the canonical repository")
|
|
1108
|
+
source_bytes = read_external_packet(args.prompt_brief, "prompt brief")
|
|
1109
|
+
source_kind = "prompt"
|
|
1110
|
+
supplied_digest = args.prompt_digest
|
|
1111
|
+
source_record = {
|
|
1112
|
+
"kind": source_kind,
|
|
1113
|
+
"storage": "run",
|
|
1114
|
+
"path": "source/bare-prompt.md",
|
|
1115
|
+
"digest": digest_bytes(source_bytes),
|
|
1116
|
+
}
|
|
1117
|
+
actual_digest = source_record["digest"]
|
|
1118
|
+
if actual_digest != supplied_digest:
|
|
1119
|
+
raise Operational("REFUSED", f"selected {source_kind} digest does not match content")
|
|
1120
|
+
binding = parse_json_arg(args.binding_json, "binding")
|
|
1121
|
+
egress = parse_json_arg(args.egress_json, "egress")
|
|
1122
|
+
fixed_route_contract(binding, egress, "REFUSED")
|
|
1123
|
+
rd = run_dir(rid)
|
|
1124
|
+
try:
|
|
1125
|
+
os.mkdir(rd, 0o700)
|
|
1126
|
+
except FileExistsError:
|
|
1127
|
+
try:
|
|
1128
|
+
existing = os.lstat(rd)
|
|
1129
|
+
except OSError as exc:
|
|
1130
|
+
raise TrustFailure(f"cannot safely inspect run directory {rd}: {exc}") from exc
|
|
1131
|
+
if stat.S_ISDIR(existing.st_mode) and not os.path.lexists(os.path.join(rd, "manifest.json")):
|
|
1132
|
+
raise Operational(
|
|
1133
|
+
"BLOCKED",
|
|
1134
|
+
"run directory exists without a controller manifest; choose a new run id or remove the directory after confirming no initialization is active",
|
|
1135
|
+
)
|
|
1136
|
+
validate_private_dir(rd)
|
|
1137
|
+
with locked_manifest(rid) as existing:
|
|
1138
|
+
validate_repo(existing)
|
|
1139
|
+
existing_source = existing.get("source")
|
|
1140
|
+
if not isinstance(existing_source, dict):
|
|
1141
|
+
plan = existing.get("plan")
|
|
1142
|
+
existing_source = {
|
|
1143
|
+
"kind": "plan",
|
|
1144
|
+
"storage": "repository",
|
|
1145
|
+
"path": plan.get("path") if isinstance(plan, dict) else None,
|
|
1146
|
+
"digest": plan.get("digest") if isinstance(plan, dict) else None,
|
|
1147
|
+
}
|
|
1148
|
+
if (
|
|
1149
|
+
existing["repository"]["identity_digest"] != info["identity_digest"]
|
|
1150
|
+
or existing_source.get("kind") != source_kind
|
|
1151
|
+
or existing_source.get("digest") != actual_digest
|
|
1152
|
+
):
|
|
1153
|
+
raise Operational("BLOCKED", "run id already belongs to another repository or source")
|
|
1154
|
+
if existing.get("binding") != binding or existing.get("egress") != egress:
|
|
1155
|
+
raise Operational(
|
|
1156
|
+
"BLOCKED",
|
|
1157
|
+
"run id binding or egress sanction differs from the recorded fixed contract; resume with the recorded contract or choose a new run id",
|
|
1158
|
+
)
|
|
1159
|
+
return "READY", {
|
|
1160
|
+
"run_id": rid,
|
|
1161
|
+
"revision": existing["revision"],
|
|
1162
|
+
"resumed": True,
|
|
1163
|
+
"source_kind": source_kind,
|
|
1164
|
+
"source_digest": actual_digest,
|
|
1165
|
+
"recovery_path": rd,
|
|
1166
|
+
}
|
|
1167
|
+
validate_private_dir(rd)
|
|
1168
|
+
for child in ("units", "jobs", "packets", "source"):
|
|
1169
|
+
ensure_private_dir(os.path.join(rd, child))
|
|
1170
|
+
if source_kind == "prompt":
|
|
1171
|
+
create_private(os.path.join(rd, source_record["path"]), source_bytes)
|
|
1172
|
+
create_private(os.path.join(rd, "manifest.lock"), b"")
|
|
1173
|
+
created = now_iso()
|
|
1174
|
+
doc = {
|
|
1175
|
+
"schema_version": SCHEMA_VERSION,
|
|
1176
|
+
"revision": 0,
|
|
1177
|
+
"run_id": rid,
|
|
1178
|
+
"created_at": created,
|
|
1179
|
+
"updated_at": created,
|
|
1180
|
+
"repository": {k: info[k] for k in ("toplevel", "git_dir", "common_dir", "common_dev", "common_ino", "identity_digest")},
|
|
1181
|
+
"branch": {"ref": info["branch_ref"], "initial_head": info["head"]},
|
|
1182
|
+
"source": source_record,
|
|
1183
|
+
"plan": {
|
|
1184
|
+
"kind": source_kind,
|
|
1185
|
+
"path": plan_rel if source_kind == "plan" else None,
|
|
1186
|
+
"digest": actual_digest,
|
|
1187
|
+
"checkpoint": None,
|
|
1188
|
+
},
|
|
1189
|
+
"binding": binding,
|
|
1190
|
+
"egress": egress,
|
|
1191
|
+
"integration_lock": None,
|
|
1192
|
+
"units": {},
|
|
1193
|
+
"verification_attempts": [],
|
|
1194
|
+
"verifications": [],
|
|
1195
|
+
"blockers": [],
|
|
1196
|
+
"events": [{"at": created, "kind": "run-created"}],
|
|
1197
|
+
}
|
|
1198
|
+
create_private(os.path.join(rd, "manifest.json"), (json.dumps(doc, sort_keys=True, separators=(",", ":")) + "\n").encode())
|
|
1199
|
+
return "READY", {
|
|
1200
|
+
"run_id": rid,
|
|
1201
|
+
"revision": 0,
|
|
1202
|
+
"resumed": False,
|
|
1203
|
+
"source_kind": source_kind,
|
|
1204
|
+
"source_digest": actual_digest,
|
|
1205
|
+
"recovery_path": rd,
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def status_paths(repo: str) -> set[str]:
|
|
1210
|
+
raw = git(repo, "status", "--porcelain=v1", "-z", "--untracked-files=all")
|
|
1211
|
+
parts = raw.split(b"\0")
|
|
1212
|
+
paths: set[str] = set()
|
|
1213
|
+
i = 0
|
|
1214
|
+
while i < len(parts):
|
|
1215
|
+
entry = parts[i]
|
|
1216
|
+
i += 1
|
|
1217
|
+
if not entry:
|
|
1218
|
+
continue
|
|
1219
|
+
if len(entry) < 4:
|
|
1220
|
+
raise Operational("BLOCKED", "unexpected porcelain status record")
|
|
1221
|
+
code = entry[:2]
|
|
1222
|
+
paths.add(entry[3:].decode("utf-8", "surrogateescape"))
|
|
1223
|
+
if b"R" in code or b"C" in code:
|
|
1224
|
+
if i >= len(parts) or not parts[i]:
|
|
1225
|
+
raise Operational("BLOCKED", "incomplete rename status record")
|
|
1226
|
+
paths.add(parts[i].decode("utf-8", "surrogateescape"))
|
|
1227
|
+
i += 1
|
|
1228
|
+
return paths
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def reconcile_plan_checkpoint(repo: str, doc: dict, info: dict, plan_rel: str) -> dict | None:
|
|
1232
|
+
"""Recover the controller's plan commit when its manifest receipt was interrupted."""
|
|
1233
|
+
prior = doc.get("branch", {}).get("initial_head")
|
|
1234
|
+
commit = info["head"]
|
|
1235
|
+
if commit == prior:
|
|
1236
|
+
return None
|
|
1237
|
+
lineage = git_text(repo, "rev-list", "--parents", "-n", "1", commit).split()
|
|
1238
|
+
changed = set(filter(None, git(repo, "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", commit).decode("utf-8", "surrogateescape").split("\0")))
|
|
1239
|
+
message = git(repo, "show", "-s", "--format=%B", commit).decode("utf-8", "surrogateescape").rstrip("\n")
|
|
1240
|
+
plan_bytes = git(repo, "show", f"{commit}:{plan_rel}", check=False)
|
|
1241
|
+
if (
|
|
1242
|
+
not _valid_git_object_id(prior)
|
|
1243
|
+
or lineage != [commit, prior]
|
|
1244
|
+
or changed != {plan_rel}
|
|
1245
|
+
or message != PLAN_CHECKPOINT_MESSAGE
|
|
1246
|
+
or digest_bytes(plan_bytes) != doc["plan"]["digest"]
|
|
1247
|
+
):
|
|
1248
|
+
raise Operational(
|
|
1249
|
+
"BLOCKED",
|
|
1250
|
+
"canonical HEAD advanced without a recorded matching plan checkpoint",
|
|
1251
|
+
{"expected_prior_head": prior, "head": commit},
|
|
1252
|
+
)
|
|
1253
|
+
committed_at = int(git_text(repo, "show", "-s", "--format=%ct", commit))
|
|
1254
|
+
return {
|
|
1255
|
+
"prior_head": prior,
|
|
1256
|
+
"commit": commit,
|
|
1257
|
+
"tree": info["head_tree"],
|
|
1258
|
+
"path": plan_rel,
|
|
1259
|
+
"digest": doc["plan"]["digest"],
|
|
1260
|
+
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(committed_at)),
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def cmd_checkpoint_plan(args) -> tuple[str, dict]:
|
|
1265
|
+
with locked_manifest(args.run_id, write=True) as doc:
|
|
1266
|
+
info = validate_repo(doc)
|
|
1267
|
+
repo = info["toplevel"]
|
|
1268
|
+
plan = doc.get("plan")
|
|
1269
|
+
if not isinstance(plan, dict) or plan.get("kind", "plan") != "plan" or not plan.get("path"):
|
|
1270
|
+
dirty = status_paths(repo)
|
|
1271
|
+
if dirty:
|
|
1272
|
+
raise Operational("BLOCKED", "prompt-backed external execution requires a clean canonical checkout", {"dirty_paths": sorted(dirty)})
|
|
1273
|
+
return "NOOP", {"checkpoint": None, "head": info["head"], "source_kind": "prompt"}
|
|
1274
|
+
plan_rel = plan["path"]
|
|
1275
|
+
plan_abs, _ = resolve_plan(repo, plan_rel)
|
|
1276
|
+
if digest_bytes(Path(plan_abs).read_bytes()) != doc["plan"]["digest"]:
|
|
1277
|
+
raise Operational("BLOCKED", "selected plan content no longer matches recorded digest")
|
|
1278
|
+
dirty = status_paths(repo)
|
|
1279
|
+
if not dirty:
|
|
1280
|
+
checkpoint = doc["plan"].get("checkpoint")
|
|
1281
|
+
if checkpoint is not None:
|
|
1282
|
+
return "NOOP", {"checkpoint": checkpoint, "head": info["head"]}
|
|
1283
|
+
checkpoint = reconcile_plan_checkpoint(repo, doc, info, plan_rel)
|
|
1284
|
+
if checkpoint is None:
|
|
1285
|
+
return "NOOP", {"checkpoint": None, "head": info["head"]}
|
|
1286
|
+
doc["plan"]["checkpoint"] = checkpoint
|
|
1287
|
+
event(doc, "plan-checkpoint", detail={"commit": checkpoint["commit"], "path": plan_rel})
|
|
1288
|
+
return "CHECKPOINTED", {"checkpoint": checkpoint}
|
|
1289
|
+
if dirty != {plan_rel}:
|
|
1290
|
+
raise Operational("BLOCKED", "canonical dirt is not exactly the selected plan", {"dirty_paths": sorted(dirty)})
|
|
1291
|
+
prior = info["head"]
|
|
1292
|
+
git(repo, "add", "--", plan_rel)
|
|
1293
|
+
staged = set(filter(None, git(repo, "diff", "--cached", "--name-only", "-z").decode("utf-8", "surrogateescape").split("\0")))
|
|
1294
|
+
if staged != {plan_rel}:
|
|
1295
|
+
git(repo, "reset", "--mixed", prior)
|
|
1296
|
+
raise Operational("BLOCKED", "staged paths are not exactly the selected plan")
|
|
1297
|
+
try:
|
|
1298
|
+
commit_index_tree(repo, PLAN_CHECKPOINT_MESSAGE)
|
|
1299
|
+
except Operational:
|
|
1300
|
+
git(repo, "reset", "--mixed", prior, check=False)
|
|
1301
|
+
raise
|
|
1302
|
+
commit = git_text(repo, "rev-parse", "HEAD")
|
|
1303
|
+
test_fault("checkpoint-plan-after-commit")
|
|
1304
|
+
if status_paths(repo):
|
|
1305
|
+
raise Operational("BLOCKED", "checkpoint committed but canonical checkout is not clean")
|
|
1306
|
+
cp = {"prior_head": prior, "commit": commit, "tree": git_text(repo, "rev-parse", "HEAD^{tree}"), "path": plan_rel, "digest": doc["plan"]["digest"], "at": now_iso()}
|
|
1307
|
+
with locked_manifest(args.run_id, write=True) as doc:
|
|
1308
|
+
validate_repo(doc)
|
|
1309
|
+
doc["plan"]["checkpoint"] = cp
|
|
1310
|
+
event(doc, "plan-checkpoint", detail={"commit": commit, "path": plan_rel})
|
|
1311
|
+
return "CHECKPOINTED", {"checkpoint": cp}
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
@contextlib.contextmanager
|
|
1315
|
+
def admin_lock(common_dir: str):
|
|
1316
|
+
root = ensure_root()
|
|
1317
|
+
key = digest_bytes(os.path.realpath(common_dir).encode())
|
|
1318
|
+
path = os.path.join(root, ".locks", f"worktree-{key}.lock")
|
|
1319
|
+
try:
|
|
1320
|
+
create_private(path, b"")
|
|
1321
|
+
except Operational:
|
|
1322
|
+
pass
|
|
1323
|
+
data = read_private(path, 64)
|
|
1324
|
+
del data
|
|
1325
|
+
fd = os.open(path, os.O_RDWR | O_NOFOLLOW)
|
|
1326
|
+
try:
|
|
1327
|
+
_lock_fd(fd, exclusive=True)
|
|
1328
|
+
yield
|
|
1329
|
+
finally:
|
|
1330
|
+
_unlock_fd(fd)
|
|
1331
|
+
os.close(fd)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def worktree_rows(repo: str) -> list[dict]:
|
|
1335
|
+
raw = git_text(repo, "worktree", "list", "--porcelain")
|
|
1336
|
+
rows, row = [], {}
|
|
1337
|
+
for line in raw.splitlines() + [""]:
|
|
1338
|
+
if not line:
|
|
1339
|
+
if row:
|
|
1340
|
+
rows.append(row)
|
|
1341
|
+
row = {}
|
|
1342
|
+
continue
|
|
1343
|
+
key, _, value = line.partition(" ")
|
|
1344
|
+
row[key] = value if value else True
|
|
1345
|
+
return rows
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def validate_workspace(doc: dict, unit: dict) -> dict:
|
|
1349
|
+
repo = doc["repository"]["toplevel"]
|
|
1350
|
+
workspace = unit["workspace"]["path"]
|
|
1351
|
+
owned = os.path.join(run_dir(doc["run_id"]), "units", unit["unit_id"])
|
|
1352
|
+
if os.path.commonpath([os.path.realpath(workspace), os.path.realpath(owned)]) != os.path.realpath(owned):
|
|
1353
|
+
raise Operational("BLOCKED", "workspace escaped its owned unit directory")
|
|
1354
|
+
validate_private_dir(workspace)
|
|
1355
|
+
matches = [r for r in worktree_rows(repo) if os.path.realpath(str(r.get("worktree", ""))) == os.path.realpath(workspace)]
|
|
1356
|
+
if len(matches) != 1:
|
|
1357
|
+
raise Operational("BLOCKED", "workspace is not registered exactly once")
|
|
1358
|
+
if "detached" not in matches[0]:
|
|
1359
|
+
raise Operational("BLOCKED", "unit workspace is not detached")
|
|
1360
|
+
common = os.path.realpath(git_text(workspace, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
1361
|
+
if common != doc["repository"]["common_dir"]:
|
|
1362
|
+
raise Operational("BLOCKED", "unit workspace belongs to another repository")
|
|
1363
|
+
return matches[0]
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def validate_pristine_unit_base(doc: dict, unit: dict) -> dict:
|
|
1367
|
+
row = validate_workspace(doc, unit)
|
|
1368
|
+
workspace = unit["workspace"]["path"]
|
|
1369
|
+
base = unit["workspace"]["base"]
|
|
1370
|
+
if git_text(workspace, "rev-parse", "HEAD") != base:
|
|
1371
|
+
raise Operational("BLOCKED", "unit workspace HEAD no longer equals the recorded base")
|
|
1372
|
+
dirty = status_paths(workspace)
|
|
1373
|
+
if dirty:
|
|
1374
|
+
raise Operational(
|
|
1375
|
+
"BLOCKED",
|
|
1376
|
+
"unit workspace is dirty before dispatch authorization",
|
|
1377
|
+
{"dirty_paths": sorted(dirty)},
|
|
1378
|
+
)
|
|
1379
|
+
return row
|