strata

package module
v1.3.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 13, 2026 License: MIT Imports: 28 Imported by: 0

README

Strata logo

Strata

Build personal automation apps in Go with typed tasks, reusable components, and a CLI-first runtime.

Strata handles routing, auth, storage, secrets, task history, and component lifecycle so your app code can stay focused on automation logic.

Project Status

Strata is usable today for building and running local automation projects, but it is still early and the APIs are still settling.

What is implemented now:

  • The core runtime at the repo root
  • An external host boundary over stdin/stdout via hostio/
  • A working CLI host in cmd/strata/
  • Typed HTTP task registration
  • SQLite-backed storage, task history, and authorization records
  • Keychain-backed secret storage
  • Typed out-of-process components
  • Timed tasks and component-triggered tasks

What is still evolving:

  • The host experience beyond the CLI
  • Permission and capability management UX
  • Sandboxing on non-macOS platforms
  • Long-term API polish for adopters building apps and components

Today, the CLI host is the primary way to run a Strata project.

How Strata Is Organized

Strata is built around four layers:

  1. Host
  2. Server runtime
  3. User tasks
  4. Optional components

Host

The host is the management surface for a Strata app. Hosts run as separate binaries and communicate with the app over stdin/stdout using the typed hostio protocol.

This repository currently ships one host: the CLI in cmd/strata/. It is the default and recommended way to run projects right now. The CLI is responsible for:

  • Building and launching your app binary
  • Receiving runtime logs and registration events
  • Showing authorization tokens
  • Prompting for permission approvals
  • Handling secret and OAuth prompts from components

Server Runtime

The Strata runtime lives at the repository root. It:

  • Registers HTTP task routes under /tasks/{taskName}
  • Decodes request input and serializes responses
  • Verifies auth for protected tasks
  • Stores task history and app data
  • Launches and manages components
  • Emits host events for logs, registration, triggers, and permission requests

User Tasks

Your application code lives in your own Go binary. You register task functions with:

  • strata.NewRouteTask(fn) for authenticated tasks
  • strata.NewPublicRouteTask(fn) for public tasks
  • strata.NewTimedTask(...) for timers
  • strata.NewTriggerTask(...) for component-driven triggers

Tasks receive a *strata.TaskContext, which gives access to a Container for storage, keychain access, logging, permissions, and component calls.

Components

Components are optional third-party subprocesses that Strata launches alongside your app. They communicate with the runtime over the component IPC protocol and are meant for reusable integrations or isolated automation capabilities.

The intended pattern is:

  • Put typed component definitions in a shared package
  • Implement component handlers in the component's main
  • Import those definitions from your app so component calls stay typed

The CLI Workflow

The CLI host is the main entrypoint for running Strata apps today.

Install it with:

go install github.com/jacksonzamorano/strata/cmd/strata@latest

Create a starter project with the embedded templates:

strata new app my-strata-app
strata new component my-component

By default, strata new infers the Go module path from the target directory name. You can override that with --module, and the CLI will run go mod tidy after writing the files.

Then run an app with:

strata run /path/to/my-strata-app --cli

Or, from this repo root:

go run ./cmd/strata run ./strata-example --cli

What that does:

  • Builds the app in ./strata-example
  • Launches it as a child process
  • Connects the CLI host to the app over stdin/stdout
  • Prints logs, registered tasks, registered components, and auth tokens
  • Prompts when the app or a component requests permission or a secret

By default, the app listens on :7700. You can override that with:

  • PORT
  • ADDRESS

Persistence defaults to a local SQLite database named strata.db in the app's working directory. You can override that with DATABASE_URL.

Run The Example App

The example app lives in strata-example/, and the example component it imports lives in component-example/.

Start it with the CLI:

go run ./cmd/strata run ./strata-example --cli

On first run, the CLI will print an authorization token created by the runtime.

Example requests:

Public task:

curl -X POST "http://127.0.0.1:7700/tasks/sayHello?name=Jackson"

Authenticated task:

