@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
|
@@ -4,14 +4,19 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createDiagnostic } from '../../schemas/diagnostics.js';
|
|
6
6
|
import { diagnosticCodes } from '../../schemas/errors.js';
|
|
7
|
-
import { hostCapabilitiesSchema, hostInstallManifestSchema, hostIntegrationPlanSchema, hostIntegrationResultSchema, hostNameSchema, hostVerificationSchema, } from '../../schemas/host.js';
|
|
7
|
+
import { hostCapabilitiesSchema, hostInstallManifestSchema, hostIntegrationPlanSchema, hostIntegrationResultSchema, hostNameSchema, hostVerificationSchema, hostUninstallResultSchema, } from '../../schemas/host.js';
|
|
8
8
|
import { observePath, readFileBounded, sha256Digest } from '../../infra/fs.js';
|
|
9
|
+
import { ensurePrivateState } from '../../infra/private-state.js';
|
|
10
|
+
import { updateJsonProperty } from '../../infra/json-property.js';
|
|
11
|
+
import { captureProcessIdentity } from '../../infra/process-identity.js';
|
|
12
|
+
import { inspectPrivateLock, publishPrivateLock, reclaimDeadPrivateLock, } from '../../infra/private-lock.js';
|
|
9
13
|
import { isPathInside } from '../../schemas/paths.js';
|
|
10
|
-
import { ensureStatePathBoundary } from '../../install/ownership.js';
|
|
14
|
+
import { ensureStatePathBoundary, removeOwnedFile, } from '../../install/ownership.js';
|
|
11
15
|
import { getPackageInfo } from '../../package-info.js';
|
|
12
16
|
import { detectPassiveHosts } from '../detect.js';
|
|
17
|
+
import { observeHostVersion } from '../version.js';
|
|
13
18
|
import { installPathForBundleFile, loadHostBundle } from './bundle.js';
|
|
14
|
-
import { hostConfigRelativePath, renderHookConfigPatch } from './templates.js';
|
|
19
|
+
import { hostConfigRelativePath, isManagedHookCommand, MIN_HOOK_TIMEOUT_SECONDS, renderHookConfigPatch, } from './templates.js';
|
|
15
20
|
import { HOST_PLUGIN_HOOKS, HOST_SUPPORT, isSupportedVersion, nativeEventFor, surfaceFor, } from './support.js';
|
|
16
21
|
const LIFECYCLE_EVENTS = [
|
|
17
22
|
'startup',
|
|
@@ -199,22 +204,27 @@ function managedGroupHasUserEdits(group, commands) {
|
|
|
199
204
|
const hookValues = group.hooks;
|
|
200
205
|
if (!Array.isArray(hookValues))
|
|
201
206
|
return false;
|
|
202
|
-
const hasManaged = hookValues.some((hook) =>
|
|
207
|
+
const hasManaged = hookValues.some((hook) => isManagedHook(hook, commands));
|
|
203
208
|
if (!hasManaged)
|
|
204
209
|
return false;
|
|
205
210
|
const extraGroupKeys = Object.keys(group).filter((key) => key !== 'hooks');
|
|
206
211
|
if (extraGroupKeys.length > 0)
|
|
207
212
|
return true;
|
|
208
213
|
return hookValues.some((hook) => {
|
|
209
|
-
if (!
|
|
214
|
+
if (!isManagedHook(hook, commands))
|
|
210
215
|
return false;
|
|
211
216
|
const keys = Object.keys(hook);
|
|
217
|
+
const validTimeout = typeof hook.timeout === 'number' &&
|
|
218
|
+
Number.isSafeInteger(hook.timeout) &&
|
|
219
|
+
hook.timeout >= MIN_HOOK_TIMEOUT_SECONDS;
|
|
220
|
+
const legacyDefault = hook.timeout === 1 &&
|
|
221
|
+
typeof hook.command === 'string' &&
|
|
222
|
+
!commands.has(hook.command);
|
|
212
223
|
return (keys.some((key) => key !== 'type' && key !== 'command' && key !== 'timeout') ||
|
|
213
|
-
!
|
|
214
|
-
hook.timeout !== 1);
|
|
224
|
+
(!validTimeout && !legacyDefault));
|
|
215
225
|
});
|
|
216
226
|
}
|
|
217
|
-
function
|
|
227
|
+
function isManagedHook(value, commands) {
|
|
218
228
|
return (typeof value === 'object' &&
|
|
219
229
|
value !== null &&
|
|
220
230
|
!Array.isArray(value) &&
|
|
@@ -222,7 +232,7 @@ function isExactManagedHook(value, commands) {
|
|
|
222
232
|
value.type === 'command' &&
|
|
223
233
|
'command' in value &&
|
|
224
234
|
typeof value.command === 'string' &&
|
|
225
|
-
|
|
235
|
+
isManagedHookCommand(value.command, commands));
|
|
226
236
|
}
|
|
227
237
|
function managedConfig(host, existing, hookExpression, path) {
|
|
228
238
|
let base = {};
|
|
@@ -256,6 +266,7 @@ function managedConfig(host, existing, hookExpression, path) {
|
|
|
256
266
|
const groups = current === undefined ? [] : current;
|
|
257
267
|
const commands = managedHookCommands(entries);
|
|
258
268
|
const preserved = [];
|
|
269
|
+
let managedTimeout = MIN_HOOK_TIMEOUT_SECONDS;
|
|
259
270
|
for (const group of groups) {
|
|
260
271
|
if (typeof group !== 'object' ||
|
|
261
272
|
group === null ||
|
|
@@ -268,27 +279,108 @@ function managedConfig(host, existing, hookExpression, path) {
|
|
|
268
279
|
if (managedGroupHasUserEdits(group, commands)) {
|
|
269
280
|
return {
|
|
270
281
|
ok: false,
|
|
271
|
-
diagnostic: diagnostic(diagnosticCodes.host_install_conflict, `Managed host hook entry was edited by the user: ${path}`, path,
|
|
282
|
+
diagnostic: diagnostic(diagnosticCodes.host_install_conflict, `Managed host hook entry was edited by the user: ${path}`, path, `Review the matcher and handler fields, and use an integer timeout of at least ${MIN_HOOK_TIMEOUT_SECONDS} seconds. Pappra preserves invalid or unsupported edits.`),
|
|
272
283
|
};
|
|
273
284
|
}
|
|
274
|
-
|
|
285
|
+
if (!group.hooks.some((hook) => isManagedHook(hook, commands))) {
|
|
286
|
+
preserved.push(group);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
for (const hook of group.hooks) {
|
|
290
|
+
if (isManagedHook(hook, commands) &&
|
|
291
|
+
typeof hook === 'object' &&
|
|
292
|
+
hook !== null &&
|
|
293
|
+
'timeout' in hook &&
|
|
294
|
+
typeof hook.timeout === 'number') {
|
|
295
|
+
managedTimeout = Math.max(managedTimeout, hook.timeout);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const remaining = group.hooks.filter((hook) => !isManagedHook(hook, commands));
|
|
275
299
|
if (remaining.length > 0) {
|
|
276
300
|
preserved.push({ ...group, hooks: remaining });
|
|
277
301
|
}
|
|
278
302
|
}
|
|
279
|
-
nextHooks[event] = [
|
|
303
|
+
nextHooks[event] = [
|
|
304
|
+
...preserved,
|
|
305
|
+
...entries.map((entry) => ({
|
|
306
|
+
...entry,
|
|
307
|
+
hooks: entry.hooks.map((hook) => ({
|
|
308
|
+
...hook,
|
|
309
|
+
timeout: managedTimeout,
|
|
310
|
+
})),
|
|
311
|
+
})),
|
|
312
|
+
];
|
|
280
313
|
}
|
|
281
314
|
const next = { ...base, hooks: nextHooks };
|
|
282
315
|
return {
|
|
283
316
|
ok: true,
|
|
284
|
-
bytes:
|
|
317
|
+
bytes: updateJsonProperty(existing, 'hooks', next['hooks']),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function removeManagedConfig(host, existing, expression, file) {
|
|
321
|
+
const parsed = parseJsonObject(existing, file);
|
|
322
|
+
if (!parsed.ok)
|
|
323
|
+
return parsed;
|
|
324
|
+
const hooks = parsed.value['hooks'];
|
|
325
|
+
if (hooks === undefined)
|
|
326
|
+
return { ok: true, bytes: existing };
|
|
327
|
+
if (typeof hooks !== 'object' || hooks === null || Array.isArray(hooks))
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
diagnostic: diagnostic(diagnosticCodes.host_config_invalid, 'Host hooks must be an object.', file, 'Inspect the configuration; no hook was removed.'),
|
|
331
|
+
};
|
|
332
|
+
const next = { ...hooks };
|
|
333
|
+
for (const [event, entries] of Object.entries(renderHookConfigPatch(host, expression).hooks)) {
|
|
334
|
+
const groups = next[event];
|
|
335
|
+
if (groups === undefined)
|
|
336
|
+
continue;
|
|
337
|
+
if (!Array.isArray(groups))
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
diagnostic: diagnostic(diagnosticCodes.host_config_invalid, 'Host event hooks must be an array.', file, 'Inspect the configuration; no hook was removed.'),
|
|
341
|
+
};
|
|
342
|
+
const commands = managedHookCommands(entries);
|
|
343
|
+
const kept = [];
|
|
344
|
+
for (const value of groups) {
|
|
345
|
+
if (typeof value !== 'object' ||
|
|
346
|
+
value === null ||
|
|
347
|
+
Array.isArray(value) ||
|
|
348
|
+
!('hooks' in value) ||
|
|
349
|
+
!Array.isArray(value.hooks)) {
|
|
350
|
+
kept.push(value);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (managedGroupHasUserEdits(value, commands))
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
diagnostic: diagnostic(diagnosticCodes.host_install_conflict, 'An edited managed hook was preserved.', file, 'Review the edited hook explicitly before uninstalling its assets.'),
|
|
357
|
+
};
|
|
358
|
+
const remaining = value.hooks.filter((hook) => !isManagedHook(hook, commands));
|
|
359
|
+
if (remaining.length === value.hooks.length)
|
|
360
|
+
kept.push(value);
|
|
361
|
+
else if (remaining.length > 0)
|
|
362
|
+
kept.push({ ...value, hooks: remaining });
|
|
363
|
+
}
|
|
364
|
+
if (kept.length > 0)
|
|
365
|
+
next[event] = kept;
|
|
366
|
+
else
|
|
367
|
+
delete next[event];
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
bytes: JSON.stringify(next) === JSON.stringify(hooks)
|
|
372
|
+
? existing
|
|
373
|
+
: updateJsonProperty(existing, 'hooks', next),
|
|
285
374
|
};
|
|
286
375
|
}
|
|
287
376
|
function validateManifestIdentity(manifest, host, hostRoot, configPath, hookExpression, migrate) {
|
|
377
|
+
const portable = host === 'pi' ||
|
|
378
|
+
(manifest.runtimeEntry === hookExpression &&
|
|
379
|
+
hookExpression.startsWith('${CLAUDE_PROJECT_DIR}/'));
|
|
288
380
|
if (manifest.host !== host ||
|
|
289
|
-
resolve(manifest.hostRoot) !== hostRoot ||
|
|
381
|
+
(!portable && resolve(manifest.hostRoot) !== hostRoot) ||
|
|
290
382
|
manifest.configPath !== configPath ||
|
|
291
|
-
(!migrate && manifest.runtimeEntry !== hookExpression)) {
|
|
383
|
+
(!migrate && host !== 'pi' && manifest.runtimeEntry !== hookExpression)) {
|
|
292
384
|
return diagnostic(diagnosticCodes.host_install_conflict, `Host manifest identity does not match the selected target: ${hostRoot}`, join(hostRoot, BUNDLE_FILE), 'Inspect the manifest and target; Pappra does not adopt a foreign installation.');
|
|
293
385
|
}
|
|
294
386
|
const seen = new Set();
|
|
@@ -326,13 +418,20 @@ function configHasExpectedHooks(host, bytes, hookExpression, path) {
|
|
|
326
418
|
!Array.isArray(group.hooks)) {
|
|
327
419
|
continue;
|
|
328
420
|
}
|
|
421
|
+
if (managedGroupHasUserEdits(group, commands))
|
|
422
|
+
return false;
|
|
329
423
|
for (const hook of group.hooks) {
|
|
330
|
-
if (!
|
|
424
|
+
if (!isManagedHook(hook, commands))
|
|
331
425
|
continue;
|
|
332
426
|
if (typeof hook === 'object' &&
|
|
333
427
|
hook !== null &&
|
|
334
428
|
'command' in hook &&
|
|
335
429
|
typeof hook.command === 'string') {
|
|
430
|
+
// A stale runner or duplicate entry requires reinstalling, while
|
|
431
|
+
// unrelated user configuration has no bearing on managed wiring.
|
|
432
|
+
if (!commands.has(hook.command) || found.has(hook.command)) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
336
435
|
found.add(hook.command);
|
|
337
436
|
}
|
|
338
437
|
}
|
|
@@ -651,6 +750,11 @@ async function writeAtomic(path, bytes, options) {
|
|
|
651
750
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
652
751
|
const handle = await open(temporary, 'wx', 0o600);
|
|
653
752
|
try {
|
|
753
|
+
if (options.replace) {
|
|
754
|
+
const before = await observePath(path);
|
|
755
|
+
if (before.kind === 'file' && before.stats)
|
|
756
|
+
await handle.chmod(before.stats.mode & 0o777);
|
|
757
|
+
}
|
|
654
758
|
const stats = await handle.stat();
|
|
655
759
|
if (!stats.isFile())
|
|
656
760
|
throw new Error('temporary is not a regular file');
|
|
@@ -734,7 +838,77 @@ async function writeAtomic(path, bytes, options) {
|
|
|
734
838
|
};
|
|
735
839
|
}
|
|
736
840
|
}
|
|
737
|
-
|
|
841
|
+
/** CLI force is an explicit stop-all-host-writers assertion, not asset ownership. */
|
|
842
|
+
async function quarantineUnknownHostLock(workspaceRoot, hostRoot) {
|
|
843
|
+
const file = join(hostRoot, '.pappra-host.lock');
|
|
844
|
+
if (await assertSafeTarget(hostRoot, file))
|
|
845
|
+
return null;
|
|
846
|
+
const owner = await inspectPrivateLock(file);
|
|
847
|
+
if (owner.state !== 'unknown')
|
|
848
|
+
return null;
|
|
849
|
+
const observed = await observePath(file);
|
|
850
|
+
const bytes = await readFileBounded(file, 64 * 1024);
|
|
851
|
+
if (observed.kind !== 'file' || !observed.stats || !bytes.ok)
|
|
852
|
+
return null;
|
|
853
|
+
const privateState = await ensurePrivateState(workspaceRoot);
|
|
854
|
+
if (!privateState.ok)
|
|
855
|
+
return null;
|
|
856
|
+
const directory = join(workspaceRoot, '.pappra', 'local', 'host-lock-archive');
|
|
857
|
+
const id = randomUUID();
|
|
858
|
+
const archive = join(directory, `${id}.lock`);
|
|
859
|
+
if (!(await ensureStatePathBoundary(workspaceRoot, archive)).ok)
|
|
860
|
+
return null;
|
|
861
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
862
|
+
const saved = await writeAtomic(archive, bytes.value.bytes, {
|
|
863
|
+
replace: false,
|
|
864
|
+
beforeDigest: null,
|
|
865
|
+
root: directory,
|
|
866
|
+
});
|
|
867
|
+
if (!saved.ok)
|
|
868
|
+
return null;
|
|
869
|
+
const receiptFile = `${archive}.receipt.json`;
|
|
870
|
+
const receipt = Buffer.from(`${JSON.stringify({
|
|
871
|
+
schema: 'pappra/host-lock-quarantine/1',
|
|
872
|
+
source: file,
|
|
873
|
+
archive,
|
|
874
|
+
digest: bytes.value.digest,
|
|
875
|
+
identity: { dev: observed.stats.dev, ino: observed.stats.ino },
|
|
876
|
+
owner: 'unknown',
|
|
877
|
+
operator_assertion: { all_host_writers_stopped: true },
|
|
878
|
+
copy_fsynced_before_removal: true,
|
|
879
|
+
created_at: now(),
|
|
880
|
+
}, null, 2)}\n`);
|
|
881
|
+
if (!(await writeAtomic(receiptFile, receipt, {
|
|
882
|
+
replace: false,
|
|
883
|
+
beforeDigest: null,
|
|
884
|
+
root: directory,
|
|
885
|
+
})).ok)
|
|
886
|
+
return null;
|
|
887
|
+
const archiveDirectory = await open(directory, 'r');
|
|
888
|
+
try {
|
|
889
|
+
await archiveDirectory.sync();
|
|
890
|
+
}
|
|
891
|
+
finally {
|
|
892
|
+
await archiveDirectory.close();
|
|
893
|
+
}
|
|
894
|
+
const freshOwner = await inspectPrivateLock(file);
|
|
895
|
+
if (freshOwner.state !== 'unknown')
|
|
896
|
+
return null;
|
|
897
|
+
const removed = await removeOwnedFile(hostRoot, {
|
|
898
|
+
path: '.pappra-host.lock',
|
|
899
|
+
installId: id,
|
|
900
|
+
operation: 'create',
|
|
901
|
+
beforeDigest: null,
|
|
902
|
+
afterDigest: bytes.value.digest,
|
|
903
|
+
backupRef: null,
|
|
904
|
+
backupDigest: null,
|
|
905
|
+
identity: { dev: observed.stats.dev, ino: observed.stats.ino },
|
|
906
|
+
owner: 'pappra',
|
|
907
|
+
verifiedAt: now(),
|
|
908
|
+
});
|
|
909
|
+
return removed.ok ? archive : null;
|
|
910
|
+
}
|
|
911
|
+
async function acquireHostInstallLock(hostRoot, host) {
|
|
738
912
|
const path = join(hostRoot, '.pappra-host.lock');
|
|
739
913
|
const safe = await assertSafeTarget(hostRoot, path);
|
|
740
914
|
if (safe !== null)
|
|
@@ -749,21 +923,36 @@ async function acquireHostInstallLock(hostRoot) {
|
|
|
749
923
|
};
|
|
750
924
|
}
|
|
751
925
|
try {
|
|
752
|
-
const token = randomUUID();
|
|
753
926
|
const rootReal = await realpath(hostRoot);
|
|
754
|
-
const
|
|
927
|
+
const token = `${JSON.stringify({ schema: 'pappra/host-install-lock/1', token: randomUUID(), pid: process.pid, process_identity: captureProcessIdentity() ?? undefined })}\n`;
|
|
928
|
+
const revalidate = async () => {
|
|
929
|
+
if ((await realpath(hostRoot)) !== rootReal ||
|
|
930
|
+
(await assertSafeTarget(hostRoot, path)) !== null)
|
|
931
|
+
throw new Error('Host lock namespace changed.');
|
|
932
|
+
};
|
|
755
933
|
try {
|
|
756
|
-
|
|
757
|
-
await handle.writeFile(token);
|
|
758
|
-
await handle.sync();
|
|
759
|
-
return {
|
|
760
|
-
ok: true,
|
|
761
|
-
lock: { path, token, dev: stats.dev, ino: stats.ino, rootReal },
|
|
762
|
-
};
|
|
934
|
+
await publishPrivateLock(path, Buffer.from(token), revalidate);
|
|
763
935
|
}
|
|
764
|
-
|
|
765
|
-
|
|
936
|
+
catch (error) {
|
|
937
|
+
if (error.code !== 'EEXIST' ||
|
|
938
|
+
(await inspectPrivateLock(path)).state !== 'dead')
|
|
939
|
+
throw error;
|
|
940
|
+
await reclaimDeadPrivateLock(path, revalidate);
|
|
941
|
+
await publishPrivateLock(path, Buffer.from(token), revalidate);
|
|
766
942
|
}
|
|
943
|
+
const observed = await observePath(path);
|
|
944
|
+
if (observed.kind !== 'file' || !observed.stats)
|
|
945
|
+
throw new Error('Host lock identity unavailable.');
|
|
946
|
+
return {
|
|
947
|
+
ok: true,
|
|
948
|
+
lock: {
|
|
949
|
+
path,
|
|
950
|
+
token,
|
|
951
|
+
dev: observed.stats.dev,
|
|
952
|
+
ino: observed.stats.ino,
|
|
953
|
+
rootReal,
|
|
954
|
+
},
|
|
955
|
+
};
|
|
767
956
|
}
|
|
768
957
|
catch (error) {
|
|
769
958
|
const code = typeof error === 'object' &&
|
|
@@ -776,7 +965,7 @@ async function acquireHostInstallLock(hostRoot) {
|
|
|
776
965
|
ok: false,
|
|
777
966
|
diagnostic: diagnostic(code === 'EEXIST'
|
|
778
967
|
? diagnosticCodes.host_install_conflict
|
|
779
|
-
: diagnosticCodes.host_install_partial, `Cannot acquire host install lock ${path}: ${error instanceof Error ? error.message : String(error)}`, path,
|
|
968
|
+
: diagnosticCodes.host_install_partial, `Cannot acquire host install lock ${path}: ${error instanceof Error ? error.message : String(error)}`, path, `A live or unverified owner is preserved. Stop all writers for this host root, then use pappra host uninstall --host ${host} --host-root ${JSON.stringify(hostRoot)} --force --apply to quarantine an unknown legacy lock. Verified live owners are never overridden.`),
|
|
780
969
|
};
|
|
781
970
|
}
|
|
782
971
|
}
|
|
@@ -819,6 +1008,7 @@ export class HostIntegrationService {
|
|
|
819
1008
|
#hostVersion;
|
|
820
1009
|
#selectedHosts;
|
|
821
1010
|
#observedAt;
|
|
1011
|
+
#versionProbe;
|
|
822
1012
|
constructor(options) {
|
|
823
1013
|
this.#root = resolve(options.root);
|
|
824
1014
|
this.#homeDir = resolve(options.homeDir);
|
|
@@ -827,6 +1017,9 @@ export class HostIntegrationService {
|
|
|
827
1017
|
this.#hostVersion = options.hostVersion ?? null;
|
|
828
1018
|
this.#selectedHosts = options.selectedHosts ?? ['pi', 'codex', 'claude'];
|
|
829
1019
|
this.#observedAt = options.observedAt ?? now();
|
|
1020
|
+
this.#versionProbe =
|
|
1021
|
+
options.versionProbe ??
|
|
1022
|
+
((host) => observeHostVersion(host, this.#pathEnv));
|
|
830
1023
|
}
|
|
831
1024
|
detect() {
|
|
832
1025
|
const observations = detectPassiveHosts({
|
|
@@ -870,7 +1063,7 @@ export class HostIntegrationService {
|
|
|
870
1063
|
async plan(host, options = {}) {
|
|
871
1064
|
const hostRoot = resolve(options.hostRoot ?? join(this.#root, '.pappra', 'local', 'hosts', host));
|
|
872
1065
|
const hookSource = resolve(options.runtimeEntry ?? this.#runtimeEntry);
|
|
873
|
-
const version =
|
|
1066
|
+
const version = this.#versionProbe(host);
|
|
874
1067
|
if (!isSupportedVersion(host, version)) {
|
|
875
1068
|
throw new Error(`Host ${host} version ${version ?? '(unknown)'} is outside the pinned support matrix.`);
|
|
876
1069
|
}
|
|
@@ -910,7 +1103,7 @@ export class HostIntegrationService {
|
|
|
910
1103
|
try {
|
|
911
1104
|
const hostRoot = resolve(options.hostRoot ?? join(this.#root, '.pappra', 'local', 'hosts', host));
|
|
912
1105
|
const hookSource = resolve(options.runtimeEntry ?? this.#runtimeEntry);
|
|
913
|
-
const version =
|
|
1106
|
+
const version = this.#versionProbe(host);
|
|
914
1107
|
if (!isSupportedVersion(host, version)) {
|
|
915
1108
|
return hostIntegrationResultSchema.parse({
|
|
916
1109
|
schema: 'pappra/host-integration/1',
|
|
@@ -980,7 +1173,24 @@ export class HostIntegrationService {
|
|
|
980
1173
|
const applied = [];
|
|
981
1174
|
const residualPaths = [];
|
|
982
1175
|
const diagnostics = [];
|
|
983
|
-
|
|
1176
|
+
if (isPathInside(join(this.#root, '.pappra', 'local'), plan.hostRoot)) {
|
|
1177
|
+
const privateState = await ensurePrivateState(this.#root);
|
|
1178
|
+
if (!privateState.ok) {
|
|
1179
|
+
return hostIntegrationResultSchema.parse({
|
|
1180
|
+
schema: 'pappra/host-integration/1',
|
|
1181
|
+
ok: false,
|
|
1182
|
+
status: 'failed',
|
|
1183
|
+
plan,
|
|
1184
|
+
appliedFiles: [],
|
|
1185
|
+
residualPaths: [],
|
|
1186
|
+
diagnostics: [
|
|
1187
|
+
diagnostic(privateState.code, privateState.message, this.#root, privateState.nextAction ??
|
|
1188
|
+
'Protect private workspace state before installing the host.'),
|
|
1189
|
+
],
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const lockResult = await acquireHostInstallLock(plan.hostRoot, host);
|
|
984
1194
|
if (!lockResult.ok) {
|
|
985
1195
|
return hostIntegrationResultSchema.parse({
|
|
986
1196
|
schema: 'pappra/host-integration/1',
|
|
@@ -1131,7 +1341,7 @@ export class HostIntegrationService {
|
|
|
1131
1341
|
}
|
|
1132
1342
|
async verify(host, options = {}) {
|
|
1133
1343
|
const hostRoot = resolve(options.hostRoot ?? join(this.#root, '.pappra', 'local', 'hosts', host));
|
|
1134
|
-
const version =
|
|
1344
|
+
const version = this.#versionProbe(host);
|
|
1135
1345
|
const capabilities = this.describe(host);
|
|
1136
1346
|
const checks = [];
|
|
1137
1347
|
if (!isSupportedVersion(host, version)) {
|
|
@@ -1323,6 +1533,10 @@ export class HostIntegrationService {
|
|
|
1323
1533
|
}
|
|
1324
1534
|
let status = 'verified';
|
|
1325
1535
|
for (const file of manifest.files) {
|
|
1536
|
+
// Native settings are shared with the user. Their complete digest is a
|
|
1537
|
+
// write-plan pin, not an ownership claim over unrelated configuration.
|
|
1538
|
+
if (file.path === manifest.configPath)
|
|
1539
|
+
continue;
|
|
1326
1540
|
const path = join(hostRoot, ...file.path.split('/'));
|
|
1327
1541
|
const digest = await fileDigest(path);
|
|
1328
1542
|
if (digest !== file.digest) {
|
|
@@ -1345,13 +1559,12 @@ export class HostIntegrationService {
|
|
|
1345
1559
|
const configPath = join(hostRoot, ...manifest.configPath.split('/'));
|
|
1346
1560
|
const config = await readFileBounded(configPath, DEFAULT_MAX_CONFIG_BYTES);
|
|
1347
1561
|
if (!config.ok ||
|
|
1348
|
-
manifestFiles.get(manifest.configPath) !== config.value.digest ||
|
|
1349
1562
|
!configHasExpectedHooks(host === 'codex' ? 'codex' : 'claude', config.value.bytes, target.expression, configPath)) {
|
|
1350
1563
|
status = 'conflict';
|
|
1351
1564
|
checks.push({
|
|
1352
1565
|
id: 'host.config',
|
|
1353
1566
|
status: 'error',
|
|
1354
|
-
detail: 'Host config is missing
|
|
1567
|
+
detail: 'Host config is missing or its managed Pappra hook entries changed, are duplicated, or are incomplete.',
|
|
1355
1568
|
});
|
|
1356
1569
|
}
|
|
1357
1570
|
else {
|
|
@@ -1380,6 +1593,243 @@ export class HostIntegrationService {
|
|
|
1380
1593
|
residualPaths: [],
|
|
1381
1594
|
});
|
|
1382
1595
|
}
|
|
1596
|
+
async uninstall(host, options) {
|
|
1597
|
+
const hostRoot = resolve(options.hostRoot ?? join(this.#root, '.pappra', 'local', 'hosts', host));
|
|
1598
|
+
const plannedFiles = [];
|
|
1599
|
+
const removedFiles = [];
|
|
1600
|
+
const retainedPaths = [];
|
|
1601
|
+
const quarantinedLocks = [];
|
|
1602
|
+
const diagnostics = [];
|
|
1603
|
+
const result = (status, ok) => {
|
|
1604
|
+
if (!ok && diagnostics.length === 0)
|
|
1605
|
+
diagnostics.push(diagnostic(diagnosticCodes.host_install_conflict, 'Host uninstall preserved edited, unsafe or unreadable content.', retainedPaths[0] ?? hostRoot, 'Inspect the reported preserved paths; no unknown user content is removed automatically.'));
|
|
1606
|
+
return hostUninstallResultSchema.parse({
|
|
1607
|
+
schema: 'pappra/host-uninstall/1',
|
|
1608
|
+
host,
|
|
1609
|
+
hostRoot,
|
|
1610
|
+
ok,
|
|
1611
|
+
status,
|
|
1612
|
+
plannedFiles,
|
|
1613
|
+
removedFiles,
|
|
1614
|
+
retainedPaths: [...new Set(retainedPaths)],
|
|
1615
|
+
quarantinedLocks,
|
|
1616
|
+
diagnostics: diagnostics.slice(0, 32),
|
|
1617
|
+
});
|
|
1618
|
+
};
|
|
1619
|
+
const manifestPath = join(hostRoot, BUNDLE_FILE);
|
|
1620
|
+
const boundary = await assertSafeTarget(hostRoot, manifestPath);
|
|
1621
|
+
if (boundary) {
|
|
1622
|
+
diagnostics.push(boundary);
|
|
1623
|
+
return result('conflict', false);
|
|
1624
|
+
}
|
|
1625
|
+
const lockPath = join(hostRoot, '.pappra-host.lock');
|
|
1626
|
+
const lockOwner = await inspectPrivateLock(lockPath);
|
|
1627
|
+
const lockFile = await observePath(lockPath);
|
|
1628
|
+
if (lockOwner.state === 'alive' ||
|
|
1629
|
+
(lockFile.kind !== 'missing' &&
|
|
1630
|
+
lockOwner.state === 'unknown' &&
|
|
1631
|
+
!options.force)) {
|
|
1632
|
+
retainedPaths.push(lockPath);
|
|
1633
|
+
diagnostics.push(diagnostic(diagnosticCodes.host_install_conflict, lockOwner.state === 'alive'
|
|
1634
|
+
? 'A verified live host writer holds this lock; --force cannot override it.'
|
|
1635
|
+
: 'The host lock owner is unknown; it was preserved.', lockPath, `Stop all host writers, then use pappra host uninstall --host ${host} --host-root ${JSON.stringify(hostRoot)} --force --apply to archive an unknown lock. --force asserts all host writers are stopped and never permits deleting edited assets.`));
|
|
1636
|
+
return result('conflict', false);
|
|
1637
|
+
}
|
|
1638
|
+
if (lockFile.kind !== 'missing' &&
|
|
1639
|
+
lockOwner.state === 'unknown' &&
|
|
1640
|
+
options.force) {
|
|
1641
|
+
plannedFiles.push('.pappra-host.lock');
|
|
1642
|
+
if (options.apply) {
|
|
1643
|
+
const archived = await quarantineUnknownHostLock(this.#root, hostRoot).catch(() => null);
|
|
1644
|
+
if (!archived) {
|
|
1645
|
+
retainedPaths.push(lockPath);
|
|
1646
|
+
return result('conflict', false);
|
|
1647
|
+
}
|
|
1648
|
+
quarantinedLocks.push(archived);
|
|
1649
|
+
removedFiles.push('.pappra-host.lock');
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const manifestRead = await readFileBounded(manifestPath, 256 * 1024);
|
|
1653
|
+
if (!manifestRead.ok) {
|
|
1654
|
+
if (manifestRead.code === diagnosticCodes.fileNotFound)
|
|
1655
|
+
return result(removedFiles.length > 0
|
|
1656
|
+
? 'removed'
|
|
1657
|
+
: plannedFiles.length > 0
|
|
1658
|
+
? 'planned'
|
|
1659
|
+
: 'unchanged', true);
|
|
1660
|
+
retainedPaths.push(manifestPath);
|
|
1661
|
+
return result('conflict', false);
|
|
1662
|
+
}
|
|
1663
|
+
const manifest = await this.readManifest(hostRoot);
|
|
1664
|
+
const expression = hookTarget(host, this.#root, hostRoot, this.#runtimeEntry).expression;
|
|
1665
|
+
if (!manifest) {
|
|
1666
|
+
retainedPaths.push(manifestPath);
|
|
1667
|
+
return result('conflict', false);
|
|
1668
|
+
}
|
|
1669
|
+
const identity = validateManifestIdentity(manifest, host, hostRoot, hostConfigRelativePath(host), expression, false);
|
|
1670
|
+
if (identity) {
|
|
1671
|
+
diagnostics.push(identity);
|
|
1672
|
+
return result('conflict', false);
|
|
1673
|
+
}
|
|
1674
|
+
let payload;
|
|
1675
|
+
try {
|
|
1676
|
+
payload = await loadHostBundle(host, getPackageInfo().version, {
|
|
1677
|
+
hookExpression: expression,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
catch {
|
|
1681
|
+
retainedPaths.push(manifestPath);
|
|
1682
|
+
return result('failed', false);
|
|
1683
|
+
}
|
|
1684
|
+
const allowed = new Set([...payload.contents.keys()].map((file) => installPathForBundleFile(host, file)));
|
|
1685
|
+
const configRelative = hostConfigRelativePath(host);
|
|
1686
|
+
let config = null;
|
|
1687
|
+
if (host !== 'pi' && configRelative) {
|
|
1688
|
+
const file = join(hostRoot, configRelative);
|
|
1689
|
+
const unsafe = await assertSafeTarget(hostRoot, file);
|
|
1690
|
+
if (unsafe) {
|
|
1691
|
+
diagnostics.push(unsafe);
|
|
1692
|
+
retainedPaths.push(file);
|
|
1693
|
+
return result('conflict', false);
|
|
1694
|
+
}
|
|
1695
|
+
const read = await readFileBounded(file, DEFAULT_MAX_CONFIG_BYTES);
|
|
1696
|
+
if (read.ok) {
|
|
1697
|
+
const patch = removeManagedConfig(host, read.value.bytes, expression, file);
|
|
1698
|
+
if (!patch.ok) {
|
|
1699
|
+
diagnostics.push(patch.diagnostic);
|
|
1700
|
+
retainedPaths.push(file);
|
|
1701
|
+
return result('conflict', false);
|
|
1702
|
+
}
|
|
1703
|
+
if (!patch.bytes.equals(read.value.bytes)) {
|
|
1704
|
+
plannedFiles.push(configRelative);
|
|
1705
|
+
config = {
|
|
1706
|
+
path: file,
|
|
1707
|
+
before: read.value.digest,
|
|
1708
|
+
bytes: patch.bytes,
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
else if (read.code !== diagnosticCodes.fileNotFound) {
|
|
1713
|
+
retainedPaths.push(file);
|
|
1714
|
+
return result('conflict', false);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const candidates = [];
|
|
1718
|
+
for (const entry of manifest.files) {
|
|
1719
|
+
if (entry.path === configRelative)
|
|
1720
|
+
continue;
|
|
1721
|
+
const file = join(hostRoot, ...entry.path.split('/'));
|
|
1722
|
+
if (!allowed.has(entry.path)) {
|
|
1723
|
+
retainedPaths.push(file);
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
const unsafe = await assertSafeTarget(hostRoot, file);
|
|
1727
|
+
if (unsafe) {
|
|
1728
|
+
diagnostics.push(unsafe);
|
|
1729
|
+
retainedPaths.push(file);
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const read = await readFileBounded(file, 8 * 1024 * 1024);
|
|
1733
|
+
if (!read.ok && read.code === diagnosticCodes.fileNotFound)
|
|
1734
|
+
continue;
|
|
1735
|
+
if (!read.ok || read.value.digest !== entry.digest) {
|
|
1736
|
+
retainedPaths.push(file);
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
plannedFiles.push(entry.path);
|
|
1740
|
+
candidates.push({ path: entry.path, digest: entry.digest });
|
|
1741
|
+
}
|
|
1742
|
+
if (host === 'pi' &&
|
|
1743
|
+
retainedPaths.some((file) => isPathInside(join(hostRoot, 'extensions'), file))) {
|
|
1744
|
+
// An edited entrypoint may still import its runtime companion.
|
|
1745
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
1746
|
+
const entry = candidates[index];
|
|
1747
|
+
if (!entry.path.startsWith('extensions/'))
|
|
1748
|
+
continue;
|
|
1749
|
+
retainedPaths.push(join(hostRoot, ...entry.path.split('/')));
|
|
1750
|
+
plannedFiles.splice(plannedFiles.indexOf(entry.path), 1);
|
|
1751
|
+
candidates.splice(index, 1);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (retainedPaths.length === 0)
|
|
1755
|
+
plannedFiles.push(BUNDLE_FILE);
|
|
1756
|
+
if (!options.apply)
|
|
1757
|
+
return result(retainedPaths.length ? 'partial' : 'planned', retainedPaths.length === 0);
|
|
1758
|
+
const lock = await acquireHostInstallLock(hostRoot, host);
|
|
1759
|
+
if (!lock.ok) {
|
|
1760
|
+
diagnostics.push(lock.diagnostic);
|
|
1761
|
+
return result('conflict', false);
|
|
1762
|
+
}
|
|
1763
|
+
let status = 'removed';
|
|
1764
|
+
try {
|
|
1765
|
+
const freshManifest = await readFileBounded(manifestPath, 256 * 1024);
|
|
1766
|
+
if (!freshManifest.ok ||
|
|
1767
|
+
freshManifest.value.digest !== manifestRead.value.digest) {
|
|
1768
|
+
retainedPaths.push(manifestPath);
|
|
1769
|
+
status = 'conflict';
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
if (config) {
|
|
1773
|
+
const written = await writeAtomic(config.path, config.bytes, {
|
|
1774
|
+
replace: true,
|
|
1775
|
+
beforeDigest: config.before,
|
|
1776
|
+
root: hostRoot,
|
|
1777
|
+
});
|
|
1778
|
+
if (!written.ok) {
|
|
1779
|
+
diagnostics.push(written.diagnostic);
|
|
1780
|
+
retainedPaths.push(config.path);
|
|
1781
|
+
status = 'conflict';
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const remove = async (entry) => {
|
|
1785
|
+
const file = join(hostRoot, ...entry.path.split('/'));
|
|
1786
|
+
const observed = await observePath(file);
|
|
1787
|
+
if (observed.kind === 'missing')
|
|
1788
|
+
return;
|
|
1789
|
+
if (observed.kind !== 'file' || !observed.stats) {
|
|
1790
|
+
retainedPaths.push(file);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const removed = await removeOwnedFile(hostRoot, {
|
|
1794
|
+
path: entry.path,
|
|
1795
|
+
installId: randomUUID(),
|
|
1796
|
+
operation: 'create',
|
|
1797
|
+
beforeDigest: null,
|
|
1798
|
+
afterDigest: entry.digest,
|
|
1799
|
+
backupRef: null,
|
|
1800
|
+
backupDigest: null,
|
|
1801
|
+
identity: { dev: observed.stats.dev, ino: observed.stats.ino },
|
|
1802
|
+
owner: 'pappra',
|
|
1803
|
+
verifiedAt: now(),
|
|
1804
|
+
});
|
|
1805
|
+
if (removed.ok)
|
|
1806
|
+
removedFiles.push(entry.path);
|
|
1807
|
+
else
|
|
1808
|
+
retainedPaths.push(file);
|
|
1809
|
+
};
|
|
1810
|
+
if (status !== 'conflict') {
|
|
1811
|
+
for (const entry of candidates)
|
|
1812
|
+
await remove(entry);
|
|
1813
|
+
if (retainedPaths.length === 0)
|
|
1814
|
+
await remove({
|
|
1815
|
+
path: BUNDLE_FILE,
|
|
1816
|
+
digest: manifestRead.value.digest,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
catch {
|
|
1822
|
+
status = 'failed';
|
|
1823
|
+
retainedPaths.push(manifestPath);
|
|
1824
|
+
}
|
|
1825
|
+
finally {
|
|
1826
|
+
if (!(await releaseHostInstallLock(lock.lock)))
|
|
1827
|
+
retainedPaths.push(lock.lock.path);
|
|
1828
|
+
}
|
|
1829
|
+
if (status === 'removed' && retainedPaths.length > 0)
|
|
1830
|
+
status = 'partial';
|
|
1831
|
+
return result(status, status === 'removed');
|
|
1832
|
+
}
|
|
1383
1833
|
async readManifest(hostRoot) {
|
|
1384
1834
|
const path = join(hostRoot, BUNDLE_FILE);
|
|
1385
1835
|
const safe = await assertSafeTarget(hostRoot, path);
|