agentera 3.0.0-dev.39 → 3.0.0-dev.41
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 +1 -1
- package/bundle/CHANGELOG.md +55 -8
- package/bundle/README.md +3 -3
- package/bundle/references/artifacts/glossary-entry-contract.yaml +18 -5
- package/bundle/references/artifacts/state-storage-authority.yaml +865 -171
- package/bundle/references/cli/agent-ready-state-contract.yaml +12 -12
- package/bundle/references/cli/audience-namespace-cli-migration.yaml +5 -5
- package/bundle/references/cli/prime-consumer-compatibility.yaml +58 -18
- package/bundle/references/cli/vocabulary-index.yaml +1 -1
- package/bundle/references/cli/vocabulary.md +4 -3
- package/bundle/skills/agentera/SKILL.md +23 -4
- package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +3 -3
- package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +3 -1
- package/bundle/skills/agentera/capabilities/discuss/schemas/artifacts.yaml +2 -1
- package/bundle/skills/agentera/capabilities/discuss/schemas/validation.yaml +3 -1
- package/bundle/skills/agentera/capabilities/plan/schemas/artifacts.yaml +2 -2
- package/bundle/skills/agentera/capabilities/plan/schemas/validation.yaml +3 -1
- package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +3 -1
- package/bundle/skills/agentera/capabilities/status/schemas/artifacts.yaml +2 -1
- package/bundle/skills/agentera/capability_schema_contract.yaml +1 -1
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +89 -30
- package/bundle/skills/agentera/schemas/artifacts/todo.yaml +103 -20
- package/dist/analytics/extractCorpus/cli.js.map +1 -1
- package/dist/analytics/extractCorpus/copilotSessions.js.map +1 -1
- package/dist/analytics/extractCorpus/core.js.map +1 -1
- package/dist/analytics/extractCorpus/corpus.js.map +1 -1
- package/dist/analytics/extractCorpus/coverageAudit.js.map +1 -1
- package/dist/analytics/extractCorpus/cursorSessions.js.map +1 -1
- package/dist/analytics/extractCorpus/evidenceTiers.js.map +1 -1
- package/dist/analytics/extractCorpus/extractCorpusParity.js.map +1 -1
- package/dist/analytics/extractCorpus/filesystemSources.js.map +1 -1
- package/dist/analytics/extractCorpus/index.js.map +1 -1
- package/dist/analytics/extractCorpus/jsonlSessions.js.map +1 -1
- package/dist/analytics/extractCorpus/sqliteCaps.js.map +1 -1
- package/dist/analytics/extractCorpus/sqliteSessions.js.map +1 -1
- package/dist/analytics/extractCorpus/tierReader.js.map +1 -1
- package/dist/analytics/extractCorpus.js.map +1 -1
- package/dist/analytics/glossaryAdviceResolution.js.map +1 -1
- package/dist/analytics/glossaryInputAcquisition.js.map +1 -1
- package/dist/analytics/personalGlossaryAdmission.js.map +1 -1
- package/dist/analytics/personalGlossaryProfile.js +2 -1
- package/dist/analytics/personalGlossaryProfile.js.map +1 -1
- package/dist/analytics/profileSignals.js.map +1 -1
- package/dist/analytics/usageStats.js.map +1 -1
- package/dist/audit/terminologyDrift.js.map +1 -1
- package/dist/bin/agentera.js.map +1 -1
- package/dist/capabilities/audit/instructions.js.map +1 -1
- package/dist/capabilities/build/instructions.js +2 -2
- package/dist/capabilities/build/instructions.js.map +1 -1
- package/dist/capabilities/design/instructions.js.map +1 -1
- package/dist/capabilities/discuss/instructions.js +2 -1
- package/dist/capabilities/discuss/instructions.js.map +1 -1
- package/dist/capabilities/document/instructions.js.map +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/index.js.map +1 -1
- package/dist/capabilities/optimize/instructions.js +1 -0
- package/dist/capabilities/optimize/instructions.js.map +1 -1
- package/dist/capabilities/orchestrate/instructions.js.map +1 -1
- package/dist/capabilities/plan/instructions.js +3 -2
- package/dist/capabilities/plan/instructions.js.map +1 -1
- package/dist/capabilities/profile/instructions.js.map +1 -1
- package/dist/capabilities/research/instructions.js.map +1 -1
- package/dist/capabilities/status/instructions.js +8 -1
- package/dist/capabilities/status/instructions.js.map +1 -1
- package/dist/capabilities/status/startupInstructions.js.map +1 -1
- package/dist/capabilities/vision/instructions.js.map +1 -1
- package/dist/cli/appContext.js.map +1 -1
- package/dist/cli/argvalidate.js.map +1 -1
- package/dist/cli/capabilityContext/benchmark.js +2 -1
- package/dist/cli/capabilityContext/benchmark.js.map +1 -1
- package/dist/cli/capabilityContext/bespoke.js.map +1 -1
- package/dist/cli/capabilityContext/build.js +6 -6
- package/dist/cli/capabilityContext/build.js.map +1 -1
- package/dist/cli/capabilityContext/closeout.js +11 -7
- package/dist/cli/capabilityContext/closeout.js.map +1 -1
- package/dist/cli/capabilityContext/contract.js +4 -4
- package/dist/cli/capabilityContext/contract.js.map +1 -1
- package/dist/cli/capabilityContext/evidence.js +4 -4
- package/dist/cli/capabilityContext/evidence.js.map +1 -1
- package/dist/cli/capabilityContext/index.js.map +1 -1
- package/dist/cli/capabilityContext/orchestration.js +12 -7
- package/dist/cli/capabilityContext/orchestration.js.map +1 -1
- package/dist/cli/capabilityContext/planState.js.map +1 -1
- package/dist/cli/capabilityContext/progress.js.map +1 -1
- package/dist/cli/capabilityContext/projectVerification.js.map +1 -1
- package/dist/cli/capabilityContext/shared.js +2 -2
- package/dist/cli/capabilityContext/shared.js.map +1 -1
- package/dist/cli/capabilityContext/slim.js.map +1 -1
- package/dist/cli/capabilityContext/startup.js +6 -2
- package/dist/cli/capabilityContext/startup.js.map +1 -1
- package/dist/cli/capabilityContext/types.js +19 -14
- package/dist/cli/capabilityContext/types.js.map +1 -1
- package/dist/cli/capabilityContext.js.map +1 -1
- package/dist/cli/commands/appHome.js.map +1 -1
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/compact.js.map +1 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/durability.js.map +1 -1
- package/dist/cli/commands/entityMigrate.js.map +1 -1
- package/dist/cli/commands/glossaryAdvice.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/personalGlossary.js.map +1 -1
- package/dist/cli/commands/prime/briefOrientation.js +260 -44
- package/dist/cli/commands/prime/briefOrientation.js.map +1 -1
- package/dist/cli/commands/prime/buildExecutionRequest.js.map +1 -1
- package/dist/cli/commands/prime/bundleStatus.js.map +1 -1
- package/dist/cli/commands/prime/collectEntityOrientation.js +22 -13
- package/dist/cli/commands/prime/collectEntityOrientation.js.map +1 -1
- package/dist/cli/commands/prime/collectOrientationState.js +20 -28
- package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
- package/dist/cli/commands/prime/orientationOutput.js +4 -5
- package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
- package/dist/cli/commands/prime/types.js.map +1 -1
- package/dist/cli/commands/prime/v1Migration.js.map +1 -1
- package/dist/cli/commands/prime.js +1 -1
- package/dist/cli/commands/prime.js.map +1 -1
- package/dist/cli/commands/profileGrounding.js +29 -50
- package/dist/cli/commands/profileGrounding.js.map +1 -1
- package/dist/cli/commands/query.js.map +1 -1
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/route.js.map +1 -1
- package/dist/cli/commands/schema.js +4 -43
- package/dist/cli/commands/schema.js.map +1 -1
- package/dist/cli/commands/state/decisions.js +1 -1
- package/dist/cli/commands/state/decisions.js.map +1 -1
- package/dist/cli/commands/state/docs.js.map +1 -1
- package/dist/cli/commands/state/experimentRecords.js +28 -15
- package/dist/cli/commands/state/experimentRecords.js.map +1 -1
- package/dist/cli/commands/state/experiments.js.map +1 -1
- package/dist/cli/commands/state/get.js +6 -3
- package/dist/cli/commands/state/get.js.map +1 -1
- package/dist/cli/commands/state/health.js.map +1 -1
- package/dist/cli/commands/state/index.js +3 -46
- package/dist/cli/commands/state/index.js.map +1 -1
- package/dist/cli/commands/state/list.js +41 -22
- package/dist/cli/commands/state/list.js.map +1 -1
- package/dist/cli/commands/state/objective.js.map +1 -1
- package/dist/cli/commands/state/plan.js.map +1 -1
- package/dist/cli/commands/state/planTasks.js +26 -13
- package/dist/cli/commands/state/planTasks.js.map +1 -1
- package/dist/cli/commands/state/plans.js +31 -19
- package/dist/cli/commands/state/plans.js.map +1 -1
- package/dist/cli/commands/state/progress.js.map +1 -1
- package/dist/cli/commands/state/shared.js.map +1 -1
- package/dist/cli/commands/state/todo.js +2 -1
- package/dist/cli/commands/state/todo.js.map +1 -1
- package/dist/cli/commands/state/write.js +52 -48
- package/dist/cli/commands/state/write.js.map +1 -1
- package/dist/cli/commands/upgrade.js.map +1 -1
- package/dist/cli/commands/upgradeVerify.js.map +1 -1
- package/dist/cli/commands/validate.js +44 -4
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/commands/verify.js.map +1 -1
- package/dist/cli/contracts/bundleStatus.js.map +1 -1
- package/dist/cli/contracts/orientationState.js.map +1 -1
- package/dist/cli/dispatch/argvParser.js.map +1 -1
- package/dist/cli/dispatch/check.js.map +1 -1
- package/dist/cli/dispatch/commands.js.map +1 -1
- package/dist/cli/dispatch/index.js +12 -4
- package/dist/cli/dispatch/index.js.map +1 -1
- package/dist/cli/dispatch/lifecycle.js.map +1 -1
- package/dist/cli/dispatch/prime.js.map +1 -1
- package/dist/cli/dispatch/shared.js.map +1 -1
- package/dist/cli/dispatch/state.js +53 -86
- package/dist/cli/dispatch/state.js.map +1 -1
- package/dist/cli/dispatch.js.map +1 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/help.js +134 -48
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/migrationRequired.js.map +1 -1
- package/dist/cli/orientation/attention.js +8 -5
- package/dist/cli/orientation/attention.js.map +1 -1
- package/dist/cli/orientation/corpusCoverage.js.map +1 -1
- package/dist/cli/orientation.js +61 -38
- package/dist/cli/orientation.js.map +1 -1
- package/dist/cli/planArtifacts.js.map +1 -1
- package/dist/cli/planEvidence.js.map +1 -1
- package/dist/cli/planLifecycleState.js +3 -1
- package/dist/cli/planLifecycleState.js.map +1 -1
- package/dist/cli/planTaskIndex.js.map +1 -1
- package/dist/cli/prime-blob.js +2 -1
- package/dist/cli/prime-blob.js.map +1 -1
- package/dist/cli/profileAcquisition.js +168 -0
- package/dist/cli/profileAcquisition.js.map +1 -0
- package/dist/cli/startupCompletenessContract.js +5 -2
- package/dist/cli/startupCompletenessContract.js.map +1 -1
- package/dist/cli/stateQuery.js.map +1 -1
- package/dist/cli/structured.js.map +1 -1
- package/dist/cli/todoMarkdown.js +25 -11
- package/dist/cli/todoMarkdown.js.map +1 -1
- package/dist/cli/todoReadinessSelection.js +52 -17
- package/dist/cli/todoReadinessSelection.js.map +1 -1
- package/dist/cli/todoSeverity.js.map +1 -1
- package/dist/core/atomicWriter.js.map +1 -1
- package/dist/core/difflib.js.map +1 -1
- package/dist/core/envPaths.js.map +1 -1
- package/dist/core/jsonValue.js.map +1 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/pyjson.js.map +1 -1
- package/dist/core/shell.js +5 -0
- package/dist/core/shell.js.map +1 -0
- package/dist/core/skillIdentity.js.map +1 -1
- package/dist/core/sourceRoot.js.map +1 -1
- package/dist/core/text.js.map +1 -1
- package/dist/core/toml.js.map +1 -1
- package/dist/core/yaml.js.map +1 -1
- package/dist/eval/evalSkills.js.map +1 -1
- package/dist/eval/hybridRouteEvaluation.js.map +1 -1
- package/dist/eval/semanticEval.js.map +1 -1
- package/dist/eval/semanticFixtures.js.map +1 -1
- package/dist/hooks/common.js.map +1 -1
- package/dist/hooks/compaction/apply.js.map +1 -1
- package/dist/hooks/compaction/dryRun.js.map +1 -1
- package/dist/hooks/compaction/index.js.map +1 -1
- package/dist/hooks/compaction/parse.js.map +1 -1
- package/dist/hooks/compaction/retention.js.map +1 -1
- package/dist/hooks/compaction/status.js.map +1 -1
- package/dist/hooks/compaction/types.js.map +1 -1
- package/dist/hooks/cursorPreToolUse.js.map +1 -1
- package/dist/hooks/cursorSessionStart.js.map +1 -1
- package/dist/hooks/projectHookInput.js.map +1 -1
- package/dist/hooks/sessionStart.js.map +1 -1
- package/dist/hooks/sessionStop.js.map +1 -1
- package/dist/hooks/validateArtifact/agentFacing.js.map +1 -1
- package/dist/hooks/validateArtifact/index.js.map +1 -1
- package/dist/hooks/validateArtifact/markdown.js.map +1 -1
- package/dist/hooks/validateArtifact/runtime.js.map +1 -1
- package/dist/hooks/validateArtifact/schema.js +6 -0
- package/dist/hooks/validateArtifact/schema.js.map +1 -1
- package/dist/hooks/validateArtifact/traversal.js.map +1 -1
- package/dist/hooks/validateArtifact/violations.js.map +1 -1
- package/dist/migrate/v2HandoffManifest.js.map +1 -1
- package/dist/registries/artifactProtocolIds.js.map +1 -1
- package/dist/registries/artifactRegistry.js.map +1 -1
- package/dist/registries/artifactSchemaProjection.js +46 -0
- package/dist/registries/artifactSchemaProjection.js.map +1 -0
- package/dist/registries/capabilityContract.js.map +1 -1
- package/dist/registries/evaluatorHandoffContract.js.map +1 -1
- package/dist/registries/evidenceTierContract.js.map +1 -1
- package/dist/registries/glossaryAdviceContract.js.map +1 -1
- package/dist/registries/glossaryCaveatContract.js.map +1 -1
- package/dist/registries/glossaryConsumerContractValidation.js +1 -5
- package/dist/registries/glossaryConsumerContractValidation.js.map +1 -1
- package/dist/registries/glossaryEntryContract.js +10 -0
- package/dist/registries/glossaryEntryContract.js.map +1 -1
- package/dist/registries/glossaryTermIdentity.js.map +1 -1
- package/dist/registries/glossaryTermOccurrence.js.map +1 -1
- package/dist/registries/hybridRoute.js.map +1 -1
- package/dist/registries/hybridRouteReceipt.js.map +1 -1
- package/dist/registries/packageRegistry.js.map +1 -1
- package/dist/registries/todoReadinessContract.js +49 -6
- package/dist/registries/todoReadinessContract.js.map +1 -1
- package/dist/registries/triggerLoader.js.map +1 -1
- package/dist/registries/verbosityBudgetContract.js.map +1 -1
- package/dist/release/releaseMetadata.js.map +1 -1
- package/dist/runtime/lifecycleAdapterContract.js.map +1 -1
- package/dist/runtime/lifecycleAdapters.js.map +1 -1
- package/dist/runtime/lifecycleAuthority.js.map +1 -1
- package/dist/runtime/lifecycleOperationContract.js.map +1 -1
- package/dist/runtime/lifecycleOperations.js.map +1 -1
- package/dist/runtime/lifecycleOwnershipJournal.js.map +1 -1
- package/dist/runtime/lifecyclePublication.js.map +1 -1
- package/dist/runtime/lifecycleSnapshot.js.map +1 -1
- package/dist/runtime/nativeResourceCleanup.js.map +1 -1
- package/dist/setup/codex/cli.js.map +1 -1
- package/dist/setup/codex/configToml.js.map +1 -1
- package/dist/setup/codex/constants.js.map +1 -1
- package/dist/setup/codex/installRoot.js.map +1 -1
- package/dist/setup/codex/state.js.map +1 -1
- package/dist/setup/codex.js.map +1 -1
- package/dist/setup/opencode.js.map +1 -1
- package/dist/setup/opencodeConstants.js.map +1 -1
- package/dist/setup/sharedSkill.js.map +1 -1
- package/dist/setup/smokeChecks.js.map +1 -1
- package/dist/state/archiveDiscovery.js +8 -5
- package/dist/state/archiveDiscovery.js.map +1 -1
- package/dist/state/archivePublication.js.map +1 -1
- package/dist/state/archiveRecovery.js.map +1 -1
- package/dist/state/archiveReplay.js.map +1 -1
- package/dist/state/canonicalMigrationRecord.js.map +1 -1
- package/dist/state/changelog.js.map +1 -1
- package/dist/state/decisionEntities.js +59 -20
- package/dist/state/decisionEntities.js.map +1 -1
- package/dist/state/decisionLegacyValidation.js.map +1 -1
- package/dist/state/decisionMigrationProvenance.js.map +1 -1
- package/dist/state/decisionOverlay.js.map +1 -1
- package/dist/state/decisionRevision.js.map +1 -1
- package/dist/state/decisionRevisionMigration.js.map +1 -1
- package/dist/state/directRetrieval.js.map +1 -1
- package/dist/state/durability.js.map +1 -1
- package/dist/state/entityCutover.js.map +1 -1
- package/dist/state/entityCutoverGit.js.map +1 -1
- package/dist/state/entityGlossaryAuthority.js.map +1 -1
- package/dist/state/entityListProjection.js +188 -0
- package/dist/state/entityListProjection.js.map +1 -0
- package/dist/state/entityListRuntimeRegistry.js +50 -0
- package/dist/state/entityListRuntimeRegistry.js.map +1 -0
- package/dist/state/entityMigrationCausality.js.map +1 -1
- package/dist/state/entityMigrationIdentity.js.map +1 -1
- package/dist/state/entityMigrationPreview.js +32 -3
- package/dist/state/entityMigrationPreview.js.map +1 -1
- package/dist/state/entityPublicationContext.js +607 -765
- package/dist/state/entityPublicationContext.js.map +1 -1
- package/dist/state/entityRetrievalHelp.js +476 -0
- package/dist/state/entityRetrievalHelp.js.map +1 -0
- package/dist/state/entityStorage.js.map +1 -1
- package/dist/state/exactReplacementRecovery.js +19 -0
- package/dist/state/exactReplacementRecovery.js.map +1 -0
- package/dist/state/experimentArchive.js.map +1 -1
- package/dist/state/experimentIdentity.js.map +1 -1
- package/dist/state/healthEntities.js +15 -16
- package/dist/state/healthEntities.js.map +1 -1
- package/dist/state/healthEntityValidation.js.map +1 -1
- package/dist/state/healthRepair.js.map +1 -1
- package/dist/state/installRoot.js.map +1 -1
- package/dist/state/legacyEntityCutoverEvidence.js.map +1 -1
- package/dist/state/legacyIdentity.js.map +1 -1
- package/dist/state/legacySummaryRecord.js.map +1 -1
- package/dist/state/listAccounting.js.map +1 -1
- package/dist/state/listClassification.js.map +1 -1
- package/dist/state/listCursor.js.map +1 -1
- package/dist/state/listRetrieval.js.map +1 -1
- package/dist/state/migrationSourceBinding.js.map +1 -1
- package/dist/state/objectiveExperimentEntities.js +22 -22
- package/dist/state/objectiveExperimentEntities.js.map +1 -1
- package/dist/state/planEntities.js +75 -29
- package/dist/state/planEntities.js.map +1 -1
- package/dist/state/planIdentity.js +4 -1
- package/dist/state/planIdentity.js.map +1 -1
- package/dist/state/planRetrieval.js.map +1 -1
- package/dist/state/planTaskRetrieval.js +41 -31
- package/dist/state/planTaskRetrieval.js.map +1 -1
- package/dist/state/progressEntities.js +40 -25
- package/dist/state/progressEntities.js.map +1 -1
- package/dist/state/progressGlossaryCaveat.js.map +1 -1
- package/dist/state/progressPublicationOrder.js.map +1 -1
- package/dist/state/projectRoot.js.map +1 -1
- package/dist/state/projectionPolicy.js +12 -5
- package/dist/state/projectionPolicy.js.map +1 -1
- package/dist/state/retrievalAuthority.js +86 -272
- package/dist/state/retrievalAuthority.js.map +1 -1
- package/dist/state/safeProjectFile.js.map +1 -1
- package/dist/state/startupAnalysis/benchmark.js.map +1 -1
- package/dist/state/startupAnalysis/contract.js.map +1 -1
- package/dist/state/startupAnalysis/helpers.js.map +1 -1
- package/dist/state/startupAnalysis/index.js.map +1 -1
- package/dist/state/startupAnalysis/metrics.js.map +1 -1
- package/dist/state/startupAnalysis/records.js.map +1 -1
- package/dist/state/startupAnalysis/report.js.map +1 -1
- package/dist/state/startupAnalysis/threshold.js.map +1 -1
- package/dist/state/startupAnalysis.js.map +1 -1
- package/dist/state/startupProjection.js.map +1 -1
- package/dist/state/stateMode.js +11 -8
- package/dist/state/stateMode.js.map +1 -1
- package/dist/state/stateStorageAuthority.js.map +1 -1
- package/dist/state/summaryEntityRead.js.map +1 -1
- package/dist/state/summaryMigrationProvenance.js.map +1 -1
- package/dist/state/summarySourceRowAuthority.js.map +1 -1
- package/dist/state/todoDocsEntities.js +811 -91
- package/dist/state/todoDocsEntities.js.map +1 -1
- package/dist/state/todoDocsEntityValidation.js +127 -2
- package/dist/state/todoDocsEntityValidation.js.map +1 -1
- package/dist/state/todoReconciliationActivation.js +63 -0
- package/dist/state/todoReconciliationActivation.js.map +1 -0
- package/dist/state/todoReconciliationTransaction.js +554 -0
- package/dist/state/todoReconciliationTransaction.js.map +1 -0
- package/dist/state/write/assign.js.map +1 -1
- package/dist/state/write/candidateMutation.js +1 -12
- package/dist/state/write/candidateMutation.js.map +1 -1
- package/dist/state/write/errors.js.map +1 -1
- package/dist/state/write/experimentPublication.js.map +1 -1
- package/dist/state/write/explain.js +198 -177
- package/dist/state/write/explain.js.map +1 -1
- package/dist/state/write/fields.js.map +1 -1
- package/dist/state/write/glossaryPublication.js.map +1 -1
- package/dist/state/write/grammar.js +271 -0
- package/dist/state/write/grammar.js.map +1 -0
- package/dist/state/write/helpers.js.map +1 -1
- package/dist/state/write/index.js.map +1 -1
- package/dist/state/write/input.js +351 -5
- package/dist/state/write/input.js.map +1 -1
- package/dist/state/write/lock.js.map +1 -1
- package/dist/state/write/mutation.js.map +1 -1
- package/dist/state/write/operations.js +140 -343
- package/dist/state/write/operations.js.map +1 -1
- package/dist/state/write/planEvaluation.js +4 -0
- package/dist/state/write/planEvaluation.js.map +1 -1
- package/dist/state/write/planPublication.js +48 -3
- package/dist/state/write/planPublication.js.map +1 -1
- package/dist/state/write/runtimeOperations.js +65 -0
- package/dist/state/write/runtimeOperations.js.map +1 -0
- package/dist/state/write/serialize.js.map +1 -1
- package/dist/state/write/transaction.js +13 -7
- package/dist/state/write/transaction.js.map +1 -1
- package/dist/state/write/validate.js.map +1 -1
- package/dist/upgrade/appContentRefresh.js.map +1 -1
- package/dist/upgrade/appModel.js.map +1 -1
- package/dist/upgrade/atomicWriter.js.map +1 -1
- package/dist/upgrade/bundleEvidence.js.map +1 -1
- package/dist/upgrade/channels.js.map +1 -1
- package/dist/upgrade/coexistenceProbe.js.map +1 -1
- package/dist/upgrade/compatibility.js.map +1 -1
- package/dist/upgrade/doctor.js.map +1 -1
- package/dist/upgrade/doctorClassifier.js.map +1 -1
- package/dist/upgrade/installedHooksRetirement.js.map +1 -1
- package/dist/upgrade/legacyAgentCleanup.js.map +1 -1
- package/dist/upgrade/lifecycleUpgrade.js.map +1 -1
- package/dist/upgrade/migrateArtifactsV2ToV3.js.map +1 -1
- package/dist/upgrade/migrationPublication.js.map +1 -1
- package/dist/upgrade/nextMajorDoctor.js.map +1 -1
- package/dist/upgrade/npxPlatformStatus.js.map +1 -1
- package/dist/upgrade/projectIntegration.js.map +1 -1
- package/dist/upgrade/projectIntegrationDecision.js.map +1 -1
- package/dist/upgrade/runtimeMigration.js.map +1 -1
- package/dist/upgrade/upgradeCommands.js.map +1 -1
- package/dist/upgrade/upgradeLock.js.map +1 -1
- package/dist/upgrade/upgradeOrchestrator.js.map +1 -1
- package/dist/upgrade/v3CapabilitySurface.js.map +1 -1
- package/dist/upgrade/versionResolution.js.map +1 -1
- package/dist/validate/appHomeContract.js.map +1 -1
- package/dist/validate/capability.js.map +1 -1
- package/dist/validate/crossCapability.js.map +1 -1
- package/dist/validate/selfAudit.js.map +1 -1
- package/dist/validate/v1LegacyCruft.js.map +1 -1
- package/dist/validate/vocabularyAuthority.js.map +1 -1
- package/package.json +2 -2
- package/dist/state/experimentRetrieval.js +0 -447
- package/dist/state/experimentRetrieval.js.map +0 -1
|
@@ -1,128 +1,241 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { ExactReplacementConflictError, FileReplacementError } from "./exactReplacementRecovery.js";
|
|
4
5
|
import { assertValidatedProjectRoot } from "./projectRoot.js";
|
|
5
|
-
import { reject } from "./write/errors.js";
|
|
6
|
-
const DIRECTORY_FLAGS = fs.constants.O_RDONLY
|
|
7
|
-
| (fs.constants.O_DIRECTORY ?? 0)
|
|
8
|
-
| (fs.constants.O_NOFOLLOW ?? 0);
|
|
9
|
-
const FILE_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
6
|
+
import { reject, StateWriteInputError } from "./write/errors.js";
|
|
10
7
|
const RECOVERY_IGNORE_BYTES = "*\n!.gitignore\n";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
&& left.ino === right.ino
|
|
14
|
-
&& left.type === right.type
|
|
15
|
-
&& left.size === right.size
|
|
16
|
-
&& left.sha256 === right.sha256;
|
|
17
|
-
}
|
|
18
|
-
function fdPath(fd, name) {
|
|
19
|
-
return name ? `/proc/self/fd/${fd}/${name}` : `/proc/self/fd/${fd}`;
|
|
20
|
-
}
|
|
8
|
+
export const FILE_REPLACEMENT_RECOVERY_VERSION = "agentera.fileReplacementRecovery.v1";
|
|
9
|
+
export const FILE_REPLACEMENT_METADATA_NAME = "replacement.json";
|
|
21
10
|
function identity(stat) {
|
|
22
11
|
return {
|
|
23
12
|
dev: stat.dev,
|
|
24
13
|
ino: stat.ino,
|
|
14
|
+
type: stat.mode & BigInt(fs.constants.S_IFMT),
|
|
25
15
|
size: stat.size,
|
|
26
16
|
mtimeNs: stat.mtimeNs,
|
|
27
17
|
ctimeNs: stat.ctimeNs,
|
|
28
18
|
nlink: stat.nlink,
|
|
19
|
+
mode: stat.mode,
|
|
29
20
|
};
|
|
30
21
|
}
|
|
31
|
-
function sameObject(left, right) {
|
|
32
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
33
|
-
}
|
|
34
22
|
function sameIdentity(left, right) {
|
|
35
23
|
return left.dev === right.dev
|
|
36
24
|
&& left.ino === right.ino
|
|
25
|
+
&& left.type === right.type
|
|
37
26
|
&& left.size === right.size
|
|
38
27
|
&& left.mtimeNs === right.mtimeNs
|
|
39
28
|
&& left.ctimeNs === right.ctimeNs
|
|
40
29
|
&& left.nlink === right.nlink;
|
|
41
30
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const bytes = Buffer.alloc(Number(stat.size));
|
|
48
|
-
let offset = 0;
|
|
49
|
-
while (offset < bytes.length) {
|
|
50
|
-
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
51
|
-
if (count === 0)
|
|
52
|
-
break;
|
|
53
|
-
offset += count;
|
|
54
|
-
}
|
|
55
|
-
if (offset !== bytes.length)
|
|
56
|
-
throw new Error("state mode marker changed while its exact bytes were read");
|
|
57
|
-
return bytes;
|
|
31
|
+
function sameDirectory(left, right) {
|
|
32
|
+
return right.isDirectory()
|
|
33
|
+
&& !right.isSymbolicLink()
|
|
34
|
+
&& left.dev === right.dev
|
|
35
|
+
&& left.ino === right.ino;
|
|
58
36
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
37
|
+
function sha256(bytes) {
|
|
38
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
function publishedIdentity(file) {
|
|
62
41
|
return {
|
|
63
|
-
dev:
|
|
64
|
-
ino:
|
|
65
|
-
type:
|
|
66
|
-
size:
|
|
67
|
-
sha256:
|
|
42
|
+
dev: file.identity.dev,
|
|
43
|
+
ino: file.identity.ino,
|
|
44
|
+
type: file.identity.type,
|
|
45
|
+
size: file.identity.size,
|
|
46
|
+
sha256: sha256(file.bytes),
|
|
68
47
|
};
|
|
69
48
|
}
|
|
70
|
-
function matchesPublished(
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
function matchesPublished(file, expected) {
|
|
50
|
+
const observed = publishedIdentity(file);
|
|
51
|
+
return observed.dev === expected.dev
|
|
52
|
+
&& observed.ino === expected.ino
|
|
53
|
+
&& observed.type === expected.type
|
|
54
|
+
&& observed.size === expected.size
|
|
55
|
+
&& observed.sha256 === expected.sha256;
|
|
56
|
+
}
|
|
57
|
+
function matchesExpected(file, expected) {
|
|
58
|
+
return Buffer.isBuffer(expected) ? file.bytes.equals(expected) : matchesPublished(file, expected);
|
|
78
59
|
}
|
|
79
|
-
function
|
|
80
|
-
return
|
|
60
|
+
function sameStableFile(left, right) {
|
|
61
|
+
return sameIdentity(left.identity, right.identity) && left.bytes.equals(right.bytes);
|
|
81
62
|
}
|
|
82
|
-
function
|
|
83
|
-
let
|
|
63
|
+
function readStableFile(absolute, label) {
|
|
64
|
+
let before;
|
|
84
65
|
try {
|
|
85
|
-
|
|
86
|
-
return sameObject(fs.fstatSync(observed, { bigint: true }), fs.fstatSync(expectedFd, { bigint: true }));
|
|
66
|
+
before = fs.lstatSync(absolute, { bigint: true });
|
|
87
67
|
}
|
|
88
|
-
catch {
|
|
89
|
-
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new Error(`${label} is unavailable: ${error.message}`, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
72
|
+
throw new Error(`${label} is not a safe regular file`);
|
|
73
|
+
}
|
|
74
|
+
let descriptor;
|
|
75
|
+
try {
|
|
76
|
+
descriptor = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
77
|
+
const opened = fs.fstatSync(descriptor, { bigint: true });
|
|
78
|
+
if (!opened.isFile() || opened.isSymbolicLink() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
79
|
+
throw new Error(`${label} changed while it was opened`);
|
|
80
|
+
}
|
|
81
|
+
const bytes = fs.readFileSync(descriptor);
|
|
82
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
83
|
+
const pathname = fs.lstatSync(absolute, { bigint: true });
|
|
84
|
+
const openedIdentity = identity(opened);
|
|
85
|
+
if (!sameIdentity(openedIdentity, identity(after))
|
|
86
|
+
|| !sameIdentity(openedIdentity, identity(pathname))
|
|
87
|
+
|| BigInt(bytes.length) !== opened.size)
|
|
88
|
+
throw new Error(`${label} changed while its exact bytes were read`);
|
|
89
|
+
return { absolute, identity: identity(after), bytes };
|
|
90
90
|
}
|
|
91
91
|
finally {
|
|
92
|
-
if (
|
|
93
|
-
fs.closeSync(
|
|
92
|
+
if (descriptor !== undefined)
|
|
93
|
+
fs.closeSync(descriptor);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
function
|
|
97
|
-
let fd;
|
|
96
|
+
function readFileIfPresent(absolute, label) {
|
|
98
97
|
try {
|
|
99
|
-
|
|
100
|
-
return true;
|
|
98
|
+
fs.lstatSync(absolute);
|
|
101
99
|
}
|
|
102
100
|
catch (error) {
|
|
103
|
-
|
|
101
|
+
if (error.code === "ENOENT")
|
|
102
|
+
return null;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
return readStableFile(absolute, label);
|
|
106
|
+
}
|
|
107
|
+
function readStableDirectory(absolute, created = false) {
|
|
108
|
+
const stat = fs.lstatSync(absolute, { bigint: true });
|
|
109
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
110
|
+
throw new Error(`publication directory '${absolute}' is not a safe real directory`);
|
|
111
|
+
}
|
|
112
|
+
return { absolute, dev: stat.dev, ino: stat.ino, created };
|
|
113
|
+
}
|
|
114
|
+
function projectLocalPath(root, absolute, allowRoot = false) {
|
|
115
|
+
const resolved = path.resolve(absolute);
|
|
116
|
+
const relative = path.relative(root.path, resolved);
|
|
117
|
+
return resolved === absolute
|
|
118
|
+
&& (allowRoot || relative.length > 0)
|
|
119
|
+
&& relative !== ".."
|
|
120
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
121
|
+
&& !path.isAbsolute(relative);
|
|
122
|
+
}
|
|
123
|
+
/** Validate a recovery directory before listing or mutating anything through it. */
|
|
124
|
+
export function validateEntityRecoveryDirectory(root, absolute, targetDirectory, label, expected) {
|
|
125
|
+
assertValidatedProjectRoot(root);
|
|
126
|
+
if (!projectLocalPath(root, absolute) || !projectLocalPath(root, targetDirectory, true)) {
|
|
127
|
+
throw new FileReplacementError(`${label} must be a real project-local directory under the validated project root`);
|
|
128
|
+
}
|
|
129
|
+
let directory;
|
|
130
|
+
let target;
|
|
131
|
+
try {
|
|
132
|
+
directory = readStableDirectory(absolute);
|
|
133
|
+
target = readStableDirectory(targetDirectory);
|
|
134
|
+
if (fs.realpathSync(absolute) !== absolute || fs.realpathSync(targetDirectory) !== targetDirectory) {
|
|
135
|
+
throw new Error("a directory path traverses a symbolic link");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw new FileReplacementError(`${label} must be a real project-local non-symlink directory: ${error.message}`, error);
|
|
140
|
+
}
|
|
141
|
+
if (typeof process.getuid === "function") {
|
|
142
|
+
const stat = fs.lstatSync(directory.absolute, { bigint: true });
|
|
143
|
+
if ((stat.mode & 18n) !== 0n || stat.uid !== BigInt(process.getuid())) {
|
|
144
|
+
throw new FileReplacementError(`${label} must be owned by the current user and not group/world-writable`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (directory.dev !== target.dev) {
|
|
148
|
+
throw new FileReplacementError(`${label} must share the replacement target filesystem`);
|
|
149
|
+
}
|
|
150
|
+
if (expected && (directory.absolute !== expected.absolute
|
|
151
|
+
|| directory.dev !== expected.dev
|
|
152
|
+
|| directory.ino !== expected.ino)) {
|
|
153
|
+
throw new FileReplacementError(`${label} changed after its recovery boundary was validated`);
|
|
154
|
+
}
|
|
155
|
+
return { absolute: directory.absolute, dev: directory.dev, ino: directory.ino };
|
|
156
|
+
}
|
|
157
|
+
function syncDirectory(absolute) {
|
|
158
|
+
if (process.platform === "win32")
|
|
159
|
+
return;
|
|
160
|
+
let descriptor;
|
|
161
|
+
try {
|
|
162
|
+
descriptor = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY ?? 0));
|
|
163
|
+
fs.fsyncSync(descriptor);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (!["EINVAL", "ENOTSUP", "ENOSYS"].includes(error.code ?? ""))
|
|
167
|
+
throw error;
|
|
104
168
|
}
|
|
105
169
|
finally {
|
|
106
|
-
if (
|
|
107
|
-
fs.closeSync(
|
|
170
|
+
if (descriptor !== undefined)
|
|
171
|
+
fs.closeSync(descriptor);
|
|
108
172
|
}
|
|
109
173
|
}
|
|
110
|
-
function
|
|
111
|
-
let fd;
|
|
174
|
+
function syncDirectoryBestEffort(absolute) {
|
|
112
175
|
try {
|
|
113
|
-
|
|
114
|
-
return readDescriptor(fd).equals(expected);
|
|
176
|
+
syncDirectory(absolute);
|
|
115
177
|
}
|
|
116
|
-
catch {
|
|
178
|
+
catch { /* The canonical rename already linearized. */ }
|
|
179
|
+
}
|
|
180
|
+
function safeSegments(relativeTarget, label) {
|
|
181
|
+
const segments = relativeTarget.split(/[\\/]/).filter(Boolean);
|
|
182
|
+
if (segments.length < 1
|
|
183
|
+
|| path.posix.isAbsolute(relativeTarget)
|
|
184
|
+
|| path.win32.isAbsolute(relativeTarget)
|
|
185
|
+
|| relativeTarget.includes("\0")
|
|
186
|
+
|| segments.includes("..")
|
|
187
|
+
|| segments.includes("."))
|
|
188
|
+
reject({
|
|
189
|
+
class: "unsupported_target",
|
|
190
|
+
message: `${label} '${relativeTarget}' is outside the validated project path grammar`,
|
|
191
|
+
recovery: "Use one project-relative path without symbolic, dot, parent, drive, UNC, or absolute segments; no state was changed.",
|
|
192
|
+
});
|
|
193
|
+
return segments;
|
|
194
|
+
}
|
|
195
|
+
function relativeDisplay(root, absolute) {
|
|
196
|
+
return path.relative(root, absolute).split(path.sep).join("/");
|
|
197
|
+
}
|
|
198
|
+
function removeExactFile(file) {
|
|
199
|
+
const current = readFileIfPresent(file.absolute, `attempt-owned file '${file.absolute}'`);
|
|
200
|
+
if (!current || !sameStableFile(file, current))
|
|
117
201
|
return false;
|
|
202
|
+
fs.unlinkSync(file.absolute);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function createDurableFile(absolute, bytes, mode = 0o600) {
|
|
206
|
+
let descriptor;
|
|
207
|
+
let opened;
|
|
208
|
+
try {
|
|
209
|
+
descriptor = fs.openSync(absolute, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), mode);
|
|
210
|
+
opened = identity(fs.fstatSync(descriptor, { bigint: true }));
|
|
211
|
+
fs.writeFileSync(descriptor, bytes);
|
|
212
|
+
if (typeof process.getuid === "function")
|
|
213
|
+
fs.fchmodSync(descriptor, mode);
|
|
214
|
+
fs.fsyncSync(descriptor);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (descriptor !== undefined) {
|
|
218
|
+
try {
|
|
219
|
+
fs.closeSync(descriptor);
|
|
220
|
+
}
|
|
221
|
+
catch { /* Preserve the primary failure. */ }
|
|
222
|
+
descriptor = undefined;
|
|
223
|
+
}
|
|
224
|
+
if (opened) {
|
|
225
|
+
try {
|
|
226
|
+
const current = readFileIfPresent(absolute, `failed stage '${absolute}'`);
|
|
227
|
+
if (current && sameIdentity(opened, current.identity))
|
|
228
|
+
fs.unlinkSync(absolute);
|
|
229
|
+
}
|
|
230
|
+
catch { /* Preserve changed or partially inspectable attempt state. */ }
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
118
233
|
}
|
|
119
234
|
finally {
|
|
120
|
-
if (
|
|
121
|
-
fs.closeSync(
|
|
235
|
+
if (descriptor !== undefined)
|
|
236
|
+
fs.closeSync(descriptor);
|
|
122
237
|
}
|
|
123
|
-
}
|
|
124
|
-
function syncDirectory(fd) {
|
|
125
|
-
fs.fsyncSync(fd);
|
|
238
|
+
return readStableFile(absolute, `durable stage '${absolute}'`);
|
|
126
239
|
}
|
|
127
240
|
function publicationConflict(markerPath) {
|
|
128
241
|
reject({
|
|
@@ -133,65 +246,28 @@ function publicationConflict(markerPath) {
|
|
|
133
246
|
});
|
|
134
247
|
}
|
|
135
248
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* the final root, marker, directory-chain, and target validation.
|
|
249
|
+
* Binds mutations to a validated project and exact entity-mode marker. Every
|
|
250
|
+
* pathname and byte identity is checked at declared boundaries. Publication
|
|
251
|
+
* linearizes through standard complete-file link or rename operations; a
|
|
252
|
+
* non-cooperating writer that races after the final successful check is outside
|
|
253
|
+
* the contract and is not represented as a cross-process compare-and-swap.
|
|
142
254
|
*/
|
|
143
255
|
export class EntityPublicationContext {
|
|
144
256
|
projectRoot;
|
|
145
257
|
validatedRoot;
|
|
146
258
|
markerPath;
|
|
147
|
-
|
|
148
|
-
markerParentEntries;
|
|
149
|
-
markerFd;
|
|
150
|
-
markerIdentity;
|
|
151
|
-
markerBytes;
|
|
259
|
+
marker;
|
|
152
260
|
createdDirectories = new Map();
|
|
153
261
|
closed = false;
|
|
154
262
|
constructor(root, markerPath, expectedBytes) {
|
|
155
263
|
this.projectRoot = root.path;
|
|
156
264
|
this.validatedRoot = root;
|
|
157
265
|
this.markerPath = markerPath;
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const rootStat = fs.fstatSync(this.rootFd, { bigint: true });
|
|
164
|
-
const rootIdentity = root.identities.at(-1);
|
|
165
|
-
if (rootStat.dev !== rootIdentity.dev || rootStat.ino !== rootIdentity.ino) {
|
|
166
|
-
throw new Error(`project root '${root.path}' changed while its publication context was opened`);
|
|
167
|
-
}
|
|
168
|
-
const segments = markerPath.split("/").filter(Boolean);
|
|
169
|
-
let parentFd = this.rootFd;
|
|
170
|
-
for (const name of segments.slice(0, -1)) {
|
|
171
|
-
const fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
|
|
172
|
-
this.markerParentEntries.push({ parentFd, name, fd, created: false });
|
|
173
|
-
parentFd = fd;
|
|
174
|
-
}
|
|
175
|
-
markerFd = fs.openSync(fdPath(parentFd, segments.at(-1)), FILE_FLAGS);
|
|
176
|
-
const before = fs.fstatSync(markerFd, { bigint: true });
|
|
177
|
-
const bytes = readDescriptor(markerFd);
|
|
178
|
-
const after = fs.fstatSync(markerFd, { bigint: true });
|
|
179
|
-
if (!before.isFile() || !sameIdentity(identity(before), after) || !bytes.equals(expectedBytes)) {
|
|
180
|
-
publicationConflict(markerPath);
|
|
181
|
-
}
|
|
182
|
-
this.markerFd = markerFd;
|
|
183
|
-
this.markerIdentity = identity(after);
|
|
184
|
-
this.assertValid();
|
|
185
|
-
markerFd = undefined;
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
if (markerFd !== undefined)
|
|
189
|
-
fs.closeSync(markerFd);
|
|
190
|
-
for (const entry of this.markerParentEntries.reverse())
|
|
191
|
-
fs.closeSync(entry.fd);
|
|
192
|
-
fs.closeSync(this.rootFd);
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
266
|
+
const markerAbsolute = path.join(root.path, ...safeSegments(markerPath, "state mode marker"));
|
|
267
|
+
this.marker = readStableFile(markerAbsolute, `state mode marker '${markerPath}'`);
|
|
268
|
+
if (!this.marker.bytes.equals(expectedBytes))
|
|
269
|
+
publicationConflict(markerPath);
|
|
270
|
+
this.assertValid();
|
|
195
271
|
}
|
|
196
272
|
static open(root, markerPath, expectedBytes) {
|
|
197
273
|
return new EntityPublicationContext(root, markerPath, expectedBytes);
|
|
@@ -199,500 +275,421 @@ export class EntityPublicationContext {
|
|
|
199
275
|
pinnedPath(relativePath = "") {
|
|
200
276
|
if (this.closed)
|
|
201
277
|
throw new Error("entity publication context is closed");
|
|
202
|
-
|
|
278
|
+
if (!relativePath)
|
|
279
|
+
return this.projectRoot;
|
|
280
|
+
return path.join(this.projectRoot, ...safeSegments(relativePath, "entity publication path"));
|
|
203
281
|
}
|
|
204
282
|
assertValid() {
|
|
205
283
|
if (this.closed)
|
|
206
284
|
throw new Error("entity publication context is closed");
|
|
207
285
|
assertValidatedProjectRoot(this.validatedRoot);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
throw new Error(`project root '${this.projectRoot}' changed after validation; restore the exact real directory and retry`);
|
|
286
|
+
let current;
|
|
287
|
+
try {
|
|
288
|
+
current = readStableFile(this.marker.absolute, `state mode marker '${this.markerPath}'`);
|
|
212
289
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
publicationConflict(this.markerPath);
|
|
216
|
-
}
|
|
290
|
+
catch {
|
|
291
|
+
publicationConflict(this.markerPath);
|
|
217
292
|
}
|
|
218
|
-
|
|
219
|
-
const markerName = this.markerPath.split("/").filter(Boolean).at(-1);
|
|
220
|
-
if (!openMatches(parentFd, markerName, this.markerFd, FILE_FLAGS)
|
|
221
|
-
|| !sameIdentity(this.markerIdentity, fs.fstatSync(this.markerFd, { bigint: true }))
|
|
222
|
-
|| !readDescriptor(this.markerFd).equals(this.markerBytes))
|
|
293
|
+
if (!sameStableFile(this.marker, current))
|
|
223
294
|
publicationConflict(this.markerPath);
|
|
224
295
|
}
|
|
225
296
|
publishImmutable(relativeTarget, bytes) {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|| path.isAbsolute(relativeTarget)
|
|
230
|
-
|| segments.includes("..")
|
|
231
|
-
|| segments.includes("."))
|
|
232
|
-
throw new Error(`unsafe entity publication target '${relativeTarget}'`);
|
|
233
|
-
this.assertValid();
|
|
297
|
+
const segments = safeSegments(relativeTarget, "immutable entity target");
|
|
298
|
+
if (segments.length < 2)
|
|
299
|
+
throw new Error(`unsafe immutable entity target '${relativeTarget}'`);
|
|
234
300
|
const directories = [];
|
|
235
|
-
const
|
|
236
|
-
let
|
|
237
|
-
let
|
|
238
|
-
let
|
|
301
|
+
const target = path.join(this.projectRoot, ...segments);
|
|
302
|
+
let stage;
|
|
303
|
+
let published;
|
|
304
|
+
let publicationOwned = false;
|
|
239
305
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
fd = fs.openSync(fdPath(entryParentFd, name), DIRECTORY_FLAGS);
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
if (error.code !== "ENOENT")
|
|
251
|
-
throw error;
|
|
252
|
-
this.assertBoundary(directories);
|
|
253
|
-
fs.mkdirSync(fdPath(entryParentFd, name));
|
|
254
|
-
created = true;
|
|
255
|
-
fd = fs.openSync(fdPath(entryParentFd, name), DIRECTORY_FLAGS);
|
|
256
|
-
}
|
|
257
|
-
const entry = { parentFd: entryParentFd, name, fd, created };
|
|
258
|
-
directories.push(entry);
|
|
259
|
-
if (created)
|
|
260
|
-
createdPaths.push({ relativePath: segments.slice(0, directories.length).join("/"), fd });
|
|
261
|
-
parentFd = fd;
|
|
262
|
-
if (created)
|
|
263
|
-
syncDirectory(entryParentFd);
|
|
264
|
-
this.assertBoundary(directories);
|
|
265
|
-
}
|
|
266
|
-
const directoryFd = directories.at(-1).fd;
|
|
267
|
-
const targetName = segments.at(-1);
|
|
268
|
-
stageName = `.${targetName}.${process.pid}.${randomUUID()}.tmp`;
|
|
269
|
-
this.assertBoundary(directories);
|
|
270
|
-
stageFd = fs.openSync(fdPath(directoryFd, stageName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
271
|
-
this.assertBoundary(directories, { name: stageName, fd: stageFd });
|
|
272
|
-
fs.writeFileSync(stageFd, bytes, "utf8");
|
|
273
|
-
fs.fsyncSync(stageFd);
|
|
274
|
-
this.assertBoundary(directories, { name: stageName, fd: stageFd });
|
|
275
|
-
try {
|
|
276
|
-
this.assertBoundary(directories, { name: stageName, fd: stageFd });
|
|
277
|
-
fs.linkSync(fdPath(directoryFd, stageName), fdPath(directoryFd, targetName));
|
|
278
|
-
targetLinked = true;
|
|
306
|
+
directories.push(...this.openDirectories(segments.slice(0, -1), true));
|
|
307
|
+
const directory = directories.at(-1)?.absolute ?? this.projectRoot;
|
|
308
|
+
const existing = readFileIfPresent(target, `immutable entity target '${relativeTarget}'`);
|
|
309
|
+
if (existing) {
|
|
310
|
+
this.removeCreatedDirectories(directories);
|
|
311
|
+
return null;
|
|
279
312
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.assertBoundary(directories);
|
|
313
|
+
const stagePath = path.join(directory, `.${segments.at(-1)}.${process.pid}.${randomUUID()}.tmp`);
|
|
314
|
+
stage = createDurableFile(stagePath, bytes);
|
|
315
|
+
this.assertBoundary(directories, undefined, [stage]);
|
|
316
|
+
if (readFileIfPresent(target, `immutable entity target '${relativeTarget}'`)) {
|
|
317
|
+
removeExactFile(stage);
|
|
318
|
+
stage = undefined;
|
|
287
319
|
this.removeCreatedDirectories(directories);
|
|
288
320
|
return null;
|
|
289
321
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
322
|
+
try {
|
|
323
|
+
fs.linkSync(stage.absolute, target);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error.code === "EEXIST") {
|
|
327
|
+
removeExactFile(stage);
|
|
328
|
+
stage = undefined;
|
|
329
|
+
this.removeCreatedDirectories(directories);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
throw new FileReplacementError(`immutable publication for '${relativeTarget}' failed before effects`, error);
|
|
333
|
+
}
|
|
334
|
+
syncDirectory(directory);
|
|
335
|
+
stage = readStableFile(stage.absolute, `immutable publication stage '${relativeTarget}'`);
|
|
336
|
+
published = readStableFile(target, `published entity '${relativeTarget}'`);
|
|
337
|
+
if (!published.bytes.equals(Buffer.from(bytes)) || published.identity.ino !== stage.identity.ino || published.identity.dev !== stage.identity.dev) {
|
|
338
|
+
throw new ExactReplacementConflictError(`immutable entity target '${relativeTarget}' changed at its publication boundary`);
|
|
339
|
+
}
|
|
340
|
+
publicationOwned = true;
|
|
341
|
+
this.assertBoundary(directories, published, [stage]);
|
|
342
|
+
removeExactFile(stage);
|
|
343
|
+
stage = undefined;
|
|
344
|
+
syncDirectory(directory);
|
|
345
|
+
published = readStableFile(target, `published entity '${relativeTarget}'`);
|
|
346
|
+
this.assertBoundary(directories, published);
|
|
347
|
+
for (const entry of directories.filter(({ created }) => created)) {
|
|
348
|
+
this.createdDirectories.set(relativeDisplay(this.projectRoot, entry.absolute), entry);
|
|
349
|
+
}
|
|
350
|
+
return publishedIdentity(published);
|
|
299
351
|
}
|
|
300
352
|
catch (error) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
catch {
|
|
309
|
-
cleanupFailures.push("entity");
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (directoryFd !== undefined && stageFd !== undefined && stageName !== undefined) {
|
|
313
|
-
try {
|
|
314
|
-
this.removeOwnedFile(directoryFd, stageName, stageFd);
|
|
353
|
+
if (publicationOwned && published) {
|
|
354
|
+
if (stage) {
|
|
355
|
+
try {
|
|
356
|
+
if (removeExactFile(stage))
|
|
357
|
+
stage = undefined;
|
|
358
|
+
}
|
|
359
|
+
catch { /* Preserve changed attempt bytes. */ }
|
|
315
360
|
}
|
|
316
|
-
|
|
317
|
-
|
|
361
|
+
if (!stage) {
|
|
362
|
+
try {
|
|
363
|
+
const current = readFileIfPresent(target, `failed immutable publication '${relativeTarget}'`);
|
|
364
|
+
if (current && matchesPublished(current, publishedIdentity(published)))
|
|
365
|
+
removeExactFile(current);
|
|
366
|
+
}
|
|
367
|
+
catch { /* Preserve a changed successor. */ }
|
|
318
368
|
}
|
|
319
369
|
}
|
|
320
|
-
if (
|
|
370
|
+
if (stage) {
|
|
321
371
|
try {
|
|
322
|
-
|
|
372
|
+
removeExactFile(stage);
|
|
323
373
|
}
|
|
324
|
-
catch {
|
|
325
|
-
cleanupFailures.push("directory sync");
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
this.removeCreatedDirectories(directories);
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
cleanupFailures.push("directory");
|
|
333
|
-
}
|
|
334
|
-
if (cleanupFailures.length > 0) {
|
|
335
|
-
throw new Error(`entity publication failed and exact rollback could not remove its ${cleanupFailures.join(", ")}; inspect the pinned project state before retrying`, { cause: error });
|
|
374
|
+
catch { /* Preserve changed attempt bytes. */ }
|
|
336
375
|
}
|
|
376
|
+
this.removeCreatedDirectories(directories);
|
|
337
377
|
throw error;
|
|
338
378
|
}
|
|
339
|
-
finally {
|
|
340
|
-
if (stageFd !== undefined)
|
|
341
|
-
fs.closeSync(stageFd);
|
|
342
|
-
for (const entry of directories.reverse())
|
|
343
|
-
fs.closeSync(entry.fd);
|
|
344
|
-
}
|
|
345
379
|
}
|
|
346
380
|
publishPreowned(relativeTarget, relativeReceipt, expected) {
|
|
347
|
-
const targetSegments = relativeTarget
|
|
348
|
-
const receiptSegments = relativeReceipt
|
|
349
|
-
if (
|
|
350
|
-
|| path.isAbsolute(relativeTarget) || path.isAbsolute(relativeReceipt))
|
|
381
|
+
const targetSegments = safeSegments(relativeTarget, "preowned target");
|
|
382
|
+
const receiptSegments = safeSegments(relativeReceipt, "preowned receipt");
|
|
383
|
+
if (targetSegments.length < 2 || receiptSegments.length < 2)
|
|
351
384
|
throw new Error("unsafe preowned publication path");
|
|
352
385
|
this.assertValid();
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
386
|
+
const receipt = readStableFile(path.join(this.projectRoot, ...receiptSegments), `preowned receipt '${relativeReceipt}'`);
|
|
387
|
+
if (!matchesPublished(receipt, expected))
|
|
388
|
+
throw new Error(`preowned receipt '${relativeReceipt}' changed before publication`);
|
|
389
|
+
const directories = this.openDirectories(targetSegments.slice(0, -1), true);
|
|
390
|
+
const directory = directories.at(-1)?.absolute ?? this.projectRoot;
|
|
391
|
+
const target = path.join(directory, targetSegments.at(-1));
|
|
359
392
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const fd = fs.openSync(fdPath(receiptParent, name), DIRECTORY_FLAGS);
|
|
363
|
-
receiptDirectories.push({ parentFd: receiptParent, name, fd, created: false });
|
|
364
|
-
receiptParent = fd;
|
|
365
|
-
}
|
|
366
|
-
receiptFd = fs.openSync(fdPath(receiptParent, receiptSegments.at(-1)), FILE_FLAGS);
|
|
367
|
-
const receiptIdentity = publishedIdentity(receiptFd);
|
|
368
|
-
if (!samePublished(receiptIdentity, expected))
|
|
369
|
-
throw new Error(`migration ownership receipt '${relativeReceipt}' changed`);
|
|
370
|
-
let targetParent = this.rootFd;
|
|
371
|
-
for (const [index, name] of targetSegments.slice(0, -1).entries()) {
|
|
372
|
-
this.assertBoundary(targetDirectories);
|
|
373
|
-
const parentFd = targetParent;
|
|
374
|
-
let fd;
|
|
375
|
-
let created = false;
|
|
393
|
+
this.assertBoundary(directories, undefined, [receipt]);
|
|
394
|
+
if (!readFileIfPresent(target, `preowned target '${relativeTarget}'`)) {
|
|
376
395
|
try {
|
|
377
|
-
|
|
396
|
+
fs.linkSync(receipt.absolute, target);
|
|
397
|
+
syncDirectory(directory);
|
|
378
398
|
}
|
|
379
399
|
catch (error) {
|
|
380
|
-
if (error.code !== "
|
|
400
|
+
if (error.code !== "EEXIST")
|
|
381
401
|
throw error;
|
|
382
|
-
fs.mkdirSync(fdPath(parentFd, name));
|
|
383
|
-
created = true;
|
|
384
|
-
fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
|
|
385
|
-
syncDirectory(parentFd);
|
|
386
402
|
}
|
|
387
|
-
targetDirectories.push({ parentFd, name, fd, created });
|
|
388
|
-
if (created)
|
|
389
|
-
createdPaths.push({ relativePath: targetSegments.slice(0, index + 1).join("/"), fd });
|
|
390
|
-
targetParent = fd;
|
|
391
403
|
}
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
fs.linkSync(fdPath(receiptParent, receiptSegments.at(-1)), fdPath(targetParent, targetName));
|
|
395
|
-
linked = true;
|
|
396
|
-
syncDirectory(targetParent);
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
if (error.code !== "EEXIST")
|
|
400
|
-
throw error;
|
|
401
|
-
}
|
|
402
|
-
targetFd = fs.openSync(fdPath(targetParent, targetName), FILE_FLAGS);
|
|
403
|
-
const targetIdentity = publishedIdentity(targetFd);
|
|
404
|
-
if (!sameObject(fs.fstatSync(receiptFd, { bigint: true }), fs.fstatSync(targetFd, { bigint: true })) || !samePublished(targetIdentity, expected)) {
|
|
404
|
+
const published = readStableFile(target, `preowned target '${relativeTarget}'`);
|
|
405
|
+
if (!matchesPublished(published, expected) || published.identity.dev !== receipt.identity.dev || published.identity.ino !== receipt.identity.ino) {
|
|
405
406
|
throw new Error(`canonical target '${relativeTarget}' collides with migration ownership receipt; byte-identical replacement inodes are never adopted`);
|
|
406
407
|
}
|
|
407
|
-
this.assertBoundary(
|
|
408
|
-
for (const
|
|
409
|
-
this.createdDirectories.set(
|
|
410
|
-
|
|
408
|
+
this.assertBoundary(directories, published, [receipt]);
|
|
409
|
+
for (const entry of directories.filter(({ created }) => created)) {
|
|
410
|
+
this.createdDirectories.set(relativeDisplay(this.projectRoot, entry.absolute), entry);
|
|
411
|
+
}
|
|
412
|
+
return publishedIdentity(published);
|
|
411
413
|
}
|
|
412
414
|
catch (error) {
|
|
413
|
-
|
|
414
|
-
if (linked && targetParent !== undefined && receiptFd !== undefined)
|
|
415
|
-
this.removeOwnedFile(targetParent, targetSegments.at(-1), receiptFd);
|
|
416
|
-
this.removeCreatedDirectories(targetDirectories);
|
|
415
|
+
this.removeCreatedDirectories(directories);
|
|
417
416
|
throw error;
|
|
418
417
|
}
|
|
419
|
-
finally {
|
|
420
|
-
if (targetFd !== undefined)
|
|
421
|
-
fs.closeSync(targetFd);
|
|
422
|
-
if (receiptFd !== undefined)
|
|
423
|
-
fs.closeSync(receiptFd);
|
|
424
|
-
for (const entry of targetDirectories.reverse())
|
|
425
|
-
fs.closeSync(entry.fd);
|
|
426
|
-
for (const entry of receiptDirectories.reverse())
|
|
427
|
-
fs.closeSync(entry.fd);
|
|
428
|
-
}
|
|
429
418
|
}
|
|
430
|
-
removeExact(relativeTarget, expected) {
|
|
431
|
-
const segments = relativeTarget
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
419
|
+
removeExact(relativeTarget, expected, verifyContext = true) {
|
|
420
|
+
const segments = safeSegments(relativeTarget, "exact removal target");
|
|
421
|
+
const directories = this.openDirectories(segments.slice(0, -1), false, true, verifyContext);
|
|
422
|
+
if (!directories)
|
|
423
|
+
return "absent";
|
|
424
|
+
const directory = directories.at(-1)?.absolute ?? this.projectRoot;
|
|
425
|
+
const target = path.join(directory, segments.at(-1));
|
|
426
|
+
const current = readFileIfPresent(target, `exact removal target '${relativeTarget}'`);
|
|
427
|
+
if (!current)
|
|
428
|
+
return "absent";
|
|
429
|
+
if (!matchesPublished(current, expected))
|
|
430
|
+
return "identity_mismatch";
|
|
431
|
+
this.assertBoundary(directories, current, [], verifyContext);
|
|
432
|
+
const confirmed = readStableFile(target, `exact removal target '${relativeTarget}'`);
|
|
433
|
+
if (!sameStableFile(current, confirmed))
|
|
434
|
+
return "identity_mismatch";
|
|
435
|
+
fs.unlinkSync(target);
|
|
436
|
+
syncDirectory(directory);
|
|
437
|
+
this.removeAttemptDirectories();
|
|
438
|
+
return "removed";
|
|
439
|
+
}
|
|
440
|
+
replaceExisting(relativeTarget, expectedBytes, bytes, maxEntityBytes) {
|
|
441
|
+
return this.replaceOwned(relativeTarget, expectedBytes, bytes, maxEntityBytes);
|
|
442
|
+
}
|
|
443
|
+
replaceVisible(relativeTarget, expectedBytes, bytes, maxEntityBytes) {
|
|
444
|
+
return this.replaceOwned(relativeTarget, expectedBytes, bytes, maxEntityBytes);
|
|
445
|
+
}
|
|
446
|
+
restoreExact(relativeTarget, expected, bytes, maxEntityBytes) {
|
|
447
|
+
return this.replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, false);
|
|
448
|
+
}
|
|
449
|
+
restoreVisible(relativeTarget, expected, bytes, maxEntityBytes) {
|
|
450
|
+
return this.replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, false);
|
|
451
|
+
}
|
|
452
|
+
close() {
|
|
453
|
+
this.closed = true;
|
|
454
|
+
}
|
|
455
|
+
replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, verifyContext = true) {
|
|
456
|
+
const segments = safeSegments(relativeTarget, "recoverable replacement target");
|
|
457
|
+
const directories = this.openDirectories(segments.slice(0, -1), false, false, verifyContext);
|
|
458
|
+
if (!directories)
|
|
459
|
+
throw new FileReplacementError(`replacement target '${relativeTarget}' is absent before publication`);
|
|
460
|
+
const directory = directories.at(-1)?.absolute ?? this.projectRoot;
|
|
461
|
+
const target = path.join(directory, segments.at(-1));
|
|
462
|
+
const baseline = readStableFile(target, `replacement target '${relativeTarget}'`);
|
|
463
|
+
if (!matchesExpected(baseline, expected)) {
|
|
464
|
+
throw new ExactReplacementConflictError(`target '${relativeTarget}' ownership or bytes changed before replacement; the detected change was preserved`);
|
|
465
|
+
}
|
|
466
|
+
if (!Number.isSafeInteger(maxEntityBytes) || maxEntityBytes < 1 || baseline.bytes.length > maxEntityBytes) {
|
|
467
|
+
throw new FileReplacementError(`target '${relativeTarget}' exact ${baseline.bytes.length}-byte baseline exceeds the authority-owned ${maxEntityBytes}-byte recovery limit`);
|
|
468
|
+
}
|
|
469
|
+
const after = Buffer.from(bytes);
|
|
470
|
+
let recovery;
|
|
471
|
+
let backup;
|
|
472
|
+
let stage;
|
|
473
|
+
let metadata;
|
|
474
|
+
let committed;
|
|
475
|
+
let renameCompleted = false;
|
|
436
476
|
try {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
477
|
+
recovery = this.createRecoveryAttempt(directory);
|
|
478
|
+
backup = createDurableFile(path.join(recovery.attempt.absolute, "original.previous"), baseline.bytes);
|
|
479
|
+
stage = createDurableFile(path.join(recovery.attempt.absolute, "replacement.tmp"), after, Number(baseline.identity.mode & 4095n));
|
|
480
|
+
metadata = createDurableFile(path.join(recovery.attempt.absolute, FILE_REPLACEMENT_METADATA_NAME), `${JSON.stringify({
|
|
481
|
+
schema_version: FILE_REPLACEMENT_RECOVERY_VERSION,
|
|
482
|
+
target_path: segments.join("/"),
|
|
483
|
+
before_sha256: sha256(baseline.bytes),
|
|
484
|
+
after_sha256: sha256(after),
|
|
485
|
+
})}\n`);
|
|
486
|
+
syncDirectory(recovery.attempt.absolute);
|
|
487
|
+
this.assertBoundary(directories, baseline, [backup, stage, metadata], verifyContext);
|
|
488
|
+
const boundaryTarget = readStableFile(target, `replacement target '${relativeTarget}'`);
|
|
489
|
+
if (!sameStableFile(boundaryTarget, baseline)) {
|
|
490
|
+
throw new ExactReplacementConflictError(`target '${relativeTarget}' changed at the final validation boundary; the detected change was preserved`);
|
|
450
491
|
}
|
|
451
|
-
const directoryFd = directories.at(-1).fd;
|
|
452
|
-
const targetName = segments.at(-1);
|
|
453
492
|
try {
|
|
454
|
-
|
|
493
|
+
fs.renameSync(stage.absolute, target);
|
|
494
|
+
renameCompleted = true;
|
|
455
495
|
}
|
|
456
496
|
catch (error) {
|
|
457
|
-
|
|
458
|
-
|
|
497
|
+
throw new FileReplacementError(`complete-file replacement for '${relativeTarget}' failed before publication${error.code ? ` (${error.code})` : ""}`, error);
|
|
498
|
+
}
|
|
499
|
+
syncDirectory(directory);
|
|
500
|
+
syncDirectory(recovery.attempt.absolute);
|
|
501
|
+
const published = readStableFile(target, `published replacement '${relativeTarget}'`);
|
|
502
|
+
if (!published.bytes.equals(after)) {
|
|
503
|
+
throw new ExactReplacementConflictError(`target '${relativeTarget}' changed through the publication boundary`);
|
|
504
|
+
}
|
|
505
|
+
this.assertBoundary(directories, published, [backup, metadata], verifyContext);
|
|
506
|
+
committed = { previousBytes: baseline.bytes.toString("utf8"), publishedIdentity: publishedIdentity(published) };
|
|
507
|
+
this.cleanupCommittedReplacement(recovery.attempt.absolute, [backup, metadata]);
|
|
508
|
+
backup = undefined;
|
|
509
|
+
metadata = undefined;
|
|
510
|
+
stage = undefined;
|
|
511
|
+
return committed;
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (committed)
|
|
515
|
+
return committed;
|
|
516
|
+
let current = null;
|
|
517
|
+
try {
|
|
518
|
+
current = readFileIfPresent(target, `replacement target '${relativeTarget}'`);
|
|
519
|
+
}
|
|
520
|
+
catch { /* Preserve all recovery evidence. */ }
|
|
521
|
+
if (!renameCompleted && error instanceof ExactReplacementConflictError) {
|
|
522
|
+
const retained = this.cleanupAttemptFiles([stage, backup, metadata]);
|
|
523
|
+
stage = undefined;
|
|
524
|
+
backup = undefined;
|
|
525
|
+
metadata = undefined;
|
|
526
|
+
if (retained.length) {
|
|
527
|
+
throw new ExactReplacementConflictError(`${error.message}; cleanup retained changed recovery roles`, retained);
|
|
528
|
+
}
|
|
459
529
|
throw error;
|
|
460
530
|
}
|
|
461
|
-
if (
|
|
462
|
-
return "identity_mismatch";
|
|
463
|
-
const rollbackName = `.${targetName}.${process.pid}.${randomUUID()}.rollback`;
|
|
464
|
-
fs.renameSync(fdPath(directoryFd, targetName), fdPath(directoryFd, rollbackName));
|
|
465
|
-
if (openMatches(directoryFd, rollbackName, targetFd, FILE_FLAGS)) {
|
|
466
|
-
this.removeOwnedFile(directoryFd, rollbackName, targetFd);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
531
|
+
if (renameCompleted && current?.bytes.equals(after) && backup) {
|
|
469
532
|
try {
|
|
470
|
-
|
|
471
|
-
fs.
|
|
533
|
+
this.assertBoundary(directories, current, [backup], false);
|
|
534
|
+
fs.renameSync(backup.absolute, target);
|
|
535
|
+
backup = undefined;
|
|
536
|
+
syncDirectory(directory);
|
|
537
|
+
const restored = readStableFile(target, `restored replacement target '${relativeTarget}'`);
|
|
538
|
+
if (!restored.bytes.equals(baseline.bytes))
|
|
539
|
+
throw new Error("restored bytes do not match the exact baseline");
|
|
540
|
+
if (stage) {
|
|
541
|
+
try {
|
|
542
|
+
removeExactFile(stage);
|
|
543
|
+
}
|
|
544
|
+
catch { /* Preserve changed stage. */ }
|
|
545
|
+
stage = undefined;
|
|
546
|
+
}
|
|
547
|
+
if (metadata) {
|
|
548
|
+
try {
|
|
549
|
+
removeExactFile(metadata);
|
|
550
|
+
}
|
|
551
|
+
catch { /* Preserve changed metadata. */ }
|
|
552
|
+
metadata = undefined;
|
|
553
|
+
}
|
|
554
|
+
syncDirectoryBestEffort(recovery?.attempt.absolute ?? directory);
|
|
555
|
+
throw new FileReplacementError(`${error.message}; restored exact prior bytes at '${relativeTarget}'`, error);
|
|
472
556
|
}
|
|
473
|
-
catch (
|
|
474
|
-
if (
|
|
475
|
-
throw
|
|
557
|
+
catch (restoreError) {
|
|
558
|
+
if (restoreError instanceof FileReplacementError && restoreError.cause === error)
|
|
559
|
+
throw restoreError;
|
|
476
560
|
}
|
|
477
|
-
syncDirectory(directoryFd);
|
|
478
|
-
return "identity_mismatch";
|
|
479
561
|
}
|
|
480
|
-
|
|
481
|
-
|
|
562
|
+
if (!renameCompleted && current && sameStableFile(current, baseline)) {
|
|
563
|
+
const retained = this.cleanupAttemptFiles([stage, backup, metadata]);
|
|
564
|
+
stage = undefined;
|
|
565
|
+
backup = undefined;
|
|
566
|
+
metadata = undefined;
|
|
567
|
+
if (retained.length) {
|
|
568
|
+
throw new ExactReplacementConflictError(`${error.message}; cleanup retained changed recovery roles`, retained);
|
|
569
|
+
}
|
|
570
|
+
if (error instanceof ExactReplacementConflictError || error instanceof FileReplacementError || error instanceof StateWriteInputError)
|
|
571
|
+
throw error;
|
|
572
|
+
throw new FileReplacementError(`${error.message}; target bytes remained unchanged`, error);
|
|
573
|
+
}
|
|
574
|
+
const retained = [backup, stage, metadata]
|
|
575
|
+
.filter((file) => Boolean(file && fs.existsSync(file.absolute)))
|
|
576
|
+
.map((file) => relativeDisplay(this.projectRoot, file.absolute));
|
|
577
|
+
throw new ExactReplacementConflictError(`${error.message}; recoverable publication did not report success and preserved the canonical target plus ${retained.length} bounded recovery role${retained.length === 1 ? "" : "s"}`, retained);
|
|
482
578
|
}
|
|
483
579
|
finally {
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
for (const entry of directories.reverse())
|
|
487
|
-
fs.closeSync(entry.fd);
|
|
488
|
-
this.removeAttemptDirectories();
|
|
580
|
+
if (recovery)
|
|
581
|
+
this.closeRecoveryAttempt(recovery);
|
|
489
582
|
}
|
|
490
583
|
}
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
584
|
+
openDirectories(segments, create, missingIsAbsent = false, verifyContext = true) {
|
|
585
|
+
const directories = [];
|
|
586
|
+
let parent = this.projectRoot;
|
|
587
|
+
for (const segment of segments) {
|
|
588
|
+
if (verifyContext)
|
|
589
|
+
this.assertValid();
|
|
590
|
+
else
|
|
591
|
+
assertValidatedProjectRoot(this.validatedRoot);
|
|
592
|
+
const absolute = path.join(parent, segment);
|
|
593
|
+
let entry;
|
|
498
594
|
try {
|
|
499
|
-
|
|
500
|
-
const fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
|
|
501
|
-
descriptors.push(fd);
|
|
502
|
-
parentFd = fd;
|
|
503
|
-
}
|
|
504
|
-
targetFd = fs.openSync(fdPath(parentFd, segments.at(-1)), DIRECTORY_FLAGS);
|
|
505
|
-
const expected = this.createdDirectories.get(relativePath);
|
|
506
|
-
const observed = fs.fstatSync(targetFd, { bigint: true });
|
|
507
|
-
if (observed.dev !== expected.dev || observed.ino !== expected.ino) {
|
|
508
|
-
this.createdDirectories.delete(relativePath);
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
fs.rmdirSync(fdPath(parentFd, segments.at(-1)));
|
|
512
|
-
syncDirectory(parentFd);
|
|
513
|
-
this.createdDirectories.delete(relativePath);
|
|
595
|
+
entry = readStableDirectory(absolute);
|
|
514
596
|
}
|
|
515
597
|
catch (error) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
598
|
+
if (error.code === "ENOENT" || error.message.includes("ENOENT")) {
|
|
599
|
+
if (missingIsAbsent)
|
|
600
|
+
return null;
|
|
601
|
+
if (!create)
|
|
602
|
+
throw error;
|
|
603
|
+
this.assertBoundary(directories, undefined, [], verifyContext);
|
|
604
|
+
fs.mkdirSync(absolute, 0o700);
|
|
605
|
+
syncDirectory(parent);
|
|
606
|
+
entry = readStableDirectory(absolute, true);
|
|
607
|
+
}
|
|
608
|
+
else
|
|
520
609
|
throw error;
|
|
521
610
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
fs.closeSync(targetFd);
|
|
525
|
-
for (const fd of descriptors.reverse())
|
|
526
|
-
fs.closeSync(fd);
|
|
527
|
-
}
|
|
611
|
+
directories.push(entry);
|
|
612
|
+
parent = absolute;
|
|
528
613
|
}
|
|
614
|
+
return directories;
|
|
529
615
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
replaceOwned(relativeTarget, expected, bytes, maxEntityBytes, verifyContext = true) {
|
|
538
|
-
const segments = relativeTarget.split(path.sep).filter(Boolean);
|
|
539
|
-
if (segments.length < 2 || path.isAbsolute(relativeTarget) || segments.includes("..") || segments.includes("."))
|
|
540
|
-
throw new Error(`unsafe entity replacement target '${relativeTarget}'`);
|
|
541
|
-
const directories = [];
|
|
542
|
-
let targetFd;
|
|
543
|
-
let stageFd;
|
|
544
|
-
let backupFd;
|
|
545
|
-
let stageName;
|
|
546
|
-
let backupName;
|
|
547
|
-
let displacedName;
|
|
548
|
-
let displacedFd;
|
|
549
|
-
let displacementActive = false;
|
|
550
|
-
let committedResult;
|
|
551
|
-
let recovery;
|
|
552
|
-
let baselineBytes;
|
|
553
|
-
try {
|
|
554
|
-
let parentFd = this.rootFd;
|
|
555
|
-
for (const name of segments.slice(0, -1)) {
|
|
556
|
-
const fd = fs.openSync(fdPath(parentFd, name), DIRECTORY_FLAGS);
|
|
557
|
-
directories.push({ parentFd, name, fd, created: false });
|
|
558
|
-
parentFd = fd;
|
|
559
|
-
}
|
|
560
|
-
const directoryFd = directories.at(-1).fd;
|
|
561
|
-
const targetName = segments.at(-1);
|
|
562
|
-
targetFd = fs.openSync(fdPath(directoryFd, targetName), FILE_FLAGS);
|
|
563
|
-
this.assertBoundary(directories, undefined, { name: targetName, fd: targetFd }, verifyContext);
|
|
564
|
-
const previousBytes = readDescriptor(targetFd);
|
|
565
|
-
baselineBytes = previousBytes;
|
|
566
|
-
if (!matchesExpected(targetFd, expected))
|
|
567
|
-
throw new Error(`entity '${relativeTarget}' ownership changed before replacement; preserve both changes and retry explicitly`);
|
|
568
|
-
if (!Number.isSafeInteger(maxEntityBytes) || maxEntityBytes < 1 || previousBytes.length > maxEntityBytes)
|
|
569
|
-
throw new Error(`entity '${relativeTarget}' exact ${previousBytes.length}-byte baseline exceeds the authority-owned ${maxEntityBytes}-byte recovery limit`);
|
|
570
|
-
recovery = this.createRecoveryAttempt(directoryFd);
|
|
571
|
-
backupName = "original.previous";
|
|
572
|
-
backupFd = fs.openSync(fdPath(recovery.attemptFd, backupName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
573
|
-
fs.writeFileSync(backupFd, previousBytes);
|
|
574
|
-
fs.fsyncSync(backupFd);
|
|
575
|
-
const backupStat = fs.fstatSync(backupFd, { bigint: true });
|
|
576
|
-
const attemptStat = fs.fstatSync(recovery.attemptFd, { bigint: true });
|
|
577
|
-
if (!backupStat.isFile() || backupStat.dev !== attemptStat.dev || backupStat.uid !== attemptStat.uid || backupStat.nlink !== 1n || (backupStat.mode & 63n) !== 0n || !openMatches(recovery.attemptFd, backupName, backupFd, FILE_FLAGS) || !readDescriptor(backupFd).equals(previousBytes))
|
|
578
|
-
throw new Error(`entity '${relativeTarget}' exact prior-byte snapshot could not be verified before replacement`);
|
|
579
|
-
stageName = "replacement.tmp";
|
|
580
|
-
stageFd = fs.openSync(fdPath(recovery.attemptFd, stageName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
581
|
-
fs.writeFileSync(stageFd, bytes, "utf8");
|
|
582
|
-
fs.fsyncSync(stageFd);
|
|
583
|
-
this.assertBoundary(directories, undefined, { name: targetName, fd: targetFd }, verifyContext);
|
|
584
|
-
if (!openMatches(recovery.attemptFd, stageName, stageFd, FILE_FLAGS) || !openMatches(recovery.attemptFd, backupName, backupFd, FILE_FLAGS) || !readDescriptor(backupFd).equals(previousBytes))
|
|
585
|
-
throw new Error(`entity '${relativeTarget}' private recovery attempt changed before displacement`);
|
|
586
|
-
displacedName = "original.displaced";
|
|
587
|
-
fs.renameSync(fdPath(directoryFd, targetName), fdPath(recovery.attemptFd, displacedName));
|
|
588
|
-
displacementActive = true;
|
|
589
|
-
displacedFd = fs.openSync(fdPath(recovery.attemptFd, displacedName), FILE_FLAGS);
|
|
590
|
-
if (!openMatches(recovery.attemptFd, displacedName, targetFd, FILE_FLAGS) || !matchesExpected(displacedFd, expected))
|
|
591
|
-
throw new Error(`entity '${relativeTarget}' ownership changed after staging; competing bytes were preserved at the target`);
|
|
616
|
+
assertBoundary(directories, target, roles = [], verifyContext = true) {
|
|
617
|
+
if (verifyContext)
|
|
618
|
+
this.assertValid();
|
|
619
|
+
else
|
|
620
|
+
assertValidatedProjectRoot(this.validatedRoot);
|
|
621
|
+
for (const directory of directories) {
|
|
622
|
+
let current;
|
|
592
623
|
try {
|
|
593
|
-
fs.
|
|
594
|
-
}
|
|
595
|
-
catch
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
throw new Error(`entity '${relativeTarget}' replacement stage changed before cleanup`);
|
|
602
|
-
stageName = undefined;
|
|
603
|
-
syncDirectory(directoryFd);
|
|
604
|
-
syncDirectory(recovery.attemptFd);
|
|
605
|
-
this.assertBoundary(directories, undefined, { name: targetName, fd: stageFd }, verifyContext);
|
|
606
|
-
if (!openMatches(recovery.attemptFd, displacedName, targetFd, FILE_FLAGS) || !matchesExpected(displacedFd, expected))
|
|
607
|
-
throw new Error(`entity '${relativeTarget}' ownership changed through the publication boundary; competing bytes remain recoverable`);
|
|
608
|
-
committedResult = { previousBytes: previousBytes.toString("utf8"), publishedIdentity: publishedIdentity(stageFd) };
|
|
609
|
-
this.cleanupCommittedReplacement(recovery.attemptFd, [[displacedName, targetFd], [backupName, backupFd]]);
|
|
610
|
-
displacementActive = false;
|
|
611
|
-
return committedResult;
|
|
624
|
+
current = fs.lstatSync(directory.absolute, { bigint: true });
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
throw new ExactReplacementConflictError(`publication directory '${relativeDisplay(this.projectRoot, directory.absolute)}' changed at a validation boundary`);
|
|
628
|
+
}
|
|
629
|
+
if (!sameDirectory(directory, current)) {
|
|
630
|
+
throw new ExactReplacementConflictError(`publication directory '${relativeDisplay(this.projectRoot, directory.absolute)}' changed at a validation boundary`);
|
|
631
|
+
}
|
|
612
632
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
throw this.recoverDisplacedReplacement({ directoryFd, recoveryFd: recovery.attemptFd, recoveryPath: recovery.relativePath, relativeTarget, baselineBytes: baselineBytes, backupFd, stageFd, stageName, backupName, displacedFd, displacedName, retainStage: !verifyContext, primary: error });
|
|
619
|
-
}
|
|
620
|
-
const cleanupFailures = [];
|
|
621
|
-
for (const [name, fd, label] of [[stageName, stageFd, "stage"], [backupName, backupFd, "backup"]])
|
|
622
|
-
if (name && fd !== undefined) {
|
|
623
|
-
try {
|
|
624
|
-
if (!recovery || !this.removeOwnedFile(recovery.attemptFd, name, fd))
|
|
625
|
-
cleanupFailures.push(`${label} '${recovery ? path.join(recovery.relativePath, name) : name}' changed`);
|
|
626
|
-
}
|
|
627
|
-
catch (cleanupError) {
|
|
628
|
-
cleanupFailures.push(`${label} '${recovery ? path.join(recovery.relativePath, name) : name}': ${cleanupError.message}`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (cleanupFailures.length)
|
|
632
|
-
throw new Error(`${error.message}; cleanup retained attempt state: ${cleanupFailures.join("; ")}`, { cause: error });
|
|
633
|
-
throw error;
|
|
633
|
+
if (target) {
|
|
634
|
+
const current = readFileIfPresent(target.absolute, `publication target '${relativeDisplay(this.projectRoot, target.absolute)}'`);
|
|
635
|
+
if (!current || !sameStableFile(target, current)) {
|
|
636
|
+
throw new ExactReplacementConflictError(`publication target '${relativeDisplay(this.projectRoot, target.absolute)}' changed at a validation boundary`);
|
|
637
|
+
}
|
|
634
638
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (stageFd !== undefined)
|
|
641
|
-
fs.closeSync(stageFd);
|
|
642
|
-
if (targetFd !== undefined)
|
|
643
|
-
fs.closeSync(targetFd);
|
|
644
|
-
for (const entry of directories.reverse())
|
|
645
|
-
fs.closeSync(entry.fd);
|
|
646
|
-
if (recovery)
|
|
647
|
-
this.closeRecoveryAttempt(recovery);
|
|
639
|
+
for (const role of roles) {
|
|
640
|
+
const current = readFileIfPresent(role.absolute, `publication role '${relativeDisplay(this.projectRoot, role.absolute)}'`);
|
|
641
|
+
if (!current || !sameStableFile(role, current)) {
|
|
642
|
+
throw new ExactReplacementConflictError(`publication recovery role '${relativeDisplay(this.projectRoot, role.absolute)}' changed at a validation boundary`);
|
|
643
|
+
}
|
|
648
644
|
}
|
|
649
645
|
}
|
|
650
|
-
createRecoveryAttempt(
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
let rootFd;
|
|
654
|
-
let ignoreFd;
|
|
655
|
-
let ignoreCreated = false;
|
|
656
|
-
let attemptFd;
|
|
657
|
-
let attemptName;
|
|
646
|
+
createRecoveryAttempt(targetDirectory) {
|
|
647
|
+
const agentera = readStableDirectory(path.join(this.projectRoot, ".agentera"));
|
|
648
|
+
const rootPath = path.join(agentera.absolute, ".entity-recovery");
|
|
658
649
|
let rootCreated = false;
|
|
650
|
+
let ignoreCreated = false;
|
|
651
|
+
let root;
|
|
652
|
+
let ignore;
|
|
653
|
+
let attempt;
|
|
659
654
|
try {
|
|
660
655
|
try {
|
|
661
|
-
|
|
656
|
+
root = readStableDirectory(rootPath);
|
|
662
657
|
}
|
|
663
658
|
catch (error) {
|
|
664
|
-
if (error.
|
|
665
|
-
throw new
|
|
666
|
-
|
|
659
|
+
if (!error.message.includes("ENOENT")) {
|
|
660
|
+
throw new FileReplacementError(`private entity recovery root '.agentera/.entity-recovery' cannot be opened safely: ${error.message}`, error);
|
|
661
|
+
}
|
|
662
|
+
fs.mkdirSync(rootPath, 0o700);
|
|
667
663
|
rootCreated = true;
|
|
668
|
-
syncDirectory(
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
catch (error) {
|
|
680
|
-
if (error.code !== "ENOENT")
|
|
681
|
-
throw new Error(`private entity recovery ignore marker cannot be opened safely: ${error.message}`, { cause: error });
|
|
682
|
-
ignoreFd = fs.openSync(fdPath(rootFd, ".gitignore"), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
664
|
+
syncDirectory(agentera.absolute);
|
|
665
|
+
root = readStableDirectory(rootPath, true);
|
|
666
|
+
}
|
|
667
|
+
const trustedRoot = validateEntityRecoveryDirectory(this.validatedRoot, root.absolute, targetDirectory, "private entity recovery root '.agentera/.entity-recovery'", root);
|
|
668
|
+
root = { ...trustedRoot, created: rootCreated };
|
|
669
|
+
const ignorePath = path.join(root.absolute, ".gitignore");
|
|
670
|
+
const existingIgnore = readFileIfPresent(ignorePath, "private entity recovery ignore marker");
|
|
671
|
+
if (existingIgnore)
|
|
672
|
+
ignore = existingIgnore;
|
|
673
|
+
else {
|
|
674
|
+
ignore = createDurableFile(ignorePath, RECOVERY_IGNORE_BYTES);
|
|
683
675
|
ignoreCreated = true;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
676
|
+
}
|
|
677
|
+
if (!ignore.bytes.equals(Buffer.from(RECOVERY_IGNORE_BYTES))) {
|
|
678
|
+
throw new FileReplacementError("private entity recovery ignore marker must contain the authoritative rules");
|
|
679
|
+
}
|
|
680
|
+
const ignoreStat = fs.lstatSync(ignore.absolute, { bigint: true });
|
|
681
|
+
if (typeof process.getuid === "function" && ((ignoreStat.mode & 18n) !== 0n || ignoreStat.uid !== BigInt(process.getuid()))) {
|
|
682
|
+
throw new FileReplacementError("private entity recovery ignore marker must be owner-controlled and not group/world-writable");
|
|
683
|
+
}
|
|
684
|
+
syncDirectory(root.absolute);
|
|
685
|
+
for (let index = 0; index < 8; index += 1) {
|
|
686
|
+
const attemptPath = path.join(root.absolute, `entity-${process.pid}-${randomUUID()}`);
|
|
693
687
|
try {
|
|
694
|
-
fs.mkdirSync(
|
|
695
|
-
|
|
688
|
+
fs.mkdirSync(attemptPath, 0o700);
|
|
689
|
+
const opened = readStableDirectory(attemptPath, true);
|
|
690
|
+
attempt = opened;
|
|
691
|
+
const trusted = validateEntityRecoveryDirectory(this.validatedRoot, attemptPath, targetDirectory, `private entity recovery attempt '${relativeDisplay(this.projectRoot, attemptPath)}'`, opened);
|
|
692
|
+
attempt = { ...trusted, created: true };
|
|
696
693
|
break;
|
|
697
694
|
}
|
|
698
695
|
catch (error) {
|
|
@@ -700,263 +697,108 @@ export class EntityPublicationContext {
|
|
|
700
697
|
throw error;
|
|
701
698
|
}
|
|
702
699
|
}
|
|
703
|
-
if (!
|
|
704
|
-
throw new
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if ((attemptStat.mode & 63n) !== 0n || attemptStat.uid !== rootStat.uid || attemptStat.dev !== rootStat.dev)
|
|
708
|
-
throw new Error("private entity recovery attempt must be owner-only, owner-controlled, and on the recovery-root filesystem");
|
|
709
|
-
syncDirectory(rootFd);
|
|
710
|
-
return { agenteraFd, rootFd, attemptFd, ignoreFd, attemptName, relativePath: `.agentera/${rootName}/${attemptName}` };
|
|
700
|
+
if (!attempt)
|
|
701
|
+
throw new FileReplacementError("could not allocate a unique private entity recovery attempt after 8 tries");
|
|
702
|
+
syncDirectory(root.absolute);
|
|
703
|
+
return { root, attempt, ignore, relativePath: relativeDisplay(this.projectRoot, attempt.absolute) };
|
|
711
704
|
}
|
|
712
705
|
catch (error) {
|
|
713
|
-
if (
|
|
714
|
-
fs.closeSync(attemptFd);
|
|
715
|
-
if (rootFd !== undefined && attemptName) {
|
|
706
|
+
if (attempt) {
|
|
716
707
|
try {
|
|
717
|
-
fs.rmdirSync(
|
|
708
|
+
fs.rmdirSync(attempt.absolute);
|
|
718
709
|
}
|
|
719
|
-
catch { /*
|
|
710
|
+
catch { /* Preserve unexpected contents. */ }
|
|
720
711
|
}
|
|
721
|
-
if (
|
|
712
|
+
if (ignore && ignoreCreated) {
|
|
722
713
|
try {
|
|
723
|
-
|
|
714
|
+
removeExactFile(ignore);
|
|
724
715
|
}
|
|
725
|
-
catch { /*
|
|
716
|
+
catch { /* Preserve changed marker. */ }
|
|
726
717
|
}
|
|
727
|
-
if (
|
|
728
|
-
fs.closeSync(ignoreFd);
|
|
729
|
-
if (rootFd !== undefined)
|
|
730
|
-
fs.closeSync(rootFd);
|
|
731
|
-
if (rootCreated) {
|
|
718
|
+
if (root && rootCreated) {
|
|
732
719
|
try {
|
|
733
|
-
fs.rmdirSync(
|
|
720
|
+
fs.rmdirSync(root.absolute);
|
|
734
721
|
}
|
|
735
|
-
catch { /*
|
|
722
|
+
catch { /* Preserve non-empty root. */ }
|
|
736
723
|
}
|
|
737
|
-
fs.closeSync(agenteraFd);
|
|
738
724
|
throw error;
|
|
739
725
|
}
|
|
740
726
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
const remaining = fs.readdirSync(fdPath(recovery.rootFd));
|
|
751
|
-
if (remaining.length === 1 && remaining[0] === ".gitignore" && openMatches(recovery.rootFd, ".gitignore", recovery.ignoreFd, FILE_FLAGS)) {
|
|
752
|
-
try {
|
|
753
|
-
this.removeOwnedFile(recovery.rootFd, ".gitignore", recovery.ignoreFd);
|
|
754
|
-
syncDirectory(recovery.rootFd);
|
|
755
|
-
}
|
|
756
|
-
catch { /* preserve marker on cleanup failure */ }
|
|
757
|
-
}
|
|
758
|
-
if (openMatches(recovery.agenteraFd, ".entity-recovery", recovery.rootFd, DIRECTORY_FLAGS)) {
|
|
759
|
-
try {
|
|
760
|
-
fs.rmdirSync(fdPath(recovery.agenteraFd, ".entity-recovery"));
|
|
761
|
-
syncDirectory(recovery.agenteraFd);
|
|
762
|
-
}
|
|
763
|
-
catch { /* best-effort private cleanup retains non-empty or changed root */ }
|
|
727
|
+
cleanupAttemptFiles(files) {
|
|
728
|
+
const retained = [];
|
|
729
|
+
for (const file of files) {
|
|
730
|
+
if (!file)
|
|
731
|
+
continue;
|
|
732
|
+
try {
|
|
733
|
+
if (!removeExactFile(file) && fs.existsSync(file.absolute))
|
|
734
|
+
retained.push(relativeDisplay(this.projectRoot, file.absolute));
|
|
764
735
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
/* committed or already-recovered state must not be reclassified by private cleanup */
|
|
768
|
-
}
|
|
769
|
-
finally {
|
|
770
|
-
for (const fd of [recovery.attemptFd, recovery.ignoreFd, recovery.rootFd, recovery.agenteraFd]) {
|
|
771
|
-
try {
|
|
772
|
-
fs.closeSync(fd);
|
|
773
|
-
}
|
|
774
|
-
catch { /* best-effort descriptor close */ }
|
|
736
|
+
catch {
|
|
737
|
+
retained.push(relativeDisplay(this.projectRoot, file.absolute));
|
|
775
738
|
}
|
|
776
739
|
}
|
|
740
|
+
return retained;
|
|
777
741
|
}
|
|
778
|
-
cleanupCommittedReplacement(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
this.removeOwnedFile(directoryFd, name, fd);
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
catch { /* committed publication remains authoritative; retry once and otherwise retain recovery link */ }
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
789
|
-
try {
|
|
790
|
-
syncDirectory(directoryFd);
|
|
791
|
-
break;
|
|
792
|
-
}
|
|
793
|
-
catch { /* cleanup durability is best-effort after the durable commit boundary */ }
|
|
794
|
-
}
|
|
742
|
+
cleanupCommittedReplacement(directory, files) {
|
|
743
|
+
this.cleanupAttemptFiles(files);
|
|
744
|
+
syncDirectoryBestEffort(directory);
|
|
795
745
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const retained = new Set();
|
|
801
|
-
let { stageName, backupName, displacedName } = state;
|
|
802
|
-
const publicPath = (name) => path.join(recoveryPath, name).split(path.sep).join("/");
|
|
803
|
-
const exactBaseline = () => Boolean(backupName && backupFd !== undefined && openMatches(recoveryFd, backupName, backupFd, FILE_FLAGS) && readDescriptor(backupFd).equals(baselineBytes));
|
|
804
|
-
const exactDisplaced = () => Boolean(displacedName && displacedFd !== undefined && openMatches(recoveryFd, displacedName, displacedFd, FILE_FLAGS));
|
|
805
|
-
const exactStage = () => Boolean(stageName && stageFd !== undefined && openMatches(recoveryFd, stageName, stageFd, FILE_FLAGS));
|
|
806
|
-
const remove = (name, fd, label) => {
|
|
807
|
-
try {
|
|
808
|
-
if (this.removeOwnedFile(recoveryFd, name, fd))
|
|
809
|
-
return true;
|
|
810
|
-
failures.push(`${label} '${publicPath(name)}' changed before cleanup`);
|
|
811
|
-
}
|
|
812
|
-
catch (error) {
|
|
813
|
-
failures.push(`${label} '${publicPath(name)}' cleanup failed: ${error.message}`);
|
|
814
|
-
}
|
|
815
|
-
retained.add(publicPath(name));
|
|
816
|
-
return false;
|
|
817
|
-
};
|
|
818
|
-
const sync = (label) => { try {
|
|
819
|
-
syncDirectory(directoryFd);
|
|
820
|
-
syncDirectory(recoveryFd);
|
|
821
|
-
return true;
|
|
746
|
+
closeRecoveryAttempt(recovery) {
|
|
747
|
+
try {
|
|
748
|
+
fs.rmdirSync(recovery.attempt.absolute);
|
|
749
|
+
syncDirectoryBestEffort(recovery.root.absolute);
|
|
822
750
|
}
|
|
823
|
-
catch
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (stageFd !== undefined && openMatches(directoryFd, targetName, stageFd, FILE_FLAGS) && source && sourceFd !== undefined) {
|
|
832
|
-
try {
|
|
833
|
-
if (this.removeOwnedFile(directoryFd, targetName, stageFd))
|
|
834
|
-
sync("replacement removal");
|
|
835
|
-
else
|
|
836
|
-
failures.push(`published replacement '${relativeTarget}' changed before cleanup`);
|
|
751
|
+
catch {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const names = fs.readdirSync(recovery.root.absolute);
|
|
756
|
+
if (names.length === 1 && names[0] === ".gitignore") {
|
|
757
|
+
removeExactFile(recovery.ignore);
|
|
758
|
+
syncDirectoryBestEffort(recovery.root.absolute);
|
|
837
759
|
}
|
|
838
|
-
|
|
839
|
-
|
|
760
|
+
if (fs.readdirSync(recovery.root.absolute).length === 0) {
|
|
761
|
+
fs.rmdirSync(recovery.root.absolute);
|
|
762
|
+
syncDirectoryBestEffort(path.dirname(recovery.root.absolute));
|
|
840
763
|
}
|
|
841
764
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
765
|
+
catch { /* Preserve non-empty or changed recovery authority. */ }
|
|
766
|
+
}
|
|
767
|
+
removeCreatedDirectories(directories) {
|
|
768
|
+
for (const directory of [...directories].reverse()) {
|
|
769
|
+
if (!directory.created)
|
|
770
|
+
continue;
|
|
846
771
|
try {
|
|
847
|
-
fs.
|
|
772
|
+
const current = fs.lstatSync(directory.absolute, { bigint: true });
|
|
773
|
+
if (!sameDirectory(directory, current))
|
|
774
|
+
continue;
|
|
775
|
+
fs.rmdirSync(directory.absolute);
|
|
776
|
+
syncDirectoryBestEffort(path.dirname(directory.absolute));
|
|
848
777
|
}
|
|
849
778
|
catch (error) {
|
|
850
|
-
if (error.code
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
restoredBaseline = pathBytesEqual(directoryFd, targetName, baselineBytes);
|
|
854
|
-
restoredCompetitor = Boolean(displacedCompetitor && displacedFd !== undefined && openMatches(directoryFd, targetName, displacedFd, FILE_FLAGS));
|
|
855
|
-
occupied = !restoredBaseline && !restoredCompetitor && entryExists(directoryFd, targetName);
|
|
856
|
-
}
|
|
857
|
-
if (restoredBaseline || restoredCompetitor) {
|
|
858
|
-
const durable = sync(restoredBaseline ? "exact baseline restoration" : "competitor restoration");
|
|
859
|
-
if (durable) {
|
|
860
|
-
if (retainStage && exactStage())
|
|
861
|
-
retained.add(publicPath(stageName));
|
|
862
|
-
else if (exactStage() && remove(stageName, stageFd, "replacement stage"))
|
|
863
|
-
stageName = undefined;
|
|
864
|
-
if (restoredCompetitor) {
|
|
865
|
-
if (exactBaseline())
|
|
866
|
-
retained.add(publicPath(backupName));
|
|
867
|
-
else
|
|
868
|
-
failures.push("exact baseline snapshot is unavailable");
|
|
869
|
-
if (exactDisplaced() && remove(displacedName, displacedFd, "canonicalized competitor duplicate"))
|
|
870
|
-
displacedName = undefined;
|
|
871
|
-
}
|
|
872
|
-
else {
|
|
873
|
-
if (displacedCompetitor && exactDisplaced())
|
|
874
|
-
retained.add(publicPath(displacedName));
|
|
875
|
-
else if (exactDisplaced() && remove(displacedName, displacedFd, "displaced baseline duplicate"))
|
|
876
|
-
displacedName = undefined;
|
|
877
|
-
if (exactBaseline() && remove(backupName, backupFd, "baseline snapshot duplicate"))
|
|
878
|
-
backupName = undefined;
|
|
879
|
-
}
|
|
880
|
-
sync("restoration cleanup");
|
|
881
|
-
}
|
|
882
|
-
for (const name of [backupName, displacedName, stageName])
|
|
883
|
-
if (name && entryExists(recoveryFd, name))
|
|
884
|
-
retained.add(publicPath(name));
|
|
885
|
-
const outcome = restoredCompetitor
|
|
886
|
-
? `recovery restored concurrent competitor bytes at '${relativeTarget}' and retained the exact discovered baseline snapshot at ${exactBaseline() ? `'${publicPath(backupName)}'` : "an unavailable path"}`
|
|
887
|
-
: `recovery restored byte-verified exact prior bytes at '${relativeTarget}'`;
|
|
888
|
-
return new Error(`${primary.message}; ${outcome}${retained.size ? `; retained recovery files: ${[...retained].join(", ")}` : ""}${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
|
|
889
|
-
}
|
|
890
|
-
if (occupied) {
|
|
891
|
-
if (exactBaseline())
|
|
892
|
-
retained.add(publicPath(backupName));
|
|
893
|
-
else
|
|
894
|
-
failures.push("exact baseline snapshot is unavailable");
|
|
895
|
-
if (displacedCompetitor && exactDisplaced())
|
|
896
|
-
retained.add(publicPath(displacedName));
|
|
897
|
-
if (sync("competing-target retention")) {
|
|
898
|
-
if (retainStage && exactStage())
|
|
899
|
-
retained.add(publicPath(stageName));
|
|
900
|
-
else if (exactStage() && remove(stageName, stageFd, "replacement stage"))
|
|
901
|
-
stageName = undefined;
|
|
902
|
-
if (!displacedCompetitor && exactDisplaced() && remove(displacedName, displacedFd, "displaced baseline duplicate"))
|
|
903
|
-
displacedName = undefined;
|
|
904
|
-
sync("retention cleanup");
|
|
905
|
-
}
|
|
906
|
-
for (const name of [backupName, displacedName, stageName])
|
|
907
|
-
if (name && entryExists(recoveryFd, name))
|
|
908
|
-
retained.add(publicPath(name));
|
|
909
|
-
return new Error(`${primary.message}; recovery preserved the competing canonical target and retained the exact discovered baseline snapshot and any displaced competitor at ${[...retained].join(", ") || "unavailable paths"}; preserve each role and resolve ownership explicitly${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
|
|
910
|
-
}
|
|
911
|
-
if (exactBaseline())
|
|
912
|
-
retained.add(publicPath(backupName));
|
|
913
|
-
if (exactDisplaced())
|
|
914
|
-
retained.add(publicPath(displacedName));
|
|
915
|
-
if (exactStage())
|
|
916
|
-
retained.add(publicPath(stageName));
|
|
917
|
-
return new Error(`${primary.message}; recovery could not restore the absent canonical target; retained role-labeled recovery files: ${[...retained].join(", ") || "none"}; restore the exact baseline or competitor explicitly before retrying${failures.length ? `; recovery issues: ${failures.join("; ")}` : ""}`, { cause: primary });
|
|
918
|
-
}
|
|
919
|
-
close() {
|
|
920
|
-
if (this.closed)
|
|
921
|
-
return;
|
|
922
|
-
this.closed = true;
|
|
923
|
-
fs.closeSync(this.markerFd);
|
|
924
|
-
for (const entry of this.markerParentEntries.reverse())
|
|
925
|
-
fs.closeSync(entry.fd);
|
|
926
|
-
fs.closeSync(this.rootFd);
|
|
927
|
-
}
|
|
928
|
-
assertBoundary(directories, stage, target, verifyContext = true) {
|
|
929
|
-
if (verifyContext)
|
|
930
|
-
this.assertValid();
|
|
931
|
-
for (const entry of directories) {
|
|
932
|
-
if (!openMatches(entry.parentFd, entry.name, entry.fd, DIRECTORY_FLAGS)) {
|
|
933
|
-
throw new Error(`entity publication directory '${entry.name}' changed during publication`);
|
|
779
|
+
if (!["ENOTEMPTY", "EEXIST", "ENOENT"].includes(error.code ?? ""))
|
|
780
|
+
throw error;
|
|
934
781
|
}
|
|
935
782
|
}
|
|
936
|
-
const directoryFd = directories.at(-1)?.fd;
|
|
937
|
-
if (directoryFd !== undefined && stage && !openMatches(directoryFd, stage.name, stage.fd, FILE_FLAGS)) {
|
|
938
|
-
throw new Error("entity publication stage changed during publication");
|
|
939
|
-
}
|
|
940
|
-
if (directoryFd !== undefined && target && !openMatches(directoryFd, target.name, target.fd, FILE_FLAGS)) {
|
|
941
|
-
throw new Error("entity publication target changed during publication");
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
removeOwnedFile(parentFd, name, expectedFd) {
|
|
945
|
-
if (!openMatches(parentFd, name, expectedFd, FILE_FLAGS))
|
|
946
|
-
return false;
|
|
947
|
-
fs.unlinkSync(fdPath(parentFd, name));
|
|
948
|
-
return true;
|
|
949
783
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
continue;
|
|
784
|
+
removeAttemptDirectories() {
|
|
785
|
+
const paths = [...this.createdDirectories].sort((left, right) => right[0].split("/").length - left[0].split("/").length);
|
|
786
|
+
for (const [relativePath, expected] of paths) {
|
|
954
787
|
try {
|
|
955
|
-
fs.
|
|
956
|
-
|
|
788
|
+
const current = fs.lstatSync(expected.absolute, { bigint: true });
|
|
789
|
+
if (!sameDirectory(expected, current)) {
|
|
790
|
+
this.createdDirectories.delete(relativePath);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
fs.rmdirSync(expected.absolute);
|
|
794
|
+
syncDirectoryBestEffort(path.dirname(expected.absolute));
|
|
795
|
+
this.createdDirectories.delete(relativePath);
|
|
957
796
|
}
|
|
958
797
|
catch (error) {
|
|
959
|
-
|
|
798
|
+
const code = error.code ?? "";
|
|
799
|
+
if (code === "ENOENT")
|
|
800
|
+
this.createdDirectories.delete(relativePath);
|
|
801
|
+
else if (!["ENOTEMPTY", "EEXIST"].includes(code))
|
|
960
802
|
throw error;
|
|
961
803
|
}
|
|
962
804
|
}
|