curl -X POST \
  -H "Authorization: YOUR_TOKEN" \
  "http://127.0.0.1:7700/tasks/getVisitorLog"

Other routes registered by the example include reset and getSecret.

Build Your Own App

A Strata app is a normal Go main package that imports github.com/jacksonzamorano/strata, defines tasks, creates a runtime, and calls Start().

What A Task Is

In Strata, a task is a Go function that the runtime registers and executes for you.

For an HTTP route task, the function signature is:

func myTask(input MyInput, ctx *strata.TaskContext) *strata.RouteResult

What those parameters mean:

  • input is the decoded request payload for the task
  • ctx *strata.TaskContext is the per-run execution context
  • the return value must be *strata.RouteResult

Notes:

  • The input type can be any Go struct or other decodable type
  • JSON request bodies are decoded into the input value
  • Query parameters and headers can also populate fields through struct tags such as query:"name"
  • If a task does not need input, use strata.RouteTaskNoInput
  • The route name is derived from the Go function name, so sayHello becomes /tasks/sayHello

Example with input:

type HelloInput struct {
	Name string `query:"name"`
}

func sayHello(input HelloInput, ctx *strata.TaskContext) *strata.RouteResult {
	return strata.RouteResultSuccess(map[string]any{
		"message": "hello " + input.Name,
	})
}

Example with no input:

func getVisitorLog(input strata.RouteTaskNoInput, ctx *strata.TaskContext) *strata.RouteResult {
	return strata.RouteResultSuccess("ok")
}

Strata also supports non-HTTP tasks with different handler shapes:

  • strata.NewTimedTask(duration, func(ctx *strata.TaskContext))
  • strata.NewTriggerTask(trigger, func(input T, ctx *strata.TaskContext))

TaskContext vs Container Lifetime

TaskContext only exists for the duration of a single task run. You should treat it as ephemeral execution state and not store it for later use.

The container-backed capabilities you access through ctx.Container are the persistent part of the model. In practice, that means data written through container APIs such as storage, entity storage, and keychain is meant to survive across task runs for that namespace.

Use them like this:

  • ctx.Logger for logs during the current run
  • ctx.Container.Storage for persistent key-value state
  • strata.NewEntityStorage[T](ctx.Container) for persistent typed records
  • ctx.Container.Keychain for persistent secrets

1. Create a starter project

Fastest path:

strata new app my-strata-app
cd my-strata-app

Manual path:

mkdir my-strata-app
cd my-strata-app
go mod init github.com/you/my-strata-app
go get github.com/jacksonzamorano/strata

2. Define one or more tasks

package main

import "github.com/jacksonzamorano/strata"

type HelloInput struct {
	Name string `query:"name"`
}

func sayHello(input HelloInput, ctx *strata.TaskContext) *strata.RouteResult {
	ctx.Logger.Log("Saying hello to %s", input.Name)
	return strata.RouteResultSuccess(map[string]any{
		"message": "hello " + input.Name,
	})
}

func main() {
	rt := strata.NewRuntime([]strata.Task{
		strata.NewPublicRouteTask(sayHello),
	}, nil)

	panic(rt.Start())
}

3. Run the app through the CLI host

Using the installed CLI:

strata run /path/to/my-strata-app --cli

Or from this repository's root:

go run ./cmd/strata run /path/to/my-strata-app --cli

That is the primary supported workflow today. Your app should expect to be launched by a host, not run directly as a standalone terminal program.

4. Add more platform features through TaskContext

Inside tasks, prefer using the Strata container APIs instead of reaching directly into the filesystem or process environment:

  • ctx.Container.Storage for key-value state
  • strata.NewEntityStorage[T](ctx.Container) for JSON-backed entity records
  • ctx.Container.Keychain for secrets
  • ctx.Logger for logs
  • ctx.Container.ReadFile(...) when you need host-approved file access

Build Your Own Component

Components are best when you want reusable typed functionality that can be shared across apps.

1. Create a starter project

Fastest path:

strata new component my-component
cd my-component

Manual path:

