@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.13
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/CHANGELOG.md +661 -0
- package/README.md +142 -6
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +2 -2
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +269 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +211 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +238 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +181 -16
- package/docs/obsidian-contract.md +223 -15
- package/docs/obsidian-plugin.md +38 -4
- package/docs/obsidian.md +613 -51
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +257 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +49 -0
- package/docs/repo-boundary-guard.md +94 -0
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +317 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +47 -2
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +75 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +59 -5
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +4 -2
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +52 -10
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +65 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +754 -63
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +30 -0
- package/src/coordination/index.mjs +110 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +17 -1
- package/src/graph/knowledge-graph.mjs +58 -8
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +44 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +385 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +10 -2
- package/src/projection/obsidian/publication/publisher.mjs +142 -13
- package/src/projection/obsidian/publication/vault-list.mjs +4 -3
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/store.mjs +160 -7
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-production-seams.mjs +48 -16
- package/src/runtime/obsidian/app-registration.mjs +175 -28
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +163 -20
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +10 -7
- package/src/runtime/obsidian/launch-plan.mjs +7 -4
- package/src/runtime/obsidian/lifecycle.mjs +92 -12
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +171 -37
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +11 -3
- package/src/runtime/obsidian/plugin-choice.mjs +16 -4
- package/src/runtime/obsidian/plugin-drift.mjs +59 -11
- package/src/runtime/obsidian/plugin-presence.mjs +14 -1
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +99 -16
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +87 -23
- package/src/runtime/obsidian/service.mjs +36 -3
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/server/local-sidecar.mjs +169 -2
- package/src/server/server.mjs +28 -2
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +1 -0
- package/templates/external-project-workspace/pack-lifecycle.json +6 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- package/templates/knowledge-workspace/repo-access.v1.json +9 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { canonicalize } from '../attestation/jcs.mjs'
|
|
6
|
+
import { verifyDocument } from '../attestation/sign.mjs'
|
|
7
|
+
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
8
|
+
|
|
9
|
+
// Owner-signed delegation of the local boundary actor check (FD-BOUND-1).
|
|
10
|
+
// A delegation lets one declared operator pass the private-domain actor check
|
|
11
|
+
// for named repositories and operations. It never changes ownership, audiences,
|
|
12
|
+
// paths, content rules, promotion, push-content approval or publication.
|
|
13
|
+
//
|
|
14
|
+
// Guarantee: attribution integrity for a cooperating operator. The actor check
|
|
15
|
+
// itself stays attribution-grade: an operator can still declare another actor
|
|
16
|
+
// (--actor, MNSTRY_ATELIER_ACTOR), edit the policy or project configuration, or
|
|
17
|
+
// control the process that runs the check. A delegation adds that an owner's
|
|
18
|
+
// signed, current, host-anchored consent is recorded when someone else commits.
|
|
19
|
+
//
|
|
20
|
+
// Trust: the policy and the delegations document are operator-writable, so a
|
|
21
|
+
// delegation is authority only when its signature verifies against an owner key
|
|
22
|
+
// in the host file at a fixed path, owned by root in root-owned directories. That
|
|
23
|
+
// file is also the only source of revocation. Each delegation binds the whole
|
|
24
|
+
// policy (ext excluded), the managed repository set and each repository's root
|
|
25
|
+
// commit, so it cannot be loosened in place or replayed into an independently
|
|
26
|
+
// created repository. Forks sharing the root commit are not distinguished.
|
|
27
|
+
// `ext` is accepted and never read.
|
|
28
|
+
|
|
29
|
+
export const DELEGATION_SCHEMA = 'atelier-boundary-delegation@v1'
|
|
30
|
+
export const DELEGATIONS_DOCUMENT_SCHEMA = 'atelier-boundary-delegations@v1'
|
|
31
|
+
export const OWNER_KEYS_SCHEMA = 'atelier-boundary-owner-keys@v1'
|
|
32
|
+
export const DELEGATION_OPERATIONS = Object.freeze(['boundary-check', 'pre-commit'])
|
|
33
|
+
export const MAX_DELEGATION_SPAN_MS = 90 * 24 * 60 * 60 * 1000
|
|
34
|
+
export const HOST_OWNER_KEYS_PATH = '/etc/atelier/boundary-owner-keys.json'
|
|
35
|
+
|
|
36
|
+
const DELEGATION_KEYS = ['schema', 'id', 'owner', 'operator', 'repos', 'operations', 'notBefore', 'expiresAt', 'policyDigest', 'repoRoots', 'signature', 'ext']
|
|
37
|
+
const VERSION_RE = /^1\.[0-9]+\.[0-9]+$/
|
|
38
|
+
const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/
|
|
39
|
+
const REPO_RE = /^[a-z0-9][a-z0-9._-]*$/
|
|
40
|
+
// Owner and operator ids are shown to the signing owner, so they are restricted
|
|
41
|
+
// to printable identifiers: no control characters, spaces or lookalikes.
|
|
42
|
+
export const DELEGATION_ACTOR_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
43
|
+
const UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
|
|
44
|
+
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/
|
|
45
|
+
const COMMIT_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/
|
|
46
|
+
const JWK_X_RE = /^[A-Za-z0-9_-]{43}$/
|
|
47
|
+
const OWNER_KEYS_MAX_BYTES = 64 * 1024
|
|
48
|
+
const MAX_DELEGATIONS = 64
|
|
49
|
+
const MAX_REPOS = 64
|
|
50
|
+
const MAX_KEYS = 64
|
|
51
|
+
const MAX_REVOKED = 1024
|
|
52
|
+
|
|
53
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
54
|
+
const boundedString = (value, max = 128) => typeof value === 'string' && value.length > 0 && value.length <= max
|
|
55
|
+
// A time must be a real calendar instant that round-trips: 2026-02-31 is refused,
|
|
56
|
+
// never rolled over to March.
|
|
57
|
+
const timeOf = (value) => {
|
|
58
|
+
if (typeof value !== 'string' || !UTC_RE.test(value)) return Number.NaN
|
|
59
|
+
const ms = Date.parse(value)
|
|
60
|
+
if (Number.isNaN(ms)) return Number.NaN
|
|
61
|
+
const iso = new Date(ms).toISOString()
|
|
62
|
+
return iso === value || iso.replace(/\.000Z$/, 'Z') === value || iso.replace(/0*Z$/, 'Z').replace(/\.Z$/, 'Z') === value ? ms : Number.NaN
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `ext` is never read, so it is removed before hashing, but only at the
|
|
66
|
+
// extension positions the schema declares. A repository or actor that happens
|
|
67
|
+
// to be named `ext` is ordinary policy and stays bound.
|
|
68
|
+
const dropExt = (value) => {
|
|
69
|
+
if (!isObject(value)) return value
|
|
70
|
+
const { ext, ...rest } = value
|
|
71
|
+
return rest
|
|
72
|
+
}
|
|
73
|
+
function withoutExt(policy) {
|
|
74
|
+
if (!isObject(policy)) return policy ?? null
|
|
75
|
+
const out = dropExt(policy)
|
|
76
|
+
const each = (map) => (isObject(map) ? Object.fromEntries(Object.entries(map).map(([key, item]) => [key, dropExt(item)])) : map)
|
|
77
|
+
if (Object.hasOwn(out, 'repos')) out.repos = each(out.repos)
|
|
78
|
+
if (Object.hasOwn(out, 'actors')) out.actors = each(out.actors)
|
|
79
|
+
if (Object.hasOwn(out, 'promotion')) out.promotion = dropExt(out.promotion)
|
|
80
|
+
if (Array.isArray(out.contentRules)) out.contentRules = out.contentRules.map(dropExt)
|
|
81
|
+
if (Array.isArray(out.contentRuleExceptions)) out.contentRuleExceptions = out.contentRuleExceptions.map(dropExt)
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const managedRepoNames = (project) => (project?.repos ?? []).filter((repo) => !repo.external).map((repo) => repo.name).sort()
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The digest a delegation binds: the whole boundary policy (ext excluded) and the
|
|
89
|
+
* project's managed repository names. Any later edit to them voids every
|
|
90
|
+
* delegation until the owner signs again. Other project configuration (repo
|
|
91
|
+
* access, graph settings, repository paths) is not bound.
|
|
92
|
+
*/
|
|
93
|
+
export function policyDigest(policy, project) {
|
|
94
|
+
const bound = { policy: withoutExt(policy), managedRepos: managedRepoNames(project) }
|
|
95
|
+
return `sha256:${crypto.createHash('sha256').update(canonicalize(bound), 'utf8').digest('hex')}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The repository's identity: the lowest-sorting root commit of HEAD, ignoring
|
|
100
|
+
* replace refs, or null when it has none. Forks and histories built on the same
|
|
101
|
+
* root share it; independently created repositories do not.
|
|
102
|
+
*/
|
|
103
|
+
export function repoRootCommit(repoPath, { gitExecutable = 'git' } = {}) {
|
|
104
|
+
if (!repoPath) return null
|
|
105
|
+
const result = spawnSync(gitExecutable, ['--no-replace-objects', '-C', repoPath, 'rev-list', '--max-parents=0', 'HEAD', '--'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
|
|
106
|
+
if (result.status !== 0) return null
|
|
107
|
+
const roots = result.stdout.split('\n').map((line) => line.trim()).filter((line) => COMMIT_RE.test(line)).sort()
|
|
108
|
+
return roots[0] ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function repoPathFor(project, name) {
|
|
112
|
+
return (project?.repos ?? []).find((repo) => repo.name === name && !repo.external)?.path ?? null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function delegationErrors(item, at, { actors, repos }) {
|
|
116
|
+
if (!isObject(item)) return [`${at} must be an object`]
|
|
117
|
+
const errors = []
|
|
118
|
+
for (const key of Object.keys(item)) {
|
|
119
|
+
if (!DELEGATION_KEYS.includes(key)) errors.push(`${at} must not include additional property ${key}`)
|
|
120
|
+
}
|
|
121
|
+
if (item.ext != null && !isObject(item.ext)) errors.push(`${at}.ext must be an object`)
|
|
122
|
+
if (item.schema !== DELEGATION_SCHEMA) errors.push(`${at}.schema must be ${DELEGATION_SCHEMA}`)
|
|
123
|
+
if (typeof item.id !== 'string' || !ID_RE.test(item.id)) errors.push(`${at}.id is invalid`)
|
|
124
|
+
if (typeof item.owner !== 'string' || !DELEGATION_ACTOR_RE.test(item.owner) || !Object.hasOwn(actors, item.owner)) errors.push(`${at}.owner must be a declared actor with a plain identifier`)
|
|
125
|
+
if (typeof item.operator !== 'string' || !DELEGATION_ACTOR_RE.test(item.operator) || !Object.hasOwn(actors, item.operator)) errors.push(`${at}.operator must be a declared actor with a plain identifier`)
|
|
126
|
+
if (item.owner === item.operator) errors.push(`${at}.operator must differ from the owner`)
|
|
127
|
+
const repoNames = Array.isArray(item.repos) ? item.repos : []
|
|
128
|
+
if (!repoNames.length || repoNames.length > MAX_REPOS || new Set(repoNames).size !== repoNames.length) {
|
|
129
|
+
errors.push(`${at}.repos must be a non-empty list of at most ${MAX_REPOS} without duplicates`)
|
|
130
|
+
}
|
|
131
|
+
for (const name of repoNames) {
|
|
132
|
+
const repo = typeof name === 'string' && REPO_RE.test(name) && Object.hasOwn(repos, name) ? repos[name] : null
|
|
133
|
+
if (!repo || repo.kind !== 'private_domain') errors.push(`${at}.repos ${String(name)} must be a declared private_domain repo`)
|
|
134
|
+
else if (repo.ownerActor !== item.owner) errors.push(`${at}.repos ${name} is not owned by ${String(item.owner)}`)
|
|
135
|
+
}
|
|
136
|
+
const operations = Array.isArray(item.operations) ? item.operations : []
|
|
137
|
+
if (!operations.length || new Set(operations).size !== operations.length || operations.some((op) => !DELEGATION_OPERATIONS.includes(op))) {
|
|
138
|
+
errors.push(`${at}.operations must be a non-empty list of ${DELEGATION_OPERATIONS.join(', ')}`)
|
|
139
|
+
}
|
|
140
|
+
const notBefore = timeOf(item.notBefore)
|
|
141
|
+
const expiresAt = timeOf(item.expiresAt)
|
|
142
|
+
if (Number.isNaN(notBefore)) errors.push(`${at}.notBefore must be an RFC 3339 UTC time`)
|
|
143
|
+
if (Number.isNaN(expiresAt)) errors.push(`${at}.expiresAt must be an RFC 3339 UTC time`)
|
|
144
|
+
if (!Number.isNaN(notBefore) && !Number.isNaN(expiresAt) && !(expiresAt > notBefore && expiresAt - notBefore <= MAX_DELEGATION_SPAN_MS)) {
|
|
145
|
+
errors.push(`${at}.expiresAt must be after notBefore and at most 90 days later`)
|
|
146
|
+
}
|
|
147
|
+
if (typeof item.policyDigest !== 'string' || !DIGEST_RE.test(item.policyDigest)) errors.push(`${at}.policyDigest must be a sha256 digest`)
|
|
148
|
+
const roots = item.repoRoots
|
|
149
|
+
if (!isObject(roots) || Object.keys(roots).length !== repoNames.length ||
|
|
150
|
+
!repoNames.every((name) => typeof name === 'string' && Object.hasOwn(roots, name) && typeof roots[name] === 'string' && COMMIT_RE.test(roots[name]))) {
|
|
151
|
+
errors.push(`${at}.repoRoots must hold one root commit per listed repo`)
|
|
152
|
+
}
|
|
153
|
+
const signature = item.signature
|
|
154
|
+
if (!isObject(signature) || signature.algorithm !== 'ed25519' || !boundedString(signature.keyId) || !boundedString(signature.value, 512) ||
|
|
155
|
+
Object.keys(signature).some((key) => !['algorithm', 'keyId', 'value', 'ext'].includes(key))) {
|
|
156
|
+
errors.push(`${at}.signature must be an ed25519 signature; an unsigned delegation grants nothing`)
|
|
157
|
+
}
|
|
158
|
+
return errors
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Errors for one delegation checked against a policy (used before signing). */
|
|
162
|
+
export function validateDelegation(item, policy) {
|
|
163
|
+
return delegationErrors(item, 'delegation', { actors: isObject(policy?.actors) ? policy.actors : {}, repos: isObject(policy?.repos) ? policy.repos : {} })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Errors for a delegations document, checked against the boundary policy it
|
|
168
|
+
* delegates under. Delegations live in their own document beside the policy,
|
|
169
|
+
* so the policy contract is not widened.
|
|
170
|
+
*/
|
|
171
|
+
export function validateDelegationsDocument(doc, policy) {
|
|
172
|
+
if (!isObject(doc)) return ['delegations document must be a JSON object']
|
|
173
|
+
const errors = []
|
|
174
|
+
for (const key of Object.keys(doc)) {
|
|
175
|
+
if (!['schema', 'contractVersion', 'delegations', 'ext'].includes(key)) errors.push(`delegations document must not include additional property ${key}`)
|
|
176
|
+
}
|
|
177
|
+
if (doc.schema !== DELEGATIONS_DOCUMENT_SCHEMA) errors.push(`delegations document schema must be ${DELEGATIONS_DOCUMENT_SCHEMA}`)
|
|
178
|
+
if (doc.contractVersion != null && !(typeof doc.contractVersion === 'string' && VERSION_RE.test(doc.contractVersion))) errors.push('delegations document contractVersion must be a 1.x.y version')
|
|
179
|
+
if (doc.ext != null && !isObject(doc.ext)) errors.push('delegations document ext must be an object')
|
|
180
|
+
if (!Array.isArray(doc.delegations)) return [...errors, 'delegations document must list delegations']
|
|
181
|
+
if (doc.delegations.length > MAX_DELEGATIONS) errors.push(`delegations document lists more than ${MAX_DELEGATIONS} delegations`)
|
|
182
|
+
const context = { actors: isObject(policy?.actors) ? policy.actors : {}, repos: isObject(policy?.repos) ? policy.repos : {} }
|
|
183
|
+
const seen = new Set()
|
|
184
|
+
for (const [index, item] of doc.delegations.entries()) {
|
|
185
|
+
const at = `delegations[${index}]`
|
|
186
|
+
errors.push(...delegationErrors(item, at, context))
|
|
187
|
+
if (isObject(item) && typeof item.id === 'string') {
|
|
188
|
+
if (seen.has(item.id)) errors.push(`${at}.id ${item.id} is duplicated`)
|
|
189
|
+
seen.add(item.id)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return errors
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ownerKeysErrors(doc) {
|
|
196
|
+
if (!isObject(doc)) return ['owner keys must be a JSON object']
|
|
197
|
+
const errors = []
|
|
198
|
+
for (const key of Object.keys(doc)) {
|
|
199
|
+
if (!['schema', 'contractVersion', 'keys', 'revokedDelegations', 'ext'].includes(key)) errors.push(`owner keys must not include additional property ${key}`)
|
|
200
|
+
}
|
|
201
|
+
if (doc.schema !== OWNER_KEYS_SCHEMA) errors.push(`owner keys schema must be ${OWNER_KEYS_SCHEMA}`)
|
|
202
|
+
if (doc.contractVersion != null && !(typeof doc.contractVersion === 'string' && VERSION_RE.test(doc.contractVersion))) errors.push('owner keys contractVersion must be a 1.x.y version')
|
|
203
|
+
if (doc.ext != null && !isObject(doc.ext)) errors.push('owner keys ext must be an object')
|
|
204
|
+
if (!Array.isArray(doc.keys) || doc.keys.length > MAX_KEYS) errors.push(`owner keys must list at most ${MAX_KEYS} keys`)
|
|
205
|
+
const seen = new Set()
|
|
206
|
+
for (const [index, key] of (Array.isArray(doc.keys) ? doc.keys : []).entries()) {
|
|
207
|
+
if (!isObject(key) || Object.keys(key).some((name) => !['actorId', 'keyId', 'algorithm', 'publicKeyJwk', 'ext'].includes(name)) ||
|
|
208
|
+
(key.ext != null && !isObject(key.ext)) || !boundedString(key.actorId) || !boundedString(key.keyId) || key.algorithm !== 'ed25519' ||
|
|
209
|
+
!isObject(key.publicKeyJwk) || key.publicKeyJwk.kty !== 'OKP' || key.publicKeyJwk.crv !== 'Ed25519' ||
|
|
210
|
+
typeof key.publicKeyJwk.x !== 'string' || !JWK_X_RE.test(key.publicKeyJwk.x) || Object.hasOwn(key.publicKeyJwk, 'd')) {
|
|
211
|
+
errors.push(`owner keys keys[${index}] must be an ed25519 public key with actorId and keyId`)
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
const identity = `${key.actorId}\u0000${key.keyId}`
|
|
215
|
+
if (seen.has(identity)) errors.push(`owner keys keys[${index}] duplicates ${key.actorId}/${key.keyId}`)
|
|
216
|
+
seen.add(identity)
|
|
217
|
+
}
|
|
218
|
+
const revoked = doc.revokedDelegations
|
|
219
|
+
if (revoked != null && (!Array.isArray(revoked) || revoked.length > MAX_REVOKED || new Set(revoked).size !== revoked.length ||
|
|
220
|
+
revoked.some((id) => typeof id !== 'string' || !ID_RE.test(id)))) {
|
|
221
|
+
errors.push('owner keys revokedDelegations must be a list of distinct delegation ids')
|
|
222
|
+
}
|
|
223
|
+
return errors
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Normalize a host-supplied owner-keys document. The caller is the trusted host. */
|
|
227
|
+
export function ownerKeysFromDocument(doc) {
|
|
228
|
+
const errors = ownerKeysErrors(doc)
|
|
229
|
+
if (errors.length) return { ok: false, reason: 'delegation-owner-keys-invalid', errors }
|
|
230
|
+
return { ok: true, keys: doc.keys, revoked: new Set(doc.revokedDelegations ?? []) }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The normalized form of a host `ownerKeys` value, re-validated. */
|
|
234
|
+
function normalizedOwnerKeys(ownerKeys) {
|
|
235
|
+
if (!isObject(ownerKeys)) return null
|
|
236
|
+
if (ownerKeys.ok === false) return ownerKeys
|
|
237
|
+
if (ownerKeys.ok === true && Array.isArray(ownerKeys.keys) && ownerKeys.revoked instanceof Set) {
|
|
238
|
+
return ownerKeysFromDocument({ schema: OWNER_KEYS_SCHEMA, keys: ownerKeys.keys, revokedDelegations: [...ownerKeys.revoked] })
|
|
239
|
+
}
|
|
240
|
+
return ownerKeysFromDocument(ownerKeys)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Load the host owner-keys file. It is trusted only when it is a regular file
|
|
245
|
+
* owned by the trusted account (root), not group- or world-writable, opened
|
|
246
|
+
* without following links, and every directory above it is owned by that
|
|
247
|
+
* account (or root) and not group- or world-writable. The checks run on the open
|
|
248
|
+
* descriptor, which is also what is read. The current account must not be the
|
|
249
|
+
* trusted one. Windows is refused until an owner and DACL check exists.
|
|
250
|
+
*/
|
|
251
|
+
export function loadOwnerKeysFile(file = HOST_OWNER_KEYS_PATH, { platform = process.platform, trustedUid = 0, currentUid = process.getuid?.() } = {}) {
|
|
252
|
+
const untrusted = (detail) => ({ ok: false, reason: 'delegation-owner-keys-untrusted', errors: [detail] })
|
|
253
|
+
if (platform === 'win32') return untrusted('owner keys are not supported on Windows')
|
|
254
|
+
if (typeof currentUid !== 'number') return untrusted('the current account cannot be determined')
|
|
255
|
+
if (currentUid === 0) return untrusted('owner keys cannot be trusted for the root account')
|
|
256
|
+
if (currentUid === trustedUid) return untrusted('the current account must not own the owner keys')
|
|
257
|
+
const resolved = path.resolve(file)
|
|
258
|
+
let fd
|
|
259
|
+
try {
|
|
260
|
+
fd = fs.openSync(resolved, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0))
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error?.code === 'ENOENT') return { ok: false, reason: 'delegation-owner-key-missing', errors: ['owner keys file not found'] }
|
|
263
|
+
if (error?.code === 'ELOOP') return untrusted('owner keys file must not be a symlink')
|
|
264
|
+
return untrusted('owner keys file cannot be opened')
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const stat = fs.fstatSync(fd)
|
|
268
|
+
if (!stat.isFile()) return untrusted('owner keys file must be a regular file')
|
|
269
|
+
if (stat.size > OWNER_KEYS_MAX_BYTES) return untrusted('owner keys file is too large')
|
|
270
|
+
if (stat.uid !== trustedUid) return untrusted('owner keys file must be owned by the trusted account')
|
|
271
|
+
if ((stat.mode & 0o022) !== 0) return untrusted('owner keys file must not be group- or world-writable')
|
|
272
|
+
let real
|
|
273
|
+
try {
|
|
274
|
+
real = fs.realpathSync(resolved)
|
|
275
|
+
const named = fs.lstatSync(real)
|
|
276
|
+
if (named.dev !== stat.dev || named.ino !== stat.ino) return untrusted('owner keys file changed while it was checked')
|
|
277
|
+
} catch {
|
|
278
|
+
return untrusted('owner keys file changed while it was checked')
|
|
279
|
+
}
|
|
280
|
+
for (let dir = path.dirname(real); ; dir = path.dirname(dir)) {
|
|
281
|
+
let info
|
|
282
|
+
try {
|
|
283
|
+
info = fs.statSync(dir)
|
|
284
|
+
} catch {
|
|
285
|
+
return untrusted('owner keys file directories changed while they were checked')
|
|
286
|
+
}
|
|
287
|
+
if ((info.uid !== trustedUid && info.uid !== 0) || (info.mode & 0o022) !== 0) {
|
|
288
|
+
return untrusted('owner keys file must be in directories owned by the trusted account and not group- or world-writable')
|
|
289
|
+
}
|
|
290
|
+
if (path.dirname(dir) === dir) break
|
|
291
|
+
}
|
|
292
|
+
let doc
|
|
293
|
+
try {
|
|
294
|
+
doc = JSON.parse(fs.readFileSync(fd, 'utf8'))
|
|
295
|
+
} catch {
|
|
296
|
+
return { ok: false, reason: 'delegation-owner-keys-invalid', errors: ['owner keys file is not valid JSON'] }
|
|
297
|
+
}
|
|
298
|
+
return ownerKeysFromDocument(doc)
|
|
299
|
+
} finally {
|
|
300
|
+
fs.closeSync(fd)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Owner keys for a CLI check: only the host file at its fixed path. The command
|
|
306
|
+
* line and environment cannot choose another file, so the host's keys and
|
|
307
|
+
* revocations always apply. Returns null when the host has none.
|
|
308
|
+
*/
|
|
309
|
+
export function ownerKeysForCommand() {
|
|
310
|
+
return fs.existsSync(HOST_OWNER_KEYS_PATH) || isSymlink(HOST_OWNER_KEYS_PATH) ? loadOwnerKeysFile(HOST_OWNER_KEYS_PATH) : null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isSymlink(file) {
|
|
314
|
+
try {
|
|
315
|
+
return fs.lstatSync(file).isSymbolicLink()
|
|
316
|
+
} catch {
|
|
317
|
+
return false
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The delegations document beside a boundary policy file. */
|
|
322
|
+
export function delegationsPathFor(policyPath) {
|
|
323
|
+
return path.join(path.dirname(policyPath), 'boundary-delegations.v1.json')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Read the delegations document beside the policy, if one exists. */
|
|
327
|
+
export function loadDelegationsFile(file) {
|
|
328
|
+
if (!fs.existsSync(file)) return { present: false, document: null, errors: [] }
|
|
329
|
+
let document
|
|
330
|
+
try {
|
|
331
|
+
document = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
332
|
+
} catch {
|
|
333
|
+
return { present: true, document: null, errors: ['delegations document is not valid JSON'] }
|
|
334
|
+
}
|
|
335
|
+
if (!isObject(document)) return { present: true, document: null, errors: ['delegations document must be a JSON object'] }
|
|
336
|
+
return { present: true, document, errors: [] }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Decide whether a delegation lets `actorId` pass the actor check for one
|
|
341
|
+
* repository and operation. Returns `{ applies: true, delegation }` or
|
|
342
|
+
* `{ applies: false, reason }`; it never widens anything else.
|
|
343
|
+
*/
|
|
344
|
+
export function resolveDelegation({ policy, project, delegations = [], repoName, repo, actorId, operation, ownerKeys = null, now = Date.now(), rootCommitOf = repoRootCommit }) {
|
|
345
|
+
const deny = (reason) => ({ applies: false, reason })
|
|
346
|
+
const all = Array.isArray(delegations) ? delegations.filter(isObject) : []
|
|
347
|
+
const forOperator = all.filter((item) => item.operator === actorId)
|
|
348
|
+
if (!forOperator.length) return deny('delegation-missing')
|
|
349
|
+
const forRepo = forOperator.filter((item) => Array.isArray(item.repos) && item.repos.includes(repoName))
|
|
350
|
+
const inScope = forRepo.filter((item) => Array.isArray(item.operations) && item.operations.includes(operation))
|
|
351
|
+
if (!inScope.length) return deny('delegation-scope')
|
|
352
|
+
if (inScope.length > 1) return deny('delegation-ambiguous')
|
|
353
|
+
const [delegation] = inScope
|
|
354
|
+
if (validateDelegation(delegation, policy).length || delegation.owner !== repo?.ownerActor) return deny('delegation-malformed')
|
|
355
|
+
const keys = normalizedOwnerKeys(ownerKeys)
|
|
356
|
+
if (!keys) return deny('delegation-owner-key-missing')
|
|
357
|
+
if (!keys.ok) return deny(keys.reason)
|
|
358
|
+
// The key must be registered for the delegation's owner, not merely present.
|
|
359
|
+
const key = keys.keys.find((item) => item.actorId === delegation.owner && item.keyId === delegation.signature.keyId)
|
|
360
|
+
if (!key) return deny('delegation-owner-key-missing')
|
|
361
|
+
if (!verifyDocument(delegation, { publicKey: key }).valid) return deny('delegation-signature-invalid')
|
|
362
|
+
if (keys.revoked.has(delegation.id)) return deny('delegation-revoked')
|
|
363
|
+
if (now < Date.parse(delegation.notBefore)) return deny('delegation-not-yet-valid')
|
|
364
|
+
if (now >= Date.parse(delegation.expiresAt)) return deny('delegation-expired')
|
|
365
|
+
if (delegation.policyDigest !== policyDigest(policy, project)) return deny('delegation-policy-changed')
|
|
366
|
+
for (const name of delegation.repos) {
|
|
367
|
+
if (rootCommitOf(repoPathFor(project, name)) !== delegation.repoRoots[name]) return deny('delegation-repository-changed')
|
|
368
|
+
}
|
|
369
|
+
return { applies: true, delegation }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The bound values a delegation must carry for this policy and project. */
|
|
373
|
+
export function delegationBindings({ policy, project, repos, rootCommitOf = repoRootCommit }) {
|
|
374
|
+
return {
|
|
375
|
+
policyDigest: policyDigest(policy, project),
|
|
376
|
+
repoRoots: Object.fromEntries((Array.isArray(repos) ? repos : []).map((name) => [name, rootCommitOf(repoPathFor(project, name))])),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** An unsigned delegation document for the owner to read and sign. */
|
|
381
|
+
export function draftDelegation({ policy, project, id, owner, operator, repos, operations, notBefore, expiresAt, rootCommitOf = repoRootCommit }) {
|
|
382
|
+
const doc = {
|
|
383
|
+
schema: DELEGATION_SCHEMA,
|
|
384
|
+
id,
|
|
385
|
+
owner,
|
|
386
|
+
operator,
|
|
387
|
+
repos,
|
|
388
|
+
operations,
|
|
389
|
+
notBefore,
|
|
390
|
+
expiresAt,
|
|
391
|
+
...delegationBindings({ policy, project, repos, rootCommitOf }),
|
|
392
|
+
signature: null,
|
|
393
|
+
}
|
|
394
|
+
const errors = validateDelegation({ ...doc, signature: { algorithm: 'ed25519', keyId: 'unsigned', value: 'unsigned' } }, policy)
|
|
395
|
+
return { ok: errors.length === 0, delegation: doc, errors }
|
|
396
|
+
}
|
package/src/boundary/policy.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
validateContentRuleExceptions,
|
|
16
16
|
validateContentRules,
|
|
17
17
|
} from './content-rules.mjs'
|
|
18
|
+
import { delegationsPathFor, loadDelegationsFile, ownerKeysForCommand, resolveDelegation, validateDelegationsDocument } from './delegation.mjs'
|
|
18
19
|
|
|
19
20
|
export const BOUNDARY_POLICY_SCHEMA = 'mnstry.atelier-boundary-policy@v1'
|
|
20
21
|
export const PROMOTE_EVENT_SCHEMA = 'git-promote-event@v1'
|
|
@@ -115,6 +116,12 @@ function unknownKeys(value, label, allowed) {
|
|
|
115
116
|
.map((key) => `${label} must not include additional property ${key}`)
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// The schema declares `ext` as a reserved extension container that consumers
|
|
120
|
+
// must ignore. It is accepted, never read, and never carries authority.
|
|
121
|
+
function extErrors(value, label) {
|
|
122
|
+
return value?.ext != null && !isObject(value.ext) ? [`${label}.ext must be an object`] : []
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
function validAudienceList(value, label, required = false) {
|
|
119
126
|
if (value == null && !required) return []
|
|
120
127
|
if (!Array.isArray(value)) return [`${label} must be a list`]
|
|
@@ -128,8 +135,12 @@ function validAudienceList(value, label, required = false) {
|
|
|
128
135
|
export function validateBoundaryPolicy(policy, project = null) {
|
|
129
136
|
const errors = []
|
|
130
137
|
if (!isObject(policy)) return ['boundary policy must be a JSON object']
|
|
131
|
-
errors.push(...unknownKeys(policy, '/', new Set(['schema', 'mode', 'actors', 'repos', 'promotion', 'forbiddenPaths', 'contentRules', 'contentRuleExceptions', 'governanceLedgerPath'])))
|
|
138
|
+
errors.push(...unknownKeys(policy, '/', new Set(['schema', 'contractVersion', 'mode', 'actors', 'repos', 'promotion', 'forbiddenPaths', 'contentRules', 'contentRuleExceptions', 'governanceLedgerPath', 'ext'])))
|
|
132
139
|
if (policy.schema !== BOUNDARY_POLICY_SCHEMA) errors.push(`schema must be ${BOUNDARY_POLICY_SCHEMA}`)
|
|
140
|
+
if (policy.contractVersion != null && !(typeof policy.contractVersion === 'string' && /^1\.[0-9]+\.[0-9]+$/.test(policy.contractVersion))) {
|
|
141
|
+
errors.push('contractVersion must be a 1.x.y version')
|
|
142
|
+
}
|
|
143
|
+
errors.push(...extErrors(policy, '/'))
|
|
133
144
|
if (!VALID_BOUNDARY_MODES.has(policy.mode)) errors.push('mode must be strict or legacy-warning')
|
|
134
145
|
|
|
135
146
|
const actors = policy.actors
|
|
@@ -138,7 +149,8 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
138
149
|
if (!isObject(actors)) errors.push('actors must be an object')
|
|
139
150
|
if (!isObject(repos)) errors.push('repos must be an object')
|
|
140
151
|
if (policy.promotion != null && !isObject(policy.promotion)) errors.push('promotion must be an object')
|
|
141
|
-
errors.push(...unknownKeys(promotion, 'promotion', new Set(['requiresGitPromote', 'recordsPath'])))
|
|
152
|
+
errors.push(...unknownKeys(promotion, 'promotion', new Set(['requiresGitPromote', 'recordsPath', 'ext'])))
|
|
153
|
+
errors.push(...extErrors(promotion, 'promotion'))
|
|
142
154
|
if (policy.forbiddenPaths != null && !Array.isArray(policy.forbiddenPaths)) errors.push('forbiddenPaths must be a list')
|
|
143
155
|
if (policy.contentRules != null) errors.push(...validateContentRules(policy.contentRules))
|
|
144
156
|
if (policy.contentRuleExceptions != null) {
|
|
@@ -153,11 +165,15 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
for (const [actorId, actor] of Object.entries(isObject(actors) ? actors : {})) {
|
|
156
|
-
errors.push(...unknownKeys(actor, `actors.${actorId}`, new Set(['githubLogin', 'gitEmails', 'privateDomainRepo'])))
|
|
168
|
+
errors.push(...unknownKeys(actor, `actors.${actorId}`, new Set(['githubLogin', 'gitEmails', 'privateDomainRepo', 'ext'])))
|
|
169
|
+
errors.push(...extErrors(actor, `actors.${actorId}`))
|
|
157
170
|
if (!isObject(actor)) {
|
|
158
171
|
errors.push(`actors.${actorId} must be an object`)
|
|
159
172
|
continue
|
|
160
173
|
}
|
|
174
|
+
// The schema types these fields; a non-string login would still match through String().
|
|
175
|
+
if (actor.githubLogin != null && typeof actor.githubLogin !== 'string') errors.push(`actors.${actorId}.githubLogin must be a string`)
|
|
176
|
+
if (actor.gitEmails != null && !Array.isArray(actor.gitEmails)) errors.push(`actors.${actorId}.gitEmails must be a list`)
|
|
161
177
|
if (!firstString(actor.githubLogin) && !asArray(actor.gitEmails).length) {
|
|
162
178
|
errors.push(`actors.${actorId} must declare githubLogin or gitEmails`)
|
|
163
179
|
}
|
|
@@ -176,7 +192,8 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
176
192
|
}
|
|
177
193
|
|
|
178
194
|
for (const [repoName, repo] of Object.entries(isObject(repos) ? repos : {})) {
|
|
179
|
-
errors.push(...unknownKeys(repo, `repos.${repoName}`, new Set(['kind', 'ownerActor', 'readBoundary', 'allowedAudiences', 'forbiddenAudiences', 'autoCommit'])))
|
|
195
|
+
errors.push(...unknownKeys(repo, `repos.${repoName}`, new Set(['kind', 'ownerActor', 'readBoundary', 'allowedAudiences', 'forbiddenAudiences', 'autoCommit', 'ext'])))
|
|
196
|
+
errors.push(...extErrors(repo, `repos.${repoName}`))
|
|
180
197
|
if (!isObject(repo)) {
|
|
181
198
|
errors.push(`repos.${repoName} must be an object`)
|
|
182
199
|
continue
|
|
@@ -306,7 +323,7 @@ function nodePlacementFindings({ node, policy }) {
|
|
|
306
323
|
return findings
|
|
307
324
|
}
|
|
308
325
|
|
|
309
|
-
function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }) {
|
|
326
|
+
function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors, operation, delegations, ownerKeys, now }) {
|
|
310
327
|
const findings = []
|
|
311
328
|
const operatedRepos = new Set(managedRepos(project).map((repo) => repo.name))
|
|
312
329
|
const ownedRepos = Object.entries(policy.repos ?? {}).filter(([name, repo]) =>
|
|
@@ -322,7 +339,20 @@ function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActo
|
|
|
322
339
|
findings.push(finding({ severity, code: 'private-domain-actor-unverified', repo: repoName,
|
|
323
340
|
message: `${repoName}: could not verify local actor for private domain repo owned by ${repo.ownerActor}${current.reason ? ` (${current.reason})` : ''}` }))
|
|
324
341
|
} else if (current.actorId !== repo.ownerActor) {
|
|
325
|
-
|
|
342
|
+
// Only an owner-signed delegation, verified against a host-supplied key,
|
|
343
|
+
// changes this outcome, and only for its operator, repos and operation.
|
|
344
|
+
const delegated = resolveDelegation({ policy, project, delegations, repoName, repo, actorId: current.actorId, operation, ownerKeys, now })
|
|
345
|
+
if (delegated.applies) {
|
|
346
|
+
const { id, owner, expiresAt } = delegated.delegation
|
|
347
|
+
findings.push(finding({ severity: 'info', code: 'private-domain-delegated-operator', repo: repoName,
|
|
348
|
+
message: `${repoName}: ${current.actorId} operates under delegation ${id} from owner ${owner} until ${expiresAt}`,
|
|
349
|
+
details: { actorId: current.actorId, owner, delegationId: id, expiresAt, operation } }))
|
|
350
|
+
} else {
|
|
351
|
+
const note = delegated.reason === 'delegation-missing' ? '' : ` (${delegated.reason})`
|
|
352
|
+
findings.push(finding({ severity, code: 'private-domain-actor-mismatch', repo: repoName,
|
|
353
|
+
message: `${repoName}: private domain repo is owned by ${repo.ownerActor}, but current actor is ${current.actorId}${note}`,
|
|
354
|
+
details: { delegationReason: delegated.reason } }))
|
|
355
|
+
}
|
|
326
356
|
}
|
|
327
357
|
}
|
|
328
358
|
return findings
|
|
@@ -635,7 +665,7 @@ function promotionFindings({ policy, project, graph }) {
|
|
|
635
665
|
return findings
|
|
636
666
|
}
|
|
637
667
|
|
|
638
|
-
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false } = {}) {
|
|
668
|
+
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false, delegations = null, ownerKeys = null, now = Date.now() } = {}) {
|
|
639
669
|
const validationErrors = validateBoundaryPolicy(policy, project)
|
|
640
670
|
let graph = null
|
|
641
671
|
const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
|
|
@@ -645,7 +675,14 @@ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnl
|
|
|
645
675
|
for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
|
|
646
676
|
findings.push(...promotionFindings({ policy, project, graph }))
|
|
647
677
|
}
|
|
648
|
-
|
|
678
|
+
// Delegations come from their own document, never from the policy. An invalid
|
|
679
|
+
// document is an error and grants nothing.
|
|
680
|
+
const delegationErrors = delegations == null ? [] : validateDelegationsDocument(delegations, policy)
|
|
681
|
+
findings.push(...delegationErrors.map((message) => finding({ severity: 'error', code: 'boundary-delegations-invalid', message })))
|
|
682
|
+
const delegationList = delegations != null && !delegationErrors.length ? delegations.delegations : []
|
|
683
|
+
// A staged check is the commit path (the installed pre-commit hook runs it).
|
|
684
|
+
const operation = staged ? 'pre-commit' : 'boundary-check'
|
|
685
|
+
findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors, operation, delegations: delegationList, ownerKeys, now }))
|
|
649
686
|
if (staged) {
|
|
650
687
|
const stagedPaths = stagedPathsForProject(project, { gitExecutable })
|
|
651
688
|
findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
|
|
@@ -673,9 +710,14 @@ export function runBoundaryCheckCommand(argv = process.argv.slice(2)) {
|
|
|
673
710
|
const project = commandProject({ argv })
|
|
674
711
|
const loaded = loadBoundaryPolicy(project)
|
|
675
712
|
const missingFindings = loaded.errors?.map((message) => finding({ code: 'boundary-policy-missing', message })) ?? []
|
|
676
|
-
const
|
|
677
|
-
|
|
713
|
+
const sidecar = loaded.ok ? loadDelegationsFile(delegationsPathFor(loaded.policyPath)) : { present: false, document: null, errors: [] }
|
|
714
|
+
const sidecarFindings = sidecar.errors.map((message) => finding({ code: 'boundary-delegations-invalid', message }))
|
|
715
|
+
// Owner keys are read only when a delegations document exists.
|
|
716
|
+
const ownerKeys = sidecar.present && sidecar.document ? ownerKeysForCommand() : null
|
|
717
|
+
let report = loaded.ok
|
|
718
|
+
? checkBoundaryPolicy({ project, policy: loaded.policy, staged, stagedOnly, actor: firstString(args.actor), delegations: sidecar.document, ownerKeys })
|
|
678
719
|
: { ok: false, mode: null, schema: BOUNDARY_POLICY_SCHEMA, graphCounts: null, findings: missingFindings, errors: missingFindings, warnings: [] }
|
|
720
|
+
if (sidecarFindings.length) report = { ...report, ok: false, findings: [...sidecarFindings, ...report.findings], errors: [...sidecarFindings, ...report.errors] }
|
|
679
721
|
|
|
680
722
|
if (json) {
|
|
681
723
|
console.log(JSON.stringify(report, null, 2))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { bytesAt, workspaceRoot, digest } from '../capabilities/files.mjs'
|
|
3
|
+
import { harnessRef } from '../harnesses/contracts.mjs'
|
|
4
|
+
import { inspectBuild } from './ledger.mjs'
|
|
5
|
+
|
|
6
|
+
// Read-only local adapter. A host still owns commands, writer custody and CI.
|
|
7
|
+
export function prepareGitCandidate({ workspaceRoot: input, records, artifact, writer }) {
|
|
8
|
+
const root = workspaceRoot(input), state = inspectBuild(records)
|
|
9
|
+
if (!state.establishment) throw new Error('build objective missing')
|
|
10
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')))
|
|
11
|
+
const git = args => execFileSync('git', ['-C', root, ...args], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
|
|
12
|
+
if (git(['status', '--porcelain', '--untracked-files=all'])) throw new Error('candidate requires clean repository sources')
|
|
13
|
+
const commit = git(['rev-parse', 'HEAD']), tree = git(['rev-parse', 'HEAD^{tree}'])
|
|
14
|
+
const artifactDigest = digest(bytesAt(root, artifact))
|
|
15
|
+
if (git(['rev-parse', 'HEAD']) !== commit || git(['status', '--porcelain', '--untracked-files=all']) || digest(bytesAt(root, artifact)) !== artifactDigest) throw new Error('candidate changed during capture')
|
|
16
|
+
return { data: { objective: harnessRef(state.establishment), repository: state.establishment.data.repository, commit, tree, artifactDigest, writer,
|
|
17
|
+
...(state.candidate ? { supersedes: harnessRef(state.candidate) } : {}) }, artifact, assurance: 'local-bytes-and-git-observation; writer-is-caller-reported', authority: 'none' }
|
|
18
|
+
}
|