@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14
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 +894 -0
- package/README.md +159 -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 +267 -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 +210 -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 +237 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +223 -19
- package/docs/obsidian-contract.md +246 -15
- package/docs/obsidian-plugin.md +252 -7
- package/docs/obsidian.md +874 -52
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +266 -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 +50 -0
- package/docs/repo-boundary-guard.md +120 -3
- 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 +318 -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 +219 -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 +76 -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 +60 -5
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- 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 +536 -69
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +185 -64
- 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 +66 -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 +772 -65
- 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 +30 -6
- package/src/graph/knowledge-graph.mjs +77 -9
- 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 +938 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -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 +49 -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 +401 -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/project/path-match.mjs +2 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
- package/src/projection/obsidian/personal-workspace.mjs +519 -0
- package/src/projection/obsidian/proposals/adapter.mjs +79 -17
- package/src/projection/obsidian/proposals/observation.mjs +50 -4
- package/src/projection/obsidian/publication/publisher.mjs +214 -18
- 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/projection/project.mjs +3 -2
- 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 +437 -56
- 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 +108 -23
- package/src/runtime/obsidian/login-item.mjs +335 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +173 -38
- package/src/runtime/obsidian/personal-selection.mjs +125 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- 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/private-lock.mjs +39 -4
- 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 +148 -19
- 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 +102 -9
- 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/runtime/obsidian/view-permission.mjs +334 -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/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -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 +20 -7
- package/templates/external-project-workspace/pack-lifecycle.json +7 -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,158 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { loadSigningKey, safeLabel, signDocument, verifyDocument } from '../attestation/sign.mjs'
|
|
3
|
+
import { commandProject, firstString, parseArgs } from '../project/config.mjs'
|
|
4
|
+
import { HOST_OWNER_KEYS_PATH, delegationBindings, draftDelegation, loadOwnerKeysFile, validateDelegation } from './delegation.mjs'
|
|
5
|
+
import { loadBoundaryPolicy, validateBoundaryPolicy } from './policy.mjs'
|
|
6
|
+
|
|
7
|
+
export const DELEGATION_USAGE = `Usage: atelier boundary delegation <draft|sign|verify>
|
|
8
|
+
|
|
9
|
+
draft --id ID --owner ACTOR --operator ACTOR --repos A[,B] --operations pre-commit[,boundary-check]
|
|
10
|
+
--expires RFC3339 [--not-before RFC3339] [--out FILE] [--project-config FILE]
|
|
11
|
+
Write an unsigned delegation. It binds the current boundary policy, the
|
|
12
|
+
managed repository set and each listed repository's root commit.
|
|
13
|
+
|
|
14
|
+
sign <delegation.json> --key FILE [--out FILE] [--project-config FILE]
|
|
15
|
+
The OWNER signs with their own private key file (from attestation keygen).
|
|
16
|
+
The project is required: sign recomputes the bindings from it, refuses a
|
|
17
|
+
draft that does not match, and prints what is being authorized.
|
|
18
|
+
|
|
19
|
+
verify <delegation.json> [--owner-keys FILE] [--json]
|
|
20
|
+
Report the signature against the host owner-keys file
|
|
21
|
+
(${HOST_OWNER_KEYS_PATH}), or against FILE to inspect a candidate host
|
|
22
|
+
file, together with that file's trust result and revocation. This is a
|
|
23
|
+
diagnostic; boundary check decides whether a delegation applies.
|
|
24
|
+
|
|
25
|
+
Exit codes: 0 success or valid, 1 invalid, 2 usage or input error.`
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
console.error(message)
|
|
29
|
+
process.exit(2)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const list = (value) => (typeof value === 'string' ? value.split(',').map((item) => item.trim()).filter(Boolean) : [])
|
|
33
|
+
|
|
34
|
+
function readJsonFile(file, label) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
37
|
+
} catch {
|
|
38
|
+
fail(`cannot read ${label} JSON: ${file}`)
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeOutput(out, value) {
|
|
44
|
+
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
45
|
+
if (!out) return process.stdout.write(text)
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(out, text, { flag: 'wx' })
|
|
48
|
+
} catch {
|
|
49
|
+
fail(`refusing to write ${out}: it exists or cannot be created`)
|
|
50
|
+
}
|
|
51
|
+
return console.error(`written to ${out}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function projectAndPolicy(argv) {
|
|
55
|
+
const project = commandProject({ argv })
|
|
56
|
+
const loaded = loadBoundaryPolicy(project)
|
|
57
|
+
if (!loaded.ok) fail(loaded.errors.join('\n'))
|
|
58
|
+
// Never draft or sign against a policy the check itself would reject.
|
|
59
|
+
const invalid = validateBoundaryPolicy(loaded.policy, project)
|
|
60
|
+
if (invalid.length) fail(`the boundary policy is invalid:\n${invalid.join('\n')}`)
|
|
61
|
+
return { project, policy: loaded.policy }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runDraft(args, argv) {
|
|
65
|
+
const { project, policy } = projectAndPolicy(argv)
|
|
66
|
+
const expiresAt = firstString(args.expires)
|
|
67
|
+
if (!expiresAt) fail(`delegation draft requires --expires\n\n${DELEGATION_USAGE}`)
|
|
68
|
+
const draft = draftDelegation({
|
|
69
|
+
policy,
|
|
70
|
+
project,
|
|
71
|
+
id: firstString(args.id),
|
|
72
|
+
owner: firstString(args.owner),
|
|
73
|
+
operator: firstString(args.operator),
|
|
74
|
+
repos: list(args.repos),
|
|
75
|
+
operations: list(args.operations),
|
|
76
|
+
notBefore: firstString(args['not-before']) ?? new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
77
|
+
expiresAt,
|
|
78
|
+
})
|
|
79
|
+
if (!draft.ok && !draft.errors.every((message) => /repoRoots/.test(message))) fail(draft.errors.join('\n'))
|
|
80
|
+
if (Object.values(draft.delegation.repoRoots).some((root) => !root)) fail('every listed repository needs at least one commit before it can be delegated')
|
|
81
|
+
if (!draft.ok) fail(draft.errors.join('\n'))
|
|
82
|
+
writeOutput(firstString(args.out), draft.delegation)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runSign(args, argv) {
|
|
86
|
+
const [file] = args._
|
|
87
|
+
if (!file) fail(`delegation sign requires a delegation file\n\n${DELEGATION_USAGE}`)
|
|
88
|
+
const keyPath = firstString(args.key)
|
|
89
|
+
if (!keyPath) fail(`delegation sign requires --key FILE (the owner's own private key)\n\n${DELEGATION_USAGE}`)
|
|
90
|
+
const doc = readJsonFile(file, 'delegation')
|
|
91
|
+
if (doc?.signature !== null) fail('delegation sign requires an unsigned delegation (signature: null)')
|
|
92
|
+
const { project, policy } = projectAndPolicy(argv)
|
|
93
|
+
const shapeErrors = validateDelegation({ ...doc, signature: { algorithm: 'ed25519', keyId: 'unsigned', value: 'unsigned' } }, policy)
|
|
94
|
+
if (shapeErrors.length) fail(shapeErrors.join('\n'))
|
|
95
|
+
// The owner signs what the current project binds, never an opaque digest.
|
|
96
|
+
const expected = delegationBindings({ policy, project, repos: doc.repos })
|
|
97
|
+
if (doc.policyDigest !== expected.policyDigest) fail('the draft does not bind the current boundary policy; draft it again from this project')
|
|
98
|
+
if (doc.repos.some((name) => !expected.repoRoots[name] || doc.repoRoots[name] !== expected.repoRoots[name])) {
|
|
99
|
+
fail('the draft does not bind these repositories\' root commits; draft it again from this project')
|
|
100
|
+
}
|
|
101
|
+
// Every value is printed through safeLabel: ids come from operator-editable files.
|
|
102
|
+
const operatorActor = policy.actors[doc.operator] ?? {}
|
|
103
|
+
const label = (value) => safeLabel(value, 160)
|
|
104
|
+
console.error([
|
|
105
|
+
`Authorizing delegation ${label(doc.id)}:`,
|
|
106
|
+
` operator ${label(doc.operator)} may pass the actor check owned by ${label(doc.owner)}`,
|
|
107
|
+
` the operator is recognised by githubLogin ${label(operatorActor.githubLogin ?? '(none)')} and gitEmails ${(operatorActor.gitEmails ?? []).map((email) => label(email)).join(', ') || '(none)'}`,
|
|
108
|
+
...doc.repos.map((name) => ` repository ${label(name)} with root commit ${label(doc.repoRoots[name])}`),
|
|
109
|
+
` operations: ${doc.operations.map((op) => label(op)).join(', ')}`,
|
|
110
|
+
` from ${label(new Date(Date.parse(doc.notBefore)).toISOString())} until ${label(new Date(Date.parse(doc.expiresAt)).toISOString())}`,
|
|
111
|
+
` while the boundary policy and repository set stay ${label(doc.policyDigest)}`,
|
|
112
|
+
].join('\n'))
|
|
113
|
+
let signed
|
|
114
|
+
try {
|
|
115
|
+
// An explicit key file only: the environment and working-directory fallbacks are not used for delegations.
|
|
116
|
+
const key = loadSigningKey({ keyPath, env: {}, cwd: '/nonexistent' })
|
|
117
|
+
if (key.algorithm !== 'ed25519') fail('delegations are signed with ed25519 keys')
|
|
118
|
+
signed = signDocument(doc, { privateKey: key.privateKeyJwk, keyId: key.keyId, algorithm: key.algorithm })
|
|
119
|
+
} catch (error) {
|
|
120
|
+
fail(error.message)
|
|
121
|
+
}
|
|
122
|
+
writeOutput(firstString(args.out), signed)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runVerify(args) {
|
|
126
|
+
const [file] = args._
|
|
127
|
+
if (!file) fail(`delegation verify requires a delegation file\n\n${DELEGATION_USAGE}`)
|
|
128
|
+
const doc = readJsonFile(file, 'delegation')
|
|
129
|
+
const keysFile = firstString(args['owner-keys']) ?? HOST_OWNER_KEYS_PATH
|
|
130
|
+
const ownerKeys = loadOwnerKeysFile(keysFile)
|
|
131
|
+
const report = { ownerKeysFile: keysFile, hostTrust: ownerKeys.ok ? { ok: true } : { ok: false, reason: ownerKeys.reason }, signatureValid: false, revoked: false, reasons: [] }
|
|
132
|
+
if (ownerKeys.ok) {
|
|
133
|
+
const key = ownerKeys.keys.find((item) => item.actorId === doc?.owner && item.keyId === doc?.signature?.keyId)
|
|
134
|
+
if (key) {
|
|
135
|
+
const checked = verifyDocument(doc, { publicKey: key })
|
|
136
|
+
report.signatureValid = checked.valid
|
|
137
|
+
report.reasons.push(...checked.reasons)
|
|
138
|
+
report.keyId = key.keyId
|
|
139
|
+
report.owner = key.actorId
|
|
140
|
+
} else report.reasons.push({ code: 'delegation-owner-key-missing', message: 'no host key for this owner and keyId' })
|
|
141
|
+
report.revoked = ownerKeys.revoked.has(doc?.id)
|
|
142
|
+
} else report.reasons.push({ code: ownerKeys.reason, message: ownerKeys.errors.join('; ') })
|
|
143
|
+
report.valid = report.hostTrust.ok && report.signatureValid && !report.revoked
|
|
144
|
+
if (args.json) console.log(JSON.stringify(report, null, 2))
|
|
145
|
+
else console.log(report.valid ? `signature valid for ${report.owner} key ${report.keyId}; scope, time and bindings are decided by boundary check` : `invalid: ${report.reasons.map((item) => item.code).join(', ') || (report.revoked ? 'delegation-revoked' : 'unknown')}`)
|
|
146
|
+
process.exit(report.valid ? 0 : 1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function runBoundaryDelegationCommand(argv = process.argv.slice(2)) {
|
|
150
|
+
const args = parseArgs(argv)
|
|
151
|
+
const [subcommand, ...rest] = args._
|
|
152
|
+
const sub = { ...args, _: rest }
|
|
153
|
+
if (args.help || args.h) return console.log(DELEGATION_USAGE)
|
|
154
|
+
if (subcommand === 'draft') return runDraft(sub, argv)
|
|
155
|
+
if (subcommand === 'sign') return runSign(sub, argv)
|
|
156
|
+
if (subcommand === 'verify') return runVerify(sub)
|
|
157
|
+
return fail(DELEGATION_USAGE)
|
|
158
|
+
}
|
|
@@ -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
|
+
}
|