create-egregore 0.21.6 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/egregore-launcher.js +58 -6
- package/assets/egregore-launcher.sh +15 -2
- package/lib/claude-runtime.js +2 -0
- package/lib/codex-runtime.js +2 -0
- package/lib/setup.js +20 -3
- package/package.json +2 -2
- package/runtime/claude/.claude/context/commit-format.md +7 -4
- package/runtime/claude/.claude/context/handoff-claiming.md +32 -0
- package/runtime/claude/.claude/context/notification-consent.md +12 -9
- package/runtime/claude/.claude/hooks/boundary-check.sh +14 -331
- package/runtime/claude/.claude/hooks/memory-read-guard.sh +9 -1
- package/runtime/claude/.claude/rules/voice-bedrock.md +6 -2
- package/runtime/claude/.claude/settings.json +1 -1
- package/runtime/claude/.claude/skills/commit/SKILL.md +5 -3
- package/runtime/claude/.claude/skills/dark-mode/SKILL.md +48 -369
- package/runtime/claude/.claude/skills/records/SKILL.md +158 -0
- package/runtime/claude/.claude/skills/reflect/SKILL.md +50 -21
- package/runtime/claude/.claude/skills/review-pr/SKILL.md +4 -2
- package/runtime/claude/.claude/skills/save/SKILL.md +9 -3
- package/runtime/claude/.claude/skills/summon/SKILL.md +138 -207
- package/runtime/claude/.claude/skills/tui-design/SKILL.md +1 -1
- package/runtime/claude/.claude/skills/update/SKILL.md +1 -1
- package/runtime/claude/.claude/skills/view/SKILL.md +52 -16
- package/runtime/claude/.claude/skills/view/compose-scaffold.html +19 -13
- package/runtime/claude/CLAUDE.md +59 -95
- package/runtime/claude/bin/agent.sh +231 -7
- package/runtime/claude/bin/api-call.sh +44 -23
- package/runtime/claude/bin/artifact-register.sh +7 -1
- package/runtime/claude/bin/artifact-writeback.sh +1 -1
- package/runtime/claude/bin/artifacts.sh +66 -265
- package/runtime/claude/bin/attendant.sh +4 -1
- package/runtime/claude/bin/boundary-guard.cjs +1848 -0
- package/runtime/claude/bin/boundary-hook.sh +41 -0
- package/runtime/claude/bin/boundary.sh +15 -155
- package/runtime/claude/bin/capture-run.sh +32 -2
- package/runtime/claude/bin/codex-render-spec.mjs +24 -46
- package/runtime/claude/bin/codex-session-start.sh +19 -0
- package/runtime/claude/bin/codex-skill-render.mjs +18 -0
- package/runtime/claude/bin/config-get.sh +10 -1
- package/runtime/claude/bin/connect-refresh.sh +16 -0
- package/runtime/claude/bin/corpus_map.py +5 -0
- package/runtime/claude/bin/data/capabilities/claude.json +1480 -0
- package/runtime/claude/bin/genui.sh +19 -0
- package/runtime/claude/bin/graph-batch.sh +1 -1
- package/runtime/claude/bin/graph-op.sh +14 -0
- package/runtime/claude/bin/graph.sh +1 -1
- package/runtime/claude/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/claude/bin/handoff-run.sh +11 -6
- package/runtime/claude/bin/ingest-item.sh +1 -1
- package/runtime/claude/bin/ingest-surface.html +22 -4
- package/runtime/claude/bin/issue.sh +1 -1
- package/runtime/claude/bin/knowledge.sh +1 -1
- package/runtime/claude/bin/lib/card-delivery.sh +329 -0
- package/runtime/claude/bin/lib/config.sh +148 -3
- package/runtime/claude/bin/lib/context.sh +38 -67
- package/runtime/claude/bin/lib/genui-entry.py +36 -0
- package/runtime/claude/bin/lib/git-message.sh +25 -6
- package/runtime/claude/bin/lib/git-sync.sh +12 -4
- package/runtime/claude/bin/lib/greeting.sh +255 -28
- package/runtime/claude/bin/lib/handoff_meta.py +294 -0
- package/runtime/claude/bin/lib/health-footer.sh +83 -2
- package/runtime/claude/bin/lib/identity.sh +31 -10
- package/runtime/claude/bin/lib/instance-registry.sh +71 -0
- package/runtime/claude/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/claude/bin/lib/metrics.sh +121 -8
- package/runtime/claude/bin/lib/notices.sh +36 -0
- package/runtime/claude/bin/lib/record_outcomes.py +729 -0
- package/runtime/claude/bin/lib/reflexes.py +267 -0
- package/runtime/claude/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/claude/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/claude/bin/lib/session_record.py +3747 -0
- package/runtime/claude/bin/lib/session_trail.py +1983 -0
- package/runtime/claude/bin/lib/settings-drift.sh +8 -5
- package/runtime/claude/bin/lib/startup-restart.sh +41 -0
- package/runtime/claude/bin/lib/worktree-links.sh +92 -0
- package/runtime/claude/bin/memory-push.sh +6 -0
- package/runtime/claude/bin/merge-gate.sh +498 -0
- package/runtime/claude/bin/notify.sh +5 -2
- package/runtime/claude/bin/page-access.sh +103 -0
- package/runtime/claude/bin/person.sh +13 -2
- package/runtime/claude/bin/pi-render-spec.mjs +0 -1
- package/runtime/claude/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/claude/bin/post-update.sh +181 -0
- package/runtime/claude/bin/prime-render-spec.mjs +0 -1
- package/runtime/claude/bin/publish-artifact.sh +89 -42
- package/runtime/claude/bin/qa-inventory.sh +2 -2
- package/runtime/claude/bin/quest.sh +1 -1
- package/runtime/claude/bin/records.sh +11 -0
- package/runtime/claude/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/claude/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/claude/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/claude/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/claude/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/claude/bin/reflexes/30-cost.py +116 -0
- package/runtime/claude/bin/reflexes/40-shells.py +51 -0
- package/runtime/claude/bin/reflexes/README.md +61 -0
- package/runtime/claude/bin/reflexes.sh +7 -0
- package/runtime/claude/bin/runtime-identity.sh +140 -42
- package/runtime/claude/bin/screenshot.sh +77 -0
- package/runtime/claude/bin/scroll.sh +1 -1
- package/runtime/claude/bin/search.sh +31 -5
- package/runtime/claude/bin/session-autosave.sh +6 -3
- package/runtime/claude/bin/session-id.sh +147 -0
- package/runtime/claude/bin/session-log.sh +19 -0
- package/runtime/claude/bin/session-record.sh +26 -0
- package/runtime/claude/bin/session-start.sh +121 -151
- package/runtime/claude/bin/settings.sh +13 -8
- package/runtime/claude/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/claude/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/claude/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/claude/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/claude/bin/spirit-discovery.sh +4 -0
- package/runtime/claude/bin/startup-check.sh +1 -1
- package/runtime/claude/bin/telemetry.sh +11 -4
- package/runtime/claude/bin/test-baseline.sh +33 -5
- package/runtime/claude/bin/todo.sh +1 -1
- package/runtime/claude/bin/worktree-create.sh +15 -45
- package/runtime/claude/bin/worktree.sh +6 -0
- package/runtime/claude/docs/spirit-discovery.md +96 -0
- package/runtime/claude/egregore_runtime/__init__.py +18 -12
- package/runtime/claude/egregore_runtime/access.py +608 -0
- package/runtime/claude/egregore_runtime/access_cli.py +190 -0
- package/runtime/claude/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/claude/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/claude/egregore_runtime/admin_entry.py +56 -0
- package/runtime/claude/egregore_runtime/artifacts.py +222 -16
- package/runtime/claude/egregore_runtime/contracts.py +27 -0
- package/runtime/claude/egregore_runtime/eligibility.py +5 -12
- package/runtime/claude/egregore_runtime/episode_binding.py +16 -5
- package/runtime/claude/egregore_runtime/github_issues.py +2 -2
- package/runtime/claude/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/claude/egregore_runtime/harness_cli.py +674 -149
- package/runtime/claude/egregore_runtime/held.py +2436 -0
- package/runtime/claude/egregore_runtime/hosted_api.py +137 -0
- package/runtime/claude/egregore_runtime/identity.py +196 -65
- package/runtime/claude/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/claude/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/claude/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/claude/egregore_runtime/installation.py +119 -0
- package/runtime/claude/egregore_runtime/investigation.py +64 -41
- package/runtime/claude/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/claude/egregore_runtime/issue_cli.py +51 -32
- package/runtime/claude/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/claude/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/claude/egregore_runtime/issues.py +12 -6
- package/runtime/claude/egregore_runtime/knowledge.py +331 -5
- package/runtime/claude/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/claude/egregore_runtime/lifecycle.py +88 -22
- package/runtime/claude/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/claude/egregore_runtime/line_ids.py +10 -0
- package/runtime/claude/egregore_runtime/memory_push.py +349 -0
- package/runtime/claude/egregore_runtime/observe.py +64 -25
- package/runtime/claude/egregore_runtime/observers.py +19 -0
- package/runtime/claude/egregore_runtime/optional_modules.py +13 -0
- package/runtime/claude/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/claude/egregore_runtime/policy.py +651 -24
- package/runtime/claude/egregore_runtime/private_result.py +2 -1
- package/runtime/claude/egregore_runtime/projection.py +946 -0
- package/runtime/claude/egregore_runtime/quest_cli.py +20 -23
- package/runtime/claude/egregore_runtime/question_cli.py +15 -11
- package/runtime/claude/egregore_runtime/questions.py +10 -10
- package/runtime/claude/egregore_runtime/quests.py +10 -10
- package/runtime/claude/egregore_runtime/record_publish.py +548 -0
- package/runtime/claude/egregore_runtime/record_subject.py +564 -0
- package/runtime/claude/egregore_runtime/research_ingest.py +2 -4
- package/runtime/claude/egregore_runtime/runtime.py +169 -60
- package/runtime/claude/egregore_runtime/sandbox.py +194 -0
- package/runtime/claude/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/claude/egregore_runtime/scrolls.py +23 -15
- package/runtime/claude/egregore_runtime/shadow.py +262 -0
- package/runtime/claude/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/claude/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/claude/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/claude/egregore_runtime/status.py +101 -44
- package/runtime/claude/egregore_runtime/status_cli.py +13 -5
- package/runtime/claude/egregore_runtime/sync.py +39 -21
- package/runtime/claude/egregore_runtime/taste.py +934 -0
- package/runtime/claude/egregore_runtime/taste_cli.py +242 -0
- package/runtime/claude/egregore_runtime/taste_rules.py +91 -0
- package/runtime/claude/egregore_runtime/telemetry.py +26 -3
- package/runtime/claude/egregore_runtime/todo_cli.py +15 -12
- package/runtime/claude/egregore_runtime/todos.py +3 -2
- package/runtime/claude/egregore_runtime/upgrade.py +85 -58
- package/runtime/claude/egregore_runtime/writeback.py +330 -24
- package/runtime/claude/manifest.json +65 -5
- package/runtime/codex/.claude/context/handoff-claiming.md +32 -0
- package/runtime/codex/.claude/context/notification-consent.md +12 -9
- package/runtime/codex/.claude/skills/view/SKILL.md +438 -0
- package/runtime/codex/.codex/hooks.json +11 -0
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/records/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +19 -13
- package/runtime/codex/.codex/spec-manifest.json +13 -19
- package/runtime/codex/AGENTS.md +54 -105
- package/runtime/codex/bin/agent.sh +231 -7
- package/runtime/codex/bin/api-call.sh +44 -23
- package/runtime/codex/bin/artifact-register.sh +7 -1
- package/runtime/codex/bin/artifact-writeback.sh +1 -1
- package/runtime/codex/bin/artifacts.sh +66 -265
- package/runtime/codex/bin/attendant.sh +4 -1
- package/runtime/codex/bin/boundary-guard.cjs +1848 -0
- package/runtime/codex/bin/boundary-hook.sh +41 -0
- package/runtime/codex/bin/capture-run.sh +32 -2
- package/runtime/codex/bin/codex-render-spec.mjs +24 -46
- package/runtime/codex/bin/codex-session-start.sh +19 -0
- package/runtime/codex/bin/codex-skill-render.mjs +18 -0
- package/runtime/codex/bin/config-get.sh +10 -1
- package/runtime/codex/bin/connect-refresh.sh +16 -0
- package/runtime/codex/bin/corpus_map.py +5 -0
- package/runtime/codex/bin/data/capabilities/codex.json +1458 -0
- package/runtime/codex/bin/genui.sh +19 -0
- package/runtime/codex/bin/graph-batch.sh +1 -1
- package/runtime/codex/bin/graph-op.sh +14 -0
- package/runtime/codex/bin/graph.sh +1 -1
- package/runtime/codex/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/codex/bin/handoff-run.sh +11 -6
- package/runtime/codex/bin/ingest-item.sh +1 -1
- package/runtime/codex/bin/ingest-surface.html +22 -4
- package/runtime/codex/bin/issue.sh +1 -1
- package/runtime/codex/bin/knowledge.sh +1 -1
- package/runtime/codex/bin/lib/card-delivery.sh +329 -0
- package/runtime/codex/bin/lib/config.sh +148 -3
- package/runtime/codex/bin/lib/context.sh +38 -67
- package/runtime/codex/bin/lib/genui-entry.py +36 -0
- package/runtime/codex/bin/lib/git-message.sh +25 -6
- package/runtime/codex/bin/lib/git-sync.sh +12 -4
- package/runtime/codex/bin/lib/greeting.sh +255 -28
- package/runtime/codex/bin/lib/handoff_meta.py +294 -0
- package/runtime/codex/bin/lib/health-footer.sh +83 -2
- package/runtime/codex/bin/lib/identity.sh +31 -10
- package/runtime/codex/bin/lib/instance-registry.sh +71 -0
- package/runtime/codex/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/codex/bin/lib/metrics.sh +121 -8
- package/runtime/codex/bin/lib/notices.sh +36 -0
- package/runtime/codex/bin/lib/record_outcomes.py +729 -0
- package/runtime/codex/bin/lib/reflexes.py +267 -0
- package/runtime/codex/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/codex/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/codex/bin/lib/session_record.py +3747 -0
- package/runtime/codex/bin/lib/session_trail.py +1983 -0
- package/runtime/codex/bin/lib/settings-drift.sh +8 -5
- package/runtime/codex/bin/lib/startup-restart.sh +41 -0
- package/runtime/codex/bin/lib/worktree-links.sh +92 -0
- package/runtime/codex/bin/memory-push.sh +6 -0
- package/runtime/codex/bin/merge-gate.sh +498 -0
- package/runtime/codex/bin/notify.sh +5 -2
- package/runtime/codex/bin/page-access.sh +103 -0
- package/runtime/codex/bin/person.sh +13 -2
- package/runtime/codex/bin/pi-render-spec.mjs +0 -1
- package/runtime/codex/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/codex/bin/post-update.sh +181 -0
- package/runtime/codex/bin/prime-render-spec.mjs +0 -1
- package/runtime/codex/bin/publish-artifact.sh +89 -42
- package/runtime/codex/bin/qa-inventory.sh +2 -2
- package/runtime/codex/bin/quest.sh +1 -1
- package/runtime/codex/bin/records.sh +11 -0
- package/runtime/codex/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/codex/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/codex/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/codex/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/codex/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/codex/bin/reflexes/30-cost.py +116 -0
- package/runtime/codex/bin/reflexes/40-shells.py +51 -0
- package/runtime/codex/bin/reflexes/README.md +61 -0
- package/runtime/codex/bin/reflexes.sh +7 -0
- package/runtime/codex/bin/runtime-identity.sh +140 -42
- package/runtime/codex/bin/screenshot.sh +77 -0
- package/runtime/codex/bin/scroll.sh +1 -1
- package/runtime/codex/bin/search.sh +31 -5
- package/runtime/codex/bin/session-autosave.sh +6 -3
- package/runtime/codex/bin/session-id.sh +147 -0
- package/runtime/codex/bin/session-log.sh +19 -0
- package/runtime/codex/bin/session-record.sh +26 -0
- package/runtime/codex/bin/settings.sh +13 -8
- package/runtime/codex/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/codex/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/codex/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/codex/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/codex/bin/spirit-discovery.sh +4 -0
- package/runtime/codex/bin/startup-check.sh +1 -1
- package/runtime/codex/bin/telemetry.sh +11 -4
- package/runtime/codex/bin/test-baseline.sh +33 -5
- package/runtime/codex/bin/todo.sh +1 -1
- package/runtime/codex/bin/worktree-create.sh +15 -45
- package/runtime/codex/bin/worktree.sh +6 -0
- package/runtime/codex/docs/spirit-discovery.md +96 -0
- package/runtime/codex/egregore_runtime/__init__.py +18 -12
- package/runtime/codex/egregore_runtime/access.py +608 -0
- package/runtime/codex/egregore_runtime/access_cli.py +190 -0
- package/runtime/codex/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/codex/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/codex/egregore_runtime/admin_entry.py +56 -0
- package/runtime/codex/egregore_runtime/artifacts.py +222 -16
- package/runtime/codex/egregore_runtime/contracts.py +27 -0
- package/runtime/codex/egregore_runtime/eligibility.py +5 -12
- package/runtime/codex/egregore_runtime/episode_binding.py +16 -5
- package/runtime/codex/egregore_runtime/github_issues.py +2 -2
- package/runtime/codex/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/codex/egregore_runtime/harness_cli.py +674 -149
- package/runtime/codex/egregore_runtime/held.py +2436 -0
- package/runtime/codex/egregore_runtime/hosted_api.py +137 -0
- package/runtime/codex/egregore_runtime/identity.py +196 -65
- package/runtime/codex/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/codex/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/codex/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/codex/egregore_runtime/installation.py +119 -0
- package/runtime/codex/egregore_runtime/investigation.py +64 -41
- package/runtime/codex/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/codex/egregore_runtime/issue_cli.py +51 -32
- package/runtime/codex/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/codex/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/codex/egregore_runtime/issues.py +12 -6
- package/runtime/codex/egregore_runtime/knowledge.py +331 -5
- package/runtime/codex/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/codex/egregore_runtime/lifecycle.py +88 -22
- package/runtime/codex/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/codex/egregore_runtime/line_ids.py +10 -0
- package/runtime/codex/egregore_runtime/memory_push.py +349 -0
- package/runtime/codex/egregore_runtime/observe.py +64 -25
- package/runtime/codex/egregore_runtime/observers.py +19 -0
- package/runtime/codex/egregore_runtime/optional_modules.py +13 -0
- package/runtime/codex/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/codex/egregore_runtime/policy.py +651 -24
- package/runtime/codex/egregore_runtime/private_result.py +2 -1
- package/runtime/codex/egregore_runtime/projection.py +946 -0
- package/runtime/codex/egregore_runtime/quest_cli.py +20 -23
- package/runtime/codex/egregore_runtime/question_cli.py +15 -11
- package/runtime/codex/egregore_runtime/questions.py +10 -10
- package/runtime/codex/egregore_runtime/quests.py +10 -10
- package/runtime/codex/egregore_runtime/record_publish.py +548 -0
- package/runtime/codex/egregore_runtime/record_subject.py +564 -0
- package/runtime/codex/egregore_runtime/research_ingest.py +2 -4
- package/runtime/codex/egregore_runtime/runtime.py +169 -60
- package/runtime/codex/egregore_runtime/sandbox.py +194 -0
- package/runtime/codex/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/codex/egregore_runtime/scrolls.py +23 -15
- package/runtime/codex/egregore_runtime/shadow.py +262 -0
- package/runtime/codex/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/codex/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/codex/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/codex/egregore_runtime/status.py +101 -44
- package/runtime/codex/egregore_runtime/status_cli.py +13 -5
- package/runtime/codex/egregore_runtime/sync.py +39 -21
- package/runtime/codex/egregore_runtime/taste.py +934 -0
- package/runtime/codex/egregore_runtime/taste_cli.py +242 -0
- package/runtime/codex/egregore_runtime/taste_rules.py +91 -0
- package/runtime/codex/egregore_runtime/telemetry.py +26 -3
- package/runtime/codex/egregore_runtime/todo_cli.py +15 -12
- package/runtime/codex/egregore_runtime/todos.py +3 -2
- package/runtime/codex/egregore_runtime/upgrade.py +85 -58
- package/runtime/codex/egregore_runtime/writeback.py +330 -24
- package/runtime/codex/manifest.json +66 -5
- package/runtime/pi/.claude/context/handoff-claiming.md +32 -0
- package/runtime/pi/.claude/context/notification-consent.md +12 -9
- package/runtime/pi/.claude/skills/view/SKILL.md +438 -0
- package/runtime/pi/.codex/hooks.json +11 -0
- package/runtime/pi/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/records/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +19 -13
- package/runtime/pi/.codex/spec-manifest.json +13 -19
- package/runtime/pi/.pi/APPEND_SYSTEM.md +53 -99
- package/runtime/pi/.pi/extensions/egregore.ts +13 -1
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +54 -105
- package/runtime/pi/bin/agent.sh +231 -7
- package/runtime/pi/bin/api-call.sh +44 -23
- package/runtime/pi/bin/artifact-register.sh +7 -1
- package/runtime/pi/bin/artifact-writeback.sh +1 -1
- package/runtime/pi/bin/artifacts.sh +66 -265
- package/runtime/pi/bin/attendant.sh +4 -1
- package/runtime/pi/bin/boundary-guard.cjs +1848 -0
- package/runtime/pi/bin/boundary-hook.sh +41 -0
- package/runtime/pi/bin/capture-run.sh +32 -2
- package/runtime/pi/bin/codex-render-spec.mjs +24 -46
- package/runtime/pi/bin/codex-session-start.sh +19 -0
- package/runtime/pi/bin/codex-skill-render.mjs +18 -0
- package/runtime/pi/bin/config-get.sh +10 -1
- package/runtime/pi/bin/connect-refresh.sh +16 -0
- package/runtime/pi/bin/corpus_map.py +5 -0
- package/runtime/pi/bin/data/capabilities/pi.json +1458 -0
- package/runtime/pi/bin/genui.sh +19 -0
- package/runtime/pi/bin/graph-batch.sh +1 -1
- package/runtime/pi/bin/graph-op.sh +14 -0
- package/runtime/pi/bin/graph.sh +1 -1
- package/runtime/pi/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/pi/bin/handoff-run.sh +11 -6
- package/runtime/pi/bin/ingest-item.sh +1 -1
- package/runtime/pi/bin/ingest-surface.html +22 -4
- package/runtime/pi/bin/issue.sh +1 -1
- package/runtime/pi/bin/knowledge.sh +1 -1
- package/runtime/pi/bin/lib/card-delivery.sh +329 -0
- package/runtime/pi/bin/lib/config.sh +148 -3
- package/runtime/pi/bin/lib/context.sh +38 -67
- package/runtime/pi/bin/lib/genui-entry.py +36 -0
- package/runtime/pi/bin/lib/git-message.sh +25 -6
- package/runtime/pi/bin/lib/git-sync.sh +12 -4
- package/runtime/pi/bin/lib/greeting.sh +255 -28
- package/runtime/pi/bin/lib/handoff_meta.py +294 -0
- package/runtime/pi/bin/lib/health-footer.sh +83 -2
- package/runtime/pi/bin/lib/identity.sh +31 -10
- package/runtime/pi/bin/lib/instance-registry.sh +71 -0
- package/runtime/pi/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/pi/bin/lib/metrics.sh +121 -8
- package/runtime/pi/bin/lib/notices.sh +36 -0
- package/runtime/pi/bin/lib/record_outcomes.py +729 -0
- package/runtime/pi/bin/lib/reflexes.py +267 -0
- package/runtime/pi/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/pi/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/pi/bin/lib/session_record.py +3747 -0
- package/runtime/pi/bin/lib/session_trail.py +1983 -0
- package/runtime/pi/bin/lib/settings-drift.sh +8 -5
- package/runtime/pi/bin/lib/startup-restart.sh +41 -0
- package/runtime/pi/bin/lib/worktree-links.sh +92 -0
- package/runtime/pi/bin/memory-push.sh +6 -0
- package/runtime/pi/bin/merge-gate.sh +498 -0
- package/runtime/pi/bin/notify.sh +5 -2
- package/runtime/pi/bin/page-access.sh +103 -0
- package/runtime/pi/bin/person.sh +13 -2
- package/runtime/pi/bin/pi-branch-policy.mjs +37 -0
- package/runtime/pi/bin/pi-observe-context.mjs +97 -4
- package/runtime/pi/bin/pi-render-spec.mjs +0 -1
- package/runtime/pi/bin/pi-session-start.sh +4 -1
- package/runtime/pi/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/pi/bin/post-update.sh +181 -0
- package/runtime/pi/bin/prime-render-spec.mjs +0 -1
- package/runtime/pi/bin/publish-artifact.sh +89 -42
- package/runtime/pi/bin/qa-inventory.sh +2 -2
- package/runtime/pi/bin/quest.sh +1 -1
- package/runtime/pi/bin/records.sh +11 -0
- package/runtime/pi/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/pi/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/pi/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/pi/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/pi/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/pi/bin/reflexes/30-cost.py +116 -0
- package/runtime/pi/bin/reflexes/40-shells.py +51 -0
- package/runtime/pi/bin/reflexes/README.md +61 -0
- package/runtime/pi/bin/reflexes.sh +7 -0
- package/runtime/pi/bin/runtime-identity.sh +140 -42
- package/runtime/pi/bin/screenshot.sh +77 -0
- package/runtime/pi/bin/scroll.sh +1 -1
- package/runtime/pi/bin/search.sh +31 -5
- package/runtime/pi/bin/session-autosave.sh +6 -3
- package/runtime/pi/bin/session-id.sh +147 -0
- package/runtime/pi/bin/session-log.sh +19 -0
- package/runtime/pi/bin/session-record.sh +26 -0
- package/runtime/pi/bin/settings.sh +13 -8
- package/runtime/pi/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/pi/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/pi/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/pi/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/pi/bin/spirit-discovery.sh +4 -0
- package/runtime/pi/bin/startup-check.sh +1 -1
- package/runtime/pi/bin/telemetry.sh +11 -4
- package/runtime/pi/bin/test-baseline.sh +33 -5
- package/runtime/pi/bin/todo.sh +1 -1
- package/runtime/pi/bin/worktree-create.sh +15 -45
- package/runtime/pi/bin/worktree.sh +6 -0
- package/runtime/pi/docs/spirit-discovery.md +96 -0
- package/runtime/pi/egregore_runtime/__init__.py +18 -12
- package/runtime/pi/egregore_runtime/access.py +608 -0
- package/runtime/pi/egregore_runtime/access_cli.py +190 -0
- package/runtime/pi/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/pi/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/pi/egregore_runtime/admin_entry.py +56 -0
- package/runtime/pi/egregore_runtime/artifacts.py +222 -16
- package/runtime/pi/egregore_runtime/contracts.py +27 -0
- package/runtime/pi/egregore_runtime/eligibility.py +5 -12
- package/runtime/pi/egregore_runtime/episode_binding.py +16 -5
- package/runtime/pi/egregore_runtime/github_issues.py +2 -2
- package/runtime/pi/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/pi/egregore_runtime/harness_cli.py +674 -149
- package/runtime/pi/egregore_runtime/held.py +2436 -0
- package/runtime/pi/egregore_runtime/hosted_api.py +137 -0
- package/runtime/pi/egregore_runtime/identity.py +196 -65
- package/runtime/pi/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/pi/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/pi/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/pi/egregore_runtime/installation.py +119 -0
- package/runtime/pi/egregore_runtime/investigation.py +64 -41
- package/runtime/pi/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/pi/egregore_runtime/issue_cli.py +51 -32
- package/runtime/pi/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/pi/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/pi/egregore_runtime/issues.py +12 -6
- package/runtime/pi/egregore_runtime/knowledge.py +331 -5
- package/runtime/pi/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/pi/egregore_runtime/lifecycle.py +88 -22
- package/runtime/pi/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/pi/egregore_runtime/line_ids.py +10 -0
- package/runtime/pi/egregore_runtime/memory_push.py +349 -0
- package/runtime/pi/egregore_runtime/observe.py +64 -25
- package/runtime/pi/egregore_runtime/observers.py +19 -0
- package/runtime/pi/egregore_runtime/optional_modules.py +13 -0
- package/runtime/pi/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/pi/egregore_runtime/policy.py +651 -24
- package/runtime/pi/egregore_runtime/private_result.py +2 -1
- package/runtime/pi/egregore_runtime/projection.py +946 -0
- package/runtime/pi/egregore_runtime/quest_cli.py +20 -23
- package/runtime/pi/egregore_runtime/question_cli.py +15 -11
- package/runtime/pi/egregore_runtime/questions.py +10 -10
- package/runtime/pi/egregore_runtime/quests.py +10 -10
- package/runtime/pi/egregore_runtime/record_publish.py +548 -0
- package/runtime/pi/egregore_runtime/record_subject.py +564 -0
- package/runtime/pi/egregore_runtime/research_ingest.py +2 -4
- package/runtime/pi/egregore_runtime/runtime.py +169 -60
- package/runtime/pi/egregore_runtime/sandbox.py +194 -0
- package/runtime/pi/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/pi/egregore_runtime/scrolls.py +23 -15
- package/runtime/pi/egregore_runtime/shadow.py +262 -0
- package/runtime/pi/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/pi/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/pi/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/pi/egregore_runtime/status.py +101 -44
- package/runtime/pi/egregore_runtime/status_cli.py +13 -5
- package/runtime/pi/egregore_runtime/sync.py +39 -21
- package/runtime/pi/egregore_runtime/taste.py +934 -0
- package/runtime/pi/egregore_runtime/taste_cli.py +242 -0
- package/runtime/pi/egregore_runtime/taste_rules.py +91 -0
- package/runtime/pi/egregore_runtime/telemetry.py +26 -3
- package/runtime/pi/egregore_runtime/todo_cli.py +15 -12
- package/runtime/pi/egregore_runtime/todos.py +3 -2
- package/runtime/pi/egregore_runtime/upgrade.py +85 -58
- package/runtime/pi/egregore_runtime/writeback.py +330 -24
- package/runtime/pi/manifest.json +66 -5
- package/runtime/prime/.claude/context/handoff-claiming.md +32 -0
- package/runtime/prime/.claude/context/notification-consent.md +12 -9
- package/runtime/prime/.claude/skills/view/SKILL.md +438 -0
- package/runtime/prime/.codex/hooks.json +11 -0
- package/runtime/prime/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/records/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +19 -13
- package/runtime/prime/.codex/spec-manifest.json +13 -19
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +53 -99
- package/runtime/prime/.prime/agent/extensions/egregore.ts +14 -1
- package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +14 -6
- package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +66 -1
- package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
- package/runtime/prime/AGENTS.md +54 -105
- package/runtime/prime/bin/agent.sh +231 -7
- package/runtime/prime/bin/api-call.sh +44 -23
- package/runtime/prime/bin/artifact-register.sh +7 -1
- package/runtime/prime/bin/artifact-writeback.sh +1 -1
- package/runtime/prime/bin/artifacts.sh +66 -265
- package/runtime/prime/bin/attendant.sh +4 -1
- package/runtime/prime/bin/boundary-guard.cjs +1848 -0
- package/runtime/prime/bin/boundary-hook.sh +41 -0
- package/runtime/prime/bin/capture-run.sh +32 -2
- package/runtime/prime/bin/codex-render-spec.mjs +24 -46
- package/runtime/prime/bin/codex-session-start.sh +19 -0
- package/runtime/prime/bin/codex-skill-render.mjs +18 -0
- package/runtime/prime/bin/config-get.sh +10 -1
- package/runtime/prime/bin/connect-refresh.sh +16 -0
- package/runtime/prime/bin/corpus_map.py +5 -0
- package/runtime/prime/bin/data/capabilities/prime.json +1458 -0
- package/runtime/prime/bin/genui.sh +19 -0
- package/runtime/prime/bin/graph-batch.sh +1 -1
- package/runtime/prime/bin/graph-op.sh +14 -0
- package/runtime/prime/bin/graph.sh +1 -1
- package/runtime/prime/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/prime/bin/handoff-run.sh +11 -6
- package/runtime/prime/bin/ingest-item.sh +1 -1
- package/runtime/prime/bin/ingest-surface.html +22 -4
- package/runtime/prime/bin/issue.sh +1 -1
- package/runtime/prime/bin/knowledge.sh +1 -1
- package/runtime/prime/bin/lib/card-delivery.sh +329 -0
- package/runtime/prime/bin/lib/config.sh +148 -3
- package/runtime/prime/bin/lib/context.sh +38 -67
- package/runtime/prime/bin/lib/genui-entry.py +36 -0
- package/runtime/prime/bin/lib/git-message.sh +25 -6
- package/runtime/prime/bin/lib/git-sync.sh +12 -4
- package/runtime/prime/bin/lib/greeting.sh +255 -28
- package/runtime/prime/bin/lib/handoff_meta.py +294 -0
- package/runtime/prime/bin/lib/health-footer.sh +83 -2
- package/runtime/prime/bin/lib/identity.sh +31 -10
- package/runtime/prime/bin/lib/instance-registry.sh +71 -0
- package/runtime/prime/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/prime/bin/lib/metrics.sh +121 -8
- package/runtime/prime/bin/lib/notices.sh +36 -0
- package/runtime/prime/bin/lib/record_outcomes.py +729 -0
- package/runtime/prime/bin/lib/reflexes.py +267 -0
- package/runtime/prime/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/prime/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/prime/bin/lib/session_record.py +3747 -0
- package/runtime/prime/bin/lib/session_trail.py +1983 -0
- package/runtime/prime/bin/lib/settings-drift.sh +8 -5
- package/runtime/prime/bin/lib/startup-restart.sh +41 -0
- package/runtime/prime/bin/lib/worktree-links.sh +92 -0
- package/runtime/prime/bin/memory-push.sh +6 -0
- package/runtime/prime/bin/merge-gate.sh +498 -0
- package/runtime/prime/bin/notify.sh +5 -2
- package/runtime/prime/bin/page-access.sh +103 -0
- package/runtime/prime/bin/person.sh +13 -2
- package/runtime/prime/bin/pi-branch-policy.mjs +37 -0
- package/runtime/prime/bin/pi-observe-context.mjs +97 -4
- package/runtime/prime/bin/pi-render-spec.mjs +0 -1
- package/runtime/prime/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/prime/bin/post-update.sh +181 -0
- package/runtime/prime/bin/prime-render-spec.mjs +0 -1
- package/runtime/prime/bin/prime-session-start.sh +3 -1
- package/runtime/prime/bin/publish-artifact.sh +89 -42
- package/runtime/prime/bin/qa-inventory.sh +2 -2
- package/runtime/prime/bin/quest.sh +1 -1
- package/runtime/prime/bin/records.sh +11 -0
- package/runtime/prime/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/prime/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/prime/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/prime/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/prime/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/prime/bin/reflexes/30-cost.py +116 -0
- package/runtime/prime/bin/reflexes/40-shells.py +51 -0
- package/runtime/prime/bin/reflexes/README.md +61 -0
- package/runtime/prime/bin/reflexes.sh +7 -0
- package/runtime/prime/bin/runtime-identity.sh +140 -42
- package/runtime/prime/bin/screenshot.sh +77 -0
- package/runtime/prime/bin/scroll.sh +1 -1
- package/runtime/prime/bin/search.sh +31 -5
- package/runtime/prime/bin/session-autosave.sh +6 -3
- package/runtime/prime/bin/session-id.sh +147 -0
- package/runtime/prime/bin/session-log.sh +19 -0
- package/runtime/prime/bin/session-record.sh +26 -0
- package/runtime/prime/bin/settings.sh +13 -8
- package/runtime/prime/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/prime/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/prime/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/prime/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/prime/bin/spirit-discovery.sh +4 -0
- package/runtime/prime/bin/startup-check.sh +1 -1
- package/runtime/prime/bin/telemetry.sh +11 -4
- package/runtime/prime/bin/test-baseline.sh +33 -5
- package/runtime/prime/bin/todo.sh +1 -1
- package/runtime/prime/bin/worktree-create.sh +15 -45
- package/runtime/prime/bin/worktree.sh +6 -0
- package/runtime/prime/docs/spirit-discovery.md +96 -0
- package/runtime/prime/egregore_runtime/__init__.py +18 -12
- package/runtime/prime/egregore_runtime/access.py +608 -0
- package/runtime/prime/egregore_runtime/access_cli.py +190 -0
- package/runtime/prime/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/prime/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/prime/egregore_runtime/admin_entry.py +56 -0
- package/runtime/prime/egregore_runtime/artifacts.py +222 -16
- package/runtime/prime/egregore_runtime/contracts.py +27 -0
- package/runtime/prime/egregore_runtime/eligibility.py +5 -12
- package/runtime/prime/egregore_runtime/episode_binding.py +16 -5
- package/runtime/prime/egregore_runtime/github_issues.py +2 -2
- package/runtime/prime/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/prime/egregore_runtime/harness_cli.py +674 -149
- package/runtime/prime/egregore_runtime/held.py +2436 -0
- package/runtime/prime/egregore_runtime/hosted_api.py +137 -0
- package/runtime/prime/egregore_runtime/identity.py +196 -65
- package/runtime/prime/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/prime/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/prime/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/prime/egregore_runtime/installation.py +119 -0
- package/runtime/prime/egregore_runtime/investigation.py +64 -41
- package/runtime/prime/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/prime/egregore_runtime/issue_cli.py +51 -32
- package/runtime/prime/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/prime/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/prime/egregore_runtime/issues.py +12 -6
- package/runtime/prime/egregore_runtime/knowledge.py +331 -5
- package/runtime/prime/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/prime/egregore_runtime/lifecycle.py +88 -22
- package/runtime/prime/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/prime/egregore_runtime/line_ids.py +10 -0
- package/runtime/prime/egregore_runtime/memory_push.py +349 -0
- package/runtime/prime/egregore_runtime/observe.py +64 -25
- package/runtime/prime/egregore_runtime/observers.py +19 -0
- package/runtime/prime/egregore_runtime/optional_modules.py +13 -0
- package/runtime/prime/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/prime/egregore_runtime/policy.py +651 -24
- package/runtime/prime/egregore_runtime/private_result.py +2 -1
- package/runtime/prime/egregore_runtime/projection.py +946 -0
- package/runtime/prime/egregore_runtime/quest_cli.py +20 -23
- package/runtime/prime/egregore_runtime/question_cli.py +15 -11
- package/runtime/prime/egregore_runtime/questions.py +10 -10
- package/runtime/prime/egregore_runtime/quests.py +10 -10
- package/runtime/prime/egregore_runtime/record_publish.py +548 -0
- package/runtime/prime/egregore_runtime/record_subject.py +564 -0
- package/runtime/prime/egregore_runtime/research_ingest.py +2 -4
- package/runtime/prime/egregore_runtime/runtime.py +169 -60
- package/runtime/prime/egregore_runtime/sandbox.py +194 -0
- package/runtime/prime/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/prime/egregore_runtime/scrolls.py +23 -15
- package/runtime/prime/egregore_runtime/shadow.py +262 -0
- package/runtime/prime/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/prime/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/prime/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/prime/egregore_runtime/status.py +101 -44
- package/runtime/prime/egregore_runtime/status_cli.py +13 -5
- package/runtime/prime/egregore_runtime/sync.py +39 -21
- package/runtime/prime/egregore_runtime/taste.py +934 -0
- package/runtime/prime/egregore_runtime/taste_cli.py +242 -0
- package/runtime/prime/egregore_runtime/taste_rules.py +91 -0
- package/runtime/prime/egregore_runtime/telemetry.py +26 -3
- package/runtime/prime/egregore_runtime/todo_cli.py +15 -12
- package/runtime/prime/egregore_runtime/todos.py +3 -2
- package/runtime/prime/egregore_runtime/upgrade.py +85 -58
- package/runtime/prime/egregore_runtime/writeback.py +330 -24
- package/runtime/prime/manifest.json +66 -5
- package/runtime/claude/bin/thread.sh +0 -9
- package/runtime/claude/egregore_runtime/admin_gate.py +0 -102
- package/runtime/claude/egregore_runtime/thread_cli.py +0 -173
- package/runtime/claude/egregore_runtime/threads.py +0 -521
- package/runtime/codex/bin/thread.sh +0 -9
- package/runtime/codex/egregore_runtime/admin_gate.py +0 -102
- package/runtime/codex/egregore_runtime/thread_cli.py +0 -173
- package/runtime/codex/egregore_runtime/threads.py +0 -521
- package/runtime/pi/bin/thread.sh +0 -9
- package/runtime/pi/egregore_runtime/admin_gate.py +0 -102
- package/runtime/pi/egregore_runtime/thread_cli.py +0 -173
- package/runtime/pi/egregore_runtime/threads.py +0 -521
- package/runtime/prime/bin/thread.sh +0 -9
- package/runtime/prime/egregore_runtime/admin_gate.py +0 -102
- package/runtime/prime/egregore_runtime/thread_cli.py +0 -173
- package/runtime/prime/egregore_runtime/threads.py +0 -521
|
@@ -2,9 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import contextlib
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
import stat
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
6
15
|
from typing import Mapping, Sequence
|
|
7
16
|
|
|
17
|
+
from .access import PROJECTION_ROOT, AuthorizationInputs, authorization_inputs, bounded_committed_memory, committed_memory, principal
|
|
18
|
+
from . import held, shadow
|
|
8
19
|
from .contracts import (
|
|
9
20
|
ActionAuthorizer,
|
|
10
21
|
ActorContext,
|
|
@@ -14,25 +25,517 @@ from .contracts import (
|
|
|
14
25
|
)
|
|
15
26
|
|
|
16
27
|
|
|
28
|
+
ACTIONS = ("read", "write", "append")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class Verdict:
|
|
33
|
+
allowed: bool
|
|
34
|
+
reason: str
|
|
35
|
+
canonical: str | None = None
|
|
36
|
+
resolved: Path | None = None
|
|
37
|
+
note: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
17
40
|
def _canonical_parts(path: str) -> tuple[str, ...] | None:
|
|
18
|
-
normalized = path.
|
|
41
|
+
normalized = path.strip()
|
|
42
|
+
if normalized.startswith("/") or re.match(r"^[A-Za-z]:", normalized) or "\x00" in normalized or "\\" in normalized:
|
|
43
|
+
return None
|
|
44
|
+
normalized = normalized.strip("/")
|
|
19
45
|
parts = tuple(normalized.split("/")) if normalized else ()
|
|
20
46
|
if not parts or any(part in {"", ".", ".."} for part in parts):
|
|
21
47
|
return None
|
|
22
48
|
return parts
|
|
23
49
|
|
|
24
50
|
|
|
25
|
-
def
|
|
26
|
-
|
|
51
|
+
def _resolve(path: Path) -> Path:
|
|
52
|
+
try:
|
|
53
|
+
return path.resolve(strict=True)
|
|
54
|
+
except FileNotFoundError:
|
|
55
|
+
return path.resolve(strict=False)
|
|
56
|
+
|
|
27
57
|
|
|
58
|
+
def within(path: str, prefixes: Sequence[str], *, root: Path | None = None) -> bool:
|
|
59
|
+
"""Containment only: both requested and resolved spellings must fit."""
|
|
28
60
|
canonical = _canonical_parts(path)
|
|
29
61
|
if canonical is None:
|
|
30
62
|
return False
|
|
31
|
-
for
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
if not any(canonical[:len(prefix)] == prefix for raw in prefixes if (prefix := _canonical_parts(raw))):
|
|
64
|
+
return False
|
|
65
|
+
if root is None:
|
|
66
|
+
return True
|
|
67
|
+
try:
|
|
68
|
+
if canonical[0] != "memory":
|
|
69
|
+
return False
|
|
70
|
+
resolved = _resolve(root / Path(*canonical[1:]))
|
|
71
|
+
relative = resolved.relative_to(_resolve(root))
|
|
72
|
+
physical = ("memory", *relative.parts)
|
|
73
|
+
return not any("\\" in part or "\x00" in part for part in physical) and any(
|
|
74
|
+
physical[:len(prefix)] == prefix for raw in prefixes if (prefix := _canonical_parts(raw)))
|
|
75
|
+
except (OSError, RuntimeError, ValueError):
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_admin(decision: PolicyDecision) -> bool:
|
|
80
|
+
return decision.allowed and decision.audience == "admin"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_POLICIES = frozenset({"all-access-v1", "roster-v1-shadow", "roster-v1"})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _trusted(decision: PolicyDecision) -> bool:
|
|
87
|
+
return decision.audience != "outside" and decision.policy_epoch in _POLICIES
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _ReadProjection:
|
|
91
|
+
__slots__ = ("decision", "root", "paths", "budget", "held", "rechecked", "binding")
|
|
92
|
+
|
|
93
|
+
def __init__(self, decision, root, paths, budget):
|
|
94
|
+
self.decision, self.root, self.paths, self.budget = decision, root, paths, budget
|
|
95
|
+
source = bounded_committed_memory(root, decision.memory_revision, budget=budget) if decision.memory_revision else None
|
|
96
|
+
self.held = held.bounded_tree(root, decision.released, budget=budget, committed=source)
|
|
97
|
+
self.rechecked, self.binding = False, None
|
|
98
|
+
|
|
99
|
+
def validate(self, decision, root):
|
|
100
|
+
self.budget.remaining()
|
|
101
|
+
if (decision != self.decision or Path(root) != self.root or self.root.resolve(strict=True) != self.root
|
|
102
|
+
or held._file_identity(self.root.stat()) != self.held.identity or self.held.rules != held.HELD_RULES_VERSION):
|
|
103
|
+
self.budget.fail()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def prepare_read_projection(decision, *, root, paths, budget):
|
|
107
|
+
"""Prepare complete bounded hold inputs; this object is not a reusable grant."""
|
|
108
|
+
held._read_budget(budget)
|
|
109
|
+
try:
|
|
110
|
+
root = Path(root)
|
|
111
|
+
if (type(decision) is not PolicyDecision or not decision.allowed or decision.permission != Permission.READ
|
|
112
|
+
or decision.scope not in {"read", "hand-over"} or decision.policy_epoch not in _POLICIES
|
|
113
|
+
or decision.audience not in {"member", "admin", "outside"}
|
|
114
|
+
or decision.audience == "outside" and (decision.scope != "hand-over" or not decision.memory_revision)
|
|
115
|
+
or not root.is_absolute() or root != root.resolve(strict=True)
|
|
116
|
+
or not isinstance(paths, (tuple, list)) or len(paths) > 64):
|
|
117
|
+
budget.fail()
|
|
118
|
+
selected = set()
|
|
119
|
+
for path in paths:
|
|
120
|
+
parts = _canonical_parts(path) if isinstance(path, str) else None
|
|
121
|
+
if (not parts or parts[0] != "memory" or path != "/".join(parts)
|
|
122
|
+
or len(os.fsencode(path)) > budget.limits.path_bytes):
|
|
123
|
+
budget.fail()
|
|
124
|
+
selected.add(path)
|
|
125
|
+
return _ReadProjection(decision, root, tuple(sorted(selected)), budget)
|
|
126
|
+
except (OSError, ValueError, RuntimeError):
|
|
127
|
+
budget.fail()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _projection(projection, decision, root):
|
|
131
|
+
if type(projection) is not _ReadProjection:
|
|
132
|
+
raise held.BoundedReadUnavailable("bounded read unavailable")
|
|
133
|
+
projection.validate(decision, root)
|
|
134
|
+
return projection
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _projection_path(projection, decision, root, path):
|
|
138
|
+
projection = _projection(projection, decision, root)
|
|
139
|
+
if path not in projection.paths or not within(path, decision.read_prefixes):
|
|
140
|
+
return None
|
|
141
|
+
relative = path.removeprefix("memory/")
|
|
142
|
+
entry = projection.held.documents.get(relative)
|
|
143
|
+
if entry is None:
|
|
144
|
+
return None
|
|
145
|
+
target, raw = entry
|
|
146
|
+
if not within("memory/" + target, decision.read_prefixes):
|
|
147
|
+
return None
|
|
148
|
+
if projection.held.source is not None:
|
|
149
|
+
if projection.held.source.entries.get(relative, (None,))[0] not in {"100644", "100755"}:
|
|
150
|
+
return None
|
|
151
|
+
else:
|
|
152
|
+
original, physical = projection.held.identities[relative]
|
|
153
|
+
if not stat.S_ISREG(original[2]) or original[3] != 1 or physical[3] != 1:
|
|
154
|
+
return None
|
|
155
|
+
return relative, target, raw
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_projection_header(decision, path, *, root, projection, max_bytes=16384):
|
|
159
|
+
try:
|
|
160
|
+
projection = _projection(projection, decision, root)
|
|
161
|
+
if type(max_bytes) is not int or not 1 <= max_bytes <= 16384:
|
|
162
|
+
projection.budget.fail()
|
|
163
|
+
entry = _projection_path(projection, decision, root, path)
|
|
164
|
+
if entry is None:
|
|
165
|
+
return None
|
|
166
|
+
raw = entry[2]
|
|
167
|
+
if not raw.startswith(b"---\n"):
|
|
168
|
+
return None
|
|
169
|
+
end = raw.find(b"\n---\n", 3, max_bytes)
|
|
170
|
+
if end < 0:
|
|
171
|
+
end = raw.find(b"\n---\r\n", 3, max_bytes)
|
|
172
|
+
size = end + 6
|
|
173
|
+
else:
|
|
174
|
+
size = end + 5
|
|
175
|
+
if end < 0 or size > max_bytes:
|
|
176
|
+
return None
|
|
177
|
+
projection.budget.charge("selected_bytes", size)
|
|
178
|
+
return raw[:size]
|
|
179
|
+
except (OSError, ValueError, RuntimeError):
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def read_projection_bytes(decision, path, *, root, projection):
|
|
184
|
+
try:
|
|
185
|
+
projection = _projection(projection, decision, root)
|
|
186
|
+
entry = _projection_path(projection, decision, root, path)
|
|
187
|
+
if entry is None:
|
|
188
|
+
return None
|
|
189
|
+
raw = entry[2]
|
|
190
|
+
if len(raw) > 256 * 1024:
|
|
191
|
+
return None
|
|
192
|
+
verdict = check(decision, path, root=root, observe_shadow=False, max_source_bytes=256 * 1024,
|
|
193
|
+
read_projection=projection)
|
|
194
|
+
if not verdict.allowed or bytes_held(decision, path, raw, root=root, read_projection=projection):
|
|
195
|
+
return None
|
|
196
|
+
projection.budget.charge("selected_bytes", len(raw))
|
|
197
|
+
return raw
|
|
198
|
+
except (OSError, ValueError, RuntimeError):
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def recheck_read_projection(decision, *, root, projection):
|
|
203
|
+
try:
|
|
204
|
+
projection = _projection(projection, decision, root)
|
|
205
|
+
projection.budget.recheck()
|
|
206
|
+
projection.held.recheck()
|
|
207
|
+
projection.validate(decision, root)
|
|
208
|
+
preimage = {"schema": "egregore.policy-read-projection/v1", "decision": decision.to_dict(),
|
|
209
|
+
"root": str(projection.root), "root_identity": projection.held.identity,
|
|
210
|
+
"source_revision": decision.memory_revision, "inventory": projection.held.fingerprint,
|
|
211
|
+
"rules": projection.held.rules, "releases": list(decision.released),
|
|
212
|
+
"selection": [{"path": path, "target": entry[0], "digest": hashlib.sha256(entry[1]).hexdigest()}
|
|
213
|
+
if (entry := projection.held.documents.get(path.removeprefix("memory/"))) is not None
|
|
214
|
+
else {"path": path, "target": None, "digest": None} for path in projection.paths],
|
|
215
|
+
"authority": sorted((list(key), value) for key, value in projection.budget.authority.items())}
|
|
216
|
+
projection.binding = hashlib.sha256(json.dumps(preimage, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
|
|
217
|
+
projection.rechecked = True
|
|
218
|
+
projection.budget.remaining()
|
|
219
|
+
return True
|
|
220
|
+
except (OSError, ValueError, RuntimeError):
|
|
221
|
+
if type(projection) is _ReadProjection:
|
|
222
|
+
projection.budget.invalid = True
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def read_projection_binding(decision, *, root, projection):
|
|
227
|
+
try:
|
|
228
|
+
projection = _projection(projection, decision, root)
|
|
229
|
+
return projection.binding if projection.rechecked else None
|
|
230
|
+
except (OSError, ValueError, RuntimeError):
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def tree_for(decision: PolicyDecision, root: Path) -> held.Tree:
|
|
235
|
+
"""The memory a decision judges every class against: as committed at its revision, or as walked,
|
|
236
|
+
with the hint only for the operator's own configured decision."""
|
|
237
|
+
if decision.memory_revision:
|
|
238
|
+
return held.committed_tree(committed_memory(root, decision.memory_revision), decision.released)
|
|
239
|
+
return held.tree(root, decision.released, trust_cache=_trusted(decision))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _committed(decision: PolicyDecision, relative: str, action: str, root: Path, every: bool) -> tuple[int, ...] | str:
|
|
243
|
+
"""Held classes of the committed file, or the reason committed memory refuses it."""
|
|
244
|
+
try:
|
|
245
|
+
tree = committed_memory(root, decision.memory_revision)
|
|
246
|
+
kind = tree.kind(relative)
|
|
247
|
+
if action == "append":
|
|
248
|
+
return "exists" if kind else ()
|
|
249
|
+
if kind != "file":
|
|
250
|
+
return "link" if kind == "link" else "not-committed"
|
|
251
|
+
data = tree.read(relative)
|
|
252
|
+
return tree_for(decision, root).classes(relative, data) if every else held.blob_classes(data)
|
|
253
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
254
|
+
return "uncommitted-source"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _counted_later(decision: PolicyDecision) -> bool:
|
|
258
|
+
"""A roster-v1-shadow read: only class 1 holds it, so the class roster-v1 would hold is counted, not awaited."""
|
|
259
|
+
return decision.scope == "read" and decision.policy_epoch == "roster-v1-shadow"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def check(decision: PolicyDecision, path: str, action: str = "read", *, root: Path, explain: bool = False,
|
|
263
|
+
observe_shadow: bool = True, max_source_bytes: int | None = None, read_projection=None) -> Verdict:
|
|
264
|
+
"""The authority predicate; callers open or write only the returned path.
|
|
265
|
+
|
|
266
|
+
A decision bound to a ``memory_revision`` is also decided on memory as
|
|
267
|
+
committed there; an outside decision without one is refused, so nothing
|
|
268
|
+
handed to another principal reads working-tree bytes or hold markers.
|
|
269
|
+
|
|
270
|
+
Under roster-v1-shadow a read holds class 1 alone, read from the bytes;
|
|
271
|
+
the class roster-v1 would hold is counted by ``shadow.defer``, which
|
|
272
|
+
never builds a tree on the read path. ``explain`` judges it now, for a
|
|
273
|
+
verdict's ``note``: ``access check`` names it. A bounded read-only caller may
|
|
274
|
+
set ``observe_shadow=False`` to suppress this optional shadow accounting,
|
|
275
|
+
including its deferred tree walk and writes, without changing the verdict.
|
|
276
|
+
This does not make roster-v1 or an explained read a bounded tree-free read.
|
|
277
|
+
``max_source_bytes`` optionally bounds each working-file authority read
|
|
278
|
+
(plus one byte to detect overflow). It does not bound a rule tree, and
|
|
279
|
+
committed-memory reads refuse this option until their owner supports it.
|
|
280
|
+
"""
|
|
281
|
+
projected = None
|
|
282
|
+
if read_projection is not None:
|
|
283
|
+
try:
|
|
284
|
+
_projection(read_projection, decision, root)
|
|
285
|
+
if action != "read":
|
|
286
|
+
return Verdict(False, "invalid-action")
|
|
287
|
+
projected = _projection_path(read_projection, decision, root, path)
|
|
288
|
+
if projected is None:
|
|
289
|
+
return Verdict(False, "bounded-read-unavailable")
|
|
290
|
+
except (OSError, ValueError, RuntimeError):
|
|
291
|
+
return Verdict(False, "bounded-read-unavailable")
|
|
292
|
+
if action not in ACTIONS:
|
|
293
|
+
return Verdict(False, "invalid-action")
|
|
294
|
+
if decision.scope not in {"read", "hand-over"}:
|
|
295
|
+
return Verdict(False, "invalid-scope")
|
|
296
|
+
if not decision.allowed:
|
|
297
|
+
return Verdict(False, "denied")
|
|
298
|
+
if max_source_bytes is not None:
|
|
299
|
+
if type(max_source_bytes) is not int or not 1 <= max_source_bytes <= 8 * 1024 * 1024:
|
|
300
|
+
return Verdict(False, "invalid-source-limit")
|
|
301
|
+
if decision.memory_revision and read_projection is None:
|
|
302
|
+
return Verdict(False, "bounded-committed-read-unavailable")
|
|
303
|
+
if decision.audience == "root": # a read-only copy: only the generation its slot admits now, never a stale one
|
|
304
|
+
from .projection import serves
|
|
305
|
+
if not serves(root):
|
|
306
|
+
return Verdict(False, "being-rebuilt")
|
|
307
|
+
prefixes = getattr(decision, action + "_prefixes") + (decision.write_prefixes if action == "append" else ())
|
|
308
|
+
if not prefixes:
|
|
309
|
+
return Verdict(False, "outside-scope")
|
|
310
|
+
canonical = _canonical_parts(path)
|
|
311
|
+
if canonical is None:
|
|
312
|
+
return Verdict(False, "invalid-path")
|
|
313
|
+
parts, canonical = canonical, "/".join(canonical)
|
|
314
|
+
if parts[0] != "memory":
|
|
315
|
+
return Verdict(False, "outside-scope", canonical)
|
|
316
|
+
if decision.audience == "outside" and not decision.memory_revision:
|
|
317
|
+
return Verdict(False, "uncommitted-source", canonical)
|
|
318
|
+
try:
|
|
319
|
+
requested = root / Path(*parts[1:])
|
|
320
|
+
resolved, base = ((root / projected[1], root) if projected is not None else (_resolve(requested), _resolve(root)))
|
|
321
|
+
if not resolved.is_relative_to(base):
|
|
322
|
+
return Verdict(False, "outside-root", canonical)
|
|
323
|
+
if not within(canonical, prefixes, root=None if projected is not None else root):
|
|
324
|
+
return Verdict(False, "outside-scope", canonical)
|
|
325
|
+
if action == "append" and (os.path.lexists(resolved) or os.path.lexists(requested)):
|
|
326
|
+
return Verdict(False, "exists", canonical)
|
|
327
|
+
every = not (decision.scope == "read" and decision.policy_epoch == "all-access-v1") # else class 1 alone matters
|
|
328
|
+
relative = "/".join(parts[1:])
|
|
329
|
+
found = (read_projection.held.classes(relative, projected[2], projected[1]) if projected is not None
|
|
330
|
+
else _committed(decision, relative, action, root, every) if decision.memory_revision else None)
|
|
331
|
+
if projected is not None and max_source_bytes is not None and len(projected[2]) > max_source_bytes:
|
|
332
|
+
return Verdict(False, "source-limit", canonical)
|
|
333
|
+
if isinstance(found, str):
|
|
334
|
+
return Verdict(False, found, canonical)
|
|
335
|
+
if is_admin(decision):
|
|
336
|
+
return Verdict(True, "allowed", canonical, resolved)
|
|
337
|
+
released = (within(canonical, decision.released) and within("memory/" + projected[1], decision.released)
|
|
338
|
+
if projected is not None else within(canonical, decision.released, root=None if decision.memory_revision else root))
|
|
339
|
+
later = None
|
|
340
|
+
if found is None and not every and max_source_bytes is None:
|
|
341
|
+
found = held.classes(resolved)
|
|
342
|
+
elif found is None:
|
|
343
|
+
if not resolved.is_file():
|
|
344
|
+
data = None
|
|
345
|
+
elif max_source_bytes is None:
|
|
346
|
+
data = resolved.read_bytes()
|
|
347
|
+
else:
|
|
348
|
+
with resolved.open("rb") as source:
|
|
349
|
+
data = source.read(max_source_bytes + 1)
|
|
350
|
+
if len(data) > max_source_bytes:
|
|
351
|
+
return Verdict(False, "source-limit", canonical)
|
|
352
|
+
physical = resolved.relative_to(base).as_posix()
|
|
353
|
+
if data is None:
|
|
354
|
+
found = ()
|
|
355
|
+
elif not every:
|
|
356
|
+
found = held.blob_classes(data)
|
|
357
|
+
elif _counted_later(decision) and not explain:
|
|
358
|
+
found = held.admin_class(data)
|
|
359
|
+
later = None if found or released else (relative, physical, data)
|
|
360
|
+
else:
|
|
361
|
+
found = tree_for(decision, root).classes(relative, data, physical)
|
|
362
|
+
hold, would = held.judge(decision.scope, decision.policy_epoch, found, released)
|
|
363
|
+
if hold:
|
|
364
|
+
return Verdict(False, f"held:{hold}", canonical)
|
|
365
|
+
if observe_shadow and would and read_projection is None:
|
|
366
|
+
shadow.note(root, would)
|
|
367
|
+
if observe_shadow and later and read_projection is None:
|
|
368
|
+
shadow.defer(root, decision.released, *later)
|
|
369
|
+
except (OSError, RuntimeError, ValueError):
|
|
370
|
+
return Verdict(False, "invalid-path", canonical)
|
|
371
|
+
note = f"shadow:{would}" if would else "released" if released and any(number != 1 for number in found) else ""
|
|
372
|
+
return Verdict(True, "allowed", canonical, resolved, note)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
_REFUSALS = {
|
|
376
|
+
"outside-root": "it resolves outside memory", "invalid-path": "not a memory path",
|
|
377
|
+
"invalid-action": "not an action", "invalid-scope": "not a decision scope",
|
|
378
|
+
"exists": "it already exists; append adds new files only", "not-committed": "it is not in committed memory",
|
|
379
|
+
"link": "it is a link in committed memory", "uncommitted-source": "decided without the committed settings",
|
|
380
|
+
"being-rebuilt": "this read-only copy is being rebuilt",
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def rule(decision: PolicyDecision, verdict: Verdict, action: str) -> str:
|
|
385
|
+
"""Name the rule that decided a verdict, in plain words."""
|
|
386
|
+
if verdict.allowed:
|
|
387
|
+
granted = getattr(decision, action + "_prefixes") + (decision.write_prefixes if action == "append" else ())
|
|
388
|
+
prefix = next((p for p in granted if within(verdict.canonical or "", (p,))), "")
|
|
389
|
+
named = f"{action} grant {prefix}" + (" · admin" if is_admin(decision) else "")
|
|
390
|
+
if verdict.note.startswith("shadow:"):
|
|
391
|
+
number = int(verdict.note.removeprefix("shadow:"))
|
|
392
|
+
return named + f" · roster-v1 would hold it ({held.CLASS_NAMES.get(number, f'class {number}')})"
|
|
393
|
+
return named + (" · released by access.release" if verdict.note == "released" else "")
|
|
394
|
+
if verdict.reason.startswith("held:"):
|
|
395
|
+
number = int(verdict.reason.removeprefix("held:"))
|
|
396
|
+
return f"held: {held.CLASS_NAMES.get(number, f'class {number}')}"
|
|
397
|
+
if verdict.reason == "denied":
|
|
398
|
+
return "no grant: " + "; ".join(decision.reasons)
|
|
399
|
+
if verdict.reason == "outside-scope":
|
|
400
|
+
return f"outside the {action} grant"
|
|
401
|
+
return _REFUSALS.get(verdict.reason, verdict.reason)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def allows(decision: PolicyDecision, path: str, action: str = "read", *, root: Path) -> bool:
|
|
405
|
+
return check(decision, path, action, root=root).allowed
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def bytes_held(decision: PolicyDecision, path: str, data: bytes, *, root: Path, read_projection=None) -> int | None:
|
|
409
|
+
"""The class that holds ``data``, bytes a caller read at memory ``path``, as ``check`` would hold
|
|
410
|
+
the file if it read them: every class the decision applies, less the rule classes ``access.release``
|
|
411
|
+
lifts. None for an admin. ``check`` decides the path and this the bytes read from it, so a file
|
|
412
|
+
rewritten between the read and the check never carries bytes that were held. Fails closed."""
|
|
413
|
+
projected = None
|
|
414
|
+
if read_projection is not None:
|
|
415
|
+
try:
|
|
416
|
+
projected = _projection_path(read_projection, decision, root, path)
|
|
417
|
+
if projected is None or type(data) is not bytes or data != projected[2]:
|
|
418
|
+
read_projection.budget.fail()
|
|
419
|
+
except (OSError, ValueError, RuntimeError, AttributeError):
|
|
420
|
+
return 1
|
|
421
|
+
if is_admin(decision):
|
|
422
|
+
return None
|
|
423
|
+
parts = _canonical_parts(path)
|
|
424
|
+
try:
|
|
425
|
+
if parts is None or parts[0] != "memory":
|
|
426
|
+
raise ValueError("not a memory path")
|
|
427
|
+
relative = "/".join(parts[1:])
|
|
428
|
+
every = not (decision.scope == "read" and decision.policy_epoch == "all-access-v1")
|
|
429
|
+
if projected is not None:
|
|
430
|
+
found = read_projection.held.classes(relative, data, projected[1])
|
|
431
|
+
elif not every:
|
|
432
|
+
found = held.blob_classes(data)
|
|
433
|
+
elif _counted_later(decision): # only class 1 holds it; check counts what roster-v1 would hold
|
|
434
|
+
found = held.admin_class(data)
|
|
435
|
+
elif decision.memory_revision:
|
|
436
|
+
found = tree_for(decision, root).classes(relative, data)
|
|
437
|
+
else:
|
|
438
|
+
physical = _resolve(root / relative).relative_to(_resolve(root)).as_posix()
|
|
439
|
+
found = tree_for(decision, root).classes(relative, data, physical)
|
|
440
|
+
released = (within("/".join(parts), decision.released) and within("memory/" + projected[1], decision.released)
|
|
441
|
+
if projected is not None else within("/".join(parts), decision.released, root=None if decision.memory_revision else root))
|
|
442
|
+
return held.judge(decision.scope, decision.policy_epoch, found, released)[0]
|
|
443
|
+
except (OSError, RuntimeError, ValueError, subprocess.SubprocessError):
|
|
444
|
+
return 1
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def strips(decision: PolicyDecision) -> bool:
|
|
448
|
+
"""Whether a decision strips held names: a non-admin's, in a scope that applies every class (``held.judge``)."""
|
|
449
|
+
return not is_admin(decision) and not (decision.scope == "read" and decision.policy_epoch in held.LENIENT)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
_REDACTORS: dict[tuple, held.Redactor] = {}
|
|
453
|
+
_DOTTED: dict[str, tuple[float, tuple]] = {}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _dotted(root: Path) -> tuple[tuple, tuple]:
|
|
457
|
+
"""What the held walk skips and ``check`` still reads, as listed within ``held.FRESH_SECONDS``: Markdown
|
|
458
|
+
documents under a dot name (``.git`` aside) with the stat of what they read as, and folder links inside
|
|
459
|
+
memory as (link, target)."""
|
|
460
|
+
now, seen = time.monotonic(), _DOTTED.get(str(root))
|
|
461
|
+
if seen is None or now - seen[0] >= held.FRESH_SECONDS:
|
|
462
|
+
found, links, base = [], [], _resolve(root)
|
|
463
|
+
for folder, folders, files in os.walk(root):
|
|
464
|
+
parts = Path(folder).relative_to(root).parts
|
|
465
|
+
for name in [name for name in folders if os.path.islink(os.path.join(folder, name))]:
|
|
466
|
+
with contextlib.suppress(OSError, RuntimeError, ValueError):
|
|
467
|
+
target = _resolve(Path(folder, name))
|
|
468
|
+
links += [("/".join((*parts, name)), target.relative_to(base).as_posix())] if target.is_dir() else []
|
|
469
|
+
folders[:] = [name for name in folders if name != ".git"]
|
|
470
|
+
for name in files:
|
|
471
|
+
if name.lower().endswith(".md") and any(part.startswith(".") for part in (*parts, name)):
|
|
472
|
+
with contextlib.suppress(OSError):
|
|
473
|
+
info = os.stat(os.path.join(folder, name))
|
|
474
|
+
found.append(("/".join((*parts, name)), info.st_mtime_ns, info.st_size, info.st_ino))
|
|
475
|
+
seen = _DOTTED[str(root)] = (now, (tuple(sorted(found)), tuple(sorted(links))))
|
|
476
|
+
return seen[1]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def redactor(decision: PolicyDecision, root: Path) -> held.Redactor | None:
|
|
480
|
+
"""The held names a decision strips, from the memory its holds are judged on (``tree_for``); None when it
|
|
481
|
+
strips none. Each held document is read for its title and ids; the shown ones only for their titles,
|
|
482
|
+
once a held title turns up in a text. Shown means readable by this decision: a committed link, or a
|
|
483
|
+
link out of the read grant, shields no name."""
|
|
484
|
+
if not strips(decision):
|
|
485
|
+
return None
|
|
486
|
+
root = Path(root)
|
|
487
|
+
tree = tree_for(decision, root)
|
|
488
|
+
memory = committed_memory(root, decision.memory_revision) if decision.memory_revision else None
|
|
489
|
+
dotted, links = ((tuple((name,) for name in memory.entries if name.lower().endswith(".md")
|
|
490
|
+
and any(part.startswith(".") for part in name.split("/"))), ()) if memory else _dotted(root))
|
|
491
|
+
stamp = tree.walk or decision.memory_revision
|
|
492
|
+
key = (str(root), stamp, dotted, links, decision.released, decision.read_prefixes, _trusted(decision))
|
|
493
|
+
if stamp and key in _REDACTORS:
|
|
494
|
+
return _REDACTORS[key]
|
|
495
|
+
if memory is not None:
|
|
496
|
+
def read(rel: str) -> bytes:
|
|
497
|
+
target = held.committed_target(memory, rel)
|
|
498
|
+
return memory.read(target) if target else b""
|
|
499
|
+
else:
|
|
500
|
+
base = _resolve(root)
|
|
501
|
+
|
|
502
|
+
def read(rel: str) -> bytes:
|
|
503
|
+
path = _resolve(base / rel)
|
|
504
|
+
return path.read_bytes() if path.is_relative_to(base) and path.is_file() else b""
|
|
505
|
+
holds = set(tree.held)
|
|
506
|
+
shown = [rel for rel, target in tree.listed if rel not in holds and (target == rel if memory else
|
|
507
|
+
within("memory/" + target, decision.read_prefixes)) and within("memory/" + rel, decision.read_prefixes)]
|
|
508
|
+
whole = replace(decision, read_prefixes=("memory",)) # held whatever the grant, as the walked tree is
|
|
509
|
+
for rel, *_ in dotted: # judged one by one, as a read of each would be
|
|
510
|
+
holds.update((rel,) if check(whole, "memory/" + rel, "read", root=root).reason.startswith("held:") else ())
|
|
511
|
+
shown += [rel] if check(decision, "memory/" + rel, "read", root=root).allowed else []
|
|
512
|
+
named = {rel: {rel} for rel in holds} # and every spelling through a folder link, a chain of them too
|
|
513
|
+
for _ in range(held.LINK_HOPS if links else 0):
|
|
514
|
+
grown = {rel: names | {link + name[len(target):] for name in names for link, target in links
|
|
515
|
+
if name.startswith(target + "/")} for rel, names in named.items()}
|
|
516
|
+
named, done = grown, grown == named
|
|
517
|
+
if done:
|
|
518
|
+
break
|
|
519
|
+
found = held.Redactor([("memory/" + name, *held.describe("memory/" + rel, read(rel)))
|
|
520
|
+
for rel in sorted(holds) for name in sorted(named[rel])],
|
|
521
|
+
["memory/" + rel for rel in shown],
|
|
522
|
+
lambda: [held.describe("memory/" + rel, read(rel))[0] for rel in shown])
|
|
523
|
+
if stamp:
|
|
524
|
+
while len(_REDACTORS) >= 4:
|
|
525
|
+
_REDACTORS.pop(next(iter(_REDACTORS)))
|
|
526
|
+
_REDACTORS[key] = found
|
|
527
|
+
return found
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def narrow(decision: PolicyDecision, action: str, prefixes: Sequence[str]) -> PolicyDecision:
|
|
531
|
+
if action not in ACTIONS:
|
|
532
|
+
raise ValueError("invalid action")
|
|
533
|
+
selected = tuple(prefix for prefix in prefixes if within(prefix, getattr(decision, action + "_prefixes")))
|
|
534
|
+
return replace(decision, **{action + "_prefixes": selected}, **({"scopes": selected} if action_for(decision.permission) == action else {}))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def action_for(permission: Permission) -> str:
|
|
538
|
+
return "read" if permission in {Permission.READ, Permission.DISCOVER} else "write"
|
|
36
539
|
|
|
37
540
|
|
|
38
541
|
@dataclass(frozen=True, slots=True)
|
|
@@ -40,6 +543,18 @@ class ActorGrant:
|
|
|
40
543
|
permissions: frozenset[Permission]
|
|
41
544
|
scopes: tuple[str, ...] = ()
|
|
42
545
|
resource_ids: tuple[str, ...] = ()
|
|
546
|
+
audience: str = "member"
|
|
547
|
+
read: tuple[str, ...] | None = None
|
|
548
|
+
write: tuple[str, ...] | None = None
|
|
549
|
+
append: tuple[str, ...] | None = None
|
|
550
|
+
|
|
551
|
+
def decision_fields(self, allowed: bool) -> dict:
|
|
552
|
+
return dict(
|
|
553
|
+
audience=self.audience if allowed else "member", scope="read", holds_epoch="",
|
|
554
|
+
read_prefixes=self.read if self.read is not None else (self.scopes if any(action_for(p) == "read" for p in self.permissions) else ()),
|
|
555
|
+
write_prefixes=self.write if self.write is not None else (self.scopes if any(action_for(p) == "write" for p in self.permissions) else ()),
|
|
556
|
+
append_prefixes=self.append if self.append is not None else (),
|
|
557
|
+
)
|
|
43
558
|
|
|
44
559
|
|
|
45
560
|
@dataclass(slots=True)
|
|
@@ -96,6 +611,7 @@ class StaticPolicy(ActionAuthorizer):
|
|
|
96
611
|
resource_ids=grant.resource_ids if grant else (),
|
|
97
612
|
scopes=grant.scopes if grant else (),
|
|
98
613
|
reasons=tuple(reasons) if reasons else ("explicit actor grant",),
|
|
614
|
+
**(grant or ActorGrant(frozenset())).decision_fields(not reasons),
|
|
99
615
|
)
|
|
100
616
|
|
|
101
617
|
def authorize_action(
|
|
@@ -124,21 +640,132 @@ class StaticPolicy(ActionAuthorizer):
|
|
|
124
640
|
resource_ids=tuple(resource_ids),
|
|
125
641
|
scopes=grant.scopes if grant else (),
|
|
126
642
|
reasons=tuple(reasons) if reasons else ("explicit actor grant",),
|
|
643
|
+
**(grant or ActorGrant(frozenset())).decision_fields(not reasons),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@dataclass(slots=True)
|
|
648
|
+
class _ConfiguredPolicy(ActionAuthorizer):
|
|
649
|
+
inputs: AuthorizationInputs
|
|
650
|
+
|
|
651
|
+
def _policy(self, actor: ActorContext) -> StaticPolicy:
|
|
652
|
+
return StaticPolicy(actor.profile.org_id, self.inputs.policy, {
|
|
653
|
+
actor.actor.actor_id: ActorGrant(frozenset(Permission), scopes=("memory",)),
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
def _finish(self, actor: ActorContext, decision: PolicyDecision) -> PolicyDecision:
|
|
657
|
+
alias = actor.actor.aliases.get("github.username")
|
|
658
|
+
handle = alias.strip().casefold() if isinstance(alias, str) else ""
|
|
659
|
+
reasons = list(decision.reasons) if not decision.allowed else []
|
|
660
|
+
if self.inputs.problem:
|
|
661
|
+
reasons.append(self.inputs.problem)
|
|
662
|
+
if handle and handle in self.inputs.removed:
|
|
663
|
+
reasons.append("this person was removed from the organization")
|
|
664
|
+
if self.inputs.policy not in {"all-access-v1", "roster-v1-shadow", "roster-v1"}:
|
|
665
|
+
reasons.append(f"unknown access policy {self.inputs.policy}")
|
|
666
|
+
return replace(decision, allowed=not reasons, reasons=tuple(reasons) or decision.reasons,
|
|
667
|
+
audience="admin" if not reasons and handle and handle in self.inputs.admins else "member",
|
|
668
|
+
holds_epoch=held.holds_epoch(self.inputs.admins, self.inputs.released),
|
|
669
|
+
released=self.inputs.released)
|
|
670
|
+
|
|
671
|
+
def authorize_observe(self, actor: ActorContext, request: RetrievalRequest) -> PolicyDecision:
|
|
672
|
+
return self._finish(actor, self._policy(actor).authorize_observe(actor, request))
|
|
673
|
+
|
|
674
|
+
def authorize_action(self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
|
|
675
|
+
return self._finish(actor, self._policy(actor).authorize_action(actor, permission, resource_ids))
|
|
676
|
+
|
|
677
|
+
def authorize_principal(self, handle: str, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
|
|
678
|
+
handle = handle.strip().casefold()
|
|
679
|
+
row = principal(self.inputs, handle)
|
|
680
|
+
reasons = [reason for reason in (self.inputs.problem, self.inputs.principals_problem) if reason]
|
|
681
|
+
if row is None:
|
|
682
|
+
reasons.append(f"no outside principal {handle}")
|
|
683
|
+
elif row.problem:
|
|
684
|
+
reasons.append(f"access.principals.{handle} is invalid: {row.problem}")
|
|
685
|
+
if row and (row.handle in self.inputs.removed or row.identity in self.inputs.removed):
|
|
686
|
+
reasons.append("this person was removed from the organization")
|
|
687
|
+
action = action_for(permission)
|
|
688
|
+
if permission not in {Permission.DISCOVER, Permission.READ, Permission.WRITE}:
|
|
689
|
+
reasons.append(f"outside principals cannot {permission.value}")
|
|
690
|
+
elif row and not (row.read if action == "read" else row.append):
|
|
691
|
+
reasons.append(f"{handle} holds no {'read' if action == 'read' else 'append'} grant")
|
|
692
|
+
if self.inputs.policy not in {"all-access-v1", "roster-v1-shadow", "roster-v1"}:
|
|
693
|
+
reasons.append(f"unknown access policy {self.inputs.policy}")
|
|
694
|
+
# A decision carries only the action it decided, so nothing it holds goes unchecked by as_principal.
|
|
695
|
+
read = row.read if not reasons and row and action == "read" else ()
|
|
696
|
+
append = row.append if not reasons and row and action != "read" else ()
|
|
697
|
+
return PolicyDecision(
|
|
698
|
+
allowed=not reasons, actor_id=f"principal:{handle}", org_id=self.inputs.org_id,
|
|
699
|
+
permission=permission, policy_epoch=self.inputs.policy, resource_ids=tuple(resource_ids),
|
|
700
|
+
reasons=tuple(reasons) or ("outside principal grant",), audience="outside", scope="hand-over",
|
|
701
|
+
read_prefixes=read, append_prefixes=append, write_prefixes=(), scopes=read if action == "read" else (),
|
|
702
|
+
holds_epoch=held.holds_epoch(self.inputs.admins, self.inputs.released),
|
|
703
|
+
released=self.inputs.released, memory_revision=self.inputs.memory_revision,
|
|
127
704
|
)
|
|
128
705
|
|
|
129
706
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
707
|
+
@dataclass(slots=True)
|
|
708
|
+
class _RootPolicy(_ConfiguredPolicy):
|
|
709
|
+
"""A Runtime over a projection root (``access materialize``) reads the generation the root admits and writes
|
|
710
|
+
nothing. Whoever its copied settings or identity name, the decision is the same: audience ``root``, read
|
|
711
|
+
``memory`` (all of it is the grant, held material and held names already gone), and ``check`` refuses it while
|
|
712
|
+
the root is dark."""
|
|
713
|
+
|
|
714
|
+
def _decide(self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
|
|
715
|
+
from .projection import READ_ONLY
|
|
716
|
+
reasons = [self.inputs.problem] if self.inputs.problem else []
|
|
717
|
+
if permission not in {Permission.READ, Permission.DISCOVER}:
|
|
718
|
+
reasons = [READ_ONLY]
|
|
719
|
+
granted = () if reasons else ("memory",)
|
|
720
|
+
return PolicyDecision(allowed=not reasons, actor_id=actor.actor.actor_id, org_id=actor.profile.org_id,
|
|
721
|
+
permission=permission, policy_epoch="all-access-v1", resource_ids=tuple(resource_ids),
|
|
722
|
+
scopes=granted, reasons=tuple(reasons) or ("read-only copy",), audience="root",
|
|
723
|
+
read_prefixes=granted)
|
|
724
|
+
|
|
725
|
+
def authorize_observe(self, actor: ActorContext, request: RetrievalRequest) -> PolicyDecision:
|
|
726
|
+
return self._decide(actor, Permission.READ)
|
|
727
|
+
|
|
728
|
+
def authorize_action(self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
|
|
729
|
+
return self._decide(actor, permission, resource_ids)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def policy_for(inputs: AuthorizationInputs) -> ActionAuthorizer:
|
|
733
|
+
"""Build operator policy and authorize_principal decisions from authorization inputs."""
|
|
734
|
+
return _RootPolicy(inputs) if inputs.source == PROJECTION_ROOT else _ConfiguredPolicy(inputs)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def committed_settings(root: Path, decision: PolicyDecision, *, read_budget=None) -> AuthorizationInputs | None:
|
|
738
|
+
"""The committed inputs an outside principal's decision (``as_principal``) was made from, read again for what a
|
|
739
|
+
hand-over copies besides memory; None once they make another decision (the base moved in between)."""
|
|
740
|
+
handle = decision.actor_id.removeprefix("principal:")
|
|
741
|
+
inputs = authorization_inputs(root, handle, read_budget=read_budget)
|
|
742
|
+
return inputs if policy_for(inputs).authorize_principal(handle, decision.permission) == decision else None
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def as_principal(root: Path, actor: ActorContext, operator: PolicyDecision, handle: str,
|
|
746
|
+
permission: Permission, *, read_budget=None) -> PolicyDecision:
|
|
747
|
+
"""Act for an outside principal: its committed grant, refused unless the actor holds all of it.
|
|
748
|
+
|
|
749
|
+
The one way a Runtime surface decides another principal. The principal's
|
|
750
|
+
grant, policy, releases and memory come only from the committed base; the
|
|
751
|
+
acting operator stays an operator: ``operator`` is their own working-tree
|
|
752
|
+
decision, and they must also be eligible on the committed base (a removal
|
|
753
|
+
undone only in the working tree acts for no one). Every prefix the result
|
|
754
|
+
holds must sit inside the operator's for that action.
|
|
755
|
+
"""
|
|
756
|
+
handle = handle.strip().casefold()
|
|
757
|
+
policy = policy_for(authorization_inputs(root, handle, read_budget=read_budget))
|
|
758
|
+
decision = policy.authorize_principal(handle, permission)
|
|
759
|
+
if not decision.allowed:
|
|
760
|
+
return decision
|
|
761
|
+
reasons = [] if operator.allowed and policy.authorize_action(actor, permission).allowed else [
|
|
762
|
+
"whoever acts for them holds no grant"]
|
|
763
|
+
if operator.org_id != decision.org_id:
|
|
764
|
+
reasons.append("the shared settings name another organization")
|
|
765
|
+
for action, holding in (("read", operator.read_prefixes), ("append", operator.append_prefixes + operator.write_prefixes)):
|
|
766
|
+
reasons += [f"acting for {handle} would widen {action} to {prefix}"
|
|
767
|
+
for prefix in getattr(decision, action + "_prefixes") if not within(prefix, holding)]
|
|
768
|
+
if not reasons:
|
|
769
|
+
return decision
|
|
770
|
+
return replace(decision, allowed=False, reasons=tuple(reasons),
|
|
771
|
+
read_prefixes=(), append_prefixes=(), write_prefixes=(), scopes=())
|