@phyzess/pappra 0.1.0-beta.14 → 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 +133 -14
- 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 +104 -3
- 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,123 @@
|
|
|
1
|
+
import type { AppliedMessage, Message, TaskRecord } from './types.js';
|
|
2
|
+
import { type CollabDelivery } from '../schemas/collaboration.js';
|
|
3
|
+
import { type Outcome } from '../schemas/outcome.js';
|
|
4
|
+
export interface MessageServiceFailure {
|
|
5
|
+
readonly ok: false;
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly nextAction: string | null;
|
|
9
|
+
}
|
|
10
|
+
export type MessageServiceResult<T> = Outcome<T, MessageServiceFailure>;
|
|
11
|
+
export interface MessagePathIdentity {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly dev: number;
|
|
14
|
+
readonly ino: number;
|
|
15
|
+
}
|
|
16
|
+
export interface MessageNamespaceIdentity {
|
|
17
|
+
readonly taskRoot: MessagePathIdentity;
|
|
18
|
+
readonly messagesDirectory: MessagePathIdentity;
|
|
19
|
+
readonly recordFile: MessagePathIdentity;
|
|
20
|
+
}
|
|
21
|
+
export interface OpenedMessageTask {
|
|
22
|
+
readonly taskRoot: string;
|
|
23
|
+
readonly domainRoot: string;
|
|
24
|
+
readonly record: TaskRecord;
|
|
25
|
+
readonly messagesDirectory: string;
|
|
26
|
+
readonly recordFile: string;
|
|
27
|
+
readonly identity: MessageNamespaceIdentity;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Opens one authoritative task root and proves it is the canonical private
|
|
31
|
+
* namespace it claims to be. Redirected paths, mismatched records and unsafe
|
|
32
|
+
* layouts fail closed before any caller writes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function openMessageTask(taskRootInput: string): Promise<MessageServiceResult<OpenedMessageTask>>;
|
|
35
|
+
/**
|
|
36
|
+
* Revalidates the task namespace after an async seam. A root that was renamed,
|
|
37
|
+
* replaced or redirected in the meantime fails closed before any mutation or
|
|
38
|
+
* native submission.
|
|
39
|
+
* task.json is atomically replaced by normal store commits; its inode is not
|
|
40
|
+
* namespace identity. openMessageTask still validates its type and task/domain.
|
|
41
|
+
* This checks paths only. Callers must separately revalidate current authority
|
|
42
|
+
* after async work, preferably inside the transaction that commits the effect.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertMessageTaskCurrent(opened: OpenedMessageTask): Promise<MessageServiceFailure | null>;
|
|
45
|
+
export interface RegisteredMessage {
|
|
46
|
+
readonly message: Message;
|
|
47
|
+
readonly file: string;
|
|
48
|
+
readonly digest: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The one bounded, identity-checked reader for registered messages. A final
|
|
52
|
+
* symlink, non-regular file, oversized file, redirected path or id/content
|
|
53
|
+
* mismatch never becomes protocol content.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readRegisteredMessageSafe(opened: OpenedMessageTask, messageId: string): Promise<MessageServiceResult<RegisteredMessage>>;
|
|
56
|
+
export interface MessageSummary {
|
|
57
|
+
readonly message_id: string;
|
|
58
|
+
readonly type: string;
|
|
59
|
+
readonly task_id: string;
|
|
60
|
+
readonly from: Message['from'];
|
|
61
|
+
readonly to: Message['to'];
|
|
62
|
+
readonly reply_to: string | null;
|
|
63
|
+
readonly subtask_id: string | null;
|
|
64
|
+
readonly attempt: number | null;
|
|
65
|
+
readonly content_digest: string | null;
|
|
66
|
+
}
|
|
67
|
+
export interface MessageSendResult {
|
|
68
|
+
readonly status: 'planned' | 'accepted' | 'duplicate' | 'conflict' | 'rejected';
|
|
69
|
+
readonly message: MessageSummary;
|
|
70
|
+
readonly registrationStatus: string | null;
|
|
71
|
+
readonly stateRevision: number | null;
|
|
72
|
+
readonly messagePath: string | null;
|
|
73
|
+
readonly digest: string | null;
|
|
74
|
+
readonly detail: string | null;
|
|
75
|
+
/** Persistence only: a successful send is not native delivery. */
|
|
76
|
+
readonly delivery: 'not-attempted' | 'persisted-not-notified';
|
|
77
|
+
readonly notified: false;
|
|
78
|
+
readonly taskAccepted: false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validates one envelope file, then (with apply) registers and applies it through
|
|
82
|
+
* the existing protocol. Planning writes nothing and spawns nothing.
|
|
83
|
+
*/
|
|
84
|
+
export declare function sendMessageFromFile(taskRootInput: string, envelopeFile: string, options: {
|
|
85
|
+
readonly apply: boolean;
|
|
86
|
+
readonly clock?: number;
|
|
87
|
+
}): Promise<MessageServiceResult<MessageSendResult>>;
|
|
88
|
+
export declare function deliveryPathFor(taskRoot: string, messageId: string): string;
|
|
89
|
+
/** Reads task-private delivery metadata without creating it. */
|
|
90
|
+
export declare function readDeliveryRecord(taskRoot: string, messageId: string): CollabDelivery | null;
|
|
91
|
+
export interface MessageReadResult {
|
|
92
|
+
readonly message: Message;
|
|
93
|
+
readonly entry: AppliedMessage | null;
|
|
94
|
+
readonly messagePath: string;
|
|
95
|
+
readonly digest: string;
|
|
96
|
+
readonly delivery: CollabDelivery | null;
|
|
97
|
+
readonly writes: false;
|
|
98
|
+
}
|
|
99
|
+
export declare function readMessageFromTaskRoot(taskRootInput: string, messageId: string): Promise<MessageServiceResult<MessageReadResult>>;
|
|
100
|
+
export interface MessageListEntry {
|
|
101
|
+
readonly message_id: string;
|
|
102
|
+
readonly type: string;
|
|
103
|
+
readonly state: string;
|
|
104
|
+
readonly reply_to: string | null;
|
|
105
|
+
readonly subtask_id: string | null;
|
|
106
|
+
readonly attempt: number | null;
|
|
107
|
+
readonly at: string;
|
|
108
|
+
readonly applied_at: string;
|
|
109
|
+
readonly content_digest: string;
|
|
110
|
+
readonly registered: boolean;
|
|
111
|
+
readonly delivery: CollabDelivery | null;
|
|
112
|
+
}
|
|
113
|
+
export interface MessageListResult {
|
|
114
|
+
readonly total: number;
|
|
115
|
+
readonly returned: number;
|
|
116
|
+
readonly truncated: boolean;
|
|
117
|
+
readonly messages: readonly MessageListEntry[];
|
|
118
|
+
readonly writes: false;
|
|
119
|
+
}
|
|
120
|
+
/** Lists the most recent applied messages from the authoritative record. */
|
|
121
|
+
export declare function listMessagesForTaskRoot(taskRootInput: string, options?: {
|
|
122
|
+
readonly limit?: number;
|
|
123
|
+
}): Promise<MessageServiceResult<MessageListResult>>;
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isSafeMessageId, messageFile, validateEnvelope } from './envelope.js';
|
|
5
|
+
import { submitMessage } from './messages.js';
|
|
6
|
+
import { messagesDir, notificationsDir, readRecord } from './store.js';
|
|
7
|
+
import { collabDeliverySchema, } from '../schemas/collaboration.js';
|
|
8
|
+
import { isPathInside } from '../schemas/paths.js';
|
|
9
|
+
import { observePath, resolveRealpath } from '../infra/fs.js';
|
|
10
|
+
import { errorMessage } from '../schemas/outcome.js';
|
|
11
|
+
/**
|
|
12
|
+
* Public persisted-message operations over the existing collaboration protocol.
|
|
13
|
+
*
|
|
14
|
+
* These operations only read and write the authoritative task record, message
|
|
15
|
+
* registration files and ledger. They never start a native process, never notify
|
|
16
|
+
* anyone, and never treat protocol acceptance as task acceptance or delivery.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_ENVELOPE_BYTES = 1024 * 1024;
|
|
19
|
+
const MAX_DELIVERY_BYTES = 64 * 1024;
|
|
20
|
+
const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
21
|
+
const DEFAULT_LIST_LIMIT = 20;
|
|
22
|
+
const MAX_LIST_LIMIT = 100;
|
|
23
|
+
function serviceFailure(code, message, nextAction) {
|
|
24
|
+
return { ok: false, code, message, nextAction };
|
|
25
|
+
}
|
|
26
|
+
function digestOf(bytes) {
|
|
27
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
function recordPathOf(taskRoot) {
|
|
30
|
+
return path.join(taskRoot, 'task.json');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Opens one authoritative task root and proves it is the canonical private
|
|
34
|
+
* namespace it claims to be. Redirected paths, mismatched records and unsafe
|
|
35
|
+
* layouts fail closed before any caller writes.
|
|
36
|
+
*/
|
|
37
|
+
export async function openMessageTask(taskRootInput) {
|
|
38
|
+
if (!path.isAbsolute(taskRootInput)) {
|
|
39
|
+
return serviceFailure('task_root_required', `collab message requires an absolute --task-root: ${taskRootInput}`, 'Pass the absolute task root that contains task.json.');
|
|
40
|
+
}
|
|
41
|
+
const requested = path.resolve(taskRootInput);
|
|
42
|
+
const tasksCandidate = path.dirname(requested);
|
|
43
|
+
const domainCandidate = path.dirname(tasksCandidate);
|
|
44
|
+
const observation = await observePath(requested).catch((error) => serviceFailure('task_root_unreadable', `Cannot inspect task root ${requested}: ${errorMessage(error)}`, 'Check permissions and the path; nothing was read or written.'));
|
|
45
|
+
if ('ok' in observation)
|
|
46
|
+
return observation;
|
|
47
|
+
if (observation.kind === 'symlink') {
|
|
48
|
+
return serviceFailure('task_root_redirected', `Task root is a symlink: ${requested}`, 'Pass the real task root; Pappra does not follow a redirected task namespace.');
|
|
49
|
+
}
|
|
50
|
+
if (observation.kind !== 'directory') {
|
|
51
|
+
return serviceFailure('task_root_missing', `Task root is not an existing directory: ${requested}`, 'Pass the absolute task root that contains task.json.');
|
|
52
|
+
}
|
|
53
|
+
// System-level aliases above the domain (for example /var on macOS) are resolved.
|
|
54
|
+
// The domain, its `tasks` parent, the task root and nested state must be real:
|
|
55
|
+
// a symlinked `tasks` parent (even into another valid domain) is a redirect.
|
|
56
|
+
if (path.basename(tasksCandidate) !== 'tasks') {
|
|
57
|
+
return serviceFailure('task_root_layout_invalid', `Task root is not a <domain>/tasks/<task-id> directory: ${requested}`, 'Pass a task root created by the collaboration prepare flow.');
|
|
58
|
+
}
|
|
59
|
+
// The domain entry itself must be a real directory. Aliases above it (for
|
|
60
|
+
// example /tmp or /var) are fine; a `domain -> real-domain` link is a redirect.
|
|
61
|
+
const domainObservation = await observePath(domainCandidate).catch((error) => serviceFailure('task_root_unreadable', `Cannot inspect the task domain ${domainCandidate}: ${errorMessage(error)}`, 'Inspect the path; nothing was read or written.'));
|
|
62
|
+
if ('ok' in domainObservation)
|
|
63
|
+
return domainObservation;
|
|
64
|
+
if (domainObservation.kind === 'symlink') {
|
|
65
|
+
return serviceFailure('task_root_redirected', `Task domain is a symlink: ${domainCandidate}`, 'Pass the real domain root; Pappra does not follow a redirected task namespace.');
|
|
66
|
+
}
|
|
67
|
+
if (domainObservation.kind !== 'directory' ||
|
|
68
|
+
domainObservation.stats === null) {
|
|
69
|
+
return serviceFailure('task_root_layout_invalid', `Task domain is not a directory: ${domainCandidate}`, 'Pass a task root created by the collaboration prepare flow.');
|
|
70
|
+
}
|
|
71
|
+
const canonicalDomain = await resolveRealpath(domainCandidate);
|
|
72
|
+
if (canonicalDomain === null) {
|
|
73
|
+
return serviceFailure('task_root_unreadable', `Cannot resolve the task domain: ${domainCandidate}`, 'Inspect the path; nothing was read or written.');
|
|
74
|
+
}
|
|
75
|
+
const tasksObservation = await observePath(tasksCandidate).catch((error) => serviceFailure('task_root_redirected', `Cannot inspect the tasks parent ${tasksCandidate}: ${errorMessage(error)}`, 'Inspect the path; nothing was read or written.'));
|
|
76
|
+
if ('ok' in tasksObservation)
|
|
77
|
+
return tasksObservation;
|
|
78
|
+
if (tasksObservation.kind !== 'directory' ||
|
|
79
|
+
tasksObservation.stats === null) {
|
|
80
|
+
return serviceFailure('task_root_redirected', `Tasks parent is not a real directory: ${tasksCandidate}`, 'Restore a real tasks directory under the domain; nothing was read or written.');
|
|
81
|
+
}
|
|
82
|
+
const canonicalTasks = await resolveRealpath(tasksCandidate);
|
|
83
|
+
if (canonicalTasks !== path.join(canonicalDomain, 'tasks')) {
|
|
84
|
+
return serviceFailure('task_root_redirected', `Tasks parent is redirected outside the domain: ${tasksCandidate}`, 'Restore a real tasks directory under the domain; nothing was read or written.');
|
|
85
|
+
}
|
|
86
|
+
const taskRoot = await resolveRealpath(requested);
|
|
87
|
+
if (taskRoot === null) {
|
|
88
|
+
return serviceFailure('task_root_unreadable', `Cannot resolve task root: ${requested}`, 'Inspect the path; nothing was read or written.');
|
|
89
|
+
}
|
|
90
|
+
if (taskRoot !== path.join(canonicalDomain, 'tasks', path.basename(requested))) {
|
|
91
|
+
return serviceFailure('task_root_redirected', `Task root resolves outside its domain: ${requested} -> ${taskRoot}`, 'Pass the real task root; Pappra does not follow a redirected task namespace.');
|
|
92
|
+
}
|
|
93
|
+
const recordFile = recordPathOf(taskRoot);
|
|
94
|
+
const recordObservation = await observePath(recordFile).catch((error) => serviceFailure('task_record_unreadable', `Cannot inspect task record ${recordFile}: ${errorMessage(error)}`, 'Check permissions; nothing was read or written.'));
|
|
95
|
+
if ('ok' in recordObservation)
|
|
96
|
+
return recordObservation;
|
|
97
|
+
if (recordObservation.kind === 'missing') {
|
|
98
|
+
return serviceFailure('task_record_missing', `No task record at ${recordFile}`, 'Prepare the task first; a message is never sent into an unregistered task.');
|
|
99
|
+
}
|
|
100
|
+
if (recordObservation.kind !== 'file') {
|
|
101
|
+
return serviceFailure('task_record_invalid', `Task record is not a regular file: ${recordFile}`, 'Inspect the record; Pappra does not follow redirected task state.');
|
|
102
|
+
}
|
|
103
|
+
const realRecord = await resolveRealpath(recordFile);
|
|
104
|
+
if (realRecord === null || !isPathInside(taskRoot, realRecord)) {
|
|
105
|
+
return serviceFailure('task_record_unsafe', `Task record resolves outside the task root: ${recordFile}`, 'Inspect or remove the redirected record; nothing was read into the protocol.');
|
|
106
|
+
}
|
|
107
|
+
let record;
|
|
108
|
+
try {
|
|
109
|
+
record = readRecord(taskRoot);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return serviceFailure('task_record_invalid', `Task record is not usable: ${errorMessage(error)}`, 'Inspect the record; Pappra does not repair or reinterpret it.');
|
|
113
|
+
}
|
|
114
|
+
if (record === null) {
|
|
115
|
+
return serviceFailure('task_record_missing', `No task record at ${recordFile}`, 'Prepare the task first; a message is never sent into an unregistered task.');
|
|
116
|
+
}
|
|
117
|
+
const recordRoot = await resolveRealpath(record.task_root);
|
|
118
|
+
if (typeof record.task_id !== 'string' ||
|
|
119
|
+
!TASK_ID_PATTERN.test(record.task_id) ||
|
|
120
|
+
recordRoot === null ||
|
|
121
|
+
recordRoot !== taskRoot ||
|
|
122
|
+
path.basename(taskRoot) !== record.task_id) {
|
|
123
|
+
return serviceFailure('task_record_identity_mismatch', `Task record does not describe ${taskRoot}`, 'Pass the task root recorded in task.json; identity is never guessed.');
|
|
124
|
+
}
|
|
125
|
+
const domainRoot = await resolveRealpath(record.domain_root);
|
|
126
|
+
if (!path.isAbsolute(record.domain_root) ||
|
|
127
|
+
domainRoot === null ||
|
|
128
|
+
domainRoot !== canonicalDomain ||
|
|
129
|
+
path.dirname(path.dirname(taskRoot)) !== canonicalDomain) {
|
|
130
|
+
return serviceFailure('task_record_identity_mismatch', `Task record domain does not contain ${taskRoot}`, 'Inspect the record domain; cross-domain messages are not inferred.');
|
|
131
|
+
}
|
|
132
|
+
const directory = messagesDir(taskRoot);
|
|
133
|
+
const messagesObservation = await observePath(directory).catch((error) => serviceFailure('task_messages_unreadable', `Cannot inspect ${directory}: ${errorMessage(error)}`, 'Check permissions; nothing was read or written.'));
|
|
134
|
+
if ('ok' in messagesObservation)
|
|
135
|
+
return messagesObservation;
|
|
136
|
+
if (messagesObservation.kind === 'missing') {
|
|
137
|
+
return serviceFailure('task_messages_missing', `Task has no messages directory: ${directory}`, 'Inspect the task; Pappra does not recreate protocol state silently.');
|
|
138
|
+
}
|
|
139
|
+
if (messagesObservation.kind !== 'directory') {
|
|
140
|
+
return serviceFailure('task_messages_unsafe', `Messages path is not a regular directory: ${directory}`, 'Inspect or remove the redirected messages path; nothing was written through it.');
|
|
141
|
+
}
|
|
142
|
+
const realMessages = await resolveRealpath(directory);
|
|
143
|
+
if (realMessages === null || realMessages !== directory) {
|
|
144
|
+
return serviceFailure('task_messages_unsafe', `Messages directory is redirected: ${directory}`, 'Restore a real messages directory inside the task root; Pappra does not follow symlinks.');
|
|
145
|
+
}
|
|
146
|
+
if (!isPathInside(taskRoot, realMessages)) {
|
|
147
|
+
return serviceFailure('task_messages_unsafe', `Messages directory resolves outside the task root: ${directory}`, 'Restore a real messages directory inside the task root.');
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
value: {
|
|
152
|
+
taskRoot,
|
|
153
|
+
domainRoot: canonicalDomain,
|
|
154
|
+
record,
|
|
155
|
+
messagesDirectory: directory,
|
|
156
|
+
recordFile,
|
|
157
|
+
identity: {
|
|
158
|
+
taskRoot: {
|
|
159
|
+
path: taskRoot,
|
|
160
|
+
dev: observation.stats.dev,
|
|
161
|
+
ino: observation.stats.ino,
|
|
162
|
+
},
|
|
163
|
+
messagesDirectory: {
|
|
164
|
+
path: directory,
|
|
165
|
+
dev: messagesObservation.stats.dev,
|
|
166
|
+
ino: messagesObservation.stats.ino,
|
|
167
|
+
},
|
|
168
|
+
recordFile: {
|
|
169
|
+
path: recordFile,
|
|
170
|
+
dev: recordObservation.stats.dev,
|
|
171
|
+
ino: recordObservation.stats.ino,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function sameIdentity(left, right) {
|
|
178
|
+
return (left.path === right.path && left.dev === right.dev && left.ino === right.ino);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Revalidates the task namespace after an async seam. A root that was renamed,
|
|
182
|
+
* replaced or redirected in the meantime fails closed before any mutation or
|
|
183
|
+
* native submission.
|
|
184
|
+
* task.json is atomically replaced by normal store commits; its inode is not
|
|
185
|
+
* namespace identity. openMessageTask still validates its type and task/domain.
|
|
186
|
+
* This checks paths only. Callers must separately revalidate current authority
|
|
187
|
+
* after async work, preferably inside the transaction that commits the effect.
|
|
188
|
+
*/
|
|
189
|
+
export async function assertMessageTaskCurrent(opened) {
|
|
190
|
+
const again = await openMessageTask(opened.taskRoot);
|
|
191
|
+
if (!again.ok)
|
|
192
|
+
return again;
|
|
193
|
+
const current = again.value.identity;
|
|
194
|
+
if (!sameIdentity(current.taskRoot, opened.identity.taskRoot) ||
|
|
195
|
+
!sameIdentity(current.messagesDirectory, opened.identity.messagesDirectory) ||
|
|
196
|
+
again.value.record.domain_id !== opened.record.domain_id ||
|
|
197
|
+
again.value.record.created_at !== opened.record.created_at) {
|
|
198
|
+
return serviceFailure('task_namespace_changed', `Task namespace changed after it was opened: ${opened.taskRoot}`, 'Inspect the task root; nothing was written or notified.');
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The one bounded, identity-checked reader for registered messages. A final
|
|
204
|
+
* symlink, non-regular file, oversized file, redirected path or id/content
|
|
205
|
+
* mismatch never becomes protocol content.
|
|
206
|
+
*/
|
|
207
|
+
export async function readRegisteredMessageSafe(opened, messageId) {
|
|
208
|
+
if (!isSafeMessageId(messageId)) {
|
|
209
|
+
return serviceFailure('message_id_invalid', `Unsafe message id: ${messageId}`, 'Use a registered message id from collab message list.');
|
|
210
|
+
}
|
|
211
|
+
let file;
|
|
212
|
+
try {
|
|
213
|
+
file = messageFile(opened.taskRoot, messageId);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return serviceFailure('message_id_invalid', errorMessage(error), null);
|
|
217
|
+
}
|
|
218
|
+
if (!isPathInside(opened.messagesDirectory, file)) {
|
|
219
|
+
return serviceFailure('message_id_invalid', `Message path escapes the messages directory: ${file}`, 'Use a safe single-segment message id.');
|
|
220
|
+
}
|
|
221
|
+
const observation = await observePath(file).catch((error) => serviceFailure('message_registration_invalid', `Cannot inspect registered message ${file}: ${errorMessage(error)}`, 'Inspect the message file; nothing was read into the protocol.'));
|
|
222
|
+
if ('ok' in observation)
|
|
223
|
+
return observation;
|
|
224
|
+
if (observation.kind === 'missing') {
|
|
225
|
+
return serviceFailure('message_not_registered', `No registered message ${messageId} in ${opened.taskRoot}`, 'Persist the message first; notification only forwards a registered pointer.');
|
|
226
|
+
}
|
|
227
|
+
if (observation.kind !== 'file' || observation.stats === null) {
|
|
228
|
+
return serviceFailure('message_registration_invalid', `Registered message is not a regular file: ${file}`, 'Restore a regular registered message; links and special files are not followed.');
|
|
229
|
+
}
|
|
230
|
+
if (observation.stats.size > MAX_ENVELOPE_BYTES) {
|
|
231
|
+
return serviceFailure('message_registration_invalid', `Registered message exceeds ${MAX_ENVELOPE_BYTES} bytes: ${file}`, 'Inspect the message file; oversized content is not accepted as protocol evidence.');
|
|
232
|
+
}
|
|
233
|
+
const real = await resolveRealpath(file);
|
|
234
|
+
if (real === null ||
|
|
235
|
+
real !== file ||
|
|
236
|
+
!isPathInside(opened.messagesDirectory, real)) {
|
|
237
|
+
return serviceFailure('message_registration_invalid', `Registered message is redirected: ${file}`, 'Restore a regular message file inside the task; nothing was read into the protocol.');
|
|
238
|
+
}
|
|
239
|
+
let payload;
|
|
240
|
+
try {
|
|
241
|
+
payload = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return serviceFailure('message_registration_invalid', `Registered message is not valid JSON: ${errorMessage(error)}`, 'Inspect the message file; nothing was read into the protocol.');
|
|
245
|
+
}
|
|
246
|
+
let message;
|
|
247
|
+
try {
|
|
248
|
+
message = validateEnvelope(payload);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
return serviceFailure('message_registration_invalid', `Registered message failed protocol validation: ${errorMessage(error)}`, 'Inspect the message file; nothing was read into the protocol.');
|
|
252
|
+
}
|
|
253
|
+
if (message.message_id !== messageId ||
|
|
254
|
+
message.task_id !== opened.record.task_id) {
|
|
255
|
+
return serviceFailure('message_registration_invalid', `Registered message identity does not match ${messageId}`, 'Inspect the message file; mismatched registration is not accepted as protocol content.');
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
value: { message, file, digest: digestOf(fs.readFileSync(file)) },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function summarize(message) {
|
|
263
|
+
return {
|
|
264
|
+
message_id: message.message_id,
|
|
265
|
+
type: message.type,
|
|
266
|
+
task_id: message.task_id,
|
|
267
|
+
from: message.from,
|
|
268
|
+
to: message.to,
|
|
269
|
+
reply_to: message.reply_to,
|
|
270
|
+
subtask_id: message.subtask_id,
|
|
271
|
+
attempt: message.attempt,
|
|
272
|
+
content_digest: message.content_digest,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function readEnvelopeFile(file) {
|
|
276
|
+
if (!path.isAbsolute(file)) {
|
|
277
|
+
return serviceFailure('message_file_required', `--message requires an absolute envelope file: ${file}`, 'Write the envelope JSON to an absolute path and pass it explicitly.');
|
|
278
|
+
}
|
|
279
|
+
const absolute = path.resolve(file);
|
|
280
|
+
const stat = (() => {
|
|
281
|
+
try {
|
|
282
|
+
return fs.lstatSync(absolute);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return serviceFailure('message_file_unreadable', `Cannot read envelope ${absolute}: ${errorMessage(error)}`, 'Check the path; no protocol state was touched.');
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
if ('ok' in stat)
|
|
289
|
+
return stat;
|
|
290
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
291
|
+
return serviceFailure('message_file_unsafe', `Envelope must be a regular file, not a symlink: ${absolute}`, 'Write the envelope to a regular file; links are not followed.');
|
|
292
|
+
}
|
|
293
|
+
if (stat.size > MAX_ENVELOPE_BYTES) {
|
|
294
|
+
return serviceFailure('message_file_too_large', `Envelope exceeds ${MAX_ENVELOPE_BYTES} bytes: ${absolute}`, 'Send one bounded envelope per invocation.');
|
|
295
|
+
}
|
|
296
|
+
let bytes;
|
|
297
|
+
try {
|
|
298
|
+
bytes = fs.readFileSync(absolute);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
return serviceFailure('message_file_unreadable', `Cannot read envelope ${absolute}: ${errorMessage(error)}`, 'Check the path; no protocol state was touched.');
|
|
302
|
+
}
|
|
303
|
+
let parsed;
|
|
304
|
+
try {
|
|
305
|
+
parsed = JSON.parse(bytes.toString('utf8'));
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return serviceFailure('message_envelope_invalid', `Envelope is not valid JSON: ${errorMessage(error)}`, 'Fix the JSON envelope; no protocol state was touched.');
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
return { ok: true, value: { message: validateEnvelope(parsed), bytes } };
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return serviceFailure('message_envelope_invalid', `Envelope failed protocol validation: ${errorMessage(error)}`, 'Correct the envelope against harness-collab/message/1; nothing was registered.');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Validates one envelope file, then (with apply) registers and applies it through
|
|
319
|
+
* the existing protocol. Planning writes nothing and spawns nothing.
|
|
320
|
+
*/
|
|
321
|
+
export async function sendMessageFromFile(taskRootInput, envelopeFile, options) {
|
|
322
|
+
const opened = await openMessageTask(taskRootInput);
|
|
323
|
+
if (!opened.ok)
|
|
324
|
+
return opened;
|
|
325
|
+
const { taskRoot, record } = opened.value;
|
|
326
|
+
const envelope = readEnvelopeFile(envelopeFile);
|
|
327
|
+
if (!envelope.ok)
|
|
328
|
+
return envelope;
|
|
329
|
+
const message = envelope.value.message;
|
|
330
|
+
if (!isSafeMessageId(message.message_id)) {
|
|
331
|
+
return serviceFailure('message_id_invalid', `Unsafe message id: ${message.message_id}`, 'Use a letter or digit followed by letters, digits, dot, dash or underscore (max 128).');
|
|
332
|
+
}
|
|
333
|
+
if (message.task_id !== record.task_id) {
|
|
334
|
+
return serviceFailure('message_task_mismatch', `Envelope task_id ${message.task_id} does not match record ${record.task_id}`, 'Send only messages for this task root; cross-task messages are refused.');
|
|
335
|
+
}
|
|
336
|
+
let target;
|
|
337
|
+
try {
|
|
338
|
+
target = messageFile(taskRoot, message.message_id);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return serviceFailure('message_id_invalid', errorMessage(error), 'Use a safe single-segment message id.');
|
|
342
|
+
}
|
|
343
|
+
if (!isPathInside(opened.value.messagesDirectory, target)) {
|
|
344
|
+
return serviceFailure('message_id_invalid', `Message path escapes the messages directory: ${target}`, 'Use a safe single-segment message id.');
|
|
345
|
+
}
|
|
346
|
+
const targetObservation = await observePath(target).catch((error) => serviceFailure('message_file_unreadable', `Cannot inspect ${target}: ${errorMessage(error)}`, 'Check permissions; nothing was registered.'));
|
|
347
|
+
if ('ok' in targetObservation)
|
|
348
|
+
return targetObservation;
|
|
349
|
+
if (targetObservation.kind !== 'missing' &&
|
|
350
|
+
targetObservation.kind !== 'file') {
|
|
351
|
+
return serviceFailure('message_registration_unsafe', `Message registration path is not a regular file: ${target}`, 'Inspect or remove that path; Pappra does not replace it.');
|
|
352
|
+
}
|
|
353
|
+
const summary = summarize(message);
|
|
354
|
+
if (!options.apply) {
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
value: {
|
|
358
|
+
status: 'planned',
|
|
359
|
+
message: summary,
|
|
360
|
+
registrationStatus: null,
|
|
361
|
+
stateRevision: null,
|
|
362
|
+
messagePath: null,
|
|
363
|
+
digest: null,
|
|
364
|
+
detail: 'Plan only: the envelope validates and nothing was persisted.',
|
|
365
|
+
delivery: 'not-attempted',
|
|
366
|
+
notified: false,
|
|
367
|
+
taskAccepted: false,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Revalidate the namespace immediately before the mutation; an await earlier in
|
|
372
|
+
// this call may have yielded to another process.
|
|
373
|
+
const changed = await assertMessageTaskCurrent(opened.value);
|
|
374
|
+
if (changed !== null)
|
|
375
|
+
return changed;
|
|
376
|
+
const outcome = submitMessage(taskRoot, message, options.clock === undefined ? {} : { clock: options.clock });
|
|
377
|
+
const registrationStatus = typeof outcome.registration?.status === 'string'
|
|
378
|
+
? outcome.registration.status
|
|
379
|
+
: null;
|
|
380
|
+
if (!outcome.ok) {
|
|
381
|
+
const status = outcome.reason === 'content_conflict' ? 'conflict' : 'rejected';
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
value: {
|
|
385
|
+
status,
|
|
386
|
+
message: summary,
|
|
387
|
+
registrationStatus,
|
|
388
|
+
stateRevision: null,
|
|
389
|
+
messagePath: targetObservation.kind === 'file' ? target : null,
|
|
390
|
+
digest: targetObservation.kind === 'file'
|
|
391
|
+
? digestOf(fs.readFileSync(target))
|
|
392
|
+
: null,
|
|
393
|
+
detail: `Protocol refused the message: ${outcome.reason}`,
|
|
394
|
+
delivery: 'persisted-not-notified',
|
|
395
|
+
notified: false,
|
|
396
|
+
taskAccepted: false,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const persisted = fs.existsSync(target);
|
|
401
|
+
const digest = persisted ? digestOf(fs.readFileSync(target)) : null;
|
|
402
|
+
const duplicate = outcome.duplicate === true;
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
value: {
|
|
406
|
+
status: duplicate ? 'duplicate' : 'accepted',
|
|
407
|
+
message: summary,
|
|
408
|
+
registrationStatus,
|
|
409
|
+
stateRevision: outcome.state_revision ?? null,
|
|
410
|
+
messagePath: persisted ? target : null,
|
|
411
|
+
digest,
|
|
412
|
+
detail: duplicate
|
|
413
|
+
? 'Protocol duplicate: the same registered message id and content already existed.'
|
|
414
|
+
: 'Protocol accepted the message; no native delivery or task acceptance is implied.',
|
|
415
|
+
delivery: 'persisted-not-notified',
|
|
416
|
+
notified: false,
|
|
417
|
+
taskAccepted: false,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
export function deliveryPathFor(taskRoot, messageId) {
|
|
422
|
+
return path.join(notificationsDir(taskRoot), `${messageId}.json`);
|
|
423
|
+
}
|
|
424
|
+
/** Reads task-private delivery metadata without creating it. */
|
|
425
|
+
export function readDeliveryRecord(taskRoot, messageId) {
|
|
426
|
+
if (!isSafeMessageId(messageId))
|
|
427
|
+
return null;
|
|
428
|
+
const file = deliveryPathFor(taskRoot, messageId);
|
|
429
|
+
try {
|
|
430
|
+
const stat = fs.lstatSync(file);
|
|
431
|
+
if (!stat.isFile() || stat.size > MAX_DELIVERY_BYTES)
|
|
432
|
+
return null;
|
|
433
|
+
const parsed = collabDeliverySchema.safeParse(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
434
|
+
return parsed.success ? parsed.data : null;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
export async function readMessageFromTaskRoot(taskRootInput, messageId) {
|
|
441
|
+
const opened = await openMessageTask(taskRootInput);
|
|
442
|
+
if (!opened.ok)
|
|
443
|
+
return opened;
|
|
444
|
+
const registered = await readRegisteredMessageSafe(opened.value, messageId);
|
|
445
|
+
if (!registered.ok)
|
|
446
|
+
return registered;
|
|
447
|
+
return {
|
|
448
|
+
ok: true,
|
|
449
|
+
value: {
|
|
450
|
+
message: registered.value.message,
|
|
451
|
+
entry: opened.value.record.messages.find((item) => item.message_id === messageId) ?? null,
|
|
452
|
+
messagePath: registered.value.file,
|
|
453
|
+
digest: registered.value.digest,
|
|
454
|
+
delivery: readDeliveryRecord(opened.value.taskRoot, messageId),
|
|
455
|
+
writes: false,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/** Lists the most recent applied messages from the authoritative record. */
|
|
460
|
+
export async function listMessagesForTaskRoot(taskRootInput, options = {}) {
|
|
461
|
+
const opened = await openMessageTask(taskRootInput);
|
|
462
|
+
if (!opened.ok)
|
|
463
|
+
return opened;
|
|
464
|
+
const requested = options.limit ?? DEFAULT_LIST_LIMIT;
|
|
465
|
+
if (!Number.isSafeInteger(requested) || requested < 1) {
|
|
466
|
+
return serviceFailure('limit_invalid', `--limit must be a positive integer: ${String(options.limit)}`, `Use a value between 1 and ${MAX_LIST_LIMIT}.`);
|
|
467
|
+
}
|
|
468
|
+
const limit = Math.min(requested, MAX_LIST_LIMIT);
|
|
469
|
+
const all = opened.value.record.messages;
|
|
470
|
+
const selected = all.slice(Math.max(0, all.length - limit)).reverse();
|
|
471
|
+
const messages = selected.map((entry) => ({
|
|
472
|
+
message_id: entry.message_id,
|
|
473
|
+
type: entry.type,
|
|
474
|
+
state: entry.state,
|
|
475
|
+
reply_to: entry.reply_to,
|
|
476
|
+
subtask_id: entry.subtask_id,
|
|
477
|
+
attempt: entry.attempt,
|
|
478
|
+
at: entry.at,
|
|
479
|
+
applied_at: entry.applied_at,
|
|
480
|
+
content_digest: entry.content_digest,
|
|
481
|
+
registered: isSafeMessageId(entry.message_id)
|
|
482
|
+
? fs.existsSync(messageFile(opened.value.taskRoot, entry.message_id))
|
|
483
|
+
: false,
|
|
484
|
+
delivery: readDeliveryRecord(opened.value.taskRoot, entry.message_id),
|
|
485
|
+
}));
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
value: {
|
|
489
|
+
total: all.length,
|
|
490
|
+
returned: messages.length,
|
|
491
|
+
truncated: all.length > messages.length,
|
|
492
|
+
messages,
|
|
493
|
+
writes: false,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
@@ -9,13 +9,13 @@ export declare function preflight(contract: Assignment, card: CapabilityCard, {
|
|
|
9
9
|
maxAgeMinutes?: number | undefined;
|
|
10
10
|
now?: number | undefined;
|
|
11
11
|
}): {
|
|
12
|
+
attempt?: never;
|
|
12
13
|
status: string;
|
|
13
14
|
errors: string[];
|
|
14
15
|
blockers: never[];
|
|
15
16
|
warnings: never[];
|
|
16
17
|
task_id?: never;
|
|
17
18
|
assignment_id?: never;
|
|
18
|
-
attempt?: never;
|
|
19
19
|
checked_at?: never;
|
|
20
20
|
limits?: never;
|
|
21
21
|
} | {
|
|
@@ -51,11 +51,21 @@ export declare function scanConflicts(records: (TaskRecord | UnreadableRecord)[]
|
|
|
51
51
|
held_access: string;
|
|
52
52
|
relation: string;
|
|
53
53
|
})[];
|
|
54
|
+
/** Check recorded protocol roles; this is not authentication of a same-user caller. */
|
|
55
|
+
export declare function checkClaimParticipant(record: TaskRecord, holder: Identity, subtaskId: string | null, attempt: number | null): {
|
|
56
|
+
reason?: never;
|
|
57
|
+
ok: true;
|
|
58
|
+
} | {
|
|
59
|
+
ok: false;
|
|
60
|
+
reason: string;
|
|
61
|
+
};
|
|
54
62
|
/**
|
|
55
63
|
* Claim resources for one task. The domain lock serialises the decision so two tasks
|
|
56
64
|
* cannot both observe an empty domain and then claim the same path.
|
|
65
|
+
* Trusted internal preparation may claim for an assignee before ASSIGN is recorded;
|
|
66
|
+
* public request adapters must enable requireAssignedHolder.
|
|
57
67
|
*/
|
|
58
|
-
export declare function claimResources(taskRoot: string, { resources, access, holder, waitMs, clock, includeSharedGit, note, subtaskId, attempt, }: {
|
|
68
|
+
export declare function claimResources(taskRoot: string, { resources, access, holder, waitMs, clock, includeSharedGit, note, subtaskId, attempt, requireAssignedHolder, }: {
|
|
59
69
|
resources: ResourceInput[];
|
|
60
70
|
access?: string;
|
|
61
71
|
holder: Identity;
|
|
@@ -65,8 +75,8 @@ export declare function claimResources(taskRoot: string, { resources, access, ho
|
|
|
65
75
|
note?: string | null;
|
|
66
76
|
subtaskId?: string | null;
|
|
67
77
|
attempt?: number | null;
|
|
78
|
+
requireAssignedHolder?: boolean;
|
|
68
79
|
}): {
|
|
69
|
-
[key: string]: unknown;
|
|
70
80
|
ok: false;
|
|
71
81
|
reason: string;
|
|
72
82
|
} | {
|
|
@@ -77,7 +87,7 @@ export declare function claimResources(taskRoot: string, { resources, access, ho
|
|
|
77
87
|
};
|
|
78
88
|
export declare function releaseClaim(taskRoot: string, { claimId, holder, reason, clock, }: {
|
|
79
89
|
claimId: string;
|
|
80
|
-
holder
|
|
90
|
+
holder: Identity;
|
|
81
91
|
reason?: string;
|
|
82
92
|
clock?: number;
|
|
83
93
|
}): {
|