glm-5.2 5462a9e560 Add AGENTS.md: commit/push policy + project conventions
opencode auto-loads AGENTS.md as instructions, overriding the built-in
default of 'only commit when explicitly asked.' This repo's stance is
the opposite — commit and push when reasonable — which already matches
the custom agents in .opencode/agents/ (coordinator.md §5 'Push Main
After Every Merge', implementation-specialist.md §5 'Push immediately').

The file also surfaces the project conventions (no comments, OsRng for
nonces, zeroize-on-drop, thiserror/anyhow, no async, frozen wire format)
so they apply to all sessions, not just spawned implementation agents.

No code change. Restart opencode to load the new instructions.
2026-08-10 11:41:35 +00:00
2026-08-02 06:11:14 +00:00
2026-08-02 09:25:43 +00:00
2026-08-10 11:26:02 +00:00
2026-08-02 06:39:49 +00:00
2026-08-10 11:26:02 +00:00
2026-08-10 11:26:02 +00:00
2026-08-10 11:26:02 +00:00

alkvault

Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, and AES-256-GCM encryption for securing provider keys, credentials, and identity material.

alkvault is a standalone crate with zero network dependencies. It holds the master seed — the root of trust for all derived keys and encrypted credentials — and provides the cryptographic primitives and a runtime API for managing it. Nothing important goes in env vars.

What it is

A local key vault built on the principle that secrets are derived, not stored. From a single BIP39 mnemonic, the vault deterministically derives Ed25519 identity keys, SSH host keys, and AES-256-GCM encryption keys on demand. External credentials (API keys, OAuth tokens) that can't be derived are encrypted with a seed-derived key and stored as EncryptedData blobs.

The vault is local-only by construction — direct method calls on VaultServiceHandle, no actor, no message enum, no wire format, no remote dispatch. The master seed and derived private keys never cross a process boundary.

Usage

use alkvault::derivation::PATHS;
use alkvault::{VaultServiceHandle, KeyType};

// Generate a new mnemonic and unlock the vault.
let vault = VaultServiceHandle::new();
let phrase = vault.unlock_new(24)?;  // 24-word BIP39 mnemonic
// Store `phrase` securely — it is the root of trust.

// Derive an Ed25519 identity keypair at m/74'/0'/0'/0'.
let identity = vault.derive_ed25519(PATHS::IDENTITY)?;
assert_eq!(identity.key_type, KeyType::Ed25519);
assert_eq!(identity.private_key.len(), 32);

// Encrypt an external credential with a seed-derived AES-256-GCM key.
let plaintext = "sk-proj-abc123xyz789";
let encrypted = vault.encrypt(plaintext, 2)?;
let decrypted = vault.decrypt(&encrypted)?;
assert_eq!(decrypted, plaintext);

// Lock the vault — purges the seed and all cached derived keys.
vault.lock();
# Ok::<(), Box<dyn std::error::Error>>(())

Features

Feature Default Description
secp256k1 off BIP-0032 secp256k1 HD key derivation for Ethereum signing keys (m/44'/60'/0'/0/0)

Without secp256k1, derive_ethereum_key returns UnsupportedKeyType. The secp256k1 crate is a heavy dependency (C library for curve operations); feature-gating it keeps the default build lightweight.

Derivation paths

alkvault reserves the 74' coin type (unallocated per SLIP-0044).

Path Purpose Key type
m/74'/0'/0'/0' Primary identity keypair Ed25519
m/74'/0'/0'/{n}' Worker/device identity Ed25519
m/74'/0'/1'/0' SSH host key Ed25519
m/74'/2'/0'/0' Encryption key (v2) AES-256-GCM
m/74'/2'/0'/{n}' Encryption key (v{n+2}) AES-256-GCM
m/44'/60'/0'/0/0 Ethereum signing key secp256k1 (feature-gated)

Key rotation re-encrypts EncryptedData from one version to another via version-indexed derivation paths — same seed, different keys, no new mnemonic needed.

Security model

  • Seed never persisted. The BIP39 mnemonic is entered at startup or via unlock and held only in Zeroize-protected RAM. lock() purges the seed and all cached derived keys.
  • Zeroize everything sensitive. Mnemonic, Seed, ExtendedPrivKey, EncryptionKey, DerivedKey, and CachedKey all implement Zeroize and ZeroizeOnDrop. Secret material does not linger in freed heap memory.
  • OsRng for nonces. AES-GCM IVs use the operating system's CSPRNG (SysRng), never a thread-local RNG. IV reuse under the same key is catastrophic for GCM.
  • DerivedKey redaction. DerivedKey serializes private_key as "[REDACTED]" in all formats (defense-in-depth for logging accidents) and rejects redacted payloads on deserialization. Debug impls also redact.
  • Move-only, not Clone. DerivedKey and EncryptionKey are move-only — no accidental duplication of secret material.
  • No unwrap() outside tests. Vault operations propagate errors. A poisoned lock is recovered with unwrap_or_else(|e| e.into_inner()), not panicked.

Crate independence

alkvault does not depend on any application or networking crate. It defines its own types (VaultServiceError, DerivedKey, EncryptedData, etc.) and is usable in contexts where networking doesn't exist — CLI tools, test harnesses, key-derivation utilities, and future WASM targets.

Documentation

Architecture documentation lives under docs/architecture/:

License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Description
No description provided
Readme 275 KiB
Languages
Rust 100%