run402 4.25.0 → 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 +1 -1
- package/git-remote-run402.mjs +80 -22
- package/lib/command-manifest.mjs +2 -0
- package/lib/deploy-v2.mjs +78 -1
- package/lib/doctor.mjs +80 -1
- package/lib/gitvault.mjs +218 -6
- package/lib/init.mjs +48 -42
- package/lib/org-context.mjs +44 -1
- package/package.json +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +50 -2
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +74 -2
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.d.ts +37 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +52 -1
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
- package/sdk/dist/node/index.d.ts +2 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +1 -1
- package/sdk/dist/node/index.js.map +1 -1
package/cli.mjs
CHANGED
|
@@ -49,7 +49,7 @@ Commands:
|
|
|
49
49
|
rooms Coordinate with the other agents on your project (who/send/ack)
|
|
50
50
|
escalations Page a human when you judge you need one (raise/list/ack)
|
|
51
51
|
claims Say what you're working on before you collide (advisory)
|
|
52
|
-
gitvault Host-blind encrypted Git remote (status/push/compact/prune/verify)
|
|
52
|
+
gitvault Host-blind encrypted Git remote (init/status/push/policy/compact/prune/verify)
|
|
53
53
|
errors Grouped error fingerprints + a promote/revert verdict (release-baselined)
|
|
54
54
|
jobs Submit and inspect platform-managed jobs
|
|
55
55
|
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
package/git-remote-run402.mjs
CHANGED
|
@@ -19,6 +19,17 @@
|
|
|
19
19
|
* push [+]<src>:<dst> → publish one atomic ref transaction
|
|
20
20
|
* option <name> <value> → ok / unsupported, never a silent lie
|
|
21
21
|
*
|
|
22
|
+
* WHICH REPOSITORY (the fail-closed rule). `process.cwd()` is NOT the
|
|
23
|
+
* repository. git identifies the repository with `GIT_DIR`, and during
|
|
24
|
+
* `git clone` cwd is the directory clone was RUN FROM — routinely some other,
|
|
25
|
+
* unrelated repository. Discovering the repository from cwd therefore wrote a
|
|
26
|
+
* vault's DECRYPTED objects into a repository the user never named, silently,
|
|
27
|
+
* on every clone (dogfood #1). Every repository-touching command now resolves
|
|
28
|
+
* through the SDK's `resolveGitInvocationRepo`, which proves `GIT_DIR` names a
|
|
29
|
+
* real repository and refuses otherwise; a refusal writes nothing at all.
|
|
30
|
+
* `capabilities`, `option` and `list` need no repository and are unaffected,
|
|
31
|
+
* so a repository-free `git ls-remote run402::<org>/<project>` still works.
|
|
32
|
+
*
|
|
22
33
|
* NOT advertised, deliberately: `list` is a COMMAND in this protocol, not a
|
|
23
34
|
* capability keyword — git's capability vocabulary is fetch/push/import/export/
|
|
24
35
|
* connect/stateless-connect/option/refspec/check-connectivity/object-format/
|
|
@@ -39,15 +50,17 @@
|
|
|
39
50
|
* `run402 gitvault push`, which sets the HEAD target from the local HEAD.
|
|
40
51
|
* - `option dry-run` is `unsupported`: this helper cannot rehearse a
|
|
41
52
|
* publication, and reporting a fake success would be worse than refusing.
|
|
42
|
-
* -
|
|
43
|
-
*
|
|
44
|
-
*
|
|
53
|
+
* - `fetch` and `push` REQUIRE the `GIT_DIR` git sets when it drives a
|
|
54
|
+
* helper against a repository, so running this binary by hand from a shell
|
|
55
|
+
* is refused rather than silently pointed at the current directory. Only
|
|
56
|
+
* `capabilities`, `option` and `list` work without one, which is exactly
|
|
57
|
+
* the set `git ls-remote <url>` outside a checkout needs.
|
|
45
58
|
*/
|
|
46
59
|
|
|
47
60
|
import { createInterface } from "node:readline";
|
|
48
61
|
import { getSdk } from "./lib/sdk.mjs";
|
|
49
62
|
import { parseGitvaultRemoteUrl } from "#sdk";
|
|
50
|
-
import { hardenedGit } from "#sdk/node";
|
|
63
|
+
import { hardenedGit, resolveGitInvocationRepo } from "#sdk/node";
|
|
51
64
|
|
|
52
65
|
const out = (line) => process.stdout.write(`${line}\n`);
|
|
53
66
|
/** Every helper response block is terminated by a blank line. */
|
|
@@ -107,12 +120,35 @@ async function main(argv) {
|
|
|
107
120
|
return 1;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
const
|
|
111
|
-
const target = { project_id: address.project_id, repo_dir: repoDir };
|
|
123
|
+
const target = { project_id: address.project_id };
|
|
112
124
|
let verbosity = 1;
|
|
113
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The repository git invoked us for, resolved once and PROVEN.
|
|
128
|
+
*
|
|
129
|
+
* Deliberately lazy: `list` needs no repository, so `git ls-remote` outside
|
|
130
|
+
* any checkout keeps working. Deliberately not cached across a failure
|
|
131
|
+
* either — a refusal is terminal for the command that asked, and there is
|
|
132
|
+
* nothing to retry.
|
|
133
|
+
*/
|
|
134
|
+
let resolvedRepo = null;
|
|
135
|
+
async function requireRepo() {
|
|
136
|
+
if (!resolvedRepo) resolvedRepo = await resolveGitInvocationRepo(process.env, process.cwd());
|
|
137
|
+
return resolvedRepo.repo_dir;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What to tell a human when we refuse. `git clone` is the case that used to
|
|
142
|
+
* fail; naming the working alternative beats a bare error.
|
|
143
|
+
*/
|
|
144
|
+
function repoRefusalNote(err) {
|
|
145
|
+
note(describeError(err));
|
|
146
|
+
note("refusing to touch a repository git did not name — nothing was read or written.");
|
|
147
|
+
note(`if you meant to restore this vault: git init --bare <dir> && git -C <dir> remote add run402 run402::${address.org_id}/${address.project_id} && git -C <dir> fetch run402 '+refs/heads/*:refs/heads/*'`);
|
|
148
|
+
}
|
|
149
|
+
|
|
114
150
|
/** Open the vault lazily — `capabilities` and `option` must never touch the network. */
|
|
115
|
-
const openVault = async () => (await getSdk().gitvault.open(target)).vault;
|
|
151
|
+
const openVault = async (repoDir) => (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
|
|
116
152
|
|
|
117
153
|
async function runList() {
|
|
118
154
|
const state = await (await openVault()).materialize();
|
|
@@ -128,30 +164,49 @@ async function main(argv) {
|
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
async function runFetch(batch) {
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
// Resolve the target repository BEFORE a single byte is decrypted: a
|
|
168
|
+
// refusal here must leave no objects anywhere. This is what makes `clone`
|
|
169
|
+
// work (git names the fresh repo in `GIT_DIR`) and what stops a clone run
|
|
170
|
+
// from inside an unrelated checkout from writing into that checkout.
|
|
171
|
+
let repoDir;
|
|
172
|
+
try {
|
|
173
|
+
repoDir = await requireRepo();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
repoRefusalNote(err);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
if (verbosity >= 1) note(`restoring the vault object database for ${batch.length} ref(s) into ${repoDir}`);
|
|
179
|
+
const restored = await getSdk().gitvault.restore({ ...target, repo_dir: repoDir, target_dir: repoDir });
|
|
133
180
|
if (verbosity >= 1) note(`restored generation ${restored.generation}`);
|
|
134
181
|
endBlock();
|
|
182
|
+
return 0;
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
async function runPush(batch) {
|
|
138
186
|
const specs = batch.map(parsePushSpec);
|
|
139
187
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
// Repository first, then every source revision, and only then the
|
|
189
|
+
// network: a push that names a ref this repository does not have must
|
|
190
|
+
// fail locally rather than after opening the vault.
|
|
191
|
+
const repoDir = await requireRepo();
|
|
192
|
+
const newOids = new Map();
|
|
143
193
|
for (const spec of specs) {
|
|
144
|
-
const expectedOld = base.refs?.[spec.dst] ?? null;
|
|
145
194
|
// A deletion carries an empty <src>. Everything else is resolved by
|
|
146
195
|
// git itself; `--end-of-options` keeps a hostile refname from being
|
|
147
196
|
// read as a flag.
|
|
148
|
-
|
|
197
|
+
newOids.set(spec, spec.src === ""
|
|
149
198
|
? null
|
|
150
|
-
: (await hardenedGit(repoDir, ["rev-parse", "--verify", "--end-of-options", spec.src])).text().trim();
|
|
199
|
+
: (await hardenedGit(repoDir, ["rev-parse", "--verify", "--end-of-options", spec.src])).text().trim());
|
|
200
|
+
}
|
|
201
|
+
const vault = await openVault(repoDir);
|
|
202
|
+
const base = await vault.materialize();
|
|
203
|
+
const updates = [];
|
|
204
|
+
for (const spec of specs) {
|
|
205
|
+
const expectedOld = base.refs?.[spec.dst] ?? null;
|
|
151
206
|
updates.push({
|
|
152
207
|
ref: spec.dst,
|
|
153
208
|
expected_old_oid: expectedOld,
|
|
154
|
-
new_oid:
|
|
209
|
+
new_oid: newOids.get(spec),
|
|
155
210
|
// Force-with-lease still requires a lease, so a CREATE is never
|
|
156
211
|
// forced. The SDK owns what force actually permits.
|
|
157
212
|
force: spec.force && expectedOld !== null,
|
|
@@ -166,10 +221,12 @@ async function main(argv) {
|
|
|
166
221
|
} catch (err) {
|
|
167
222
|
// The transaction is atomic, so a failure failed every ref in it. Report
|
|
168
223
|
// it against each one rather than letting some look like they landed.
|
|
224
|
+
if (err?.code === "GIT_INVOCATION_REPO_UNRESOLVED") repoRefusalNote(err);
|
|
169
225
|
const reason = describeError(err);
|
|
170
226
|
for (const spec of specs) out(`error ${spec.dst} ${reason}`);
|
|
171
227
|
}
|
|
172
228
|
endBlock();
|
|
229
|
+
return 0;
|
|
173
230
|
}
|
|
174
231
|
|
|
175
232
|
function handleOption(name, value) {
|
|
@@ -200,18 +257,19 @@ async function main(argv) {
|
|
|
200
257
|
let fetchBatch = [];
|
|
201
258
|
let pushBatch = [];
|
|
202
259
|
|
|
260
|
+
/** Returns the process exit code the flushed batch demands (0 = keep going). */
|
|
203
261
|
async function flushBatches() {
|
|
204
262
|
if (fetchBatch.length > 0) {
|
|
205
263
|
const batch = fetchBatch;
|
|
206
264
|
fetchBatch = [];
|
|
207
|
-
await runFetch(batch);
|
|
208
|
-
return;
|
|
265
|
+
return await runFetch(batch);
|
|
209
266
|
}
|
|
210
267
|
if (pushBatch.length > 0) {
|
|
211
268
|
const batch = pushBatch;
|
|
212
269
|
pushBatch = [];
|
|
213
|
-
await runPush(batch);
|
|
270
|
+
return await runPush(batch);
|
|
214
271
|
}
|
|
272
|
+
return 0;
|
|
215
273
|
}
|
|
216
274
|
|
|
217
275
|
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
@@ -219,7 +277,8 @@ async function main(argv) {
|
|
|
219
277
|
for await (const raw of rl) {
|
|
220
278
|
const line = raw.replace(/\r$/, "");
|
|
221
279
|
if (line === "") {
|
|
222
|
-
await flushBatches();
|
|
280
|
+
const code = await flushBatches();
|
|
281
|
+
if (code !== 0) return code;
|
|
223
282
|
continue;
|
|
224
283
|
}
|
|
225
284
|
const space = line.indexOf(" ");
|
|
@@ -254,8 +313,7 @@ async function main(argv) {
|
|
|
254
313
|
// EOF. Git always terminates a batch with a blank line, but flushing here
|
|
255
314
|
// means a truncated stream still does the work it already asked for
|
|
256
315
|
// instead of silently dropping it.
|
|
257
|
-
await flushBatches();
|
|
258
|
-
return 0;
|
|
316
|
+
return await flushBatches();
|
|
259
317
|
} finally {
|
|
260
318
|
rl.close();
|
|
261
319
|
}
|
package/lib/command-manifest.mjs
CHANGED
|
@@ -238,8 +238,10 @@ export const COMMAND_MANIFEST = [
|
|
|
238
238
|
// all but `status`) a local git working tree, so the gate runs structural
|
|
239
239
|
// checks only — an in-process behavioral run would either no-op against the
|
|
240
240
|
// universal `{}` fetch mock or touch the gate's own checkout.
|
|
241
|
+
{ path: ["gitvault", "init"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "allocates a vault: mints key material on this machine and runs the six-stage creation journal" },
|
|
241
242
|
{ path: ["gitvault", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the local principal keystore and the live vault record" },
|
|
242
243
|
{ path: ["gitvault", "push"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "captures the cwd git working tree and publishes a signed head" },
|
|
244
|
+
{ path: ["gitvault", "policy"], positionals: [p("gitvault_policy")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["required"], runStyle: "sub", skipBehavioral: "owner + step-up mutation of the live project's activation policy" },
|
|
243
245
|
{ path: ["gitvault", "compact"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "takes a maintenance lease and builds a checkpoint from the local repository" },
|
|
244
246
|
{ path: ["gitvault", "prune"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "materializes the live vault head to enumerate retention roots" },
|
|
245
247
|
{ path: ["gitvault", "verify"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain against the keystore's authenticated pin" },
|
package/lib/deploy-v2.mjs
CHANGED
|
@@ -1349,11 +1349,88 @@ const CI_DEPLOY_ERROR_GUIDANCE = {
|
|
|
1349
1349
|
};
|
|
1350
1350
|
|
|
1351
1351
|
function reportDeployApplyError(err, useGithubActionsOidc) {
|
|
1352
|
-
const warningEnhanced = enhanceDeployWarningError(err);
|
|
1352
|
+
const warningEnhanced = enhanceGitvaultDeployError(enhanceDeployWarningError(err));
|
|
1353
1353
|
if (!useGithubActionsOidc) return reportSdkError(warningEnhanced);
|
|
1354
1354
|
return reportSdkError(enhanceCiDeployError(warningEnhanced));
|
|
1355
1355
|
}
|
|
1356
1356
|
|
|
1357
|
+
/**
|
|
1358
|
+
* Whether THIS client's deploy lane speaks the gitvault protocol.
|
|
1359
|
+
*
|
|
1360
|
+
* `"unsupported"` is a statement of fact about the code in this file: it
|
|
1361
|
+
* declares no capture at plan time and sends no gitvault block on commit, so a
|
|
1362
|
+
* project with `gitvault_policy: required` refuses its commits. The vault
|
|
1363
|
+
* itself works — `run402 gitvault push` and `git push run402` publish captures
|
|
1364
|
+
* — it is the DEPLOY lane that has no capture wiring.
|
|
1365
|
+
*
|
|
1366
|
+
* RETIREMENT CONDITION: flip this to `"supported"` in the same change that
|
|
1367
|
+
* makes `applyCmd` declare `{capture_id, snapshot_oid_hmac}` on
|
|
1368
|
+
* `POST /apply/v1/plans` and present an activation token on
|
|
1369
|
+
* `POST /apply/v1/plans/:id/commit` (protocol §6.5). Doing that faithfully also
|
|
1370
|
+
* means building the deploy artifacts from an isolated materialization of the
|
|
1371
|
+
* snapshot commit, which is why it is a design change and not a wiring one:
|
|
1372
|
+
* build output is normally gitignored, so it is not in the snapshot at all.
|
|
1373
|
+
* `cli-deploy-gitvault-advisory.test.mjs` fails if the flag and the advisory
|
|
1374
|
+
* ever disagree.
|
|
1375
|
+
*/
|
|
1376
|
+
export const GITVAULT_DEPLOY_LANE = "unsupported";
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Tell the truth about a deploy the vault gate refused.
|
|
1380
|
+
*
|
|
1381
|
+
* The gateway's `GITVAULT_CLIENT_UPGRADE_REQUIRED` envelope leads with
|
|
1382
|
+
* `upgrade_client` / `npm i -g run402@latest`, which is correct in principle
|
|
1383
|
+
* and WRONG right now: no published `run402` has a gitvault-capable deploy
|
|
1384
|
+
* lane, so upgrading changes nothing about this refusal (dogfood #1, finding
|
|
1385
|
+
* C). Relaying it unedited sends an agent into an upgrade loop. The gateway's
|
|
1386
|
+
* SECOND action — grandfather the policy — is real and reachable, and this
|
|
1387
|
+
* client now has the verb for it.
|
|
1388
|
+
*
|
|
1389
|
+
* The gateway's own actions are PRESERVED, never dropped: they are re-ordered
|
|
1390
|
+
* behind an honest one, and the upgrade action keeps its place with its
|
|
1391
|
+
* promise corrected. When the lane ships, the flag above retires this whole
|
|
1392
|
+
* function's rewrite and the envelope passes through untouched.
|
|
1393
|
+
*/
|
|
1394
|
+
export function enhanceGitvaultDeployError(err) {
|
|
1395
|
+
const body = err?.body && typeof err.body === "object" && !Array.isArray(err.body) ? err.body : {};
|
|
1396
|
+
const code = body.code || err?.code || null;
|
|
1397
|
+
if (code !== "GITVAULT_CLIENT_UPGRADE_REQUIRED") return err;
|
|
1398
|
+
if (GITVAULT_DEPLOY_LANE === "supported") return err;
|
|
1399
|
+
|
|
1400
|
+
const gatewayActions = Array.isArray(body.next_actions) ? body.next_actions : [];
|
|
1401
|
+
const enhanced = Object.assign(new Error(err?.message || body.message || code), err);
|
|
1402
|
+
enhanced.body = {
|
|
1403
|
+
...body,
|
|
1404
|
+
hint:
|
|
1405
|
+
"This project requires a vaulted capture on deploy, and this run402 CLI's deploy lane does not produce one — " +
|
|
1406
|
+
"upgrading the CLI does not currently fix it. Either grandfather the policy (owner + step-up) and deploy, or " +
|
|
1407
|
+
"keep the project vaulted and deploy later. Your source can still be vaulted today: `run402 gitvault push` and " +
|
|
1408
|
+
"`git push run402 <branch>` are not gated on a deploy.",
|
|
1409
|
+
next_actions: [
|
|
1410
|
+
editRequestAction(
|
|
1411
|
+
"run402 gitvault policy grandfathered --reason \"<why>\"",
|
|
1412
|
+
"Un-gate this project so deploys activate without a vaulted capture. Owner + step-up, audited, and it leaves a doctor-persistent warning until you return the project to `required`.",
|
|
1413
|
+
),
|
|
1414
|
+
editRequestAction(
|
|
1415
|
+
"run402 gitvault push",
|
|
1416
|
+
"Capture and publish your source into the vault. Independent of deploy — a vault-only project pushes for months without one.",
|
|
1417
|
+
),
|
|
1418
|
+
editRequestAction(
|
|
1419
|
+
"run402 gitvault policy required",
|
|
1420
|
+
"Restore the gate once a gitvault-capable deploy lane is available.",
|
|
1421
|
+
),
|
|
1422
|
+
// Preserved verbatim in shape, with the promise corrected: the gateway
|
|
1423
|
+
// is describing the eventual client, not one you can install today.
|
|
1424
|
+
...gatewayActions.map((action) =>
|
|
1425
|
+
action?.type === "upgrade_client"
|
|
1426
|
+
? { ...action, why: "Tracks the eventual gitvault-capable deploy lane. No published run402 has one yet, so this does not resolve the refusal today." }
|
|
1427
|
+
: action,
|
|
1428
|
+
),
|
|
1429
|
+
],
|
|
1430
|
+
};
|
|
1431
|
+
return enhanced;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1357
1434
|
function enhanceDeployWarningError(err) {
|
|
1358
1435
|
const existingBody = err?.body && typeof err.body === "object" && !Array.isArray(err.body)
|
|
1359
1436
|
? err.body
|
package/lib/doctor.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, statSync } from "node:fs";
|
|
15
|
-
import { configDir, readAllowance, loadKeyStore } from "./config.mjs";
|
|
15
|
+
import { configDir, readAllowance, loadKeyStore, getActiveProjectId } from "./config.mjs";
|
|
16
16
|
import { getSdk } from "./sdk.mjs";
|
|
17
17
|
import {
|
|
18
18
|
resolveScanRoot,
|
|
@@ -58,6 +58,10 @@ Checks performed:
|
|
|
58
58
|
- Function runtime staleness: deployed functions running an older platform
|
|
59
59
|
runtime than the current gateway build (refresh with 'run402 functions
|
|
60
60
|
rebuild --all'; re-bundles from your stored source, no source change)
|
|
61
|
+
- gitvault: the active project's vault — activation policy, whether THIS
|
|
62
|
+
machine can produce the capture a 'required' policy demands, open
|
|
63
|
+
unvaulted-override journals, and where the keystore lives (back it up:
|
|
64
|
+
whole-keystore loss is terminal for vault history)
|
|
61
65
|
- Source scan: hallucinated SDK auth names (R402_AUTH_UNKNOWN_EXPORT),
|
|
62
66
|
state-changing GET handlers (R402_AUTH_STATE_CHANGING_GET),
|
|
63
67
|
auth.* calls in prerendered pages (R402_AUTH_PRERENDERED),
|
|
@@ -392,6 +396,81 @@ export async function run(sub, args = []) {
|
|
|
392
396
|
});
|
|
393
397
|
}
|
|
394
398
|
|
|
399
|
+
// 6c. gitvault (add-gitvault). Doctor was completely silent about the vault
|
|
400
|
+
// even when `gitvault_policy: required` was the single thing that would break
|
|
401
|
+
// the project's next deploy (dogfood #1, finding D1) — and doctor is where a
|
|
402
|
+
// user looks when something is wrong. It also prints WHERE the keystore is:
|
|
403
|
+
// "whole-keystore loss is terminal" was stated three times across this
|
|
404
|
+
// surface while the directory to back up was stated nowhere (finding D2).
|
|
405
|
+
//
|
|
406
|
+
// Read-only and best-effort in every branch: no project, no vault, or a
|
|
407
|
+
// gateway that does not know gitvault are all ordinary and report `skipped`
|
|
408
|
+
// or `ok`, never a doctor failure. A vault-only project that has never
|
|
409
|
+
// deployed is a first-class shape (protocol D183), so its mere absence of a
|
|
410
|
+
// deploy raises nothing.
|
|
411
|
+
{
|
|
412
|
+
const projectId = (process.env.RUN402_PROJECT_ID || "").trim() || getActiveProjectId() || null;
|
|
413
|
+
if (!projectId) {
|
|
414
|
+
checks.push({
|
|
415
|
+
name: "gitvault",
|
|
416
|
+
status: "skipped",
|
|
417
|
+
...(verbose && { hint: "no active project — run 'run402 projects use <project_id>' to check its vault." }),
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
try {
|
|
421
|
+
const gv = await getSdk().gitvault.status({ project_id: projectId, repo_dir: process.cwd() });
|
|
422
|
+
const value = {
|
|
423
|
+
project_id: projectId,
|
|
424
|
+
repo_id: gv.repo_id,
|
|
425
|
+
vault: gv.vault === null ? null : "allocated",
|
|
426
|
+
gitvault_policy: gv.gitvault_policy,
|
|
427
|
+
keystore_root: gv.keystore.root,
|
|
428
|
+
can_sign: gv.keystore.can_sign,
|
|
429
|
+
holds_repo_key: gv.keystore.holds_repo_key,
|
|
430
|
+
pending_overrides: gv.pending_overrides,
|
|
431
|
+
pins: gv.pins,
|
|
432
|
+
remote: gv.remote,
|
|
433
|
+
};
|
|
434
|
+
const gaps = [];
|
|
435
|
+
// The one that actually breaks the next deploy: the project demands a
|
|
436
|
+
// vaulted capture and THIS machine cannot produce one.
|
|
437
|
+
if (gv.gitvault_policy === "required" && !gv.keystore.holds_repo_key) {
|
|
438
|
+
gaps.push(
|
|
439
|
+
"gitvault_policy is 'required' but this machine holds no key for the vault — a deploy from here is refused with GITVAULT_CLIENT_UPGRADE_REQUIRED. " +
|
|
440
|
+
"Run 'run402 gitvault init' (idempotent; resolves to the existing vault), or 'run402 gitvault policy grandfathered --reason <why>' to un-gate the project.",
|
|
441
|
+
);
|
|
442
|
+
} else if (gv.gitvault_policy === "required" && !gv.keystore.can_sign) {
|
|
443
|
+
gaps.push("gitvault_policy is 'required' and this keystore is read-only (no signing key) — it can verify but cannot publish the capture a deploy needs");
|
|
444
|
+
}
|
|
445
|
+
if (gv.pending_overrides > 0) {
|
|
446
|
+
gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 gitvault push' to drain them`);
|
|
447
|
+
}
|
|
448
|
+
if (gv.remote && !gv.remote.matches) {
|
|
449
|
+
gaps.push(`the '${gv.remote.name}' git remote points at a different project than ${projectId} (${gv.remote.url})`);
|
|
450
|
+
}
|
|
451
|
+
// Echoed exactly as the SDK reported them — including the
|
|
452
|
+
// doctor-persistent `grandfathered` advisory it owns.
|
|
453
|
+
for (const w of gv.warnings ?? []) gaps.push(`${w.kind}: ${w.message}`);
|
|
454
|
+
checks.push({
|
|
455
|
+
name: "gitvault",
|
|
456
|
+
status: gaps.length > 0 ? "warning" : "ok",
|
|
457
|
+
value: gaps.length > 0 ? { ...value, gaps } : value,
|
|
458
|
+
hint: gv.vault === null
|
|
459
|
+
? `No vault for this project (that is a normal shape). Allocate one with 'run402 gitvault init'. Keystore: ${gv.keystore.root}`
|
|
460
|
+
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history.`,
|
|
461
|
+
});
|
|
462
|
+
} catch (err) {
|
|
463
|
+
// A gateway without gitvault, an unreachable API, or a project this
|
|
464
|
+
// wallet cannot see. None of those is a local health problem.
|
|
465
|
+
checks.push({
|
|
466
|
+
name: "gitvault",
|
|
467
|
+
status: "skipped",
|
|
468
|
+
message: describeCheckFailure("gitvault status check", err),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
395
474
|
// 7. Source-tree scan (auth-aware-ssr Section 9). Detects hallucinated
|
|
396
475
|
// SDK names, state-changing GETs, auth.* in prerendered pages, and
|
|
397
476
|
// direct mutation of internal.sessions.authz_version. Hits with severity
|