go-process/docs/RFC.md

8.5 KiB

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.goNewService(opts) func(*Core) (any, error)old factory signature. Change to Register(c *Core) core.Result
  • OnStartup() error / OnShutdown() errorChange 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]*ManagedProcessReplace 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

// 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

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

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

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

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

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

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

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().

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:

# 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.