@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18
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/README.md +134 -15
- package/assets/integrations/skills/pappra/SKILL.md +28 -7
- package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
- package/assets/integrations/skills/pappra/references/environment.md +360 -0
- package/assets/integrations/skills/pappra/references/hosts.md +20 -0
- package/assets/integrations/skills/pappra/references/operations.md +36 -5
- package/dist/adapters/pi/recovery-extension.d.ts +65 -0
- package/dist/adapters/pi/recovery-extension.js +238 -0
- package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
- package/dist/adapters/pi/session-transition-extension.js +265 -0
- package/dist/app/automatic-handoff-events.d.ts +26 -0
- package/dist/app/automatic-handoff-events.js +134 -0
- package/dist/app/bounded-file.d.ts +2 -0
- package/dist/app/bounded-file.js +2 -0
- package/dist/app/capability-evidence/claude.d.ts +37 -0
- package/dist/app/capability-evidence/claude.js +206 -0
- package/dist/app/capability-evidence/codex.d.ts +35 -0
- package/dist/app/capability-evidence/codex.js +193 -0
- package/dist/app/capability-evidence/index.d.ts +33 -0
- package/dist/app/capability-evidence/index.js +117 -0
- package/dist/app/capability-evidence/pi.d.ts +73 -0
- package/dist/app/capability-evidence/pi.js +669 -0
- package/dist/app/capability-evidence/shared.d.ts +69 -0
- package/dist/app/capability-evidence/shared.js +195 -0
- package/dist/app/collab-auto-handoff.d.ts +55 -0
- package/dist/app/collab-auto-handoff.js +384 -0
- package/dist/app/collab-bindings.d.ts +57 -0
- package/dist/app/collab-bindings.js +1006 -0
- package/dist/app/collab-bootstrap-transport.d.ts +163 -0
- package/dist/app/collab-bootstrap-transport.js +769 -0
- package/dist/app/collab-bootstrap.d.ts +137 -0
- package/dist/app/collab-bootstrap.js +3455 -0
- package/dist/app/collab-handoff.d.ts +12 -0
- package/dist/app/collab-handoff.js +511 -0
- package/dist/app/collab-messages.d.ts +2 -0
- package/dist/app/collab-messages.js +2 -0
- package/dist/app/collab-notify.d.ts +156 -0
- package/dist/app/collab-notify.js +1208 -0
- package/dist/app/collab-preflight.d.ts +34 -0
- package/dist/app/collab-preflight.js +96 -0
- package/dist/app/collab-recovery-executor.d.ts +161 -0
- package/dist/app/collab-recovery-executor.js +1153 -0
- package/dist/app/collab-recovery-projection.d.ts +103 -0
- package/dist/app/collab-recovery-projection.js +659 -0
- package/dist/app/collab-recovery.d.ts +25 -0
- package/dist/app/collab-recovery.js +429 -0
- package/dist/app/collab-resources.d.ts +16 -0
- package/dist/app/collab-resources.js +278 -0
- package/dist/app/collab-role-events.d.ts +22 -0
- package/dist/app/collab-role-events.js +329 -0
- package/dist/app/collab-role-orchestration.d.ts +53 -0
- package/dist/app/collab-role-orchestration.js +601 -0
- package/dist/app/collab-role-plan.d.ts +81 -0
- package/dist/app/collab-role-plan.js +504 -0
- package/dist/app/collab-role-repair.d.ts +109 -0
- package/dist/app/collab-role-repair.js +821 -0
- package/dist/app/collab-rollover.d.ts +263 -0
- package/dist/app/collab-rollover.js +1112 -0
- package/dist/app/collab-route.d.ts +64 -0
- package/dist/app/collab-route.js +240 -0
- package/dist/app/collab-session-transition.d.ts +221 -0
- package/dist/app/collab-session-transition.js +3334 -0
- package/dist/app/collab-state.d.ts +7 -0
- package/dist/app/collab-state.js +162 -0
- package/dist/app/collab-task-store.d.ts +22 -0
- package/dist/app/collab-task-store.js +204 -0
- package/dist/app/collab-work-execution.d.ts +127 -0
- package/dist/app/collab-work-execution.js +1027 -0
- package/dist/app/collab-work-plan.d.ts +87 -0
- package/dist/app/collab-work-plan.js +437 -0
- package/dist/app/collaboration.d.ts +8 -2
- package/dist/app/collaboration.js +101 -12
- package/dist/app/doctor.js +108 -5
- package/dist/app/host-mcp.js +34 -8
- package/dist/app/host-probe.js +4 -4
- package/dist/app/init.d.ts +58 -4
- package/dist/app/init.js +768 -155
- package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
- package/dist/app/managed-pi-bootstrap-admission.js +104 -0
- package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
- package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
- package/dist/app/managed-pi-delivery.d.ts +8 -0
- package/dist/app/managed-pi-delivery.js +110 -0
- package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
- package/dist/app/managed-pi-handoff-receiver.js +263 -0
- package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
- package/dist/app/managed-pi-handoff-reference.js +60 -0
- package/dist/app/managed-pi-herdr.d.ts +21 -0
- package/dist/app/managed-pi-herdr.js +185 -0
- package/dist/app/managed-pi-initial-admission.d.ts +28 -0
- package/dist/app/managed-pi-initial-admission.js +416 -0
- package/dist/app/managed-pi-initial-launch.d.ts +5 -0
- package/dist/app/managed-pi-initial-launch.js +61 -0
- package/dist/app/managed-pi-native-observation.d.ts +36 -0
- package/dist/app/managed-pi-native-observation.js +191 -0
- package/dist/app/managed-pi-readiness.d.ts +29 -0
- package/dist/app/managed-pi-readiness.js +288 -0
- package/dist/app/managed-pi-repair-stop.d.ts +21 -0
- package/dist/app/managed-pi-repair-stop.js +371 -0
- package/dist/app/managed-pi-resume-admission.d.ts +32 -0
- package/dist/app/managed-pi-resume-admission.js +618 -0
- package/dist/app/managed-pi-retirement.d.ts +28 -0
- package/dist/app/managed-pi-retirement.js +112 -0
- package/dist/app/managed-pi-sdk-control.d.ts +7 -0
- package/dist/app/managed-pi-sdk-control.js +169 -0
- package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
- package/dist/app/managed-pi-sdk-runtime.js +483 -0
- package/dist/app/managed-pi-source-admission.d.ts +14 -0
- package/dist/app/managed-pi-source-admission.js +323 -0
- package/dist/app/managed-pi-transition-host.d.ts +36 -0
- package/dist/app/managed-pi-transition-host.js +292 -0
- package/dist/app/managed-pi.d.ts +54 -0
- package/dist/app/managed-pi.js +248 -0
- package/dist/app/shell-readiness.d.ts +66 -0
- package/dist/app/shell-readiness.js +368 -0
- package/dist/app/task-pin.d.ts +2 -23
- package/dist/app/task-pin.js +2 -113
- package/dist/cli/commands/collaboration-binding.d.ts +13 -0
- package/dist/cli/commands/collaboration-binding.js +118 -0
- package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
- package/dist/cli/commands/collaboration-bootstrap.js +84 -0
- package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
- package/dist/cli/commands/collaboration-capabilities.js +127 -0
- package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
- package/dist/cli/commands/collaboration-handoff.js +102 -0
- package/dist/cli/commands/collaboration-message.d.ts +17 -0
- package/dist/cli/commands/collaboration-message.js +177 -0
- package/dist/cli/commands/collaboration-plan.d.ts +8 -0
- package/dist/cli/commands/collaboration-plan.js +108 -0
- package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
- package/dist/cli/commands/collaboration-preflight.js +167 -0
- package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
- package/dist/cli/commands/collaboration-recovery-run.js +224 -0
- package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
- package/dist/cli/commands/collaboration-recovery.js +104 -0
- package/dist/cli/commands/collaboration-resources.d.ts +11 -0
- package/dist/cli/commands/collaboration-resources.js +96 -0
- package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
- package/dist/cli/commands/collaboration-role-plan.js +69 -0
- package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
- package/dist/cli/commands/collaboration-role-run.js +128 -0
- package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
- package/dist/cli/commands/collaboration-rollover.js +103 -0
- package/dist/cli/commands/collaboration-state.d.ts +10 -0
- package/dist/cli/commands/collaboration-state.js +96 -0
- package/dist/cli/commands/collaboration-transition.d.ts +10 -0
- package/dist/cli/commands/collaboration-transition.js +75 -0
- package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
- package/dist/cli/commands/collaboration-work-run.js +178 -0
- package/dist/cli/commands/collaboration.d.ts +4 -0
- package/dist/cli/commands/collaboration.js +39 -4
- package/dist/cli/commands/continuity.js +2 -6
- package/dist/cli/commands/docs.d.ts +2 -0
- package/dist/cli/commands/docs.js +89 -15
- package/dist/cli/commands/host.d.ts +7 -1
- package/dist/cli/commands/host.js +122 -7
- package/dist/cli/commands/init.d.ts +20 -3
- package/dist/cli/commands/init.js +897 -232
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +11 -5
- package/dist/cli/commands/repair.d.ts +12 -0
- package/dist/cli/commands/repair.js +41 -0
- package/dist/cli/commands/runtime.d.ts +1 -1
- package/dist/cli/commands/runtime.js +139 -38
- package/dist/cli/commands/session.d.ts +7 -0
- package/dist/cli/commands/session.js +82 -0
- package/dist/cli/commands/task.js +19 -5
- package/dist/cli/run.js +866 -24
- package/dist/code/identity.d.ts +7 -0
- package/dist/code/identity.js +7 -3
- package/dist/code/registration.js +7 -0
- package/dist/code/service.d.ts +9 -0
- package/dist/code/service.js +16 -3
- package/dist/code/text.js +19 -7
- package/dist/collab/admission.d.ts +91 -66
- package/dist/collab/admission.js +21 -6
- package/dist/collab/canonical.d.ts +1 -1
- package/dist/collab/canonical.js +44 -3
- package/dist/collab/envelope.d.ts +2 -2
- package/dist/collab/envelope.js +12 -12
- package/dist/collab/first-work.d.ts +3 -0
- package/dist/collab/first-work.js +15 -0
- package/dist/collab/handoff.d.ts +16 -0
- package/dist/collab/handoff.js +39 -5
- package/dist/collab/index.d.ts +97 -1
- package/dist/collab/index.js +9 -1
- package/dist/collab/messages.js +126 -3
- package/dist/collab/persisted-messages.d.ts +123 -0
- package/dist/collab/persisted-messages.js +496 -0
- package/dist/collab/preflight.d.ts +1 -1
- package/dist/collab/resources.d.ts +13 -3
- package/dist/collab/resources.js +37 -6
- package/dist/collab/rollover.d.ts +311 -0
- package/dist/collab/rollover.js +1024 -0
- package/dist/collab/store.d.ts +1 -0
- package/dist/collab/store.js +1 -4
- package/dist/collab/types.d.ts +2 -354
- package/dist/collab/validation.d.ts +2 -10
- package/dist/collab/validation.js +2 -303
- package/dist/collab/work-plan.d.ts +105 -0
- package/dist/collab/work-plan.js +843 -0
- package/dist/continuity/context.d.ts +11 -2
- package/dist/continuity/context.js +100 -13
- package/dist/continuity/handoff-bundle.d.ts +296 -0
- package/dist/continuity/handoff-bundle.js +477 -0
- package/dist/continuity/index.d.ts +2 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.js +10 -2
- package/dist/continuity/service.js +91 -15
- package/dist/continuity/task-pin.d.ts +24 -0
- package/dist/continuity/task-pin.js +590 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +6 -0
- package/dist/experimental/index.d.ts +13 -0
- package/dist/experimental/index.js +25 -0
- package/dist/experimental/pi.d.ts +4 -0
- package/dist/experimental/pi.js +12 -0
- package/dist/hosts/claude/native.js +1 -1
- package/dist/hosts/codex/native.js +1 -1
- package/dist/hosts/codex/protocol.js +1 -1
- package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
- package/dist/hosts/integration/bootstrap-config.js +254 -0
- package/dist/hosts/integration/bundle-export.d.ts +44 -0
- package/dist/hosts/integration/bundle-export.js +366 -3
- package/dist/hosts/integration/bundle.js +3 -0
- package/dist/hosts/integration/hook.js +36 -9
- package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
- package/dist/hosts/integration/pappra-hook.mjs +61 -24749
- package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
- package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
- package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
- package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
- package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
- package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
- package/dist/hosts/integration/pi-native-events.d.ts +112 -0
- package/dist/hosts/integration/pi-native-events.js +348 -0
- package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
- package/dist/hosts/integration/pi-rollover-extension.js +431 -0
- package/dist/hosts/integration/recovery-config.d.ts +48 -0
- package/dist/hosts/integration/recovery-config.js +117 -0
- package/dist/hosts/integration/service.d.ts +6 -1
- package/dist/hosts/integration/service.js +485 -35
- package/dist/hosts/integration/support.d.ts +7 -0
- package/dist/hosts/integration/support.js +9 -1
- package/dist/hosts/integration/templates.d.ts +3 -0
- package/dist/hosts/integration/templates.js +26 -3
- package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
- package/dist/hosts/pi/managed-host-identity.js +40 -0
- package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
- package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
- package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
- package/dist/hosts/pi/managed-sdk-session.js +1639 -0
- package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
- package/dist/hosts/pi/managed-session-extension.js +84 -0
- package/dist/hosts/pi/managed-session.d.ts +177 -0
- package/dist/hosts/pi/managed-session.js +1557 -0
- package/dist/hosts/pi/protocol.js +1 -1
- package/dist/hosts/pi/query-result.js +1 -1
- package/dist/hosts/pi/resume-capability.d.ts +41 -0
- package/dist/hosts/pi/resume-capability.js +33 -0
- package/dist/hosts/pi/rpc.d.ts +182 -134
- package/dist/hosts/pi/rpc.js +230 -37
- package/dist/hosts/pilot.d.ts +97 -74
- package/dist/hosts/pilot.js +5 -1
- package/dist/hosts/version.d.ts +3 -0
- package/dist/hosts/version.js +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/bounded-file.d.ts +19 -0
- package/dist/infra/bounded-file.js +66 -0
- package/dist/infra/fs.js +10 -2
- package/dist/infra/git.d.ts +29 -0
- package/dist/infra/git.js +45 -9
- package/dist/infra/json-property.d.ts +2 -0
- package/dist/infra/json-property.js +69 -0
- package/dist/infra/json.d.ts +4 -0
- package/dist/infra/json.js +24 -0
- package/dist/infra/owned-process-group.js +1 -1
- package/dist/infra/private-lock.d.ts +16 -0
- package/dist/infra/private-lock.js +148 -0
- package/dist/infra/private-state.d.ts +7 -0
- package/dist/infra/private-state.js +146 -0
- package/dist/infra/process-identity.d.ts +12 -0
- package/dist/infra/process-identity.js +142 -0
- package/dist/infra/validation.d.ts +5 -0
- package/dist/infra/validation.js +15 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +6 -0
- package/dist/install/apply.d.ts +9 -1
- package/dist/install/apply.js +176 -12
- package/dist/install/dependencies.d.ts +38 -0
- package/dist/install/dependencies.js +282 -0
- package/dist/install/ownership.js +30 -1
- package/dist/install/references.d.ts +31 -0
- package/dist/install/references.js +208 -0
- package/dist/install/repair.d.ts +37 -0
- package/dist/install/repair.js +798 -0
- package/dist/install/reset-local.d.ts +16 -0
- package/dist/install/reset-local.js +304 -0
- package/dist/install/rollback.d.ts +3 -0
- package/dist/install/rollback.js +9 -3
- package/dist/install/runtime.d.ts +10 -6
- package/dist/install/runtime.js +299 -59
- package/dist/install/source-fingerprint.d.ts +2 -0
- package/dist/install/source-fingerprint.js +103 -0
- package/dist/knowledge/catalog.d.ts +1 -0
- package/dist/knowledge/catalog.js +143 -10
- package/dist/knowledge/journal.js +17 -7
- package/dist/knowledge/paths.js +2 -1
- package/dist/knowledge/refs.js +4 -0
- package/dist/knowledge/register-batch.d.ts +4 -0
- package/dist/knowledge/register-batch.js +225 -0
- package/dist/knowledge/relocate.d.ts +5 -0
- package/dist/knowledge/relocate.js +92 -0
- package/dist/knowledge/scan.js +103 -47
- package/dist/knowledge/service.d.ts +3 -0
- package/dist/knowledge/service.js +33 -1
- package/dist/knowledge/write.d.ts +4 -1
- package/dist/knowledge/write.js +1 -1
- package/dist/mcp/server.d.ts +4 -2
- package/dist/mcp/server.js +87 -136
- package/dist/schemas/collaboration-state-validation.d.ts +6 -0
- package/dist/schemas/collaboration-state-validation.js +342 -0
- package/dist/schemas/collaboration-state.d.ts +436 -0
- package/dist/schemas/collaboration-state.js +1 -0
- package/dist/schemas/collaboration.d.ts +335 -0
- package/dist/schemas/collaboration.js +272 -0
- package/dist/schemas/continuity.d.ts +8 -0
- package/dist/schemas/continuity.js +13 -3
- package/dist/schemas/doctor.d.ts +1 -0
- package/dist/schemas/doctor.js +1 -0
- package/dist/schemas/document.d.ts +61 -0
- package/dist/schemas/document.js +9 -0
- package/dist/schemas/errors.d.ts +1 -0
- package/dist/schemas/errors.js +1 -0
- package/dist/schemas/host.d.ts +24 -0
- package/dist/schemas/host.js +19 -0
- package/dist/schemas/install.d.ts +8 -2
- package/dist/schemas/install.js +11 -1
- package/dist/schemas/managed-pi-execution.d.ts +16 -0
- package/dist/schemas/managed-pi-execution.js +2 -0
- package/dist/schemas/outcome.d.ts +9 -5
- package/dist/workspace/config-file.js +1 -3
- package/dist/workspace/documents.js +12 -2
- package/dist/workspace/repositories.d.ts +7 -0
- package/dist/workspace/repositories.js +14 -5
- package/package.json +35 -35
- package/skills/pappra/SKILL.md +28 -7
- package/skills/pappra/references/collaboration.md +633 -0
- package/skills/pappra/references/environment.md +360 -0
- package/skills/pappra/references/hosts.md +20 -0
- package/skills/pappra/references/operations.md +36 -5
- package/dist/app/collab-from.d.ts +0 -35
- package/dist/app/collab-from.js +0 -84
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in in-session Pi managed-tool freeze (rollover prerequisite).
|
|
3
|
+
*
|
|
4
|
+
* A separately opt-in Pi extension/controller: a later explicit integration calls
|
|
5
|
+
* `registerPiRolloverFreeze(pi, …)` inside a managed interactive session. This module does NOT
|
|
6
|
+
* activate in the current session, install itself, change default lifecycle hooks or touch user
|
|
7
|
+
* settings. It is self-contained: no subprocess, model, network, transcript, file watcher, timer,
|
|
8
|
+
* automatic handoff or successor launch, and no imports of the rollover orchestrator.
|
|
9
|
+
*
|
|
10
|
+
* What it is: a fail-closed managed Pi tool barrier. While latched it BLOCKS every tool except an
|
|
11
|
+
* explicit, vetted native read allowlist. Only tools in a fixed vetted read-tool universe can be
|
|
12
|
+
* allow-listed; bash/write/edit and every unknown/custom tool are blocked regardless of
|
|
13
|
+
* configuration. It never parses shell commands or tool paths and never calls a shell.
|
|
14
|
+
*
|
|
15
|
+
* What it is NOT: an OS sandbox, a stop proof, or an authority control. Pre-existing/background
|
|
16
|
+
* processes and external writers are outside it. A freeze latch alone is not "confirmed stopped".
|
|
17
|
+
* Native settled/queue/in-flight/approval facts are caller-supplied, UNAUTHENTICATED observations,
|
|
18
|
+
* never host-verified; any unknown/busy/pending condition and any session change or tool activity
|
|
19
|
+
* yield not-ready. Each readiness snapshot must be a complete coherent observation; absent facts are
|
|
20
|
+
* unknown, never an inherited historical all-clear. The latch is memory-only: a restart/reload loses
|
|
21
|
+
* it. There is intentionally NO thaw: recovery/release is a later, separately scoped,
|
|
22
|
+
* authority-checked integration.
|
|
23
|
+
*
|
|
24
|
+
* Installed Pi 1.3.0 API pinned from the installed example extensions (the package ships no .d.ts):
|
|
25
|
+
* permission-gate.ts (sha256:e32086ba9dd9b4ba0fd308b65241d9affa0b5ecd3f90ef7ea1b9f2b30cab8cc1),
|
|
26
|
+
* protected-paths.ts (sha256:9ca66b1f3b1b9a61cc5ddc660f92cca2ea66c4264a1bdb6b00e0da5844399fe2),
|
|
27
|
+
* commands.ts (sha256:36716b53da169936c7e1360a4fde1e2fc0c3356a6505f177235f09c5538c6e4f),
|
|
28
|
+
* docs/extensions.md (sha256:a2b5ff0f95e7e470ed0846d7c4fbfc5781fe7e694b8e7f51ea08859f3ad7325d):
|
|
29
|
+
* `pi.on("tool_call", async (event, ctx) => { … return { block: true, reason }; }` [allow: return
|
|
30
|
+
* undefined] with `event.toolName: string` and `event.input: Record<string, unknown>`;
|
|
31
|
+
* `pi.registerCommand(name, { description, handler })`; and the running session is read from
|
|
32
|
+
* `ctx.sessionManager.getSessionId()`. The narrow structural `PiFreezeHost` interface matches that
|
|
33
|
+
* shape. Structural compatibility checks are NOT a shipped-declaration or live-load proof.
|
|
34
|
+
*/
|
|
35
|
+
export const PI_ROLLOVER_FREEZE_STATUS_SCHEMA = 'pappra/pi-rollover-freeze-status/1';
|
|
36
|
+
/**
|
|
37
|
+
* The fixed vetted native read-tool universe. Only these may ever be allow-listed while latched.
|
|
38
|
+
* Pi 1.3.0 built-in tools are read/bash/edit/write; `read` is the only read-only one. `bash` can
|
|
39
|
+
* read but also write, so it is deliberately excluded; write/edit/unknown/custom are never allowed.
|
|
40
|
+
*/
|
|
41
|
+
export const VETTED_NATIVE_READ_TOOLS = Object.freeze([
|
|
42
|
+
'read',
|
|
43
|
+
]);
|
|
44
|
+
/** Default allowlist (the full vetted read universe). A caller may only narrow within the vetted set. */
|
|
45
|
+
export const DEFAULT_READ_TOOL_ALLOWLIST = Object.freeze([
|
|
46
|
+
...VETTED_NATIVE_READ_TOOLS,
|
|
47
|
+
]);
|
|
48
|
+
const MAX_ID_LENGTH = 256;
|
|
49
|
+
const MAX_OBSERVED_COUNT = 1_000_000;
|
|
50
|
+
const LIMITATIONS = Object.freeze([
|
|
51
|
+
'memory_only_latch_lost_on_restart_or_reload',
|
|
52
|
+
'managed_pi_tool_barrier_not_os_sandbox',
|
|
53
|
+
'preexisting_or_background_or_external_writers_not_fenced',
|
|
54
|
+
'observations_caller_supplied_unauthenticated_not_a_native_stop_proof',
|
|
55
|
+
'readiness_from_persistent_caller_data_is_not_live_quiescence_revalidation_required',
|
|
56
|
+
'freeze_latch_alone_is_not_confirmed_stopped',
|
|
57
|
+
'no_thaw_recovery_pending_separate_authority_checked_integration',
|
|
58
|
+
]);
|
|
59
|
+
function isSafeId(value) {
|
|
60
|
+
return (typeof value === 'string' &&
|
|
61
|
+
value.length > 0 &&
|
|
62
|
+
value.length <= MAX_ID_LENGTH);
|
|
63
|
+
}
|
|
64
|
+
function isSafeGeneration(value) {
|
|
65
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
66
|
+
}
|
|
67
|
+
function isValidToken(value) {
|
|
68
|
+
if (value === null || typeof value !== 'object')
|
|
69
|
+
return false;
|
|
70
|
+
const t = value;
|
|
71
|
+
return (isSafeId(t['sessionId']) &&
|
|
72
|
+
isSafeId(t['taskId']) &&
|
|
73
|
+
isSafeId(t['handoffId']) &&
|
|
74
|
+
isSafeGeneration(t['generation']));
|
|
75
|
+
}
|
|
76
|
+
function isSafeCount(value) {
|
|
77
|
+
return (typeof value === 'number' &&
|
|
78
|
+
Number.isSafeInteger(value) &&
|
|
79
|
+
value >= 0 &&
|
|
80
|
+
value <= MAX_OBSERVED_COUNT);
|
|
81
|
+
}
|
|
82
|
+
function sameToken(a, b) {
|
|
83
|
+
return (a.sessionId === b.sessionId &&
|
|
84
|
+
a.taskId === b.taskId &&
|
|
85
|
+
a.handoffId === b.handoffId &&
|
|
86
|
+
a.generation === b.generation);
|
|
87
|
+
}
|
|
88
|
+
function copyToken(token) {
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
sessionId: token.sessionId,
|
|
91
|
+
taskId: token.taskId,
|
|
92
|
+
handoffId: token.handoffId,
|
|
93
|
+
generation: token.generation,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function unknownNative() {
|
|
97
|
+
return {
|
|
98
|
+
settled: 'unknown',
|
|
99
|
+
queuedMessages: 'unknown',
|
|
100
|
+
inFlightTools: 'unknown',
|
|
101
|
+
pendingApprovals: 'unknown',
|
|
102
|
+
observedSequence: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* In-memory, no-I/O freeze controller. One instance per managed session; the `pi` registration
|
|
107
|
+
* wires it to `tool_call` and the freeze/status commands. A fresh instance starts UNPROVEN
|
|
108
|
+
* (not frozen, not ready). There is no thaw.
|
|
109
|
+
*/
|
|
110
|
+
export class RolloverFreezeController {
|
|
111
|
+
#sessionId;
|
|
112
|
+
#allow;
|
|
113
|
+
#active = null;
|
|
114
|
+
#native = unknownNative();
|
|
115
|
+
/** Monotonic last-accepted observation sequence: the replay fence. Readiness invalidation,
|
|
116
|
+
* tool activity, a session mismatch and an idempotent freeze reuse never reset it. */
|
|
117
|
+
#lastAcceptedSequence = null;
|
|
118
|
+
constructor(options) {
|
|
119
|
+
if (!isSafeId(options.sessionId)) {
|
|
120
|
+
throw new RangeError('RolloverFreezeController requires a non-empty session id');
|
|
121
|
+
}
|
|
122
|
+
this.#sessionId = options.sessionId;
|
|
123
|
+
// Vet the allowlist against the fixed read universe; unsafe names are dropped.
|
|
124
|
+
const requested = options.readAllowlist ?? DEFAULT_READ_TOOL_ALLOWLIST;
|
|
125
|
+
const vetted = new Set(VETTED_NATIVE_READ_TOOLS);
|
|
126
|
+
this.#allow = new Set(requested.filter((name) => vetted.has(name)));
|
|
127
|
+
}
|
|
128
|
+
get sessionId() {
|
|
129
|
+
return this.#sessionId;
|
|
130
|
+
}
|
|
131
|
+
get frozen() {
|
|
132
|
+
return this.#active !== null;
|
|
133
|
+
}
|
|
134
|
+
freeze(token) {
|
|
135
|
+
if (!isValidToken(token)) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: 'invalid_token',
|
|
139
|
+
detail: 'token fields are invalid',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (token.sessionId !== this.#sessionId) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: 'wrong_session',
|
|
146
|
+
detail: `token session ${token.sessionId} is not the bound session ${this.#sessionId}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (this.#active) {
|
|
150
|
+
if (sameToken(this.#active, token)) {
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
state: 'already_frozen',
|
|
154
|
+
idempotent: true,
|
|
155
|
+
token: copyToken(this.#active),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (token.handoffId === this.#active.handoffId &&
|
|
159
|
+
token.generation < this.#active.generation) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: 'stale_generation',
|
|
163
|
+
detail: `generation ${token.generation} is older than active ${this.#active.generation}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: 'conflicting_freeze',
|
|
169
|
+
detail: `an active freeze (${this.#active.handoffId}#${this.#active.generation}) already holds this session`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
this.#active = copyToken(token);
|
|
173
|
+
this.#native = unknownNative();
|
|
174
|
+
this.#lastAcceptedSequence = null;
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
state: 'frozen',
|
|
178
|
+
idempotent: false,
|
|
179
|
+
token: copyToken(this.#active),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The fail-closed tool barrier, revalidating the live session on every call. A wrong/missing/
|
|
184
|
+
* changed context session invalidates readiness and fails closed while latched; any tool activity
|
|
185
|
+
* invalidates a prior all-clear so it cannot be reused without a fresh observation.
|
|
186
|
+
*/
|
|
187
|
+
decideToolCall(toolName, contextSessionId) {
|
|
188
|
+
if (contextSessionId !== this.#sessionId) {
|
|
189
|
+
this.invalidateReadiness();
|
|
190
|
+
if (this.#active) {
|
|
191
|
+
return {
|
|
192
|
+
block: true,
|
|
193
|
+
reason: `rollover freeze active (${this.#active.handoffId}#${this.#active.generation}): blocked (context session ${contextSessionId ?? 'missing'} != bound ${this.#sessionId})`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { block: false, reason: null };
|
|
197
|
+
}
|
|
198
|
+
if (!this.#active)
|
|
199
|
+
return { block: false, reason: null };
|
|
200
|
+
// Any tool activity means the session is not settled; invalidate prior readiness.
|
|
201
|
+
this.invalidateReadiness();
|
|
202
|
+
if (typeof toolName === 'string' && this.#allow.has(toolName)) {
|
|
203
|
+
return { block: false, reason: null };
|
|
204
|
+
}
|
|
205
|
+
const name = typeof toolName === 'string' ? toolName : 'unknown-tool';
|
|
206
|
+
return {
|
|
207
|
+
block: true,
|
|
208
|
+
reason: `rollover freeze active (${this.#active.handoffId}#${this.#active.generation}): ${name} blocked until ownership transfer`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Reset readiness to unknown (on session change or activity). Does not clear the latch. */
|
|
212
|
+
invalidateReadiness() {
|
|
213
|
+
this.#native = unknownNative();
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Apply one caller-supplied, unauthenticated complete native snapshot correlated to the active
|
|
217
|
+
* freeze. Validates the full token and every field type; a malformed or stale observation never
|
|
218
|
+
* changes readiness. Omitted fields become unknown (no inherited all-clear).
|
|
219
|
+
*/
|
|
220
|
+
observe(observation) {
|
|
221
|
+
if (!this.#active) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
reason: 'not_frozen',
|
|
225
|
+
detail: 'no active freeze to correlate against',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (observation === null ||
|
|
229
|
+
typeof observation !== 'object' ||
|
|
230
|
+
!isValidToken(observation.token)) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: 'invalid_observation',
|
|
234
|
+
detail: 'observation token is malformed',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (!sameToken(this.#active, observation.token)) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: 'token_mismatch',
|
|
241
|
+
detail: 'observation token does not fully match the active freeze',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (!isSafeGeneration(observation.sequence)) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: 'invalid_observation',
|
|
248
|
+
detail: 'sequence is not a safe non-negative integer',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (observation.settled !== undefined &&
|
|
252
|
+
typeof observation.settled !== 'boolean') {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
reason: 'invalid_observation',
|
|
256
|
+
detail: 'settled must be a boolean',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
for (const count of [
|
|
260
|
+
observation.queuedMessages,
|
|
261
|
+
observation.inFlightTools,
|
|
262
|
+
observation.pendingApprovals,
|
|
263
|
+
]) {
|
|
264
|
+
if (count !== undefined && !isSafeCount(count)) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
reason: 'invalid_observation',
|
|
268
|
+
detail: 'counts must be safe non-negative bounded integers',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (this.#lastAcceptedSequence !== null &&
|
|
273
|
+
observation.sequence <= this.#lastAcceptedSequence) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
reason: 'stale_sequence',
|
|
277
|
+
detail: 'observation sequence is not newer than the last accepted',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
this.#native = {
|
|
281
|
+
settled: observation.settled === undefined
|
|
282
|
+
? 'unknown'
|
|
283
|
+
: observation.settled
|
|
284
|
+
? 'yes'
|
|
285
|
+
: 'no',
|
|
286
|
+
queuedMessages: observation.queuedMessages === undefined
|
|
287
|
+
? 'unknown'
|
|
288
|
+
: observation.queuedMessages,
|
|
289
|
+
inFlightTools: observation.inFlightTools === undefined
|
|
290
|
+
? 'unknown'
|
|
291
|
+
: observation.inFlightTools,
|
|
292
|
+
pendingApprovals: observation.pendingApprovals === undefined
|
|
293
|
+
? 'unknown'
|
|
294
|
+
: observation.pendingApprovals,
|
|
295
|
+
observedSequence: observation.sequence,
|
|
296
|
+
};
|
|
297
|
+
this.#lastAcceptedSequence = observation.sequence;
|
|
298
|
+
return { ok: true };
|
|
299
|
+
}
|
|
300
|
+
/** A defensive, frozen in-memory status snapshot; mutating it cannot change the controller. */
|
|
301
|
+
status() {
|
|
302
|
+
const blockers = [];
|
|
303
|
+
if (!this.#active)
|
|
304
|
+
blockers.push('not_frozen');
|
|
305
|
+
if (this.#native.settled !== 'yes') {
|
|
306
|
+
blockers.push(this.#native.settled === 'unknown' ? 'settled_unknown' : 'not_settled');
|
|
307
|
+
}
|
|
308
|
+
if (this.#native.queuedMessages !== 0) {
|
|
309
|
+
blockers.push(this.#native.queuedMessages === 'unknown'
|
|
310
|
+
? 'queue_unknown'
|
|
311
|
+
: 'queue_nonempty');
|
|
312
|
+
}
|
|
313
|
+
if (this.#native.inFlightTools !== 0) {
|
|
314
|
+
blockers.push(this.#native.inFlightTools === 'unknown'
|
|
315
|
+
? 'inflight_unknown'
|
|
316
|
+
: 'inflight_present');
|
|
317
|
+
}
|
|
318
|
+
if (this.#native.pendingApprovals !== 0) {
|
|
319
|
+
blockers.push(this.#native.pendingApprovals === 'unknown'
|
|
320
|
+
? 'approvals_unknown'
|
|
321
|
+
: 'approvals_pending');
|
|
322
|
+
}
|
|
323
|
+
return Object.freeze({
|
|
324
|
+
schema: PI_ROLLOVER_FREEZE_STATUS_SCHEMA,
|
|
325
|
+
sessionId: this.#sessionId,
|
|
326
|
+
frozen: this.#active !== null,
|
|
327
|
+
token: this.#active ? copyToken(this.#active) : null,
|
|
328
|
+
native: Object.freeze({ ...this.#native }),
|
|
329
|
+
lastAcceptedSequence: this.#lastAcceptedSequence,
|
|
330
|
+
ready: blockers.length === 0,
|
|
331
|
+
readyBlockers: Object.freeze(blockers),
|
|
332
|
+
observationSource: 'caller-supplied-unauthenticated',
|
|
333
|
+
readinessIsNativeStopProof: false,
|
|
334
|
+
thawSupported: false,
|
|
335
|
+
limitations: LIMITATIONS,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Pinned installed API: the running session id is read from `ctx.sessionManager.getSessionId()`. */
|
|
340
|
+
function getContextSessionId(context) {
|
|
341
|
+
if (typeof context !== 'object' || context === null)
|
|
342
|
+
return null;
|
|
343
|
+
const manager = context.sessionManager;
|
|
344
|
+
if (typeof manager === 'object' &&
|
|
345
|
+
manager !== null &&
|
|
346
|
+
'getSessionId' in manager &&
|
|
347
|
+
typeof manager.getSessionId === 'function') {
|
|
348
|
+
const value = manager.getSessionId();
|
|
349
|
+
return typeof value === 'string' ? value : null;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function notify(context, message) {
|
|
354
|
+
if (typeof context === 'object' &&
|
|
355
|
+
context !== null &&
|
|
356
|
+
'ui' in context &&
|
|
357
|
+
typeof context.ui === 'object' &&
|
|
358
|
+
context.ui !== null) {
|
|
359
|
+
const ui = context.ui;
|
|
360
|
+
if (typeof ui.notify === 'function') {
|
|
361
|
+
ui.notify(message, 'info');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function parseFreezeToken(args) {
|
|
366
|
+
let raw;
|
|
367
|
+
try {
|
|
368
|
+
raw = JSON.parse(args);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return { ok: false, detail: 'freeze argument must be a JSON token object' };
|
|
372
|
+
}
|
|
373
|
+
if (!isValidToken(raw)) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
detail: 'freeze token requires sessionId, taskId, handoffId and an integer generation',
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return { ok: true, token: copyToken(raw) };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Concrete registration entry for a later explicit integration. Wires the controller to the Pi
|
|
383
|
+
* `tool_call` barrier (revalidating the live session on every call) and to validated
|
|
384
|
+
* `/rollover-freeze` and `/rollover-status` commands. There is intentionally NO thaw command.
|
|
385
|
+
* Returns the controller (the no-I/O orchestration surface). It only touches the supplied `pi`.
|
|
386
|
+
*/
|
|
387
|
+
export function registerPiRolloverFreeze(pi, options) {
|
|
388
|
+
if (options.controller &&
|
|
389
|
+
options.controller.sessionId !== options.sessionId) {
|
|
390
|
+
throw new RangeError('injected controller session does not match the registration session');
|
|
391
|
+
}
|
|
392
|
+
const controller = options.controller ?? new RolloverFreezeController(options);
|
|
393
|
+
pi.on('tool_call', async (event, context) => {
|
|
394
|
+
const decision = controller.decideToolCall(event.toolName, getContextSessionId(context));
|
|
395
|
+
if (decision.block && decision.reason !== null) {
|
|
396
|
+
return { block: true, reason: decision.reason };
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
});
|
|
400
|
+
pi.registerCommand('rollover-freeze', {
|
|
401
|
+
description: 'Latch a managed-tool freeze for a rollover handoff (JSON token: sessionId, taskId, handoffId, generation).',
|
|
402
|
+
handler: (args, context) => {
|
|
403
|
+
const parsed = parseFreezeToken(args);
|
|
404
|
+
if (!parsed.ok) {
|
|
405
|
+
notify(context, `rollover-freeze rejected: ${parsed.detail}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const contextSession = getContextSessionId(context);
|
|
409
|
+
if (contextSession !== controller.sessionId ||
|
|
410
|
+
contextSession !== parsed.token.sessionId) {
|
|
411
|
+
controller.invalidateReadiness();
|
|
412
|
+
notify(context, `rollover-freeze rejected: session mismatch (context ${contextSession ?? 'missing'})`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const outcome = controller.freeze(parsed.token);
|
|
416
|
+
notify(context, outcome.ok
|
|
417
|
+
? `rollover-freeze ${outcome.state}${outcome.idempotent ? ' (idempotent)' : ''}`
|
|
418
|
+
: `rollover-freeze rejected: ${outcome.reason} (${outcome.detail})`);
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
pi.registerCommand('rollover-status', {
|
|
422
|
+
description: 'Report the in-memory managed-tool freeze and advisory readiness status.',
|
|
423
|
+
handler: (_args, context) => {
|
|
424
|
+
if (getContextSessionId(context) !== controller.sessionId) {
|
|
425
|
+
controller.invalidateReadiness();
|
|
426
|
+
}
|
|
427
|
+
notify(context, JSON.stringify(controller.status()));
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
return controller;
|
|
431
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Identity } from '../../collab/types.js';
|
|
2
|
+
export declare const PI_RECOVERY_CONFIG_SCHEMA: 'pappra/pi-recovery-config/1';
|
|
3
|
+
/** Bound for the sibling `recovery.json`; bundle export pins the same limit. */
|
|
4
|
+
export declare const PI_RECOVERY_MAX_CONFIG_BYTES: number;
|
|
5
|
+
export type PiRecoveryRole = 'writer' | 'coordinator' | 'reviewer';
|
|
6
|
+
export type PiRecoveryMode = 'plan' | 'apply';
|
|
7
|
+
export interface PiRecoveryPolicy {
|
|
8
|
+
readonly role: PiRecoveryRole;
|
|
9
|
+
/** When present, only these subtask ids are projected; everything else is ignored. */
|
|
10
|
+
readonly authorizedSubtasks?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface PiRecoveryExpected {
|
|
13
|
+
readonly session_id: string;
|
|
14
|
+
readonly cwd: string;
|
|
15
|
+
readonly binding_ref: string;
|
|
16
|
+
readonly binding_digest: string;
|
|
17
|
+
readonly pane_id: string;
|
|
18
|
+
readonly harness: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PiRecoveryConfig {
|
|
21
|
+
readonly schema: typeof PI_RECOVERY_CONFIG_SCHEMA;
|
|
22
|
+
readonly taskRoot: string;
|
|
23
|
+
readonly planFile: string;
|
|
24
|
+
readonly self: Identity;
|
|
25
|
+
readonly policy: PiRecoveryPolicy;
|
|
26
|
+
readonly expected: PiRecoveryExpected;
|
|
27
|
+
readonly mode: PiRecoveryMode;
|
|
28
|
+
readonly target: {
|
|
29
|
+
readonly agent: string;
|
|
30
|
+
};
|
|
31
|
+
readonly requestId: string;
|
|
32
|
+
}
|
|
33
|
+
export type PiRecoveryConfigLoad = {
|
|
34
|
+
readonly ok: true;
|
|
35
|
+
readonly config: PiRecoveryConfig;
|
|
36
|
+
readonly digest: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly ok: false;
|
|
39
|
+
readonly code: string;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Strict shape check for one recovery configuration document. Unknown fields are
|
|
44
|
+
* rejected by construction: every accepted field is read explicitly below.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validatePiRecoveryConfig(parsed: unknown): PiRecoveryConfig | null;
|
|
47
|
+
/** Read and validate the sibling recovery configuration through the bounded no-follow reader. */
|
|
48
|
+
export declare function readPiRecoveryConfig(file: string): PiRecoveryConfigLoad;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Lower-layer recovery configuration contract.
|
|
2
|
+
//
|
|
3
|
+
// The opt-in Pi recovery bundle is validated here rather than in the
|
|
4
|
+
// orchestration layer: `bundle-export` runs inside `src/hosts` and must read,
|
|
5
|
+
// validate and pin the exact configuration bytes without importing `src/app`,
|
|
6
|
+
// and the adapter that composes the Pi extension reuses this same reader, so
|
|
7
|
+
// the format has exactly one definition.
|
|
8
|
+
//
|
|
9
|
+
// Role and mode are declared structurally. The orchestration layer keeps its own
|
|
10
|
+
// enums (`RecoveryRole`, `RecoveryExecutionMode`) and accepts these shapes as-is,
|
|
11
|
+
// which is why this module must stay free of application imports.
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { readBoundedFile } from '../../infra/bounded-file.js';
|
|
14
|
+
export const PI_RECOVERY_CONFIG_SCHEMA = 'pappra/pi-recovery-config/1';
|
|
15
|
+
/** Bound for the sibling `recovery.json`; bundle export pins the same limit. */
|
|
16
|
+
export const PI_RECOVERY_MAX_CONFIG_BYTES = 64 * 1024;
|
|
17
|
+
function isIdentity(value) {
|
|
18
|
+
return (typeof value === 'object' &&
|
|
19
|
+
value !== null &&
|
|
20
|
+
typeof value.agent_id === 'string' &&
|
|
21
|
+
typeof value.session_id === 'string' &&
|
|
22
|
+
(typeof value.harness === 'string' ||
|
|
23
|
+
value.harness === null));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Strict shape check for one recovery configuration document. Unknown fields are
|
|
27
|
+
* rejected by construction: every accepted field is read explicitly below.
|
|
28
|
+
*/
|
|
29
|
+
export function validatePiRecoveryConfig(parsed) {
|
|
30
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
31
|
+
return null;
|
|
32
|
+
const raw = parsed;
|
|
33
|
+
if (raw.schema !== PI_RECOVERY_CONFIG_SCHEMA)
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof raw.taskRoot !== 'string' || typeof raw.planFile !== 'string')
|
|
36
|
+
return null;
|
|
37
|
+
if (!isIdentity(raw.self))
|
|
38
|
+
return null;
|
|
39
|
+
const policy = raw.policy;
|
|
40
|
+
if (typeof policy !== 'object' ||
|
|
41
|
+
policy === null ||
|
|
42
|
+
(policy.role !== 'writer' &&
|
|
43
|
+
policy.role !== 'coordinator' &&
|
|
44
|
+
policy.role !== 'reviewer'))
|
|
45
|
+
return null;
|
|
46
|
+
const expected = raw.expected;
|
|
47
|
+
if (typeof expected !== 'object' ||
|
|
48
|
+
expected === null ||
|
|
49
|
+
Object.keys(expected).sort().join('|') !==
|
|
50
|
+
'binding_digest|binding_ref|cwd|harness|pane_id|session_id' ||
|
|
51
|
+
typeof expected.session_id !== 'string' ||
|
|
52
|
+
expected.session_id.length === 0 ||
|
|
53
|
+
typeof expected.cwd !== 'string' ||
|
|
54
|
+
!path.isAbsolute(expected.cwd) ||
|
|
55
|
+
typeof expected.binding_ref !== 'string' ||
|
|
56
|
+
!path.isAbsolute(expected.binding_ref) ||
|
|
57
|
+
typeof expected.binding_digest !== 'string' ||
|
|
58
|
+
!/^sha256:[0-9a-f]{64}$/.test(expected.binding_digest) ||
|
|
59
|
+
typeof expected.pane_id !== 'string' ||
|
|
60
|
+
!/^[A-Za-z0-9]+:p[A-Za-z0-9]+$/.test(expected.pane_id) ||
|
|
61
|
+
typeof expected.harness !== 'string' ||
|
|
62
|
+
expected.harness.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
if (raw.mode !== 'plan' && raw.mode !== 'apply')
|
|
65
|
+
return null;
|
|
66
|
+
const target = raw.target;
|
|
67
|
+
if (typeof target !== 'object' ||
|
|
68
|
+
target === null ||
|
|
69
|
+
typeof target.agent !== 'string')
|
|
70
|
+
return null;
|
|
71
|
+
if (typeof raw.requestId !== 'string' || raw.requestId.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
return {
|
|
74
|
+
schema: PI_RECOVERY_CONFIG_SCHEMA,
|
|
75
|
+
taskRoot: raw.taskRoot,
|
|
76
|
+
planFile: raw.planFile,
|
|
77
|
+
self: {
|
|
78
|
+
agent_id: raw.self.agent_id,
|
|
79
|
+
session_id: raw.self.session_id,
|
|
80
|
+
harness: raw.self.harness,
|
|
81
|
+
},
|
|
82
|
+
policy: {
|
|
83
|
+
role: policy.role,
|
|
84
|
+
...(Array.isArray(policy.authorizedSubtasks)
|
|
85
|
+
? { authorizedSubtasks: [...policy.authorizedSubtasks] }
|
|
86
|
+
: {}),
|
|
87
|
+
},
|
|
88
|
+
expected: {
|
|
89
|
+
session_id: expected.session_id,
|
|
90
|
+
cwd: expected.cwd,
|
|
91
|
+
binding_ref: expected.binding_ref,
|
|
92
|
+
binding_digest: expected.binding_digest,
|
|
93
|
+
pane_id: expected.pane_id,
|
|
94
|
+
harness: expected.harness,
|
|
95
|
+
},
|
|
96
|
+
mode: raw.mode,
|
|
97
|
+
target: { agent: target.agent },
|
|
98
|
+
requestId: raw.requestId,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Read and validate the sibling recovery configuration through the bounded no-follow reader. */
|
|
102
|
+
export function readPiRecoveryConfig(file) {
|
|
103
|
+
const read = readBoundedFile(file, PI_RECOVERY_MAX_CONFIG_BYTES);
|
|
104
|
+
if (!read.ok)
|
|
105
|
+
return { ok: false, code: `config_${read.code}`, message: file };
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(read.text);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { ok: false, code: 'config_unparseable', message: file };
|
|
112
|
+
}
|
|
113
|
+
const config = validatePiRecoveryConfig(parsed);
|
|
114
|
+
if (config === null)
|
|
115
|
+
return { ok: false, code: 'config_invalid', message: file };
|
|
116
|
+
return { ok: true, config, digest: read.digest };
|
|
117
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { HostCapabilities, HostInstallManifest, HostIntegrationPlan, HostIntegrationResult, HostName, HostVerification } from '../../schemas/host.js';
|
|
1
|
+
import type { HostCapabilities, HostInstallManifest, HostIntegrationPlan, HostIntegrationResult, HostName, HostVerification, HostUninstallResult } from '../../schemas/host.js';
|
|
2
2
|
export interface HostIntegrationOptions {
|
|
3
|
+
readonly versionProbe?: (host: HostName) => string | null;
|
|
3
4
|
readonly root: string;
|
|
4
5
|
readonly homeDir: string;
|
|
5
6
|
readonly pathEnv: string;
|
|
@@ -36,6 +37,10 @@ export declare class HostIntegrationService {
|
|
|
36
37
|
readonly apply: boolean;
|
|
37
38
|
}): Promise<HostIntegrationResult>;
|
|
38
39
|
verify(host: HostName, options?: HostTargetOptions): Promise<HostVerification>;
|
|
40
|
+
uninstall(host: HostName, options: HostTargetOptions & {
|
|
41
|
+
readonly apply: boolean;
|
|
42
|
+
readonly force?: boolean;
|
|
43
|
+
}): Promise<HostUninstallResult>;
|
|
39
44
|
readManifest(hostRoot: string): Promise<HostInstallManifest | null>;
|
|
40
45
|
}
|
|
41
46
|
export declare class HostAdapter {
|