run402 4.66.1 → 4.67.1
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/README.md +4 -2
- package/gitvault-surface.json +10 -2
- package/lib/cold-start.mjs +77 -0
- package/lib/command-manifest.mjs +6 -1
- package/lib/doctor.mjs +10 -0
- package/lib/gitvault-capabilities.mjs +12 -0
- package/lib/org-context.mjs +26 -0
- package/lib/path-lookup.mjs +24 -0
- package/lib/path-lookup.test.mjs +66 -0
- package/lib/repos.mjs +307 -10
- package/lib/rooms-context.mjs +17 -1
- package/package.json +1 -1
- package/sdk/dist/errors.d.ts +1 -1
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +1 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +152 -4
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +296 -8
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-address.d.ts +2 -0
- package/sdk/dist/node/gitvault-address.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-address.js +13 -2
- package/sdk/dist/node/gitvault-address.js.map +1 -1
- package/sdk/dist/node/gitvault-handoff.d.ts +111 -0
- package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-handoff.js +277 -0
- package/sdk/dist/node/gitvault-handoff.js.map +1 -0
- package/sdk/dist/node/gitvault-keystore.d.ts +1 -1
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-restore.d.ts +46 -0
- package/sdk/dist/node/gitvault-restore.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-restore.js +123 -0
- package/sdk/dist/node/gitvault-restore.js.map +1 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts +87 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +216 -11
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
- package/sdk/dist/node/index.d.ts +6 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +8 -1
- package/sdk/dist/node/index.js.map +1 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* never persisted into an agent-surface result store, and never logged.
|
|
30
30
|
*/
|
|
31
31
|
import { LocalError, isRun402Error } from "../errors.js";
|
|
32
|
-
import { GITVAULT_BYO_UNMIRRORED_REMEDY_STATEMENT, GITVAULT_DEGRADED_READ_STATEMENT, GITVAULT_DURABILITY_STATEMENT, GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT, GITVAULT_TERMINAL_LOSS_STATEMENT, GITVAULT_UNMIRRORED_FINDING_STATEMENT, } from "./gitvault.crypto.js";
|
|
32
|
+
import { GITVAULT_BYO_UNMIRRORED_REMEDY_STATEMENT, GITVAULT_DEGRADED_READ_STATEMENT, GITVAULT_DURABILITY_STATEMENT, GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT, GITVAULT_TERMINAL_LOSS_STATEMENT, GITVAULT_UNMIRRORED_FINDING_STATEMENT, bytesToHex, hexToBytes, parseGitvaultStrict, randomBytes, sha256Hex, verifyGitvaultObject, } from "./gitvault.crypto.js";
|
|
33
33
|
/** A keystore path, or `null` when there is no id to derive it from (or it is malformed). */
|
|
34
34
|
function safePath(derive, repoId) {
|
|
35
35
|
if (!repoId)
|
|
@@ -53,6 +53,39 @@ async function nodeOnly(load, verb) {
|
|
|
53
53
|
function mib(bytes) {
|
|
54
54
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A client-generated handoff id (RFC 4122 v4), minted BEFORE the mint call
|
|
58
|
+
* (kygit-handoff design D3): `auth_secret`/`wrap_key` derive off it, so it
|
|
59
|
+
* cannot be gateway-assigned the way `internal.gitvault_claims.id` alone
|
|
60
|
+
* would suggest — this SDK supplies it, mirroring the SAME
|
|
61
|
+
* client_creation_id/client_open_id convention this protocol family
|
|
62
|
+
* already uses elsewhere for idempotent creation. `handoff()` refuses if
|
|
63
|
+
* the gateway's minted `handoff_id` disagrees.
|
|
64
|
+
*/
|
|
65
|
+
function randomHandoffUuid() {
|
|
66
|
+
const b = randomBytes(16);
|
|
67
|
+
b[6] = ((b[6] ?? 0) & 0x0f) | 0x40;
|
|
68
|
+
b[8] = ((b[8] ?? 0) & 0x3f) | 0x80;
|
|
69
|
+
const hex = bytesToHex(b);
|
|
70
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The gateway names the vault by its three ids on the wire — `repo_id`,
|
|
74
|
+
* `org_id`, `project_id` (docs/style.md's API-boundary vocabulary) — on
|
|
75
|
+
* BOTH the handoff mint (`POST /gitvault/v1/vaults/:vault_id/handoffs`) and
|
|
76
|
+
* claim (`POST /gitvault/v1/handoffs/:handoff_id/claim`) responses. The SDK
|
|
77
|
+
* groups them under `vault` with the `organization_id` spelling every other
|
|
78
|
+
* SDK result uses. Neither response carries a slug-form address, so
|
|
79
|
+
* `address` is `null` unless the caller already knows one (a slug-form
|
|
80
|
+
* remote at mint time). Pure; exported for tests.
|
|
81
|
+
*/
|
|
82
|
+
export function handoffVaultFromWire(wire, address = null) {
|
|
83
|
+
return { vault_id: wire.repo_id, address, organization_id: wire.org_id, project_id: wire.project_id };
|
|
84
|
+
}
|
|
85
|
+
/** The claim response's `membership` block (`org_id` on the wire) in the SDK's `organization_id` spelling. Pure; exported for tests. */
|
|
86
|
+
export function handoffMembershipFromWire(wire) {
|
|
87
|
+
return { organization_id: wire.org_id, role: wire.role, status: wire.status };
|
|
88
|
+
}
|
|
56
89
|
/**
|
|
57
90
|
* gitvault-byo-primary-bucket task 3.3 — the number of `{key, object_kind}`
|
|
58
91
|
* entries `GITVAULT_BYO_OBJECT_MISSING`'s `details.missing` lists before
|
|
@@ -1111,6 +1144,237 @@ export class Gitvault {
|
|
|
1111
1144
|
const reconcileRecipients = await this.#tryReconcileEnvelopeRecipients(handle.vault);
|
|
1112
1145
|
return { ...result, snapshot, gitvault_commit: snapshot.oid, gitvault_commit_line: line, mirror_push: mirrorPush, byo_chain_copy: byoChainCopy, reconcile_recipients: reconcileRecipients };
|
|
1113
1146
|
}
|
|
1147
|
+
// ── Handoff / resume (kygit-handoff design D1-D10) ─────────────────────────
|
|
1148
|
+
/**
|
|
1149
|
+
* Mint a Handoff Key: capture a stash-shaped checkpoint (design D1),
|
|
1150
|
+
* push it (retained, on no branch — {@link GITVAULT_DEPLOY_REF} carries
|
|
1151
|
+
* it exactly like an ordinary `push()`, never `refs/heads/*`), seal the
|
|
1152
|
+
* vault's current epoch key under a fresh `wrap_key`, and mint through
|
|
1153
|
+
* the gateway. The assembled `kgh1_…` key is returned exactly ONCE —
|
|
1154
|
+
* nothing here or downstream persists it.
|
|
1155
|
+
*
|
|
1156
|
+
* `options.note` omits `capture` — this method fills it with the real
|
|
1157
|
+
* capture figures and runs the client-side secret scan BEFORE the
|
|
1158
|
+
* handoff commit is written (design D10: no override flag).
|
|
1159
|
+
*/
|
|
1160
|
+
async handoff(options) {
|
|
1161
|
+
const [{ deployRefTransaction }, { captureHandoffSnapshot, snapshotCommitment }, ho] = await Promise.all([this.#publication(), this.#snapshot(), this.#handoff()]);
|
|
1162
|
+
const { assembleHandoffKey, deriveHandoffSecrets, sealHandoffEnvelope, assertHandoffNoteHasNoSecret, HANDOFF_ENVELOPE_KIND } = ho;
|
|
1163
|
+
const handle = options.address ? (await this.resolveOrCreateAddress({ ...options, address: options.address, allow_create: false })).handle : await this.open(options);
|
|
1164
|
+
const repoDir = options.repo_dir ?? process.cwd();
|
|
1165
|
+
const repoFile = handle.keystore.readRepo(handle.repo_id);
|
|
1166
|
+
if (!repoFile) {
|
|
1167
|
+
throw new LocalError(`no local key material for ${handle.repo_id} — this principal is not yet a member with a materialized envelope (push once first)`, "minting a handoff", { code: "GITVAULT_VAULT_UNRESOLVED" });
|
|
1168
|
+
}
|
|
1169
|
+
const kRepo = hexToBytes(repoFile.k_repo_hex);
|
|
1170
|
+
const snapshot = await captureHandoffSnapshot({
|
|
1171
|
+
dir: repoDir,
|
|
1172
|
+
...(options.includeSensitive !== undefined ? { includeSensitive: options.includeSensitive } : {}),
|
|
1173
|
+
message: (stats) => {
|
|
1174
|
+
const note = {
|
|
1175
|
+
...options.note,
|
|
1176
|
+
capture: {
|
|
1177
|
+
base_head: stats.base_head_oid,
|
|
1178
|
+
branch: stats.branch,
|
|
1179
|
+
modified_captured: stats.modified_captured.length,
|
|
1180
|
+
untracked_captured: stats.untracked_captured.length,
|
|
1181
|
+
sensitive_excluded: stats.sensitive_excluded,
|
|
1182
|
+
ignored_not_transferred_count: stats.ignored_not_transferred_count,
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
assertHandoffNoteHasNoSecret(note);
|
|
1186
|
+
return JSON.stringify(note);
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
options.onCommitLine?.(`handoff checkpoint ${snapshot.oid}`);
|
|
1190
|
+
const materialized = await handle.vault.materialize();
|
|
1191
|
+
const pushResult = await handle.vault.push({
|
|
1192
|
+
transaction: deployRefTransaction(materialized.refs, snapshot.oid),
|
|
1193
|
+
head_target: snapshot.head,
|
|
1194
|
+
protocol_refs: "allow",
|
|
1195
|
+
}).catch((e) => { throw this.#enrichEpochRotationRequired(e, handle.repo_id); });
|
|
1196
|
+
const snapshotOidHmac = snapshotCommitment(kRepo, handle.repo_id, repoFile.epoch, snapshot.oid);
|
|
1197
|
+
// Client-generated handoff_id (mirrors this protocol family's own
|
|
1198
|
+
// client_creation_id/client_open_id convention): needed to derive
|
|
1199
|
+
// auth_secret/wrap_key and seal the envelope BEFORE the mint call, so
|
|
1200
|
+
// it cannot be gateway-assigned. The gateway's own `handoff_id` in the
|
|
1201
|
+
// response is authoritative; a disagreement (an id collision the
|
|
1202
|
+
// gateway resolved differently) is refused rather than silently
|
|
1203
|
+
// trusted, since a mismatched id would make the recipient's derived
|
|
1204
|
+
// secrets useless anyway.
|
|
1205
|
+
const handoffId = randomHandoffUuid();
|
|
1206
|
+
const { key, handoff_id_bytes, master_secret } = assembleHandoffKey(handoffId, randomBytes(32));
|
|
1207
|
+
const secrets = deriveHandoffSecrets(handoff_id_bytes, master_secret);
|
|
1208
|
+
const sealed = sealHandoffEnvelope(handoff_id_bytes, secrets.wrap_key, {
|
|
1209
|
+
v: 1,
|
|
1210
|
+
kind: "handoff",
|
|
1211
|
+
repo_id: handle.repo_id,
|
|
1212
|
+
epoch: repoFile.epoch,
|
|
1213
|
+
k_e_hex: repoFile.k_repo_hex,
|
|
1214
|
+
checkpoint: { generation: pushResult.generation, commit_oid: snapshot.oid },
|
|
1215
|
+
note_schema: "kygit.handoff-note.v1",
|
|
1216
|
+
});
|
|
1217
|
+
// The wire shape is the gateway's documented one (llms-full.txt
|
|
1218
|
+
// "Handoff / resume"): `role` (the minted role), `repo_id` / `org_id` /
|
|
1219
|
+
// `project_id`, a verbatim `warning` sentence plus its machine-readable
|
|
1220
|
+
// `warnings[]` twin. Read those names exactly — an SDK-side spelling
|
|
1221
|
+
// that the gateway never sends surfaces as "role undefined" at the CLI.
|
|
1222
|
+
const response = await this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(handle.repo_id)}/handoffs`, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
body: {
|
|
1225
|
+
handoff_id: handoffId,
|
|
1226
|
+
...(options.role !== undefined ? { role: options.role } : {}),
|
|
1227
|
+
...(options.ttlSeconds !== undefined ? { expires_in_seconds: options.ttlSeconds } : {}),
|
|
1228
|
+
checkpoint: { generation: pushResult.generation, snapshot_oid_hmac: snapshotOidHmac },
|
|
1229
|
+
sealed_envelope: sealed.sealed_envelope,
|
|
1230
|
+
envelope_kind: sealed.envelope_kind ?? HANDOFF_ENVELOPE_KIND,
|
|
1231
|
+
auth_hash: secrets.auth_hash_hex,
|
|
1232
|
+
},
|
|
1233
|
+
context: "minting a handoff key",
|
|
1234
|
+
});
|
|
1235
|
+
if (response.handoff_id !== handoffId) {
|
|
1236
|
+
throw new LocalError(`the gateway minted a different handoff_id (${response.handoff_id}) than requested (${handoffId}) — the assembled key would not match; retry`, "minting a handoff key", { code: "HANDOFF_ID_MISMATCH", details: { requested: handoffId, minted: response.handoff_id } });
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
handoff_key: key,
|
|
1240
|
+
handoff_id: response.handoff_id,
|
|
1241
|
+
kind: response.kind,
|
|
1242
|
+
minted_role: response.role,
|
|
1243
|
+
expires_at: response.expires_at,
|
|
1244
|
+
// A slug-form remote is the one address the minter already knows;
|
|
1245
|
+
// an id-form one carries nothing a directory name should be built from.
|
|
1246
|
+
vault: handoffVaultFromWire(response, options.address && gitvaultRemoteAddressForm(options.address) === "slug" ? `${options.address.org_id}/${options.address.project_id}` : null),
|
|
1247
|
+
checkpoint: response.checkpoint,
|
|
1248
|
+
capture: {
|
|
1249
|
+
modified_captured: snapshot.modified_captured.length,
|
|
1250
|
+
untracked_captured: snapshot.untracked_captured.length,
|
|
1251
|
+
sensitive_excluded: snapshot.sensitive_excluded,
|
|
1252
|
+
ignored_not_transferred_count: snapshot.ignored_not_transferred_count,
|
|
1253
|
+
},
|
|
1254
|
+
snapshot,
|
|
1255
|
+
warnings: response.warnings ?? [],
|
|
1256
|
+
next_actions: response.next_actions ?? [],
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/** List a vault's handoffs (ids, kind, state, role, expiry, claimed_by — never the hash or envelope). */
|
|
1260
|
+
async listHandoffs(options) {
|
|
1261
|
+
const repoId = await this.#resolveRepoId(options);
|
|
1262
|
+
return this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(repoId)}/handoffs`, { context: "listing handoffs" });
|
|
1263
|
+
}
|
|
1264
|
+
/** Revoke a handoff (idempotent — a second revoke of an already-revoked/claimed/expired row still answers `200`). */
|
|
1265
|
+
async revokeHandoff(handoffId, options) {
|
|
1266
|
+
const repoId = await this.#resolveRepoId(options);
|
|
1267
|
+
return this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(repoId)}/handoffs/${encodeURIComponent(handoffId)}`, { method: "DELETE", context: "revoking a handoff" });
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Resume a Handoff Key: parse → claim (SIWX wallet, no payment; the
|
|
1271
|
+
* default Node credentials provider creates the allowance file on a
|
|
1272
|
+
* fresh machine automatically — see `createLazyPaidFetch`) → open the
|
|
1273
|
+
* sealed envelope → write the repo file to the keystore BEFORE touching
|
|
1274
|
+
* disk → clone at the base HEAD → `git stash apply --index` → local
|
|
1275
|
+
* git-config pins only → the session-start reconcile so a principal
|
|
1276
|
+
* envelope supersedes the bearer one.
|
|
1277
|
+
*/
|
|
1278
|
+
async resume(options) {
|
|
1279
|
+
const [ho, { GitvaultKeystore }, { createGitvaultHttpTransport }, restore] = await Promise.all([this.#handoff(), this.#keystore(), this.#publication(), this.#restore()]);
|
|
1280
|
+
const { parseHandoffKey, deriveHandoffSecrets, openHandoffEnvelope } = ho;
|
|
1281
|
+
const { cloneGitvaultRemote, applyHandoffCheckpoint, resolveResumeTargetDir, readGitCommitMessage } = restore;
|
|
1282
|
+
const parsed = parseHandoffKey(options.key);
|
|
1283
|
+
const secrets = deriveHandoffSecrets(parsed.handoff_id_bytes, parsed.master_secret);
|
|
1284
|
+
const claim = await this.#client.request(`/gitvault/v1/handoffs/${encodeURIComponent(parsed.handoff_id)}/claim`, {
|
|
1285
|
+
method: "POST",
|
|
1286
|
+
body: { auth_secret: bytesToHex(secrets.auth_secret) },
|
|
1287
|
+
context: "claiming a handoff key",
|
|
1288
|
+
});
|
|
1289
|
+
// The claim names the vault by id only (no slug-form address rides the
|
|
1290
|
+
// wire), so the default target directory falls back to the vault id —
|
|
1291
|
+
// `--to <dir>` names it explicitly.
|
|
1292
|
+
const vault = handoffVaultFromWire(claim);
|
|
1293
|
+
const payload = openHandoffEnvelope(parsed.handoff_id_bytes, secrets.wrap_key, claim.sealed_envelope, claim.envelope_kind);
|
|
1294
|
+
if (payload.repo_id !== vault.vault_id) {
|
|
1295
|
+
throw new LocalError("the opened envelope's repo_id does not match the claim response's vault — refusing", "resuming a handoff", { code: "HANDOFF_ENVELOPE_INVALID" });
|
|
1296
|
+
}
|
|
1297
|
+
const keystore = new GitvaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
1298
|
+
keystore.ensureIdentity();
|
|
1299
|
+
// Genesis must be pinned before ANY materialize call can succeed
|
|
1300
|
+
// (`GitvaultVault.genesis()` requires a keystore repo file — this is
|
|
1301
|
+
// the one read that happens BEFORE one exists, via the transport
|
|
1302
|
+
// directly, mirroring `restoreRepoFromEnvelope`'s own signature check).
|
|
1303
|
+
const transport = createGitvaultHttpTransport(this.#client);
|
|
1304
|
+
const genesisBytes = await transport.getGenesis({ repo_id: vault.vault_id });
|
|
1305
|
+
if (!genesisBytes) {
|
|
1306
|
+
throw new LocalError("the vault has no admitted genesis", "resuming a handoff", { code: "CHAIN_BROKEN", details: { repo_id: vault.vault_id } });
|
|
1307
|
+
}
|
|
1308
|
+
const genesis = parseGitvaultStrict(new TextDecoder().decode(genesisBytes));
|
|
1309
|
+
if (!verifyGitvaultObject(genesis, genesis.creator_signing_pubkey)) {
|
|
1310
|
+
throw new LocalError("vault_genesis signature does not verify", "resuming a handoff", { code: "GITVAULT_SIGNATURE_INVALID", details: { repo_id: vault.vault_id } });
|
|
1311
|
+
}
|
|
1312
|
+
const genesisSha = sha256Hex(genesisBytes);
|
|
1313
|
+
// Write the repo file to the keystore BEFORE touching disk (design D10).
|
|
1314
|
+
keystore.saveRepo({
|
|
1315
|
+
repo_id: vault.vault_id,
|
|
1316
|
+
org_id: vault.organization_id,
|
|
1317
|
+
project_id: vault.project_id ?? "",
|
|
1318
|
+
k_repo_hex: payload.k_e_hex,
|
|
1319
|
+
epoch: payload.epoch,
|
|
1320
|
+
epoch_keys: { [payload.epoch]: payload.k_e_hex },
|
|
1321
|
+
genesis_sha256: genesisSha,
|
|
1322
|
+
head_pin: null,
|
|
1323
|
+
last_ref_transaction: null,
|
|
1324
|
+
provenance: "restored_from_handoff",
|
|
1325
|
+
});
|
|
1326
|
+
const targetDir = await resolveResumeTargetDir(options.to, vault.address, vault.vault_id);
|
|
1327
|
+
options.onLine?.(`resuming into ${targetDir}`);
|
|
1328
|
+
const remoteUrl = gitvaultRemoteUrl(vault.organization_id, vault.project_id);
|
|
1329
|
+
await cloneGitvaultRemote(remoteUrl, targetDir);
|
|
1330
|
+
const restored = await applyHandoffCheckpoint({ dir: targetDir, stash_oid: payload.checkpoint.commit_oid });
|
|
1331
|
+
// Local-only pins (design D10) — never a worktree file, never the
|
|
1332
|
+
// global active project. Reuses the SAME pin-writer every other
|
|
1333
|
+
// gitvault resolution path uses, which also writes `r402.room`.
|
|
1334
|
+
const { pinGitvaultRepo } = await this.#address();
|
|
1335
|
+
const addressParts = vault.address ? vault.address.split("/") : null;
|
|
1336
|
+
await pinGitvaultRepo(targetDir, vault.vault_id, addressParts && addressParts.length === 2 ? { org_slug: addressParts[0], repo_name: addressParts[1] } : undefined, { project_id: vault.project_id, org_id: vault.organization_id });
|
|
1337
|
+
// The bearer envelope is superseded within minutes of use — run the
|
|
1338
|
+
// same reconcile `push()` runs, best-effort (never a `resume()` throw).
|
|
1339
|
+
const handle = await this.open({ repo_id: vault.vault_id, repo_dir: targetDir, keystore_root: options.keystore_root });
|
|
1340
|
+
const reconcile = await this.#tryReconcileEnvelopeRecipients(handle.vault);
|
|
1341
|
+
const senderIsOwner = claim.membership.role === "owner";
|
|
1342
|
+
const nextActions = [...(claim.next_actions ?? [])];
|
|
1343
|
+
if (claim.kind === "handoff" && senderIsOwner && !nextActions.some((a) => a.type === "remove_member")) {
|
|
1344
|
+
nextActions.push({
|
|
1345
|
+
type: "remove_member",
|
|
1346
|
+
why: "The previous agent is still an owner; if its environment is gone for good, remove it.",
|
|
1347
|
+
destructive: true,
|
|
1348
|
+
requires_approval: true,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
if (!nextActions.some((a) => a.type === "push_repo")) {
|
|
1352
|
+
nextActions.push({ type: "push_repo", command: "git push origin main", why: "Publish continued work back to the vault." });
|
|
1353
|
+
}
|
|
1354
|
+
let note = null;
|
|
1355
|
+
let noteRaw = null;
|
|
1356
|
+
try {
|
|
1357
|
+
noteRaw = (await readGitCommitMessage(targetDir, payload.checkpoint.commit_oid)) ?? null;
|
|
1358
|
+
if (noteRaw)
|
|
1359
|
+
note = JSON.parse(noteRaw);
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
note = null;
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
handoff_id: claim.handoff_id,
|
|
1366
|
+
kind: claim.kind,
|
|
1367
|
+
deduplicated: claim.deduplicated,
|
|
1368
|
+
note,
|
|
1369
|
+
note_raw: noteRaw,
|
|
1370
|
+
restored: { dir: targetDir, branch: restored.branch, base_head_oid: restored.base_head_oid, stash_oid: restored.stash_oid },
|
|
1371
|
+
membership: handoffMembershipFromWire(claim.membership),
|
|
1372
|
+
members: claim.members ?? [],
|
|
1373
|
+
expires_at: claim.expires_at,
|
|
1374
|
+
reconcile_recipients: reconcile,
|
|
1375
|
+
next_actions: nextActions,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1114
1378
|
/** Best-effort dual-push: catches EVERYTHING, including the lazy module import itself, so a mirror problem can never surface as a `push()` throw. */
|
|
1115
1379
|
/**
|
|
1116
1380
|
* `EPOCH_ROTATION_REQUIRED` (D193) is left THROWN — never swallowed into a
|
|
@@ -2446,6 +2710,12 @@ export class Gitvault {
|
|
|
2446
2710
|
#recovery() {
|
|
2447
2711
|
return nodeOnly(() => import("../node/gitvault-recover.js"), "recover");
|
|
2448
2712
|
}
|
|
2713
|
+
#handoff() {
|
|
2714
|
+
return nodeOnly(() => import("../node/gitvault-handoff.js"), "handoff");
|
|
2715
|
+
}
|
|
2716
|
+
#restore() {
|
|
2717
|
+
return nodeOnly(() => import("../node/gitvault-restore.js"), "resume");
|
|
2718
|
+
}
|
|
2449
2719
|
#degradedRead() {
|
|
2450
2720
|
return nodeOnly(() => import("../node/gitvault-degraded-read.js"), "list");
|
|
2451
2721
|
}
|
|
@@ -2615,24 +2885,42 @@ export function gitvaultUnmirroredFinding(state) {
|
|
|
2615
2885
|
export function gitvaultDegradedReadNote(source) {
|
|
2616
2886
|
return `degraded read from ${source.destination}: ${GITVAULT_DEGRADED_READ_STATEMENT}`;
|
|
2617
2887
|
}
|
|
2618
|
-
/**
|
|
2888
|
+
/**
|
|
2889
|
+
* The remote door (kygit-handoff design D8): `"run402"` (the canonical,
|
|
2890
|
+
* plumbing spelling — accepted forever) or `"kygit"` (what the
|
|
2891
|
+
* `@kychee/kygit` shim renders once it sets `RUN402_REMOTE_SCHEME=kygit`
|
|
2892
|
+
* before exec). The gateway never sees this — `address` and every registry
|
|
2893
|
+
* `next_actions` command stay `run402::`; only client-side RENDERING reads
|
|
2894
|
+
* it. Any other value falls back to `"run402"` rather than emitting an
|
|
2895
|
+
* unparseable scheme.
|
|
2896
|
+
*/
|
|
2897
|
+
export function gitvaultRemoteScheme() {
|
|
2898
|
+
return typeof process !== "undefined" && process.env?.RUN402_REMOTE_SCHEME === "kygit" ? "kygit" : "run402";
|
|
2899
|
+
}
|
|
2900
|
+
/** `<door>::<org_id>/<project_id>` — what `git-remote-run402`/`git-remote-kygit` resolves. */
|
|
2619
2901
|
export function gitvaultRemoteUrl(orgId, projectId) {
|
|
2620
|
-
return
|
|
2902
|
+
return `${gitvaultRemoteScheme()}::${orgId}/${projectId}`;
|
|
2621
2903
|
}
|
|
2622
2904
|
/**
|
|
2623
|
-
*
|
|
2905
|
+
* `<door>::<org-slug>/<repo-name>` — the address-form remote builder
|
|
2624
2906
|
* (repo-first-onramp task 4, design D6). Same string shape as
|
|
2625
2907
|
* {@link gitvaultRemoteUrl} (the wire slot admits both forms undiscriminated
|
|
2626
2908
|
* — see {@link gitvaultRemoteAddressForm}); kept as its own named function so
|
|
2627
2909
|
* a call site states which form it means rather than reusing the id-form
|
|
2628
|
-
* builder for a semantically different pair of arguments.
|
|
2910
|
+
* builder for a semantically different pair of arguments. Rendered by
|
|
2911
|
+
* {@link gitvaultRemoteScheme} (kygit-handoff design D8) — `run402 repos
|
|
2912
|
+
* create` renders `run402::`, `kygit create` renders `kygit::`.
|
|
2629
2913
|
*/
|
|
2630
2914
|
export function gitvaultRemoteUrlForRepo(orgSlug, repoName) {
|
|
2631
|
-
return
|
|
2915
|
+
return `${gitvaultRemoteScheme()}::${orgSlug}/${repoName}`;
|
|
2632
2916
|
}
|
|
2633
|
-
/**
|
|
2917
|
+
/**
|
|
2918
|
+
* Parse a `run402::<org>/<project>` OR `kygit::<org>/<project>` remote URL
|
|
2919
|
+
* (kygit-handoff design D8) into ONE canonical, scheme-less address — the
|
|
2920
|
+
* door never changes resolution, only rendering. `null` when it is neither.
|
|
2921
|
+
*/
|
|
2634
2922
|
export function parseGitvaultRemoteUrl(url) {
|
|
2635
|
-
const m = /^run402::([^/]+)\/(.+)$/.exec(url.trim());
|
|
2923
|
+
const m = /^(?:run402|kygit)::([^/]+)\/(.+)$/.exec(url.trim());
|
|
2636
2924
|
if (!m)
|
|
2637
2925
|
return null;
|
|
2638
2926
|
return { org_id: m[1], project_id: m[2] };
|