@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,477 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { checkpointSchema, isValidTaskId, taskIdSchema, } from '../schemas/continuity.js';
|
|
5
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
+
import { failed, succeeded } from '../schemas/outcome.js';
|
|
7
|
+
import { readFileBounded, resolveRealpath, sha256Digest } from '../infra/fs.js';
|
|
8
|
+
import { continuityTaskPaths, ensurePrivateTaskBoundary, readArtifactPath, readContinuityBytes, writeContinuityBytes, } from './paths.js';
|
|
9
|
+
import { loadWorkspaceConfig } from '../workspace/config.js';
|
|
10
|
+
/**
|
|
11
|
+
* Linked private handoff bundles.
|
|
12
|
+
*
|
|
13
|
+
* A bundle is a separately versioned manifest that *links* one existing durable
|
|
14
|
+
* checkpoint to a collaboration task/attempt and the source native session, and
|
|
15
|
+
* carries the bounded additional handoff snapshot (claims, pending work, next
|
|
16
|
+
* action, recovery). It never duplicates checkpoint content, transfers
|
|
17
|
+
* authority, launches a replacement, archives a session or grants execution.
|
|
18
|
+
* Reading or verifying a bundle is purely advisory: it never returns execution
|
|
19
|
+
* permission or native control. The later rollover orchestrator (E3-B) must
|
|
20
|
+
* independently revalidate current authority, quiescence, ACK and transfer.
|
|
21
|
+
*
|
|
22
|
+
* Bundles are published immutably and exclusively under the existing
|
|
23
|
+
* workspace-private continuity task namespace
|
|
24
|
+
* (`.pappra/local/tasks/<continuityTaskId>/handoff-bundles/<bundleId>.json`),
|
|
25
|
+
* reusing the guarded continuity filesystem helpers. There is no latest
|
|
26
|
+
* pointer, alternate task record or mutable authority store.
|
|
27
|
+
*/
|
|
28
|
+
export const HANDOFF_BUNDLE_SCHEMA = 'pappra/handoff-bundle/1';
|
|
29
|
+
/** Subdirectory holding immutable handoff bundles inside a private task dir. */
|
|
30
|
+
export const HANDOFF_BUNDLES_DIRNAME = 'handoff-bundles';
|
|
31
|
+
/**
|
|
32
|
+
* Upper bound for a serialized bundle. The manifest only links a checkpoint and
|
|
33
|
+
* carries a bounded snapshot, so it stays well under the shared continuity
|
|
34
|
+
* payload cap; an oversized file is rejected without inspecting its content.
|
|
35
|
+
*/
|
|
36
|
+
export const HANDOFF_BUNDLE_MAX_BYTES = 256 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* Collaboration identifiers follow the message-envelope contract (up to 128),
|
|
39
|
+
* which is deliberately wider than the continuity task limit (64). The two are
|
|
40
|
+
* never truncated into one another; when they differ the manifest must carry an
|
|
41
|
+
* explicit association string instead of an invented mapping.
|
|
42
|
+
*/
|
|
43
|
+
const COLLABORATION_ID_MAX_LENGTH = 128;
|
|
44
|
+
const COLLABORATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
45
|
+
const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
46
|
+
const collaborationIdSchema = z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(COLLABORATION_ID_MAX_LENGTH)
|
|
50
|
+
.regex(COLLABORATION_ID_PATTERN);
|
|
51
|
+
/** The bound reference to one existing, schema-valid checkpoint. */
|
|
52
|
+
export const handoffCheckpointReferenceSchema = z.strictObject({
|
|
53
|
+
checkpointId: z.uuid(),
|
|
54
|
+
taskId: taskIdSchema,
|
|
55
|
+
workspaceId: z.uuid(),
|
|
56
|
+
/** Private relative reference inside the task dir, e.g. `checkpoints/<id>.json`. */
|
|
57
|
+
ref: z.string().min(1).max(2048),
|
|
58
|
+
digest: digestSchema,
|
|
59
|
+
authorSession: z.string().max(256).nullable(),
|
|
60
|
+
contextCompleteness: z.enum(['complete', 'partial']),
|
|
61
|
+
});
|
|
62
|
+
/** The collaboration task/attempt/input/owner-epoch this handoff belongs to. */
|
|
63
|
+
export const handoffCollaborationBindingSchema = z.strictObject({
|
|
64
|
+
taskId: collaborationIdSchema,
|
|
65
|
+
attempt: z.number().int().positive(),
|
|
66
|
+
ownerEpoch: z.number().int().positive(),
|
|
67
|
+
subtaskId: z.string().min(1).max(128).nullable(),
|
|
68
|
+
inputRevision: z.string().max(256).nullable(),
|
|
69
|
+
/**
|
|
70
|
+
* Explicit, producer-authored association used only when the collaboration id
|
|
71
|
+
* differs from the continuity task id; never a derived mapping.
|
|
72
|
+
*/
|
|
73
|
+
association: z.string().min(1).max(2048).nullable(),
|
|
74
|
+
});
|
|
75
|
+
/** The source native session. Model/provider are advisory observations only. */
|
|
76
|
+
export const handoffSourceNativeSessionSchema = z.strictObject({
|
|
77
|
+
harness: z.enum(['pi', 'codex', 'claude', 'unknown']),
|
|
78
|
+
sessionId: z.string().min(1).max(256),
|
|
79
|
+
agentId: z.string().min(1).max(128).nullable(),
|
|
80
|
+
paneId: z.string().max(128).nullable(),
|
|
81
|
+
surface: z.string().max(64).nullable(),
|
|
82
|
+
configurationId: z.string().max(128).nullable(),
|
|
83
|
+
modelObservation: z.strictObject({
|
|
84
|
+
model: z.string().max(256).nullable(),
|
|
85
|
+
provider: z.string().max(256).nullable(),
|
|
86
|
+
reasoning: z.string().max(64).nullable(),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
const pendingCollectionSchema = z.strictObject({
|
|
90
|
+
items: z.array(z.string().min(1).max(2048)).max(64),
|
|
91
|
+
completeness: z.enum(['complete', 'partial', 'unknown']),
|
|
92
|
+
});
|
|
93
|
+
const scopedClaimSchema = z.strictObject({
|
|
94
|
+
claimId: z.string().min(1).max(128),
|
|
95
|
+
resource: z.string().min(1).max(2048),
|
|
96
|
+
resourceEpoch: z.number().int().nonnegative(),
|
|
97
|
+
holder: z.string().min(1).max(128).nullable(),
|
|
98
|
+
});
|
|
99
|
+
/** The bounded additional snapshot carried alongside the linked checkpoint. */
|
|
100
|
+
export const handoffSnapshotSchema = z.strictObject({
|
|
101
|
+
claims: z.array(scopedClaimSchema).max(64),
|
|
102
|
+
claimsCompleteness: z.enum(['complete', 'partial', 'unknown']),
|
|
103
|
+
pendingTools: pendingCollectionSchema,
|
|
104
|
+
pendingMessages: pendingCollectionSchema,
|
|
105
|
+
pendingApprovals: pendingCollectionSchema,
|
|
106
|
+
nextAction: z.string().min(1).max(4096),
|
|
107
|
+
recovery: z.array(z.string().min(1).max(4096)).max(32),
|
|
108
|
+
/** A bundle never carries execution authority; this is an observation. */
|
|
109
|
+
executionAuthority: z.literal('none'),
|
|
110
|
+
/** Snapshots are advisory; partial/unknown context is preserved, not healed. */
|
|
111
|
+
advisory: z.literal(true),
|
|
112
|
+
});
|
|
113
|
+
/** The complete linked handoff bundle manifest. */
|
|
114
|
+
export const handoffBundleSchema = z
|
|
115
|
+
.strictObject({
|
|
116
|
+
schema: z.literal(HANDOFF_BUNDLE_SCHEMA),
|
|
117
|
+
bundleId: z.uuid(),
|
|
118
|
+
createdAt: z.iso.datetime({ offset: true }),
|
|
119
|
+
continuityTaskId: taskIdSchema,
|
|
120
|
+
checkpoint: handoffCheckpointReferenceSchema,
|
|
121
|
+
collaboration: handoffCollaborationBindingSchema,
|
|
122
|
+
sourceNativeSession: handoffSourceNativeSessionSchema,
|
|
123
|
+
handoff: handoffSnapshotSchema,
|
|
124
|
+
limitations: z.array(z.string().min(1).max(4096)).max(32),
|
|
125
|
+
notes: z.string().min(1).max(4096).nullable(),
|
|
126
|
+
})
|
|
127
|
+
.superRefine((bundle, ctx) => {
|
|
128
|
+
// The linked checkpoint must live in this private continuity namespace.
|
|
129
|
+
if (bundle.checkpoint.taskId !== bundle.continuityTaskId) {
|
|
130
|
+
ctx.addIssue({
|
|
131
|
+
code: 'custom',
|
|
132
|
+
path: ['checkpoint', 'taskId'],
|
|
133
|
+
message: 'checkpoint.taskId must equal continuityTaskId; the bundle does not map across task namespaces',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Differing collaboration/continuity ids require an explicit association.
|
|
137
|
+
if (bundle.collaboration.taskId !== bundle.continuityTaskId &&
|
|
138
|
+
bundle.collaboration.association === null) {
|
|
139
|
+
ctx.addIssue({
|
|
140
|
+
code: 'custom',
|
|
141
|
+
path: ['collaboration', 'association'],
|
|
142
|
+
message: 'collaboration.taskId differs from continuityTaskId; an explicit association is required (no mapping is invented)',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const HANDOFF_ARTIFACT_MAX_BYTES = 8 * 1024 * 1024;
|
|
147
|
+
function invalid(message, nextAction) {
|
|
148
|
+
return failed(diagnosticCodes.document_record_invalid, message, {
|
|
149
|
+
nextAction,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** Deterministic key-sorted serialization for stable bytes and digests. */
|
|
153
|
+
function sortDeep(value) {
|
|
154
|
+
if (Array.isArray(value))
|
|
155
|
+
return value.map((item) => sortDeep(item));
|
|
156
|
+
if (value !== null && typeof value === 'object') {
|
|
157
|
+
const source = value;
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const key of Object.keys(source).sort()) {
|
|
160
|
+
out[key] = sortDeep(source[key]);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
export function serializeHandoffBundle(bundle) {
|
|
167
|
+
return Buffer.from(`${JSON.stringify(sortDeep(bundle), null, 2)}\n`, 'utf8');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A checkpoint reference is a private relative path only. Absolute paths,
|
|
171
|
+
* parent redirection and NUL bytes are refused before any filesystem access;
|
|
172
|
+
* the guarded continuity boundary re-validates the resolved path as well.
|
|
173
|
+
*/
|
|
174
|
+
function resolveCheckpointPath(root, continuityTaskId, ref) {
|
|
175
|
+
if (ref.length === 0 ||
|
|
176
|
+
ref.startsWith('/') ||
|
|
177
|
+
ref.includes('\0') ||
|
|
178
|
+
ref.split('/').includes('..')) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
let taskDir;
|
|
182
|
+
try {
|
|
183
|
+
taskDir = continuityTaskPaths(root, continuityTaskId).taskDir;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return join(taskDir, ref);
|
|
189
|
+
}
|
|
190
|
+
export function handoffBundleRelativeRef(bundleId) {
|
|
191
|
+
return `${HANDOFF_BUNDLES_DIRNAME}/${bundleId}.json`;
|
|
192
|
+
}
|
|
193
|
+
function handoffBundlePath(root, continuityTaskId, bundleId) {
|
|
194
|
+
return resolveCheckpointPath(root, continuityTaskId, handoffBundleRelativeRef(bundleId));
|
|
195
|
+
}
|
|
196
|
+
/** Build and validate a bundle manifest without touching the filesystem. */
|
|
197
|
+
export function buildHandoffBundle(input) {
|
|
198
|
+
const bundleId = input.bundleId ?? randomUUID();
|
|
199
|
+
const createdAt = (input.now?.() ?? new Date()).toISOString();
|
|
200
|
+
const candidate = {
|
|
201
|
+
schema: HANDOFF_BUNDLE_SCHEMA,
|
|
202
|
+
bundleId,
|
|
203
|
+
createdAt,
|
|
204
|
+
continuityTaskId: input.continuityTaskId,
|
|
205
|
+
checkpoint: input.checkpoint,
|
|
206
|
+
collaboration: input.collaboration,
|
|
207
|
+
sourceNativeSession: input.sourceNativeSession,
|
|
208
|
+
handoff: input.handoff,
|
|
209
|
+
limitations: [...(input.limitations ?? [])],
|
|
210
|
+
notes: input.notes ?? null,
|
|
211
|
+
};
|
|
212
|
+
const parsed = handoffBundleSchema.safeParse(candidate);
|
|
213
|
+
if (!parsed.success) {
|
|
214
|
+
return invalid('Handoff bundle failed shape or association validation.', 'Fix the reported field; identifiers are never truncated and no mapping is invented.');
|
|
215
|
+
}
|
|
216
|
+
return succeeded(parsed.data);
|
|
217
|
+
}
|
|
218
|
+
/** Parse untrusted bytes into a validated bundle without leaking content. */
|
|
219
|
+
export function parseHandoffBundle(raw) {
|
|
220
|
+
if (raw === null || typeof raw !== 'object') {
|
|
221
|
+
return invalid('Handoff bundle is not an object.', 'Provide a well-formed handoff bundle manifest.');
|
|
222
|
+
}
|
|
223
|
+
const schema = raw.schema;
|
|
224
|
+
if (schema !== HANDOFF_BUNDLE_SCHEMA) {
|
|
225
|
+
return failed(diagnosticCodes.configUnsupportedSchema, 'Handoff bundle schema is missing or unsupported.', {
|
|
226
|
+
nextAction: `Use a ${HANDOFF_BUNDLE_SCHEMA} manifest; other versions are not interpreted.`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const parsed = handoffBundleSchema.safeParse(raw);
|
|
230
|
+
if (!parsed.success) {
|
|
231
|
+
return invalid('Handoff bundle failed shape or association validation.', 'Repair the malformed bundle; private content is not echoed.');
|
|
232
|
+
}
|
|
233
|
+
return succeeded(parsed.data);
|
|
234
|
+
}
|
|
235
|
+
async function loadReferencedCheckpoint(root, bundle) {
|
|
236
|
+
const reference = bundle.checkpoint;
|
|
237
|
+
const path = resolveCheckpointPath(root, bundle.continuityTaskId, reference.ref);
|
|
238
|
+
if (path === null) {
|
|
239
|
+
return failed(diagnosticCodes.document_path_unsafe, 'Checkpoint reference is not a safe private relative path.', {
|
|
240
|
+
nextAction: 'Reference a checkpoint inside the private task directory; redirections are not followed.',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const read = await readContinuityBytes(root, bundle.continuityTaskId, path);
|
|
244
|
+
if (!read.ok) {
|
|
245
|
+
if (read.code === diagnosticCodes.fileNotFound) {
|
|
246
|
+
return failed(diagnosticCodes.fileNotFound, 'Referenced checkpoint is missing; the handoff bundle is not complete.', {
|
|
247
|
+
nextAction: 'Reference an existing checkpoint; a missing checkpoint is never reported as complete.',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return read;
|
|
251
|
+
}
|
|
252
|
+
if (sha256Digest(read.value) !== reference.digest) {
|
|
253
|
+
return failed(diagnosticCodes.document_file_changed, 'Referenced checkpoint bytes do not match the bound digest.', {
|
|
254
|
+
nextAction: 'Rebuild the bundle from the current checkpoint; changed bytes are never reported as complete.',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
let raw;
|
|
258
|
+
try {
|
|
259
|
+
raw = JSON.parse(read.value.toString('utf8'));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return failed(diagnosticCodes.document_source_invalid, 'Referenced checkpoint is not valid JSON.', { nextAction: 'Reference a valid checkpoint file.' });
|
|
263
|
+
}
|
|
264
|
+
const parsed = checkpointSchema.safeParse(raw);
|
|
265
|
+
if (!parsed.success) {
|
|
266
|
+
return failed(diagnosticCodes.document_source_invalid, 'Referenced checkpoint does not satisfy the current checkpoint schema.', { nextAction: 'Reference a checkpoint valid under the current schema.' });
|
|
267
|
+
}
|
|
268
|
+
const checkpoint = parsed.data;
|
|
269
|
+
// Enforce task/workspace/source-session association by identity, not path.
|
|
270
|
+
const associationMismatch = checkpoint.checkpointId !== reference.checkpointId ||
|
|
271
|
+
checkpoint.taskId !== reference.taskId ||
|
|
272
|
+
checkpoint.workspaceId !== reference.workspaceId ||
|
|
273
|
+
checkpoint.authorSession !== reference.authorSession ||
|
|
274
|
+
checkpoint.contextCompleteness !== reference.contextCompleteness;
|
|
275
|
+
if (associationMismatch) {
|
|
276
|
+
return failed(diagnosticCodes.document_source_invalid, 'Referenced checkpoint identity does not match the bound association.', {
|
|
277
|
+
nextAction: 'Bind the exact checkpoint id/task/workspace/author-session; no mapping is inferred.',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (checkpoint.authorSession !== null &&
|
|
281
|
+
bundle.sourceNativeSession.sessionId !== checkpoint.authorSession) {
|
|
282
|
+
return failed(diagnosticCodes.document_source_invalid, 'Source native session does not match the checkpoint author session.', {
|
|
283
|
+
nextAction: 'Hand off from the session that authored the checkpoint, or record the correct source session.',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Bind the checkpoint to the actual registered workspace of this root, not to
|
|
287
|
+
// the producer's own two matching copies of a workspace id. Inspect the
|
|
288
|
+
// current workspace configuration safely; a missing or invalid workspace
|
|
289
|
+
// stays unavailable and is never silently initialized.
|
|
290
|
+
const workspace = await loadWorkspaceConfig(root);
|
|
291
|
+
if (!workspace.ok) {
|
|
292
|
+
return failed(workspace.code, 'Current workspace configuration is unavailable; the checkpoint workspace identity cannot be confirmed.', {
|
|
293
|
+
nextAction: 'Run from a registered workspace; a missing or invalid workspace is never assumed or initialized.',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (workspace.value.workspaceId !== checkpoint.workspaceId) {
|
|
297
|
+
return failed(diagnosticCodes.document_source_invalid, 'Checkpoint workspace id does not match the current registered workspace.', {
|
|
298
|
+
nextAction: 'Use a checkpoint produced in this workspace; cross-workspace references are refused.',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return succeeded(checkpoint);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Validate the referenced checkpoint, then publish the bundle immutably and
|
|
305
|
+
* exclusively. A missing, changed or mismatched checkpoint fails before any
|
|
306
|
+
* write; an existing bundle id is never replaced.
|
|
307
|
+
*/
|
|
308
|
+
export async function saveHandoffBundle(input) {
|
|
309
|
+
if (!isValidTaskId(input.continuityTaskId)) {
|
|
310
|
+
return invalid('Invalid continuity task id for handoff bundle.', 'Use a continuity task id of up to 64 safe characters.');
|
|
311
|
+
}
|
|
312
|
+
const built = buildHandoffBundle(input);
|
|
313
|
+
if (!built.ok)
|
|
314
|
+
return built;
|
|
315
|
+
const bundle = built.value;
|
|
316
|
+
const checkpoint = await loadReferencedCheckpoint(input.root, bundle);
|
|
317
|
+
if (!checkpoint.ok)
|
|
318
|
+
return checkpoint;
|
|
319
|
+
const bytes = serializeHandoffBundle(bundle);
|
|
320
|
+
if (bytes.length > HANDOFF_BUNDLE_MAX_BYTES) {
|
|
321
|
+
return failed(diagnosticCodes.fileTooLarge, 'Serialized handoff bundle exceeds the maximum size.', {
|
|
322
|
+
nextAction: 'Reduce the snapshot collections; oversized bundles are refused without storing content.',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const path = handoffBundlePath(input.root, bundle.continuityTaskId, bundle.bundleId);
|
|
326
|
+
if (path === null) {
|
|
327
|
+
return failed(diagnosticCodes.document_path_unsafe, 'Computed handoff bundle path is unsafe.', { nextAction: 'Use a valid private continuity task namespace.' });
|
|
328
|
+
}
|
|
329
|
+
// Re-validate the private boundary immediately before the exclusive publish so
|
|
330
|
+
// a parent redirected after the initial check (TOCTOU) cannot place the file
|
|
331
|
+
// outside the task namespace. The caller's own seam still runs first.
|
|
332
|
+
const writeOptions = {
|
|
333
|
+
beforeExclusivePublish: async () => {
|
|
334
|
+
await input.writeOptions?.beforeExclusivePublish?.();
|
|
335
|
+
const recheck = await ensurePrivateTaskBoundary(input.root, bundle.continuityTaskId, path);
|
|
336
|
+
if (!recheck.ok) {
|
|
337
|
+
throw new Error(recheck.message);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
const write = await writeContinuityBytes(input.root, bundle.continuityTaskId, path, bytes, true, writeOptions);
|
|
342
|
+
if (!write.ok)
|
|
343
|
+
return write;
|
|
344
|
+
return succeeded({
|
|
345
|
+
bundleId: bundle.bundleId,
|
|
346
|
+
path,
|
|
347
|
+
relativeRef: handoffBundleRelativeRef(bundle.bundleId),
|
|
348
|
+
digest: write.value.digest,
|
|
349
|
+
bundle,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** Read and validate one stored bundle; private content is never leaked. */
|
|
353
|
+
export async function readHandoffBundle(options) {
|
|
354
|
+
if (!isValidTaskId(options.continuityTaskId)) {
|
|
355
|
+
return invalid('Invalid continuity task id for handoff bundle.', 'Use a continuity task id of up to 64 safe characters.');
|
|
356
|
+
}
|
|
357
|
+
const path = handoffBundlePath(options.root, options.continuityTaskId, options.bundleId);
|
|
358
|
+
if (path === null) {
|
|
359
|
+
return failed(diagnosticCodes.document_path_unsafe, 'Handoff bundle id does not resolve to a safe private path.', {
|
|
360
|
+
nextAction: 'Use a valid bundle id inside the private task namespace.',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const read = await readContinuityBytes(options.root, options.continuityTaskId, path);
|
|
364
|
+
if (!read.ok)
|
|
365
|
+
return read;
|
|
366
|
+
if (read.value.length > HANDOFF_BUNDLE_MAX_BYTES) {
|
|
367
|
+
return failed(diagnosticCodes.fileTooLarge, 'Stored handoff bundle exceeds the maximum size.', {
|
|
368
|
+
nextAction: 'Inspect the oversized bundle explicitly; it is not parsed.',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
let raw;
|
|
372
|
+
try {
|
|
373
|
+
raw = JSON.parse(read.value.toString('utf8'));
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return invalid('Stored handoff bundle is not valid JSON.', 'Inspect the malformed bundle; private content is not echoed.');
|
|
377
|
+
}
|
|
378
|
+
const parsed = parseHandoffBundle(raw);
|
|
379
|
+
if (!parsed.ok)
|
|
380
|
+
return parsed;
|
|
381
|
+
if (parsed.value.bundleId !== options.bundleId) {
|
|
382
|
+
return invalid('Stored handoff bundle id does not match its filename.', 'Inspect the bundle; its id and location must agree.');
|
|
383
|
+
}
|
|
384
|
+
if (parsed.value.continuityTaskId !== options.continuityTaskId) {
|
|
385
|
+
return failed(diagnosticCodes.document_source_invalid, 'Stored handoff bundle targets a different continuity task than requested.', {
|
|
386
|
+
nextAction: 'Read a bundle under its own continuity task; a copied manifest is not followed across namespaces.',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return succeeded({
|
|
390
|
+
bundle: parsed.value,
|
|
391
|
+
digest: sha256Digest(read.value),
|
|
392
|
+
path,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function completenessOf(bundle) {
|
|
396
|
+
return {
|
|
397
|
+
claims: bundle.handoff.claimsCompleteness,
|
|
398
|
+
pendingTools: bundle.handoff.pendingTools.completeness,
|
|
399
|
+
pendingMessages: bundle.handoff.pendingMessages.completeness,
|
|
400
|
+
pendingApprovals: bundle.handoff.pendingApprovals.completeness,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async function checkCheckpointArtifacts(root, checkpoint) {
|
|
404
|
+
const checks = [];
|
|
405
|
+
// Resolve the canonical workspace root so an artifact missing under a
|
|
406
|
+
// non-canonical root (for example a /var -> /private/var symlink) is still
|
|
407
|
+
// classified as missing rather than unsafe.
|
|
408
|
+
const canonicalRoot = (await resolveRealpath(root)) ?? root;
|
|
409
|
+
for (const artifact of checkpoint.artifacts) {
|
|
410
|
+
const resolved = await readArtifactPath(canonicalRoot, artifact.path);
|
|
411
|
+
if (!resolved.ok) {
|
|
412
|
+
checks.push({ path: artifact.path, status: 'unsafe' });
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const read = await readFileBounded(resolved.value, HANDOFF_ARTIFACT_MAX_BYTES);
|
|
416
|
+
if (!read.ok) {
|
|
417
|
+
checks.push({
|
|
418
|
+
path: artifact.path,
|
|
419
|
+
status: read.code === diagnosticCodes.fileNotFound ? 'missing' : 'unreadable',
|
|
420
|
+
});
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
checks.push({
|
|
424
|
+
path: artifact.path,
|
|
425
|
+
status: read.value.digest === artifact.digest ? 'unchanged' : 'changed',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return checks;
|
|
429
|
+
}
|
|
430
|
+
function contextIntegrityOf(checkpoint, artifactChecks) {
|
|
431
|
+
if (artifactChecks.some((check) => check.status === 'unsafe' || check.status === 'unreadable')) {
|
|
432
|
+
return 'unknown';
|
|
433
|
+
}
|
|
434
|
+
if (checkpoint.contextCompleteness !== 'complete' ||
|
|
435
|
+
artifactChecks.some((check) => check.status !== 'unchanged')) {
|
|
436
|
+
return 'incomplete';
|
|
437
|
+
}
|
|
438
|
+
return 'complete';
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Re-validate a bundle against its referenced checkpoint. Any missing, changed,
|
|
442
|
+
* mismatched or invalid checkpoint fails verification, so a defective bundle is
|
|
443
|
+
* never reported as complete. Success never implies execution authority.
|
|
444
|
+
*/
|
|
445
|
+
export async function verifyHandoffBundle(options) {
|
|
446
|
+
let bundle;
|
|
447
|
+
if ('bundle' in options) {
|
|
448
|
+
const reparsed = parseHandoffBundle(options.bundle);
|
|
449
|
+
if (!reparsed.ok)
|
|
450
|
+
return reparsed;
|
|
451
|
+
bundle = reparsed.value;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
const read = await readHandoffBundle(options);
|
|
455
|
+
if (!read.ok)
|
|
456
|
+
return read;
|
|
457
|
+
bundle = read.value.bundle;
|
|
458
|
+
}
|
|
459
|
+
const checkpoint = await loadReferencedCheckpoint(options.root, bundle);
|
|
460
|
+
if (!checkpoint.ok)
|
|
461
|
+
return checkpoint;
|
|
462
|
+
const artifactChecks = await checkCheckpointArtifacts(options.root, checkpoint.value);
|
|
463
|
+
return succeeded({
|
|
464
|
+
bundleId: bundle.bundleId,
|
|
465
|
+
continuityTaskId: bundle.continuityTaskId,
|
|
466
|
+
checkpointPresent: true,
|
|
467
|
+
checkpointDigestMatches: true,
|
|
468
|
+
checkpointSchemaValid: true,
|
|
469
|
+
associationValid: true,
|
|
470
|
+
workspaceIdentityMatches: true,
|
|
471
|
+
checkpointContextCompleteness: checkpoint.value.contextCompleteness,
|
|
472
|
+
artifactChecks,
|
|
473
|
+
contextIntegrity: contextIntegrityOf(checkpoint.value, artifactChecks),
|
|
474
|
+
executionAllowed: false,
|
|
475
|
+
completeness: completenessOf(bundle),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { CheckpointService } from './service.js';
|
|
2
2
|
export type { ContinuityServiceOptions, NativeRefInput, ResumeCheckpointInput, ResumeCheckpointResult, SaveCheckpointInput, SaveCheckpointResult, ValidationInput, } from './service.js';
|
|
3
3
|
export type { Checkpoint, CheckpointPointer, ResumePacket, TaskContext, } from '../schemas/continuity.js';
|
|
4
|
+
export { HANDOFF_BUNDLE_SCHEMA, HANDOFF_BUNDLE_MAX_BYTES, HANDOFF_BUNDLES_DIRNAME, handoffBundleSchema, handoffCheckpointReferenceSchema, handoffCollaborationBindingSchema, handoffSourceNativeSessionSchema, handoffSnapshotSchema, handoffBundleRelativeRef, serializeHandoffBundle, buildHandoffBundle, parseHandoffBundle, saveHandoffBundle, readHandoffBundle, verifyHandoffBundle, } from './handoff-bundle.js';
|
|
5
|
+
export type { HandoffBundle, HandoffCheckpointReference, HandoffCollaborationBinding, HandoffSourceNativeSession, HandoffSnapshot, HandoffCompletenessSummary, HandoffArtifactCheck, SaveHandoffBundleInput, SaveHandoffBundleResult, ReadHandoffBundleResult, VerifyHandoffBundleResult, } from './handoff-bundle.js';
|
package/dist/continuity/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { CheckpointService } from './service.js';
|
|
2
|
+
export { HANDOFF_BUNDLE_SCHEMA, HANDOFF_BUNDLE_MAX_BYTES, HANDOFF_BUNDLES_DIRNAME, handoffBundleSchema, handoffCheckpointReferenceSchema, handoffCollaborationBindingSchema, handoffSourceNativeSessionSchema, handoffSnapshotSchema, handoffBundleRelativeRef, serializeHandoffBundle, buildHandoffBundle, parseHandoffBundle, saveHandoffBundle, readHandoffBundle, verifyHandoffBundle, } from './handoff-bundle.js';
|
package/dist/continuity/paths.js
CHANGED
|
@@ -3,10 +3,12 @@ import { link, mkdir, open, rename, rm, unlink } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
|
|
5
5
|
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
+
import { isValidTaskId } from '../schemas/continuity.js';
|
|
6
7
|
import { isPathInside, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
|
|
7
8
|
import { observePath, readFileBounded, readTextFileBounded, resolveRealpath, sha256Digest, } from '../infra/fs.js';
|
|
8
9
|
import { resolveWorkspaceRoot } from '../workspace/resolve.js';
|
|
9
|
-
|
|
10
|
+
import { ensurePrivateState } from '../infra/private-state.js';
|
|
11
|
+
/* The shared task identifier rule lives in schemas/continuity.ts. */
|
|
10
12
|
/**
|
|
11
13
|
* Shared bound for private recovery payloads. Capture, durable writes and
|
|
12
14
|
* resume reads all use this value; a payload over the bound is captured only
|
|
@@ -14,7 +16,7 @@ const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
14
16
|
*/
|
|
15
17
|
export const CONTINUITY_PAYLOAD_MAX_BYTES = 16 * 1024 * 1024;
|
|
16
18
|
export function continuityTaskPaths(root, taskId) {
|
|
17
|
-
if (!
|
|
19
|
+
if (!isValidTaskId(taskId)) {
|
|
18
20
|
throw new RangeError(`Invalid task id: ${taskId}`);
|
|
19
21
|
}
|
|
20
22
|
const taskDir = join(root, '.pappra', 'local', 'tasks', taskId);
|
|
@@ -134,6 +136,9 @@ export async function writeContinuityBytes(root, taskId, path, bytes, exclusive,
|
|
|
134
136
|
const boundary = await ensurePrivateTaskBoundary(root, taskId, path);
|
|
135
137
|
if (!boundary.ok)
|
|
136
138
|
return boundary;
|
|
139
|
+
const privateState = await ensurePrivateState(root);
|
|
140
|
+
if (!privateState.ok)
|
|
141
|
+
return privateState;
|
|
137
142
|
const temporary = await writeTemporary(path, bytes);
|
|
138
143
|
if (!temporary.ok) {
|
|
139
144
|
return failed(diagnosticCodes.fileUnreadable, temporary.message, {
|
|
@@ -180,6 +185,9 @@ export async function replaceContinuityBytesIfDigest(root, taskId, path, bytes,
|
|
|
180
185
|
const boundary = await ensurePrivateTaskBoundary(root, taskId, path);
|
|
181
186
|
if (!boundary.ok)
|
|
182
187
|
return boundary;
|
|
188
|
+
const privateState = await ensurePrivateState(root);
|
|
189
|
+
if (!privateState.ok)
|
|
190
|
+
return privateState;
|
|
183
191
|
const temporary = await writeTemporary(path, bytes);
|
|
184
192
|
if (!temporary.ok) {
|
|
185
193
|
return failed(diagnosticCodes.fileUnreadable, temporary.message, {
|