run402 4.63.0 → 4.64.0
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/gitvault-surface.json +2 -1
- package/lib/doctor.mjs +28 -15
- package/lib/gitvault-capabilities.mjs +8 -0
- package/lib/remote-helper-session.mjs +166 -32
- package/lib/repos.mjs +122 -9
- package/package.json +1 -1
- package/sdk/dist/errors.d.ts +4 -2
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +1 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.d.ts +80 -0
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +80 -0
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +219 -3
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +282 -5
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-byo-config.d.ts +29 -0
- package/sdk/dist/node/gitvault-byo-config.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-byo-config.js +100 -0
- package/sdk/dist/node/gitvault-byo-config.js.map +1 -0
- package/sdk/dist/node/gitvault-byo-probe.d.ts +20 -0
- package/sdk/dist/node/gitvault-byo-probe.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-byo-probe.js +57 -0
- package/sdk/dist/node/gitvault-byo-probe.js.map +1 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts +43 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js +34 -3
- package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
- package/sdk/dist/node/gitvault-degraded-read.d.ts +120 -0
- package/sdk/dist/node/gitvault-degraded-read.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-degraded-read.js +164 -0
- package/sdk/dist/node/gitvault-degraded-read.js.map +1 -0
- package/sdk/dist/node/gitvault-mirror-backend.d.ts +73 -0
- package/sdk/dist/node/gitvault-mirror-backend.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-mirror-backend.js +114 -8
- package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror-config.d.ts +17 -1
- package/sdk/dist/node/gitvault-mirror-config.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-mirror-config.js +21 -0
- package/sdk/dist/node/gitvault-mirror-config.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror.d.ts +42 -0
- package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-mirror.js +162 -1
- package/sdk/dist/node/gitvault-mirror.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +51 -1
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +88 -6
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
package/gitvault-surface.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"surface_version": "4.
|
|
2
|
+
"surface_version": "4.64.0",
|
|
3
3
|
"verbs": [
|
|
4
4
|
"repos create",
|
|
5
5
|
"repos list",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"revocation_live": true,
|
|
69
69
|
"human_envelope_add_live": true,
|
|
70
70
|
"mirror_live": true,
|
|
71
|
+
"byo_live": true,
|
|
71
72
|
"recover_live": true,
|
|
72
73
|
"allocation": "create_immediate",
|
|
73
74
|
"snapshot_dirty_default": "refuse"
|
package/lib/doctor.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, statSync } from "node:fs";
|
|
15
|
+
import { GITVAULT_BYO_NO_PAYLOAD_COPY_STATEMENT } from "#sdk";
|
|
15
16
|
import { configDir, readAllowance, loadKeyStore } from "./config.mjs";
|
|
16
17
|
import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
17
18
|
import { getSdk } from "./sdk.mjs";
|
|
@@ -665,6 +666,10 @@ export async function run(sub, args = []) {
|
|
|
665
666
|
project_id: gv.project_id ?? projectId,
|
|
666
667
|
repo_id: gv.repo_id,
|
|
667
668
|
vault: gv.vault === null ? null : "allocated",
|
|
669
|
+
// gitvault-byo-primary-bucket task 3.5 — absent-or-"managed" is
|
|
670
|
+
// byte-identical to before this fold for every non-BYO vault.
|
|
671
|
+
storage_profile: gv.vault?.storage_profile ?? null,
|
|
672
|
+
byo_destination: gv.vault?.byo_destination ?? null,
|
|
668
673
|
gitvault_policy: gv.gitvault_policy,
|
|
669
674
|
keystore_root: gv.keystore.root,
|
|
670
675
|
can_sign: gv.keystore.can_sign,
|
|
@@ -704,30 +709,34 @@ export async function run(sub, args = []) {
|
|
|
704
709
|
// doctor-persistent `grandfathered` advisory it owns.
|
|
705
710
|
for (const w of gv.warnings ?? []) gaps.push(`${w.kind}: ${w.message}`);
|
|
706
711
|
|
|
707
|
-
// gitvault-mirror-and-recover task 4.3
|
|
708
|
-
// ALONGSIDE (never in place of) the
|
|
709
|
-
// never blocking `run402 deploy`'s
|
|
710
|
-
// outcome is unaffected regardless of
|
|
711
|
-
// `mirror_currency` mirrors `mirror
|
|
712
|
-
// `current` / `stale` / `unknown` (mirror
|
|
713
|
-
// unread)
|
|
714
|
-
//
|
|
715
|
-
//
|
|
712
|
+
// gitvault-mirror-and-recover task 4.3 + gitvault-mirror-default:
|
|
713
|
+
// mirror currency, reported ALONGSIDE (never in place of) the
|
|
714
|
+
// deploy-related gaps above, and never blocking `run402 deploy`'s
|
|
715
|
+
// own gate — the vault lane's outcome is unaffected regardless of
|
|
716
|
+
// mirror state (design D6). `mirror_currency` mirrors `mirror
|
|
717
|
+
// status`'s own tri-state: `current` / `stale` / `unknown` (mirror
|
|
718
|
+
// unreachable or vault unread). Only STALE is actionable enough to
|
|
719
|
+
// become a warning gap; a vault with no successful mirror copy yet
|
|
720
|
+
// carries the SDK-computed `vault_unmirrored` finding — named and
|
|
721
|
+
// standing (gitvault-mirror-default supersedes the old anonymous
|
|
722
|
+
// `advisory` string), echoed verbatim, and deliberately NOT pushed
|
|
723
|
+
// into `gaps`: informational, never blocking, computed client-side
|
|
724
|
+
// only, cleared by the first successful mirror write or sync.
|
|
716
725
|
if (gv.vault !== null && value.repo_id) {
|
|
717
726
|
try {
|
|
718
|
-
const mirrorStatus = await getSdk().gitvault.mirrorStatus({ repo_id: value.repo_id });
|
|
727
|
+
const mirrorStatus = await getSdk().gitvault.mirrorStatus({ repo_id: value.repo_id, is_byo: value.storage_profile === "byo" });
|
|
719
728
|
value.gitvault_mirror = {
|
|
720
729
|
configured: mirrorStatus.configured,
|
|
721
730
|
destination: mirrorStatus.destination,
|
|
722
731
|
mirrored_generation: mirrorStatus.mirrored_generation,
|
|
723
732
|
newest_generation: mirrorStatus.newest_generation,
|
|
724
733
|
is_current: mirrorStatus.is_current,
|
|
734
|
+
last_success_at: mirrorStatus.last_success_at,
|
|
735
|
+
finding: mirrorStatus.finding,
|
|
725
736
|
validity_not_freshness: mirrorStatus.validity_not_freshness,
|
|
726
737
|
keystore_still_required: mirrorStatus.keystore_still_required,
|
|
727
738
|
};
|
|
728
|
-
if (
|
|
729
|
-
value.gitvault_mirror.advisory = "no ciphertext mirror is configured for this vault — the exit ramp is opt-in; 'run402 repos mirror <destination>' to configure one.";
|
|
730
|
-
} else if (mirrorStatus.is_current === false) {
|
|
739
|
+
if (mirrorStatus.is_current === false) {
|
|
731
740
|
gaps.push(`the ciphertext mirror at ${mirrorStatus.destination} is STALE (mirrored generation ${mirrorStatus.mirrored_generation ?? "(none)"}, vault newest ${mirrorStatus.newest_generation ?? "(none)"}) — ${mirrorStatus.closing_command}`);
|
|
732
741
|
}
|
|
733
742
|
} catch {
|
|
@@ -736,15 +745,19 @@ export async function run(sub, args = []) {
|
|
|
736
745
|
}
|
|
737
746
|
}
|
|
738
747
|
|
|
748
|
+
// gitvault-byo-primary-bucket task 3.5 — unconditional, independent
|
|
749
|
+
// of mirror status (D7); imported from the canonical constants, never
|
|
750
|
+
// paraphrased.
|
|
751
|
+
const byoDisclosure = value.storage_profile === "byo" ? ` Storage: byo (${value.byo_destination ?? "(unknown)"}) — ${GITVAULT_BYO_NO_PAYLOAD_COPY_STATEMENT}` : "";
|
|
739
752
|
checks.push({
|
|
740
753
|
name: "gitvault",
|
|
741
754
|
status: gaps.length > 0 ? "warning" : "ok",
|
|
742
755
|
value: gaps.length > 0 ? { ...value, gaps } : value,
|
|
743
|
-
hint: gv.vault === null
|
|
756
|
+
hint: (gv.vault === null
|
|
744
757
|
? `No vault for this project (that is a normal shape). Allocate one with 'run402 repos create --project <id>'. Keystore: ${gv.keystore.root}`
|
|
745
758
|
: gv.durability_statement
|
|
746
759
|
? `Back up ${gv.keystore.root} anyway — ${gv.durability_statement} (covering_recipients: ${gv.covering_recipients})`
|
|
747
|
-
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history
|
|
760
|
+
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history.`) + byoDisclosure,
|
|
748
761
|
});
|
|
749
762
|
} catch (err) {
|
|
750
763
|
// A gateway without gitvault, an unreachable API, or a project this
|
|
@@ -28,6 +28,14 @@ export const GITVAULT_CAPABILITIES = {
|
|
|
28
28
|
// `repos mirror` reads/writes a mirror destination beside the keystore and
|
|
29
29
|
// moves real bytes into a customer-owned bucket.
|
|
30
30
|
mirror_live: true,
|
|
31
|
+
// `repos create --byo <s3://bucket/prefix>` allocates a
|
|
32
|
+
// `storage_profile: "byo"` vault whose payload ciphertext is written by
|
|
33
|
+
// the client straight to the customer's own bucket (create-only probe
|
|
34
|
+
// fail-closed before allocation; chain-copy dual-write; attested finalize
|
|
35
|
+
// with `storage_verification: "client_attested"`; degraded reads from the
|
|
36
|
+
// destination; fsck absence check) — shipped end to end in the same
|
|
37
|
+
// change that flips this flag (gitvault-byo-primary-bucket, Phase 3).
|
|
38
|
+
byo_live: true,
|
|
31
39
|
// `repos recover` materializes a git repository from a mirror source,
|
|
32
40
|
// offline, with no server call.
|
|
33
41
|
recover_live: true,
|
|
@@ -112,6 +112,23 @@
|
|
|
112
112
|
* is refused rather than silently pointed at the current directory. Only
|
|
113
113
|
* `capabilities`, `option` and `list` work without one, which is exactly
|
|
114
114
|
* the set `git ls-remote <url>` outside a checkout needs.
|
|
115
|
+
*
|
|
116
|
+
* DEGRADED READ MODE (gitvault-byo-primary-bucket, design D4). `list` and
|
|
117
|
+
* `fetch` each wrap their own live gateway read in
|
|
118
|
+
* `getSdk().gitvault.withDegradedRead` — on a NETWORK-CLASS failure (never a
|
|
119
|
+
* 401/403/404 or any other 4xx; one bounded retry runs first so a transient
|
|
120
|
+
* blip does not flap between sources) with a mirror configured for this
|
|
121
|
+
* vault, the read is served from it instead via the SAME `r402s-recover`
|
|
122
|
+
* engine `run402 repos recover` uses — `git fetch`/`clone` keep working
|
|
123
|
+
* while run402 is down. The result is marked `degraded: true` with source
|
|
124
|
+
* provenance, and exactly one stderr line names the fallback and its source
|
|
125
|
+
* (`gitvaultDegradedReadNote`). Local trust pins never advance past what the
|
|
126
|
+
* mirror copy itself chain-verifies (the fallback engine never touches the
|
|
127
|
+
* live vault's own keystore pins at all — see `gitvault-degraded-read.ts`'s
|
|
128
|
+
* own doc comment). A vault with no mirror configured is BYTE-IDENTICAL to
|
|
129
|
+
* today: the original gateway error surfaces unchanged. `push` is UNTOUCHED
|
|
130
|
+
* by this — writes are never rerouted; admission always requires the
|
|
131
|
+
* gateway.
|
|
115
132
|
*/
|
|
116
133
|
|
|
117
134
|
import { createInterface } from "node:readline";
|
|
@@ -127,7 +144,7 @@ const nodeModP = import("#sdk/node");
|
|
|
127
144
|
const configModP = import("./config.mjs");
|
|
128
145
|
const { getSdk } = await sdkModP;
|
|
129
146
|
const { resolveWalletCore, enforceWalletExistsCore, WalletSelectionError } = await walletModP;
|
|
130
|
-
const { gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, parseGitvaultRemoteUrl } = await isoModP;
|
|
147
|
+
const { gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, parseGitvaultRemoteUrl, gitvaultDegradedReadNote } = await isoModP;
|
|
131
148
|
const { GITVAULT_R402_REF_NAMESPACE, hardenedGit, resolveGitInvocationRepo, readPinnedGitvaultRepo, pinGitvaultRepo, readGitvaultRestoreMarker, readGitvaultAutoGcThreshold } = await nodeModP;
|
|
132
149
|
const { allowanceFile, projectCredentialsFile, profileStateFile } = await configModP;
|
|
133
150
|
|
|
@@ -408,6 +425,23 @@ export function shouldRunAutoGc(threshold, generationsSinceCheckpoint) {
|
|
|
408
425
|
return Number.isFinite(threshold) && threshold > 0 && Number.isFinite(generationsSinceCheckpoint) && generationsSinceCheckpoint >= threshold;
|
|
409
426
|
}
|
|
410
427
|
|
|
428
|
+
/**
|
|
429
|
+
* The auto-gc cycle's `compact()`/`prune()` target — exported standalone,
|
|
430
|
+
* same reasoning as `shouldRunAutoGc` above, because the merge itself was
|
|
431
|
+
* the bug: `GitvaultVaultHandleOptions.repo_dir` defaults to
|
|
432
|
+
* `process.cwd()` only in CALLER convention, never inside the SDK's own
|
|
433
|
+
* `open()` (which sets it ONLY when `options.repo_dir !== undefined`).
|
|
434
|
+
* Building the auto-gc target from `{ ...target }` alone — omitting
|
|
435
|
+
* `repo_dir` — made every real post-push auto-gc cycle fail closed with
|
|
436
|
+
* `GITVAULT_REPO_DIR_REQUIRED`, silently degrading to the advisory on
|
|
437
|
+
* EVERY push, live in production shape, until a push-latency guard bench
|
|
438
|
+
* finally drove the real trigger path (found 2026-08-31; `repos gc`'s CLI
|
|
439
|
+
* wrapper passes repo_dir explicitly and never hit this).
|
|
440
|
+
*/
|
|
441
|
+
export function buildAutoGcCompactionTarget(target, repoDir) {
|
|
442
|
+
return { ...target, repo_dir: repoDir };
|
|
443
|
+
}
|
|
444
|
+
|
|
411
445
|
async function main(argv, { onBackgroundWork } = {}) {
|
|
412
446
|
// gitvault-connection-amortization (bench P5) note: the prewarm now fires
|
|
413
447
|
// at the module TOP, before the SDK graph loads (gitvault-startup-
|
|
@@ -507,7 +541,7 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
507
541
|
*/
|
|
508
542
|
const openVault = async (repoDir) => {
|
|
509
543
|
const result = await getCachedSdk().gitvault.resolveOrCreateAddress({ address, allow_create: false, ...(repoDir ? { repo_dir: repoDir } : {}) });
|
|
510
|
-
return { vault: result.handle.vault, resolution: result.resolution };
|
|
544
|
+
return { vault: result.handle.vault, keystore: result.handle.keystore, repo_id: result.handle.repo_id, resolution: result.resolution };
|
|
511
545
|
};
|
|
512
546
|
|
|
513
547
|
/**
|
|
@@ -552,7 +586,10 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
552
586
|
note(`vault ${result.handle.repo_id} allocated (genesis ${result.created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
553
587
|
note(JSON.stringify(result.created.recovery_receipt));
|
|
554
588
|
try {
|
|
555
|
-
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
589
|
+
const { getGitvaultKeystoreRoot, GITVAULT_MIRROR_SETUP_HINT } = await import("#sdk/node");
|
|
590
|
+
// gitvault-mirror-default: every allocation teaches the mirror door
|
|
591
|
+
// beside the recovery receipt, lazy push-to-create included.
|
|
592
|
+
note(GITVAULT_MIRROR_SETUP_HINT);
|
|
556
593
|
note(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship`);
|
|
557
594
|
} catch {
|
|
558
595
|
// Never let a diagnostic line fail a push that already allocated successfully.
|
|
@@ -596,13 +633,38 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
596
633
|
}
|
|
597
634
|
}
|
|
598
635
|
const materializeOpts = fetchMarker ? { deltaSince: fetchMarker.generation } : {};
|
|
599
|
-
let
|
|
600
|
-
let state;
|
|
636
|
+
let opened;
|
|
601
637
|
try {
|
|
602
|
-
|
|
603
|
-
|
|
638
|
+
opened = await openVault(repoDir ?? undefined);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
// An unallocated vault is not an error here: `list` is the read half of
|
|
641
|
+
// the protocol dance and must never create anything on its own (D2
|
|
642
|
+
// scopes lazy creation to `push`). Reporting it as an EMPTY ref set is
|
|
643
|
+
// exactly what a fresh repository looks like to git, and `push` still
|
|
644
|
+
// runs `list` first either way — this is what lets a first push land in
|
|
645
|
+
// one command instead of `list` failing the whole exchange before
|
|
646
|
+
// `push` ever gets a turn. Nothing to share with `push` either
|
|
647
|
+
// (design D7): an unallocated vault has no base to reuse.
|
|
648
|
+
if (isVaultNotFound(err)) {
|
|
649
|
+
endBlock();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
throw err;
|
|
653
|
+
}
|
|
654
|
+
let vault = opened.vault;
|
|
655
|
+
|
|
656
|
+
// gitvault-byo-primary-bucket (design D4, task 3.4 — mirror half): the
|
|
657
|
+
// live materialize (with its own stale-pin retry, unchanged from before
|
|
658
|
+
// this change) is the "attemptLive" `withDegradedRead` wraps — on a
|
|
659
|
+
// NETWORK-CLASS failure (never 4xx) with a mirror configured for this
|
|
660
|
+
// vault, it falls back to `r402s-recover`'s engine against it instead of
|
|
661
|
+
// failing the whole `list`/`clone`. `repoDir` is required for the
|
|
662
|
+
// fallback to materialize into (`out_dir`); a repo-free `list` (bare
|
|
663
|
+
// `git ls-remote`) simply has none, so the fallback never runs and the
|
|
664
|
+
// original error surfaces exactly as it always did.
|
|
665
|
+
const attemptLive = async () => {
|
|
604
666
|
try {
|
|
605
|
-
|
|
667
|
+
return await vault.materialize(materializeOpts);
|
|
606
668
|
} catch (err) {
|
|
607
669
|
// An OFFLINE (id-carrying pin) resolution discovers a stale pin on
|
|
608
670
|
// its FIRST repo-scoped read (client-surface spec, id-pinning
|
|
@@ -617,30 +679,54 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
617
679
|
if (!recovered) throw err;
|
|
618
680
|
note(`pinned vault ${opened.resolution.repo_id} no longer resolves — re-resolved to ${recovered.resolution.repo_id}, retrying`);
|
|
619
681
|
vault = recovered.handle.vault;
|
|
620
|
-
|
|
682
|
+
opened = { ...opened, vault: recovered.handle.vault, keystore: recovered.handle.keystore, repo_id: recovered.handle.repo_id, resolution: recovered.resolution };
|
|
683
|
+
return await vault.materialize(materializeOpts);
|
|
621
684
|
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
let outcome;
|
|
688
|
+
try {
|
|
689
|
+
outcome = await getCachedSdk().gitvault.withDegradedRead({ attemptLive, keystore: opened.keystore, repo_id: opened.repo_id, out_dir: repoDir });
|
|
622
690
|
} catch (err) {
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
// one command instead of `list` failing the whole exchange before
|
|
629
|
-
// `push` ever gets a turn. Nothing to share with `push` either
|
|
630
|
-
// (design D7): an unallocated vault has no base to reuse.
|
|
691
|
+
// Same "an unallocated vault is not an error" reasoning as above — a
|
|
692
|
+
// 404 discovered only once materialize actually runs takes this same
|
|
693
|
+
// empty-ref-set path (network-class errors never reach here at all:
|
|
694
|
+
// `withDegradedRead` only rethrows them after exhausting its own
|
|
695
|
+
// fallback, and a 404 is never network-class).
|
|
631
696
|
if (isVaultNotFound(err)) {
|
|
632
697
|
endBlock();
|
|
633
698
|
return;
|
|
634
699
|
}
|
|
635
700
|
throw err;
|
|
636
701
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
702
|
+
|
|
703
|
+
let refs;
|
|
704
|
+
let head;
|
|
705
|
+
if (outcome.degraded) {
|
|
706
|
+
// Exactly ONE stderr line naming the degraded read and its source
|
|
707
|
+
// (design D4) — the validity-not-freshness limit rides inside the
|
|
708
|
+
// canonical statement this composes, verbatim.
|
|
709
|
+
note(gitvaultDegradedReadNote(outcome.result.source));
|
|
710
|
+
refs = outcome.result.refs ?? {};
|
|
711
|
+
head = outcome.result.head_target;
|
|
712
|
+
// No live vault instance materialized this pass — nothing safe to
|
|
713
|
+
// share with a following `push` (design D7's own "unallocated vault"
|
|
714
|
+
// reasoning applies equally to a degraded one: writes are never
|
|
715
|
+
// rerouted, so `push` must resolve and materialize live on its own).
|
|
716
|
+
} else {
|
|
717
|
+
const state = outcome.live;
|
|
718
|
+
refs = state.refs ?? {};
|
|
719
|
+
head = state.head_target;
|
|
720
|
+
// gitvault-session-state-reuse design D1: `fetchState`/`fetchMarker`
|
|
721
|
+
// extend the SAME session-scoped handoff `push` already reuses (`base`)
|
|
722
|
+
// — the `fetch` phase of THIS session reuses this response instead of
|
|
723
|
+
// issuing its own state read. Session-scoped only: dropped the moment a
|
|
724
|
+
// push admits in this same session (see `runPush`'s reset below).
|
|
725
|
+
// `keystore`/`repo_id` ride along so `fetch`'s own degraded-read wrap
|
|
726
|
+
// can fall back without re-resolving.
|
|
727
|
+
if (repoDir) sharedListSession = { repoDir, walletName: resolvedWallet?.name ?? null, vault, keystore: opened.keystore, repo_id: opened.repo_id, base: state, fetchMarker, fetchState: state };
|
|
728
|
+
}
|
|
729
|
+
|
|
644
730
|
for (const ref of Object.keys(refs).sort()) out(`${refs[ref]} ${ref}`);
|
|
645
731
|
// A snapshot-only vault holds protocol refs but no branch heads, so a
|
|
646
732
|
// plain `git clone` prints "cloned an empty repository" with no hint the
|
|
@@ -650,7 +736,6 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
650
736
|
note(`this vault has no branch heads yet — its history lives on ${refNames.sort()[0]}`);
|
|
651
737
|
note(`fetch it with: git fetch <remote> '+${refNames.sort()[0]}:${refNames.sort()[0]}' && git checkout -b restored ${refNames.sort()[0]}`);
|
|
652
738
|
}
|
|
653
|
-
const head = state.head_target;
|
|
654
739
|
// A symref is only advertised when its target is actually present:
|
|
655
740
|
// pointing HEAD at a ref that does not exist is what an empty repository
|
|
656
741
|
// looks like, and git reads the empty list correctly on its own.
|
|
@@ -683,15 +768,55 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
683
768
|
// different repository/wallet, or a push that already admitted in this
|
|
684
769
|
// same session — see `runPush`'s reset) falls back to the vault's own
|
|
685
770
|
// read, unchanged.
|
|
771
|
+
//
|
|
772
|
+
// gitvault-byo-primary-bucket (design D4, task 3.4 — mirror half):
|
|
773
|
+
// either path's actual chain/payload read is the "attemptLive" that
|
|
774
|
+
// `withDegradedRead` wraps — `gitvault.restore(...)` is exactly `open()`
|
|
775
|
+
// followed by `handle.vault.restoreObjectsInto(target_dir)` (see that
|
|
776
|
+
// method's own one-line body), decomposed here so the SECOND half can be
|
|
777
|
+
// wrapped, with byte-identical resolution to today on the live-success
|
|
778
|
+
// path. On a NETWORK-CLASS failure (never 4xx) with a mirror configured
|
|
779
|
+
// for this vault, the fetch is served from it instead of failing the
|
|
780
|
+
// whole clone; the destination it materialized into is the SAME
|
|
781
|
+
// `repoDir` git already prepared, so `git fetch`/`clone` completes
|
|
782
|
+
// exactly as if the live path had run.
|
|
686
783
|
const shared = sharedListSession && sharedListSession.repoDir === repoDir && sharedListSession.walletName === (resolvedWallet?.name ?? null) ? sharedListSession : null;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
784
|
+
let attemptLive;
|
|
785
|
+
let degradedKeystore;
|
|
786
|
+
let degradedRepoId;
|
|
787
|
+
if (shared) {
|
|
788
|
+
attemptLive = () => shared.vault.restoreObjectsInto(repoDir, { marker: shared.fetchMarker, state: shared.fetchState });
|
|
789
|
+
degradedKeystore = shared.keystore;
|
|
790
|
+
degradedRepoId = shared.repo_id;
|
|
791
|
+
} else {
|
|
792
|
+
const handle = await getCachedSdk().gitvault.open({ ...target, repo_dir: repoDir });
|
|
793
|
+
attemptLive = () => handle.vault.restoreObjectsInto(repoDir);
|
|
794
|
+
degradedKeystore = handle.keystore;
|
|
795
|
+
degradedRepoId = handle.repo_id;
|
|
796
|
+
}
|
|
797
|
+
const outcome = await getCachedSdk().gitvault.withDegradedRead({
|
|
798
|
+
attemptLive,
|
|
799
|
+
keystore: degradedKeystore,
|
|
800
|
+
repo_id: degradedRepoId,
|
|
801
|
+
out_dir: repoDir,
|
|
802
|
+
});
|
|
803
|
+
let restored;
|
|
804
|
+
if (outcome.degraded) {
|
|
805
|
+
// Exactly ONE stderr line naming the degraded read and its source
|
|
806
|
+
// (design D4) — the validity-not-freshness limit rides inside the
|
|
807
|
+
// canonical statement this composes, verbatim. A later `git push`
|
|
808
|
+
// still requires the gateway unchanged — this function only ever
|
|
809
|
+
// serves reads; `runPush` below is untouched by this change.
|
|
810
|
+
note(gitvaultDegradedReadNote(outcome.result.source));
|
|
811
|
+
restored = { generation: outcome.result.generation, retained_refs: outcome.result.retained_refs };
|
|
812
|
+
} else {
|
|
813
|
+
restored = { generation: outcome.live.generation, retained_refs: outcome.live.retained_refs };
|
|
814
|
+
}
|
|
690
815
|
if (verbosity >= 1) note(`restored generation ${restored.generation}`);
|
|
691
816
|
// clone-installs-retained-refs D3: a bookkeeping failure here degrades to
|
|
692
817
|
// exactly today's (pre-change) behavior — one stderr note, fetch still
|
|
693
|
-
// completes. `restored.retained_refs` is never absent (the
|
|
694
|
-
//
|
|
818
|
+
// completes. `restored.retained_refs` is never absent (both the live and
|
|
819
|
+
// the degraded path always return a result, never throw for this step).
|
|
695
820
|
if (restored.retained_refs?.warning) note(restored.retained_refs.warning);
|
|
696
821
|
else if (verbosity >= 1 && (restored.retained_refs?.written.length > 0 || restored.retained_refs?.deleted.length > 0)) {
|
|
697
822
|
note(`refs/r402/retain: +${restored.retained_refs.written.length} -${restored.retained_refs.deleted.length} (${restored.retained_refs.retained_count} retained tip(s) total)`);
|
|
@@ -741,10 +866,19 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
741
866
|
}
|
|
742
867
|
if (!shouldRunAutoGc(threshold, generationsSinceCheckpoint)) return;
|
|
743
868
|
|
|
869
|
+
// `compact()`/`prune()` never default repo_dir to process.cwd() —
|
|
870
|
+
// GitvaultVaultHandleOptions's own doc comment describes CALLER
|
|
871
|
+
// convention, not an SDK fallback (`open()` only sets repo_dir when
|
|
872
|
+
// `options.repo_dir !== undefined`). Omitting it here made every real
|
|
873
|
+
// post-push auto-gc cycle fail closed with GITVAULT_REPO_DIR_REQUIRED,
|
|
874
|
+
// silently degrading to the advisory on every push (found live,
|
|
875
|
+
// 2026-08-31, via a push-latency guard bench that finally drove the
|
|
876
|
+
// ACTUAL trigger path instead of a synthetic `repos gc` invocation).
|
|
877
|
+
const compactionTarget = buildAutoGcCompactionTarget(target, repoDir);
|
|
744
878
|
const runCycle = async () => {
|
|
745
879
|
const sdk = getCachedSdk();
|
|
746
|
-
const checkpoint = await sdk.gitvault.compact({ ...
|
|
747
|
-
const prune = await sdk.gitvault.prune(
|
|
880
|
+
const checkpoint = await sdk.gitvault.compact({ ...compactionTarget });
|
|
881
|
+
const prune = await sdk.gitvault.prune(compactionTarget); // PLAN only — never `submit`; see compact()'s own grant-close doc comment for why prune needs no headroom of its own
|
|
748
882
|
return { checkpoint, prune };
|
|
749
883
|
};
|
|
750
884
|
|