mkdir my-component
cd my-component
go mod init github.com/you/my-component
go get github.com/jacksonzamorano/strata

2. Define the shared component contract

Put your manifest, request/response types, exported component definitions, and triggers in a package that callers can import.

package definitions

import "github.com/jacksonzamorano/strata/component"

var Manifest = component.ComponentManifest{
	Name:    "example",
	Version: "0.1.0",
}

type EchoRequest struct {
	Message string
}

type EchoResponse struct {
	Message string
}

var Echo = component.Define[EchoRequest, EchoResponse](Manifest, "echo")

3. Implement the component binary

package main

import (
	d "github.com/you/my-component/definitions"
	"github.com/jacksonzamorano/strata/component"
)

func echo(
	input *component.ComponentInput[d.EchoRequest, d.EchoResponse],
	ctx *component.ComponentContainer,
) *component.ComponentReturn[d.EchoResponse] {
	ctx.Logger.Log("Echo called")
	return input.Return(d.EchoResponse{
		Message: input.Body.Message,
	})
}

func main() {
	component.CreateComponent(
		d.Manifest,
		component.Mount(d.Echo, echo),
	).Start()
}

4. Import the component into your app

Your app can import components in a few ways:

  • strata.ImportLocal("/path/to/component-project")
  • strata.ImportBinary("component-binary-name")
  • strata.ImportGit("repo-url")
  • strata.ImportGitSubdirectory("repo-url", "subdir")

Example:

rt := strata.NewRuntime(
	[]strata.Task{
		strata.NewPublicRouteTask(sayHello),
	},
	strata.Import(
		strata.ImportLocal("/path/to/my-component"),
	),
)

Once imported, your tasks can call the component through the shared typed definitions package:

res, ok := definitions.Echo.Execute(ctx.Container, definitions.EchoRequest{
	Message: "hello",
})

Inside component code, prefer the provided context APIs:

  • ctx.Storage
  • ctx.Keychain
  • ctx.Logger
  • ctx.RequestSecret(...)

Security Notes

Current security boundaries are intentionally conservative but still incomplete:

  • Task auth is enforced by Strata route wrappers
  • Some container operations go through host-mediated permission prompts
  • Components run through sandbox-exec on macOS
  • On non-macOS platforms, component execution is still more permissive than the long-term design

The long-term direction is stronger host-managed capability control and tighter component isolation.

Repository Layout

  • ./ - the runtime library root package
  • hostio/ - the typed host IPC contract
  • component/ - the reusable component library
  • cmd/strata/ - the current reference host and primary way to run apps
  • strata-example/ - example Strata app
  • component-example/ - example reusable component
  • sdk/ - schema and generation sources for shared protocol models

Contributing

When extending Strata, preserve the separation between:

  • External hosts
  • The runtime library
  • User-authored apps
  • Third-party components

Avoid designs that push task or component authors toward direct filesystem coupling when a Container or component context API would keep the boundary explicit.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ImportBinary = core.ImportBinary
View Source
var ImportGit = core.ImportGit
View Source
var ImportGitSubdirectory = core.ImportGitSubdirectory
View Source
var ImportLocal = core.ImportLocal

Functions

func AllowAll added in v1.2.1

func AllowAll(nm string, pm core.PermissionAction) core.Permission

func AllowOne added in v1.2.1

func AllowOne(nm string, pm core.PermissionAction, scope string) core.Permission

func Import

func Import(deps ...core.ComponentImport) []core.ComponentImport

func MCPIcon added in v1.3.1

func MCPIcon(filename string) mcpModTask

func MCPInstructions added in v1.3.1

func MCPInstructions(ins string) mcpModTask

func NewMCPTool added in v1.1.0

func NewMCPTool[T any](fn func(input T, t *TaskContext) *MCPToolResult, cfg MCPToolConfig) mcpModTask

Types

type Container

type Container struct {
	Storage    core.Storage
	Keychain   core.Keychain
	StorageDir string
	// contains filtered or unexported fields
}

