Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ maintainer contact without disclosing vulnerability details.
audited for hostile multi-tenant workloads.
- `-strict` checks that authentication headers are present; it does not
validate credentials or implement authorization.
- The deprecated SQLite backend is single-process and is not a production
target.
- PostgreSQL journals tool attempts, but an external side effect can still be
ambiguous if execution succeeds and its durable result is lost. Exactly-once
behavior requires idempotency from the external system.
Expand Down
84 changes: 4 additions & 80 deletions cmd/managed-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"syscall"
"time"

"github.com/yanpgwang/managed-agent-go/internal/agentruntime"
"github.com/yanpgwang/managed-agent-go/internal/app"
"github.com/yanpgwang/managed-agent-go/internal/controlplane"
"github.com/yanpgwang/managed-agent-go/internal/domain"
Expand All @@ -23,7 +22,6 @@ import (
"github.com/yanpgwang/managed-agent-go/internal/model"
"github.com/yanpgwang/managed-agent-go/internal/pg"
"github.com/yanpgwang/managed-agent-go/internal/sandbox"
"github.com/yanpgwang/managed-agent-go/internal/store"
temporalpkg "github.com/yanpgwang/managed-agent-go/internal/temporal"
)

Expand Down Expand Up @@ -69,11 +67,8 @@ const (
daytonaAutoPauseEnv = "DAYTONA_AUTO_PAUSE_MINUTES"
)

