@cassiomc1/forgeloop 1.14.0 → 2.0.0-rc.4
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/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +2 -1
- package/ENG/c-development-eng.md +2 -2
- package/ENG/cpp-development-eng.md +2 -2
- package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
- package/ENG/flutter-development-eng.md +4 -2
- package/ENG/go-development-eng.md +2 -2
- package/ENG/java-development-eng.md +3 -3
- package/ENG/nodejs-backend-development-eng.md +8 -4
- package/ENG/php-development-eng.md +3 -3
- package/ENG/python-development-eng.md +205 -0
- package/ENG/rust-development-eng.md +42 -1
- package/ENG/sql-development-eng.md +4 -4
- package/ENG/swift-development-eng.md +5 -4
- package/ENG/test-code-eng.md +1 -1
- package/ENG/typescript-development-eng.md +4 -4
- package/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/README.md +30 -40
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/completions/_forgeloop +11 -3
- package/completions/forgeloop.bash +19 -3
- package/completions/forgeloop.fish +61 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
- package/docs/ARTIFACT_REFERENCE.md +50 -37
- package/docs/CLI_REFERENCE.md +147 -3
- package/docs/DOCUMENTATION_GUIDE.md +6 -4
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/PACKAGE_CONTENTS.md +14 -13
- package/docs/RELEASE_CHECKLIST.md +110 -10
- package/docs/SELF_HOSTED_TESTS.md +121 -0
- package/docs/TROUBLESHOOTING.md +33 -0
- package/docs/documentation-manifest.json +25 -12
- package/package.json +15 -10
- package/schemas/task-bundle.schema.json +7 -2
- package/scripts/generate-agent-protocol-summary.mjs +2 -2
- package/scripts/run-mcp-clean-tests.mjs +30 -0
- package/src/adapters/agent-browser/provider.js +14 -11
- package/src/adapters/emulated-services/provider.js +11 -4
- package/src/adapters/temporary-paths.js +18 -0
- package/src/cli.js +146 -210
- package/src/commands/action-show.js +5 -2
- package/src/commands/advance.js +5 -1
- package/src/commands/baseline.js +13 -11
- package/src/commands/checkpoint-revalidate.js +24 -8
- package/src/commands/complete.js +19 -3
- package/src/commands/contract-create.js +3 -1
- package/src/commands/doctor.js +36 -13
- package/src/commands/gate-record.js +20 -31
- package/src/commands/gate-revalidate.js +11 -13
- package/src/commands/handoff-list.js +6 -1
- package/src/commands/handoff-show.js +6 -1
- package/src/commands/migrate-protocol.js +2 -2
- package/src/commands/model-route.js +9 -1
- package/src/commands/next.js +7 -1
- package/src/commands/policy-diff.js +25 -4
- package/src/commands/progress.js +17 -11
- package/src/commands/record-diagnosis.js +3 -0
- package/src/commands/semantic-plan.js +26 -2
- package/src/commands/status.js +6 -1
- package/src/commands/storage-backup.js +15 -0
- package/src/commands/storage-migrate.js +5 -0
- package/src/commands/storage-migration-resume.js +5 -0
- package/src/commands/storage-migration-status.js +44 -0
- package/src/commands/storage-restore-resume.js +7 -0
- package/src/commands/storage-restore.js +8 -0
- package/src/commands/task-abandon.js +1 -1
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- package/src/commands/validate-protocol.js +29 -14
- package/src/commands/validate-receipt.js +8 -1
- package/src/config/guides.json +4 -0
- package/src/core/action-ledger-projection.js +10 -3
- package/src/core/actions.js +38 -75
- package/src/core/activation.js +11 -3
- package/src/core/approvals.js +17 -51
- package/src/core/artifact-registry.js +59 -2
- package/src/core/artifacts.js +81 -27
- package/src/core/attachment-paths.js +45 -0
- package/src/core/attestation-coverage.js +6 -1
- package/src/core/attestation-verifier.js +21 -2
- package/src/core/attestation.js +17 -6
- package/src/core/audit.js +6 -1
- package/src/core/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +12 -10
- package/src/core/cli-command-definitions.js +109 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +182 -126
- package/src/core/command-input.js +21 -1
- package/src/core/completion-artifacts.js +85 -36
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +60 -9
- package/src/core/conformance.js +2 -1
- package/src/core/continuity-lint.js +13 -2
- package/src/core/continuity-reconciliation.js +16 -3
- package/src/core/continuity.js +16 -8
- package/src/core/contract-bootstrap-recovery.js +42 -41
- package/src/core/contract-revision.js +8 -9
- package/src/core/decision/artifact.js +2 -1
- package/src/core/decision/events.js +9 -6
- package/src/core/decision/resolver.js +19 -11
- package/src/core/decision/service.js +63 -38
- package/src/core/decision/task-bindings.js +6 -1
- package/src/core/diagnosis-model.js +11 -0
- package/src/core/diagnosis.js +10 -1
- package/src/core/diagnostic-projection.js +29 -12
- package/src/core/diagnostic-record.js +28 -3
- package/src/core/efficiency.js +6 -1
- package/src/core/events.js +316 -97
- package/src/core/execution-prerequisites.js +22 -13
- package/src/core/execution.js +26 -40
- package/src/core/filesystem.js +11 -0
- package/src/core/gate-artifact.js +105 -19
- package/src/core/gate-provenance.js +6 -5
- package/src/core/handoff.js +26 -14
- package/src/core/history.js +39 -35
- package/src/core/hypothesis-projection.js +32 -0
- package/src/core/information-gain-projection.js +104 -100
- package/src/core/inspect.js +12 -3
- package/src/core/integration-invocation-policy.js +10 -0
- package/src/core/integration-resources.js +41 -34
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -0
- package/src/core/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-phases.js +1 -1
- package/src/core/next-action-review-phase.js +2 -2
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +21 -2
- package/src/core/phase.js +138 -93
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +72 -59
- package/src/core/preflight-loaders.js +19 -12
- package/src/core/preflight-model.js +6 -1
- package/src/core/preflight.js +29 -12
- package/src/core/progress.js +21 -10
- package/src/core/protocol-info.js +3 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/reconcile-closure.js +162 -24
- package/src/core/recovery-history.js +66 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/route-artifact.js +13 -3
- package/src/core/runtime-context.js +5 -0
- package/src/core/schema-validation.js +8 -3
- package/src/core/settlement.js +12 -1
- package/src/core/signing/bundle-file.js +53 -0
- package/src/core/signing/bundle-reference.js +15 -0
- package/src/core/signing/sigstore.js +60 -33
- package/src/core/signing/statement-file.js +27 -0
- package/src/core/storage-runtime-registry.js +41 -0
- package/src/core/structural-quality/artifacts.js +20 -14
- package/src/core/structural-quality/service.js +81 -56
- package/src/core/target-layout.js +1 -1
- package/src/core/task-claim-state.js +52 -18
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +112 -107
- package/src/core/task-descriptor.js +29 -22
- package/src/core/task-discovery.js +288 -90
- package/src/core/task-lock.js +66 -306
- package/src/core/task-migration-validation.js +5 -5
- package/src/core/task-migration.js +17 -287
- package/src/core/task-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +20 -8
- package/src/core/test-intelligence/prune.js +5 -1
- package/src/core/trace.js +154 -99
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -288
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- package/src/repository-index/binary-manager.js +15 -7
- package/src/storage/artifact-bytes.js +22 -0
- package/src/storage/attachment-binding.js +11 -0
- package/src/storage/attachment-files.js +82 -0
- package/src/storage/attachment-references.js +36 -0
- package/src/storage/backup.js +53 -0
- package/src/storage/capabilities.js +31 -0
- package/src/storage/connection-owner.js +119 -0
- package/src/storage/connection.js +376 -0
- package/src/storage/existing-project-scope.js +45 -0
- package/src/storage/export-destination.js +44 -0
- package/src/storage/exporter.js +284 -0
- package/src/storage/file-durability.js +46 -0
- package/src/storage/importer.js +461 -0
- package/src/storage/index.js +74 -0
- package/src/storage/leases.js +63 -0
- package/src/storage/ledger-event-snapshot.js +150 -0
- package/src/storage/ledger-relations.js +220 -0
- package/src/storage/legacy-maintenance-boundary.js +18 -0
- package/src/storage/legacy-signature.js +115 -0
- package/src/storage/maintenance-handoff.js +129 -0
- package/src/storage/maintenance-owner.js +42 -0
- package/src/storage/maintenance.js +310 -0
- package/src/storage/metadata-json.js +35 -0
- package/src/storage/migration-activate.js +59 -0
- package/src/storage/migration-archive.js +57 -0
- package/src/storage/migration-attachment-root.js +61 -0
- package/src/storage/migration-candidate.js +398 -0
- package/src/storage/migration-publication.js +143 -0
- package/src/storage/migration-rollback-admission.js +54 -0
- package/src/storage/migration-rollback-preparation.js +132 -0
- package/src/storage/migration-rollback-publication.js +183 -0
- package/src/storage/migration-rollback-release.js +66 -0
- package/src/storage/migration-rollback-source-stage.js +103 -0
- package/src/storage/migration-rollback.js +43 -0
- package/src/storage/migration-source-partition.js +56 -0
- package/src/storage/migration-source.js +255 -0
- package/src/storage/migration-terminal.js +39 -0
- package/src/storage/migration-validation.js +75 -0
- package/src/storage/migration.js +43 -0
- package/src/storage/operational-attachments.js +49 -0
- package/src/storage/operational-context.js +50 -0
- package/src/storage/project-backup.js +98 -0
- package/src/storage/project-bootstrap.js +24 -0
- package/src/storage/project-boundary.js +79 -0
- package/src/storage/project-read-snapshot.js +18 -0
- package/src/storage/project-replacement.js +137 -0
- package/src/storage/project-restore.js +203 -0
- package/src/storage/record-definitions.js +45 -0
- package/src/storage/repository.js +507 -0
- package/src/storage/restore-execution-provenance.js +26 -0
- package/src/storage/restore-journal.js +62 -0
- package/src/storage/restore-layout.js +30 -0
- package/src/storage/restore-preparation.js +84 -0
- package/src/storage/restore-replacement-archive.js +162 -0
- package/src/storage/restore-replacement-owner.js +31 -0
- package/src/storage/restore-replacement-preparation.js +128 -0
- package/src/storage/restore-replacement.js +142 -0
- package/src/storage/restore-terminal.js +32 -0
- package/src/storage/runtime.js +31 -0
- package/src/storage/schema.js +214 -0
- package/src/storage/singleton-import.js +84 -0
- package/src/storage/snapshot-continuity.js +16 -0
- package/src/storage/snapshot.js +36 -0
- package/src/storage/storage-marker.js +79 -0
- package/src/storage/task-guards.js +107 -0
- package/src/storage/topology.js +42 -0
- package/src/storage/transaction.js +179 -0
- package/src/storage/unit-of-work.js +667 -0
- package/src/storage/windows-process-incarnation.js +206 -0
- package/.forgeloop/forgeloop.gitignore +0 -4
- package/LICENSE-DOCS.md +0 -13
- package/src/core/transaction-maintenance.js +0 -70
|
@@ -181,11 +181,18 @@ export function validateArchiveEntry(entry) {
|
|
|
181
181
|
return normalized;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
export function resolveArchiveTool(archive, { platform = process.platform, env = process.env } = {}) {
|
|
185
|
+
// Windows ships a ZIP-capable bsdtar. Git's GNU tar can shadow it on PATH.
|
|
186
|
+
if (platform === "win32") return path.win32.join(env.SystemRoot || "C:\\Windows", "System32", "tar.exe");
|
|
187
|
+
return archive === "zip" ? "unzip" : "tar";
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
|
|
185
|
-
const listCommand = archive
|
|
186
|
-
const
|
|
191
|
+
const listCommand = resolveArchiveTool(archive);
|
|
192
|
+
const archiveName = path.basename(archivePath);
|
|
193
|
+
const listArgs = listCommand === "unzip" ? ["-Z1", archiveName] : ["-tf", archiveName];
|
|
187
194
|
try {
|
|
188
|
-
const listed = await execFileAsync(execFileImpl, listCommand, listArgs, { maxBuffer: 2 * 1024 * 1024 });
|
|
195
|
+
const listed = await execFileAsync(execFileImpl, listCommand, listArgs, { cwd: path.dirname(archivePath), maxBuffer: 2 * 1024 * 1024 });
|
|
189
196
|
return listed.stdout.split(/\r?\n/).filter(Boolean).map(validateArchiveEntry);
|
|
190
197
|
} catch (cause) {
|
|
191
198
|
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.ENGINE_EXTRACTION_FAILED, `Unable to inspect tgrep archive: ${cause.message}`, { cause });
|
|
@@ -194,12 +201,13 @@ async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImp
|
|
|
194
201
|
|
|
195
202
|
async function extractArchive(archivePath, extractionDirectory, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
|
|
196
203
|
await listArchiveEntries(archivePath, { archive, execFileImpl });
|
|
197
|
-
const extractCommand = archive
|
|
204
|
+
const extractCommand = resolveArchiveTool(archive);
|
|
205
|
+
const destination = path.relative(path.dirname(archivePath), extractionDirectory).replaceAll("\\", "/") || ".";
|
|
198
206
|
const extractArgs = extractCommand === "unzip"
|
|
199
|
-
? ["-q", archivePath, "-d",
|
|
200
|
-
: ["-xf", archivePath, "-C",
|
|
207
|
+
? ["-q", path.basename(archivePath), "-d", destination]
|
|
208
|
+
: ["-xf", path.basename(archivePath), "-C", destination];
|
|
201
209
|
try {
|
|
202
|
-
await execFileAsync(execFileImpl, extractCommand, extractArgs, { maxBuffer: 2 * 1024 * 1024 });
|
|
210
|
+
await execFileAsync(execFileImpl, extractCommand, extractArgs, { cwd: path.dirname(archivePath), maxBuffer: 2 * 1024 * 1024 });
|
|
203
211
|
} catch (cause) {
|
|
204
212
|
throw repositoryIndexError(REPOSITORY_INDEX_ERROR_CODES.ENGINE_EXTRACTION_FAILED, `Unable to extract tgrep archive: ${cause.message}`, { cause });
|
|
205
213
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
3
|
+
|
|
4
|
+
/** Validate byte evidence separately from the domain payload schema. */
|
|
5
|
+
export function artifactByteDigest(record) {
|
|
6
|
+
if (typeof record?.sourceText !== "string" || typeof record?.byteDigest !== "string") {
|
|
7
|
+
const error = new Error("Original artifact byte evidence is unavailable");
|
|
8
|
+
error.code = "E_STORAGE_ARTIFACT_BYTES_UNAVAILABLE";
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const digest = createHash("sha256").update(record.sourceText).digest("hex");
|
|
13
|
+
if (digest !== record.byteDigest || canonicalFingerprint(JSON.parse(record.sourceText)) !== canonicalFingerprint(record.payload)) {
|
|
14
|
+
throw new Error("Artifact source bytes, digest and canonical payload disagree");
|
|
15
|
+
}
|
|
16
|
+
return digest;
|
|
17
|
+
} catch (cause) {
|
|
18
|
+
const error = new Error("Stored artifact byte evidence is invalid", { cause });
|
|
19
|
+
error.code = "E_STORAGE_ARTIFACT_INVALID";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Pure canonical binding validation, independent of files and SQLite. */
|
|
2
|
+
export function validateAttachmentReference(reference) {
|
|
3
|
+
if (!reference || typeof reference.taskId !== "string" || !reference.taskId
|
|
4
|
+
|| typeof reference.referenceId !== "string" || !reference.referenceId || reference.referenceId.length > 256
|
|
5
|
+
|| !Number.isSafeInteger(reference.size) || reference.size < 0
|
|
6
|
+
|| !/^[a-f0-9]{64}$/.test(reference.sha256 ?? "")
|
|
7
|
+
|| reference.path !== `.forgeloop/attachments/objects/${reference.sha256}`) {
|
|
8
|
+
throw Object.assign(new Error("Invalid canonical attachment reference"), { code: "E_STORAGE_ATTACHMENT_INVALID" });
|
|
9
|
+
}
|
|
10
|
+
return reference;
|
|
11
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants, createWriteStream } from "node:fs";
|
|
3
|
+
import { link, lstat, mkdir, mkdtemp, open, rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { Transform } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import { digestFile, syncDirectory } from "./file-durability.js";
|
|
9
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
10
|
+
import { assertWritableContext } from "./transaction.js";
|
|
11
|
+
|
|
12
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_ATTACHMENT_INVALID" }); }
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/** Verify referenced immutable bytes without loading the attachment into memory. */
|
|
16
|
+
async function attachmentFilename(target, reference) {
|
|
17
|
+
if (!reference || !Number.isSafeInteger(reference.size) || reference.size < 0
|
|
18
|
+
|| !/^[a-f0-9]{64}$/.test(reference.sha256 ?? "")
|
|
19
|
+
|| reference.path !== `.forgeloop/attachments/objects/${reference.sha256}`) throw invalid("Invalid attachment reference");
|
|
20
|
+
const filename = await assertSafePath(target, reference.path);
|
|
21
|
+
if (!(await lstat(filename)).isFile()) throw invalid("Attachment reference must name a regular file");
|
|
22
|
+
return filename;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function verifyAttachmentFile(target, reference) {
|
|
26
|
+
const filename = await attachmentFilename(target, reference);
|
|
27
|
+
const actual = await digestFile(filename, { includeSize: true });
|
|
28
|
+
if (actual.size !== reference.size || actual.sha256 !== reference.sha256) throw invalid("Attachment bytes disagree with their reference");
|
|
29
|
+
return reference;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Give consumers private verified bytes, closing the source check/use window. */
|
|
33
|
+
export async function withVerifiedAttachmentFile(target, reference, callback) {
|
|
34
|
+
const source = await attachmentFilename(target, reference);
|
|
35
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "forgeloop-attachment-read-"));
|
|
36
|
+
try {
|
|
37
|
+
const filename = path.join(directory, "bytes");
|
|
38
|
+
const handle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
39
|
+
try {
|
|
40
|
+
if (!(await handle.stat()).isFile()) throw invalid("Attachment must remain a regular file");
|
|
41
|
+
const hash = createHash("sha256");
|
|
42
|
+
let size = 0;
|
|
43
|
+
const check = new Transform({ transform(bytes, _encoding, done) {
|
|
44
|
+
size += bytes.length;
|
|
45
|
+
if (size > reference.size) { done(invalid("Attachment exceeds its reference size")); return; }
|
|
46
|
+
hash.update(bytes);
|
|
47
|
+
done(null, bytes);
|
|
48
|
+
} });
|
|
49
|
+
await pipeline(handle.createReadStream({ autoClose: false }), check, createWriteStream(filename, { flags: "wx", mode: 0o600 }));
|
|
50
|
+
if (size !== reference.size || hash.digest("hex") !== reference.sha256) throw invalid("Attachment bytes disagree with their reference");
|
|
51
|
+
} finally { await handle.close(); }
|
|
52
|
+
return await callback(filename);
|
|
53
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Publish immutable bytes before a caller commits their database reference. */
|
|
57
|
+
export async function publishAttachmentFile(target, readable, { db, temporaryRoot } = {}) {
|
|
58
|
+
if (db?.isTransaction) throw invalid("Attachment publication must precede the database write transaction");
|
|
59
|
+
if (db) assertWritableContext(db);
|
|
60
|
+
const root = await assertSafePath(target, ".forgeloop/attachments/objects");
|
|
61
|
+
await mkdir(root, { recursive: true });
|
|
62
|
+
await assertSafePath(target, ".forgeloop/attachments/objects");
|
|
63
|
+
const temporaryParent = temporaryRoot ? await assertSafePath(temporaryRoot, ".") : root;
|
|
64
|
+
const temporary = await mkdtemp(path.join(temporaryParent, ".publishing-"));
|
|
65
|
+
const filename = path.join(temporary, "bytes");
|
|
66
|
+
try {
|
|
67
|
+
await pipeline(readable, createWriteStream(filename, { flags: "wx", mode: 0o600 }));
|
|
68
|
+
const binding = await digestFile(filename, { includeSize: true });
|
|
69
|
+
const file = await open(filename, "r+");
|
|
70
|
+
try { await file.sync(); } finally { await file.close(); }
|
|
71
|
+
const reference = { path: `.forgeloop/attachments/objects/${binding.sha256}`, ...binding };
|
|
72
|
+
const destination = await assertSafePath(target, reference.path);
|
|
73
|
+
try { await link(filename, destination); }
|
|
74
|
+
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
75
|
+
await verifyAttachmentFile(target, reference);
|
|
76
|
+
await syncDirectory(root);
|
|
77
|
+
await syncDirectory(path.dirname(root));
|
|
78
|
+
await syncDirectory(path.dirname(path.dirname(root)));
|
|
79
|
+
await syncDirectory(target);
|
|
80
|
+
return reference;
|
|
81
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
82
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { publishAttachmentFile, verifyAttachmentFile } from "./attachment-files.js";
|
|
2
|
+
import { assertWritableContext, runInTransaction } from "./transaction.js";
|
|
3
|
+
import { validateAttachmentReference } from "./attachment-binding.js";
|
|
4
|
+
export { validateAttachmentReference } from "./attachment-binding.js";
|
|
5
|
+
|
|
6
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_ATTACHMENT_INVALID" }); }
|
|
7
|
+
|
|
8
|
+
/** Synchronous insertion is used only after the referenced bytes were verified. */
|
|
9
|
+
export function insertVerifiedAttachmentReference(db, reference) {
|
|
10
|
+
assertWritableContext(db);
|
|
11
|
+
validateAttachmentReference(reference);
|
|
12
|
+
const prior = db.prepare("SELECT path, size, sha256 FROM attachment_references WHERE task_id = ? AND reference_id = ?").get(reference.taskId, reference.referenceId);
|
|
13
|
+
if (prior) {
|
|
14
|
+
if (prior.path !== reference.path || prior.size !== reference.size || prior.sha256 !== reference.sha256) throw invalid("An immutable attachment identity already names different bytes");
|
|
15
|
+
return reference;
|
|
16
|
+
}
|
|
17
|
+
db.prepare("INSERT INTO attachment_references(task_id, reference_id, path, size, sha256) VALUES (?, ?, ?, ?, ?)")
|
|
18
|
+
.run(reference.taskId, reference.referenceId, reference.path, reference.size, reference.sha256);
|
|
19
|
+
return reference;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Publish first, then commit the immutable binding in a short writer transaction. */
|
|
23
|
+
export async function registerAttachment(db, target, { taskId, referenceId, readable }) {
|
|
24
|
+
if (db.isTransaction) throw invalid("Attachment registration cannot span an active write transaction");
|
|
25
|
+
assertWritableContext(db);
|
|
26
|
+
const binding = await publishAttachmentFile(target, readable, { db });
|
|
27
|
+
const reference = validateAttachmentReference({ taskId, referenceId, ...binding });
|
|
28
|
+
await verifyAttachmentFile(target, reference);
|
|
29
|
+
return runInTransaction(db, () => insertVerifiedAttachmentReference(db, reference));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function* iterateAttachmentReferences(db) {
|
|
33
|
+
for (const row of db.prepare("SELECT * FROM attachment_references ORDER BY task_id, reference_id").iterate()) {
|
|
34
|
+
yield validateAttachmentReference({ taskId: row.task_id, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { loadStorageDriver } from "./runtime.js";
|
|
2
|
+
import { mkdtemp, rm, link, open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
5
|
+
import { checkStorageIntegrity, openStorageDatabase } from "./connection.js";
|
|
6
|
+
|
|
7
|
+
/** Database-only backup; attachment restore requires its own verified inventory. */
|
|
8
|
+
export async function backupStorageDatabase(db, destination) {
|
|
9
|
+
const { backup, DatabaseSync } = loadStorageDriver();
|
|
10
|
+
if (db.isTransaction) throw Object.assign(new Error("Backup requires committed storage state"), { code: "E_STORAGE_SNAPSHOT_TRANSACTION" });
|
|
11
|
+
const absolute = path.resolve(destination);
|
|
12
|
+
const parent = path.dirname(absolute);
|
|
13
|
+
await assertSafePath(parent, path.basename(absolute));
|
|
14
|
+
const temporary = await mkdtemp(path.join(parent, ".forgeloop-backup-"));
|
|
15
|
+
const filename = path.join(temporary, "state.sqlite");
|
|
16
|
+
try {
|
|
17
|
+
await backup(db, filename);
|
|
18
|
+
const snapshot = new DatabaseSync(filename, { readOnly: true, enableForeignKeyConstraints: true });
|
|
19
|
+
try {
|
|
20
|
+
const integrity = checkStorageIntegrity(snapshot);
|
|
21
|
+
if (!integrity.ok) throw Object.assign(new Error("Backup snapshot failed storage integrity validation"), { code: "E_STORAGE_BACKUP_INVALID", integrity });
|
|
22
|
+
} finally { snapshot.close(); }
|
|
23
|
+
// FlushFileBuffers on Windows requires write access to this owned snapshot.
|
|
24
|
+
const file = await open(filename, "r+");
|
|
25
|
+
try { await file.sync(); } finally { await file.close(); }
|
|
26
|
+
// Same-directory hard-link publication is atomic and cannot overwrite a
|
|
27
|
+
// retained backup. The private temporary name is removed afterward.
|
|
28
|
+
await link(filename, absolute);
|
|
29
|
+
try {
|
|
30
|
+
const directory = await open(parent, "r");
|
|
31
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Match the existing portable atomic-file boundary on platforms that
|
|
34
|
+
// cannot open/sync directories. Other I/O failures remain visible.
|
|
35
|
+
if (!["EINVAL", "EPERM", "EISDIR", "ENOTSUP", "UNKNOWN"].includes(error.code)) throw error;
|
|
36
|
+
}
|
|
37
|
+
return { path: absolute, kind: "DATABASE_ONLY", attachmentsIncluded: false };
|
|
38
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Restore into a fresh database pathname; never replace an active project. */
|
|
42
|
+
export async function restoreStorageDatabase(source, destination) {
|
|
43
|
+
const absolute = path.resolve(source);
|
|
44
|
+
const parent = path.dirname(absolute);
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) await assertSafePath(parent, `${path.basename(absolute)}${suffix}`);
|
|
46
|
+
// Read-only open rejects unsupported schema/format metadata rather than
|
|
47
|
+
// silently upgrading a retained backup during a restore drill.
|
|
48
|
+
const db = openStorageDatabase(absolute, { readOnly: true });
|
|
49
|
+
try {
|
|
50
|
+
const restored = await backupStorageDatabase(db, destination);
|
|
51
|
+
return { ...restored, source: absolute, restored: true };
|
|
52
|
+
} finally { db.close(); }
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { STORAGE_FORMAT, STORAGE_FORMAT_VERSION, STORAGE_RELATIVE_PATH, STORAGE_SCHEMA_VERSION } from "./schema.js";
|
|
2
|
+
import { STORAGE_MINIMUM_NODE } from "./runtime.js";
|
|
3
|
+
|
|
4
|
+
/** Package capabilities only; no project selection, database open or runtime probe. */
|
|
5
|
+
export function getStorageCapabilities() {
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
defaultBackend: "sqlite",
|
|
9
|
+
soleSQLiteWriter: false,
|
|
10
|
+
sqlite: {
|
|
11
|
+
format: STORAGE_FORMAT,
|
|
12
|
+
formatVersion: STORAGE_FORMAT_VERSION,
|
|
13
|
+
schemaVersion: STORAGE_SCHEMA_VERSION,
|
|
14
|
+
path: STORAGE_RELATIVE_PATH,
|
|
15
|
+
driver: "node:sqlite",
|
|
16
|
+
minimumNode: STORAGE_MINIMUM_NODE,
|
|
17
|
+
selection: "FRESH_WRITABLE_BOOTSTRAP_OR_EXISTING_DATABASE",
|
|
18
|
+
},
|
|
19
|
+
portableFormats: ["JSON", "NDJSON"],
|
|
20
|
+
migration: {
|
|
21
|
+
explicit: true,
|
|
22
|
+
requiresWritersQuiesced: true,
|
|
23
|
+
resumeRequiresRecordedInventory: true,
|
|
24
|
+
automaticDowngrade: false,
|
|
25
|
+
commands: ["storage-migrate", "storage-migration-resume", "storage-migration-status", "storage-rollback", "storage-rollback-resume"],
|
|
26
|
+
rollbackRequiresNativeWritesExcluded: true,
|
|
27
|
+
rollbackRequiresPinnedLegacyTarget: true,
|
|
28
|
+
},
|
|
29
|
+
backup: { database: true, referencedAttachments: true, command: "storage-backup" },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const activeOwner = new AsyncLocalStorage();
|
|
6
|
+
function closed(message) { return Object.assign(new Error(message), { code: "E_STORAGE_RUNTIME_CLOSED" }); }
|
|
7
|
+
function storageChanged() { return Object.assign(new Error("Project database changed while other connection leases are active"), { code: "E_STORAGE_MIGRATION_REQUIRED" }); }
|
|
8
|
+
|
|
9
|
+
async function openOwnedConnection(entry, filename, options) {
|
|
10
|
+
if (entry.db?.isTransaction && entry.active.size > 1) throw Object.assign(new Error("Another lease left an active native transaction; overlapping access is forbidden"), { code: "E_STORAGE_TRANSACTION_LEAKED" });
|
|
11
|
+
const identity = await lstat(filename, { bigint: true });
|
|
12
|
+
const sameFile = entry.identity?.dev === identity.dev && entry.identity?.ino === identity.ino;
|
|
13
|
+
if (entry.db && (!sameFile || entry.readOnly !== options.readOnly)) {
|
|
14
|
+
if (entry.active.size > 1) throw storageChanged();
|
|
15
|
+
entry.db.close(); entry.db = null;
|
|
16
|
+
}
|
|
17
|
+
if (!entry.db) {
|
|
18
|
+
const { openStorageDatabase } = await import("./connection.js");
|
|
19
|
+
const db = openStorageDatabase(filename, options);
|
|
20
|
+
const after = await lstat(filename, { bigint: true });
|
|
21
|
+
if (after.dev !== identity.dev || after.ino !== identity.ino) { db.close(); throw closed("Project database changed while opening its connection"); }
|
|
22
|
+
entry.db = db; entry.identity = after; entry.readOnly = options.readOnly;
|
|
23
|
+
}
|
|
24
|
+
const { assertStorageMetadataCurrent, assertProjectDatabaseAdmission } = await import("./connection.js");
|
|
25
|
+
assertProjectDatabaseAdmission(entry.db, options.projectAdmission);
|
|
26
|
+
assertStorageMetadataCurrent(entry.db);
|
|
27
|
+
return entry.db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function connectionLease(entry, scope) {
|
|
31
|
+
return {
|
|
32
|
+
assertUnselected() {
|
|
33
|
+
if (entry.db) throw Object.assign(new Error("Previously selected project storage has lost its database; filesystem fallback is forbidden"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
34
|
+
},
|
|
35
|
+
open(filename, options) {
|
|
36
|
+
// Opening/replacing a handle includes filesystem awaits. Serialize only
|
|
37
|
+
// this short admission work, never the external command callback.
|
|
38
|
+
const opening = entry.openTail.then(() => openOwnedConnection(entry, filename, options));
|
|
39
|
+
entry.openTail = opening.then(() => undefined, () => undefined);
|
|
40
|
+
return opening.then(db => { scope.opened = true; return db; });
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function admitLease(entry, readOnly) {
|
|
46
|
+
let release;
|
|
47
|
+
const lifetime = new Promise(resolve => { release = resolve; });
|
|
48
|
+
const admission = entry.tail.then(async () => {
|
|
49
|
+
if (entry.mode !== readOnly) await Promise.all([...entry.active]);
|
|
50
|
+
entry.mode = readOnly;
|
|
51
|
+
entry.active.add(lifetime);
|
|
52
|
+
});
|
|
53
|
+
entry.tail = admission.then(() => undefined, () => undefined);
|
|
54
|
+
await admission;
|
|
55
|
+
return () => { entry.active.delete(lifetime); release(); };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function executeLease(entry, owner, callback, readOnly) {
|
|
59
|
+
const release = await admitLease(entry, readOnly);
|
|
60
|
+
const scope = { owner, active: true, opened: false };
|
|
61
|
+
try {
|
|
62
|
+
return await activeOwner.run(scope, () => callback(connectionLease(entry, scope)));
|
|
63
|
+
} finally {
|
|
64
|
+
scope.active = false;
|
|
65
|
+
try {
|
|
66
|
+
if (scope.opened && entry.db?.isTransaction) {
|
|
67
|
+
entry.db.close(); entry.db = null;
|
|
68
|
+
throw Object.assign(new Error("Command left an active native storage transaction; connection was closed"), { code: "E_STORAGE_TRANSACTION_LEAKED" });
|
|
69
|
+
}
|
|
70
|
+
} finally { release(); }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One connection per project; same-mode leases share it without external serialization. */
|
|
75
|
+
export function createStorageConnectionOwner() {
|
|
76
|
+
const entries = new Map();
|
|
77
|
+
const pending = new Set();
|
|
78
|
+
let closing = false;
|
|
79
|
+
let closePromise;
|
|
80
|
+
const owner = {
|
|
81
|
+
isActive() { return activeOwner.getStore()?.owner === owner && activeOwner.getStore()?.active === true; },
|
|
82
|
+
run(target, callback, { readOnly = false } = {}) {
|
|
83
|
+
if (closing) return Promise.reject(closed("Persistent storage runtime is closing or closed"));
|
|
84
|
+
if (owner.isActive()) return Promise.reject(closed("Nested connection ownership must reuse the active operational scope"));
|
|
85
|
+
const request = (async () => {
|
|
86
|
+
let key;
|
|
87
|
+
try { key = await realpath(path.resolve(target)); }
|
|
88
|
+
catch (error) { if (error.code !== "ENOENT") throw error; key = path.resolve(target); }
|
|
89
|
+
let entry = entries.get(key);
|
|
90
|
+
if (!entry) {
|
|
91
|
+
entry = { tail: Promise.resolve(), openTail: Promise.resolve(), active: new Set(), mode: null, db: null, identity: null };
|
|
92
|
+
entries.set(key, entry);
|
|
93
|
+
}
|
|
94
|
+
return executeLease(entry, owner, callback, readOnly);
|
|
95
|
+
})();
|
|
96
|
+
pending.add(request);
|
|
97
|
+
const finished = () => { pending.delete(request); };
|
|
98
|
+
request.then(finished, finished);
|
|
99
|
+
return request;
|
|
100
|
+
},
|
|
101
|
+
close() {
|
|
102
|
+
if (owner.isActive()) return Promise.reject(closed("Close the runtime outside an active command"));
|
|
103
|
+
if (closePromise) return closePromise;
|
|
104
|
+
closing = true;
|
|
105
|
+
closePromise = (async () => {
|
|
106
|
+
await Promise.all([...pending].map(request => request.catch(() => undefined)));
|
|
107
|
+
let failure;
|
|
108
|
+
for (const entry of entries.values()) {
|
|
109
|
+
try { entry.db?.close(); } catch (error) { failure ??= error; }
|
|
110
|
+
entry.db = null;
|
|
111
|
+
}
|
|
112
|
+
entries.clear();
|
|
113
|
+
if (failure) throw failure;
|
|
114
|
+
})();
|
|
115
|
+
return closePromise;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return owner;
|
|
119
|
+
}
|