func (*Container) GetKeychain added in v1.3.0

func (c *Container) GetKeychain() core.Keychain

func (*Container) GetStorage added in v1.3.0

func (c *Container) GetStorage() core.Storage

func (*Container) HasPermission

func (c *Container) HasPermission(act core.PermissionAction, scope string) bool

func (*Container) MakeDirectory added in v1.3.0

func (c *Container) MakeDirectory(name string) bool

func (*Container) Namespace added in v1.3.0

func (c *Container) Namespace() string

func (*Container) ReadFile

func (c *Container) ReadFile(name string) ([]byte, bool)

func (*Container) TemporaryFile added in v1.3.0

func (c *Container) TemporaryFile() string

func (*Container) WriteFile added in v1.3.0

func (c *Container) WriteFile(name string, contents []byte) bool

type ContainerEntityStorage

type ContainerEntityStorage[T any] struct {
	// contains filtered or unexported fields
}

func NewEntityStorage

func NewEntityStorage[T any](c *Container) *ContainerEntityStorage[T]

func (*ContainerEntityStorage[T]) Delete

func (s *ContainerEntityStorage[T]) Delete(id int64)

func (*ContainerEntityStorage[T]) DeleteWhere

func (s *ContainerEntityStorage[T]) DeleteWhere(filter FilterFn[T])

func (*ContainerEntityStorage[T]) Find

func (s *ContainerEntityStorage[T]) Find(filter FilterFn[T]) []T

func (*ContainerEntityStorage[T]) Get

func (s *ContainerEntityStorage[T]) Get(id int64) *T

func (*ContainerEntityStorage[T]) Insert

func (s *ContainerEntityStorage[T]) Insert(record T) int64

func (*ContainerEntityStorage[T]) Update

func (s *ContainerEntityStorage[T]) Update(id int64, record T)

type FilterFn

type FilterFn[T any] = func(v T) bool

type HourSpecificTask

type HourSpecificTask struct {
	// contains filtered or unexported fields
}

func (*HourSpecificTask) Attach

func (tt *HourSpecificTask) Attach(ctx *TaskAttachContext)

type MCPDate added in v1.1.1

type MCPDate struct{ time.Time }

func (*MCPDate) UnmarshalJSON added in v1.1.1

func (d *MCPDate) UnmarshalJSON(b []byte) error

type MCPTask added in v1.1.0

type MCPTask struct {
	Name         string
	Version      string
	Instructions string
	Icon         *mcpTaskIcon
	Tools        map[string]mcpTool
}

func (*MCPTask) Attach added in v1.1.0

func (tt *MCPTask) Attach(ctx *TaskAttachContext)

type MCPToolConfig added in v1.1.0

type MCPToolConfig struct {
	Title       string
	Description string
	ToolType    MCPToolType
}

type MCPToolResult added in v1.1.0

type MCPToolResult struct {
	Response any
	Success  bool
	Error    string
}

func ToolError added in v1.1.1

func ToolError(msg string) *MCPToolResult

func ToolSuccess added in v1.1.1

func ToolSuccess(res any) *MCPToolResult

type MCPToolType added in v1.1.0

type MCPToolType int
const (
	MCPToolTypeDestructive MCPToolType = iota
	MCPToolTypeReadOnly
	MCPToolTypeIdempotent
)

type RouteResult

type RouteResult struct {
	// contains filtered or unexported fields
}

func RouteRequestInvalid

func RouteRequestInvalid(error string) *RouteResult

func RouteResultSuccess

func RouteResultSuccess(data any) *RouteResult

type RouteResultStatus

type RouteResultStatus int
const (
	RouteResultStatusSuccess     RouteResultStatus = 200
	RouteResultStatusBadRequest  RouteResultStatus = 400
	RouteResultStatusServerError RouteResultStatus = 500
)

type RouteTask

type RouteTask struct {
	// contains filtered or unexported fields
}

func (*RouteTask) Attach

func (tt *RouteTask) Attach(ctx *TaskAttachContext)

type RouteTaskFunction

