run402 4.24.1 → 4.26.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/cli.mjs +6 -6
- package/git-remote-run402.mjs +328 -0
- package/lib/command-manifest.mjs +13 -1
- package/lib/credentials.mjs +4 -0
- package/lib/deploy-v2.mjs +78 -1
- package/lib/doctor.mjs +80 -1
- package/lib/domains.mjs +0 -14
- package/lib/gitvault.mjs +582 -0
- package/lib/init.mjs +153 -5
- package/lib/org-context.mjs +44 -1
- package/lib/pay.test.mjs +0 -4
- package/package.json +8 -2
- package/sdk/dist/index.d.ts +20 -3
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +22 -3
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/admin.d.ts +0 -5
- package/sdk/dist/namespaces/admin.d.ts.map +1 -1
- package/sdk/dist/namespaces/admin.js +0 -9
- package/sdk/dist/namespaces/admin.js.map +1 -1
- package/sdk/dist/namespaces/assets.d.ts +1 -20
- package/sdk/dist/namespaces/assets.d.ts.map +1 -1
- package/sdk/dist/namespaces/assets.js +0 -27
- package/sdk/dist/namespaces/assets.js.map +1 -1
- package/sdk/dist/namespaces/domains.d.ts +0 -19
- package/sdk/dist/namespaces/domains.d.ts.map +1 -1
- package/sdk/dist/namespaces/domains.js +0 -19
- package/sdk/dist/namespaces/domains.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.d.ts +319 -0
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -0
- package/sdk/dist/namespaces/gitvault.crypto.js +690 -0
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -0
- package/sdk/dist/namespaces/gitvault.d.ts +521 -0
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -0
- package/sdk/dist/namespaces/gitvault.js +786 -0
- package/sdk/dist/namespaces/gitvault.js.map +1 -0
- package/sdk/dist/namespaces/gitvault.types.d.ts +481 -0
- package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/gitvault.types.js +19 -0
- package/sdk/dist/namespaces/gitvault.types.js.map +1 -0
- package/sdk/dist/namespaces/org.d.ts +1 -3
- package/sdk/dist/namespaces/org.d.ts.map +1 -1
- package/sdk/dist/namespaces/org.js +6 -10
- package/sdk/dist/namespaces/org.js.map +1 -1
- package/sdk/dist/namespaces/pay.d.ts +0 -8
- package/sdk/dist/namespaces/pay.d.ts.map +1 -1
- package/sdk/dist/namespaces/pay.js.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts +2 -2
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +4 -12
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/namespaces/secrets.d.ts +0 -2
- package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.js +3 -10
- package/sdk/dist/namespaces/secrets.js.map +1 -1
- package/sdk/dist/namespaces/subdomains.d.ts +0 -2
- package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
- package/sdk/dist/namespaces/subdomains.js +5 -16
- package/sdk/dist/namespaces/subdomains.js.map +1 -1
- package/sdk/dist/namespaces/transfers.d.ts +0 -2
- package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
- package/sdk/dist/namespaces/transfers.js +8 -10
- package/sdk/dist/namespaces/transfers.js.map +1 -1
- package/sdk/dist/namespaces/wallets.d.ts +0 -12
- package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
- package/sdk/dist/namespaces/wallets.js +0 -14
- package/sdk/dist/namespaces/wallets.js.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.d.ts +222 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-creation-journal.js +365 -0
- package/sdk/dist/node/gitvault-creation-journal.js.map +1 -0
- package/sdk/dist/node/gitvault-deploy.d.ts +211 -0
- package/sdk/dist/node/gitvault-deploy.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-deploy.js +292 -0
- package/sdk/dist/node/gitvault-deploy.js.map +1 -0
- package/sdk/dist/node/gitvault-keystore.d.ts +207 -0
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-keystore.js +529 -0
- package/sdk/dist/node/gitvault-keystore.js.map +1 -0
- package/sdk/dist/node/gitvault-prune.d.ts +341 -0
- package/sdk/dist/node/gitvault-prune.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-prune.js +410 -0
- package/sdk/dist/node/gitvault-prune.js.map +1 -0
- package/sdk/dist/node/gitvault-publication.d.ts +730 -0
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-publication.js +1475 -0
- package/sdk/dist/node/gitvault-publication.js.map +1 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts +227 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-snapshot.js +556 -0
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -0
- package/sdk/dist/node/index.d.ts +11 -0
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +9 -0
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
- package/sdk/dist/node/paid-fetch.js +0 -4
- package/sdk/dist/node/paid-fetch.js.map +1 -1
- package/sdk/dist/node/sites-node.d.ts +1 -8
- package/sdk/dist/node/sites-node.d.ts.map +1 -1
- package/sdk/dist/node/sites-node.js +1 -7
- package/sdk/dist/node/sites-node.js.map +1 -1
- package/sdk/dist/scoped.d.ts +4 -35
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +9 -74
- package/sdk/dist/scoped.js.map +1 -1
- package/lib/sender-domain.mjs +0 -36
- package/sdk/dist/deprecate.d.ts +0 -23
- package/sdk/dist/deprecate.d.ts.map +0 -1
- package/sdk/dist/deprecate.js +0 -42
- package/sdk/dist/deprecate.js.map +0 -1
- package/sdk/dist/namespaces/sender-domain.d.ts +0 -47
- package/sdk/dist/namespaces/sender-domain.d.ts.map +0 -1
- package/sdk/dist/namespaces/sender-domain.js +0 -45
- package/sdk/dist/namespaces/sender-domain.js.map +0 -1
package/lib/gitvault.mjs
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run402 gitvault` — the host-blind encrypted Git remote (wire tag `r402s/v0`).
|
|
3
|
+
*
|
|
4
|
+
* ARCHITECTURAL LAW (gitvault-client-surface, "All protocol logic lives in the
|
|
5
|
+
* SDK"): every piece of vault protocol behaviour — crypto core, keystore,
|
|
6
|
+
* creation journal, snapshot + capture, publication state machines, ref
|
|
7
|
+
* transactions, verification budget, token exchange, repair — lives ONCE in
|
|
8
|
+
* `@run402/sdk` under `r.gitvault`. This module is a THIN ADAPTER: argument
|
|
9
|
+
* parsing, TTY output, exit codes, local file I/O. It adds zero protocol
|
|
10
|
+
* behaviour, and imports only the SDK (via `./sdk.mjs`) plus the CLI's own
|
|
11
|
+
* argument/error helpers — never a crypto, HTTP, or git library.
|
|
12
|
+
*
|
|
13
|
+
* Pipe contract (docs/style.md): the payload is JSON on stdout; every human
|
|
14
|
+
* line — progress, the terminal-loss statement, advisories — goes to stderr, so
|
|
15
|
+
* `run402 gitvault status | jq` stays clean.
|
|
16
|
+
*
|
|
17
|
+
* Run these from inside the git working tree: `repo_dir` is `process.cwd()`.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { resolveProjectId } from "./config.mjs";
|
|
21
|
+
import { resolveOwningOrgId } from "./org-context.mjs";
|
|
22
|
+
import { getSdk } from "./sdk.mjs";
|
|
23
|
+
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
24
|
+
import {
|
|
25
|
+
normalizeArgv,
|
|
26
|
+
hasHelp,
|
|
27
|
+
assertKnownFlags,
|
|
28
|
+
parseIntegerFlag,
|
|
29
|
+
flagValue,
|
|
30
|
+
requirePositionalCount,
|
|
31
|
+
failUnknownSubcommand,
|
|
32
|
+
} from "./argparse.mjs";
|
|
33
|
+
|
|
34
|
+
/** Value-taking flags every gitvault subcommand accepts. */
|
|
35
|
+
const COMMON_VALUE_FLAGS = ["--project", "--repo"];
|
|
36
|
+
|
|
37
|
+
export const HELP = `run402 gitvault — your source, encrypted before it leaves the machine
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
run402 gitvault init [--project <id>] [--org <org_id>] [--git-remote] [--no-remote]
|
|
41
|
+
run402 gitvault status [--project <id>] [--repo <repo_id>] [--refs]
|
|
42
|
+
run402 gitvault push [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint]
|
|
43
|
+
run402 gitvault policy <required|grandfathered> [--project <id>] [--repo <repo_id>]
|
|
44
|
+
[--reason <why>]
|
|
45
|
+
run402 gitvault compact [--project <id>] [--repo <repo_id>]
|
|
46
|
+
run402 gitvault prune [--project <id>] [--repo <repo_id>]
|
|
47
|
+
[--submit --intent-core <path> --verifier-receipt <path> [--wait]]
|
|
48
|
+
run402 gitvault verify [--project <id>] [--repo <repo_id>] [--budget <n>]
|
|
49
|
+
|
|
50
|
+
Subcommands:
|
|
51
|
+
init ALLOCATE the project's vault. This is the one step that mints key
|
|
52
|
+
material on this machine and emits the one-shot recovery receipt,
|
|
53
|
+
so it is explicit rather than a side effect of \`run402 init\` (which
|
|
54
|
+
only adds the git remote). Idempotent: an existing vault is
|
|
55
|
+
reported with \`deduplicated: true\` and nothing is re-minted. Adds
|
|
56
|
+
the \`run402\` remote too when the current directory is already a
|
|
57
|
+
repository.
|
|
58
|
+
status What this machine and the control plane each believe about the
|
|
59
|
+
vault: allocation, policy, whether this keystore can sign, the
|
|
60
|
+
authenticated and materialized pins, any pending
|
|
61
|
+
unvaulted-override journals, and where the keystore lives. Never
|
|
62
|
+
reports key material.
|
|
63
|
+
policy Set the activation policy — \`required\` (a deploy must present a
|
|
64
|
+
vaulted capture) or \`grandfathered\` (it need not). Owner + step-up,
|
|
65
|
+
audited. \`grandfathered\` is the documented way out of a deploy
|
|
66
|
+
blocked by GITVAULT_CLIENT_UPGRADE_REQUIRED, and leaves a
|
|
67
|
+
doctor-persistent warning until the project returns to \`required\`.
|
|
68
|
+
push Capture the working tree and publish it. This is NOT gated on a
|
|
69
|
+
deploy — a vault-only project pushes for months without one.
|
|
70
|
+
Before reporting a push as landed the SDK compares finalization
|
|
71
|
+
receipts against the expected manifest and reads the admitted head
|
|
72
|
+
back from storage; a 200 alone is never enough.
|
|
73
|
+
compact Publish a checkpoint covering the canonical refs, every root
|
|
74
|
+
unexpired at the cutoff, and the HEAD target, under a maintenance
|
|
75
|
+
lease so a concurrent cycle cannot race it.
|
|
76
|
+
prune Plan a prune, and — with both verifier receipts — submit it.
|
|
77
|
+
Two phases, because the protocol is two-phase; see below.
|
|
78
|
+
verify Verify the head chain from the authenticated pin up to the newest
|
|
79
|
+
listed generation. Fails closed on a regression, a gap, or a
|
|
80
|
+
transition descriptor this client cannot validate.
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
--project <id> Project whose vault to act on (defaults to the active project)
|
|
84
|
+
--org <org_id> init: the owning organization (resolved from the project
|
|
85
|
+
when omitted)
|
|
86
|
+
--git-remote init: 'git init' the current directory when it is not a
|
|
87
|
+
repository yet, so the run402 remote can be added there.
|
|
88
|
+
Opt-in: creating a repository where you did not ask for one
|
|
89
|
+
is a bad surprise, so without it a non-repository directory
|
|
90
|
+
allocates the vault and adds no remote.
|
|
91
|
+
--no-remote init: allocate the vault only; touch no git configuration
|
|
92
|
+
--reason <why> policy: why the policy is changing — recorded in the audit
|
|
93
|
+
event. REQUIRED for \`grandfathered\`, which is a deliberate
|
|
94
|
+
weakening of the activation guarantee.
|
|
95
|
+
--refs status: also materialize and report the vault's ref map and
|
|
96
|
+
HEAD target. This is a VERIFICATION (it walks the head
|
|
97
|
+
chain and advances the local materialized pin), which is
|
|
98
|
+
why plain \`status\` — an observation — does not do it.
|
|
99
|
+
--repo <repo_id> Address the vault directly by id, skipping project lookup
|
|
100
|
+
--message <text> push: commit message for the synthetic commit a dirty tree
|
|
101
|
+
produces (a clean tree pushes HEAD itself, no message used)
|
|
102
|
+
--checkpoint push: force the checkpoint-bearing form regardless of delta size
|
|
103
|
+
--budget <n> verify: heads to verify in this call. The verified prefix is
|
|
104
|
+
persisted, so a budget-exceeded run resumes where it stopped
|
|
105
|
+
instead of restarting.
|
|
106
|
+
--submit prune: submit the planned intent. Requires --intent-core and
|
|
107
|
+
--verifier-receipt.
|
|
108
|
+
--intent-core <path>
|
|
109
|
+
prune: the plan's \`intent_core\`, saved verbatim from a prior
|
|
110
|
+
planning run. A rebuilt core carries a different nonce, so
|
|
111
|
+
the r402s-verify receipt would no longer bind to it.
|
|
112
|
+
--verifier-receipt <path>
|
|
113
|
+
prune: r402s-verify's \`verifier_receipt\` over that core.
|
|
114
|
+
--wait prune: poll the submitted intent until the control-plane-
|
|
115
|
+
signed completion appears, instead of returning immediately.
|
|
116
|
+
--json No-op: stdout is already JSON.
|
|
117
|
+
|
|
118
|
+
prune is TWO PHASES, because the protocol is:
|
|
119
|
+
1. \`run402 gitvault prune\` plans. It walks the verified chain, computes the GC
|
|
120
|
+
root set, subtracts it, and prints a SIGNED \`intent_core\` plus its
|
|
121
|
+
\`intent_core_sha256\`. Nothing is submitted and nothing is deleted.
|
|
122
|
+
2. Run \`r402s-verify\` against that core, then re-run with
|
|
123
|
+
\`--submit --intent-core <core.json> --verifier-receipt <receipt.json>\`.
|
|
124
|
+
The intent carries TWO receipts over the same core, one per implementation:
|
|
125
|
+
this CLI produces the \`run402-cli\` half by restoring the latest checkpoint
|
|
126
|
+
and recomputing its commitments, and \`r402s-verify\` produces the other.
|
|
127
|
+
A second receipt from this lineage would prove nothing, so it is never
|
|
128
|
+
synthesized here.
|
|
129
|
+
Only the control-plane-signed completion says what was deleted, and only its
|
|
130
|
+
\`deleted\` result means the bytes are gone — \`present_after_attempt\` is a
|
|
131
|
+
FAILED deletion, never counted as a success. There is deliberately no purge
|
|
132
|
+
verb in V0 at all. Retention is an operational promise of the platform, not a
|
|
133
|
+
cryptographic guarantee against it.
|
|
134
|
+
|
|
135
|
+
Expiry is permissive, by design:
|
|
136
|
+
A retention root whose \`effective_admitted_at\` this client cannot resolve is
|
|
137
|
+
RETAINED, and a compact that cannot obtain a retention-cutoff ticket keeps
|
|
138
|
+
every root. That costs storage, never history.
|
|
139
|
+
|
|
140
|
+
Terminal loss (protocol §0):
|
|
141
|
+
In V0-A, whole-machine or whole-keystore loss is terminal for vault history
|
|
142
|
+
until human envelopes ship. \`status\` prints the full statement verbatim on
|
|
143
|
+
stderr and carries it in its JSON — read it before you rely on this.
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
run402 gitvault init
|
|
147
|
+
run402 gitvault status --refs
|
|
148
|
+
run402 gitvault push --message "wip: refactor the parser"
|
|
149
|
+
run402 gitvault policy grandfathered --reason "migrating CI to a vaulted client"
|
|
150
|
+
run402 gitvault verify --budget 500
|
|
151
|
+
run402 gitvault prune --project prj_1a2b3c
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve which vault to act on, plus the local git tree.
|
|
156
|
+
*
|
|
157
|
+
* `--repo` addresses the vault directly (the cold-restart path: an agent that
|
|
158
|
+
* knows its repo_id needs no project lookup). Otherwise the project is
|
|
159
|
+
* resolved the CLI-wide way — `--project`, then RUN402_PROJECT_ID, then the
|
|
160
|
+
* active project — and the SDK resolves the vault from it.
|
|
161
|
+
*/
|
|
162
|
+
function vaultTarget(a) {
|
|
163
|
+
const repoId = flagValue(a, "--repo");
|
|
164
|
+
const project = flagValue(a, "--project");
|
|
165
|
+
const target = { repo_dir: process.cwd() };
|
|
166
|
+
if (repoId != null) target.repo_id = repoId;
|
|
167
|
+
// Only demand a project when one is actually needed: `--repo` alone is a
|
|
168
|
+
// complete address, and requiring an active project on top of it would make
|
|
169
|
+
// the cold-restart path fail for no reason.
|
|
170
|
+
if (repoId == null || project != null) target.project_id = resolveProjectId(project);
|
|
171
|
+
return target;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Print the protocol §0 terminal-loss statement.
|
|
176
|
+
*
|
|
177
|
+
* NORMATIVE COPY, printed verbatim straight from the SDK's own constants and
|
|
178
|
+
* never paraphrased, summarized, or reassembled here. Both lines also ride in
|
|
179
|
+
* the JSON payload on stdout.
|
|
180
|
+
*
|
|
181
|
+
* The PATH is printed with it. "Whole-keystore loss is terminal" appeared three
|
|
182
|
+
* times across this surface while the directory to back up appeared nowhere
|
|
183
|
+
* (dogfood #1, finding D2) — a warning nobody can act on.
|
|
184
|
+
*/
|
|
185
|
+
function printTerminalLoss(status) {
|
|
186
|
+
console.error("");
|
|
187
|
+
console.error(status.terminal_loss_statement);
|
|
188
|
+
console.error(status.terminal_loss_detail);
|
|
189
|
+
console.error(`Back up this directory: ${status.keystore.root}`);
|
|
190
|
+
console.error("");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Where the keystore lives — for verbs whose payload is not a `status`. */
|
|
194
|
+
async function printKeystoreLocation() {
|
|
195
|
+
try {
|
|
196
|
+
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
197
|
+
console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for vault history`);
|
|
198
|
+
} catch {
|
|
199
|
+
// Never let a diagnostic line fail a command that already succeeded.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `run402 gitvault init` — allocate the project's vault.
|
|
205
|
+
*
|
|
206
|
+
* WHY THIS EXISTS AS ITS OWN VERB (dogfood #1, finding A). Until it did, the
|
|
207
|
+
* only way to allocate was `sdk.gitvault.init()` through the vendored SDK:
|
|
208
|
+
* `gitvault status` pointed at `run402 init`, which scaffolds the remote and
|
|
209
|
+
* says so in a comment; `gitvault push` and `git push run402` both 404'd and
|
|
210
|
+
* handed the user a raw `POST /gitvault/v1/vaults`. A published CLI that can
|
|
211
|
+
* do everything except start is not a usable product.
|
|
212
|
+
*
|
|
213
|
+
* It stays SEPARATE from `run402 init` on purpose: this is the step that mints
|
|
214
|
+
* key material on this machine and emits a one-shot recovery receipt, and
|
|
215
|
+
* whole-keystore loss is terminal for vault history. That belongs to a command
|
|
216
|
+
* the user typed, not to a setup command's side effects.
|
|
217
|
+
*/
|
|
218
|
+
async function init(args) {
|
|
219
|
+
const a = normalizeArgv(args);
|
|
220
|
+
const valueFlags = ["--project", "--org"];
|
|
221
|
+
assertKnownFlags(a, [...valueFlags, "--git-remote", "--no-remote", "--help", "-h"], valueFlags);
|
|
222
|
+
requirePositionalCount(a, valueFlags, {
|
|
223
|
+
min: 0, max: 0, command: "run402 gitvault init", missing: "",
|
|
224
|
+
});
|
|
225
|
+
if (a.includes("--git-remote") && a.includes("--no-remote")) {
|
|
226
|
+
fail({
|
|
227
|
+
code: "BAD_USAGE",
|
|
228
|
+
message: "--git-remote and --no-remote contradict each other.",
|
|
229
|
+
hint: "--git-remote creates a repository to add the remote to; --no-remote touches no git configuration at all.",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const projectId = resolveProjectId(flagValue(a, "--project"));
|
|
233
|
+
const orgId = flagValue(a, "--org") ?? await resolveOwningOrgId(projectId);
|
|
234
|
+
if (!orgId) {
|
|
235
|
+
fail({
|
|
236
|
+
code: "ORG_UNRESOLVED",
|
|
237
|
+
message: `Could not resolve the organization that owns ${projectId}.`,
|
|
238
|
+
hint: "Pass --org <org_id>, or check that this wallet can see the project (`run402 projects list`).",
|
|
239
|
+
details: { project_id: projectId },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Whether to touch git at all. Mirrors `run402 init`: adding a remote inside
|
|
244
|
+
// an EXISTING repository is pure addition and is the default; CREATING a
|
|
245
|
+
// repository is opt-in, because a vault can be allocated from anywhere and
|
|
246
|
+
// `git init`-ing whatever directory you happened to be in is a bad surprise.
|
|
247
|
+
let scaffold = !a.includes("--no-remote");
|
|
248
|
+
let remoteSkipped = null;
|
|
249
|
+
if (scaffold && !a.includes("--git-remote")) {
|
|
250
|
+
const { hardenedGit } = await import("#sdk/node");
|
|
251
|
+
try {
|
|
252
|
+
await hardenedGit(process.cwd(), ["rev-parse", "--git-dir"]);
|
|
253
|
+
} catch {
|
|
254
|
+
scaffold = false;
|
|
255
|
+
remoteSkipped = "not a git repository — the vault was allocated; re-run with --git-remote to create one and add the remote";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const result = await getSdk().gitvault.init({
|
|
261
|
+
org_id: orgId,
|
|
262
|
+
project_id: projectId,
|
|
263
|
+
...(scaffold ? { repo_dir: process.cwd() } : { scaffold_git: false }),
|
|
264
|
+
});
|
|
265
|
+
console.log(JSON.stringify(remoteSkipped ? { ...result, remote_skipped: remoteSkipped } : result, null, 2));
|
|
266
|
+
console.error(
|
|
267
|
+
result.deduplicated
|
|
268
|
+
? `vault ${result.repo_id} already existed — nothing was re-allocated and no new key material was minted`
|
|
269
|
+
: `allocated vault ${result.repo_id} (genesis ${result.genesis_sha256})`,
|
|
270
|
+
);
|
|
271
|
+
if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url}`);
|
|
272
|
+
if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
|
|
273
|
+
// The recovery receipt is integrity data, not a secret, and it is worth
|
|
274
|
+
// exactly as much as the number of copies you keep. It is persisted into
|
|
275
|
+
// the keystore automatically; say where, because "keep many copies" is
|
|
276
|
+
// advice nobody can act on without a path.
|
|
277
|
+
console.error("");
|
|
278
|
+
console.error(result.terminal_loss_statement);
|
|
279
|
+
await printKeystoreLocation();
|
|
280
|
+
console.error("");
|
|
281
|
+
} catch (err) {
|
|
282
|
+
reportSdkError(err);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* `run402 gitvault policy <required|grandfathered>` — the activation gate.
|
|
288
|
+
*
|
|
289
|
+
* The gateway's own `GITVAULT_CLIENT_UPGRADE_REQUIRED` envelope names
|
|
290
|
+
* `run402 gitvault policy grandfathered --reason <why>` as the second way out
|
|
291
|
+
* of a blocked deploy. Until this verb existed, running exactly what the
|
|
292
|
+
* platform told you to run returned UNKNOWN_SUBCOMMAND, so a user could
|
|
293
|
+
* allocate themselves into a blocked-deploy state with no way back.
|
|
294
|
+
*/
|
|
295
|
+
async function policy(args) {
|
|
296
|
+
const a = normalizeArgv(args);
|
|
297
|
+
const valueFlags = [...COMMON_VALUE_FLAGS, "--reason"];
|
|
298
|
+
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
299
|
+
const [requested] = requirePositionalCount(a, valueFlags, {
|
|
300
|
+
min: 1, max: 1, command: "run402 gitvault policy <required|grandfathered>",
|
|
301
|
+
missing: "Missing <policy>. Expected `required` or `grandfathered`.",
|
|
302
|
+
});
|
|
303
|
+
if (requested !== "required" && requested !== "grandfathered") {
|
|
304
|
+
fail({
|
|
305
|
+
code: "BAD_USAGE",
|
|
306
|
+
message: `Unknown policy: ${requested}.`,
|
|
307
|
+
hint: "Expected `required` (a deploy must present a vaulted capture) or `grandfathered` (it need not).",
|
|
308
|
+
details: { policy: requested, known_policies: ["required", "grandfathered"] },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const reason = flagValue(a, "--reason");
|
|
312
|
+
// Required only for the weakening direction. Returning to `required` is
|
|
313
|
+
// restoring the default and needs no justification; leaving it does.
|
|
314
|
+
if (requested === "grandfathered" && (reason == null || reason.trim() === "")) {
|
|
315
|
+
fail({
|
|
316
|
+
code: "BAD_USAGE",
|
|
317
|
+
message: "`grandfathered` needs --reason <why>.",
|
|
318
|
+
hint: "It weakens the activation guarantee for this project and is recorded in the audit event. Say why, e.g. --reason \"migrating CI to a vaulted client\".",
|
|
319
|
+
details: { policy: requested },
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const target = vaultTarget(a);
|
|
324
|
+
try {
|
|
325
|
+
const sdk = getSdk();
|
|
326
|
+
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
327
|
+
const result = await sdk.gitvault.setPolicy(repoId, {
|
|
328
|
+
gitvault_policy: requested,
|
|
329
|
+
...(reason != null ? { reason } : {}),
|
|
330
|
+
});
|
|
331
|
+
console.log(JSON.stringify({ repo_id: repoId, ...result }, null, 2));
|
|
332
|
+
console.error(
|
|
333
|
+
result.changed
|
|
334
|
+
? `gitvault_policy is now ${result.gitvault_policy} (version ${result.gitvault_policy_version})`
|
|
335
|
+
: `gitvault_policy was already ${result.gitvault_policy} — nothing changed`,
|
|
336
|
+
);
|
|
337
|
+
for (const w of result.warnings ?? []) console.error(`warning (${w.kind}): ${w.message}`);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
reportSdkError(err);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function status(args) {
|
|
344
|
+
const a = normalizeArgv(args);
|
|
345
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--refs", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
346
|
+
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
347
|
+
min: 0, max: 0, command: "run402 gitvault status", missing: "",
|
|
348
|
+
});
|
|
349
|
+
const target = vaultTarget(a);
|
|
350
|
+
if (a.includes("--refs")) target.refs = true;
|
|
351
|
+
try {
|
|
352
|
+
const s = await getSdk().gitvault.status(target);
|
|
353
|
+
console.log(JSON.stringify(s, null, 2));
|
|
354
|
+
printTerminalLoss(s);
|
|
355
|
+
// Two facts the user otherwise has to leave the CLI for: which vault this
|
|
356
|
+
// checkout is wired to, and what the control plane says is in it.
|
|
357
|
+
if (s.remote) {
|
|
358
|
+
console.error(`remote '${s.remote.name}': ${s.remote.url}${s.remote.matches ? "" : " ← points at a DIFFERENT project than this status"}`);
|
|
359
|
+
}
|
|
360
|
+
if (s.refs) {
|
|
361
|
+
const names = Object.keys(s.refs).sort();
|
|
362
|
+
console.error(names.length === 0 ? "refs: (none yet)" : `refs (${names.length}):`);
|
|
363
|
+
for (const ref of names) console.error(` ${s.refs[ref]} ${ref}`);
|
|
364
|
+
if (s.head_target) {
|
|
365
|
+
console.error(s.head_target.kind === "symref" ? ` HEAD -> ${s.head_target.ref}` : ` HEAD ${s.head_target.oid} (detached)`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Advisories are echoed EXACTLY as the SDK reported them. Nothing is
|
|
369
|
+
// synthesized here — in particular a project that has never deployed gets
|
|
370
|
+
// no deploy-related warning, because a vault-only project is a first-class
|
|
371
|
+
// shape (protocol D183), not a half-configured deploy.
|
|
372
|
+
for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
|
|
373
|
+
for (const n of s.next_actions) console.error(`next: ${n.action}${n.command ? ` — ${n.command}` : ""}`);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
reportSdkError(err);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function push(args) {
|
|
380
|
+
const a = normalizeArgv(args);
|
|
381
|
+
const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
|
|
382
|
+
assertKnownFlags(a, [...valueFlags, "--checkpoint", "--help", "-h"], valueFlags);
|
|
383
|
+
requirePositionalCount(a, valueFlags, {
|
|
384
|
+
min: 0, max: 0, command: "run402 gitvault push", missing: "",
|
|
385
|
+
});
|
|
386
|
+
const message = flagValue(a, "--message");
|
|
387
|
+
const opts = {
|
|
388
|
+
...vaultTarget(a),
|
|
389
|
+
// The gitvault_commit line is progress, not payload: print it the moment
|
|
390
|
+
// the snapshot exists, well before the publication round-trips finish, so
|
|
391
|
+
// a human watching a slow push sees what is being pushed.
|
|
392
|
+
onCommitLine: (line) => console.error(line),
|
|
393
|
+
};
|
|
394
|
+
// The message rides on `snapshot`, which is what `captureSnapshot` reads —
|
|
395
|
+
// and, since 5.12b removed the dead top-level `push({ message })` field, is
|
|
396
|
+
// the ONE place it can ride.
|
|
397
|
+
if (message != null) opts.snapshot = { message };
|
|
398
|
+
if (a.includes("--checkpoint")) opts.checkpoint = true;
|
|
399
|
+
try {
|
|
400
|
+
const result = await getSdk().gitvault.push(opts);
|
|
401
|
+
console.log(JSON.stringify(result, null, 2));
|
|
402
|
+
console.error(`published generation ${result.generation} (${result.form})`);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
reportSdkError(err);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function compact(args) {
|
|
409
|
+
const a = normalizeArgv(args);
|
|
410
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
411
|
+
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
412
|
+
min: 0, max: 0, command: "run402 gitvault compact", missing: "",
|
|
413
|
+
});
|
|
414
|
+
try {
|
|
415
|
+
const result = await getSdk().gitvault.compact(vaultTarget(a));
|
|
416
|
+
console.log(JSON.stringify(result, null, 2));
|
|
417
|
+
console.error(
|
|
418
|
+
`checkpoint published at generation ${result.generation}: ` +
|
|
419
|
+
`${result.covered_refs} ref(s), ${result.covered_roots} retention root(s).`,
|
|
420
|
+
);
|
|
421
|
+
if (!result.cutoff_bound) {
|
|
422
|
+
// Say what actually happened rather than reporting a clean compaction:
|
|
423
|
+
// without a ticket no root can leave the map, so this run reclaimed
|
|
424
|
+
// nothing from expiry.
|
|
425
|
+
console.error(
|
|
426
|
+
"no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. " +
|
|
427
|
+
"The checkpoint published, but no expired root left the map; re-run compact once the ticket route answers.",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
reportSdkError(err);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Read a protocol object a prior planning run (or r402s-verify) wrote to disk. */
|
|
436
|
+
function readJsonFile(flag, path) {
|
|
437
|
+
let text;
|
|
438
|
+
try {
|
|
439
|
+
text = readFileSync(path, "utf8");
|
|
440
|
+
} catch (err) {
|
|
441
|
+
fail({
|
|
442
|
+
code: "BAD_USAGE",
|
|
443
|
+
message: `${flag} ${path} could not be read: ${err?.message ?? String(err)}`,
|
|
444
|
+
hint: "Point it at the file a prior `run402 gitvault prune` (or r402s-verify) wrote.",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
return JSON.parse(text);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
fail({
|
|
451
|
+
code: "BAD_USAGE",
|
|
452
|
+
message: `${flag} ${path} is not valid JSON: ${err?.message ?? String(err)}`,
|
|
453
|
+
hint: "Pass the file verbatim; do not reformat or re-serialize it.",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function prune(args) {
|
|
459
|
+
const a = normalizeArgv(args);
|
|
460
|
+
const valueFlags = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-receipt"];
|
|
461
|
+
assertKnownFlags(a, [...valueFlags, "--submit", "--wait", "--help", "-h"], valueFlags);
|
|
462
|
+
requirePositionalCount(a, valueFlags, {
|
|
463
|
+
min: 0, max: 0, command: "run402 gitvault prune", missing: "",
|
|
464
|
+
});
|
|
465
|
+
const submitting = a.includes("--submit");
|
|
466
|
+
const corePath = flagValue(a, "--intent-core");
|
|
467
|
+
const receiptPath = flagValue(a, "--verifier-receipt");
|
|
468
|
+
// Refuse the half-specified submit here rather than planning and silently
|
|
469
|
+
// discarding the flags — an agent that typed --submit meant to submit.
|
|
470
|
+
if (submitting && (corePath == null || receiptPath == null)) {
|
|
471
|
+
fail({
|
|
472
|
+
code: "BAD_USAGE",
|
|
473
|
+
message: "run402 gitvault prune --submit needs both --intent-core and --verifier-receipt.",
|
|
474
|
+
hint: "Plan first (`run402 gitvault prune`), save its `intent_core`, run r402s-verify against it, then submit both.",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (!submitting && (corePath != null || receiptPath != null)) {
|
|
478
|
+
fail({
|
|
479
|
+
code: "BAD_USAGE",
|
|
480
|
+
message: "--intent-core / --verifier-receipt only apply with --submit.",
|
|
481
|
+
hint: "Add --submit, or drop the flags to plan.",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
const opts = vaultTarget(a);
|
|
485
|
+
if (submitting) {
|
|
486
|
+
opts.submit = {
|
|
487
|
+
core: readJsonFile("--intent-core", corePath),
|
|
488
|
+
verifier_receipt: readJsonFile("--verifier-receipt", receiptPath),
|
|
489
|
+
};
|
|
490
|
+
if (a.includes("--wait")) opts.submit.wait = {};
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const result = await getSdk().gitvault.prune(opts);
|
|
494
|
+
console.log(JSON.stringify(result, null, 2));
|
|
495
|
+
// Never imply a deletion. State what actually happened, then reproduce the
|
|
496
|
+
// SDK's own note verbatim rather than summarizing it.
|
|
497
|
+
if (!result.submitted) {
|
|
498
|
+
console.error(
|
|
499
|
+
result.blocked_reason
|
|
500
|
+
? `planned — nothing to submit: ${result.blocked_reason}`
|
|
501
|
+
: `planned — nothing submitted. ${result.object_candidates.length} object(s) proposed for deletion` +
|
|
502
|
+
`${result.deferred_object_count > 0 ? ` (${result.deferred_object_count} more deferred to a later intent)` : ""}` +
|
|
503
|
+
`; ${result.eligible_count} retention root(s) past their window, ${result.retained_count} retained.`,
|
|
504
|
+
);
|
|
505
|
+
if (result.intent_core_sha256) {
|
|
506
|
+
console.error(`intent_core_sha256: ${result.intent_core_sha256} — run r402s-verify against this core, then re-run with --submit.`);
|
|
507
|
+
}
|
|
508
|
+
} else if (result.confirmation?.outcome) {
|
|
509
|
+
console.error(
|
|
510
|
+
`submitted — the signed completion reports ${result.confirmation.deleted.length} deleted, ` +
|
|
511
|
+
`${result.confirmation.present.length} still present` +
|
|
512
|
+
`${result.confirmation.unadjudicated.length > 0 ? `, ${result.confirmation.unadjudicated.length} unadjudicated` : ""}.`,
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
console.error("submitted — no completion yet. Nothing is deleted until the control-plane-signed completion says so; re-run with --wait or poll the intent.");
|
|
516
|
+
}
|
|
517
|
+
console.error(result.note);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
reportSdkError(err);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function verify(args) {
|
|
524
|
+
const a = normalizeArgv(args);
|
|
525
|
+
const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
|
|
526
|
+
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
527
|
+
requirePositionalCount(a, valueFlags, {
|
|
528
|
+
min: 0, max: 0, command: "run402 gitvault verify", missing: "",
|
|
529
|
+
});
|
|
530
|
+
const target = vaultTarget(a);
|
|
531
|
+
const budget = flagValue(a, "--budget");
|
|
532
|
+
if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
|
|
533
|
+
try {
|
|
534
|
+
const state = await getSdk().gitvault.verify(target);
|
|
535
|
+
console.log(JSON.stringify(state, null, 2));
|
|
536
|
+
console.error(`verified through generation ${state.generation}`);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
reportSdkError(err);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function run(sub, args) {
|
|
543
|
+
const argv = Array.isArray(args) ? args : [];
|
|
544
|
+
if (!sub || hasHelp([sub, ...argv])) {
|
|
545
|
+
console.log(HELP);
|
|
546
|
+
process.exit(0);
|
|
547
|
+
}
|
|
548
|
+
switch (sub) {
|
|
549
|
+
case "init": {
|
|
550
|
+
await init(argv);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case "policy": {
|
|
554
|
+
await policy(argv);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case "status": {
|
|
558
|
+
await status(argv);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case "push": {
|
|
562
|
+
await push(argv);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case "compact": {
|
|
566
|
+
await compact(argv);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "prune": {
|
|
570
|
+
await prune(argv);
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case "verify": {
|
|
574
|
+
await verify(argv);
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
default:
|
|
578
|
+
failUnknownSubcommand("gitvault", sub, {
|
|
579
|
+
hint: "Run `run402 gitvault --help` for usage.",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|