// resolveRuntime returns the self-hosted agent core and whether it is backed by
// a real (network-backed) model. It uses the real Messages API client when
// MANAGED_AGENT_MODEL_BASE_URL and MANAGED_AGENT_MODEL_API_KEY are set,
// otherwise the offline deterministic fake so the binary runs (and tests stay
// offline) with no configuration.
// resolveModelClient returns the worker model client and reports whether it is
// backed by a real, network-connected model.
func resolveModelClient() (client model.Client, realModel bool, err error) {
if client, ok, err := model.AnthropicFromEnv(); err != nil {
return nil, false, err
Expand All @@ -85,14 +80,6 @@ func resolveModelClient() (client model.Client, realModel bool, err error) {
return model.NewFake(), false, nil
}

func resolveRuntime() (rt agentruntime.AgentRuntime, realModel bool, err error) {
client, realModel, err := resolveModelClient()
if err != nil {
return nil, false, err
}
return agentruntime.NewAgentCore(client, domain.NewRandomIDGen()), realModel, nil
}

// sandboxProviderRegistry declares the adapters compiled into this worker.
// Factories are lazy: selecting local never initializes Docker, and future
// optional remote adapters will not require credentials unless selected.
Expand Down Expand Up @@ -313,22 +300,6 @@ func newHTTPServer(addr string, handler http.Handler) *http.Server {
}
}

func buildHandler(db *store.DB, cfg httpapi.Config, rt agentruntime.AgentRuntime, sbx sandbox.Provider) (http.Handler, *app.SessionService) {
ids := domain.NewRandomIDGen()
clk := realClock{}
hub := app.NewHub(256)
es := app.NewEventService(store.NewEventStore(db, ids, clk), hub)
agents := app.NewAgentService(store.NewAgentRepo(db), ids, clk)
envs := app.NewEnvironmentService(store.NewEnvironmentRepo(db), ids, clk)
sessions := app.NewSessionService(store.NewSessionRepo(db), store.NewAgentRepo(db),
store.NewEnvironmentRepo(db), es, store.NewRunStore(db, ids, clk),
rt, sbx, ids, clk)
srv := httpapi.NewServer(httpapi.Deps{
Agents: agents, Envs: envs, Sessions: sessions, Events: es, Hub: hub,
}, cfg)
return srv.Handler(), sessions
}

func main() {
if len(os.Args) < 2 {
log.Fatal("usage: managed-agent <serve|orchestrate> [flags]")
Expand All @@ -346,66 +317,19 @@ func main() {
func runServe() {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
addr := fs.String("addr", defaultAddr, "listen address (default binds to loopback; use e.g. :8080 to expose on all interfaces)")
backend := fs.String("backend", "postgres", "control-plane backend: postgres (default) or sqlite (deprecated compatibility mode)")
dbPath := fs.String("db", "managed-agent.db", "sqlite path (used only with -backend sqlite)")
strict := fs.Bool("strict", false, "require Claude API wire headers (auth, version, beta, content-type) to be present and valid; this is header validation, NOT authentication")
_ = fs.Parse(os.Args[2:])

cfg := httpapi.Config{
RequireBeta: *strict, RequireAuth: *strict, RequireVersion: *strict, RequireContentType: *strict,
}
switch *backend {
case "postgres":
runPostgresAPI(*addr, cfg)
case "sqlite":
log.Printf("startup: -backend sqlite is deprecated compatibility mode; new deployments should use postgres")
runSQLiteAPI(*addr, *dbPath, cfg)
default:
log.Fatalf("serve: unsupported backend %q (want postgres or sqlite)", *backend)
}
}

func runSQLiteAPI(addr, dbPath string, cfg httpapi.Config) {
// resolveRuntime selects the real Messages-API-backed agent core when the
// model env is configured, otherwise the offline deterministic fake.
rt, realModel, err := resolveRuntime()
if err != nil {
log.Fatalf("runtime: %v", err)
}

sbx, localSandbox, err := resolveSandboxProvider()
if err != nil {
log.Fatalf("sandbox: %v", err)
}

// Refuse the unsafe real-model + local-sandbox pairing unless explicitly
// overridden. The zero-config fake + local default is always allowed.
allowUnsafe := os.Getenv(unsafeLocalSandboxEnv) == "1"
if err := guardModelSandbox(realModel, localSandbox, allowUnsafe); err != nil {
log.Fatalf("startup: %v", err)
}

db, err := store.Open(dbPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()

handler, sessions := buildHandler(db, cfg, rt, sbx)
if err := sessions.Recover(context.Background()); err != nil {
log.Printf("recovery: %v", err)
}
serveHTTP(addr, handler)
runPostgresAPI(*addr, cfg)
}

func runPostgresAPI(addr string, cfg httpapi.Config) {
databaseURL := os.Getenv(envDatabaseURL)
if databaseURL == "" {
log.Fatalf(
"serve: %s is required for the default PostgreSQL control plane "+
"(use -backend sqlite only for deprecated compatibility mode)",
envDatabaseURL,
)
log.Fatalf("serve: %s is required", envDatabaseURL)
}
ctx := context.Background()
pool, err := pg.Pool(ctx, databaseURL)
Expand Down
53 changes: 16 additions & 37 deletions cmd/managed-agent/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@ package main
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/yanpgwang/managed-agent-go/internal/agentruntime"
"github.com/yanpgwang/managed-agent-go/internal/httpapi"
"github.com/yanpgwang/managed-agent-go/internal/sandbox"
"github.com/yanpgwang/managed-agent-go/internal/store"
)

// TestResolveSandboxProvider_DefaultsToLocal asserts that, with no
Expand Down Expand Up @@ -122,21 +118,6 @@ func TestSandboxEnvironmentParsersRejectInvalidValues(t *testing.T) {
}
}

func TestResolveRuntime_UsesFakeModelWithoutEnv(t *testing.T) {
t.Setenv("MANAGED_AGENT_MODEL_BASE_URL", "")
t.Setenv("MANAGED_AGENT_MODEL_API_KEY", "")
rt, realModel, err := resolveRuntime()
if err != nil {
t.Fatal(err)
}
if realModel {
t.Fatalf("resolveRuntime realModel=true without model env; want false (offline fake)")
}
if _, ok := rt.(*agentruntime.AgentCore); !ok {
t.Fatalf("runtime = %T, want *agentruntime.AgentCore", rt)
}
}

// TestGuardModelSandbox_Matrix covers the safe-defaults startup guard: a real
// model against the local (non-isolating) sandbox must fail unless the operator
// explicitly opts in via MANAGED_AGENT_ALLOW_UNSAFE_LOCAL_SANDBOX=1. Every other
Expand Down Expand Up @@ -204,37 +185,35 @@ func TestNewHTTPServer_Timeouts(t *testing.T) {
}
}

// TestResolveRuntime_ReportsRealModelWithEnv proves resolveRuntime reports
// TestResolveModelClient_ReportsRealModelWithEnv proves model selection reports
// realModel=true when both the model base URL and API key are configured. It
// performs no network call: resolveRuntime only constructs the client
// (AnthropicFromEnv → NewAnthropic), which does not contact the endpoint. The
// base URL is a non-routable placeholder to make that guarantee explicit.
func TestResolveRuntime_ReportsRealModelWithEnv(t *testing.T) {
// performs no network call: construction does not contact the endpoint.
func TestResolveModelClient_ReportsRealModelWithEnv(t *testing.T) {
t.Setenv("MANAGED_AGENT_MODEL_BASE_URL", "https://model.invalid")
t.Setenv("MANAGED_AGENT_MODEL_API_KEY", "sk-test")
rt, realModel, err := resolveRuntime()
client, realModel, err := resolveModelClient()
if err != nil {
t.Fatal(err)
}
if !realModel {
t.Fatalf("resolveRuntime realModel=false with model env configured; want true")
t.Fatalf("resolveModelClient realModel=false with model env configured; want true")
}
if _, ok := rt.(*agentruntime.AgentCore); !ok {
t.Fatalf("runtime = %T, want *agentruntime.AgentCore", rt)
if client == nil {
t.Fatal("resolveModelClient returned a nil client")
}
}

func TestBuildHandler_Health(t *testing.T) {
db, err := store.OpenMemory()
func TestResolveModelClient_UsesFakeWithoutEnv(t *testing.T) {
t.Setenv("MANAGED_AGENT_MODEL_BASE_URL", "")
t.Setenv("MANAGED_AGENT_MODEL_API_KEY", "")
client, realModel, err := resolveModelClient()
if err != nil {
t.Fatal(err)
}
defer db.Close()
h, _ := buildHandler(db, httpapi.Config{}, agentruntime.NewFake(), sandbox.NewLocalProvider())
ts := httptest.NewServer(h)
defer ts.Close()
resp, err := ts.Client().Get(ts.URL + "/healthz")
if err != nil || resp.StatusCode != 200 {
t.Fatalf("healthz: %v status=%v", err, resp)
if realModel {
t.Fatal("resolveModelClient realModel=true without model configuration")
}
if client == nil {
t.Fatal("resolveModelClient returned a nil fake client")
}
}
6 changes: 2 additions & 4 deletions cmd/managed-agent/orchestrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ func runOrchestrate() {
store.SetEventNotifier(broker)
log.Printf("orchestrate: NATS live channel connected")

// The execution plane uses the same model selection as the SQLite path. New
// Workflow executions call it through granular model/tool Activities; the
// runtime also retains AgentCore internally for replaying older RunTurn
// histories. The offline fake model is sufficient with no configuration.
// Workflow executions call the selected model through granular model/tool
// Activities. The offline fake model needs no configuration.
modelClient, realModel, err := resolveModelClient()
if err != nil {
log.Fatalf("orchestrate: runtime: %v", err)
Expand Down
6 changes: 2 additions & 4 deletions docs/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ shape; persisted events receive an `id` and `processed_at`.
The list must be non-empty. Clients cannot provide `id` or `processed_at`, and
server-emitted event types are rejected.

The default PostgreSQL/Temporal backend currently accepts:
The PostgreSQL/Temporal control plane currently accepts:

| Event | Current behavior |
| --- | --- |
Expand All @@ -34,9 +34,7 @@ The default PostgreSQL/Temporal backend currently accepts:
| `user.define_outcome` | Stored and validated |

Generic `user.tool_result`, `system.message`, and targeted multi-agent interrupt
shapes return `422 unsupported_error` on the primary backend. The deprecated
`serve -backend sqlite` compatibility mode is frozen and remains only for
transition comparison.
shapes return `422 unsupported_error`.

An interrupt is first committed to PostgreSQL and then delivered to the
Session Workflow as a metadata-only wakeup. An interrupt that commits before
Expand Down
11 changes: 5 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Compose stack runs those roles separately; they can also be packaged in one
deployment for development.

The selected stack is Temporal orchestration, PostgreSQL, NATS Core, and
replaceable sandbox providers. Object storage and provider-backed sandbox
leases are not implemented.
replaceable sandbox providers. Provider sandbox bindings are persisted in
PostgreSQL; object storage is not implemented.

```mermaid
flowchart LR
Expand Down Expand Up @@ -77,16 +77,15 @@ model vendor, sandbox backend, or worker topology.
| --- | --- |
| `cmd/managed-agent` | Composition root, configuration, process lifecycle |
| `internal/httpapi` | HTTP routes, strict validation, DTO mapping, SSE |
| `internal/app` | Shared resource validation and legacy compatibility services |
| `internal/app` | Shared resource validation and transport-neutral use-case types |
| `internal/controlplane` | PostgreSQL-backed public Session/Event use cases |
| `internal/domain` | Resource, event, message, tool, and run semantics |
| `internal/pg` | PostgreSQL repositories, ledger, outbox, and tool journal |
| `internal/temporal` | Session Workflow, Activities, worker, and relay |
| `internal/live` | NATS wakeups/previews plus PostgreSQL cursor reconciliation |
| `internal/store` | Deprecated SQLite compatibility implementation |
| `internal/agentruntime` | Model/tool orchestration behind `AgentRuntime` |
| `internal/agentruntime` | Reusable model, message, and tool execution primitives |
| `internal/model` | Offline and Messages API model clients |
| `internal/sandbox` | Provider registry, lifecycle contract, and local/Docker adapters |
| `internal/sandbox` | Provider registry, lifecycle contract, and local/remote adapters |

The dependency direction points inward: transport and infrastructure depend on
application/domain semantics, while the domain has no HTTP, SQL, model-client,
Expand Down
11 changes: 5 additions & 6 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@ compatibility.
| Multi-agent orchestration | Not supported | `multiagent` configuration can be stored, but rosters, threads, delegation, and orchestration are not executed. |
| Distributed workers | Limited | API and Temporal worker roles are separate and can be replicated around PostgreSQL/NATS. Sandbox ownership is durable: local and Docker references require workers connected to the same host filesystem or daemon, while remote references can reattach from workers sharing the same provider configuration. Task queues must remain configuration-homogeneous. Worker Versioning, production manifests, and rollout tests remain open. |

## Backend transition
## Runtime architecture

`serve` defaults to the PostgreSQL control plane. `serve -backend sqlite`
temporarily retains the former single-process behavior for transition
comparison. This is not a commitment to maintain two backends: SQLite is frozen
and will be removed after the PostgreSQL/Temporal conformance gate passes.
`serve` has one authoritative control-plane path: PostgreSQL for resources,
events, projections, and admission; Temporal for durable orchestration; and
NATS Core for ephemeral wakeups and previews.

## Integration contract

Expand All @@ -76,7 +75,7 @@ the behavior:

- raw HTTP tests for JSON shapes, status codes, headers, and validation;
- official Go SDK tests for client interoperability;
- application and store tests for durable execution semantics;
- application and PostgreSQL tests for durable execution semantics;
- end-to-end tests for runtime, tool, interrupt, and streaming workflows.

Test names and edge-case details live beside the implementation and in the
Expand Down
12 changes: 0 additions & 12 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,3 @@ make local-down
This keeps the PostgreSQL volume. Add `VOLUMES=1` only when you intentionally
want to delete local data. PostgreSQL schema changes are applied by embedded,
versioned `goose` migrations when API or worker processes start.

## Temporary SQLite compatibility mode

The former single-process runtime remains available during the final
PostgreSQL/Temporal conformance transition:

```bash
go run ./cmd/managed-agent serve -backend sqlite -db managed-agent.db
```

It is a frozen comparison/rollback path, not a production backend. New
deployments should use the PostgreSQL/Temporal stack above.
9 changes: 5 additions & 4 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ messages and untargeted interrupts; cursor pagination and SSE; a multi-round
model loop; durable custom-tool and `always_ask` waits; six `always_allow`
built-ins; local and Docker sandboxes; and opt-in assistant text previews.

The remaining transition gate before removing the deprecated SQLite comparison
backend is black-box conformance coverage for the supported PostgreSQL/Temporal
surface. MCP execution, files/skills/memory/vaults, multi-agent orchestration,
remote self-hosted workers, schedules, and webhooks remain future product work.
The remaining infrastructure closure work is autonomous recovery for interrupted
session deletion, sandbox orphan reconciliation, and repeatable black-box
conformance for the PostgreSQL/Temporal path. MCP execution,
files/skills/memory/vaults, multi-agent orchestration, remote self-hosted
workers, schedules, and webhooks remain future product work.

The runtime is the product; Claude API compatibility is an integration surface.
Public behavior is derived from official documentation and tested through raw
Expand Down
8 changes: 5 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ not by reproducing Anthropic's internal implementation.

These are the release-blocking capabilities for the managed-agent harness:

1. conformance tests for supported Managed Agents API behavior and removal of
the frozen SQLite comparison backend;
2. bounded context management and compaction over server-owned history.
1. autonomous recovery for interrupted deletion plus sandbox orphan
reconciliation;
2. repeatable black-box conformance for the supported PostgreSQL/Temporal and
external-sandbox paths;
3. bounded context management and compaction over server-owned history.

Durable cross-process interrupt, deterministic finish-vs-interrupt ordering,
Sandbox identity, worker-restart reattachment, and deletion cleanup are durable
Expand Down
Loading