@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,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in successor-bootstrap managed-tool gate (one rollover prerequisite).
|
|
3
|
+
*
|
|
4
|
+
* A separately opt-in Pi `tool_call` guard for the NEW (successor) writer session during a rollover.
|
|
5
|
+
* Registered before any model work, it starts DENIED for every non-vetted-read managed tool
|
|
6
|
+
* (bash/write/edit and any unknown/custom tool); only the fixed vetted native-read set reused from the
|
|
7
|
+
* accepted freeze module is ever allowed, and only for the exact pinned native session. It permits the
|
|
8
|
+
* successor's first non-read tool ONLY after the authoritative, validated current task record proves the
|
|
9
|
+
* completed writer transfer to the pinned successor — read fresh on EVERY call. It does not require a
|
|
10
|
+
* first-work REPORT to permit the first write; that proof comes afterward.
|
|
11
|
+
*
|
|
12
|
+
* Authority is validated against the record itself for cross-record CONSISTENCY, not merely schema
|
|
13
|
+
* validity: the pinned intent in `record.rollover`, the completed WRITER `record.handoffs` entry (kind,
|
|
14
|
+
* subtask/attempt, coordinator epoch, exact from/to, resources and ownership snapshot consistent with the
|
|
15
|
+
* intent), a transfer boundary bounded by the current record and history, the latest actionable attempt
|
|
16
|
+
* owned by the successor with `ownership:'transferred'` and its full confirmed native binding, and every
|
|
17
|
+
* original claim held by the successor at `original + 1`. Duplicate or ambiguous ids anywhere block rather
|
|
18
|
+
* than letting a first-match lookup resolve contradictory records to allow. Every check is fail-closed: a
|
|
19
|
+
* malformed event, a throwing property/record accessor, or any traversal/validation failure returns an
|
|
20
|
+
* explicit block and never escapes into host default behavior.
|
|
21
|
+
*
|
|
22
|
+
* What it is NOT: it is not a transfer action, a stop/quiescence inference, an authority store, or a
|
|
23
|
+
* filesystem/cross-process write fence. It never mutates claims, epochs, handoffs, bindings, messages,
|
|
24
|
+
* the bundle or the archive; it never feeds observations into the freeze controller; it adds no timer,
|
|
25
|
+
* watcher, spawned command, transcript access, network or model call. There is no model-accessible allow/
|
|
26
|
+
* thaw/release method and no boolean authorization callback. The record comes only from an explicitly
|
|
27
|
+
* trusted, read-only host adapter supplied at registration that returns record DATA (never `allowed:true`,
|
|
28
|
+
* a self-signed token or an invented stop proof); this slice does not authenticate a filesystem/remote
|
|
29
|
+
* source, establish freshness against a dishonest adapter, or install a durable reader — a later real
|
|
30
|
+
* adapter must bind and read the actual task domain safely, and this is not complete native authorization.
|
|
31
|
+
*
|
|
32
|
+
* There is no filesystem lock covering a tool's later execution: the guard answers the call-time question
|
|
33
|
+
* only (check/use gap), and background/external processes and effects outside managed Pi tools are never
|
|
34
|
+
* fenced.
|
|
35
|
+
*
|
|
36
|
+
* Installed Pi 1.3.0 `tool_call` blocking contract and the vetted native-read set are reused from the
|
|
37
|
+
* accepted `pi-rollover-extension.ts`; the running session is read from `ctx.sessionManager.getSessionId()`
|
|
38
|
+
* (pinned API). Structural compatibility is not a shipped-declaration or live-load proof.
|
|
39
|
+
*/
|
|
40
|
+
import { VETTED_NATIVE_READ_TOOLS, } from './pi-rollover-extension.js';
|
|
41
|
+
import { latestAttemptNumber } from '../../collab/lifecycle.js';
|
|
42
|
+
import { isTaskRecord } from '../../collab/validation.js';
|
|
43
|
+
export const PI_BOOTSTRAP_GUARD_STATUS_SCHEMA = 'pappra/pi-bootstrap-guard-status/1';
|
|
44
|
+
const MAX_ID_LENGTH = 256;
|
|
45
|
+
const MAX_CLAIMS = 1024;
|
|
46
|
+
/**
|
|
47
|
+
* The only current writer states that may hold first-write authority (active + owned + actionable). It is
|
|
48
|
+
* `ACTIVE_STATES` minus `unknown`; `needs_input`, terminal and any unknown/blocked state can never
|
|
49
|
+
* silently grant authority.
|
|
50
|
+
*/
|
|
51
|
+
const PERMITTED_WRITER_STATES = new Set([
|
|
52
|
+
'assigned',
|
|
53
|
+
'accepted',
|
|
54
|
+
'running',
|
|
55
|
+
]);
|
|
56
|
+
const LIMITATIONS = Object.freeze([
|
|
57
|
+
'managed_pi_tool_gate_not_a_filesystem_or_cross_process_write_fence',
|
|
58
|
+
'record_from_an_explicitly_trusted_read_only_adapter_not_authenticated_by_this_slice',
|
|
59
|
+
'answers_the_call_time_question_only_check_use_gap_background_effects_not_fenced',
|
|
60
|
+
'not_a_transfer_action_stop_inference_authority_store_or_native_authorization',
|
|
61
|
+
'no_model_accessible_allow_thaw_or_release_and_no_boolean_authorization_callback',
|
|
62
|
+
'first_work_is_not_a_prerequisite_for_the_first_write_that_proof_comes_afterward',
|
|
63
|
+
'cross_record_consistency_validated_not_schema_only_ambiguous_ids_fail_closed',
|
|
64
|
+
]);
|
|
65
|
+
function isSafeId(value) {
|
|
66
|
+
return (typeof value === 'string' &&
|
|
67
|
+
value.length > 0 &&
|
|
68
|
+
value.length <= MAX_ID_LENGTH);
|
|
69
|
+
}
|
|
70
|
+
function isSafeEpoch(value) {
|
|
71
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
72
|
+
}
|
|
73
|
+
/** A claim epoch must leave room for a safe `+1` (original resource epoch raised at transfer). */
|
|
74
|
+
function isSafeRaisableEpoch(value) {
|
|
75
|
+
return isSafeEpoch(value) && value < Number.MAX_SAFE_INTEGER;
|
|
76
|
+
}
|
|
77
|
+
/** Return the single element, or null when there are zero or more than one (ambiguous). */
|
|
78
|
+
function exactlyOne(items) {
|
|
79
|
+
if (items.length !== 1)
|
|
80
|
+
return null;
|
|
81
|
+
const only = items[0];
|
|
82
|
+
return only === undefined ? null : only;
|
|
83
|
+
}
|
|
84
|
+
/** Read the tool name defensively: a throwing/proxy accessor or a non-string yields null (fail-closed). */
|
|
85
|
+
function readToolName(event) {
|
|
86
|
+
try {
|
|
87
|
+
if (typeof event !== 'object' || event === null)
|
|
88
|
+
return null;
|
|
89
|
+
const name = event.toolName;
|
|
90
|
+
return typeof name === 'string' ? name : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Pinned API: the running session is `ctx.sessionManager.getSessionId()`. Any failure stays null. */
|
|
97
|
+
function readLiveSessionId(context) {
|
|
98
|
+
try {
|
|
99
|
+
if (typeof context !== 'object' || context === null)
|
|
100
|
+
return null;
|
|
101
|
+
const manager = context.sessionManager;
|
|
102
|
+
if (typeof manager !== 'object' || manager === null)
|
|
103
|
+
return null;
|
|
104
|
+
const getter = manager.getSessionId;
|
|
105
|
+
if (typeof getter !== 'function')
|
|
106
|
+
return null;
|
|
107
|
+
const value = getter.call(manager);
|
|
108
|
+
return isSafeId(value) ? value : null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function copyIdentity(identity) {
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
agentId: identity.agentId,
|
|
117
|
+
sessionId: identity.sessionId,
|
|
118
|
+
harness: identity.harness,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Two record identities match on agent id, native session id and harness. */
|
|
122
|
+
function sameIdentity(a, b) {
|
|
123
|
+
return (a !== null &&
|
|
124
|
+
a !== undefined &&
|
|
125
|
+
b !== null &&
|
|
126
|
+
b !== undefined &&
|
|
127
|
+
a.agent_id === b.agent_id &&
|
|
128
|
+
a.session_id === b.session_id &&
|
|
129
|
+
a.harness === b.harness);
|
|
130
|
+
}
|
|
131
|
+
/** A record identity matches the pinned successor on agent id, native session id and harness. */
|
|
132
|
+
function matchesSuccessor(identity, successor) {
|
|
133
|
+
return (identity !== null &&
|
|
134
|
+
identity !== undefined &&
|
|
135
|
+
identity.agent_id === successor.agentId &&
|
|
136
|
+
identity.session_id === successor.sessionId &&
|
|
137
|
+
identity.harness === successor.harness);
|
|
138
|
+
}
|
|
139
|
+
function block(reason) {
|
|
140
|
+
return { allow: false, reason };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Pure decision over the current record. Allows a non-read tool only when the validated record proves a
|
|
144
|
+
* consistent completed WRITER transfer to the pinned successor. First-work is NOT required. Any traversal
|
|
145
|
+
* or accessor failure fails closed.
|
|
146
|
+
*/
|
|
147
|
+
export function decideBootstrapWrite(record, pinned, liveSessionId) {
|
|
148
|
+
try {
|
|
149
|
+
if (liveSessionId === null || liveSessionId !== pinned.successor.sessionId)
|
|
150
|
+
return block('native_session_unknown_or_mismatch');
|
|
151
|
+
if (!isTaskRecord(record))
|
|
152
|
+
return block('record_invalid');
|
|
153
|
+
const task = record;
|
|
154
|
+
if (task.task_id !== pinned.taskId)
|
|
155
|
+
return block('task_mismatch');
|
|
156
|
+
// Coordinator owner_epoch, never a per-claim resource epoch.
|
|
157
|
+
if ((task.coordinator?.owner_epoch ?? null) !== pinned.coordinatorEpoch)
|
|
158
|
+
return block('coordinator_epoch_drift');
|
|
159
|
+
// Pinned rollover intent read directly from the record; a duplicate/ambiguous id fails closed.
|
|
160
|
+
const intent = exactlyOne((task.rollover ?? []).filter((item) => item.intent_id === pinned.intentId));
|
|
161
|
+
if (intent === null)
|
|
162
|
+
return block('ambiguous_or_missing_intent');
|
|
163
|
+
if (intent.task_id !== pinned.taskId ||
|
|
164
|
+
intent.handoff_id !== pinned.handoffId ||
|
|
165
|
+
intent.generation !== pinned.generation ||
|
|
166
|
+
intent.subtask_id !== pinned.subtaskId ||
|
|
167
|
+
intent.attempt !== pinned.attempt ||
|
|
168
|
+
intent.input_revision !== pinned.inputRevision ||
|
|
169
|
+
intent.coordinator_epoch !== pinned.coordinatorEpoch ||
|
|
170
|
+
!matchesSuccessor(intent.to, pinned.successor))
|
|
171
|
+
return block('intent_mismatch');
|
|
172
|
+
// The claim set the transfer concerns: the intent originals (deduplicated + pinned-consistent later).
|
|
173
|
+
const intentClaims = intent.claims ?? [];
|
|
174
|
+
const intentIds = new Set(intentClaims.map((item) => item.claim_id));
|
|
175
|
+
if (intentIds.size !== intentClaims.length)
|
|
176
|
+
return block('intent_claims_duplicated');
|
|
177
|
+
// Completed WRITER handoff, consistent with the pinned intent (not merely a completed id match).
|
|
178
|
+
const handoff = exactlyOne((task.handoffs ?? []).filter((item) => item.handoff_id === pinned.handoffId));
|
|
179
|
+
if (handoff === null)
|
|
180
|
+
return block('ambiguous_or_missing_handoff');
|
|
181
|
+
if (handoff.state !== 'completed')
|
|
182
|
+
return block('handoff_not_completed');
|
|
183
|
+
if (handoff.kind !== 'writer')
|
|
184
|
+
return block('handoff_not_writer');
|
|
185
|
+
if (handoff.subtask_id !== pinned.subtaskId ||
|
|
186
|
+
handoff.attempt !== pinned.attempt)
|
|
187
|
+
return block('handoff_target_mismatch');
|
|
188
|
+
if (handoff.owner_epoch !== pinned.coordinatorEpoch)
|
|
189
|
+
return block('handoff_epoch_mismatch');
|
|
190
|
+
if (!sameIdentity(handoff.from, intent.from))
|
|
191
|
+
return block('handoff_from_mismatch');
|
|
192
|
+
if (!matchesSuccessor(handoff.to, pinned.successor))
|
|
193
|
+
return block('handoff_to_mismatch');
|
|
194
|
+
// `beginHandoff` records the pre-transfer resources as claim ids and an ownership snapshot of the
|
|
195
|
+
// original holder/epoch. Both must match the intent originals.
|
|
196
|
+
const resourceIds = new Set(handoff.resources ?? []);
|
|
197
|
+
if (resourceIds.size !== intentIds.size)
|
|
198
|
+
return block('handoff_resources_mismatch');
|
|
199
|
+
for (const id of intentIds)
|
|
200
|
+
if (!resourceIds.has(id))
|
|
201
|
+
return block('handoff_resources_mismatch');
|
|
202
|
+
const snapshot = handoff.ownership_snapshot ?? [];
|
|
203
|
+
const snapIds = new Set(snapshot.map((item) => item.claim_id));
|
|
204
|
+
if (snapIds.size !== snapshot.length)
|
|
205
|
+
return block('ownership_snapshot_duplicated');
|
|
206
|
+
if (snapIds.size !== intentIds.size)
|
|
207
|
+
return block('ownership_snapshot_mismatch');
|
|
208
|
+
for (const id of intentIds)
|
|
209
|
+
if (!snapIds.has(id))
|
|
210
|
+
return block('ownership_snapshot_mismatch');
|
|
211
|
+
// Transfer boundary must be bounded by the current record and history (a record cannot predate its
|
|
212
|
+
// own completed transfer); keep safe-integer checks.
|
|
213
|
+
const boundary = intent.transfer_boundary ?? null;
|
|
214
|
+
if (boundary === null)
|
|
215
|
+
return block('transfer_not_bounded');
|
|
216
|
+
if (!Number.isSafeInteger(boundary.state_revision) ||
|
|
217
|
+
!Number.isSafeInteger(boundary.message_count))
|
|
218
|
+
return block('transfer_boundary_unsafe');
|
|
219
|
+
if (boundary.state_revision > task.state_revision)
|
|
220
|
+
return block('transfer_boundary_state_revision_out_of_range');
|
|
221
|
+
if (boundary.message_count > (task.messages ?? []).length)
|
|
222
|
+
return block('transfer_boundary_message_count_out_of_range');
|
|
223
|
+
// Successor native pinned at the transfer boundary must be the configured Pi successor.
|
|
224
|
+
const successorNative = intent.successor_native ?? null;
|
|
225
|
+
if (successorNative === null ||
|
|
226
|
+
successorNative.agent_id !== pinned.successor.agentId ||
|
|
227
|
+
successorNative.session_id !== pinned.successor.sessionId ||
|
|
228
|
+
successorNative.runner !== pinned.successor.harness)
|
|
229
|
+
return block('successor_native_mismatch');
|
|
230
|
+
// Latest actionable attempt, owned by the successor via a completed transfer, with a full native bind.
|
|
231
|
+
if (latestAttemptNumber(task, pinned.subtaskId) !== pinned.attempt)
|
|
232
|
+
return block('attempt_superseded');
|
|
233
|
+
const attempt = exactlyOne((task.attempts ?? []).filter((item) => item.subtask_id === pinned.subtaskId &&
|
|
234
|
+
item.attempt === pinned.attempt));
|
|
235
|
+
if (attempt === null)
|
|
236
|
+
return block('ambiguous_or_missing_attempt');
|
|
237
|
+
if (!matchesSuccessor(attempt.assignee, pinned.successor))
|
|
238
|
+
return block('attempt_not_reassigned');
|
|
239
|
+
if (attempt.input_revision !== pinned.inputRevision)
|
|
240
|
+
return block('attempt_input_drift');
|
|
241
|
+
if (!PERMITTED_WRITER_STATES.has(attempt.state))
|
|
242
|
+
return block('attempt_not_actionable');
|
|
243
|
+
if (typeof attempt.awaiting === 'string' && attempt.awaiting.length > 0)
|
|
244
|
+
return block('attempt_blocked');
|
|
245
|
+
if (attempt.ownership !== 'transferred')
|
|
246
|
+
return block('ownership_not_transferred');
|
|
247
|
+
if (attempt.native.observation !== 'confirmed')
|
|
248
|
+
return block('native_binding_not_confirmed');
|
|
249
|
+
if (attempt.native.session_id !== pinned.successor.sessionId)
|
|
250
|
+
return block('native_session_binding_mismatch');
|
|
251
|
+
if (attempt.native.runner !== successorNative.runner ||
|
|
252
|
+
attempt.native.session_id !== successorNative.session_id ||
|
|
253
|
+
attempt.native.run_id !== successorNative.run_id ||
|
|
254
|
+
(attempt.native.mission_id ?? null) !==
|
|
255
|
+
(successorNative.mission_id ?? null))
|
|
256
|
+
return block('native_binding_mismatch');
|
|
257
|
+
if (attempt.native.runner !== pinned.successor.harness)
|
|
258
|
+
return block('native_runner_mismatch');
|
|
259
|
+
// Exact set equality: pinned claims == intent originals. Each original epoch matches the intent and the
|
|
260
|
+
// ownership snapshot (original holder was the source), and each current record claim is active, held by
|
|
261
|
+
// the successor, at original + 1. A duplicate/ambiguous claim id fails closed.
|
|
262
|
+
const pinnedOriginals = new Map(pinned.claims.map((item) => [item.claimId, item.originalResourceEpoch]));
|
|
263
|
+
if (pinnedOriginals.size !== intentIds.size)
|
|
264
|
+
return block('claim_set_mismatch');
|
|
265
|
+
for (const id of intentIds)
|
|
266
|
+
if (!pinnedOriginals.has(id))
|
|
267
|
+
return block('claim_set_mismatch');
|
|
268
|
+
for (const intentClaim of intentClaims) {
|
|
269
|
+
const pinnedOriginal = pinnedOriginals.get(intentClaim.claim_id);
|
|
270
|
+
if (pinnedOriginal === undefined)
|
|
271
|
+
return block('claim_set_mismatch');
|
|
272
|
+
if (!isSafeRaisableEpoch(intentClaim.resource_epoch))
|
|
273
|
+
return block('claim_epoch_unsafe');
|
|
274
|
+
if (pinnedOriginal !== intentClaim.resource_epoch)
|
|
275
|
+
return block('claim_original_epoch_mismatch');
|
|
276
|
+
const snap = snapshot.find((item) => item.claim_id === intentClaim.claim_id) ?? null;
|
|
277
|
+
if (snap === null)
|
|
278
|
+
return block('ownership_snapshot_mismatch');
|
|
279
|
+
if ((snap.resource_epoch ?? null) !== intentClaim.resource_epoch)
|
|
280
|
+
return block('ownership_snapshot_epoch_mismatch');
|
|
281
|
+
if (!sameIdentity(snap.holder ?? null, intent.from))
|
|
282
|
+
return block('ownership_snapshot_holder_mismatch');
|
|
283
|
+
const current = exactlyOne((task.claims ?? []).filter((item) => item.claim_id === intentClaim.claim_id));
|
|
284
|
+
if (current === null)
|
|
285
|
+
return block('ambiguous_or_missing_claim');
|
|
286
|
+
if (current.released_at !== null)
|
|
287
|
+
return block('claim_released');
|
|
288
|
+
if (!matchesSuccessor(current.holder, pinned.successor))
|
|
289
|
+
return block('claim_not_held_by_successor');
|
|
290
|
+
if (current.resource_epoch !== intentClaim.resource_epoch + 1)
|
|
291
|
+
return block('claim_resource_epoch_not_raised');
|
|
292
|
+
}
|
|
293
|
+
// No other open handoff may overlap this attempt or any pinned claim id (resources are claim ids).
|
|
294
|
+
for (const other of task.handoffs ?? []) {
|
|
295
|
+
if (other.handoff_id === pinned.handoffId)
|
|
296
|
+
continue;
|
|
297
|
+
if (other.state === 'completed' || other.state === 'aborted')
|
|
298
|
+
continue;
|
|
299
|
+
if (other.subtask_id === pinned.subtaskId &&
|
|
300
|
+
other.attempt === pinned.attempt)
|
|
301
|
+
return block('conflicting_handoff');
|
|
302
|
+
for (const snap of other.ownership_snapshot ?? [])
|
|
303
|
+
if (intentIds.has(snap.claim_id))
|
|
304
|
+
return block('conflicting_handoff');
|
|
305
|
+
for (const resource of other.resources ?? [])
|
|
306
|
+
if (intentIds.has(resource))
|
|
307
|
+
return block('conflicting_handoff');
|
|
308
|
+
}
|
|
309
|
+
return { allow: true, reason: 'completed_transfer_to_successor' };
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Any accessor/traversal/validation exception is a conservative block, never a raw rejection.
|
|
313
|
+
return block('record_traversal_failed');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
class BootstrapGuard {
|
|
317
|
+
#pinned;
|
|
318
|
+
#readRecord;
|
|
319
|
+
constructor(pinned, readRecord) {
|
|
320
|
+
this.#pinned = pinned;
|
|
321
|
+
this.#readRecord = readRecord;
|
|
322
|
+
}
|
|
323
|
+
async decide(event, context) {
|
|
324
|
+
try {
|
|
325
|
+
const liveBefore = readLiveSessionId(context);
|
|
326
|
+
// Unknown/malformed native identity blocks even vetted reads.
|
|
327
|
+
if (liveBefore === null ||
|
|
328
|
+
liveBefore !== this.#pinned.successor.sessionId)
|
|
329
|
+
return deny('native_session_unknown_or_mismatch');
|
|
330
|
+
const toolName = readToolName(event);
|
|
331
|
+
if (toolName === null)
|
|
332
|
+
return deny('malformed_tool_event');
|
|
333
|
+
if (VETTED_NATIVE_READ_TOOLS.includes(toolName))
|
|
334
|
+
return undefined;
|
|
335
|
+
// Non-read tool: read the current full record from the trusted adapter and revalidate every time.
|
|
336
|
+
let record;
|
|
337
|
+
try {
|
|
338
|
+
record = await this.#readRecord();
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return deny('trusted_record_read_failed');
|
|
342
|
+
}
|
|
343
|
+
// Recheck the native session after the asynchronous read, before deciding.
|
|
344
|
+
const liveAfter = readLiveSessionId(context);
|
|
345
|
+
if (liveAfter === null || liveAfter !== this.#pinned.successor.sessionId)
|
|
346
|
+
return deny('native_session_drift');
|
|
347
|
+
const decision = decideBootstrapWrite(record, this.#pinned, liveAfter);
|
|
348
|
+
return decision.allow ? undefined : deny(decision.reason);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// No raw exception ever escapes into host default behavior.
|
|
352
|
+
return deny('guard_internal_error');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
status() {
|
|
356
|
+
const status = {
|
|
357
|
+
schema: PI_BOOTSTRAP_GUARD_STATUS_SCHEMA,
|
|
358
|
+
taskId: this.#pinned.taskId,
|
|
359
|
+
intentId: this.#pinned.intentId,
|
|
360
|
+
handoffId: this.#pinned.handoffId,
|
|
361
|
+
generation: this.#pinned.generation,
|
|
362
|
+
subtaskId: this.#pinned.subtaskId,
|
|
363
|
+
attempt: this.#pinned.attempt,
|
|
364
|
+
coordinatorEpoch: this.#pinned.coordinatorEpoch,
|
|
365
|
+
successor: copyIdentity(this.#pinned.successor),
|
|
366
|
+
pinnedClaimCount: this.#pinned.claims.length,
|
|
367
|
+
mode: 'deny-until-completed-transfer',
|
|
368
|
+
vettedReadTools: Object.freeze([...VETTED_NATIVE_READ_TOOLS]),
|
|
369
|
+
limitations: LIMITATIONS,
|
|
370
|
+
};
|
|
371
|
+
return Object.freeze(status);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function deny(reason) {
|
|
375
|
+
return { block: true, reason: `bootstrap-guard: ${reason}` };
|
|
376
|
+
}
|
|
377
|
+
function validateIdentity(value) {
|
|
378
|
+
if (!isSafeId(value.agentId) ||
|
|
379
|
+
!isSafeId(value.sessionId) ||
|
|
380
|
+
!isSafeId(value.harness))
|
|
381
|
+
throw new RangeError('pi-bootstrap-guard: invalid successor identity');
|
|
382
|
+
return copyIdentity(value);
|
|
383
|
+
}
|
|
384
|
+
/** Deep-copy and bound-check options so a later mutation of the caller object cannot change the config. */
|
|
385
|
+
function pinOptions(options) {
|
|
386
|
+
if (!isSafeId(options.taskId) ||
|
|
387
|
+
!isSafeId(options.intentId) ||
|
|
388
|
+
!isSafeId(options.handoffId) ||
|
|
389
|
+
!isSafeId(options.subtaskId) ||
|
|
390
|
+
!isSafeId(options.inputRevision))
|
|
391
|
+
throw new RangeError('pi-bootstrap-guard: invalid pinned identifiers');
|
|
392
|
+
if (!isSafeEpoch(options.generation) ||
|
|
393
|
+
!isSafeEpoch(options.attempt) ||
|
|
394
|
+
!isSafeEpoch(options.coordinatorEpoch))
|
|
395
|
+
throw new RangeError('pi-bootstrap-guard: invalid generation/attempt/epoch');
|
|
396
|
+
if (!Array.isArray(options.claims) ||
|
|
397
|
+
options.claims.length === 0 ||
|
|
398
|
+
options.claims.length > MAX_CLAIMS)
|
|
399
|
+
throw new RangeError('pi-bootstrap-guard: claims must be a non-empty bounded set');
|
|
400
|
+
const seen = new Set();
|
|
401
|
+
const claims = options.claims.map((claim) => {
|
|
402
|
+
if (!isSafeId(claim.claimId) ||
|
|
403
|
+
!isSafeRaisableEpoch(claim.originalResourceEpoch))
|
|
404
|
+
throw new RangeError('pi-bootstrap-guard: invalid claim reference');
|
|
405
|
+
if (seen.has(claim.claimId))
|
|
406
|
+
throw new RangeError('pi-bootstrap-guard: duplicate pinned claim id');
|
|
407
|
+
seen.add(claim.claimId);
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
claimId: claim.claimId,
|
|
410
|
+
originalResourceEpoch: claim.originalResourceEpoch,
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
return Object.freeze({
|
|
414
|
+
taskId: options.taskId,
|
|
415
|
+
intentId: options.intentId,
|
|
416
|
+
handoffId: options.handoffId,
|
|
417
|
+
generation: options.generation,
|
|
418
|
+
subtaskId: options.subtaskId,
|
|
419
|
+
attempt: options.attempt,
|
|
420
|
+
inputRevision: options.inputRevision,
|
|
421
|
+
coordinatorEpoch: options.coordinatorEpoch,
|
|
422
|
+
successor: validateIdentity(options.successor),
|
|
423
|
+
claims: Object.freeze(claims),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Register the opt-in successor bootstrap gate on `pi`. It only subscribes `tool_call`; it registers no
|
|
428
|
+
* model-accessible command, removes no tools/MCP/skills/hooks, and mutates nothing. The returned handle
|
|
429
|
+
* exposes a conservative frozen status only — no allow/thaw/release.
|
|
430
|
+
*/
|
|
431
|
+
export function registerPiBootstrapGuard(pi, options) {
|
|
432
|
+
if (typeof options.readRecord !== 'function')
|
|
433
|
+
throw new RangeError('pi-bootstrap-guard: a trusted readRecord adapter is required');
|
|
434
|
+
const pinned = pinOptions(options);
|
|
435
|
+
const guard = new BootstrapGuard(pinned, options.readRecord);
|
|
436
|
+
pi.on('tool_call', (event, context) => guard.decide(event, context));
|
|
437
|
+
return guard;
|
|
438
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete local task-record reader + opt-in bootstrap-guard registration (one rollover prerequisite).
|
|
3
|
+
*
|
|
4
|
+
* This supplies the accepted successor bootstrap guard (`pi-bootstrap-guard.ts`) with a real, bounded,
|
|
5
|
+
* read-only task-record dependency and wires the actual `tool_call` gate. It is NOT old-writer stop, a
|
|
6
|
+
* transfer executor, automatic rollover, or an installation; it changes no accepted guard/core/host code.
|
|
7
|
+
*
|
|
8
|
+
* Reader contract (fail-closed): it reads ONLY `<domainRoot>/tasks/<taskId>/task.json` for an explicit,
|
|
9
|
+
* trusted, CANONICAL absolute domain root (rejected at registration if a symlink prefix means
|
|
10
|
+
* `realpathSync(root) !== root`) and a short safe task id. The three ancestor directories (domain root,
|
|
11
|
+
* `tasks`, task root) are pinned by device/inode once and re-checked by no-follow `lstat` both BEFORE and
|
|
12
|
+
* AFTER the bounded read, so a replaced/redirected/symlinked ancestor during the read interval denies the
|
|
13
|
+
* call instead of silently rebinding; it never creates directories (it does NOT use
|
|
14
|
+
* `resolveDomainRoot`/`taskRootFor`, which `mkdir`) and never resolves from cwd or a cache. The record is
|
|
15
|
+
* read via `src/infra/bounded-file.ts` (`readBoundedFile`): a bounded (8 MiB), regular-file,
|
|
16
|
+
* no-follow, non-blocking descriptor read refusing symlink/FIFO/device/directory targets and oversized
|
|
17
|
+
* content. The record file's observable identity and change metadata (inode, size, mtime, ctime) are
|
|
18
|
+
* captured before and after the read; a replacement or in-place change during the read is resolved with a
|
|
19
|
+
* strictly bounded coherent retry (the record inode is never pinned forever, so normal atomic store updates
|
|
20
|
+
* between calls are supported). Only a stable, coherent read is parsed, schema-validated (`isTaskRecord`)
|
|
21
|
+
* and identity-checked: `task_id`, `domain_id` (= `domainIdOf(domainRoot)`), `domain_root` and `task_root`
|
|
22
|
+
* must all equal the selected canonical root. An in-memory revision/digest floor rejects a lower
|
|
23
|
+
* `state_revision` (rollback) or different bytes at the same revision, and is NOT reset by a failed read
|
|
24
|
+
* (same revision + same bytes is repeatable). Any violation/read-failure/malformed result throws, which the
|
|
25
|
+
* guard turns into an explicit block; a fresh parsed object is returned and the floor/pinned identities are
|
|
26
|
+
* primitives, immune to outward mutation.
|
|
27
|
+
*
|
|
28
|
+
* Boundaries (explicit): rollback detection is for THIS runtime only — not durable restart fencing, and not
|
|
29
|
+
* authentication against arbitrary user-owned filesystem changes (explicit local-root trust boundary). The
|
|
30
|
+
* reader detects a change DURING its own read; there is still an UNAVOIDABLE check/use interval AFTER
|
|
31
|
+
* `read()` returns, before the tool actually runs. There is no filesystem lock, atomic global snapshot, OS
|
|
32
|
+
* write fence, native-stop proof, old-writer stop, transfer executor, automatic rollover, or installation
|
|
33
|
+
* here. No writes, locks, sidecars, watchers, timers, process/model/network/transcript calls, new authority
|
|
34
|
+
* store, or automatic recovery. The imported guard/store/validation APIs are read-only dependencies.
|
|
35
|
+
*/
|
|
36
|
+
import type { TaskRecord } from '../../collab/types.js';
|
|
37
|
+
import { type BootstrapGuardOptions, type PiBootstrapGuard, type PiBootstrapGuardHost } from './pi-bootstrap-guard.js';
|
|
38
|
+
/** The accepted guard's pinned configuration, minus the reader this slice supplies. */
|
|
39
|
+
export type BootstrapGuardPinned = Omit<BootstrapGuardOptions, 'readRecord'>;
|
|
40
|
+
export interface BootstrapRuntimeConfig {
|
|
41
|
+
/** Canonical, existing, absolute domain root (trusted host integration; never model-supplied). */
|
|
42
|
+
readonly domainRoot: string;
|
|
43
|
+
/** Exact short task id; must equal the guard's `taskId`. */
|
|
44
|
+
readonly taskId: string;
|
|
45
|
+
/** The accepted guard's pinned configuration (no caller reader override). */
|
|
46
|
+
readonly guard: BootstrapGuardPinned;
|
|
47
|
+
}
|
|
48
|
+
/** The bounded local reader. `read()` returns the current validated record or throws (fail-closed). */
|
|
49
|
+
export interface LocalTaskRecordReader {
|
|
50
|
+
read(): TaskRecord;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the bounded local reader for an explicit canonical domain root + task id. Requires existing
|
|
54
|
+
* directories; never creates any. Exposed for focused testing and reuse.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createLocalTaskRecordReader(domainRoot: string, taskId: string): LocalTaskRecordReader;
|
|
57
|
+
/**
|
|
58
|
+
* Register the opt-in successor bootstrap gate wired to the concrete local reader. Configuration is
|
|
59
|
+
* trusted host integration (never model input); the reader is built here (no caller-reader override). The
|
|
60
|
+
* `tool_call` gate is registered synchronously before any model work. Returns the guard's conservative
|
|
61
|
+
* status handle only — no allow/release surface.
|
|
62
|
+
*/
|
|
63
|
+
export declare function registerPiBootstrapRuntime(pi: PiBootstrapGuardHost, config: BootstrapRuntimeConfig): PiBootstrapGuard;
|