* feat: cloud Mount() per HIP-0106
Adds pkg/vfs.Mount(*zip.App, cloud.Deps) error so the unified Hanzo
Cloud binary can blank-import and register VFS alongside every
other Hanzo subsystem.
VFS has no public HTTP surface today — it's an S3-backed virtual
block filesystem driven through FUSE/CLI/sidecar. Mount() exposes
liveness/readiness probes plus a minimal block-CRUD HTTP API so
co-resident subsystems can talk to VFS without falling back to the
CLI:
- GET /v1/vfs/health — always 200
- GET /v1/vfs/readyz — 200 only when an instance is
attached via vfs.SetInstance
- PUT /v1/vfs/blocks — payload upload
- GET /v1/vfs/blocks/:id — payload download
- DELETE /v1/vfs/blocks/:id — backend delete
- GET /v1/vfs/stats — cache/backend counters
cmd/vfsd is a HIP-0106 thin shim that wires backend + age keys from
env (VFSD_BACKEND, VFSD_AGE_KEY) and listens on cloud.Config.ListenAddr.
The existing cmd/vfs CLI (put/get/stats/mount) is untouched.
init() registers with cloud.Register("vfs", 20, …). schema/vfs.zap
adds the minimal Health + Block ZAP interface; full multi-block
File surface lands in follow-up PRs.
Tests:
- pkg/vfs: TestMount_HealthReadyz + TestMount_PutGetRoundtrip green
- existing TestRoundTrip + TestStats unchanged
* refactor: collapse pkg/vfs/ → root for clean import path
Subsystem code lives in package vfs at repo root. Importers now use
`github.com/hanzoai/vfs` (no /pkg/vfs/ nesting). Standalone shim moved
to cmd/vfs/ where applicable.
Per Hanzo Go-stdlib pattern: repo IS the package.
* refactor: flatten import path — github.com/hanzoai/cloud (drop /pkg/cloud)
70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
package vfs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/luxfi/age"
|
|
)
|
|
|
|
// Crypto wraps a set of age recipients (write-side) and identities
|
|
// (read-side) for per-block encryption. luxfi/age supports classical
|
|
// X25519 + hybrid PQ ML-KEM-768 recipients in the same recipient list.
|
|
type Crypto struct {
|
|
recipients []age.Recipient
|
|
identities []age.Identity
|
|
}
|
|
|
|
// NewCrypto constructs a Crypto with the given recipients (encrypt
|
|
// targets) and identities (decrypt keys). A node that only writes can
|
|
// pass nil identities; a read-only node can pass nil recipients.
|
|
func NewCrypto(recipients []age.Recipient, identities []age.Identity) (*Crypto, error) {
|
|
if len(recipients) == 0 && len(identities) == 0 {
|
|
return nil, fmt.Errorf("crypto: at least one of recipients or identities required")
|
|
}
|
|
return &Crypto{recipients: recipients, identities: identities}, nil
|
|
}
|
|
|
|
// Encrypt produces an age-encrypted ciphertext for the given plaintext
|
|
// block using the configured recipients.
|
|
func (c *Crypto) Encrypt(plaintext []byte) ([]byte, error) {
|
|
if len(c.recipients) == 0 {
|
|
return nil, fmt.Errorf("crypto: encrypt called with no recipients")
|
|
}
|
|
var buf bytes.Buffer
|
|
w, err := age.Encrypt(&buf, c.recipients...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: age.Encrypt: %w", err)
|
|
}
|
|
if _, err := w.Write(plaintext); err != nil {
|
|
return nil, fmt.Errorf("crypto: write plaintext: %w", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, fmt.Errorf("crypto: finalize: %w", err)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// Decrypt reverses Encrypt using the configured identities.
|
|
func (c *Crypto) Decrypt(ciphertext []byte) ([]byte, error) {
|
|
if len(c.identities) == 0 {
|
|
return nil, fmt.Errorf("crypto: decrypt called with no identities")
|
|
}
|
|
r, err := age.Decrypt(bytes.NewReader(ciphertext), c.identities...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: age.Decrypt: %w", err)
|
|
}
|
|
plaintext, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: read plaintext: %w", err)
|
|
}
|
|
return plaintext, nil
|
|
}
|
|
|
|
// HasIdentities reports whether read decryption is possible.
|
|
func (c *Crypto) HasIdentities() bool { return len(c.identities) > 0 }
|
|
|
|
// HasRecipients reports whether write encryption is possible.
|
|
func (c *Crypto) HasRecipients() bool { return len(c.recipients) > 0 }
|