From 4f4ca6ce65dc4078c571050b1fb2d10601db5145 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 4 Apr 2026 15:48:47 +0100 Subject: [PATCH] docs: add core/go RFC primitives for agent reference Co-Authored-By: Virgil --- docs/RFC-CORE-GO-REQUEST.md | 239 +++++++ docs/RFC-CORE-GO.md | 1337 +++++++++++++++++++++++++++++++++++ 2 files changed, 1576 insertions(+) create mode 100644 docs/RFC-CORE-GO-REQUEST.md create mode 100644 docs/RFC-CORE-GO.md diff --git a/docs/RFC-CORE-GO-REQUEST.md b/docs/RFC-CORE-GO-REQUEST.md new file mode 100644 index 0000000..d540577 --- /dev/null +++ b/docs/RFC-CORE-GO-REQUEST.md @@ -0,0 +1,239 @@ +# RFC Request — go-blockchain needs from Core (FINAL) + +> From: Charon (go-blockchain) +> To: Cladius (core/go + go-* packages) +> Date: 2 Apr 2026 00:55 +> Snider's answers inline. Updated with precise asks. + +## 1. core/api — DONE, pulled (+125 commits) +Using it. No ask needed. + +## 2. core.Subscribe/Publish — Raindrops forming +When ready, go-blockchain will: +- Publish: `blockchain.block.new`, `blockchain.alias.registered`, `blockchain.hardfork.activated` +- Wire format: `core.Event{Type: string, Data: any, Timestamp: int64}` + +No blocking ask — will integrate when available. + +## 3. core.Wallet() — I can do this today via core.Service + +```go +c.RegisterService("blockchain.wallet", walletService) +c.Service("blockchain.wallet", core.Service{ + Name: "blockchain.wallet", + Instance: walletService, + OnStart: func() core.Result { return walletService.Start() }, + OnStop: func() core.Result { return walletService.Stop() }, +}) +``` + +Then register actions: +```go +c.Action("blockchain.wallet.create", walletService.HandleCreate) +c.Action("blockchain.wallet.transfer", walletService.HandleTransfer) +c.Action("blockchain.wallet.balance", walletService.HandleBalance) +``` + +**No ask. Implementing now.** + +## 4. Structured Logging — PRECISE ASK + +**I want package-level logging that works WITHOUT a Core instance.** + +The chain sync runs in goroutines that don't hold `*core.Core`. Currently using `log.Printf`. + +**Exact ask:** Confirm these work at package level: +```go +core.Print(nil, "block synced height=%d hash=%s", height, hash) // info +core.Error(nil, "sync failed: %v", err) // error +``` + +Or do I need `core.NewLog()` → pass the logger into the sync goroutine? + +## 5. core.Escrow() — Improvement to go-blockchain, sane with Chain + Asset + +Escrow is a tx type (HF4+). I build it in go-blockchain's wallet package: +```go +wallet.BuildEscrowTx(provider, customer, amount, terms) +``` + +Then expose via action: `c.Action("blockchain.escrow.create", ...)` + +**No ask from Core. I implement this.** + +## 6. core.Asset() — Same, go-blockchain implements + +HF5 enables deploy/emit/burn. I add to wallet package + actions: +```go +c.Action("blockchain.asset.deploy", ...) +c.Action("blockchain.asset.emit", ...) +c.Action("blockchain.asset.burn", ...) +``` + +**No ask. Implementing after HF5 activates.** + +## 7. core.Chain() — Same pattern + +```go +c.RegisterService("blockchain.chain", chainService) +c.Action("blockchain.chain.height", ...) +c.Action("blockchain.chain.block", ...) +c.Action("blockchain.chain.sync", ...) +``` + +**No ask. Doing this today.** + +## 8. core.DNS() — Do you want a go-dns package? + +The LNS is 672 lines of Go at `~/Code/lthn/lns/`. It could become `go-dns` in the Core ecosystem. + +**Ask: Should I make it `dappco.re/go/core/dns` or keep it as a standalone?** + +If yes to go-dns, the actions would be: +```go +c.Action("dns.resolve", ...) // A record +c.Action("dns.resolve.txt", ...) // TXT record +c.Action("dns.reverse", ...) // PTR +c.Action("dns.register", ...) // via sidechain +``` + +## 9. Portable Storage Encoder — DONE + +Already implemented in `p2p/encode.go` using `go-p2p/node/levin/EncodeStorage`. Committed and pushed. HandshakeResponse.Encode, ResponseChainEntry.Encode, RequestChain.Decode all working. + +**go-storage/go-io improvement ask:** The chain stores blocks in go-store (SQLite). For high-throughput sync, a `go-io` backed raw block file store would be faster. Want me to spec a `BlockStore` interface that can swap between go-store and go-io backends? + +## 10. CGo boilerplate — YES PLEASE + +**Exact ask:** A `go-cgo` package with: + +```go +// Safe C buffer allocation with automatic cleanup +buf := cgo.NewBuffer(32) +defer buf.Free() +buf.CopyFrom(goSlice) +result := buf.Bytes() + +// C function call wrapper with error mapping +err := cgo.Call(C.my_function, buf.Ptr(), cgo.SizeT(len)) +// Returns Go error if C returns non-zero + +// C string conversion +goStr := cgo.GoString(cStr) +cStr := cgo.CString(goStr) +defer cgo.Free(cStr) +``` + +Every CGo package (go-blockchain/crypto, go-mlx, go-rocm) does this dance manually. A shared helper saves ~50 lines per package and prevents use-after-free bugs. + +## Summary + +| # | What | Who Does It | Status | +|---|------|-------------|--------| +| 1 | core/api | Cladius | DONE, pulled | +| 2 | Pub/Sub events | Cladius | Forming → core/stream (go-ws rename) | +| 3 | Wallet service | **Charon** | Implementing today | +| 4 | Package-level logging | **Answered below** | RTFM — it works | +| 5 | Escrow txs | **Charon** | In go-blockchain | +| 6 | Asset operations | **Charon** | After HF5 | +| 7 | Chain service | **Charon** | Implementing today | +| 8 | go-dns | **Cladius** | `dappco.re/go/dns` — DNS record DTOs + ClouDNS API types | +| 9 | Storage encoder | **Charon** | DONE | +| 10 | go-cgo | **Cladius** | RFC written, dispatching | + +— Charon + +--- + +## Cladius Answers — How To Do It With Core Primitives + +> These examples show Charon how each ask maps to existing Core APIs. +> Most of what he asked for already exists — he just needs the patterns. + +### #4 Answer: Package-Level Logging + +**Yes, `core.Print(nil, ...)` works.** The first arg is `*core.Core` and `nil` is valid — it falls back to the package-level logger. Your goroutines don't need a Core instance: + +```go +// In your sync goroutine — no *core.Core needed: +core.Print(nil, "block synced height=%d hash=%s", height, hash) +core.Error(nil, "sync failed: %v", err) + +// If you HAVE a Core instance (e.g. in a service handler): +core.Print(c, "wallet created id=%s", id) // tagged with service context +``` + +Both work. `nil` = package logger, `c` = contextual logger. Same output format. + +### #3 Answer: Service + Action Pattern (You Got It Right) + +Your code is correct. The full pattern with Core primitives: + +```go +// Register service with lifecycle +c.RegisterService("blockchain.wallet", core.Service{ + OnStart: func(ctx context.Context) core.Result { + return walletService.Start(ctx) + }, + OnStop: func(ctx context.Context) core.Result { + return walletService.Stop(ctx) + }, +}) + +// Register actions — path IS the CLI/HTTP/MCP route +c.Action("blockchain.wallet.create", walletService.HandleCreate) +c.Action("blockchain.wallet.balance", walletService.HandleBalance) + +// Call another service's action (for #8 dns.discover → blockchain.chain.aliases): +result := c.Run("blockchain.chain.aliases", core.Options{}) +``` + +### #5/#6/#7 Answer: Same Pattern, Different Path + +```go +// Escrow (HF4+) +c.Action("blockchain.escrow.create", escrowService.HandleCreate) +c.Action("blockchain.escrow.release", escrowService.HandleRelease) + +// Asset (HF5+) +c.Action("blockchain.asset.deploy", assetService.HandleDeploy) + +// Chain +c.Action("blockchain.chain.height", chainService.HandleHeight) +c.Action("blockchain.chain.block", chainService.HandleBlock) + +// All of these automatically get: +// - CLI: core blockchain chain height +// - HTTP: GET /blockchain/chain/height +// - MCP: blockchain.chain.height tool +// - i18n: blockchain.chain.height.* keys +``` + +### #9 Answer: BlockStore Interface + +For the go-store vs go-io backend swap: + +```go +// Define as a Core Data type +type BlockStore struct { + core.Data // inherits Store/Load/Delete +} + +// The backing medium is chosen at init: +store := core.NewData("blockchain.blocks", + core.WithMedium(gostore.SQLite("blocks.db")), // or: + // core.WithMedium(goio.File("blocks/")), // raw file backend +) + +// Usage is identical regardless of backend: +store.Store("block:12345", blockBytes) +block := store.Load("block:12345") +``` + +### #10 Answer: go-cgo + +RFC written at `plans/code/core/go/cgo/RFC.md`. Buffer, Scope, Call, String helpers. Dispatching to Codex when repo is created on Forge. + +### #8 Answer: go-dns + +`dappco.re/go/dns` — Core package. DNS record structs as DTOs mapping 1:1 to ClouDNS API. Your LNS code at `~/Code/lthn/lns/` moves in as the service layer on top. Dispatching when repo exists. diff --git a/docs/RFC-CORE-GO.md b/docs/RFC-CORE-GO.md new file mode 100644 index 0000000..db3d7ac --- /dev/null +++ b/docs/RFC-CORE-GO.md @@ -0,0 +1,1337 @@ +--- +module: dappco.re/go/core +repo: core/go +lang: go +tier: lib +tags: + - framework + - container + - dependency-injection + - lifecycle + - service-runtime +--- +# CoreGO RFC — API Contract + +> `dappco.re/go/core` — Dependency injection, service lifecycle, permission, and message-passing framework. +> This document is the authoritative API contract. An agent should be able to write a service +> that registers with Core from this document alone. + +**Status:** Living document +**Module:** `dappco.re/go/core` +**Version:** v0.8.0 + +--- + +## 1. Core — The Container + +Core is the central application container. Everything registers with Core, communicates through Core, and has its lifecycle managed by Core. + +### 1.1 Creation + +```go +c := core.New( + core.WithOption("name", "my-app"), + core.WithService(mypackage.Register), + core.WithService(anotherpackage.Register), + core.WithServiceLock(), +) +c.Run() +``` + +`core.New()` returns `*Core` (not Result — Core is the one type that can't wrap its own creation error). Functional options are applied in order. `WithServiceLock()` prevents late service registration. + +### 1.2 Lifecycle + +``` +New() → WithService factories called → LockApply() +RunE() → defer ServiceShutdown() → ServiceStartup() → Cli.Run() → returns error +Run() → RunE() → os.Exit(1) on error +``` + +`RunE()` is the primary lifecycle — returns `error`, always calls `ServiceShutdown` via defer (even on startup failure or panic). `Run()` is sugar that calls `RunE()` and exits on error. `ServiceStartup` calls `OnStartup(ctx)` on all `Startable` services in registration order. `ServiceShutdown` calls `OnShutdown(ctx)` on all `Stoppable` services. + +### 1.3 Subsystem Accessors + +Every subsystem is accessed via a method on Core: + +```go +c.Options() // *Options — input configuration +c.App() // *App — application metadata (name, version) +c.Config() // *Config — runtime settings, feature flags +c.Data() // *Data — embedded assets (Registry[*Embed]) +c.Drive() // *Drive — transport handles (Registry[*DriveHandle]) +c.Fs() // *Fs — filesystem I/O (sandboxable) +c.Cli() // *Cli — CLI command framework +c.IPC() // *Ipc — message bus internals +c.I18n() // *I18n — internationalisation +c.Error() // *ErrorPanic — panic recovery +c.Log() // *ErrorLog — structured logging +c.Process() // *Process — managed execution (Action sugar) +c.API() // *API — remote streams (protocol handlers) +c.Action(name) // *Action — named callable (register/invoke) +c.Task(name) // *Task — composed Action sequence +c.Entitled(name) // Entitlement — permission check +c.RegistryOf(n) // *Registry — cross-cutting queries +c.Context() // context.Context +c.Env(key) // string — environment variable (cached at init) +``` + +--- + +## 2. Primitive Types + +### 2.1 Option + +The atom. A single key-value pair. + +```go +core.Option{Key: "name", Value: "brain"} +core.Option{Key: "port", Value: 8080} +core.Option{Key: "debug", Value: true} +``` + +### 2.2 Options + +A collection of Option with typed accessors. + +```go +opts := core.NewOptions( + core.Option{Key: "name", Value: "myapp"}, + core.Option{Key: "port", Value: 8080}, + core.Option{Key: "debug", Value: true}, +) + +opts.String("name") // "myapp" +opts.Int("port") // 8080 +opts.Bool("debug") // true +opts.Has("name") // true +opts.Len() // 3 + +opts.Set("name", "new-name") +opts.Get("name") // Result{Value: "new-name", OK: true} +``` + +### 2.3 Result + +Universal return type. Every Core operation returns Result. + +```go +type Result struct { + Value any + OK bool +} +``` + +Usage patterns: + +```go +// Check success +r := c.Config().Get("database.host") +if r.OK { + host := r.Value.(string) +} + +// Service factory returns Result +func Register(c *core.Core) core.Result { + svc := &MyService{} + return core.Result{Value: svc, OK: true} +} + +// Error as Result +return core.Result{Value: err, OK: false} +``` + +No generics on Result. Type-assert the Value when needed. This is deliberate — `Result` is universal across all subsystems without carrying type parameters. + +### 2.4 Message, Query + +IPC type aliases for the broadcast/request system: + +```go +type Message any // broadcast via ACTION — fire and forget +type Query any // request/response via QUERY — returns first handler's result +``` + +For tracked work, use named Actions: `c.PerformAsync("action.name", opts)`. + +--- + +## 3. Service System + +### 3.1 Registration + +Services register via factory functions passed to `WithService`: + +```go +core.New( + core.WithService(mypackage.Register), +) +``` + +The factory signature is `func(*Core) Result`. The returned `Result.Value` is the service instance. + +### 3.2 Factory Pattern + +```go +func Register(c *core.Core) core.Result { + svc := &MyService{ + runtime: core.NewServiceRuntime(c, MyOptions{}), + } + return core.Result{Value: svc, OK: true} +} +``` + +`NewServiceRuntime[T]` gives the service access to Core and typed options: + +```go +type MyService struct { + *core.ServiceRuntime[MyOptions] +} + +// Access Core from within the service: +func (s *MyService) doSomething() { + c := s.Core() + cfg := s.Config().String("my.setting") +} +``` + +### 3.3 Auto-Discovery + +`WithService` reflects on the returned instance to discover: +- **Package name** → service name (from reflect type path) +- **Startable interface** → `OnStartup(ctx) Result` called during `ServiceStartup` +- **Stoppable interface** → `OnShutdown(ctx) Result` called during `ServiceShutdown` +- **HandleIPCEvents method** → auto-registered as IPC handler + +### 3.4 Retrieval + +```go +// Type-safe retrieval +svc, ok := core.ServiceFor[*MyService](c, "mypackage") +if !ok { + // service not registered +} + +// Must variant (panics if not found) +svc := core.MustServiceFor[*MyService](c, "mypackage") + +// List all registered services +names := c.Services() // []string in registration order +``` + +### 3.5 Lifecycle Interfaces + +```go +type Startable interface { + OnStartup(ctx context.Context) Result +} + +type Stoppable interface { + OnShutdown(ctx context.Context) Result +} +``` + +Services implementing these are called during `RunE()` / `Run()` in registration order. + +--- + +## 4. IPC — Message Passing + +### 4.1 ACTION (broadcast) + +Fire-and-forget broadcast to all registered handlers: + +```go +// Send +c.ACTION(messages.AgentCompleted{ + Agent: "codex", Repo: "go-io", Status: "completed", +}) + +// Register handler +c.RegisterAction(func(c *core.Core, msg core.Message) core.Result { + if ev, ok := msg.(messages.AgentCompleted); ok { + // handle completion + } + return core.Result{OK: true} +}) +``` + +All handlers receive all messages. Type-switch to filter. Handler return values are ignored — broadcast calls ALL handlers regardless. Each handler is wrapped in panic recovery. + +### 4.2 QUERY (request/response) + +First handler to return a non-empty result wins: + +```go +// Send +result := c.QUERY(MyQuery{Name: "brain"}) +if result.OK { + svc := result.Value +} + +// Register handler +c.RegisterQuery(func(c *core.Core, q core.Query) core.Result { + if mq, ok := q.(MyQuery); ok { + return core.Result{Value: found, OK: true} + } + return core.Result{OK: false} // not my query +}) +``` + +### 4.3 PerformAsync (background action) + +```go +// Execute a named action in background with progress tracking +r := c.PerformAsync("agentic.dispatch", opts) +taskID := r.Value.(string) + +// Report progress +c.Progress(taskID, 0.5, "halfway done", "agentic.dispatch") +``` + +Broadcasts `ActionTaskStarted`, `ActionTaskProgress`, `ActionTaskCompleted` as ACTION messages. + +--- + +## 5. Config + +Runtime configuration with typed accessors and feature flags. + +```go +c.Config().Set("database.host", "localhost") +c.Config().Set("database.port", 5432) + +host := c.Config().String("database.host") // "localhost" +port := c.Config().Int("database.port") // 5432 + +// Feature flags +c.Config().Enable("dark-mode") +c.Config().Enabled("dark-mode") // true +c.Config().Disable("dark-mode") +c.Config().EnabledFeatures() // []string + +// Type-safe generic getter +val := core.ConfigGet[string](c.Config(), "database.host") +``` + +--- + +## 6. Data — Embedded Assets + +Mount embedded filesystems and read from them: + +```go +//go:embed prompts/* +var promptFS embed.FS + +// Mount during service registration +c.Data().New(core.NewOptions( + core.Option{Key: "name", Value: "prompts"}, + core.Option{Key: "source", Value: promptFS}, + core.Option{Key: "path", Value: "prompts"}, +)) + +// Read +r := c.Data().ReadString("prompts/coding.md") +if r.OK { + content := r.Value.(string) +} + +// List +r := c.Data().List("prompts/") +r := c.Data().ListNames("prompts/") +r := c.Data().Mounts() // []string (insertion order) + +// Data embeds Registry[*Embed] — all Registry methods available: +c.Data().Has("prompts") +c.Data().Each(func(name string, emb *Embed) { ... }) +``` + +--- + +## 7. Drive — Transport Handles + +Registry of named transport handles (API endpoints, MCP servers, etc): + +```go +c.Drive().New(core.NewOptions( + core.Option{Key: "name", Value: "forge"}, + core.Option{Key: "transport", Value: "https://forge.lthn.ai"}, +)) + +r := c.Drive().Get("forge") // Result with *DriveHandle +c.Drive().Has("forge") // true +c.Drive().Names() // []string (insertion order) + +// Drive embeds Registry[*DriveHandle] — all Registry methods available. +``` + +--- + +## 8. Fs — Filesystem + +Sandboxable filesystem I/O. All paths are validated against the root. + +```go +fs := c.Fs() + +// Read/Write +r := fs.Read("/path/to/file") // Result{Value: string} +r := fs.Write("/path/to/file", content) // Result{OK: bool} +r := fs.WriteMode(path, content, 0600) // With permissions + +// Directory ops +r := fs.EnsureDir("/path/to/dir") +r := fs.List("/path/to/dir") // Result{Value: []os.DirEntry} +fs.IsDir(path) // bool +fs.IsFile(path) // bool +fs.Exists(path) // bool + +// Streams +r := fs.Open(path) // Result{Value: *os.File} +r := fs.Create(path) // Result{Value: *os.File} +r := fs.Append(path) // Result{Value: io.WriteCloser} +r := fs.ReadStream(path) // Result{Value: io.ReadCloser} +r := fs.WriteStream(path) // Result{Value: io.WriteCloser} + +// Atomic write (write-to-temp-then-rename, safe for concurrent readers) +r := fs.WriteAtomic(path, content) + +// Delete +r := fs.Delete(path) // single file +r := fs.DeleteAll(path) // recursive +r := fs.Rename(old, new) +r := fs.Stat(path) // Result{Value: os.FileInfo} + +// Sandbox control +fs.Root() // sandbox root path +fs.NewUnrestricted() // Fs with root "/" — full access +``` + +--- + +## 9. CLI + +Command tree with path-based routing: + +```go +c.Command("issue/get", core.Command{ + Description: "Get a Forge issue", + Action: s.cmdIssueGet, +}) + +c.Command("issue/list", core.Command{ + Description: "List Forge issues", + Action: s.cmdIssueList, +}) + +// Action signature +func (s *MyService) cmdIssueGet(opts core.Options) core.Result { + repo := opts.String("_arg") // positional arg + num := opts.String("number") // --number=N flag + // ... + return core.Result{OK: true} +} +``` + +Path = command hierarchy. `issue/get` becomes `myapp issue get` in CLI. + +Managed commands have lifecycle provided by go-process: + +```go +c.Command("serve", core.Command{ + Action: handler, + Managed: "process.daemon", // go-process provides start/stop/restart +}) +``` + +--- + +## 10. Error Handling + +All errors use `core.E()`: + +```go +// Standard error +return core.E("service.Method", "what failed", underlyingErr) + +// With format +return core.E("service.Method", core.Sprintf("not found: %s", name), nil) + +// Error inspection +core.Operation(err) // "service.Method" +core.ErrorMessage(err) // "what failed" +core.ErrorCode(err) // code if set via WrapCode +core.Root(err) // unwrap to root cause +core.Is(err, target) // errors.Is +core.As(err, &target) // errors.As +``` + +**NEVER use `fmt.Errorf`, `errors.New`, or `log.*`.** Core handles all error reporting. + +--- + +## 11. Logging + +```go +core.Info("server started", "port", 8080) +core.Debug("processing", "item", name) +core.Warn("deprecated", "feature", "old-api") +core.Error("failed", "err", err) +core.Security("access denied", "user", username) +``` + +Key-value pairs after the message. Structured, not formatted strings. + +--- + +## 12. String Helpers + +Core re-exports string operations to avoid `strings` import: + +```go +core.Contains(s, substr) +core.HasPrefix(s, prefix) +core.HasSuffix(s, suffix) +core.TrimPrefix(s, prefix) +core.TrimSuffix(s, suffix) +core.Split(s, sep) +core.SplitN(s, sep, n) +core.Join(sep, parts...) +core.Replace(s, old, new) +core.Lower(s) / core.Upper(s) +core.Trim(s) +core.Sprintf(format, args...) +core.Concat(parts...) +core.NewBuilder() / core.NewReader(s) +``` + +--- + +## 13. Path Helpers + +```go +core.Path(segments...) // ~/segments joined +core.JoinPath(segments...) // filepath.Join +core.PathBase(p) // filepath.Base +core.PathDir(p) // filepath.Dir +core.PathExt(p) // filepath.Ext +core.PathIsAbs(p) // filepath.IsAbs +core.PathGlob(pattern) // filepath.Glob +core.CleanPath(p, sep) // normalise separators +``` + +--- + +## 14. Utility Functions + +```go +core.Print(writer, format, args...) // formatted output +core.Env(key) // cached env var (set at init) +core.EnvKeys() // all available env keys + +// Arg extraction (positional) +core.Arg(0, args...) // Result +core.ArgString(0, args...) // string +core.ArgInt(0, args...) // int +core.ArgBool(0, args...) // bool + +// Flag parsing +core.IsFlag("--name") // true +core.ParseFlag("--name=value") // "name", "value", true +core.FilterArgs(args) // strip flags, keep positional + +// Identifiers and validation +core.ID() // "id-42-a3f2b1" — unique per process +core.ValidateName("brain") // Result{OK: true} — rejects "", ".", "..", path seps +core.SanitisePath("../../x") // "x" — extracts safe base, "invalid" for dangerous + +// JSON (wraps encoding/json — consumers don't import it directly) +core.JSONMarshal(myStruct) // Result{Value: []byte, OK: bool} +core.JSONMarshalString(myStruct) // string (returns "{}" on error) +core.JSONUnmarshal(data, &target) // Result{OK: bool} +core.JSONUnmarshalString(s, &target) +``` + +--- + +## 15. Lock System + +Per-Core mutex registry for coordinating concurrent access: + +```go +c.Lock("drain").Mutex.Lock() +defer c.Lock("drain").Mutex.Unlock() + +// Enable named locks +c.LockEnable("service-registry") + +// Apply lock (prevents further registration) +c.LockApply() +``` + +--- + +## 16. ServiceRuntime Generic Helper + +Embed in services to get Core access and typed options: + +```go +type MyService struct { + *core.ServiceRuntime[MyOptions] +} + +type MyOptions struct { + BufferSize int + Timeout time.Duration +} + +func NewMyService(c *core.Core) core.Result { + svc := &MyService{ + ServiceRuntime: core.NewServiceRuntime(c, MyOptions{ + BufferSize: 1024, + Timeout: 30 * time.Second, + }), + } + return core.Result{Value: svc, OK: true} +} + +// Within the service: +func (s *MyService) DoWork() { + c := s.Core() // access Core + opts := s.Options() // MyOptions{BufferSize: 1024, ...} + cfg := s.Config() // shortcut to s.Core().Config() +} +``` + +--- + +## 17. Process — Managed Execution + +`c.Process()` is sugar over named Actions. core/go defines the primitive. go-process provides the implementation via `c.Action("process.run", handler)`. + +```go +// Synchronous — returns Result +r := c.Process().Run(ctx, "git", "log", "--oneline") +r := c.Process().RunIn(ctx, "/repo", "go", "test", "./...") +r := c.Process().RunWithEnv(ctx, dir, []string{"GOWORK=off"}, "go", "test") + +// Async — returns process ID +r := c.Process().Start(ctx, opts) + +// Control +c.Process().Kill(ctx, core.NewOptions(core.Option{Key: "id", Value: processID})) + +// Capability check +if c.Process().Exists() { /* go-process is registered */ } +``` + +**Permission by registration:** No go-process registered → `c.Process().Run()` returns `Result{OK: false}`. No config, no tokens. The service either exists or it doesn't. + +```go +// Sandboxed Core — no process capability +c := core.New() +c.Process().Run(ctx, "rm", "-rf", "/") // Result{OK: false} — nothing happens + +// Full Core — process registered +c := core.New(core.WithService(process.Register)) +c.Process().Run(ctx, "git", "log") // executes, returns output +``` + +> Consumer implementation: see `go-process/docs/RFC.md` + +--- + +## 18. Action and Task — The Execution Primitives + +An Action is a named, registered callable. A Task is a composed sequence of Actions. + +### 18.1 Action — The Atomic Unit + +```go +// Register +c.Action("git.log", func(ctx context.Context, opts core.Options) core.Result { + dir := opts.String("dir") + return c.Process().RunIn(ctx, dir, "git", "log", "--oneline") +}) + +// Invoke +r := c.Action("git.log").Run(ctx, core.NewOptions( + core.Option{Key: "dir", Value: "/repo"}, +)) + +// Check capability +c.Action("process.run").Exists() // true if go-process registered + +// List all +c.Actions() // []string{"process.run", "agentic.dispatch", ...} +``` + +`c.Action(name)` is dual-purpose: with handler arg → register; without → return for invocation. + +### 18.2 Action Type + +```go +type ActionHandler func(context.Context, Options) Result + +type Action struct { + Name string + Handler ActionHandler + Description string + Schema Options // expected input keys +} +``` + +`Action.Run()` has panic recovery and entitlement checking (Section 21) built in. + +### 18.3 Where Actions Come From + +Services register during `OnStartup`: + +```go +func (s *MyService) OnStartup(ctx context.Context) core.Result { + c := s.Core() + c.Action("process.run", s.handleRun) + c.Action("git.clone", s.handleGitClone) + return core.Result{OK: true} +} +``` + +The action namespace IS the capability map. go-process registers `process.*`, core/agent registers `agentic.*`. + +### 18.4 Permission Model + +Three states for any action: + +| State | `Exists()` | `Entitled()` | `Run()` | +|-------|-----------|-------------|---------| +| Not registered | false | — | `Result{OK: false}` not registered | +| Registered, not entitled | true | false | `Result{OK: false}` not entitled | +| Registered and entitled | true | true | executes handler | + +### 18.5 Task — Composing Actions + +```go +c.Task("deploy", core.Task{ + Description: "Build, test, deploy", + Steps: []core.Step{ + {Action: "go.build"}, + {Action: "go.test"}, + {Action: "docker.push"}, + {Action: "ansible.deploy", Async: true}, // doesn't block + }, +}) + +r := c.Task("deploy").Run(ctx, c, opts) +``` + +Sequential steps stop on first failure. `Async: true` steps fire without blocking. +`Input: "previous"` pipes last step's output to next step. + +### 18.6 Background Execution + +```go +r := c.PerformAsync("agentic.dispatch", opts) +taskID := r.Value.(string) + +// Broadcasts ActionTaskStarted, ActionTaskProgress, ActionTaskCompleted +c.Progress(taskID, 0.5, "halfway", "agentic.dispatch") +``` + +### 18.7 How Process Fits + +`c.Process()` is sugar over Actions: + +```go +c.Process().Run(ctx, "git", "log") +// equivalent to: +c.Action("process.run").Run(ctx, core.NewOptions( + core.Option{Key: "command", Value: "git"}, + core.Option{Key: "args", Value: []string{"log"}}, +)) +``` + +--- + +## 19. API — Remote Streams + +Drive is the phone book (WHERE). API is the phone (HOW). Consumer packages register protocol handlers. + +```go +// Configure endpoint in Drive +c.Drive().New(core.NewOptions( + core.Option{Key: "name", Value: "charon"}, + core.Option{Key: "transport", Value: "http://10.69.69.165:9101/mcp"}, +)) + +// Open stream — looks up Drive, finds protocol handler +r := c.API().Stream("charon") +if r.OK { + stream := r.Value.(core.Stream) + stream.Send(payload) + resp, _ := stream.Receive() + stream.Close() +} +``` + +### 19.1 Stream Interface + +```go +type Stream interface { + Send(data []byte) error + Receive() ([]byte, error) + Close() error +} +``` + +### 19.2 Protocol Handlers + +Consumer packages register factories per URL scheme: + +```go +// In a transport package's OnStartup: +c.API().RegisterProtocol("http", httpStreamFactory) +c.API().RegisterProtocol("mcp", mcpStreamFactory) +``` + +Resolution: `c.API().Stream("charon")` → Drive lookup → extract scheme → find factory → create Stream. + +No protocol handler = no capability. + +### 19.3 Remote Action Dispatch + +Actions transparently cross machine boundaries via `host:action` syntax: + +```go +// Local +r := c.RemoteAction("agentic.status", ctx, opts) + +// Remote — same API, different host +r := c.RemoteAction("charon:agentic.status", ctx, opts) +// → splits on ":" → endpoint="charon", action="agentic.status" +// → c.API().Call("charon", "agentic.status", opts) + +// Web3 — Lethean dVPN routed +r := c.RemoteAction("snider.lthn:brain.recall", ctx, opts) +``` + +### 19.4 Direct Call + +```go +r := c.API().Call("charon", "agentic.dispatch", opts) +// Opens stream, sends JSON-RPC, receives response, closes stream +``` + +--- + +## 20. Registry — The Universal Collection Primitive + +Thread-safe named collection. The brick all registries build on. + +### 20.1 The Type + +```go +// Registry is a thread-safe named collection. The universal brick +// for all named registries in Core. +type Registry[T any] struct { + items map[string]T + mu sync.RWMutex + locked bool +} +``` + +### 20.3 Operations + +```go +r := core.NewRegistry[*Service]() + +r.Set("brain", brainSvc) // register +r.Get("brain") // Result{brainSvc, true} +r.Has("brain") // true +r.Names() // []string{"brain", "monitor", ...} +r.List("brain.*") // glob/prefix match +r.Each(func(name string, item T)) // iterate +r.Len() // count +r.Lock() // prevent further Set calls +r.Locked() // bool +r.Delete("brain") // remove (if not locked) +``` + +### 20.4 Core Accessor + +`c.Registry(name)` accesses named registries. Each subsystem's registry is accessible through it: + +```go +c.RegistryOf("services") // the service registry +c.Registry("commands") // the command tree +c.RegistryOf("actions") // IPC action handlers +c.RegistryOf("drives") // transport handles +c.Registry("data") // mounted filesystems +``` + +Cross-cutting queries become natural: + +```go +c.RegistryOf("actions").List("process.*") // all process capabilities +c.RegistryOf("drives").Names() // all configured transports +c.RegistryOf("services").Has("brain") // is brain service loaded? +c.RegistryOf("actions").Len() // how many actions registered? +``` + +### 20.5 Typed Accessors Are Sugar + +The existing subsystem accessors become typed convenience over Registry: + +```go +// These are equivalent: +c.Service("brain") // typed sugar +c.RegistryOf("services").Get("brain") // universal access + +c.Drive().Get("forge") // typed sugar +c.RegistryOf("drives").Get("forge") // universal access + +c.Action("process.run") // typed sugar +c.RegistryOf("actions").Get("process.run") // universal access +``` + +The typed accessors stay — they're ergonomic and type-safe. `c.Registry()` adds the universal query layer on top. + +### 20.6 What Embeds Registry + +All named collections in Core embed `Registry[T]`: + +- `ServiceRegistry` → `Registry[*Service]` +- `CommandRegistry` → `Registry[*Command]` +- `Drive` → `Registry[*DriveHandle]` +- `Data` → `Registry[*Embed]` +- `Lock.locks` → `Registry[*sync.RWMutex]` +- `IPC.actions` → `Registry[*Action]` +- `IPC.tasks` → `Registry[*Task]` + +--- + +## Design Philosophy + +### Core Is Lego Bricks + +Core is infrastructure, not an encapsulated library. Downstream packages (core/agent, core/mcp, go-process) compose with Core's primitives. **Exported fields are intentional, not accidental.** Every unexported field that forces a consumer to write a wrapper method adds LOC downstream — the opposite of Core's purpose. + +```go +// Core reduces downstream code: +if r.OK { use(r.Value) } + +// vs Go convention that adds downstream LOC: +val, err := thing.Get() +if err != nil { + return fmt.Errorf("get: %w", err) +} +``` + +This is why `core.Result` exists — it replaces multiple lines of error handling with `if r.OK {}`. That's the design: expose the primitive, reduce consumer code. + +### Export Rules + +| Should Export | Why | +|--------------|-----| +| Struct fields used by consumers | Removes accessor boilerplate downstream | +| Registry types (`ServiceRegistry`) | Lets consumers extend service management | +| IPC internals (`Ipc` handlers) | Lets consumers build custom dispatch | +| Lifecycle hooks (`OnStart`, `OnStop`) | Composable without interface overhead | + +| Should NOT Export | Why | +|------------------|-----| +| Mutexes and sync primitives | Concurrency must be managed by Core | +| Context/cancel pairs | Lifecycle is Core's responsibility | +| Internal counters | Implementation detail, not a brick | + +### DTO Pattern — Structs Not Props + +Exported functions that handle Actions MUST accept and return typed structs, never loose key-value params. The struct IS the DTO — CoreTS and CorePHP generate their typed clients from Go struct definitions. + +```go +// CORRECT — struct defines the contract +type CreateOrderInput struct { + ProductID string `json:"product_id"` + Quantity int `json:"quantity"` + Currency string `json:"currency"` +} + +type CreateOrderOutput struct { + OrderID string `json:"order_id"` + Total int64 `json:"total"` +} + +func (s *CommerceService) CreateOrder(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) { + // ... +} + +// WRONG — loose Options, no type contract +func (s *CommerceService) CreateOrder(ctx context.Context, opts core.Options) core.Result { + productID := opts.String("product_id") // invisible to codegen +} +``` + +The generation pipeline: + +``` +Go struct (tagged with json) + → CoreCommand reads struct metadata via reflection + → OpenAPI spec generated (JSON Schema per struct) + → CoreTS: TypeScript interfaces + typed fetch calls + → CorePHP: PHP DTOs + typed Action calls +``` + +Each package defines its own input/output structs in its source. The structs live next to the handler — no separate DTO layer. The SDK pipeline reads them at build time. + +**core.Options is for framework internals** (service registration, config). **Typed structs are for business logic** (actions, commands, API endpoints). This separation is what makes codegen possible. + +### Why core/go Is Minimal + +core/go deliberately avoids importing anything beyond stdlib + go-io + go-log. This keeps it as a near-pure stdlib implementation. Packages that add external dependencies (CLI frameworks, HTTP routers, MCP SDK) live in separate repos: + +``` +core/go — pure primitives (stdlib only) +core/go-process — process management (adds os/exec) +core/mcp — MCP server (adds go-sdk) +core/agent — orchestration (adds forge, yaml, mcp) +``` + +Each layer imports the one below. core/go imports nothing from the ecosystem — everything imports core/go. + + + +## Consumer RFCs + +core/go provides the primitives. These RFCs describe how consumers use them: + +| Package | RFC | Scope | +|---------|-----|-------| +| go-process | `core/go-process/docs/RFC.md` | Action handlers for process.run/start/kill, ManagedProcess, daemon registry | +| core/agent | `code/core/agent/docs/RFC.md` | Named Actions, completion pipeline (P6-1 fix), WriteAtomic migration, Process migration, Entitlement gating | + +Each consumer RFC is self-contained — an agent can implement it from the document alone. + +--- + +## Versioning + +### Release Model + +The patch count after a release IS the quality metric. v0.8.1 means the spec missed one thing. + +### Cadence + +1. **RFC spec** — design the version in prose +2. **Implement** — build to spec with AX-7 tests +3. **Refine** — review passes catch drift +4. **Tag** — when all sections pass +5. **Measure** — patches tell you what was missed + +## 21. Entitlement — The Permission Primitive + +Core provides the primitive. go-entitlements and commerce-matrix provide implementations. + +### 21.1 The Problem + +`*Core` grants God Mode (P11-1). Every service sees everything. The 14 findings in Root Cause 2 all stem from this. The conclave is trusted — but the SaaS platform (RFC-004), the commerce hierarchy (RFC-005), and the agent sandbox all need boundaries. + +Three systems ask the same question with different vocabulary: + +``` +Can [subject] do [action] with [quantity] in [context]? +``` + +| System | Subject | Action | Quantity | Context | +|--------|---------|--------|----------|---------| +| RFC-004 Entitlements | workspace | feature.code | N | active packages | +| RFC-005 Commerce Matrix | entity (M1/M2/M3) | permission.key | 1 | hierarchy path | +| Core Actions | this Core instance | action.name | 1 | registered services | + +### 21.2 The Primitive + +```go +// Entitlement is the result of a permission check. +// Carries context for both boolean gates (Allowed) and usage limits (Limit/Used/Remaining). +// Maps directly to RFC-004 EntitlementResult and RFC-005 PermissionResult. +type Entitlement struct { + Allowed bool // permission granted + Unlimited bool // no cap (agency tier, admin, trusted conclave) + Limit int // total allowed (0 = boolean gate, no quantity dimension) + Used int // current consumption + Remaining int // Limit - Used + Reason string // denial reason — for UI feedback and audit logging +} + +// Entitled checks if an action is permitted in the current context. +// Default: always returns Allowed=true, Unlimited=true (trusted conclave). +// With go-entitlements: checks workspace packages, features, usage, boosts. +// With commerce-matrix: checks entity hierarchy, lock cascade. +// +// e := c.Entitled("process.run") // boolean — can this Core run processes? +// e := c.Entitled("social.accounts", 3) // quantity — can workspace create 3 more accounts? +// if e.Allowed { proceed() } +// if e.NearLimit(0.8) { showWarning() } +func (c *Core) Entitled(action string, quantity ...int) Entitlement +``` + +### 21.3 The Checker — Consumer-Provided + +Core defines the interface. Consumer packages provide the implementation. + +```go +// EntitlementChecker answers "can [subject] do [action] with [quantity]?" +// Subject comes from context (workspace, entity, user — consumer's concern). +type EntitlementChecker func(action string, quantity int, ctx context.Context) Entitlement +``` + +Registration via Core: + +```go +// SetEntitlementChecker replaces the default (permissive) checker. +// Called by go-entitlements or commerce-matrix during OnStartup. +// +// func (s *EntitlementService) OnStartup(ctx context.Context) core.Result { +// s.Core().SetEntitlementChecker(s.check) +// return core.Result{OK: true} +// } +func (c *Core) SetEntitlementChecker(checker EntitlementChecker) +``` + +Default checker (no entitlements package loaded): + +```go +// defaultChecker — trusted conclave, everything permitted +func defaultChecker(action string, quantity int, ctx context.Context) Entitlement { + return Entitlement{Allowed: true, Unlimited: true} +} +``` + +### 21.4 Enforcement Point — Action.Run() + +The entitlement check lives in `Action.Run()`, before execution. One enforcement point for all capabilities. + +```go +func (a *Action) Run(ctx context.Context, opts Options) (result Result) { + if !a.Exists() { return not-registered } + if !a.enabled { return disabled } + + // Entitlement check — permission boundary + if e := a.core.Entitled(a.Name); !e.Allowed { + return Result{E("action.Run", + Concat("not entitled: ", a.Name, " — ", e.Reason), nil), false} + } + + defer func() { /* panic recovery */ }() + return a.Handler(ctx, opts) +} +``` + +Three states for any action: + +| State | Exists() | Entitled() | Run() | +|-------|----------|------------|-------| +| Not registered | false | — | Result{OK: false} "not registered" | +| Registered, not entitled | true | false | Result{OK: false} "not entitled" | +| Registered and entitled | true | true | executes handler | + +### 21.5 How RFC-004 (SaaS Entitlements) Plugs In + +go-entitlements registers as a service and replaces the checker: + +```go +// In go-entitlements: +func (s *Service) OnStartup(ctx context.Context) core.Result { + s.Core().SetEntitlementChecker(func(action string, qty int, ctx context.Context) core.Entitlement { + workspace := s.workspaceFromContext(ctx) + if workspace == nil { + return core.Entitlement{Allowed: true, Unlimited: true} // no workspace = system context + } + + result := s.Can(workspace, action, qty) + + return core.Entitlement{ + Allowed: result.IsAllowed(), + Unlimited: result.IsUnlimited(), + Limit: result.Limit, + Used: result.Used, + Remaining: result.Remaining, + Reason: result.Message(), + } + }) + return core.Result{OK: true} +} +``` + +Maps 1:1 to RFC-004's `EntitlementResult`: +- `$result->isAllowed()` → `e.Allowed` +- `$result->isUnlimited()` → `e.Unlimited` +- `$result->limit` → `e.Limit` +- `$result->used` → `e.Used` +- `$result->remaining` → `e.Remaining` +- `$result->getMessage()` → `e.Reason` +- `$result->isNearLimit()` → `e.NearLimit(0.8)` +- `$result->getUsagePercentage()` → `e.UsagePercent()` + +### 21.6 How RFC-005 (Commerce Matrix) Plugs In + +commerce-matrix registers and replaces the checker with hierarchy-aware logic: + +```go +// In commerce-matrix: +func (s *MatrixService) OnStartup(ctx context.Context) core.Result { + s.Core().SetEntitlementChecker(func(action string, qty int, ctx context.Context) core.Entitlement { + entity := s.entityFromContext(ctx) + if entity == nil { + return core.Entitlement{Allowed: true, Unlimited: true} + } + + result := s.Can(entity, action, "") + + return core.Entitlement{ + Allowed: result.IsAllowed(), + Reason: result.Reason, + } + }) + return core.Result{OK: true} +} +``` + +Maps to RFC-005's cascade model: +- `M1 says NO → everything below is NO` → checker walks hierarchy, returns `{Allowed: false, Reason: "Locked by M1"}` +- Training mode → checker returns `{Allowed: false, Reason: "undefined — training required"}` +- Production strict mode → undefined = denied + +### 21.7 Composing Both Systems + +When a SaaS platform ALSO has commerce hierarchy (Host UK), the checker composes internally: + +```go +func (s *CompositeService) check(action string, qty int, ctx context.Context) core.Entitlement { + // Check commerce matrix first (hard permissions) + matrixResult := s.matrix.Can(entityFromCtx(ctx), action, "") + if matrixResult.IsDenied() { + return core.Entitlement{Allowed: false, Reason: matrixResult.Reason} + } + + // Then check entitlements (usage limits) + entResult := s.entitlements.Can(workspaceFromCtx(ctx), action, qty) + return core.Entitlement{ + Allowed: entResult.IsAllowed(), + Unlimited: entResult.IsUnlimited(), + Limit: entResult.Limit, + Used: entResult.Used, + Remaining: entResult.Remaining, + Reason: entResult.Message(), + } +} +``` + +Matrix (hierarchy) gates first. Entitlements (usage) gate second. One checker, composed. + +### 21.8 Convenience Methods on Entitlement + +```go +// NearLimit returns true if usage exceeds the threshold percentage. +// RFC-004: $result->isNearLimit() uses 80% threshold. +// +// if e.NearLimit(0.8) { showUpgradePrompt() } +func (e Entitlement) NearLimit(threshold float64) bool + +// UsagePercent returns current usage as a percentage of the limit. +// RFC-004: $result->getUsagePercentage() +// +// pct := e.UsagePercent() // 75.0 +func (e Entitlement) UsagePercent() float64 + +// RecordUsage is called after a gated action succeeds. +// Delegates to the entitlement service for usage tracking. +// This is the equivalent of RFC-004's $workspace->recordUsage(). +// +// e := c.Entitled("ai.credits", 10) +// if e.Allowed { +// doWork() +// c.RecordUsage("ai.credits", 10) +// } +func (c *Core) RecordUsage(action string, quantity ...int) +``` + +### 21.9 Audit Trail — RFC-004 Section: Audit Logging + +Every entitlement check can be logged via `core.Security()`: + +```go +func (c *Core) Entitled(action string, quantity ...int) Entitlement { + qty := 1 + if len(quantity) > 0 { + qty = quantity[0] + } + + e := c.entitlementChecker(action, qty, c.Context()) + + // Audit logging for denials (P11-6) + if !e.Allowed { + Security("entitlement.denied", "action", action, "quantity", qty, "reason", e.Reason) + } + + return e +} +``` + +### 21.10 Core Struct Changes + +```go +type Core struct { + // ... existing fields ... + entitlementChecker EntitlementChecker // default: everything permitted +} +``` + +Constructor: + +```go +func New(opts ...CoreOption) *Core { + c := &Core{ + // ... existing ... + entitlementChecker: defaultChecker, + } + // ... +} +``` + +### 21.11 What This Does NOT Do + +- **Does not add database dependencies** — Core is stdlib only. Usage tracking, package management, billing — all in consumer packages. +- **Does not define features** — The feature catalogue (social.accounts, ai.credits, etc.) is defined by the SaaS platform, not Core. +- **Does not manage subscriptions** — Commerce (RFC-005) and billing (Blesta/Stripe) are consumer concerns. +- **Does not replace Action registration** — Registration IS capability. Entitlement IS permission. Both must be true. +- **Does not enforce at Config/Data/Fs level** — v0.8.0 gates Actions. Config/Data/Fs gating requires per-subsystem entitlement checks (same pattern, more integration points). + +### 21.12 The Subsystem Map (Updated) + +``` +c.Registry() — universal named collection +c.Options() — input configuration +c.App() — identity +c.Config() — runtime settings +c.Data() — embedded assets +c.Drive() — connection config (WHERE) +c.API() — remote streams (HOW) [planned] +c.Fs() — filesystem +c.Process() — managed execution (Action sugar) +c.Action() — named callables (register, invoke, inspect) +c.Task() — composed Action sequences +c.IPC() — local message bus +c.Cli() — command tree +c.Log() — logging +c.Error() — panic recovery +c.I18n() — internationalisation +c.Entitled() — permission check (NEW) +c.RecordUsage() — usage tracking (NEW) +``` + +--- + +## Changelog + +- 2026-03-25: v0.8.0 — All 21 sections implemented. 483 tests, 84.7% coverage, 100% AX-7 naming. +- 2026-03-25: Initial specification created from 500k token discovery session. 108 findings, 5 root causes, 13 review passes. Discovery detail preserved in git history.