type RouteTaskFunction[T any] func(input T, container *TaskContext) *RouteResult

type RouteTaskNoInput

type RouteTaskNoInput struct{}

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

func NewRuntime

func NewRuntime(tasks []Task, deps []core.ComponentImport, approvedPermissions ...core.Permission) *Runtime

func (*Runtime) Start

func (as *Runtime) Start() error

type StartupTask

type StartupTask struct {
	// contains filtered or unexported fields
}

func (*StartupTask) Attach

func (tt *StartupTask) Attach(ctx *TaskAttachContext)

type Task

type Task struct {
	Name           string
	Implementation TaskImpl
}

func NewMCPTask added in v1.1.0

func NewMCPTask(name, version string, tools ...mcpModTask) Task

func NewPublicRouteTask

func NewPublicRouteTask[T any](fn RouteTaskFunction[T]) Task

func NewRouteTask

func NewRouteTask[T any](fn RouteTaskFunction[T]) Task

func NewStartupTask

func NewStartupTask(handler func(*TaskContext)) Task

func NewTask

func NewTask(fn any, impl TaskImpl) Task

func NewTimeSpecificTask

func NewTimeSpecificTask(hour int, minute int, handler func(*TaskContext)) Task

func NewTimedTask

func NewTimedTask(duration time.Duration, handler func(*TaskContext)) Task

func NewTriggerTask

func NewTriggerTask[T any](trigger component.ComponentTrigger[T], fn TriggerTaskFn[T]) Task

type TaskAttachContext

type TaskAttachContext struct {
	Logger    core.Logger
	Container *Container
	Context   context.Context
	// contains filtered or unexported fields
}

func (*TaskAttachContext) HTTP

func (tac *TaskAttachContext) HTTP(path string, handler http.HandlerFunc)

func (*TaskAttachContext) TaskContext

func (tac *TaskAttachContext) TaskContext(ctx context.Context) *TaskContext

func (*TaskAttachContext) TaskContextGlobal

func (tac *TaskAttachContext) TaskContextGlobal() *TaskContext

func (*TaskAttachContext) Trigger

func (tac *TaskAttachContext) Trigger(ns, name string, body func([]byte))

func (*TaskAttachContext) VerifyAuthentication

func (tac *TaskAttachContext) VerifyAuthentication(secret string) bool

type TaskContext

type TaskContext struct {
	Container *Container
	Logger    core.Logger
	// contains filtered or unexported fields
}

func BuildTaskContext

func BuildTaskContext(container *Container, logger core.Logger, cmps map[string]*runtimecomponent.Runner, ctx context.Context) *TaskContext

func (*TaskContext) ExecuteFunction

func (c *TaskContext) ExecuteFunction(cname, fname string, args any) ([]byte, error)

func (*TaskContext) OpenUrl

func (c *TaskContext) OpenUrl(url string) bool

func (*TaskContext) Run

func (c *TaskContext) Run(maxTime time.Duration, cmd string, args ...string) core.TerminalResult

func (*TaskContext) RunInDirectory

func (c *TaskContext) RunInDirectory(maxTime time.Duration, wd, cmd string, args ...string) core.TerminalResult

type TaskImpl

type TaskImpl interface {
	Attach(ctx *TaskAttachContext)
}

type TimedEveryTask

type TimedEveryTask struct {
	// contains filtered or unexported fields
}

func (*TimedEveryTask) Attach

func (tt *TimedEveryTask) Attach(ctx *TaskAttachContext)

type TriggerTaskFn

type TriggerTaskFn[T any] = func(input T, container *TaskContext)

type TriggeredTask

type TriggeredTask struct {
	// contains filtered or unexported fields
}

func (*TriggeredTask) Attach

func (tt *TriggeredTask) Attach(ctx *TaskAttachContext)

Directories

Path Synopsis
cmd
strata command
Generated by Passport.
Generated by Passport.
Generated by Passport.
Generated by Passport.
internal
componentipc
Generated by Passport.
Generated by Passport.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL