# go-process API Contract — RFC Specification > `dappco.re/go/core/process` — Managed process execution for the Core ecosystem. > This package is the ONLY package that imports `os/exec`. Everything else uses > `c.Process()` which delegates to Actions registered by this package. **Status:** v0.8.0 **Module:** `dappco.re/go/core/process` **Depends on:** core/go v0.8.0 --- ## 1. Purpose go-process provides the implementation behind `c.Process()`. Core defines the primitive (Section 17). go-process registers the Action handlers that make it work. ``` core/go defines: c.Process().Run(ctx, "git", "log") → calls c.Action("process.run").Run(ctx, opts) go-process provides: c.Action("process.run", s.handleRun) → actually executes the command via os/exec ``` Without go-process registered, `c.Process().Run()` returns `Result{OK: false}`. Permission-by-registration. ### Current State (2026-03-30) The codebase now matches the v0.8.0 target. The bullets below are the historical migration delta that was closed out: - `service.go` — `NewService(opts) func(*Core) (any, error)` — **old factory signature**. Change to `Register(c *Core) core.Result` - `OnStartup() error` / `OnShutdown() error` — **Change** to return `core.Result` - `process.SetDefault(svc)` global singleton — **Remove**. Service registers in Core conclave - Own ID generation `fmt.Sprintf("proc-%d", ...)` — **Replace** with `core.ID()` - Custom `map[string]*ManagedProcess` — **Replace** with `core.Registry[*ManagedProcess]` - No named Actions registered — **Add** `process.run/start/kill/list/get` during OnStartup ### File Layout ``` service.go — main service (factory, lifecycle, process execution) registry.go — daemon registry (PID files, health, restart) daemon.go — DaemonEntry, managed daemon lifecycle health.go — health check endpoints pidfile.go — PID file management buffer.go — output buffering actions.go — Action payloads and Core action handlers global.go — global Default() singleton — DELETE after migration ``` --- ## 2. Registration ```go // Register is the WithService factory. // // core.New(core.WithService(process.Register)) func Register(c *core.Core) core.Result { svc := &Service{ ServiceRuntime: core.NewServiceRuntime(c, Options{}), managed: core.NewRegistry[*ManagedProcess](), } return core.Result{Value: svc, OK: true} } ``` ### OnStartup — Register Actions ```go func (s *Service) OnStartup(ctx context.Context) core.Result { c := s.Core() c.Action("process.run", s.handleRun) c.Action("process.start", s.handleStart) c.Action("process.kill", s.handleKill) c.Action("process.list", s.handleList) c.Action("process.get", s.handleGet) return core.Result{OK: true} } ``` ### OnShutdown — Kill Managed Processes ```go func (s *Service) OnShutdown(ctx context.Context) core.Result { s.managed.Each(func(id string, p *ManagedProcess) { p.Kill() }) return core.Result{OK: true} } ``` --- ## 3. Action Handlers ### process.run — Synchronous Execution ```go func (s *Service) handleRun(ctx context.Context, opts core.Options) core.Result { command := opts.String("command") args, _ := opts.Get("args").Value.([]string) dir := opts.String("dir") env, _ := opts.Get("env").Value.([]string) cmd := exec.CommandContext(ctx, command, args...) if dir != "" { cmd.Dir = dir } if len(env) > 0 { cmd.Env = append(os.Environ(), env...) } output, err := cmd.CombinedOutput() if err != nil { return core.Result{Value: err, OK: false} } return core.Result{Value: string(output), OK: true} } ``` > Note: go-process is the ONLY package allowed to import `os` and `os/exec`. ### process.start — Detached/Background ```go func (s *Service) handleStart(ctx context.Context, opts core.Options) core.Result { command := opts.String("command") args, _ := opts.Get("args").Value.([]string) cmd := exec.Command(command, args...) cmd.Dir = opts.String("dir") if err := cmd.Start(); err != nil { return core.Result{Value: err, OK: false} } id := core.ID() managed := &ManagedProcess{ ID: id, PID: cmd.Process.Pid, Command: command, cmd: cmd, done: make(chan struct{}), } s.managed.Set(id, managed) go func() { cmd.Wait() close(managed.done) managed.ExitCode = cmd.ProcessState.ExitCode() }() return core.Result{Value: id, OK: true} } ``` ### process.kill — Terminate by ID or PID ```go func (s *Service) handleKill(ctx context.Context, opts core.Options) core.Result { id := opts.String("id") if id != "" { r := s.managed.Get(id) if !r.OK { return core.Result{Value: core.E("process.kill", core.Concat("not found: ", id), nil), OK: false} } r.Value.(*ManagedProcess).Kill() return core.Result{OK: true} } pid := opts.Int("pid") if pid > 0 { proc, err := os.FindProcess(pid) if err != nil { return core.Result{Value: err, OK: false} } proc.Signal(syscall.SIGTERM) return core.Result{OK: true} } return core.Result{Value: core.E("process.kill", "need id or pid", nil), OK: false} } ``` ### process.list / process.get ```go func (s *Service) handleList(ctx context.Context, opts core.Options) core.Result { return core.Result{Value: s.managed.Names(), OK: true} } func (s *Service) handleGet(ctx context.Context, opts core.Options) core.Result { id := opts.String("id") r := s.managed.Get(id) if !r.OK { return r } return core.Result{Value: r.Value.(*ManagedProcess).Info(), OK: true} } ``` --- ## 4. ManagedProcess ```go type ManagedProcess struct { ID string PID int Command string ExitCode int StartedAt time.Time cmd *exec.Cmd done chan struct{} } func (p *ManagedProcess) IsRunning() bool { select { case <-p.done: return false default: return true } } func (p *ManagedProcess) Kill() { if p.cmd != nil && p.cmd.Process != nil { p.cmd.Process.Signal(syscall.SIGTERM) } } func (p *ManagedProcess) Done() <-chan struct{} { return p.done } func (p *ManagedProcess) Info() ProcessInfo { return ProcessInfo{ ID: p.ID, PID: p.PID, Command: p.Command, Running: p.IsRunning(), ExitCode: p.ExitCode, StartedAt: p.StartedAt, } } ``` --- ## 5. Daemon Registry Higher-level abstraction over `process.start`: ``` process.start → low level: start a command, get a handle daemon.Start → high level: PID file, health endpoint, restart policy, signals ``` Daemon registry uses `core.Registry[*DaemonEntry]`. --- ## 6. Error Handling All errors via `core.E()`. String building via `core.Concat()`. ```go return core.Result{Value: core.E("process.run", core.Concat("command failed: ", command), err), OK: false} ``` --- ## 7. Test Strategy AX-7: `TestFile_Function_{Good,Bad,Ugly}` ``` TestService_Register_Good — factory returns Result TestService_OnStartup_Good — registers 5 Actions TestService_HandleRun_Good — runs command, returns output TestService_HandleRun_Bad — command not found TestService_HandleRun_Ugly — timeout via context TestService_HandleStart_Good — starts detached, returns ID TestService_HandleStart_Bad — invalid command TestService_HandleKill_Good — kills by ID TestService_HandleKill_Bad — unknown ID TestService_HandleList_Good — returns managed process IDs TestService_OnShutdown_Good — kills all managed processes TestService_Ugly_PermissionModel — no go-process = c.Process().Run() fails ``` --- ## 8. Quality Gates go-process is the ONE exception — it imports `os` and `os/exec` because it IS the process primitive. All other disallowed imports still apply: ```bash # Should only find os/exec in service.go, os in service.go grep -rn '"os"\|"os/exec"' *.go | grep -v _test.go # No other disallowed imports grep -rn '"io"\|"fmt"\|"errors"\|"log"\|"encoding/json"\|"path/filepath"\|"unsafe"\|"strings"' *.go \ | grep -v _test.go ``` --- ## Consumer RFCs | Package | RFC | Role | |---------|-----|------| | core/go | `core/go/docs/RFC.md` | Primitives — Process primitive (Section 17) | | core/agent | `core/agent/docs/RFC.md` | Consumer — `c.Process().RunIn()` for git/build ops | --- ## Changelog - 2026-03-25: v0.8.0 spec — written with full core/go domain context.