refactor(ax): tighten AX-facing docs
Some checks failed
CI / auto-fix (push) Failing after 0s
CI / test (push) Failing after 3s
CI / auto-merge (push) Failing after 0s

This commit is contained in:
Virgil 2026-03-30 21:29:35 +00:00
parent 16d968b551
commit a8eaaa1581
14 changed files with 66 additions and 170 deletions

View file

@ -1,14 +1,9 @@
// Package datanode provides an in-memory io.Medium backed by Borg's DataNode.
// Package datanode keeps io.Medium data in Borg's DataNode.
//
// medium := datanode.New()
// _ = medium.Write("jobs/run.log", "started")
// snapshot, _ := medium.Snapshot()
// restored, _ := datanode.FromTar(snapshot)
//
// DataNode is an in-memory fs.FS that serialises to tar. Wrapping it as a
// Medium lets any code that works with io.Medium transparently operate on an
// in-memory filesystem that can be snapshotted, shipped as a crash report, or
// wrapped in a TIM container for runc execution.
package datanode
import (
@ -39,16 +34,12 @@ var (
// Example: medium := datanode.New()
// _ = medium.Write("jobs/run.log", "started")
// snapshot, _ := medium.Snapshot()
//
// All paths are relative (no leading slash). Thread-safe via RWMutex.
type Medium struct {
dataNode *borgdatanode.DataNode
directorySet map[string]bool // explicit directories that exist without file contents
mu sync.RWMutex
}
// Example: medium := datanode.New()
// _ = medium.Write("jobs/run.log", "started")
func New() *Medium {
return &Medium{
dataNode: borgdatanode.New(),
@ -56,11 +47,9 @@ func New() *Medium {
}
}
// FromTar restores a Medium from tar bytes.
//
// sourceMedium := datanode.New()
// snapshot, _ := sourceMedium.Snapshot()
// restored, _ := datanode.FromTar(snapshot)
// Example: sourceMedium := datanode.New()
// snapshot, _ := sourceMedium.Snapshot()
// restored, _ := datanode.FromTar(snapshot)
func FromTar(data []byte) (*Medium, error) {
dataNode, err := borgdatanode.FromTar(data)
if err != nil {
@ -73,7 +62,6 @@ func FromTar(data []byte) (*Medium, error) {
}
// Example: snapshot, _ := medium.Snapshot()
// Use this for crash reports, workspace packaging, or TIM creation.
func (m *Medium) Snapshot() ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
@ -98,7 +86,6 @@ func (m *Medium) Restore(data []byte) error {
}
// Example: dataNode := medium.DataNode()
// Use this to wrap the filesystem in a TIM container.
func (m *Medium) DataNode() *borgdatanode.DataNode {
m.mu.RLock()
defer m.mu.RUnlock()

6
doc.go
View file

@ -1,11 +1,7 @@
// Package io defines the storage boundary used across CoreGO.
// Package io gives CoreGO a single storage surface.
//
// medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// backup, _ := io.NewSandboxed("/srv/backup")
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
//
// Callers work against Medium so the same code can read and write state from
// sandboxed local paths, in-memory nodes, SQLite, S3, or other backends
// without changing application logic.
package io

40
io.go
View file

@ -10,12 +10,10 @@ import (
"dappco.re/go/core/io/local"
)
// Medium is the storage boundary used across CoreGO.
//
// medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// backup, _ := io.NewSandboxed("/srv/backup")
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
// Example: medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// backup, _ := io.NewSandboxed("/srv/backup")
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
type Medium interface {
Read(path string) (string, error)
@ -61,7 +59,7 @@ type Medium interface {
IsDir(path string) bool
}
// FileInfo provides a simple implementation of fs.FileInfo for mock testing.
// FileInfo is a test helper that satisfies fs.FileInfo.
type FileInfo struct {
name string
size int64
@ -82,7 +80,7 @@ func (fi FileInfo) IsDir() bool { return fi.isDir }
func (fi FileInfo) Sys() any { return nil }
// DirEntry provides a simple implementation of fs.DirEntry for mock testing.
// DirEntry is a test helper that satisfies fs.DirEntry.
type DirEntry struct {
name string
isDir bool
@ -98,9 +96,7 @@ func (de DirEntry) Type() fs.FileMode { return de.mode.Type() }
func (de DirEntry) Info() (fs.FileInfo, error) { return de.info, nil }
// Local is the unsandboxed filesystem medium rooted at "/".
//
// io.Local.Read("/etc/hostname")
// Example: io.Local.Read("/etc/hostname")
var Local Medium
var _ Medium = (*local.Medium)(nil)
@ -113,14 +109,8 @@ func init() {
}
}
// Use NewSandboxed to confine file operations to a root directory.
// All file operations are restricted to paths within the root, and the root
// directory will be created if it does not exist.
//
// Example usage:
//
// medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// Example: medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
func NewSandboxed(root string) (Medium, error) {
return local.New(root)
}
@ -171,10 +161,8 @@ func Copy(source Medium, sourcePath string, destination Medium, destinationPath
// --- MockMedium ---
// MockMedium is an in-memory Medium for tests.
//
// medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
// Example: medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
type MockMedium struct {
Files map[string]string
Dirs map[string]bool
@ -183,10 +171,8 @@ type MockMedium struct {
var _ Medium = (*MockMedium)(nil)
// Use NewMockMedium when tests need an in-memory Medium.
//
// medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
// Example: medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
func NewMockMedium() *MockMedium {
return &MockMedium{
Files: make(map[string]string),

View file

@ -1,4 +1,4 @@
// Package local provides the local filesystem implementation of io.Medium.
// Package local binds io.Medium to the local filesystem.
//
// medium, _ := local.New("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
@ -13,18 +13,16 @@ import (
core "dappco.re/go/core"
)
// Medium is the local filesystem backend returned by New.
// Example: medium, _ := local.New("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
type Medium struct {
filesystemRoot string
}
var unrestrictedFileSystem = (&core.Fs{}).NewUnrestricted()
// local.New("/") exposes the full filesystem.
// local.New("/srv/app") confines access to a project root.
//
// medium, _ := local.New("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// Example: medium, _ := local.New("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
func New(root string) (*Medium, error) {
absoluteRoot := absolutePath(root)
// Resolve symlinks so sandbox checks compare like-for-like.
@ -179,7 +177,7 @@ func logSandboxEscape(root, path, attempted string) {
core.Security("sandbox escape detected", "root", root, "path", path, "attempted", attempted, "user", username)
}
// sandboxedPath sanitises and returns the full filesystem path.
// sandboxedPath resolves a path inside the filesystem root.
// Absolute paths are sandboxed under root (unless root is "/").
func (m *Medium) sandboxedPath(path string) string {
if path == "" {

View file

@ -1,12 +1,9 @@
// Package node provides an in-memory filesystem implementation of io.Medium.
// Package node keeps io.Medium data in memory.
//
// nodeTree := node.New()
// nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
// snapshot, _ := nodeTree.ToTar()
// restored, _ := node.FromTar(snapshot)
//
// It stores files in memory with implicit directory structure and supports
// tar serialisation.
package node
import (
@ -27,18 +24,14 @@ import (
// nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
// snapshot, _ := nodeTree.ToTar()
// restored, _ := node.FromTar(snapshot)
//
// Directories are implicit: they exist whenever a file path contains a "/".
type Node struct {
files map[string]*dataFile
}
// compile-time interface checks
// Compile-time interface checks.
var _ coreio.Medium = (*Node)(nil)
var _ fs.ReadFileFS = (*Node)(nil)
// Example: nodeTree := node.New()
// nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
func New() *Node {
return &Node{files: make(map[string]*dataFile)}
}
@ -89,7 +82,7 @@ func (n *Node) ToTar() ([]byte, error) {
return buf.Bytes(), nil
}
// Use FromTar(data) to restore an in-memory tree from tar bytes.
// Example: restored, _ := node.FromTar(snapshot)
func FromTar(data []byte) (*Node, error) {
n := New()
if err := n.LoadTar(data); err != nil {

View file

@ -1,4 +1,4 @@
// Package s3 provides an S3-backed io.Medium.
// Package s3 stores io.Medium data in S3 objects.
//
// client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"})
// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
@ -21,8 +21,8 @@ import (
coreio "dappco.re/go/core/io"
)
// Client is the subset of the AWS S3 client API used by this package.
// Tests can provide any mock that satisfies the same method set.
// Example: client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"})
// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
type Client interface {
GetObject(ctx context.Context, params *awss3.GetObjectInput, optFns ...func(*awss3.Options)) (*awss3.GetObjectOutput, error)
PutObject(ctx context.Context, params *awss3.PutObjectInput, optFns ...func(*awss3.Options)) (*awss3.PutObjectOutput, error)
@ -33,10 +33,8 @@ type Client interface {
CopyObject(ctx context.Context, params *awss3.CopyObjectInput, optFns ...func(*awss3.Options)) (*awss3.CopyObjectOutput, error)
}
// Medium is the S3-backed io.Medium returned by New.
//
// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
// _ = medium.Write("reports/daily.txt", "done")
// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
// _ = medium.Write("reports/daily.txt", "done")
type Medium struct {
client Client
bucket string

View file

@ -1,11 +1,8 @@
// Package sigil provides pre-obfuscation helpers for XChaCha20-Poly1305.
// Package sigil wraps XChaCha20-Poly1305 with deterministic pre-obfuscation.
//
// cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
// ciphertext, _ := cipherSigil.In([]byte("payload"))
// plaintext, _ := cipherSigil.Out(ciphertext)
//
// Use NewChaChaPolySigilWithObfuscator when you want ShuffleMaskObfuscator
// instead of the default XOR pre-obfuscation layer.
package sigil
import (
@ -32,10 +29,7 @@ var (
NoKeyConfiguredError = core.E("sigil.NoKeyConfiguredError", "no encryption key configured", nil)
)
// PreObfuscator is the hook ChaChaPolySigil uses before and after encryption.
//
// XORObfuscator is the default. ShuffleMaskObfuscator is available when you
// want byte shuffling as well as masking.
// PreObfuscator customises the bytes mixed in before and after encryption.
type PreObfuscator interface {
// Obfuscate transforms plaintext before encryption using the provided entropy.
// The entropy is typically the encryption nonce, ensuring the transformation
@ -47,7 +41,7 @@ type PreObfuscator interface {
Deobfuscate(data []byte, entropy []byte) []byte
}
// XORObfuscator is the default pre-obfuscator returned by NewChaChaPolySigil.
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key)
type XORObfuscator struct{}
// Obfuscate XORs the data with a key stream derived from the entropy.
@ -215,11 +209,9 @@ type ChaChaPolySigil struct {
randomReader goio.Reader // for testing injection
}
// NewChaChaPolySigil returns a ChaChaPolySigil backed by a 32-byte key.
//
// cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
// ciphertext, _ := cipherSigil.In([]byte("payload"))
// plaintext, _ := cipherSigil.Out(ciphertext)
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
// ciphertext, _ := cipherSigil.In([]byte("payload"))
// plaintext, _ := cipherSigil.Out(ciphertext)
func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) {
if len(key) != 32 {
return nil, InvalidKeyError
@ -235,14 +227,14 @@ func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) {
}, nil
}
// NewChaChaPolySigilWithObfuscator returns a ChaChaPolySigil with a custom pre-obfuscator.
// Example: cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator(
//
// cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator(
// []byte("0123456789abcdef0123456789abcdef"),
// &sigil.ShuffleMaskObfuscator{},
// )
// ciphertext, _ := cipherSigil.In([]byte("payload"))
// plaintext, _ := cipherSigil.Out(ciphertext)
// []byte("0123456789abcdef0123456789abcdef"),
// &sigil.ShuffleMaskObfuscator{},
//
// )
// ciphertext, _ := cipherSigil.In([]byte("payload"))
// plaintext, _ := cipherSigil.Out(ciphertext)
func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil, error) {
cipherSigil, err := NewChaChaPolySigil(key)
if err != nil {
@ -334,9 +326,7 @@ func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
return plaintext, nil
}
// GetNonceFromCiphertext returns the nonce embedded in ciphertext.
//
// nonce, _ := sigil.GetNonceFromCiphertext(ciphertext)
// Example: nonce, _ := sigil.GetNonceFromCiphertext(ciphertext)
func GetNonceFromCiphertext(ciphertext []byte) ([]byte, error) {
nonceSize := chacha20poly1305.NonceSizeX
if len(ciphertext) < nonceSize {

View file

@ -1,49 +1,23 @@
// Package sigil provides the Sigil transformation framework for composable,
// reversible data transformations.
// Package sigil chains reversible byte transformations.
//
// hexSigil, _ := sigil.NewSigil("hex")
// gzipSigil, _ := sigil.NewSigil("gzip")
// encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
//
// Sigils are the core abstraction - each sigil implements a specific
// transformation (encoding, compression, hashing, encryption) with a uniform
// interface. Sigils can be chained together to create transformation pipelines.
package sigil
import core "dappco.re/go/core"
// Sigil defines the interface for a data transformer.
//
// A Sigil represents a single transformation unit that can be applied to byte data.
// Sigils may be reversible (encoding, compression, encryption) or irreversible (hashing).
//
// For reversible sigils: Out(In(x)) == x for all valid x
// For irreversible sigils: Out returns the input unchanged
// For symmetric sigils: In(x) == Out(x)
//
// Implementations must handle nil input by returning nil without error,
// and empty input by returning an empty slice without error.
// Sigil transforms byte slices.
type Sigil interface {
// In applies the forward transformation to the data.
// For encoding sigils, this encodes the data.
// For compression sigils, this compresses the data.
// For hash sigils, this computes the digest.
// Example: encoded, _ := hexSigil.In([]byte("payload"))
In(data []byte) ([]byte, error)
// Out applies the reverse transformation to the data.
// For reversible sigils, this recovers the original data.
// For irreversible sigils (e.g., hashing), this returns the input unchanged.
// Example: decoded, _ := hexSigil.Out(encoded)
Out(data []byte) ([]byte, error)
}
// Transmute applies a series of sigils to data in sequence.
//
// Each sigil's In method is called in order, with the output of one sigil
// becoming the input of the next. If any sigil returns an error, Transmute
// stops immediately and returns nil with that error.
//
// To reverse a transmutation, call each sigil's Out method in reverse order.
// Example: encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
var err error
for _, s := range sigils {
@ -55,11 +29,7 @@ func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
return data, nil
}
// Untransmute reverses a transmutation by applying Out in reverse order.
//
// Each sigil's Out method is called in reverse order, with the output of one sigil
// becoming the input of the next. If any sigil returns an error, Untransmute
// stops immediately and returns nil with that error.
// Example: decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
func Untransmute(data []byte, sigils []Sigil) ([]byte, error) {
var err error
for i := len(sigils) - 1; i >= 0; i-- {

View file

@ -1,4 +1,4 @@
// Package sqlite persists io.Medium content in a SQLite database.
// Package sqlite stores io.Medium content in SQLite.
//
// medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
// _ = medium.Write("config/app.yaml", "port: 8080")
@ -18,10 +18,8 @@ import (
_ "modernc.org/sqlite" // Pure Go SQLite driver
)
// Medium stores filesystem-shaped content in SQLite.
//
// medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
// _ = medium.Write("config/app.yaml", "port: 8080")
// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
// _ = medium.Write("config/app.yaml", "port: 8080")
type Medium struct {
database *sql.DB
table string
@ -29,7 +27,6 @@ type Medium struct {
var _ coreio.Medium = (*Medium)(nil)
// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:", Table: "files"})
type Options struct {
// Path is the SQLite database path. Use ":memory:" for tests.
Path string

View file

@ -1,10 +1,7 @@
// Package store provides a SQLite-backed group-namespaced key-value store.
// Package store maps grouped keys onto SQLite rows.
//
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// _ = keyValueStore.Set("app", "theme", "midnight")
// medium := keyValueStore.AsMedium()
// _ = medium.Write("app/theme", "midnight")
//
// It also exposes an io.Medium adapter so grouped values can participate in
// the same storage workflows as filesystem-backed mediums.
package store

View file

@ -12,11 +12,8 @@ import (
// Example: medium, _ := store.NewMedium(store.Options{Path: "config.db"})
// _ = medium.Write("app/theme", "midnight")
// entries, _ := medium.List("")
// entries, _ := medium.List("app")
//
// Paths are mapped as group/key - the first segment is the group,
// the rest is the key. List("") returns groups as directories,
// List("group") returns keys as files.
type Medium struct {
store *Store
}

View file

@ -10,17 +10,15 @@ import (
_ "modernc.org/sqlite"
)
// NotFoundError is returned when a key does not exist in the store.
// Example: _, err := keyValueStore.Get("app", "theme")
// err matches store.NotFoundError when the key is missing.
var NotFoundError = errors.New("key not found")
// Store is the grouped key/value database returned by New.
//
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
type Store struct {
database *sql.DB
}
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
type Options struct {
// Path is the SQLite database path. Use ":memory:" for tests.
Path string
@ -134,13 +132,9 @@ func (s *Store) GetAll(group string) (map[string]string, error) {
return result, nil
}
// Render loads all key-value pairs from a group and renders a Go template.
//
// Example usage:
//
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// _ = keyValueStore.Set("user", "name", "alice")
// out, _ := keyValueStore.Render("hello {{ .name }}", "user")
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// _ = keyValueStore.Set("user", "name", "alice")
// out, _ := keyValueStore.Render("hello {{ .name }}", "user")
func (s *Store) Render(templateText, group string) (string, error) {
rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
if err != nil {

View file

@ -1,10 +1,7 @@
// Package workspace provides encrypted user workspaces backed by io.Medium.
// Package workspace creates encrypted workspaces on top of io.Medium.
//
// service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
// workspaceID, _ := service.CreateWorkspace("alice", "pass123")
// _ = service.SwitchWorkspace(workspaceID)
// _ = service.WorkspaceFileSet("notes/todo.txt", "ship it")
//
// Workspaces are rooted under the caller's configured home directory and keep
// file access constrained to the active workspace.
package workspace

View file

@ -11,9 +11,7 @@ import (
"dappco.re/go/core/io"
)
// Workspace is the workspace service interface returned by New.
//
// service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
type Workspace interface {
CreateWorkspace(identifier, password string) (string, error)
SwitchWorkspace(workspaceID string) error
@ -34,7 +32,7 @@ type Options struct {
Crypt CryptProvider
}
// Service is the Workspace implementation returned by New.
// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
type Service struct {
core *core.Core
crypt CryptProvider
@ -77,8 +75,6 @@ func New(options Options) (*Service, error) {
}
// Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123")
// Identifier is hashed (SHA-256) to create the directory name.
// A PGP keypair is generated using the password.
func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()