Files
weichen 2de4eda16d feat(workspace): register workspaces and gate cleanup by receipts
Track workspace size and age, expose an admin list and cleanup route, and refuse TTL or quota deletion until a frozen manifest matches verified receipts.
2026-09-21 14:58:21 +00:00

113 lines
3.1 KiB
Go

package workspace
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// RegistryDDL is the table used by PGRegistry. Goose migration 00062 is the deploy path.
const RegistryDDL = `
CREATE TABLE IF NOT EXISTS agentx_workspace_registry (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL DEFAULT '',
bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL,
last_seen TIMESTAMPTZ NOT NULL,
frozen JSONB,
receipts JSONB NOT NULL DEFAULT '[]'::jsonb
)`
// PGRegistry persists workspace records in Postgres.
type PGRegistry struct {
pool *pgxpool.Pool
}
func NewPGRegistry(pool *pgxpool.Pool) *PGRegistry {
return &PGRegistry{pool: pool}
}
func (p *PGRegistry) Ensure(ctx context.Context) error {
_, err := p.pool.Exec(ctx, RegistryDDL)
return err
}
func (p *PGRegistry) Upsert(ctx context.Context, rec Record) error {
if err := validateID(rec.ID); err != nil {
return err
}
if rec.CreatedAt.IsZero() {
rec.CreatedAt = time.Now().UTC()
}
if rec.LastSeen.IsZero() {
rec.LastSeen = rec.CreatedAt
}
frozen, err := json.Marshal(rec.Frozen)
if err != nil {
return err
}
if rec.Frozen == nil {
frozen = []byte("null")
}
receipts, err := json.Marshal(rec.Receipts)
if err != nil {
return err
}
if rec.Receipts == nil {
receipts = []byte("[]")
}
_, err = p.pool.Exec(ctx, `
INSERT INTO agentx_workspace_registry (id, run_id, bytes, created_at, last_seen, frozen, receipts)
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb)
ON CONFLICT (id) DO UPDATE SET
run_id=EXCLUDED.run_id, bytes=EXCLUDED.bytes, last_seen=EXCLUDED.last_seen,
frozen=EXCLUDED.frozen, receipts=EXCLUDED.receipts`,
rec.ID, rec.RunID, rec.Bytes, rec.CreatedAt, rec.LastSeen, frozen, receipts)
return err
}
func (p *PGRegistry) Get(ctx context.Context, id string) (Record, bool, error) {
row := p.pool.QueryRow(ctx, `SELECT id, run_id, bytes, created_at, last_seen, frozen, receipts FROM agentx_workspace_registry WHERE id=$1`, id)
rec, err := scanRecord(row)
if err == pgx.ErrNoRows {
return Record{}, false, nil
}
if err != nil {
return Record{}, false, err
}
return rec, true, nil
}
func scanRecord(row pgx.Row) (Record, error) {
var rec Record
var frozen, receipts []byte
if err := row.Scan(&rec.ID, &rec.RunID, &rec.Bytes, &rec.CreatedAt, &rec.LastSeen, &frozen, &receipts); err != nil {
return Record{}, err
}
if len(frozen) > 0 && string(frozen) != "null" {
rec.Frozen = &FrozenManifest{}
if err := json.Unmarshal(frozen, rec.Frozen); err != nil {
return Record{}, err
}
}
if len(receipts) > 0 {
if err := json.Unmarshal(receipts, &rec.Receipts); err != nil {
return Record{}, err
}
}
return rec, nil
}
// TouchSchema creates an isolated schema for tests and points the pool at it.
func TouchSchema(ctx context.Context, pool *pgxpool.Pool, schema string) error {
if _, err := pool.Exec(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schema)); err != nil {
return err
}
_, err := pool.Exec(ctx, fmt.Sprintf("SET search_path TO %s", schema))
return err
}