run402 4.27.0 → 4.29.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/lib/command-manifest.mjs +2 -0
- package/lib/deploy-v2.mjs +57 -86
- package/lib/init.mjs +23 -0
- package/lib/org-context.mjs +5 -0
- package/lib/org.mjs +102 -0
- package/lib/wallet-context.mjs +41 -1
- package/lib/wallets.mjs +10 -6
- package/package.json +1 -1
- package/sdk/dist/namespaces/deploy.d.ts +9 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +38 -5
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +44 -0
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-apply.d.ts +104 -0
- package/sdk/dist/node/gitvault-apply.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-apply.js +192 -0
- package/sdk/dist/node/gitvault-apply.js.map +1 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js +1 -1
- package/sdk/dist/node/gitvault-deploy.d.ts +58 -17
- package/sdk/dist/node/gitvault-deploy.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-deploy.js +92 -17
- package/sdk/dist/node/gitvault-deploy.js.map +1 -1
- package/sdk/dist/node/gitvault-keystore.d.ts +1 -1
- package/sdk/dist/node/gitvault-keystore.js +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +1 -1
- package/sdk/dist/node/gitvault-publication.js +2 -2
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.d.ts +58 -1
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +129 -39
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
- package/sdk/dist/node/index.d.ts +6 -4
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +6 -2
- package/sdk/dist/node/index.js.map +1 -1
package/lib/command-manifest.mjs
CHANGED
|
@@ -202,6 +202,8 @@ export const COMMAND_MANIFEST = [
|
|
|
202
202
|
{ path: ["org", "use"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["11111111-2222-3333-4444-555555555555"] },
|
|
203
203
|
{ path: ["org", "current"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
204
204
|
{ path: ["org", "clear"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
205
|
+
{ path: ["org", "bind"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "11111111-2222-3333-4444-555555555555"] },
|
|
206
|
+
{ path: ["org", "unbind"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
205
207
|
{ path: ["org", "member", "list"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
|
|
206
208
|
{ path: ["org", "member", "add"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--wallet", "0x1111111111111111111111111111111111111111"] },
|
|
207
209
|
{ path: ["org", "member", "role"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--principal", "prn_gate1", "--role", "viewer"] },
|
package/lib/deploy-v2.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { existsSync, fstatSync, readFileSync } from "node:fs";
|
|
26
26
|
import { resolve, dirname, extname, isAbsolute } from "node:path";
|
|
27
27
|
import {
|
|
28
|
+
applyWithGitvault,
|
|
28
29
|
buildDeployResolveSummary,
|
|
29
30
|
githubActionsCredentials,
|
|
30
31
|
loadDeployManifest,
|
|
@@ -1241,17 +1242,52 @@ async function applyCmd(args) {
|
|
|
1241
1242
|
const requiredPlan = opts.mode && opts.mode.kind === "applyReviewed"
|
|
1242
1243
|
? { planId: opts.mode.planId, ...(opts.mode.planFingerprint ? { planFingerprint: opts.mode.planFingerprint } : {}) }
|
|
1243
1244
|
: undefined;
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1245
|
+
// A thin shim over the SDK lane (add-gitvault 5.0). `applyWithGitvault`
|
|
1246
|
+
// takes the project's `gitvault_policy` into account: a project that is
|
|
1247
|
+
// not `required` runs the same `apply()` this line always ran, with the
|
|
1248
|
+
// same options object; a `required` one captures, publishes, mints an
|
|
1249
|
+
// activation token, and commits with it.
|
|
1250
|
+
const outcome = await withAutoApprove(() =>
|
|
1251
|
+
applyWithGitvault({
|
|
1252
|
+
sdk: getSdk(sdkOpts),
|
|
1253
|
+
spec: releaseSpec,
|
|
1254
|
+
apply: {
|
|
1255
|
+
onEvent: makeStderrEventWriter(opts.quiet),
|
|
1256
|
+
idempotencyKey,
|
|
1257
|
+
allowWarnings: opts.allowWarnings,
|
|
1258
|
+
allowWarningCodes: opts.allowWarningCodes,
|
|
1259
|
+
...(requiredPlan ? { requiredPlan } : {}),
|
|
1260
|
+
},
|
|
1251
1261
|
target: isCoreApiTarget() ? "core" : "cloud",
|
|
1262
|
+
// The `gitvault_commit` line is printed ALWAYS, and to stderr, so
|
|
1263
|
+
// `run402 deploy apply | jq` stays clean (the pipe contract).
|
|
1264
|
+
onCommitLine: (line) => { if (!opts.quiet) process.stderr.write(`${line}\n`); },
|
|
1252
1265
|
}),
|
|
1253
1266
|
);
|
|
1254
|
-
|
|
1267
|
+
if (!outcome.gitvault) {
|
|
1268
|
+
console.log(JSON.stringify(outcome.deploy, null, 2));
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const vaulted = outcome.gitvault;
|
|
1272
|
+
// Both `DEPLOYED_*` outcomes activated a release; the override one just
|
|
1273
|
+
// did it without a vaulted capture and carries a draining advisory.
|
|
1274
|
+
const activated = vaulted.outcome === "DEPLOYED_AND_VAULTED" || vaulted.outcome === "DEPLOYED_UNVAULTED_OVERRIDE";
|
|
1275
|
+
console.log(JSON.stringify({
|
|
1276
|
+
ok: activated,
|
|
1277
|
+
outcome: vaulted.outcome,
|
|
1278
|
+
gitvault_commit: vaulted.gitvault_commit,
|
|
1279
|
+
capture_id: vaulted.capture_id,
|
|
1280
|
+
operation_id: vaulted.operation_id,
|
|
1281
|
+
...("generation" in vaulted ? { generation: vaulted.generation } : {}),
|
|
1282
|
+
...("push_error" in vaulted ? { push_error: vaulted.push_error } : {}),
|
|
1283
|
+
...("deploy_error" in vaulted ? { deploy_error: vaulted.deploy_error } : {}),
|
|
1284
|
+
next_actions: vaulted.next_actions,
|
|
1285
|
+
deploy: outcome.deploy,
|
|
1286
|
+
}, null, 2));
|
|
1287
|
+
// A non-activating outcome is a failed deploy even though it resolved
|
|
1288
|
+
// rather than threw: the five outcomes are a result type, not an error
|
|
1289
|
+
// channel, so the exit code has to carry the verdict.
|
|
1290
|
+
if (!activated) process.exit(1);
|
|
1255
1291
|
} catch (err) {
|
|
1256
1292
|
reportDeployApplyError(err, useGithubActionsOidc);
|
|
1257
1293
|
}
|
|
@@ -1348,89 +1384,24 @@ const CI_DEPLOY_ERROR_GUIDANCE = {
|
|
|
1348
1384
|
},
|
|
1349
1385
|
};
|
|
1350
1386
|
|
|
1387
|
+
/**
|
|
1388
|
+
* The gitvault advisory rewrite is RETIRED (change `gitvault-deploy-lane`,
|
|
1389
|
+
* design D4). `GITVAULT_DEPLOY_LANE = "unsupported"` and the client-side
|
|
1390
|
+
* rewrite of `GITVAULT_CLIENT_UPGRADE_REQUIRED` existed only for as long as no
|
|
1391
|
+
* published client could satisfy `gitvault_policy: required`. This one can:
|
|
1392
|
+
* `applyCmd` deploys through `applyWithGitvault`, which declares
|
|
1393
|
+
* `{capture_id, snapshot_oid_hmac}` at plan time and presents an activation
|
|
1394
|
+
* token at commit. The gateway's envelope — `upgrade_client` first, then
|
|
1395
|
+
* `grandfather_policy` — is now TRUE as written, so it is relayed untouched;
|
|
1396
|
+
* re-authoring it here would be the client lying in the opposite direction.
|
|
1397
|
+
* `cli-deploy-gitvault-lane.test.mjs` is the successor gate.
|
|
1398
|
+
*/
|
|
1351
1399
|
function reportDeployApplyError(err, useGithubActionsOidc) {
|
|
1352
|
-
const warningEnhanced =
|
|
1400
|
+
const warningEnhanced = enhanceDeployWarningError(err);
|
|
1353
1401
|
if (!useGithubActionsOidc) return reportSdkError(warningEnhanced);
|
|
1354
1402
|
return reportSdkError(enhanceCiDeployError(warningEnhanced));
|
|
1355
1403
|
}
|
|
1356
1404
|
|
|
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
|
-
|
|
1434
1405
|
function enhanceDeployWarningError(err) {
|
|
1435
1406
|
const existingBody = err?.body && typeof err.body === "object" && !Array.isArray(err.body)
|
|
1436
1407
|
? err.body
|
package/lib/init.mjs
CHANGED
|
@@ -627,6 +627,29 @@ export async function run(args = []) {
|
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
// 5b. The org. `init` already materializes it (the tier read above
|
|
631
|
+
// authenticates, which provisions the wallet's org-of-one), so NOT reporting
|
|
632
|
+
// it forced every agent that wanted to coordinate to go find it with a second
|
|
633
|
+
// command and copy a UUID by hand. It is an identifier, not a credential.
|
|
634
|
+
try {
|
|
635
|
+
const orgs = await getSdk().orgs.list();
|
|
636
|
+
const rows = Array.isArray(orgs) ? orgs : (orgs?.orgs ?? []);
|
|
637
|
+
summary.orgs = rows.map((o) => ({
|
|
638
|
+
org_id: o.org_id,
|
|
639
|
+
display_name: o.display_name ?? null,
|
|
640
|
+
role: o.role ?? null,
|
|
641
|
+
}));
|
|
642
|
+
if (rows.length === 1) {
|
|
643
|
+
line("Org", `${rows[0].org_id}${rows[0].display_name ? ` (${rows[0].display_name})` : ""}`);
|
|
644
|
+
} else if (rows.length > 1) {
|
|
645
|
+
line("Org", `${rows.length} organizations — run402 org list`);
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
// Best-effort, exactly like the billing read: a listing failure must never
|
|
649
|
+
// fail setup.
|
|
650
|
+
summary.orgs = null;
|
|
651
|
+
}
|
|
652
|
+
|
|
630
653
|
// 6. Next step — canonical typed action(s); `next_step` is the back-compat
|
|
631
654
|
// string mirror of the first action's command (one spelling, surface-wide).
|
|
632
655
|
write("");
|
package/lib/org-context.mjs
CHANGED
|
@@ -258,6 +258,11 @@ export async function resolveOrgId(a, opts = {}) {
|
|
|
258
258
|
return resolved ? resolved.orgId : null;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/** Validate an org id supplied by a human, naming the origin. Throws via fail(). */
|
|
262
|
+
export function requireOrgIdShape(orgId, origin = "--org") {
|
|
263
|
+
return assertOrgIdShape(orgId, origin);
|
|
264
|
+
}
|
|
265
|
+
|
|
261
266
|
/** The profile's selected organization, without running the chain. */
|
|
262
267
|
export function getSelectedOrgId() {
|
|
263
268
|
return trimmed(coreGetActiveOrgId());
|
package/lib/org.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { getSdk } from "./sdk.mjs";
|
|
2
2
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
3
|
+
import { readBindingFile, updateBindingFile } from "./wallet-context.mjs";
|
|
4
|
+
import { nextAction } from "./next-actions.mjs";
|
|
3
5
|
import {
|
|
6
|
+
requireOrgIdShape,
|
|
4
7
|
resolveOrg,
|
|
5
8
|
orgProvenance,
|
|
6
9
|
getSelectedOrgId,
|
|
@@ -31,6 +34,8 @@ Usage:
|
|
|
31
34
|
run402 org use <org_id>
|
|
32
35
|
run402 org current
|
|
33
36
|
run402 org clear
|
|
37
|
+
run402 org bind [--org <org_id>] [--room <key>]
|
|
38
|
+
run402 org unbind
|
|
34
39
|
run402 org audit <org_id> [--limit N] [--after <cursor>] [--before <cursor>]
|
|
35
40
|
run402 org member list <org_id>
|
|
36
41
|
run402 org member add <org_id> --wallet <wallet_address> [--role <role>]
|
|
@@ -51,6 +56,8 @@ Subcommands:
|
|
|
51
56
|
use Select the current org for this wallet profile
|
|
52
57
|
current Report the resolved current org and where it came from
|
|
53
58
|
clear Clear this wallet profile's org selection
|
|
59
|
+
bind Write this checkout's org (+room) into .run402.json — commit it
|
|
60
|
+
unbind Remove the org/room keys from .run402.json
|
|
54
61
|
payout-wallet Set or clear the tenant route payout wallet (admin+)
|
|
55
62
|
whoami Resolved principal + org memberships (GET /agent/v1/whoami)
|
|
56
63
|
member Manage members (list, add, role, rm) — mutations require owner
|
|
@@ -239,6 +246,99 @@ async function clear(args) {
|
|
|
239
246
|
console.log(JSON.stringify({ org_id: null, selected: false, previous_org_id: previous ?? null }, null, 2));
|
|
240
247
|
}
|
|
241
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Slugify a directory name into a legal room key.
|
|
251
|
+
* Room keys match /^[a-z0-9][a-z0-9._-]{0,63}$/ (the DB CHECK on every
|
|
252
|
+
* agent-messaging table), so the repo's own name has to be coerced, not trusted.
|
|
253
|
+
*/
|
|
254
|
+
function roomKeyFromDir(dir) {
|
|
255
|
+
const base = dir.split("/").filter(Boolean).pop() ?? "";
|
|
256
|
+
const slug = base.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").slice(0, 64);
|
|
257
|
+
return /^[a-z0-9]/.test(slug) ? slug : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Write this checkout's org (and room) into `.run402.json`.
|
|
262
|
+
*
|
|
263
|
+
* WHY THIS PICKS FOR YOU WHEN YOU OWN EXACTLY ONE ORG, while the resolution
|
|
264
|
+
* chain never does: they are different acts. The chain runs on EVERY command
|
|
265
|
+
* and must not change meaning the day you are invited to a second org — so it
|
|
266
|
+
* refuses to infer. This runs ONCE, because you asked it to, and it WRITES THE
|
|
267
|
+
* ANSWER DOWN. Nothing is inferred afterwards; the file is read verbatim
|
|
268
|
+
* forever after. An explicit act with a recorded result is not a heuristic.
|
|
269
|
+
*
|
|
270
|
+
* With two or more orgs there is nothing to pick, so it lists them and stops.
|
|
271
|
+
*/
|
|
272
|
+
async function bind(args) {
|
|
273
|
+
const a = normalizeArgv(args);
|
|
274
|
+
const valueFlags = ["--org", "--room"];
|
|
275
|
+
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
276
|
+
requirePositionalCount(a, valueFlags, { min: 0, max: 0, command: "run402 org bind [--org <org_id>] [--room <key>]" });
|
|
277
|
+
|
|
278
|
+
let orgId = flagValue(a, "--org");
|
|
279
|
+
let picked = "flag";
|
|
280
|
+
if (!orgId) {
|
|
281
|
+
let orgs;
|
|
282
|
+
try {
|
|
283
|
+
orgs = await getSdk().orgs.list();
|
|
284
|
+
} catch (err) {
|
|
285
|
+
reportSdkError(err);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const rows = Array.isArray(orgs) ? orgs : (orgs?.orgs ?? []);
|
|
289
|
+
if (rows.length === 0) {
|
|
290
|
+
fail({
|
|
291
|
+
code: "NO_ORGS",
|
|
292
|
+
message: "This wallet is a member of no organization yet.",
|
|
293
|
+
hint: "Run 'run402 init' to provision one, or ask an owner to add you with 'run402 org member add'.",
|
|
294
|
+
next_actions: [nextAction("initialize_wallet", { command: "run402 init", why: "Provision this wallet's organization, then retry." })],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (rows.length > 1) {
|
|
298
|
+
fail({
|
|
299
|
+
code: "AMBIGUOUS_ORG",
|
|
300
|
+
message: `This wallet belongs to ${rows.length} organizations — name the one to bind.`,
|
|
301
|
+
hint: "run402 org bind --org <org_id>",
|
|
302
|
+
details: { orgs: rows.map((o) => ({ org_id: o.org_id, display_name: o.display_name ?? null, role: o.role ?? null })) },
|
|
303
|
+
next_actions: [nextAction("edit_request", { command: "run402 org bind --org <org_id>", why: "Name which organization this checkout coordinates in." })],
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
orgId = rows[0].org_id;
|
|
307
|
+
picked = "sole_membership";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const room = flagValue(a, "--room") ?? roomKeyFromDir(process.cwd());
|
|
311
|
+
const { contents, file } = updateBindingFile(process.cwd(), {
|
|
312
|
+
org: requireOrgIdShape(orgId, picked === "flag" ? "--org" : "org list"),
|
|
313
|
+
...(room ? { room } : {}),
|
|
314
|
+
});
|
|
315
|
+
console.log(JSON.stringify({
|
|
316
|
+
org_id: orgId,
|
|
317
|
+
room_key: room ?? null,
|
|
318
|
+
org_source: picked,
|
|
319
|
+
file: ".run402.json",
|
|
320
|
+
path: file,
|
|
321
|
+
bound: true,
|
|
322
|
+
safe_to_commit: true,
|
|
323
|
+
note: "Safe to commit — an org id is an identifier, not a credential; authorization stays server-side.",
|
|
324
|
+
binding: contents,
|
|
325
|
+
}, null, 2));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function unbind(args) {
|
|
329
|
+
const a = normalizeArgv(args);
|
|
330
|
+
assertKnownFlags(a, ["--help", "-h"]);
|
|
331
|
+
requirePositionalCount(a, [], { min: 0, max: 0, command: "run402 org unbind" });
|
|
332
|
+
const previous = readBindingFile(process.cwd());
|
|
333
|
+
const { contents, removed } = updateBindingFile(process.cwd(), { org: null, room: null });
|
|
334
|
+
console.log(JSON.stringify({
|
|
335
|
+
file: ".run402.json",
|
|
336
|
+
unbound: previous.org !== undefined || previous.room !== undefined,
|
|
337
|
+
removed,
|
|
338
|
+
binding: contents,
|
|
339
|
+
}, null, 2));
|
|
340
|
+
}
|
|
341
|
+
|
|
242
342
|
async function current(args) {
|
|
243
343
|
const a = normalizeArgv(args);
|
|
244
344
|
assertKnownFlags(a, ["--help", "-h"]);
|
|
@@ -534,6 +634,8 @@ export async function run(sub, args) {
|
|
|
534
634
|
case "use": await use(args); break;
|
|
535
635
|
case "current": await current(args); break;
|
|
536
636
|
case "clear": await clear(args); break;
|
|
637
|
+
case "bind": await bind(args); break;
|
|
638
|
+
case "unbind": await unbind(args); break;
|
|
537
639
|
case "audit": await audit(args); break;
|
|
538
640
|
default:
|
|
539
641
|
failUnknownSubcommand("org", sub);
|
package/lib/wallet-context.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* wallets and no flag is given, that is a hard error (not a silent pick).
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { readFileSync } from "node:fs";
|
|
22
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { join, dirname, resolve } from "node:path";
|
|
24
24
|
import { fail } from "./sdk-errors.mjs";
|
|
25
25
|
import { isValidProfileName } from "../core-dist/config.js";
|
|
@@ -27,6 +27,7 @@ import { getDefaultWallet, profileExists, readMeta, profileDir } from "../core-d
|
|
|
27
27
|
import { readAllowance } from "../core-dist/allowance.js";
|
|
28
28
|
|
|
29
29
|
const DEFAULT = "default";
|
|
30
|
+
const BINDING_FILE = ".run402.json";
|
|
30
31
|
const GLOBAL_FLAGS = new Set(["--wallet", "--profile"]);
|
|
31
32
|
// The `wallets` group is the management + escape surface — it must work even
|
|
32
33
|
// when selection is ambiguous (so you can `wallets unbind`), and it validates
|
|
@@ -84,6 +85,45 @@ function readBindingKeyFrom(dir, key) {
|
|
|
84
85
|
return null;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** Path of the committed binding file for a directory. */
|
|
89
|
+
export function bindingFilePath(dir = process.cwd()) {
|
|
90
|
+
return join(dir, BINDING_FILE);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Parse a directory's committed binding file, or `{}` when absent/unreadable. */
|
|
94
|
+
export function readBindingFile(dir = process.cwd()) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(bindingFilePath(dir), "utf8"));
|
|
97
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
98
|
+
} catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* MERGE keys into a directory's binding file. A `null` value removes its key;
|
|
105
|
+
* a file left with no keys is deleted rather than committed empty.
|
|
106
|
+
*
|
|
107
|
+
* The file is shared by tiers (`wallet` from `wallets bind`, `org`/`room` from
|
|
108
|
+
* `org bind`) and unknown keys are preserved, so one tier can never clobber
|
|
109
|
+
* another's binding — which a whole-file write did until this existed.
|
|
110
|
+
*/
|
|
111
|
+
export function updateBindingFile(dir, patch) {
|
|
112
|
+
const file = bindingFilePath(dir);
|
|
113
|
+
const next = { ...readBindingFile(dir) };
|
|
114
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
115
|
+
if (v === null || v === undefined) delete next[k];
|
|
116
|
+
else next[k] = v;
|
|
117
|
+
}
|
|
118
|
+
if (Object.keys(next).length === 0) {
|
|
119
|
+
const existed = existsSync(file);
|
|
120
|
+
if (existed) rmSync(file, { force: true });
|
|
121
|
+
return { file, contents: null, removed: existed };
|
|
122
|
+
}
|
|
123
|
+
writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
124
|
+
return { file, contents: next, removed: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
87
127
|
/**
|
|
88
128
|
* Nearest binding carrying `key`, walking up from `startDir` to the root.
|
|
89
129
|
*
|
package/lib/wallets.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "../core-dist/profiles.js";
|
|
33
33
|
import { readAllowance, saveAllowance } from "../core-dist/allowance.js";
|
|
34
34
|
import { getSdk } from "./sdk.mjs";
|
|
35
|
+
import { readBindingFile, updateBindingFile } from "./wallet-context.mjs";
|
|
35
36
|
|
|
36
37
|
const DEFAULT = "default";
|
|
37
38
|
const PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
@@ -208,14 +209,15 @@ async function cmdRename(args) {
|
|
|
208
209
|
function cmdBind(args) {
|
|
209
210
|
let name = args.find((a) => a && !a.startsWith("-"));
|
|
210
211
|
name = name ? requireName(name) : getActiveProfile();
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
// MERGE, never clobber: the same file carries `org`/`room` from `org bind`.
|
|
213
|
+
const { contents } = updateBindingFile(process.cwd(), { wallet: name });
|
|
213
214
|
const result = {
|
|
214
215
|
wallet: name,
|
|
215
216
|
file: ".run402.json",
|
|
216
217
|
bound: true,
|
|
217
218
|
safe_to_commit: true,
|
|
218
219
|
note: "Safe to commit — contains no secrets, only the wallet name.",
|
|
220
|
+
binding: contents,
|
|
219
221
|
};
|
|
220
222
|
if (name !== DEFAULT && !profileExists(name)) {
|
|
221
223
|
result.warning = `No local wallet named '${name}' yet — create it with 'run402 wallets new ${name}'.`;
|
|
@@ -224,10 +226,12 @@ function cmdBind(args) {
|
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
function cmdUnbind() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
// Removes only the WALLET key. An `org`/`room` binding in the same file
|
|
230
|
+
// belongs to another tier and must survive; the file is deleted only when
|
|
231
|
+
// unbinding leaves nothing behind.
|
|
232
|
+
const had = readBindingFile(process.cwd()).wallet !== undefined;
|
|
233
|
+
const { contents, removed } = updateBindingFile(process.cwd(), { wallet: null });
|
|
234
|
+
out({ file: ".run402.json", unbound: had, removed, binding: contents });
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
async function cmdImport(args) {
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* behavior; this file is the implementation.
|
|
20
20
|
*/
|
|
21
21
|
import type { Client } from "../kernel.js";
|
|
22
|
-
import type { ApplyOptions, ActiveReleaseInventory, DeployEvent, DeployEventsResponse, DeployListOptions, DeployListResponse, DeployOperation, DeployResult, DeployResolveOptions, DeployResolveResponse, EdgeCoherenceReport, EdgeCoherenceWaitOptions, EdgeCoherenceWaitResult, OperationSnapshot, PlanResponse, PromoteOptions, PromoteResult, ReleaseDiffOptions, ReleaseInventory, ReleaseInventoryByIdOptions, ReleaseInventoryOptions, ReleaseSpec, ReleaseToReleaseDiff, RehearsePlanOptions, RehearsePlanResult, StartOptions } from "./deploy.types.js";
|
|
22
|
+
import type { ApplyOptions, ActiveReleaseInventory, DeployEvent, DeployEventsResponse, DeployListOptions, DeployListResponse, DeployOperation, DeployResult, DeployResolveOptions, DeployResolveResponse, EdgeCoherenceReport, EdgeCoherenceWaitOptions, EdgeCoherenceWaitResult, GitvaultCommitDeclaration, GitvaultPlanDeclaration, OperationSnapshot, PlanResponse, PromoteOptions, PromoteResult, ReleaseDiffOptions, ReleaseInventory, ReleaseInventoryByIdOptions, ReleaseInventoryOptions, ReleaseSpec, ReleaseToReleaseDiff, RehearsePlanOptions, RehearsePlanResult, StartOptions } from "./deploy.types.js";
|
|
23
23
|
export declare class Deploy {
|
|
24
24
|
private readonly client;
|
|
25
25
|
constructor(client: Client);
|
|
@@ -47,6 +47,8 @@ export declare class Deploy {
|
|
|
47
47
|
planId: string;
|
|
48
48
|
planFingerprint?: string;
|
|
49
49
|
};
|
|
50
|
+
/** gitvault §6.5: bind this plan to a capture (see `runGitvaultDeploy`). */
|
|
51
|
+
gitvault?: GitvaultPlanDeclaration;
|
|
50
52
|
}): Promise<{
|
|
51
53
|
plan: PlanResponse;
|
|
52
54
|
byteReaders: Map<string, ByteReader>;
|
|
@@ -76,6 +78,12 @@ export declare class Deploy {
|
|
|
76
78
|
planId: string;
|
|
77
79
|
planFingerprint?: string;
|
|
78
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* gitvault §6.5: present the activation token minted for this operation,
|
|
83
|
+
* or the audited unvaulted override. A `required` project refuses a
|
|
84
|
+
* commit that carries neither.
|
|
85
|
+
*/
|
|
86
|
+
gitvault?: GitvaultCommitDeclaration;
|
|
79
87
|
}): Promise<DeployResult>;
|
|
80
88
|
rehearse(planId: string, opts?: RehearsePlanOptions): Promise<RehearsePlanResult>;
|
|
81
89
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqB3C,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EAQtB,WAAW,EACX,oBAAoB,EAEpB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqB3C,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EAQtB,WAAW,EACX,oBAAoB,EAEpB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EAIvB,yBAAyB,EACzB,uBAAuB,EAUvB,iBAAiB,EAGjB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,2BAA2B,EAC3B,uBAAuB,EACvB,WAAW,EACX,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiF3B,qBAAa,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAE3C;;;;OAIG;IACG,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,YAAY,CAAC;IAiD9E;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IAI3E;;;;OAIG;IACG,IAAI,CACR,IAAI,EAAE,WAAW,EACjB,IAAI,GAAE;QACJ,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,cAAc,GAAG,cAAc,CAAC;QACvC,YAAY,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,eAAe,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5D,4EAA4E;QAC5E,QAAQ,CAAC,EAAE,uBAAuB,CAAC;KAC/B,GACL,OAAO,CAAC;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;KAAE,CAAC;IASxE;;;;;OAKG;IACG,MAAM,CACV,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;KACxC,GACA,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;IACG,MAAM,CACV,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;QACJ,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;QACvC,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,eAAe,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5D;;;;WAIG;QACH,QAAQ,CAAC,EAAE,yBAAyB,CAAC;KACjC,GACL,OAAO,CAAC,YAAY,CAAC;IASlB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAkC3F;;;;;;;;;OASG;IACG,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GACtE,OAAO,CAAC,YAAY,CAAC;IAqBxB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACG,OAAO,CACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,CAAC;IA2CzB;;;;OAIG;IACG,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAC9B,OAAO,CAAC,iBAAiB,CAAC;IAmB7B;;;;;;OAMG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAC/B,OAAO,CAAC,kBAAkB,CAAC;IA6B9B;;;;;;;;OAQG;IACG,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GACxB,OAAO,CAAC,oBAAoB,CAAC;IAmBhC;;;;OAIG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GACxB,OAAO,CAAC,mBAAmB,CAAC;IAyB/B;;;;OAIG;IACG,gBAAgB,CACpB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,uBAAuB,CAAC;IAoCnC;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,2BAA2B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA2B9E;;;;OAIG;IACG,gBAAgB,CACpB,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAqBlC;;;;OAIG;IACG,IAAI,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA+BnE;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAW1E;AA+6DD;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;iEAG6D;IAC7D,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;CACvC"}
|
|
@@ -93,7 +93,12 @@ export class Deploy {
|
|
|
93
93
|
* {@link Run402DeployError} on any state-machine failure.
|
|
94
94
|
*/
|
|
95
95
|
async apply(spec, opts = {}) {
|
|
96
|
-
|
|
96
|
+
// A gitvault-bound apply never auto-retries: each attempt plans a NEW
|
|
97
|
+
// operation, and an activation token is minted for exactly one. Retrying
|
|
98
|
+
// would either present a token bound elsewhere (the platform refuses, as
|
|
99
|
+
// it should) or demand a fresh capture, which is the caller's deliberate
|
|
100
|
+
// re-run — not something the client does behind their back.
|
|
101
|
+
const maxRetries = opts.gitvault ? 0 : normalizeApplyMaxRetries(opts.maxRetries);
|
|
97
102
|
const maxAttempts = maxRetries + 1;
|
|
98
103
|
const emit = makeEmitter(opts.onEvent);
|
|
99
104
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -153,6 +158,7 @@ export class Deploy {
|
|
|
153
158
|
dryRun: opts.dryRun ?? opts.mode === "legacyDryRun",
|
|
154
159
|
reviewedPlan: opts.mode === "reviewedPlan",
|
|
155
160
|
requiredPlan: opts.requiredPlan,
|
|
161
|
+
...(opts.gitvault ? { gitvault: opts.gitvault } : {}),
|
|
156
162
|
});
|
|
157
163
|
}
|
|
158
164
|
/**
|
|
@@ -173,7 +179,7 @@ export class Deploy {
|
|
|
173
179
|
*/
|
|
174
180
|
async commit(planId, opts = {}) {
|
|
175
181
|
const emit = makeEmitter(opts.onEvent);
|
|
176
|
-
const commit = requireCloudCommitResponse(await commitInternal(this.client, planId, opts.idempotencyKey, opts.project, opts.requiredPlan), "committing deploy");
|
|
182
|
+
const commit = requireCloudCommitResponse(await commitInternal(this.client, planId, opts.idempotencyKey, opts.project, opts.requiredPlan, opts.gitvault), "committing deploy");
|
|
177
183
|
return await pollUntilReady(this.client, commit, {}, [], emit, opts.project);
|
|
178
184
|
}
|
|
179
185
|
async rehearse(planId, opts = {}) {
|
|
@@ -543,11 +549,17 @@ function deriveSliceKinds(spec) {
|
|
|
543
549
|
async function applyOnce(client, spec, opts, emit) {
|
|
544
550
|
const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
|
|
545
551
|
const target = opts.target === "core" ? "core" : "cloud";
|
|
552
|
+
if (opts.gitvault && target === "core") {
|
|
553
|
+
// Refuse rather than drop the hooks: silently committing without the
|
|
554
|
+
// activation block would activate a `required` project unvaulted.
|
|
555
|
+
throw new Run402DeployError("gitvault-bound deploys are not supported against a self-hosted Core gateway; the vault gate is a Cloud control-plane capability.", { code: "GITVAULT_UNSUPPORTED_TARGET", phase: "validate", retryable: false, context: "applying deploy" });
|
|
556
|
+
}
|
|
546
557
|
const sliceKinds = deriveSliceKinds(spec);
|
|
547
558
|
emit({ type: "plan.started" });
|
|
548
559
|
const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey, {
|
|
549
560
|
target,
|
|
550
561
|
requiredPlan: opts.requiredPlan,
|
|
562
|
+
...(opts.gitvault ? { gitvault: opts.gitvault.declaration } : {}),
|
|
551
563
|
});
|
|
552
564
|
emit({ type: "plan.diff", diff: plan.diff });
|
|
553
565
|
emitPlanWarnings(plan, emit);
|
|
@@ -588,8 +600,20 @@ async function applyOnce(client, spec, opts, emit) {
|
|
|
588
600
|
status: "started",
|
|
589
601
|
...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
|
|
590
602
|
});
|
|
591
|
-
const { planId } = requirePersistedPlan(plan, "applying deploy");
|
|
592
|
-
|
|
603
|
+
const { planId, operationId } = requirePersistedPlan(plan, "applying deploy");
|
|
604
|
+
// gitvault §6.5 — the handshake. Content is uploaded, so the artifacts this
|
|
605
|
+
// release ships are fixed; `authorize` verifies snapshot correspondence,
|
|
606
|
+
// mints the activation token, and hands back the commit block. It throws
|
|
607
|
+
// rather than returning when the deploy must not proceed, and nothing has
|
|
608
|
+
// been committed at that point.
|
|
609
|
+
const gitvaultCommit = opts.gitvault
|
|
610
|
+
? await opts.gitvault.authorize({
|
|
611
|
+
plan_id: planId,
|
|
612
|
+
operation_id: operationId,
|
|
613
|
+
apply_plan_sha256: plan.gitvault?.apply_plan_sha256 ?? null,
|
|
614
|
+
})
|
|
615
|
+
: undefined;
|
|
616
|
+
const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project, opts.requiredPlan, gitvaultCommit), "applying deploy");
|
|
593
617
|
const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project, sliceKinds);
|
|
594
618
|
// v1.48 unified-apply: thread the plan response's `asset_entries[]` back
|
|
595
619
|
// into DeployResult.assets so callers reading `result.assets.byKey[key]`
|
|
@@ -1013,6 +1037,11 @@ async function planInternal(client, spec, idempotencyKey, opts = {}) {
|
|
|
1013
1037
|
inlineBody.mode = "reviewed_plan";
|
|
1014
1038
|
if (opts.requiredPlan)
|
|
1015
1039
|
inlineBody.required_plan = requiredPlanToWire(opts.requiredPlan);
|
|
1040
|
+
// gitvault §6.5: the capture declaration must ride the SAME plan the gateway
|
|
1041
|
+
// digests, so it is set before the inline/manifest_ref size decision below
|
|
1042
|
+
// and re-attached to the manifest_ref body too.
|
|
1043
|
+
if (opts.gitvault)
|
|
1044
|
+
inlineBody.gitvault = opts.gitvault;
|
|
1016
1045
|
const inlineBytes = new TextEncoder().encode(JSON.stringify(inlineBody)).byteLength;
|
|
1017
1046
|
let body;
|
|
1018
1047
|
if (inlineBytes <= PLAN_BODY_LIMIT_BYTES) {
|
|
@@ -1054,6 +1083,8 @@ async function planInternal(client, spec, idempotencyKey, opts = {}) {
|
|
|
1054
1083
|
body = { spec: { project_id: spec.project }, manifest_ref: contentRefToWire(ref) };
|
|
1055
1084
|
if (idempotencyKey)
|
|
1056
1085
|
body.idempotency_key = idempotencyKey;
|
|
1086
|
+
if (opts.gitvault)
|
|
1087
|
+
body.gitvault = opts.gitvault;
|
|
1057
1088
|
}
|
|
1058
1089
|
let plan;
|
|
1059
1090
|
try {
|
|
@@ -1431,13 +1462,15 @@ function expandCronNumberPart(part, min, max) {
|
|
|
1431
1462
|
}
|
|
1432
1463
|
return values;
|
|
1433
1464
|
}
|
|
1434
|
-
async function commitInternal(client, planId, idempotencyKey, project, requiredPlan) {
|
|
1465
|
+
async function commitInternal(client, planId, idempotencyKey, project, requiredPlan, gitvault) {
|
|
1435
1466
|
try {
|
|
1436
1467
|
const body = {};
|
|
1437
1468
|
if (idempotencyKey)
|
|
1438
1469
|
body.idempotency_key = idempotencyKey;
|
|
1439
1470
|
if (requiredPlan)
|
|
1440
1471
|
body.required_plan = requiredPlanToWire(requiredPlan);
|
|
1472
|
+
if (gitvault)
|
|
1473
|
+
body.gitvault = gitvault;
|
|
1441
1474
|
return await client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/commit`, {
|
|
1442
1475
|
method: "POST",
|
|
1443
1476
|
body,
|