# CoreGO API Contract — RFC Specification > `dappco.re/go/core` — Dependency injection, service lifecycle, 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.7.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() Run() → ServiceStartup() → Cli.Run() → ServiceShutdown() ``` `Run()` is blocking. `ServiceStartup` calls `OnStartup(ctx)` on all services implementing `Startable`. `ServiceShutdown` calls `OnShutdown(ctx)` on all `Stoppable` services. Shutdown uses `context.Background()` — not the Core context (which is already cancelled). ### 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 mounted by packages c.Drive() // *Drive — transport handles (API, MCP, SSH) 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.Context() // context.Context — Core's lifecycle 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, Task IPC type aliases — all are `any` at the type level, distinguished by usage: ```go type Message any // broadcast via ACTION — fire and forget type Query any // request/response via QUERY — returns first handler's result type Task any // work unit via PERFORM — tracked with progress ``` --- ## 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) error` called during `ServiceStartup` - **Stoppable interface** → `OnShutdown(ctx) error` 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 ``` ### 3.5 Lifecycle Interfaces ```go type Startable interface { OnStartup(ctx context.Context) error } type Stoppable interface { OnShutdown(ctx context.Context) error } ``` Services implementing these are automatically called during `c.Run()`. --- ## 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. Return `Result{OK: true}` always (errors are logged, not propagated). ### 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 PERFORM (tracked task) ```go // Execute with progress tracking c.PERFORM(MyTask{Data: payload}) // Register task handler c.RegisterTask(func(c *core.Core, t core.Task) core.Result { // do work, report progress c.Progress(taskID, 0.5, "halfway done", t) return core.Result{Value: output, OK: true} }) ``` --- ## 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 of mount names ``` --- ## 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 ``` --- ## 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} // 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} ``` --- ## 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. --- ## 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 ``` --- ## 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() } ``` --- ## 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 | ### 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/go-cli — CLI framework (if separated) 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. ## Known Issues ### 1. Dual IPC Naming `ACTION()` and `Action()` do the same thing. `QUERY()` and `Query()`. Two names for one operation. Pick one or document when to use which. ```go // Currently both exist: c.ACTION(msg) // uppercase alias c.Action(msg) // actual implementation ``` **Recommendation:** Keep both — `ACTION`/`QUERY`/`PERFORM` are the public "intent" API (semantically loud, used by services). `Action`/`Query`/`Perform` are the implementation methods. Document: services use uppercase, Core internals use lowercase. ### 2. MustServiceFor Uses Panic ```go func MustServiceFor[T any](c *Core, name string) T { panic(...) } ``` RFC-025 says "no hidden panics." `Must` prefix signals it, but the pattern contradicts the Result philosophy. Consider deprecating in favour of `ServiceFor` + `if !ok` pattern. ### 3. Embed() Legacy Accessor ```go func (c *Core) Embed() Result { return c.data.Get("app") } ``` Dead accessor with "use Data()" comment. Should be removed — it's API surface clutter that confuses agents. ### 4. Package-Level vs Core-Level Logging ```go core.Info("msg") // global default logger c.Log().Info("msg") // Core's logger instance ``` Both work. Global functions exist for code without Core access (early init, proc.go helpers). Services with Core access should use `c.Log()`. Document the boundary. ### 5. RegisterAction Lives in task.go IPC registration (`RegisterAction`, `RegisterActions`, `RegisterTask`) is in `task.go` but the dispatch functions (`Action`, `Query`, `QueryAll`) are in `ipc.go`. All IPC should be in one file or the split should follow a clear boundary (dispatch vs registration). ### 6. serviceRegistry Is Unexported `serviceRegistry` is unexported, meaning consumers can't extend service management. Per the Lego Bricks philosophy, this should be exported so downstream packages can build on it. ### 7. No c.Process() Accessor Process management (go-process) should be a Core subsystem accessor like `c.Fs()`, not a standalone service retrieved via `ServiceFor`. Planned for go-process v0.7.0 update. ### 8. NewRuntime / NewWithFactories — Legacy These pre-v0.7.0 functions take `app any` instead of `*Core`. Verify if they're still used — if not, deprecate. ## AX Principles Applied This API follows RFC-025 Agent Experience (AX): 1. **Predictable names** — `Config` not `Cfg`, `Service` not `Srv` 2. **Usage-example comments** — every public function shows HOW with real values 3. **Path is documentation** — `c.Data().ReadString("prompts/coding.md")` 4. **Universal types** — Option, Options, Result everywhere 5. **Event-driven** — ACTION/QUERY/PERFORM, not direct function calls between services 6. **Tests as spec** — `TestFile_Function_{Good,Bad,Ugly}` for every function 7. **Export primitives** — Core is Lego bricks, not an encapsulated library ## Changelog - 2026-03-25: Initial specification — matches v0.7.0 implementation