run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
2,511 lines • 134 kB
TypeScript
/**
* `r.repos` — the host-blind encrypted Git remote, as the SDK's programmatic
* API (add-vault task 5.10).
*
* ARCHITECTURAL LAW (client-surface spec, "All protocol logic lives in the
* SDK"): every piece of vault protocol behaviour — crypto core, keystore,
* creation journal, snapshot + capture, publication state machines, ref
* transactions, verification budget, token exchange, repair — is implemented
* ONCE here. `run402 repos …`, `git-remote-run402`, and the MCP tools are
* adapters over this namespace: argument parsing, TTY output, exit codes, and
* local file I/O only. Anything the CLI can do is reachable programmatically
* with identical semantics.
*
* `r402s-verify` is the deliberate exception: an independent second lineage
* that must NOT share implementation code with this namespace, because
* differential verification is its entire purpose.
*
* ISOMORPHIC / NODE SPLIT. Vault reads (the record, heads listing, policy,
* override completion) need nothing but the HTTP client and run anywhere. The
* verbs that touch a git working tree or the on-disk keystore — `init`,
* `push`, `compact`, `verify`, `deploy`, `restore` — are Node-only and are
* reached through DYNAMIC imports of `../node/*`, so importing `@run402/sdk`
* in a browser or worker never pulls `node:fs` into the graph.
*
* CACHING. Nothing here is memoised. Two of these responses are
* secret-bearing — the maintenance lease's `holder_token` (returned exactly
* once) and anything derived from the keystore — and per
* docs/agent-response-design.md a secret-bearing response is never cached,
* never persisted into an agent-surface result store, and never logged.
*/
import type { Client } from "../kernel.js";
import type { TransferVaultHandover } from "./transfers.js";
import { VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT, VAULT_DURABILITY_STATEMENT, VAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, VAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, VAULT_TERMINAL_LOSS_DOCTOR_TEXT, VAULT_TERMINAL_LOSS_STATEMENT, VAULT_UNMIRRORED_FINDING_STATEMENT } from "./vault.crypto.js";
import type { NextAction } from "../errors.js";
/** The cold open's own verdicts — surfaced from `open()`; anything else leaves the handle lazy (see `open`). */
import type { VaultCaptureReceipt, VaultHeadsListingPage, VaultHeadsListingRequest, VaultHeadTarget, VaultOpenReceipt, VaultRecipientConfirmationReceipt, VaultRecoveryReceipt, VaultRotationReason } from "./vault.types.js";
import type { VaultCompactionGrant, VaultMaintenanceLease, VaultMaintenanceLeaseRequest, VaultTransport, VaultRecord } from "../node/vault-publication.js";
import type { VaultDeployOptions, VaultDeployResult } from "../node/vault-deploy.js";
import type { VaultPublishResult, VaultRefMap, VaultReconcileEnvelopeRecipientsResult, VaultReconcileWriterAdmissionsResult, VaultVerifiedState } from "../node/vault-publication.js";
import type { VaultCreationResult } from "../node/vault-creation-journal.js";
import type { VaultKeystore } from "../node/vault-keystore.js";
import type { VaultSnapshot } from "../node/vault-snapshot.js";
import type { VaultMirrorConfig, VaultMirrorCredential } from "../node/vault-mirror-config.js";
import type { VaultByoPresenceReport, VaultMirrorPushResult, VaultMirrorSyncSummary } from "../node/vault-mirror.js";
import type { VaultRecoverResult, VaultVerifyReport } from "../node/vault-recover.js";
import type { VaultMemberRecoveryBundle } from "../node/vault-member-bundle.js";
import type { VaultDegradedReadLive, VaultDegradedReadOutcome, VaultDegradedReadSource } from "../node/vault-degraded-read.js";
import type { RoomPresence } from "./rooms.types.js";
/** What `r.repos.status()` reports. Never carries key material. */
export interface VaultStatus {
repo_id: string | null;
project_id: string | null;
/** The control plane's view. `null` when no vault is allocated for the project. */
vault: VaultRecord | null;
/** vault-agent-envelopes D5: what this KEY-HOLDER's session-start fulfilment did (`null` on a machine that holds no K_repo, or `reconcile: "forbidden"`). */
reconcile_recipients?: VaultSessionReconcileResult | null;
/** vault-agent-envelopes D3: what the enroll-if-absent step did for this keystore's key (`null` when it did not run). */
enrollment?: VaultEnrollmentOutcome | null;
keystore: {
present: boolean;
/** The principal's Ed25519 signing fingerprint, or `null` when identity is absent. */
identity_fingerprint: string | null;
/** `false` ⇒ read-only: this principal can decrypt and verify but cannot sign a head. */
can_sign: boolean;
/** `true` once this machine holds K_repo for the vault. */
holds_repo_key: boolean;
/**
* WHERE the keystore lives, and what is in it.
*
* Terminal loss is stated three times in this surface; until 5.13 the path
* to back up was stated nowhere, which made the warning unactionable. These
* are file paths, never contents — nothing here is key material.
* `repo` / `recovery_receipt` are `null` until a `repo_id` is known.
*/
root: string;
paths: {
identity: string;
repos: string;
receipts: string;
journal: string;
audit_log: string;
repo: string | null;
recovery_receipt: string | null;
};
};
/**
* The `run402` git remote in the local repository, when there is one to read.
* `null` when no `repo_dir` was given, the directory is not a repository, or
* no such remote is configured.
*
* `matches` is a TRI-STATE. For an ID-FORM remote
* (`run402::<org_id>/<project_id>`) it is `true`/`false` from the URL text
* alone — the second half is a real project id, so comparing it against
* this status's own project needs no lookup. For a SLUG-FORM remote
* (`run402::<org-slug>/<name>`) the URL's second half is a repo NAME, not
* a project id — comparing it as if it were one made a CORRECTLY
* configured slug-form remote always report `matches: false` (the bug).
* Instead it is compared against the local id-pin (`git config
* r402.repoId`, task 4.5): pin present + equal → `true`; pin present +
* different → `false`; no pin yet → `null` with `reason` explaining why
* (no network read — `status` stays a pure observation). Render NO
* mismatch warning for the `null` case anywhere — it is not evidence of
* anything wrong, only of "not yet resolved on this machine".
*/
remote: {
name: string;
url: string;
matches: boolean | null;
reason: string | null;
} | null;
/**
* The id-pinning state of this checkout — `null`
* when no `repo_dir` was given, or nothing is pinned there yet. A SLUG-form
* remote pins `repo_id` in local git state the first time it resolves;
* `resolved_from` names the `org-slug/name` it was resolved from. An
* id-form remote never pins (it needs no pin — see
* `resolveVaultAddress`'s doc comment), so a checkout on one always
* reports `null` here even once its vault is otherwise fully resolved.
*/
pinned: {
repo_id: string;
resolved_from: {
org_slug: string;
repo_name: string;
} | null;
room: string | null;
} | null;
/**
* Whether this checkout's LOCAL `.git/info/exclude` already carries
* `.run402/` (kygit-invite design D9's risk list: "`.git/info/exclude`
* is per-clone and silent" — this is what makes the state visible).
* `null` when no `repo_dir` was given.
*/
messaging_cache_excluded: boolean | null;
/**
* The vault's ref map and HEAD target — present only when `refs: true` was
* requested. Reading them means MATERIALIZING the chain, which is a
* verification and advances the local materialized pin, so plain `status`
* (an observation) leaves both `null`.
*/
refs: Record<string, string> | null;
head_target: VaultHeadTarget | null;
pins: {
highest_authenticated: string | null;
highest_materialized: string | null;
};
vault_policy: "required" | "grandfathered" | null;
/** Override journals on this machine that have not yet been completed. */
pending_overrides: number;
/**
* Best-effort count of this vault's covering `key_envelope` recipients (the
* same envelope-recipients read `Repos.access` uses), taken inside
* `status()` when a vault is allocated. `null` when unknown — no vault, or
* the read failed (never a new failure mode for `status`; falls back to the
* single-principal V0-A statements below). This is what decides between
* `terminal_loss_statement` and `durability_statement`: the V0-A terminal-loss
* claim is specifically a single-principal claim, and is factually false to
* print for a vault this client can locally prove has >= 2 recipients.
*/
covering_recipients: number | null;
/**
* Stated verbatim per the client-surface spec — this sentence is normative
* copy, not a summary, and is printed by `status` and `doctor` alike.
* `null` exactly when `covering_recipients >= 2` — see `durability_statement`
* for what is printed in that case instead.
*/
terminal_loss_statement: typeof VAULT_TERMINAL_LOSS_STATEMENT | null;
/** `null` exactly when `terminal_loss_statement` is — same condition, same reasoning. */
terminal_loss_detail: typeof VAULT_TERMINAL_LOSS_DOCTOR_TEXT | null;
/**
* The protocol's durability sentence — printed in place of
* `terminal_loss_statement` exactly when `covering_recipients >= 2`, since
* the terminal-loss claim is false for a vault this client has locally
* proven has a second covering recipient. `null` otherwise (including
* "unknown" — the single-principal statements are the honest default).
*/
durability_statement: typeof VAULT_DURABILITY_STATEMENT | null;
warnings: {
kind: string;
message: string;
}[];
next_actions: {
action: string;
command?: string;
}[];
}
/**
* What {@link Repos.scaffoldRemote} did, and why (D1). `name` is `origin`
* when it was free (or a caller-supplied name), `run402` when `origin` was
* already taken by something else, or the caller's explicit `remote_name`.
* `reason` is a human-readable sentence naming exactly what happened —
* printed to stderr by every CLI caller, never synthesized twice.
*/
export interface VaultScaffoldRemoteResult {
/** `scaffolded`: the remote is in place (added now or already present). `skipped`: `repo_dir` lies inside
* another repository, which is never touched — `toplevel` names it. */
status: "scaffolded" | "skipped";
/** Always `run402` unless an explicit `remote_name` was given; `origin` is never claimed. */
name: string;
url: string;
created_repository: boolean;
already_present: boolean;
existing_url: string | null;
reason: string;
/** The enclosing repository's toplevel when `status` is `skipped`. */
toplevel?: string;
/**
* `true` when `repo_dir` lies inside ANOTHER repository and was scaffolded
* as its own nested repository (`nested: true`). The enclosing repository
* is named by `enclosing_toplevel` and is never touched beyond one
* `/<relative path>/` line in its local `.git/info/exclude`
* (`excluded_in_enclosing` says whether that line is in place).
*/
nested?: boolean;
enclosing_toplevel?: string;
excluded_in_enclosing?: boolean;
/**
* A `skipped` result names the way out: `create_nested_repo`, pointing at
* `run402 repos create --nested --project <project_id>` — a nested repository
* keeps an encrypted remote for the app without touching the enclosing
* repository. Absent on a `scaffolded` result.
*/
next_actions?: NextAction[];
}
export interface VaultInitResult {
repo_id: string;
project_id: string;
/** Emitted once at creation — integrity data, not a secret. Print it, copy it, keep many copies. */
recovery_receipt: VaultCreationResult["recovery_receipt"];
genesis_sha256: string;
/** The git remote that was added, when a working tree was scaffolded. */
remote: VaultScaffoldRemoteResult | null;
deduplicated: boolean;
terminal_loss_statement: typeof VAULT_TERMINAL_LOSS_STATEMENT;
/**
* vault-byo-primary-bucket task 3.1/3.5 — the AUTHORITATIVE profile
* (read back from the vault record, never assumed from what `init`'s
* `byo` option requested). `"managed"` when `byo` was omitted, OR when it
* was requested but this project's vault already existed as a managed
* one under a different creation attempt.
*/
storage_profile: "managed" | "byo";
byo_destination: string | null;
}
export interface VaultCompactResult {
generation: string;
head_sha256: string;
form: "wal" | "checkpoint";
/** Set when a maintenance lease was taken and released around the checkpoint. */
maintenance_lease_id: string | null;
/** `false` when no `retention_cutoff` ticket could be obtained — roots are RETAINED (expiry is permissive). */
cutoff_bound: boolean;
covered_refs: number;
covered_roots: number;
/**
* What the transient-storage preflight saw
* (vault-compaction-headroom-preflight). `null` when the preflight could
* not be answered — see {@link VaultCompactHeadroom} for why an
* unanswerable check never blocks maintenance.
*/
headroom: VaultCompactHeadroom | null;
}
/**
* Compaction's transient pooled-storage arithmetic.
*
* Compaction publishes a checkpoint pack roughly the size of the vault's live
* content while every superseded object stays stored until prune completes —
* a transient footprint of roughly TWICE `source_bytes`, counted against the
* ORG's pooled tier storage. Saying nothing about this leaves an org near
* its cap paying the full encrypt+upload cost and then taking a mid-flight
* quota refusal that names a quota rather than the mechanism.
*
* This is advisory-grade by construction: it can only refuse EARLIER and more
* legibly than the platform's own storage-quota enforcement, never admit
* something that enforcement would refuse.
*/
export interface VaultCompactHeadroom {
/** Pooled storage the org is already using, across every project it owns. */
pool_used_bytes: number;
/** The org's plain, unraised pooled tier storage limit — always the tier's own figure, never the grant-raised one, so a disclosed "used of X pooled" never implies the tier itself grew. */
pool_limit_bytes: number;
/** The vault's billed `source_bytes` — the checkpoint-size proxy (design D1). */
vault_source_bytes: number;
/** `pool_used_bytes + vault_source_bytes`. */
projected_transient_bytes: number;
/** `false` when the projection exceeds the EFFECTIVE limit (`effective_pool_limit_bytes` when a grant is active, else `pool_limit_bytes`). */
ok: boolean;
/** `true` when the caller passed the override and a `false` `ok` was proceeded past anyway. */
overridden: boolean;
/**
* vault-checkpoint-cadence design D3: `pool_limit_bytes` PLUS an active
* compaction grant's `granted_bytes`, when one is active for this cycle —
* the limit `ok`/`projected_transient_bytes` are actually computed
* against. Equal to `pool_limit_bytes` (and omittable) when no grant is
* active.
*/
effective_pool_limit_bytes?: number;
/** The grant this compaction opened for itself, when one was opened and is still tracked at disclosure time — `null`/absent otherwise (no grant, an older gateway, or one already closed). */
compaction_grant?: {
granted_bytes: number;
expires_at: string;
} | null;
}
/**
* `run402 repos capture --dry-run`'s report shape
* — {@link Repos.planCapture}'s return type. Every sizing field is `null`,
* and `refs`/`objects` are empty, exactly when `allocation_needed` is `true`
* — see that method's doc comment for why sizing is genuinely UNKNOWABLE
* (not merely unreported) before the vault's encryption key exists.
*/
export interface VaultSnapshotPushPlan {
/** `true` when this project has no vault yet — a real push/snapshot would allocate one first (push-to-create). This dry run never does. */
allocation_needed: boolean;
base_generation: string | null;
would_admit_generation: string | null;
/** `would_admit_generation` as a plain decimal string. */
would_admit_generation_decimal: string | null;
form: "wal" | "checkpoint" | null;
refs: VaultRefMap;
head_target: VaultHeadTarget | null;
/** Every object that would be uploaded, with REAL sealed (encrypted) sizes. Empty when `allocation_needed`. */
objects: Array<{
object_kind: string;
size_bytes: string;
}>;
object_count: number | null;
/** Sum of `objects[].size_bytes` — the REAL ciphertext byte count. */
encrypted_bytes: string | null;
/** Sum of the plaintext pack bytes before sealing. */
raw_bytes: string | null;
/** The local capture this dry run computed — real, regardless of `allocation_needed`: capturing the work tree touches no network. */
snapshot: VaultSnapshot;
vault_commit: string;
vault_commit_line: string;
}
/** One retention ROOT (a dropped ref tip) and whether its 90-day window has closed. */
export interface VaultPruneCandidate {
ref: string;
oid: string;
dropped_at_generation: string;
eligible: boolean;
reason: string;
}
/** One STORED OBJECT the plan proposes deleting — what a `prune_intent`'s `delete_set` actually names. */
export interface VaultPruneObjectCandidate {
object_id: string;
object_kind: string;
size_bytes: string;
}
export interface VaultPruneResult {
/** Retention ROOTS and their windows — the retention view, unchanged. */
candidates: VaultPruneCandidate[];
eligible_count: number;
retained_count: number;
/**
* The stored objects the plan proposes deleting, in the intent's canonical
* order. A PROPOSAL: the gateway re-checks every candidate against the bound
* `retention_cutoff` ticket and its own admission times, and refuses any that
* is not retention-eligible. A client cannot prove either fact.
*/
object_candidates: VaultPruneObjectCandidate[];
/** Pruneable objects left out by the 10 000-per-intent cap; chunk into a later intent. */
deferred_object_count: number;
/** Why nothing may be pruned yet, or `null` when a submission is structurally possible. */
blocked_reason: string | null;
/**
* The SIGNED `prune_intent_core` this plan proposes, and its stored-bytes
* hash — what BOTH verifier receipts must sign. `null` when blocked.
*
* Round-trip this object verbatim into `submit`: it carries a random nonce
* and object id, so a rebuilt core is a DIFFERENT core and the receipt
* `r402s-verify` produced would no longer bind to it.
*/
intent_core: import("../node/vault-prune.js").VaultPruneIntentCore | null;
intent_core_sha256: string | null;
/** What this SDK observed while restoring the latest checkpoint — the receipt's evidence. */
attestation: import("../node/vault-publication.js").VaultStoredCheckpointAttestation | null;
/** `true` only when an intent was accepted by the gateway. */
submitted: boolean;
/** The gateway's view of the submitted intent; `null` when nothing was submitted. */
intent: import("../node/vault-prune.js").VaultPruneIntentRecord | null;
/**
* What the control-plane-signed completion CONFIRMS. `deleted` is the only
* result that means the bytes are gone: `present_after_attempt` is a failed
* deletion, not a successful one.
*/
confirmation: import("../node/vault-prune.js").VaultPruneConfirmation | null;
note: string;
}
/** Options shared by every Node-only verb. */
/** Result of {@link Repos.completeTransferHandover}. */
export interface TransferHandoverResult {
transfer_id: string;
repo_id: string;
writer: "activated" | "already_admitted";
envelope: "wrapped" | "already_covered";
vault_handover: TransferVaultHandover;
}
/** Result of {@link Repos.retirePreviousOwnerWriters}. */
export interface RetirePreviousOwnerWritersResult {
repo_id: string;
retired: {
writer_key_id: string;
principal_id: string | null;
}[];
rotation_required: boolean;
note: string;
/** The rotation that removed the retired keys; null when there was nothing to retire. */
rotation: import("../node/vault-publication.js").VaultRotationResult | null;
}
export interface VaultHandleOptions {
/** The vault to act on. Resolved from `project_id` when omitted. */
repo_id?: string;
/** Resolve `repo_id` from the project (the cold-restart entry point). */
project_id?: string;
/**
* vault-agent-envelopes D5 — the session-start envelope fulfilment a
* KEY-HOLDING client runs on its first ordinary vault operation in a
* process (read verbs included), so a pending member is covered the next
* time ANY key-holder does anything. `"auto"` (default): run once per
* process per vault, best-effort, reported on the handle. `"deferred"`
* (`--no-reconcile`): skip, report `deferred_by_local_policy` with the
* pending count — never pretends coverage. `"forbidden"`: forensic and
* offline operations (`fsck`, `--no-write`, `recover`) — investigating a
* suspicious pending recipient must not complete the disclosure.
*/
reconcile?: "auto" | "deferred" | "forbidden";
/** The local git working tree. Defaults to `process.cwd()`. */
repo_dir?: string;
/** Keystore root override (defaults to `~/.config/run402/vault`; `~/.config/run402/profiles/<wallet>/vault` under a named wallet). */
keystore_root?: string;
/** Pinned service public key for control-plane signature checks (cutoff tickets). */
service_public_key?: Uint8Array | string;
/** Heads verified per call; the verified prefix persists, so a budget-exceeded client resumes. */
verification_budget?: number;
}
/** An opened vault plus the pieces the caller may want to keep using. */
export interface VaultHandle {
repo_id: string;
keystore: VaultKeystore;
transport: VaultTransport;
/** The full protocol object — every verb below is built on it. */
vault: import("../node/vault-publication.js").Vault;
/**
* vault-agent-envelopes D4: non-null when this open restored the repo
* file from the keystore's OWN envelope (a cold keystore joining a vault it
* is a recipient of). Carries the honest trust tier — `platform_attested`
* is not end-to-end authentication.
*/
restored: import("../node/vault-publication.js").VaultColdOpenResult | null;
/**
* vault-agent-envelopes D5: the session-start fulfilment outcome for a
* key-holding client (`null` when this open did not run one — a restored
* keystore has nothing to wrap yet, `reconcile: "forbidden"`, or a later
* open in the same process). Reported beside the verb's own result, never
* folded into it.
*/
reconcile_recipients: VaultSessionReconcileResult | null;
/**
* vault-multi-writer (task 5.7) — the session-start writer-admission
* reconcile's outcome. `null` exactly when `reconcile: "forbidden"` — this
* reconcile carries no disclosure risk (it wraps nothing), so unlike
* {@link reconcile_recipients} it is otherwise ALWAYS attempted, no
* custody-verification gate and no once-per-process memoization: {@link
* import("../node/vault-publication.js").Vault.
* reconcileWriterAdmissions} is already a fast no-op (one local pin check,
* no network call) whenever this session's own key is not an active
* writer.
*/
writer_reconcile: VaultReconcileWriterAdmissionsPushResult | null;
/** vault-agent-envelopes D3: what the enroll-if-absent step did for this keystore's key on this open. */
enrollment: VaultEnrollmentOutcome;
}
/** The session-start reconcile's outcome (vault-agent-envelopes D5). */
export interface VaultSessionReconcileResult {
attempted: boolean;
outcome: "reconciled" | "skipped_error" | "deferred_by_local_policy" | "forbidden" | "custody_unverified";
result?: VaultReconcileEnvelopeRecipientsResult;
/** `deferred_by_local_policy` only: desired recipients this vault does not yet cover (best-effort; `null` when the read failed). */
pending_count?: number | null;
error?: string;
}
/** The enroll-if-absent step's outcome (vault-agent-envelopes D3). Never a rotation. */
export interface VaultEnrollmentOutcome {
/** `already_active`: the directory holds this keystore's key (BOTH halves current — encryption active AND signing_fingerprint already matches, vault-multi-writer rev 47). `enrolled`: published + possession-proven in this call. `activated_pending`: an earlier unfinished publish was completed. `signing_republished` (rev 47): the encryption half was ALREADY active and needed no work; only the signing half was (re)published this call — never a rotation, the signing half is always freely republishable. `skipped_no_identity`: no local keystore identity (nothing to enroll — vault creation mints one). `skipped_no_principal`: whoami resolved no enrolling principal (e.g. a service key). `skipped_not_enrollable`: the principal's type is not custody-eligible (ci/system) — no identity is minted. `skipped_error`: whoami/publish/activate failed (older gateway, transport) — the verb still ran, but custody continuity is UNVERIFIED, so no automatic reconcile follows; `error` says why. */
outcome: "already_active" | "enrolled" | "activated_pending" | "signing_republished" | "skipped_no_identity" | "skipped_no_principal" | "skipped_not_enrollable" | "skipped_error";
ek_fingerprint: string | null;
/**
* vault-multi-writer rev 47 (task 5.3) — the keystore's vault-WRITER
* signing half, published in the SAME call as the encryption half
* whenever this keystore holds a signing seed. `null` when the identity
* has no local signing seed (a read-only recovery identity — see
* `VaultIdentityFile.signing_seed_hex`'s own doc comment) or when the
* publish itself was never attempted (`skipped_no_identity`/
* `skipped_no_principal`/`skipped_not_enrollable`). UNLIKE the encryption
* half, the signing half is NEVER rotation-gated — a differing published
* signing key is simply refreshed in place, never a `KEY_ROTATION_REQUIRED`
* refusal, so this field's presence says nothing about whether a
* publish actually happened this call vs. was already current.
*/
signing_fingerprint: string | null;
error?: string;
}
/** {@link Repos.capture}'s best-effort envelope-recipient reconcile outcome, reported beside (never folded into) the vault result — same non-blocking contract as {@link VaultMirrorPushResult}. */
export interface VaultReconcileEnvelopeRecipientsPushResult {
attempted: boolean;
outcome: "reconciled" | "skipped_error";
result?: VaultReconcileEnvelopeRecipientsResult;
error?: string;
}
/**
* vault-multi-writer (task 5.7) — the writer-admission twin of {@link
* VaultReconcileEnvelopeRecipientsPushResult}: best-effort, reported
* beside (never folded into) the vault result. ONE shape used at every
* wiring site (push/snapshot/deploy/session-start/read) — unlike the
* envelope reconcile's split between this push-result shape and the
* richer {@link VaultSessionReconcileResult}, `"forbidden"` is included
* here too since this reconcile's session-start policy (task 5.7's own,
* deliberately simpler than the envelope reconcile's custody-gated one —
* see {@link VaultHandle.writer_reconcile}) honors only that one value;
* it is simply unreachable at the push/snapshot/deploy sites, which never
* check `options.reconcile` for this hook.
*/
export interface VaultReconcileWriterAdmissionsPushResult {
attempted: boolean;
outcome: "reconciled" | "skipped_error" | "forbidden";
result?: VaultReconcileWriterAdmissionsResult;
error?: string;
}
/** {@link Repos.handoff}'s result. `handoff_key` (the assembled `kgh1_…`) is returned exactly ONCE. */
export interface VaultHandoffMintResult {
handoff_key: string;
handoff_id: string;
kind: "handoff";
minted_role: string;
expires_at: string;
vault: {
vault_id: string;
address?: string | null;
organization_id: string;
project_id: string;
};
checkpoint: {
generation: string;
snapshot_oid_hmac: string;
};
capture: {
modified_captured: number;
untracked_captured: number;
sensitive_excluded: string[];
ignored_not_transferred_count: number;
};
/** The full local capture result, for a caller that wants more than the summarized `capture` block. */
snapshot: import("../node/vault-snapshot.js").VaultHandoffSnapshot;
warnings: {
code: string;
message: string;
}[];
next_actions: NextAction[];
}
export interface VaultHandoffListEntry {
handoff_id: string;
kind: string;
state: "issued" | "redeemed" | "expired" | "revoked";
minted_role: string;
minted_by: string;
expires_at: string;
redeemed_by?: string | null;
}
export interface VaultHandoffListResult {
handoffs: VaultHandoffListEntry[];
}
/** {@link Repos.resume}'s result. */
export interface VaultHandoffResumeResult {
handoff_id: string;
kind: "handoff";
deduplicated: boolean;
/** The Handoff Note, parsed — `null` when the commit message could not be read/parsed (still restored either way). */
note: import("../node/vault-handoff.js").KygitHandoffNote | null;
/** The note's raw commit-message text, for a caller that wants Markdown rendering over the parsed shape. */
note_raw: string | null;
restored: {
dir: string;
branch: string;
base_head_oid: string;
stash_oid: string;
};
membership: {
organization_id: string;
role: string;
status: string;
};
members: unknown[];
expires_at: string;
/** vault-multi-writer rev 47 (task 5.6, design D5) — this checkout's own writer activation. `outcome: "active"` covers BOTH a fresh submission this call made and the idempotent-skip case (a prior attempt's activation already landed) — the writer IS active either way. */
writer_activation: {
outcome: "active";
writer_key_id: string;
generation: string;
};
reconcile_recipients: VaultReconcileEnvelopeRecipientsPushResult;
next_actions: NextAction[];
}
/**
* The gateway names the vault by its three ids on the wire — `repo_id`,
* `org_id`, `project_id` (docs/style.md's API-boundary vocabulary) — on
* BOTH the handoff mint (`POST /vaults/v1/:vault_id/handoffs`) and
* claim (`POST /vaults/v1/handoffs/:handoff_id/redeem`) responses. The SDK
* groups them under `vault` with the `organization_id` spelling every other
* SDK result uses. Neither response carries a slug-form address, so
* `address` is `null` unless the caller already knows one (a slug-form
* remote at mint time). Pure; exported for tests.
*/
export declare function handoffVaultFromWire(wire: {
repo_id: string;
org_id: string;
project_id: string;
}, address?: string | null): VaultHandoffMintResult["vault"];
/** The redeem response's `membership` block (`org_id` on the wire) in the SDK's `organization_id` spelling. Pure; exported for tests. */
export declare function handoffMembershipFromWire(wire: {
org_id: string;
role: string;
status: string;
}): VaultHandoffResumeResult["membership"];
/**
* An invite mints at `developer` unless `--role` narrows it or the minter's
* own role is narrower — the ONE descriptor difference from a handoff, which
* mints at the minter's own role (kygit-invite design D1). Declared here
* because the client must predict the gateway's attenuated answer EXACTLY:
* the `writer_admission_grant` is signed with `minted_role` inside it before
* the mint call, and a disagreement is a `VALIDATION_FAILED` after the
* checkpoint has already been captured and pushed.
*/
export declare const INVITE_DEFAULT_ROLE = "developer";
/** A room's `(org_id, room_key)` pair, as it rides the invite mint/redeem responses. */
export interface VaultInviteRoom {
organization_id: string;
room_key: string;
}
/** Best-effort report of whether {@link Repos.invite}'s room message was posted — a failure here (e.g. the daily quota) never voids the mint (design D4). */
export interface VaultInviteRoomMessageResult {
posted: boolean;
message_id?: string;
/** The posted message's room cursor — a CLI advances the inviter's stored cursor past it, so the inviter's own `messages wait` is woken by the joiner's arrival, never by its own message. */
cursor?: string;
reason?: string;
}
/** Best-effort report of whether the inviter's own presence was registered before minting (design D4) — the mint still proceeds without one. */
export interface VaultInvitePresenceResult {
registered: boolean;
presence_id?: string;
name?: string;
error?: string;
}
/** {@link Repos.invite}'s result. `invite_key` (the assembled `kgi1_…`) is returned exactly ONCE. */
export interface VaultInviteMintResult {
invite_key: string;
invite_id: string;
kind: "invite";
minted_role: string;
expires_at: string;
vault: {
vault_id: string;
address?: string | null;
organization_id: string;
project_id: string;
};
room: VaultInviteRoom;
checkpoint: {
generation: string;
snapshot_oid_hmac: string;
};
capture: {
modified_captured: number;
untracked_captured: number;
sensitive_excluded: string[];
ignored_not_transferred_count: number;
};
/** The full local capture result, for a caller that wants more than the summarized `capture` block. */
snapshot: import("../node/vault-snapshot.js").VaultHandoffSnapshot;
inviter_presence: VaultInvitePresenceResult;
room_message: VaultInviteRoomMessageResult;
warnings: {
code: string;
message: string;
}[];
next_actions: NextAction[];
}
export interface VaultInviteListEntry {
invite_id: string;
kind: string;
state: "issued" | "redeemed" | "expired" | "revoked";
minted_role: string;
minted_by: string;
room_key: string;
expires_at: string;
redeemed_by?: string | null;
}
export interface VaultInviteListResult {
invites: VaultInviteListEntry[];
}
/** The inviter's presence, resolved live at redeem time (design vault-invite's own claim requirement) — `null` when the inviter never registered one. */
export interface VaultInviteInviter {
presence_id: string;
name: string;
program: string | null;
model: string | null;
state: string;
last_active: string;
}
/** {@link Repos.join}'s result. */
export interface VaultInviteJoinResult {
invite_id: string;
kind: "invite";
deduplicated: boolean;
/** The Invite Note, parsed — `null` when the commit message could not be read/parsed (still restored either way). */
note: import("../node/vault-handoff.js").KygitInviteNote | null;
/** The note's raw commit-message text, for a caller that wants Markdown rendering over the parsed shape. */
note_raw: string | null;
restored: {
dir: string;
branch: string;
base_head_oid: string;
stash_oid: string;
};
membership: {
organization_id: string;
role: string;
status: string;
};
members: unknown[];
room: VaultInviteRoom;
inviter: VaultInviteInviter | null;
/** This session's OWN presence in the room (what the arrival fact was posted as), or `null` when registration failed. A CLI persists it into the joined checkout so the first `messages wait` there speaks as the same session. */
presence: {
presence_id: string;
name: string;
} | null;
/** Why `presence` is null (the registration refusal, verbatim), else null. A joined checkout without a presence still works — its first `messages wait` registers one — but the reason is never silent. */
presence_failure: string | null;
live_presences: RoomPresence[];
cursor: string | null;
recent_messages: unknown[];
expires_at: string;
/**
* vault-multi-writer rev 47 (kygit-invite design D5/D9) — this
* checkout's own writer activation, driven through the SAME shared
* `add_writer_key` door {@link Repos.resume} uses, BEFORE `join()`
* returns, so the joiner's first `git push` is an ordinary push.
* `outcome: "active"` covers both a fresh submission and the
* idempotent-skip case. `outcome: "pending"` is D9's not-stranded path: a
* concurrent rotation or a lost network left this key a PENDING writer of
* the vault; any live writer's next push or `repos access sync` admits
* it, and `next_actions` carries `request_writer_sync` saying so.
*/
writer_activation: {
outcome: "active";
writer_key_id: string;
generation: string;
} | {
outcome: "pending";
writer_key_id: string;
reason: string;
};
reconcile_recipients: VaultReconcileEnvelopeRecipientsPushResult;
next_actions: NextAction[];
}
/** What {@link Repos.openOrCreate} did. `created` is `null` exactly when `found` is `true`. */
/** `run402 repos mirror` (no-arg, a READ) and `repos view`'s mirror summary both compose this — what this machine and the mirror each believe (both honesty statements ride every response). */
export interface VaultMirrorStatus {
repo_id: string;
configured: boolean;
/** `s3://bucket/prefix` or a directory path — never a credential. */
destination: string | null;
credential_kind: "profile" | "ambient" | null;
/** The newest generation the MIRROR holds and chain-verifies (keyless). `null` when unconfigured, unreachable, or empty. */
mirrored_generation: string | null;
/** The LIVE vault's newest generation (one vault-record read). `null` when unconfigured or the vault has never captured. */
newest_generation: string | null;
/** `null` when either side is unknown (never fabricated from a partial read). */
is_current: boolean | null;
/** Present exactly when `is_current === false`. */
closing_command: string | null;
/** vault-mirror-default: when a mirror write/sync last completed with zero failures (local config fact — never transmitted). `null` when unconfigured or never succeeded. */
last_success_at: string | null;
/** vault-mirror-default: the standing `vault_unmirrored` finding — informational, never blocking; `null` once a mirror write or sync has succeeded (see {@link vaultUnmirroredFinding}). */
finding: VaultUnmirroredFinding | null;
validity_not_freshness: typeof VAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT;
keystore_still_required: typeof VAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT;
}
/**
* vault-mirror-default — the named standing finding for a vault with no
* customer-held mirror copy yet. vault-byo-primary-bucket task 3.5
* widens `message` to the BYO remedy wording ({@link
* VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT}) for a BYO vault — it applies
* there too (D7): a single-bucket BYO vault has exactly as few copies as an
* unmirrored managed one, and the remedy names a SECOND customer-held
* location the same way.
*/
export interface VaultUnmirroredFinding {
kind: "vault_unmirrored";
message: typeof VAULT_UNMIRRORED_FINDING_STATEMENT | typeof VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT;
/** The one command that moves toward clearing it: configure when unconfigured, backfill when configured-but-never-succeeded. */
setup_command: string;
}
/**
* vault-byo-primary-bucket task 3.3 — the number of `{key, object_kind}`
* entries `VAULT_BYO_OBJECT_MISSING`'s `details.missing` lists before
* truncating (`details.missing_count` always carries the true total, and
* `details.missing_truncated` says whether the list above was cut). A
* chain-referenced object list can be arbitrarily large; an error envelope
* is not the place to reproduce it whole. Matches the platform's existing
* `sample_keys`-class cap (asset-sync's plan response, `PLATFORM_INCIDENT_
* FANOUT_CAP`) rather than inventing a new number.
*/
export declare const VAULT_BYO_OBJECT_MISSING_LIST_CAP = 50;
/**
* vault-byo-primary-bucket task 3.3 (design D6) — `repos fsck`'s wiring of
* the shipped {@link import("../node/vault-mirror.js").verifyByoObjectsPresent}
* read-half primitive. Present on {@link VaultFsckResult.byo_presence}
* ONLY for a `storage_profile: "byo"` vault — `undefined` (the key absent
* from JSON entirely) for a managed vault, so a managed vault's `fsck`
* output is byte-identical to before this task.
*
* `verified: true` NEVER coexists with a missing object: a nonzero absence
* throws `VAULT_BYO_OBJECT_MISSING` instead of returning here (see
* {@link Repos.fsck}'s own doc comment) — this shape only ever reports
* the two honest non-failure outcomes, "checked, all present" and "could
* not check".
*/
export interface VaultFsckByoPresence {
/**
* `true` iff this call actually HEAD-checked the destination (local BYO
* write credentials were configured on this machine). `false` means "not
* checked" — see `not_checked_reason` — which is deliberately NOT a
* failure: a BYO vault a returning agent has no local credentials for
* must never break its ordinary `fsck`.
*/
verified: boolean;
/** The BYO destination's address, from the vault record. Populated even when `verified` is `false` — a BYO vault always names its destination; it is the CREDENTIAL to reach it that may be missing on this machine. */
destination: string | null;
/** Objects HEAD-checked against the destination. `0` when `verified` is `false`. */
checked_count: number;
/** Present only when `verified` is `false` — why this call could not verify presence. Never blank: silence must never be mistaken for a clean verdict. */
not_checked_reason: string | null;
}
/**
* `repos fsck`'s result. Explicit pin fields make a local mutation visible rather than
* implicit — the external review's clause-5 requirement for any verb that
* may advance local trust state.
*/
export interface VaultFsckResult {
repo_id: string;
/** `false` under `--no-write` (`write: false`) — nothing local was persisted, regardless of what was computed. */
write: boolean;
verified_from_generation: string | null;
/**
* Alias for `chain_verified_to_generation` below (kept for back-compat —
* every existing caller reads this field). Chain/signature verification
* ALWAYS reaches this generation, independent of decrypt capability
* (D193, rev 42: an admitted `rotate_epoch` transition is keylessly
* structural, never a stopping point for chain verification).
*/
verified_to_generation: string;
/** Same value as `verified_to_generation` — the honest name for what this field actually measures (Request 1's split). */
chain_verified_to_generation: string;
/**
* The newest generation this call could actually DECRYPT — restoration's
* real ceiling. Equals `chain_verified_to_generation` on a healthy vault;
* falls short of it when an admitted `rotate_epoch` transition's own
* envelope could not be opened by this keystore (`epoch_decrypt_failure`
* names exactly which epoch/rotation and why — never a bare, undifferentiated
* `VAULT_AEAD_AUTH_FAILURE`). `refs`/`head_target`/`pin_after.highest_materialized`
* below all reflect THIS generation, not the chain-verified one, when they differ.
*/
decryptable_to_generation: string;
/** Non-null iff `decryptable_to_generation` fell short of `chain_verified_to_generation` — the named epoch boundary (Request 1). */
epoch_decrypt_failure: import("../node/vault-publication.js").VaultEpochDecryptFailure | null;
/** `true` only when `write` was true AND a pin actually moved. */
local_state_changed: boolean;
pin_before: {
highest_authenticated: string | null;
highest_materialized: string | null;
};
pin_after: {
highest_authenticated: string | null;
highest_materialized: string | null;
};
/** The real, computed ref map, AS OF `decryptable_to_generation` — present even under `--no-write` (computing is not the same as persisting). */
refs: VaultRefMap;
head_target: VaultHeadTarget | null;
/** Present only when `--mirror` was requested. Proves validity, never freshness — see its own honesty statements. */
mirror: VaultVerifyReport | null;
/**
* clone-installs-retained-refs (D2, task 1.3): the healing path for a
* pre-change clone (or a checkout whose original write degraded per D3) —
* `null` under `--no-write` (a genuine audit mode persists nothing, refs
* included) or when no local checkout was addressed (`repo_dir` absent, or
* `repo_dir` is not itself a git repository — an ordinary, silent no-op,
* never a warning).
*/
retained_refs: import("../node/vault-publication.js").VaultRetainedRefsReconcileResult | null;
/**
* D210 (rev 44) — best-effort submission of THIS call's OWN
* `chain_verified_to_generation`/`decryptable_to_generation` as
* recipient-attested proof-of-open evidence (`POST
* …/recipients/:principal_id/proof-of-open`), the SDK/CLI follow-up
* D210's own decision log names ("an `fsck --attest-open`-class
* submitter"). Automatic in WRITE mode when this keystore holds a local
* encryption identity; `{attempted: false, ...}` under `--no-write` (a
* genuine audit mode creates no server-side state) or when there is no
* local identity to submit evidence for. This NEVER changes `fsck`'s own
* verdict above — a submission failure (principal resolution, network,
* `OPEN_PROOF_MISMATCH`) is recorded in `error` and nothing else; a
* `recipient_open_receipt` is EVIDENCE, never authorization (§4.14).
*/
open_proof: VaultOpenProofOutcome;
/**
* vault-byo-primary-bucket task 3.3 — see {@link VaultFsckByoPresence}.
* Absent entirely (not `null`) for a `storage_profile: "managed"` vault, so
* `JSON.stringify` drops the key and a managed vault's `fsck` output stays
* byte-identical to before this task existed. A `verified: true` result
* with objects missing is never returned here — see the interface's own
* doc comment for why that case throws `VAULT_BYO_OBJECT_MISSING`
* instead.
*/
byo_presence?: VaultFsckByoPresence;
}
/** {@link VaultFsckResult.open_proof} — the outcome of `fsck`'s best-effort D210 proof-of-open submission. */
export interface VaultOpenProofOutcome {
/** `false` iff this call decided LOCALLY there was nothing to submit (write:false, or no local encryption identity) — no network call was made. */
attempted: boolean;
/** `true` iff the gateway accepted the submission (200 or 201) — `receipt`/`deduplicated` are populated. */
submitted: boolean;
/** `true` — the gateway returned the tuple's EXISTING receipt (200, an idempotent replay). `false` — a fresh receipt was minted (201). `null` when `submitted` is `false`. */
deduplicated: boolean | null;
/** The `recipient_open_receipt` the gateway returned, present iff `submitted`. */
receipt: VaultOpenReceipt | null;
/** Present iff `attempted` and NOT `submitted` — why the gateway (or principal resolution before it) refused. */
error: {
code: string;
message: string;
} | null;
}
/**
* D210 (rev 44) — the decision function behind `fsck`'s best-effort
* proof-of-open submission, factored out of {@link Repos}'s private
* `#submitFsckOpenProof` so it is directly unit-testable with injected
* (fake) `resolvePrincipalId`/`submit` dependencies, independent of a real
* HTTP-backed vault. `fsck()` itself supplies the REAL dependencies (`GET
* /agent/v1/whoami`, {@link Repos.submitProofOfOpen}) — this function
* contains no HTTP/transport code of its own.
*
* Gating (all LOCAL, no network call made when either holds):
* - `write === false` — a genuine audit mode; submitting a receipt is a
* real server-side mutation (idempotent or not), so it never fires.
* - `ekFingerprint === null` — no local encryption identity to submit
* evidence FOR.
*
* Otherwise: resolve `principal_id` (`resolvePrincipalId`), then submit
* `evidence` VERBATIM — `chain_verified_to_generation`/
* `decryptable_to_generation` pass through completely unchanged from the
* caller, never recomputed or rounded here.
*
* FAILURE CONTAINMENT (the load-bearing property): this function NEVER
* throws. Every failure — `resolvePrincipalId` resolving `null`,
* `resolvePrincipalId`/`readerEntrypoint`/`submit` throwing for any reason
* (network, `OPEN_PROOF_MISMATCH`, anything) — is caught and reported in
* the returned outcome's `error`; the caller's own already-computed result
* (`fsck`'s verdict) is never touched by this function's own failure.
*/
export declare function computeOpenProofOutcome(input: {
write: boolean;
ekFingerprint: string | null;
evidence: {
chain_verified_to_generation: string;
decryptable_to_generation: string;
};
resolvePrincipalId: () => Promise<string | null>;
readerEntrypoint: () => Promise<string>;
submit: (principalId: string, ekFingerprint: string, evidence: {
chain_verified_to_generation: string;
decryptable_to_generation: string;
reader_entrypoint: string;
}) => Promise<{
receipt: VaultOpenReceipt;
deduplicated: boolean;
}>;
}): Promise<VaultOpenProofOutcome>;
/**
* vault-byo-primary-bucket task 3.3 (design D6) — the decision function
* behind `fsck`'s BYO presence check, factored out of {@link Repos}'s
* private `#checkByoPresence` the same way {@link computeOpenProofOutcome}
* is factored out of `#submitFsckOpenProof`, so it is directly unit-testable
* with an injected (fake) `hasLocalConfig`/`verifyPresence`, independent of
* a real keystore or HTTP client. `#checkByoPresence` supplies the REAL
* dependencies (`readByoConfig` against the real keystore,
* `verifyByoObjectsPresent` against the real client) — this function
* contains no filesystem/HTTP code of its own.
*
* Runs AUTOMATICALLY for a `storage_profile: "byo"` vault — never behind a
* new flag, so a returning agent's ordinary `fsck` catches a
* silently-emptied bucket without having to know to ask for it. Returns
* `undefined` for a managed vault (`vault?.storage_profile !== "byo"` is
* the FIRST check, before `hasLocalConfig`/`verifyPresence` are ever
* called) — a managed vault pays zero extra network/filesystem work and
* this task changes nothing about it (design D6/D9).
*
* NO local credentials configured (`hasLocalConfig()` returns `false`) is
* reported, never thrown — a BYO vault this machine cannot reach is an
* honest "not checked", not a failure that should break `fsck` for a
* credential-less returning agent (see {@link VaultFsckByoPresence}'s
* own doc comment). A confirmed absence is the opposite: it THROWS
* `VAULT_BYO_OBJECT_MISSING` — the same severity class as {@link
* Repos.mirrorVerify}'s own `VAULT_MIRROR_NOT_CONFIGURED` throw when
* `--mirror` is requested against an unconfigured vault — this is exactly
* the honesty D6 names: "we can tell the customer what SHOULD exist and
* does not", stated as a real refusal rather than a silently-embedded
* finding. The listed entries are capped at
* {@link VAULT_BYO_OBJECT_MISSING_LIST_CAP} (a chain-referenced object
* list can be arbitrarily large and this is an error envelope, not a
* report); `missing_count` always carries the true total regardless of how
* many are listed.
*
* `verifyPresence()` is a pure read (HEAD checks only, via
* `verifyByoObjectsPresent`) — this function persists nothing itself, so
* it behaves identically whether `fsck` was called with `write: true` or
* `write: false`.
*/
export declare function computeByoPresenceOutcome(input: {
repoId: string;
vault: {
storage_profile?: "managed" | "byo";
byo_destination?: string | null;
} | null;
hasLocalConfig: () => boolean;
verifyPresence: () => Promise<VaultByoPresenceReport>;
}): Promise<VaultFsckByoPresence | undefined>;
/**
* `repos access`'s result — a READ over whatever the live gateway surface
* exposes today. The gateway's `envelope-recipients` read reports
* server-authoritative, membership-driven desired-recipient state
* (`desired[]` + `desired_state_version`), so per-recipient `envelope_state`
* (`converged` when the desired key is wrapped on this vault, `pending` when
* it is not, `pending_removal` when membership removal has not yet been
* enforced) is real. `envelope_state_available` reflects this at RUNTIME —
* it is `false` only against an older gateway that has not shipped
* `desired[]` yet (a rolling deploy window, or a pinned older `apiBase`),
* never a hardcoded claim.
*
* ONE gap remains genuine and gateway-independent: `history_scope` (which
* epochs each recipient can read) has no substrate to report, because
* vault protocol v0 pins a single fixed epoch for a vault's entire
* lifetime — there is no per-epoch scope to have. `stale_access` below is
* the closest observable proxy for the same underlying limitation: a
* `pending_removal` recipient whose fingerprint is STILL covered still
* decrypts every commit (past AND future) under that one epoch, because
* epoch rotation — the mechanism that would actually revoke them — is
* mid-fold in `vault-human-envelopes` under adversarial protocol review.
*
* `access` reports what the read surface HAS: the org's directory of
* encryption-key-holding members, which of the vault's current
* envelope-recipient fingerprints match a directory entry, the
* server-reported desired state per recipient when available, and
* (Node-only, best-effort) this machine's own local TOFU pin for each
* principal, when it has ever wrapped one.
*/
export interface VaultAccessRecipient {
principal_id: string;
display_name: string | null;
/** This recipient's fingerprint per the org directory. */
fingerprint: string;
/** `true` when this fingerprint has a `key_envelope` on the vault today. */
covered: boolean;
/**
* The server's desired-recipient state for this principal, cross-referenced
* against `covered`: `"converged"` (desired + covered), `"pending"`
* (desired, not yet wrapped), `"pending_removal"` (membership removed
* them but this vault has not been re-keyed away from them — see
* `stale_access`), or `null` when the gateway did not report desired-state
* for this principal (older gateway, or the directory and desired-state
* reads disagree, which should not happen but is reported honestly rather
* than papered over).
*/
envelope_state: "converged" | "pending" | "pending_removal" | null;
/** THIS machine's own local trust-on-first-use record for this principal, or `null` if this machine never wrapped them. Never a server-side fact. */
tofu_pin: {
fingerprint: string;
matches_directory: boolean;
} | null;
}
/** A principal whose membership was removed (desired state `pending_removal`) but who STILL holds a covering `key_envelope` on this vault — real, continuing access that removal did not revoke, because vault v0 has no epoch-rotation mechanism yet. See {@link VaultAccessResult}'s doc comment. */
export interface VaultStaleAccessEntry {
principal_id: string;
display_name: string | null;
fingerprint: string;
}
export interface VaultAccessResult {
repo_id: string;
org_id: string | null;
/** vault-agent-envelopes D5: this KEY-HOLDER's session-start fulfilment outcome (`null` when this machine holds no K_repo for the vault, or `reconcile: "forbidden"`). */
reconcile_recipients?: VaultSessionReconcileResult | null;
/** vault-agent-envelopes D3: the enroll-if-absent step's outcome (`null` when it did not run). */
enrollment?: VaultEnrollmentOutcome | null;
recipients: VaultAccessRecipient[];
/** Vault-covering fingerprints (from the server) that match neither a directory entry nor a desired-state row — genuinely orphaned, revoked outside this org's membership model, or external. Excludes fingerprints already explained by `stale_access` AND by `this_keystore`. */
unmatched_covered_fingerprints: string[];
/**
* Node-only, best-effort: set when an otherwise-unmatched covering
* fingerprint equals THIS keystore's own encryption-key fingerprint — the
* one case an unmatched fingerprint is locally provable rather than a
* genuine unknown. Typically the vault creator's own wallet-principal
* keystore, which the org directory never lists (it only enrolls human
* keys). `null` when no local match was found (including non-Node
* callers, or a machine that never held this identity).
*/
this_keystore: {
fingerprint: string;
/** `true` iff the directory holds this keystore's key as ACTIVE (vault-agent-envelopes: agents enroll too). */
enrolled: boolean;
/** `absent` = no published key (the next vault operation enrolls); `pending` = published, possession unproven; `rotation_required` = the principal's published key is a DIFFERENT key; `unknown` = whoami unavailable. */
publish_state: "active" | "pending" | "absent" | "rotation_required" | "unknown";
covered_on_this_vault: boolean;
/** @deprecated alias of `covered_on_this_vault`. */
covered: boolean;
next_actions?: Array<{
action: string;
why: string;
}>;
} | null;
/** Removed members who still decrypt this vault — see {@link VaultStaleAccessEntry}. Always `[]` when `envelope_state_available` is `false` (no desired-state substrate to compute it from). */
stale_access: VaultStaleAccessEntry[];
/** `true` when the gateway reported desired-recipient state (`desired[]`) for this read, making `recipients[].envelope_state` and `stale_access` real rather than absent. `false` only against an older gateway. */
envelope_state_available: boolean;
/** Always `false` — see this type's own doc comment for why this is a protocol-level absence, not a missing gateway feature. */
history_scope_available: false;
/** The honest, human-readable statement of the gap above. Read it before assuming `covered: true` means "converged," and before assuming `pending_removal` means access was actually revoked. */
gap: string;
/** Present only when `stale_access` is nonempty — the exact owner-driven remedy (D193-D203, rev 42): `repos access repair` for a general re-key, `repos access revoke-key <principal_id>` for one targeted principal. */
next_actions?: {
action: string;
why: string;
}[];
}
/**
* `repos list`'s bulk read —
* `GET /vaults/v1?org_id=<uuid>`.
* FROZEN response shape, agreed with the gateway
* team ahead of the route landing; the route may still 404 on a gateway
* that has not shipped it yet, and callers should fall back to the
* per-project walk (`status()` in a loop) until then — see
* `cli/lib/repos.mjs`'s `list()` for that fallback, kept only until every
* deployed gateway answers this route.
*/
export interface VaultOrgVaultSummary {
repo_id: string;
project_id: string;
project_name: string | null;
repo_name: string | null;
org_slug: string | null;
vault_policy: "required" | "grandfathered" | null;
newest_generation: string | null;
source_bytes: string;
genesis_admitted_at: string | null;
created_at: string;
/** vault-byo-primary-bucket task 3.5 — absent-or-`"managed"` is byte-identical to today. */
storage_profile?: "managed" | "byo";
}
export interface VaultOrgVaultsListing {
vaults: VaultOrgVaultSummary[];
/** Keyset pagination (agent-response-design): opaque, store-and-echo. */
has_more?: boolean;
next_cursor?: string | null;
}
export interface VaultOpenOrCreateResult {
handle: VaultHandle;
/** `true` when the vault already existed — nothing was allocated by this call. */
found: boolean;
created: {
/** Mirrors `VaultInitResult.deduplicated`: an existing local creation journal was resumed to completion rather than started fresh — nothing was re-minted. */
deduplicated: boolean;
/** Emitted once at creation — integrity data, not a secret. Print it, copy it, keep many copies. Persisted into the keystore regardless. */
recovery_receipt: VaultCreationResult["recovery_receipt"];
genesis_sha256: string;
} | null;
terminal_loss_statement: typeof VAULT_TERMINAL_LOSS_STATEMENT;
}
/**
* {@link Repos.deploy}'s post-push glue: attach the best-effort
* mirror/reconcile hooks to a deploy result, gated on whether this deploy
* actually landed a new generation in the vault. Extracted as a standalone
* function (rather than inlined in `deploy()`) purely so the gating can be
* unit-tested with fake thunks — the real hooks (`#tryMirrorPush` /
* `#tryReconcileEnvelopeRecipients`) are private class methods that already
* catch everything and never reject, so this function does not need its own
* try/catch: it only decides WHETHER to call them and how to merge what they
* resolve to.
*
* `DEPLOYED_AND_VAULTED` and `DEPLOY_FAILED_VAULTED` are the only two
* outcomes that carry a `generation` — a vault push actually landed. The
* other three (`DEPLOY_BLOCKED_PUSH_FAILED`, `DEPLOY_FAILED_UNVAULTED`,
* `DEPLOYED_UNVAULTED_OVERRIDE`) published nothing new, so `mirror_push` /
* `reconcile_recipients` are OMITTED rather than a faked `skipped_*` value —
* there is nothing this deploy did that either hook could report on.
*/
export declare function attachVaultDeployHooks(result: VaultDeployResult, mirror: () => Promise<VaultMirrorPushResult>, reconcile: () => Promise<VaultReconcileEnvelopeRecipientsPushResult>): Promise<VaultDeployResult & {
mirror_push?: VaultMirrorPushResult;
reconcile_recipients?: VaultReconcileEnvelopeRecipientsPushResult;
}>;
export declare class Repos {
#private;
constructor(client: Client);
/** The vault record — policy, allocation generation, storage + maintenance state. */
get(repoId: string): Promise<VaultRecord>;
/**
* Resolve a project's vault with no local state. This is the cold-restart
* entry point: an agent that lost its machine but still holds authority on
* the project learns its `repo_id` here.
*/
forProject(projectId: string): Promise<VaultRecord>;
/**
* Every vault the organization owns, one round trip — `repos list`'s bulk
* read. See {@link
* VaultOrgVaultsListing}'s doc comment for the FROZEN response shape and
* the 404-until-shipped fallback contract.
*/
listByOrg(orgId: string): Promise<VaultOrgVaultsListing>;
/**
* Resolve a vault by its address-form `org-slug/name` —
* `GET /vaults/v1?repo=<org-slug>/<name>`.
* `RESOURCE_NOT_FOUND` for no such org OR no such name (deliberately
* collapsed — see the design's slug-namespace-probing note);
* `SLUG_RELEASED` (read it with {@link vaultSlugReleasedInfo}) while the
* slug is in its post-rename cooldown — never auto-followed.
*/
forRepo(address: {
org_slug: string;
repo_name: string;
}): Promise<VaultRecord>;
/**
* Resolve a parsed remote address (`parseVaultRemoteUrl`'s output),
* dispatching on its form: id-form resolves exactly like
* {@link forProject}; slug-form resolves via {@link forRepo}. A pure read —
* no pinning, no creation. Node-only callers wanting BOTH should use
* {@link resolveOrCreateAddress} instead, which also drives the local pin
* and (opt-in) push-to-create.
*/
resolveAddress(address: VaultRemoteAddress): Promise<VaultRecord>;
/**
* One page of the heads listing (D186).
*
* `after_generation` is the REQUIRED verification anchor — a semantic input,
* never a paging knob — and must stay CONSTANT across a page sequence.
* `limit` is required. `cursor` is omitted on the first request and is then
* the prior page's `next_cursor` echoed UNCHANGED: store and echo, never
* parse. A malformed or stale cursor is `INVALID_CURSOR`; recover by
* restarting from `after_generation` with no cursor.
*/
heads(repoId: string, request: VaultHeadsListingRequest): Promise<VaultHeadsListingPage>;
/**
* Walk the whole listing above the anchor, verifying page coupling, ordering,
* and gaplessness as it goes (a gap is `CHAIN_BROKEN`, never a silent skip).
*/
allHeads(repoId: string, options: {
after_generation: string;
limit?: string;
}): Promise<{
heads: VaultHeadsListingPage["heads"];
pages: number;
total: string | null;
}>;
/**
* Set the activation policy. Owner + step-up, audited, reason required.
* `grandfathered` leaves a doctor-persistent warning until it returns to
* `required`.
*/
setPolicy(repoId: string, input: {
vault_policy: "required" | "grandfathered";
reason?: string;
}): Promise<{
vault_policy: string;
vault_policy_version: string;
changed: boolean;
warnings: {
kind: string;
message: string;
}[];
}>;
/**
* Clear an unvaulted-override advisory by presenting a capture receipt that
* matches the journaled operation on EVERY field. A partial match never
* clears it.
*/
completeOverride(repoId: string, input: {
operation_id: string;
capture_receipt: VaultCaptureReceipt;
}): Promise<{
operation_id: string;
advisory_cleared: boolean;
generation: string;
head_sha256: string;
}>;
/**
* Take the owner's maintenance reservation for a compact/prune cycle.
*
* SECRET-BEARING: `holder_token` is returned exactly ONCE and is the liveness
* instrument for heartbeat/release. Never log it, never cache it, never place
* it in an agent-surface result store.
*/
/** `POST …/recipients/:principal_id/confirm` (D197) — first-seen pin confirmation. */
confirmRecipient(repoId: string, principalId: string, newFingerprint: string): Promise<VaultRecipientConfirmationReceipt>;
/** `POST …/recipients/:principal_id/repin` (D197) — re-pin ceremony. */
repinRecipient(repoId: string, principalId: string, input: {
old_ek_fingerprint: string;
new_fingerprint: string;
}): Promise<VaultRecipientConfirmationReceipt>;
/**
* vault-agent-envelopes D3 — a KEY-HOLDER explicitly accepts a recipient's
* CHANGED key. The session-start reconcile refuses to wrap under a
* fingerprint that differs from this keystore's TOFU pin
* (`pinned_key_mismatch`) — that refusal is the substitution defence and
* is never bypassed automatically, not even after an owner's revoke: from
* this machine's view an owner-driven re-key and a platform substitution
* look identical, and the audit event is not proof against the platform.
* Acceptance is a deliberate act naming the new fingerprint (the
* out-of-band verification point — read it back with the recipient over any
* channel; it is public data): this records the D197 re-pin receipt with the
* gateway AND moves the local pin, so the next reconcile wraps. Refuses when
* `new_fingerprint` is not what the org directory currently serves for the
* principal (a stale or mistyped fingerprint never pins).
*/
acceptRecipientKeyChange(options: VaultHandleOptions & {
principal_id: string;
new_fingerprint: string;
}): Promise<{
repo_id: string;
principal_id: string;
old_fingerprint: string | null;
new_fingerprint: string;
receipt: VaultRecipientConfirmationReceipt | null;
}>;
/**
* `POST …/recipients/:principal_id/key-revocation` (D199) — declares
* `reason:"recipient_key_revoked"` admissible for the NEXT rotation this
* org's vaults submit; org-scoped, advances the same watermark a member
* removal does. Returns the D194 counters — the ONE client-visible read of
* them, which is why {@link rotateEpochForKeyRevocation} exists as the
* fully self-contained entry point.
*/
declareRecipientKeyRevoked(repoId: string, principalId: string): Promise<{
recipient_state_version: string;
recipient_revocation_version: string;
}>;
/**
* `POST …/epoch-secret-exposure` (D199) — declares `reason:"epoch_secret_exposed"`
* admissible for THIS vault (deliberately vault-scoped, not org-wide — one
* vault's leaked `K_repo`/`K_e` is not evidence any sibling vault is
* compromised). This is the rekey remedy for a leaked/exposed vault key:
* declare exposure here, then drive a `rotate_epoch` with
* `reason:"epoch_secret_exposed"` (the required counters must be supplied
* from a source other than this call — see {@link rotateEpoch}'s doc
* comment on the confirmed gap in what the gateway exposes today).
*/
declareEpochSecretExposed(repoId: string): Promise<{
epoch_secret_exposure_version: string;
}>;
/** `POST …/writer-authority/declare-unavailable` (D202) — an explicit, audited fact that the writer signing key is gone. */
declareWriterAuthorityUnavailable(repoId: string): Promise<{
declared_at: string;
declared_by: string | null;
}>;
/**
* `POST …/recipients/:principal_id/proof-of-open` (D210, rev 44) — submit
* `fsck`'s OWN `chain_verified_to_generation`/`decryptable_to_generation`
* evidence VERBATIM as proof that `principalId` can open this vault under
* its current epoch. `fsck()` itself calls this automatically (in write
* mode, when a local encryption identity exists) — call it directly only
* for a manual/explicit submission (e.g. resubmitting after fixing a
* local keystore issue, or from a caller that already resolved its own
* `principal_id` and does not want the extra `whoami` round trip
* `fsck()`'s own auto-submission pays).
*
* Self-match only: the gateway requires `principalId` to equal the
* AUTHENTICATED caller, never overridable by any credential class — a
* mismatch is the ordinary 403 `VAULT_ACCESS_DENIED`. Idempotent on
* `(repo_id, principal_id, ek_fingerprint, decryptable_to_generation)` —
* `deduplicated: true` means the gateway returned the tuple's EXISTING
* receipt (HTTP 200) rather than minting a fresh one (HTTP 201).
*/
submitProofOfOpen(repoId: string, principalId: string, evidence: {
ek_fingerprint: string;
chain_verified_to_generation: string;
decryptable_to_generation: string;
reader_entrypoint: string;
}): Promise<{
receipt: VaultOpenReceipt;
deduplicated: boolean;
}>;
acquireMaintenanceLease(request: VaultMaintenanceLeaseRequest): Promise<VaultMaintenanceLease>;
/**
* Open the vault: keystore + HTTP transport + the protocol object. Every verb
* below builds on this; call it directly when you need the raw protocol
* surface (ref transactions, repair, checkpoint building).
*/
open(options?: VaultHandleOptions): Promise<VaultHandle>;
/**
* Open a vault, allocating it first when it does not exist yet (D2 — lazy
* allocation on first push).
*
* This is the ONE primitive the remote helper and the capture lane
* (`run402 repos capture`/`snapshot`) drive so that a push against an
* unallocated project runs the six-stage creation journal inline instead of
* refusing: it resolves `repo_id` from `project_id` exactly like
* {@link open}, and when that resolution fails, tries to create the vault —
* but ONLY when `org_id` was supplied. Without `org_id` this method is
* byte-identical to `open()`: the original resolution failure is rethrown
* unchanged, so every existing caller of `open()` that has no reason to
* create anything sees no behavior change at all by switching to this.
*
* RESUMABILITY (client-surface spec, D2): before starting a fresh creation
* attempt this looks for an INCOMPLETE local journal already matching this
* exact `(org_id, project_id)` and resumes ITS `client_creation_id`, rather
* than starting a second competing attempt every time the process is
* interrupted mid-creation. Kill the process after ALLOCATED and call this
* again: the same journal — not a new one — drives to ACTIVE, and exactly
* one vault exists either way. An explicit `client_creation_id` (tests, or
* a caller resuming a specific attempt by hand) always wins over that search.
*/
openOrCreate(options: VaultHandleOptions & {
org_id?: string;
client_creation_id?: string;
}): Promise<VaultOpenOrCreateResult>;
/**
* Resolve a parsed remote address to an OPEN handle, pinning `repo_id` in
* local git state on the first successful resolution of EITHER form
* (vault-client-round-trips design D4 widens the original slug-form-only
* pin to id-form too — an id-form address needs no pin to survive a
* rename, but the resolution round trip it skips is the same either way),
* and — when `allow_create` is set and resolution misses — push-to-create
* it. This is what the remote helper and `vault snapshot` drive for a
* `run402::<org>/<name>` remote, whichever form it names, so a caller need
* not branch on the address's form itself. A pinned id/address that no
* longer resolves (404) clears the pin and re-resolves once.
*
* `SLUG_RELEASED` is NEVER auto-followed — it rethrows unchanged; read it
* with {@link vaultSlugReleasedInfo} for the successor slug and cooldown.
*/
resolveOrCreateAddress(options: VaultHandleOptions & {
address: VaultRemoteAddress;
/** Push-to-create on a slug-form miss (D6). `false` (default): a miss is an ordinary not-found refusal — the read path (`list`/`fetch`, `status`). */
allow_create?: boolean;
client_creation_id?: string;
/** Fires once, only when THIS call allocated the vault. Awaited before the handle is returned. */
onVaultCreated?: (created: NonNullable<import("../node/vault-address.js").VaultAddressResolution["created"]>) => void | Promise<void>;
}): Promise<VaultOpenOrCreateResult & {
resolution: import("../node/vault-address.js").VaultAddressResolution;
}>;
/**
* Stale-pin recovery for OFFLINE address resolutions
* (vault-force-spelling-and-pin-fold): an id-carrying pin resolves with
* zero network reads, so a pin gone stale (its vault deleted and the
* project re-allocated a new one) surfaces as the VERB's first repo-scoped
* read failing rather than as a resolution failure. Call this with that
* failure: it answers a fresh opened handle + resolution to retry the verb
* against exactly once, or `null` when there is nothing to recover — the
* error is not a vault-absent signal, the resolution was not offline, or
* re-resolution lands on the SAME `repo_id` (the pin was fine and the
* original refusal is real; it is restored, and recovery never widens what
* an unauthorized caller learns).
*/
recoverStalePin(options: VaultHandleOptions & {
address: VaultRemoteAddress;
repo_dir: string;
resolution: import("../node/vault-address.js").VaultAddressResolution;
error: unknown;
}): Promise<(VaultOpenOrCreateResult & {
resolution: import("../node/vault-address.js").VaultAddressResolution;
}) | null>;
/**
* Create the vault for a project and scaffold the git remote.
*
* Runs the six-stage creation journal — `LOCAL_KEYS_PREPARED → ALLOCATED →
* OBJECTS_PREPARED → OBJECTS_FINALIZED → GENESIS_PREPARED → ACTIVE`, each
* fsynced before the next step, with no ciphertext existing before allocation
* supplies the `repo_id` the key derivation needs. Crash-safe and resumable
* on `client_creation_id`.
*
* An existing `origin` remote is never modified or claimed; only a remote
* named `run402` is added.
*/
init(options: {
org_id: string;
project_id: string;
repo_dir?: string;
keystore_root?: string;
/** Resume a specific creation attempt (or pin it in tests). */
client_creation_id?: string;
/** Skip the git scaffold — allocate the vault only. */
scaffold_git?: boolean;
/**
* Scaffold `repo_dir` as its OWN repository even when it lies inside
* another one (see {@link Repos.scaffoldRemote}'s `nested`). Default
* `false`: an app root inside another repository is reported `skipped`.
*/
nested?: boolean;
remote_name?: string;
remote_url?: string;
service_public_key?: Uint8Array | string;
/**
* vault-byo-primary-bucket task 3.1/3.2/3.5 — request a BYO vault.
* Same raw-string shape `mirrorSet` already takes (`destination_url` +
* `credential` + `region`/`endpoint`), so a CLI edge stays a thin
* adapter — no destination parsing lives outside the SDK. Omitted (the
* default) is byte-identical to today. This method RUNS the
* allocation-time bucket probe (D6) itself, BEFORE any allocation
* request — a failed probe throws `VAULT_BYO_BUCKET_PROBE_FAILED`
* and nothing is created.
*/
byo?: {
destination_url: string;
credential?: import("../node/vault-mirror-config.js").VaultMirrorCredential;
region?: string;
endpoint?: string;
};
}): Promise<VaultInitResult>;
/**
* Add the run402 git remote, initialising the repository if absent.
*
* D1 — claim `origin` additively. LLM muscle memory is `git push origin
* main` from a billion training examples, and a side-remote name costs
* every agent a correction cycle, so when the repository has no `origin`
* remote ours BECOMES `origin`. An existing `origin` is NEVER modified or
* reclaimed — no matter what it points at, including a prior run of this
* exact call — so the fallback is `run402`, and when THAT is also taken by
* something else, nothing is added at all: the additive discipline holds
* for every remote name this method ever touches, not just `origin`.
*
* An explicit `remote_name` is a caller override (no current caller passes
* one) and skips the origin/run402 dance entirely — the caller already
* decided the name; this method still never modifies an existing remote
* under it.
*
* No key material or allocation is required — the cold-start path gains no
* prompts or network dependencies from this.
*
* `nested: true` — the monorepo case. An app root that lies INSIDE another
* repository (an agent workspace with many apps under one checkout) is
* still made its own repository: `git init -b main` in `repo_dir`, the
* remote added there, and ONE line — `/<relative path of repo_dir>/` —
* appended to the ENCLOSING repository's local `.git/info/exclude`
* (located via `rev-parse --git-path info/exclude`, so a worktree resolves
* to its common dir) so the enclosing checkout never lists the nested
* repository as untracked noise. Never `.gitignore` (tracked content),
* never the index, never a submodule: nothing else in the enclosing
* repository changes, and its remotes are untouched. Idempotent — a second
* call on the already-nested app root adds nothing and re-reports the same
* shape. Without `nested`, the same situation is reported `skipped` with a
* `create_nested_repo` next_action naming this way out.
*/
scaffoldRemote(options: {
repo_dir: string;
org_id: string;
project_id: string;
remote_name?: string;
remote_url?: string;
nested?: boolean;
}): Promise<VaultScaffoldRemoteResult>;
/**
* What this machine and the control plane each believe about the vault.
*
* Truthful for a VAULT-ONLY project (protocol D183): a project that has never
* deployed raises no deploy-related warning, and the terminal-loss statement
* is stated verbatim exactly as for any other vault.
*/
status(options?: VaultHandleOptions & {
/**
* Also report the vault's ref map and HEAD target.
*
* Opt-in because it is not free and not read-only in the local sense:
* materializing walks the head chain (a verification) and advances the
* keystore's materialized pin. `status` without it stays a pure
* observation, which is what makes it safe to run anywhere.
*/
refs?: boolean;
}): Promise<VaultStatus>;
/**
* Capture the working tree and publish it — the push half of a deploy, run on
* its own. Never gated on a deploy: a vault-only project pushes for months
* without one.
*
* WHAT THIS PUBLISHES, and what it deliberately does not. This is the CAPTURE
* lane: it publishes the protocol-owned `refs/run402/deploys/latest` plus the
* `head_target` read off the local HEAD. The repository's OWN refs —
* `refs/heads/*` and `refs/tags/*` — reach the vault through
* `git push run402 …` via `git-remote-run402`, which is the lane the spec
* charges with reproducing them ("the helper SHALL reproduce the exact set of
* objects reachable from the declared canonical refs (`refs/heads/*`,
* `refs/tags/*`, protocol-owned `refs/run402/*`), plus the `HEAD` target" —
* vault-client-surface, "Faithful helper"; `run402 repos capture` is
* separately defined as "capture and push outside a deploy" under "Source
* verbs"). The split is forced by §6.6: a dirty tree captures as a SYNTHETIC
* commit that sits on no branch, so moving `refs/heads/main` onto it would
* rewrite the user's branch on every dirty push — exactly the history
* clobbering §6.1's fast-forward + force-with-lease rules exist to prevent.
*
* Hence `protocol_refs: "allow"`: this lane BUILDS the protocol ref itself,
* so it opts in. The remote helper must keep the `"refuse"` default, because
* there the refnames are user-supplied and a user must not be able to squat
* the `refs/run402/*` namespace.
*
* Before reporting a push as landed the vault compares finalization receipts
* against its expected manifest and reads the admitted head back from
* storage. A 200 alone is never enough (§0 client obligations).
*/
capture(options: VaultHandleOptions & {
/**
* The owning org. Supply it to run D2's lazy allocation: when this
* project's vault does not exist yet, capture() runs the six-stage
* creation journal inline (via {@link openOrCreate}) before capturing
* and publishing — one command, no prior `vault init`. Without it,
* capture() behaves exactly as it always did: a push against an
* unallocated vault throws the resolution failure unchanged.
*/
org_id?: string;
/**
* A parsed remote address (`parseVaultRemoteUrl`'s output).
* When given, this push resolves
* (and, for a slug-form address, pins) through
* {@link resolveOrCreateAddress} INSTEAD of `openOrCreate`'s
* project_id-based path; `org_id`/`project_id`/`repo_id` are ignored.
* Mirrors `git push`'s own push-to-create dispatch in the remote
* helper, for callers (`vault snapshot`) that resolve the target
* from the local git remote instead of `--project`.
*/
address?: VaultRemoteAddress;
/** Resume a specific creation attempt (or pin it in tests); auto-discovered from the local keystore otherwise. */
client_creation_id?: string;
/**
* Fires once, only when THIS call allocated the vault — the moment to
* print the one-shot recovery receipt and keystore path. Awaited
* before capture/publish continue, so any async work inside it (e.g.
* resolving the keystore path to print) completes and stays ordered
* BEFORE the rest of this push's own output.
*/
onVaultCreated?: (created: NonNullable<VaultOpenOrCreateResult["created"]>) => void | Promise<void>;
/**
* Capture options, forwarded verbatim to `captureSnapshot`. The commit
* message for the synthetic commit a dirty tree produces lives HERE
* (`snapshot: { message }`) — there is deliberately no second top-level
* `message` field, because a `snapshot` passthrough plus a sibling
* shortcut is two ways to say one thing with no defined precedence.
*/
snapshot?: Omit<import("../node/vault-snapshot.js").VaultSnapshotOptions, "dir">;
onCommitLine?: (line: string) => void;
checkpoint?: boolean;
}): Promise<VaultPublishResult & {
snapshot: VaultSnapshot;
vault_commit: string;
vault_commit_line: string;
mirror_push: VaultMirrorPushResult;
byo_chain_copy: VaultMirrorPushResult;
reconcile_recipients: VaultReconcileEnvelopeRecipientsPushResult;
writer_reconcile: VaultReconcileWriterAdmissionsPushResult;
}>;
/**
* Mint a Handoff Key: capture a stash-shaped checkpoint (design D1),
* push it (retained, on no branch — {@link VAULT_DEPLOY_REF} carries
* it exactly like an ordinary `capture()`, never `refs/heads/*`), seal the
* vault's current epoch key under a fresh `wrap_key`, and mint through
* the gateway. The assembled `kgh1_…` key is returned exactly ONCE —
* nothing here or downstream persists it.
*
* `options.note` omits `capture` — this method fills it with the real
* capture figures and runs the client-side secret scan BEFORE the
* handoff commit is written (design D10: no override flag).
*/
handoff(options: VaultHandleOptions & {
address?: VaultRemoteAddress;
role?: string;
ttlSeconds?: number;
note: Omit<import("../node/vault-handoff.js").KygitHandoffNote, "capture">;
includeSensitive?: string[];
onCommitLine?: (line: string) => void;
}): Promise<VaultHandoffMintResult>;
/** List a vault's handoffs (ids, kind, state, role, expiry, redeemed_by — never the hash or envelope). */
listHandoffs(options: VaultHandleOptions): Promise<VaultHandoffListResult>;
/** Revoke a handoff (idempotent — a second revoke of an already-revoked/redeemed/expired row still answers `200`). */
revokeHandoff(handoffId: string, options: VaultHandleOptions): Promise<{
handoff_id: string;
state: string;
}>;
/**
* Mint an Invite Key: capture a stash-shaped checkpoint exactly as
* {@link Repos.handoff} does (design D1), push it (retained, on no
* branch), register the INVITER's own presence in the invite's room
* (design D4 — before the mint, so the row can carry
* `inviter_presence_id`; a registration failure is reported and the mint
* proceeds without one), seal the vault's current epoch key under a fresh
* `wrap_key`, mint through the gateway at `developer` (or `role`,
* attenuated to never exceed the minter's own), and post ONE message from
* the inviter's presence naming the checkpoint and the invite id (never
* the key) — a message-post failure is reported in `room_message` and never
* voids the mint (design D4). The assembled `kgi1_…` key is returned
* exactly ONCE — nothing here or downstream persists it. Creating an
* invite never touches the inviter's worktree, index, branch, refs, or
* access.
*
* `options.note` omits `capture` — this method fills it with the real
* capture figures and runs the client-side secret scan BEFORE the invite
* commit is written (design D10/kygit-invite D3: no override flag).
*/
invite(options: VaultHandleOptions & {
address?: VaultRemoteAddress;
role?: string;
ttlSeconds?: number;
/** Named org room to invite into. Omitted: the project's default room (its own id). */
roomKey?: string;
note: Omit<import("../node/vault-handoff.js").KygitInviteNote, "capture">;
includeSensitive?: string[];
onCommitLine?: (line: string) => void;
/** Harness-derived presence labels (design D8) — never guessed by the SDK itself. */
program?: string;
model?: string;
/** Opaque session identity for the inviter's own presence registration (see {@link RegisterPresenceOptions.sessionKey}). */
sessionKey?: string;
/** Display-only "what I'm working on" for the inviter's presence. */
task?: string;
}): Promise<VaultInviteMintResult>;
/** List a vault's invites (ids, kind, state, role, room, expiry, redeemed_by — never the hash or envelope). */
listInvites(options: VaultHandleOptions): Promise<VaultInviteListResult>;
/** Revoke an invite (idempotent — a second revoke of an already-revoked/redeemed/expired row still answers `200`). */
revokeInvite(inviteId: string, options: VaultHandleOptions): Promise<{
invite_id: string;
state: string;
}>;
/**
* Resume a Handoff Key: parse → ensure this machine has a wallet (the
* claim is bare SIWX, so on a fresh machine the wallet file is created
* here — a keypair on disk, no faucet, no tier, no payment, ever; design
* D5) → redeem → open the
* sealed envelope → write the repo file to the keystore BEFORE touching
* disk → clone at the base HEAD → `git stash apply --index` → local
* git-config pins only → the session-start reconcile so a principal
* envelope supersedes the bearer one.
*/
/**
* vault-multi-writer rev 47 (task 5.6, design D4/D5) — order: parse →
* ensure wallet → ensure identity → derive auth/wrap/admission → build
* acceptance → redeem → verify grant → open envelope → check the grant
* hash → persist repo file with `pending_writer_admission` → clone →
* verify chain → submit the ref-neutral activation head → reconcile
* principal envelope → apply the checkpoint. Reported as
* `writer_activation` and `reconcile_recipients` (D5's "recipient
* coverage"), two blocks, because they are two properties.
*
* Crash-resumable by construction, not by a special-cased retry branch:
* the acceptance is derived fresh every call from the (deterministic)
* admission seed + this checkout's own identity, so re-running this
* ENTIRE method after a crash anywhere before activation lands is safe —
* the redeem route is itself idempotent (same-redeemer replay returns the
* SAME grant). The one step that is NOT safely repeatable is submitting
* the activation head a second time (the chain burns `handoff_id`
* single-use) — `submitWriterActivationHead` handles that by checking
* whether this checkout's own key is ALREADY in the freshly-verified
* writer set before ever building a transition, exactly mirroring
* `handoff()`'s own writer precheck (task 5.5), just inverted: there
* "not yet a writer" refuses; here "already a writer" means skip.
*/
resume(options: {
key: string;
to?: string;
keystore_root?: string;
onLine?: (line: string) => void;
}): Promise<VaultHandoffResumeResult>;
/**
* Join an Invite Key: parse (refusing a `kgh1_` handoff key by name,
* pointing at `resume` — design D9) → ensure this machine has a wallet
* (design D5, mirroring {@link Repos.resume}'s own bare-wallet
* backstop; the CALLER folds the fuller cold-start chain — wallet,
* faucet, one x402 prototype payment — before invoking this, never
* blocking the redemption itself) → redeem → open the sealed envelope → write
* the repo file to the keystore BEFORE touching disk → clone at the base
* HEAD → `git stash apply --index` → local git-config pins (including
* `r402.room` set to the invite's OWN room, never just the project id) →
* append `.run402/` to `.git/info/exclude` → the session-start reconcile
* → register THIS session's presence in the room and post ONE arrival
* fact naming it and the checkpoint (idempotent on the invite id;
* best-effort — a presence or fact failure never blocks arrival) → read
* the most recent messages for the arrival view.
*/
join(options: {
key: string;
to?: string;
keystore_root?: string;
onLine?: (line: string) => void;
/** Harness-derived presence labels (design D8) — never guessed by the SDK itself. */
program?: string;
model?: string;
/** Opaque session identity for this session's own presence registration (see {@link RegisterPresenceOptions.sessionKey}). */
sessionKey?: string;
/** Display-only "what I'm working on" for this session's presence; defaults to naming the invite. */
task?: string;
/** How many recent messages to read for the arrival view. Default 10. */
recentMessagesLimit?: number;
}): Promise<VaultInviteJoinResult>;
/**
* The capture-time copies a plain `git push` through the remote helper
* owes exactly as {@link capture} and {@link deploy} do: the opt-in mirror
* mirror write and, on a BYO vault, the signed-chain copy into the customer
* bucket (vault-byo-primary-bucket task 3.3). The helper publishes
* through `Vault.push` directly, so it calls this right after a
* generation lands. Same contract as the private hooks it composes:
* best-effort, NEVER throws, never alters the push already committed —
* each outcome is reported on its own field (`skipped_no_mirror` costs no
* network call).
*/
postPublishCopies(input: {
repo_id: string;
keystore: VaultKeystore;
}): Promise<{
mirror_push: VaultMirrorPushResult;
byo_chain_copy: VaultMirrorPushResult;
}>;
/**
* A REAL preview of what {@link capture} would publish
* — never publishes anything, and never allocates. `run402 repos
* capture --dry-run` and `git-remote-run402`'s `option dry-run true` are
* both thin adapters over this method.
*
* "Refusing beats fake success" (this file's own architectural law) governs
* the shape of what happens when the vault does not exist yet: unlike
* {@link capture}, this NEVER allocates one (push-to-create must not allocate
* on a dry run) — it resolves READ-ONLY (`open`/`resolveOrCreateAddress`
* with `allow_create: false`), and when that resolution misses, returns
* `allocation_needed: true` with every sizing field `null` rather than a
* guess. Sizing is genuinely unknowable before allocation: encryption uses
* a `K_repo` this project has not been assigned yet, so there is no key to
* preview a push under — only the real capture (this method still runs
* `captureSnapshot`, which is local, filter-free work) can be computed.
*
* Everything else is the SAME real local pipeline `push` runs — capture,
* pack building, sealing/encryption — computed by
* {@link import("../node/vault-publication.js").Vault.planPush},
* which is what actually stops short of the two network mutations
* (`uploadObjects`, `admitHead`). See its doc comment for what
* `would_admit_generation` does and does not promise.
*/
planCapture(options: VaultHandleOptions & {
/** Same as {@link capture}'s `address` — a parsed remote address. Resolved READ-ONLY (`allow_create: false`); never push-to-creates. */
address?: VaultRemoteAddress;
snapshot?: Omit<import("../node/vault-snapshot.js").VaultSnapshotOptions, "dir">;
onCommitLine?: (line: string) => void;
checkpoint?: boolean;
}): Promise<VaultSnapshotPushPlan>;
/**
* Compaction's transient-storage preflight
* (vault-compaction-headroom-preflight, design D1/D2).
*
* Three postures, in order of information quality:
* 1. the check answers "won't fit" → typed refusal, before any upload;
* 2. the caller passed the override → proceed, marked `overridden`;
* 3. the check CANNOT be answered → proceed after one stderr note.
*
* (3) is deliberate: failing closed on ignorance would make compaction's
* availability depend on an ADVISORY read, which is wrong for a maintenance
* verb whose refusals must come from the authority that owns the quota.
*
* The size proxy is the vault's billed `source_bytes` (D1). The true
* checkpoint-pack size is unknowable before packing, but it is bounded above
* by — and usually near — the vault's live content. Deliberately
* conservative when history exceeds live content; the override exists
* precisely because a proxy can overestimate.
*/
/**
* Read compaction's transient-storage arithmetic WITHOUT compacting — the
* same figures `compact` preflights on, with none of its policy. `repos gc`
* uses it to disclose headroom on the `--submit` half, where no compaction
* runs but the numbers are just as worth showing. `null` when the pooled
* figures cannot be read.
*/
compactHeadroom(options?: VaultHandleOptions): Promise<VaultCompactHeadroom | null>;
/**
* Open this vault's compaction headroom grant directly (vault-checkpoint-cadence
* design D3) — `compact()` already does this internally; this standalone
* entry point exists for callers that need to inspect or drive the grant
* without also running a full compaction cycle (tests; a future
* operator/diagnostic surface). Throws `VAULT_COMPACTION_GRANT_ACTIVE`
* (409) verbatim when another compaction already holds this project's
* grant.
*/
openCompactionGrant(options?: VaultHandleOptions): Promise<VaultCompactionGrant>;
/** Close this vault's compaction headroom grant directly — idempotent; `{closed: false}` when nothing was active. */
closeCompactionGrant(options?: VaultHandleOptions): Promise<{
closed: boolean;
}>;
/**
* Publish a checkpoint covering the canonical refs, every root unexpired at
* the cutoff, and the `HEAD` target — under a maintenance lease so a
* concurrent cycle cannot race it.
*
* The lease's `holder_token` is held in memory for the duration and released
* in a `finally`; it is never returned to the caller, logged, or cached.
* A repository beyond the V0 checkpoint maximum is refused at preflight with
* `CHECKPOINT_SET_LIMIT_EXCEEDED`.
*/
compact(options?: VaultHandleOptions & {
/** Skip the lease entirely (single-writer situations, tests). */
lease?: boolean;
/**
* The cycle's reservation sizes. Both default to `"0"` — a ZERO
* reservation, which is honest but not free: the control plane reserves
* nothing, so a cycle whose objects exceed the (absent) headroom can be
* refused mid-flight rather than at preflight. Supply real figures once
* the caller can size the checkpoint it is about to write.
*/
r1_size_bytes?: string;
r2_cap_size_bytes?: string;
/**
* Proceed even when the transient-storage preflight says the org's
* pooled tier storage cannot hold both the new checkpoint and the
* not-yet-pruned history. The platform's own quota enforcement remains
* the authority either way — this only skips the earlier, more legible
* refusal. CLI: `--force-headroom`.
*/
ignoreHeadroom?: boolean;
}): Promise<VaultCompactResult>;
/**
* Plan a prune — and, with both verifier receipts in hand, submit it.
*
* TWO PHASES, because the protocol is two-phase (§7.3) and no amount of API
* sugar can collapse it:
*
* 1. `prune()` walks the verified chain, computes the GC root set, subtracts
* it from the pruneable universe, and returns a SIGNED
* `prune_intent_core` plus its `intent_core_sha256`. Nothing is
* submitted and nothing is deleted.
* 2. Run `r402s-verify` against that core, then call
* `prune({ submit: { core, verifier_receipt } })` with the core
* ROUND-TRIPPED VERBATIM. This SDK produces its own `run402-cli` receipt
* by restoring the latest checkpoint and recomputing its commitments;
* the second receipt is `r402s-verify`'s and is never synthesized here,
* because two receipts from one lineage prove nothing.
*
* The `deleted` list in the result comes from the control-plane-signed
* completion and nothing else. `present_after_attempt` is a FAILED deletion
* and is never counted as one.
*/
prune(options?: VaultHandleOptions & {
now?: () => Date;
/**
* `effective_admitted_at` for a root's drop generation, or `null` when
* this client cannot resolve it. A `null` RETAINS the root: expiry is
* permissive, and `effective_admitted_at = max(prepared_at, the admission
* record's storage creation time)` — deriving it from `prepared_at` alone
* would SHORTEN the retention lane, which the protocol forbids.
*/
effective_admitted_at?: (droppedAtGeneration: string) => string | null;
/**
* Submit the plan. `core` MUST be the exact object a prior `prune()`
* returned, and `verifier_receipt` MUST be `r402s-verify`'s receipt over
* that core's `intent_core_sha256`.
*/
submit?: {
core: import("../node/vault-prune.js").VaultPruneIntentCore;
verifier_receipt: import("../node/vault-prune.js").VaultVerifierReceipt;
/** Poll budget for the signed completion. Absent ⇒ submit and report the intent without waiting. */
wait?: {
attempts?: number;
interval_ms?: number;
};
};
}): Promise<VaultPruneResult>;
/**
* Verify the head chain from the authenticated pin up to the newest listed
* generation, then return the verified state.
*
* Fails closed: a regression below the pin is `GENERATION_REGRESSION`, a gap
* or bad link is `CHAIN_BROKEN`, an unvalidatable transition descriptor is
* `UPGRADE_REQUIRED` (read-only past it, never skipped). The verification
* budget is resumable — a `VERIFICATION_BUDGET_EXCEEDED` client continues
* from its verified prefix rather than restarting.
*/
verify(options?: VaultHandleOptions & {
persist?: boolean;
}): Promise<VaultVerifiedState>;
/**
* `repos fsck` (repo-surface-consolidation D2/D3): walk the head chain
* AND materialize the ref map, reporting
* BOTH local trust pins — authenticated and materialized — before and
* after, with an explicit `local_state_changed` flag. This is the one
* place chain materialization and pin advance live; `view` never
* calls it.
*
* `options.write` (default `true`) is the inverse of the CLI's
* `--no-write`: `false` still walks, verifies, and decrypt-validates
* everything (the returned `refs`/`chain_verified_to_generation`/
* `decryptable_to_generation` are real, computed answers, not estimates)
* but persists neither pin — a genuine audit mode, not a simulation.
* `options.mirror` additionally runs the keyless mirror verification
* ({@link mirrorVerify}) and folds its report in; that half proves the
* mirror's validity, never its freshness (its own honesty statements ride
* the result unchanged).
*
* **Request 1 (decrypt-validation, D193-D203 rev 42).** `fsck` walks the
* chain in TOLERANT decrypt-validation mode
* (`verifyToNewest({decryptValidate: true, strict: false})`) rather than
* the ordinary fail-closed `materialize()` — chain/signature verification
* ALWAYS reaches the true newest generation regardless of decrypt
* capability (an admitted `rotate_epoch` is keylessly structural), and a
* generation this call cannot actually DECRYPT is never again reported as
* bare "verified": `chain_verified_to_generation` and
* `decryptable_to_generation` are reported separately and can genuinely
* differ — the incident-shape truth ("chain verified to 10, decryptable
* to 7, `VAULT_EPOCH_NOT_OPENABLE` at epoch 2/rotation …") is exactly
* what this split makes representable. `refs`/`head_target`/
* `pin_after.highest_materialized` all reflect `decryptable_to_generation`.
*
* **vault-byo-primary-bucket task 3.3 (design D6).** For a
* `storage_profile: "byo"` vault, `fsck` ALSO adjudicates the customer's
* own bucket against run402's signed chain — see {@link
* #checkByoPresence} for the full contract (automatic, never a flag;
* fails soft with no local credentials; throws
* `VAULT_BYO_OBJECT_MISSING` on a confirmed absence). A managed vault
* is byte-identical to before this task.
*/
fsck(options?: VaultHandleOptions & {
write?: boolean;
mirror?: boolean;
}): Promise<VaultFsckResult>;
/**
* Wrap this vault's current epoch key to every org member who has
* published an encryption key but has no `key_envelope` on this vault yet
* — vault-human-envelopes task 4.1's ADD-path workaround. See {@link
* import("../node/vault-publication.js").Vault.
* reconcileEnvelopeRecipients} for the full design (TOFU pinning, the
* gateway `public_key` gap, and why this is a workaround rather than the
* eventual epoch-rotation design).
*
* No CLI verb runs this directly — `repos access` is the inspection
* read. `deploy()` runs it, best-effort,
* whenever a deploy lands a new generation in the vault — design D5's
* "deploy time" hook, "the same 'one command every agent runs' argument
* that decided deploy-implies-capture." `capture()` (capture-and-publish
* outside a deploy) runs the identical hook after every successful
* publish, for the vault-only-project cadence. See
* `#tryReconcileEnvelopeRecipients` below for both call sites, and
* {@link Repos.access} for the READ half repo-surface-consolidation
* ships in its place.
*/
reconcileEnvelopeRecipients(options?: VaultHandleOptions): Promise<VaultReconcileEnvelopeRecipientsResult>;
/**
* vault-multi-writer (task 5.7) — admit every eligible `pending_writers[]`
* candidate (active org membership at role developer+, a published,
* possession-verified signing key, not yet in the writer set) via a fresh
* `add_writer_key{"writer"}` head per candidate. This session's OWN key
* must already be an active writer — see {@link
* import("../node/vault-publication.js").Vault.
* reconcileWriterAdmissions}'s doc comment for why that is checked
* locally, once, rather than surfaced as N identical gateway refusals.
*
* The explicit standalone entry point for the SAME reconcile task 5.7
* also wires onto session-start/push/snapshot/deploy — calling it here
* is idempotent-safe alongside whatever `open()` above already ran
* internally (an already-admitted candidate is simply reported under
* `already_covered`, never re-admitted), mirroring {@link
* reconcileEnvelopeRecipients}'s own identical redundancy-tolerant shape.
*/
reconcile(options?: VaultHandleOptions): Promise<VaultReconcileWriterAdmissionsResult>;
/**
* Drive one epoch rotation (D193-D203, rev 42) — the client half of
* epoch rotation: sample a fresh `K_e`, compute the H-partition from live
* desired-recipient state + the effective pin manifest, seal one
* `key_envelope` per included recipient, submit the create-only
* `rotation_attempt_descriptor`, submit the `rotate_epoch` head, verify
* this principal's own envelope opens to the committed key (when it is
* itself a recipient), and advance the local keystore's epoch pointer.
* See {@link import("../node/vault-publication.js").Vault.
* rotateEpoch}'s own doc comment for the full obligations and — load-
* bearing — the confirmed gap in what the gateway exposes for
* `recipient_state_version`/`recipient_revocation_version` outside the
* `recipient_key_revoked` reason, and (also load-bearing) `options.
* pending_confirmations` for folding a receipted `/confirm`/`/repin`
* result into THIS rotation's head instead of a separately-gated
* `publishPinManifestUpdate` call.
*/
rotateEpoch(options: VaultHandleOptions & {
reason: VaultRotationReason;
recipient_state_version: string;
recipient_revocation_version: string;
client_idempotency_key?: string;
pending_confirmations?: {
principal_id: string;
ek_fingerprint: string;
receipt: VaultRecipientConfirmationReceipt;
}[];
}): Promise<import("../node/vault-publication.js").VaultRotationResult>;
/**
* The ONE fully self-contained rotation entry point: declares
* `reason:"recipient_key_revoked"` for `principalId` (owner + step-up)
* and drives the rotation from that call's OWN returned counters — no
* external counter source needed. The rekey remedy for the exact witness
* task 5.0 records: a specific member's key is compromised/should no
* longer be trusted.
*/
/**
* The writer-capable rotation that completes an org membership removal
* (`reason:"member_removed"`, vault-multi-writer D6): counters come off
* the envelope-recipients read, no declaration, no owner step-up — any
* surviving writer can run it. `capture()` runs it automatically when the
* gate names an outstanding removal; this is the explicit entry point
* (`run402 orgs members rm` drives it on every vault the caller can).
*/
rotateEpochForMemberRemoval(options?: VaultHandleOptions & {
client_idempotency_key?: string;
}): Promise<import("../node/vault-publication.js").VaultRotationResult>;
rotateEpochForKeyRevocation(principalId: string, options?: VaultHandleOptions & {
client_idempotency_key?: string;
}): Promise<import("../node/vault-publication.js").VaultRotationResult>;
/**
* Publish this machine's keystore identity (both halves) for the active
* credential's principal, minting the identity when there is none — the
* enrollment every vault operation runs on the way in, as its own step.
* A transfer recipient runs it before naming itself as the vault recipient.
*/
publishKeystoreIdentity(options?: {
keystore_root?: string;
}): Promise<VaultEnrollmentOutcome>;
/**
* The sender's step of a project transfer's vault handover
* (project-transfer-vault-handover): read the transfer's nominated
* recipient, admit its own published signing key as a writer (a ref-neutral
* `add_writer_key` head), and wrap the current epoch's key for it. The
* caller's key must be a writer of the vault. Idempotent: a step already
* done is reported as such. The recipient can accept once
* `vault_handover.state` is `complete`.
*/
completeTransferHandover(transferId: string, options?: Omit<VaultHandleOptions, "repo_id" | "project_id">): Promise<TransferHandoverResult>;
/**
* After a project transfer, retire the previous owner's writers: the
* gateway blocks every writer key whose principal is not a developer+
* member of the vault's organization (owner + step-up), then this commits
* the `writer_key_revoked` rotation that removes them. Protects only what
* is written afterwards. Idempotent; nothing to retire means no rotation.
*/
retirePreviousOwnerWriters(options?: VaultHandleOptions & {
client_idempotency_key?: string;
}): Promise<RetirePreviousOwnerWritersResult>;
/**
* Publish a receipted `recipient_pin_manifest` update (D197) — the
* publication half of the `/confirm`/`/repin` ceremonies above. Call
* after {@link confirmRecipient}/{@link repinRecipient} returns a receipt;
* this is `gitvault.writer`-sufficient (the owner-gated half already
* happened at the ceremony route).
*
* **This publish is an ORDINARY admission** and is therefore itself
* refused `EPOCH_ROTATION_REQUIRED` while this vault has a migration/
* revocation/exposure condition outstanding (D193).
* `#enrichEpochRotationRequiredForPinManifest`
* below decorates that refusal with the remedy: fold the SAME receipt
* into `rotateEpoch({..., pending_confirmations: [...]})` instead, which
* durably publishes it on a `rotate_epoch` admission (the gate's own
* escape valve) rather than a separately-gated ordinary one.
*/
publishPinManifestUpdate(input: {
principal_id: string;
ek_fingerprint: string;
receipt: VaultRecipientConfirmationReceipt;
} & VaultHandleOptions): Promise<import("../node/vault-publication.js").VaultPublishResult>;
/**
* `repos access` (repo-surface-consolidation D5/D10) — a READ-ONLY report
* of who can open this vault, composed from whatever the live gateway
* surface exposes today. Never wraps, never mutates a `key_envelope` —
* that mutating half stays {@link reconcileEnvelopeRecipients}, reachable
* only through the deploy/push best-effort hooks until `access repair`
* ships (gated on `vault-human-envelopes`' epoch-rotation work). See
* {@link VaultAccessResult}'s doc comment for the honest gap this
* reports rather than invents.
*/
access(options?: VaultHandleOptions): Promise<VaultAccessResult>;
/**
* The vault-gated deploy: both lanes under one fresh `capture_id`, resolving
* to exactly one of five outcomes — `DEPLOYED_AND_VAULTED`,
* `DEPLOY_BLOCKED_PUSH_FAILED`, `DEPLOY_FAILED_VAULTED`,
* `DEPLOY_FAILED_UNVAULTED`, `DEPLOYED_UNVAULTED_OVERRIDE`.
*
* The push is never gated on deploy success, and a build that fails before a
* canonical apply plan exists still captures (with a null plan digest, so no
* activation token can be minted from it).
*
* `run402 deploy` is design D5's PRIMARY envelope-recipient reconcile hook
* ("the same 'one command every agent runs' argument that decided
* deploy-implies-capture") and design D6's primary mirror-write trigger
* — both fire HERE, not inside {@link runVaultDeploy} itself, mirroring
* {@link capture}'s exact non-blocking contract: best-effort, NEVER throw,
* NEVER alter the deploy outcome already resolved above, reported BESIDE it
* on `mirror_push` / `reconcile_recipients`. They fire only when this
* deploy actually landed a new generation in the vault
* (`DEPLOYED_AND_VAULTED` / `DEPLOY_FAILED_VAULTED` — the outcomes carrying
* a `generation`); the other three outcomes published nothing new, so
* there is nothing to mirror or reconcile against ({@link
* import("../node/vault-mirror.js").mirrorPushForGeneration}'s own
* contract: "fires after an ordinary vault push/deploy produces a new
* generation"). Both fields are therefore OMITTED — never a faked
* `skipped_*` outcome — when no generation landed.
*/
deploy(options: Omit<VaultDeployOptions, "vault"> & VaultHandleOptions): Promise<VaultDeployResult & {
mirror_push?: VaultMirrorPushResult;
byo_chain_copy?: VaultMirrorPushResult;
reconcile_recipients?: VaultReconcileEnvelopeRecipientsPushResult;
writer_reconcile?: VaultReconcileWriterAdmissionsPushResult;
}>;
/**
* Drain every unvaulted-override journal on this machine: push the exact
* journaled snapshot and present the capture receipt for full-field
* comparison. A partial match never clears the advisory.
*/
drainOverrides(options?: VaultHandleOptions): Promise<import("../node/vault-deploy.js").VaultOverrideDrainReport>;
/**
* Reproduce the vault's object database into a git repository — the clone-back
* path. Needs only git and a surviving keystore: no deployment artifact, CAS
* entry, or apply operation is consulted.
*/
restore(options: VaultHandleOptions & {
target_dir: string;
}): Promise<{
refs: Record<string, string>;
generation: string;
retained_refs: import("../node/vault-publication.js").VaultRetainedRefsReconcileResult;
}>;
/**
* Wrap an ALREADY-RESOLVED vault read with degraded-read fallback: run
* `options.attemptLive` (the caller's own live call — its
* `vault.materialize()` for a `list`-shaped read, or
* `vault.restoreObjectsInto(dir)` for a `fetch`-shaped one), and on a
* network-class failure (never on an authorization refusal or any other
* 4xx — see `isNetworkClassVaultReadError`), fall back to the vault's
* configured mirror via the SAME `r402s-recover` engine `recover()` uses.
*
* Deliberately takes an already-open `vault`/`keystore`/`repo_id` rather
* than resolving them itself: named-address resolution (slug-form vs.
* id-form), stale-pin recovery, and cross-command vault-instance reuse
* within one `git-remote-run402` session are CLI-layer concerns that
* already live in `cli/lib/remote-helper-session.mjs` — this method owns
* only the trigger discipline and the fallback engine, never vault
* resolution, so it composes with that existing flow instead of
* duplicating it.
*
* `out_dir` is REQUIRED for the fallback to ever run — pass the resolved,
* git-proven repository directory (`resolveGitInvocationRepo`'s own
* `repo_dir`, never a guess from `cwd`). `null` (no resolvable repository,
* e.g. a bare `git ls-remote` outside any checkout) disables the fallback
* entirely: the original gateway error surfaces exactly as it always did.
*/
withDegradedRead<T extends VaultDegradedReadLive>(options: {
attemptLive: () => Promise<T>;
keystore: VaultKeystore;
repo_id: string;
out_dir: string | null;
}): Promise<VaultDegradedReadOutcome<T>>;
/**
* vault-byo-primary-bucket (design D4): the degraded read for the OPEN
* itself. {@link withDegradedRead} wraps a handle's live read, but a plain
* `git fetch`/`clone` needs the gateway once BEFORE any read — to open the
* handle (enrollment, the vault record). When that open fails
* network-class and this checkout already knows the vault — a pinned
* `repo_id`, or the keystore's own project→repo file for an id-form
* address (vault-offline-clone-resolve) — the whole read is served from
* the vault's BYO destination or configured mirror, exactly as
* `withDegradedRead` would have after a failed live read. `null` when this
* cannot help (a non-network failure, no local identity for the vault, no
* local copy source), so the caller rethrows its ORIGINAL error unchanged.
* Writes are never rerouted.
*/
degradedOpenFallback(input: {
repo_dir: string;
error: unknown;
address?: import("../node/vault-address.js").ResolveVaultAddressOptions["address"];
repo_id?: string;
}): Promise<VaultDegradedReadOutcome<VaultDegradedReadLive> | null>;
/**
* Configure (or replace) the customer-owned mirror destination for one
* vault. Client-side ONLY — the destination + credential NAME are written
* beside the keystore (design D2); run402 never sees or stores a raw
* secret value. `s3://<bucket>/<prefix>` needs `credential`; a plain
* filesystem path needs none.
*/
mirrorSet(options: VaultHandleOptions & {
destination_url: string;
credential?: VaultMirrorCredential;
region?: string;
endpoint?: string;
}): Promise<VaultMirrorConfig>;
/** Remove the mirror config for one vault. Never touches the mirror's own bytes (design: config removal ≠ data deletion). */
mirrorRemove(options?: VaultHandleOptions): Promise<{
repo_id: string;
removed: boolean;
}>;
/**
* What this machine and the mirror each believe: whether a mirror is
* configured, the newest generation the MIRROR holds and can chain-verify
* (keyless — never touches keys), the LIVE vault's newest generation (one
* read of the vault record), and — when they disagree — the closing
* command. Both honesty statements (design D8) ride every response.
*/
mirrorStatus(options?: VaultHandleOptions & {
is_byo?: boolean;
}): Promise<VaultMirrorStatus>;
/** List the vault's stored objects, diff against the mirror, fetch + hash-verify + write what's missing, in admission order. Resumable and idempotent. */
mirrorSync(options?: VaultHandleOptions): Promise<VaultMirrorSyncSummary>;
/** Keyless integrity probe against the CONFIGURED mirror: discovery + chain verification + closure/absence adjudication, never decryption (`run402 repos fsck --mirror`). */
mirrorVerify(options?: VaultHandleOptions): Promise<VaultVerifyReport>;
/**
* `r402s-recover` (design D4): rebuild a working git repository straight
* from a mirrored `source/<repo_id>/` prefix — NO SERVER INVOLVED. `source`
* is `s3://<bucket>[/<prefix>]` or a local directory; when it names more
* than one mirrored vault, pass `repo_id` explicitly. Recovery proves
* validity, never freshness (both honesty statements ride every result).
*
* vault-recovery-custody: a human member with no keystore recovers with
* `member_bundle` (the exported `r402s-member-recovery-bundle/v1`, or the
* mirror's own `member-recovery-bundles/` sidecar when omitted) +
* `source_recovery_code`; a raw WebAuthn PRF output is NOT a supported
* input. The recovery-receipt pin stays the trust anchor either way —
* pass `recovery_receipt` when there is no keystore holding one.
*/
recover(options: {
source: string;
out_dir: string;
repo_id?: string;
credential?: VaultMirrorCredential;
region?: string;
endpoint?: string;
recovery_receipt?: VaultRecoveryReceipt;
keystore_root?: string;
member_bundle?: VaultMemberRecoveryBundle;
source_recovery_code?: string;
rp_id?: string;
}): Promise<VaultRecoverResult>;
/**
* Re-apply the local immutable-object cache's eviction window (design D3,
* task 4.2) for one vault — a periodic backstop wired into `run402 repos
* gc`, catching any orphaned cache file a crashed write might have left
* behind. Purely local: no network call beyond resolving `repo_id` when
* only `project_id` was supplied. Idempotent and safe to call on a repo
* with no cache directory at all (a no-op).
*/
sweepObjectCache(options: VaultHandleOptions): Promise<void>;
}
/**
* The composite escalation trigger (design D7): ANY of these crossing its
* threshold escalates the quiet genesis-time note into a standing warning.
* Deliberately an OR — any single metric alone is gameable by triviality
* (an agent that pads one dimension to stay under a lone threshold). Shipped
* as defaults; tunable in this ONE place, not a protocol contract.
*/
export declare const VAULT_LOSS_WARNING_THRESHOLDS: {
readonly generations: 10;
readonly source_bytes: number;
readonly days_since_genesis: 14;
};
/**
* vault-clone-scaling (bench P3): generations-since-checkpoint at or
* above this advises `run402 repos gc`. Same one-place-to-tune convention
* as {@link VAULT_LOSS_WARNING_THRESHOLDS}; an advisory, never a gate.
*/
export declare const VAULT_CHECKPOINT_ADVISORY_GENERATIONS = 25;
/** {@link vaultCheckpointStaleness}'s result. */
export interface VaultCheckpointStaleness {
generations_since_checkpoint: number;
advised: boolean;
}
/**
* Pure staleness computation over two 16-hex generations. TOTAL: garbage
* or unknown coverage (`null`) yields `{0, advised: false}` — advisory
* call sites must never be able to break a verb, so unparseable input is
* silence, not an error. Coverage `null` means "this checkout has not
* locally learned the vault's checkpoint coverage" (see the keystore's
* `checkpoint_covers_through` doc), which is deliberately indistinguishable
* from fresh here.
*/
export declare function vaultCheckpointStaleness(input: {
newest_generation: string;
covers_through_generation: string | null;
}): VaultCheckpointStaleness;
/** Which composite metric(s) crossed their D7 threshold, if any. */
export interface VaultLossWarningTrip {
generations: boolean;
source_bytes: boolean;
days_since_genesis: boolean;
}
/**
* D7: has this vault crossed the composite terminal-loss escalation trigger?
* Pure — no I/O, no clock dependency beyond the optional `now` override
* (tests). `admitted_generations` and `storage.source_bytes` are decimal
* strings (protocol convention for values that could exceed safe-integer
* precision in principle; comfortably within it here).
*
* There is deliberately no companion "is this resolved" function on the
* TRIP computation itself: whether the generations/bytes/days thresholds
* crossed is independent of how many principals cover the vault, and this
* function stays a pure threshold check forever — a caller does not un-trip
* it, ever, in V0. What DOES now exist, one call site up in `status()`, is a
* way to downgrade the COPY once tripped: a locally-provable read of the
* vault's covering-recipient count (the same envelope-recipients read
* `Repos.access` uses) can show this is no longer the single-principal
* case the message below describes. See
* {@link vaultKeystoreBackupReminderMessage} for that downgraded form.
*/
export declare function vaultLossWarningTrip(record: Pick<VaultRecord, "admitted_generations" | "storage" | "genesis_admitted_at">, now?: Date): VaultLossWarningTrip;
/** Any composite metric tripped — the standing warning threshold itself. */
export declare function vaultLossWarningTripped(trip: VaultLossWarningTrip): boolean;
/**
* The standing warning text (design D7): names what tripped, states the
* resolution honestly (a second principal or human envelope — nothing this
* client can verify yet), and never claims an attestation would clear it.
*
* This is the SINGLE-PRINCIPAL form — use it only when the vault's
* covering-recipient count is unknown or <= 1. Once a caller has locally
* proven >= 2 covering recipients, its premise ("only one principal can open
* it") is false; use {@link vaultKeystoreBackupReminderMessage} instead.
*/
export declare function vaultLossWarningMessage(trip: VaultLossWarningTrip): string;
/**
* D7's downgraded form (dogfood item 2): once `status()` has locally proven
* this vault carries >= 2 covering recipients (via the same envelope-recipients
* read `Repos.access` uses), the single-principal premise behind
* {@link vaultLossWarningMessage} is false for THIS vault — printing it
* anyway would be a false terminal-loss claim. The composite threshold still
* fires the SAME way (this function does not touch `vaultLossWarningTrip`/
* `vaultLossWarningTripped` at all — a real value-at-risk signal is still
* worth a reminder), but the copy switches to the protocol's own
* keystore-qualified durability sentence instead of asserting single-principal
* risk, and never claims terminal loss.
*/
export declare function vaultKeystoreBackupReminderMessage(trip: VaultLossWarningTrip, coveringRecipients: number): string;
/**
* vault-mirror-default — the pure `vault_unmirrored` computation, in ONE
* place so doctor and `repos view` echo the same finding instead of each
* deriving their own (the loss-warning pattern above). Present when no mirror
* is configured, OR when one is configured but has no success evidence yet —
* either the local `last_success_at` stamp (survives a transiently unreachable
* mirror) or a chain-verified `mirrored_generation` read from the mirror
* itself (covers mirrors synced before the stamp existed). Informational,
* never blocking; every input is client-local or read from the CUSTOMER'S
* mirror — nothing here touches the gateway.
*/
export declare function vaultUnmirroredFinding(state: {
configured: boolean;
last_success_at: string | null;
mirrored_generation: string | null;
/** vault-byo-primary-bucket task 3.5 — when true, `message` uses the BYO remedy wording (a SECOND customer-held location) instead of the plain unmirrored statement. */
is_byo?: boolean;
}): VaultUnmirroredFinding | null;
/**
* vault-byo-primary-bucket (design D4) — the ONE stderr line a degraded
* chain/payload read prints: the fallback's own destination (never a
* credential) plus the canonical, mechanism-only statement. `list`/`fetch`
* degrading in the SAME `git-remote-run402` session each print their own
* line (one per degraded READ, not one per session) — see
* `Repos.withDegradedRead`'s own doc comment.
*/
export declare function vaultDegradedReadNote(source: VaultDegradedReadSource): string;
/** `run402::<org_id>/<project_id>` — what `git-remote-run402` resolves. */
export declare function vaultRemoteUrl(orgId: string, projectId: string): string;
/**
* `run402::<org-slug>/<repo-name>` — the address-form remote builder
* (repo-first-onramp task 4, design D6). Same string shape as
* {@link vaultRemoteUrl} (the wire slot admits both forms undiscriminated
* — see {@link vaultRemoteAddressForm}); kept as its own named function so
* a call site states which form it means rather than reusing the id-form
* builder for a semantically different pair of arguments.
*/
export declare function vaultRemoteUrlForRepo(orgSlug: string, repoName: string): string;
/** What {@link parseVaultRemoteUrl} returns — the two undiscriminated address halves. */
export interface VaultRemoteAddress {
org_id: string;
project_id: string;
}
/**
* Parse a `run402::<org>/<project>` remote URL into its two address halves.
* `run402::` is the one remote scheme: `null` for anything that is not a
* `<scheme>::` address at all, and a typed `REMOTE_SCHEME_UNSUPPORTED`
* refusal naming the scheme for any other `<scheme>::` spelling.
*/
export declare function parseVaultRemoteUrl(url: string): VaultRemoteAddress | null;
/** Which address form a parsed `run402::` remote is (repo-first-onramp task 4, design D6). */
export type VaultRemoteAddressForm = "id" | "slug";
/**
* Discriminate a parsed address's form (design D6, "resolved"): "org UUIDs
* and `prj_` prefixes make slug-vs-id in the same slot unambiguous." Both
* halves must look id-shaped for the address to be treated as id-form — a
* genuine `run402::<org_uuid>/<prj_id>` address always satisfies both at
* once, since real org ids are UUIDs and real project ids are always
* `prj_`-prefixed; anything else (an org slug half, or a bare repo-name half)
* is slug-form. Resolution accepts either (task 4.3).
*/
export declare function vaultRemoteAddressForm(address: VaultRemoteAddress): VaultRemoteAddressForm;
/** What a `SLUG_RELEASED` refusal names — the successor slug and the cooldown window (design D6). */
export interface VaultSlugReleasedInfo {
successor_slug: string | null;
released_at: string | null;
cooldown_until: string | null;
}
/**
* `SLUG_RELEASED` (a renamed/deleted org slug, still inside its ~90-day
* cooldown) surfaces as a typed, actionable fact — NEVER auto-followed
* (design D6: "no redirects, deliberately"). `null` when `err` is not a
* `SLUG_RELEASED` refusal.
*/
export declare function vaultSlugReleasedInfo(err: unknown): VaultSlugReleasedInfo | null;
//# sourceMappingURL=repos.d.ts.map