devx

package
v0.7.3 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ScopeObserve = "dev:observe"
	ScopeOperate = "dev:operate"
)

ScopeObserve gates read-only dev endpoints (logs, job list/get, doctor); ScopeOperate gates mutating ones (job retry/cancel). Both follow the PAT scope model (webx.CredentialScope): non-PAT principals (browser sessions) pass untouched, PATs must carry the scope.

Variables

View Source
var (
	// ErrNotWired means the job service was built without a river client
	// (events not constructed) — the surface is present but inert.
	ErrNotWired = errors.New("devx: job service not wired (events not constructed)")
	// ErrJobNotFound means no river job with that id exists.
	ErrJobNotFound = errors.New("devx: job not found")
	// ErrValidation wraps input-shape errors.
	ErrValidation = errors.New("devx: validation")
	// ErrScopeDenied is returned to MCP tool calls whose PAT lacks the
	// required dev scope (the HTTP layer answers 403 via webx.RequireScope
	// before reaching handlers, so only the MCP path surfaces this).
	ErrScopeDenied = errors.New("devx: scope denied")
)

Sentinel errors surfaced by the service; the HTTP layer and MCP tools map them to statuses/codes once, here.

Functions

func Mount

func Mount(mux webx.Router, deps Deps)

Mount registers the dev endpoints on the router (webx.Mux or a plain *http.ServeMux — both satisfy webx.Router):

GET  /dev/logs?request_id=&level=&limit=
GET  /dev/jobs?state=&queue=&kind=&limit=
GET  /dev/jobs/{id}
POST /dev/jobs/{id}/retry
POST /dev/jobs/{id}/cancel
GET  /dev/doctor

Auth is NOT applied here — mount behind webx.Authenticate + webx.RequireScope(ScopeObserve / ScopeOperate) so PAT callers are scoped. The intended wiring gates the whole /dev/ prefix at observe level and re-gates the mutating subset at operate level.

func RegisterTools

func RegisterTools(s *mcp.Server, deps *Deps)

RegisterTools installs the devx built-in tools on an MCP server. Products call this from their mcpx register callback with the same Deps instance Mount received, so both surfaces (HTTP /dev/* and MCP tools) share one service layer and stay behaviorally identical.

Scope enforcement: the mount gates /mcp with webx.Authenticate; in stateless mode the HTTP request context (principal included) is the tool handler's ctx, so tools re-check the caller's PAT scope exactly like the HTTP routes do via webx.RequireScope — session callers pass, PAT callers must carry dev:observe / dev:operate.

Types

type AuditHook

type AuditHook func(r *http.Request, action, target string)

AuditHook is the optional audit sink for mutating dev operations. It is a hook, not an import: platform packages cannot reach the audit domain, so the product injects its Auditor here (mounted endpoints do not auto-audit).

type Check

type Check struct {
	// Name is a stable machine identifier (e.g. "river_schema").
	Name string
	// Summary is a one-line description of what the check verifies.
	Summary string
	// Run executes the check. Timeout is enforced by the registry; a check
	// that outlives it reports Fail with a timeout symptom.
	Run func(ctx context.Context) Result
}

Check is one named self-diagnostic. A check answers a question the agent would otherwise have to reverse-engineer from a confusing failure ("is the river schema even migrated?"); it returns Pass with a human-readable detail, or Fail with the symptom AND the fix.

func CookieSecureCheck

func CookieSecureCheck(secureCookie, production bool) Check

CookieSecureCheck catches the production trap of Secure=false cookies behind HTTPS (session cookies leaked over plain HTTP) and the inverse dev trap of forcing Secure on local HTTP (logins fail with an opaque CSRF error).

func DrainWindowCheck

func DrainWindowCheck(drain, softStop time.Duration) Check

DrainWindowCheck verifies the graceful-shutdown drain window covers the events soft-stop window (SoftStopTimeout + margin). A shorter Drain force-kills in-flight event handlers mid-work.

func HealthProbesCheck

func HealthProbesCheck(readyChecks int) Check

HealthProbesCheck verifies the readiness endpoint actually probes dependencies (a bare-200 readiness check stays green through a database outage and the LB keeps routing to a dead instance).

func RiverSchemaCheck

func RiverSchemaCheck(conn *sql.DB) Check

RiverSchemaCheck verifies the river schema was migrated (events.Migrate was called at boot). Symptom when skipped: the first transactional event dies with `relation "river_job" does not exist`, two layers away from the cause. conn is a plain *sql.DB (or anything running SQL) — devx does not depend on pgx directly here so any driver works.

func SealKeyCheck

func SealKeyCheck(keyEnv string) Check

SealKeyCheck verifies tenant-credential encryption is configured. Without PLOYKIT_SEAL_KEY, new secret writes are rejected and legacy plaintext rows error on read — surfaced to the product as a confusing runtime failure, never as a boot error.

type Deps

type Deps struct {
	// Logs serves /dev/logs. Optional; nil → endpoint reports not enabled.
	Logs *LogService
	// Jobs serves /dev/jobs. Optional; nil → endpoints report not wired.
	Jobs *JobService
	// Doctor serves /dev/doctor. Required for that endpoint; nil → not found.
	Doctor *Doctor
	// Audit, when set, records mutating operations (retry/cancel).
	Audit AuditHook
	// Log is used for the platform's own operational lines (mount, denials).
	Log *slog.Logger
}

Deps wires the dev surface.

type Doctor

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

Doctor is the check registry. Products register framework checks via the constructor helpers plus their own domain-specific ones; RunDoctor executes all of them concurrently with a per-check timeout.

func NewDoctor

func NewDoctor() *Doctor

NewDoctor returns an empty registry.

func (*Doctor) Register

func (d *Doctor) Register(c Check)

Register adds a check. Duplicate names panic — a duplicated check name is a wiring bug, and silent overwrite would hide one of the two diagnostics.

func (*Doctor) RunDoctor

func (d *Doctor) RunDoctor(ctx context.Context, perCheckTimeout time.Duration) doctorResult

RunDoctor executes every registered check concurrently (per-check timeout, results sorted by name for stable output) and aggregates.

type JobAttemptError

type JobAttemptError struct {
	At      time.Time `json:"at"`
	Error   string    `json:"error"`
	TraceID string    `json:"trace_id,omitempty"`
}

JobAttemptError mirrors rivertype.AttemptError for JSON output.

type JobDetail

type JobDetail struct {
	JobSummary
	Args json.RawMessage `json:"args"`
}

JobDetail adds the arguments payload to the summary — the single most valuable field when an agent needs to understand what a failed job was trying to do.

type JobService

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

JobService wraps the river client with the list/get/retry/cancel verbs used by both the HTTP surface and the MCP tools.

func NewJobService

func NewJobService(client *river.Client[pgxTx]) *JobService

NewJobService builds the service over an events emitter's client. A nil client (events not wired) yields a service whose queries return ErrNotWired — surfaces gate on Available.

func (*JobService) Available

func (s *JobService) Available() bool

Available reports whether a river client backs this service.

func (*JobService) Cancel

func (s *JobService) Cancel(ctx context.Context, id int64) error

Cancel cancels a job: queued jobs immediately, running jobs cooperatively (via context cancellation inside the worker). Already-finalized jobs error.

func (*JobService) Get

func (s *JobService) Get(ctx context.Context, id int64) (*JobDetail, error)

Get returns one job with its args payload.

func (*JobService) List

func (s *JobService) List(ctx context.Context, state, queue, kind string, limit int) ([]JobSummary, error)

List returns jobs filtered by state/queue/kind, ordered by ID descending (newest first). Default and maximum page size: 50.

func (*JobService) Retry

func (s *JobService) Retry(ctx context.Context, id int64) error

Retry makes a job immediately available to run again (running jobs are left untouched; MaxAttempts is incremented when already exhausted).

type JobSummary

type JobSummary struct {
	ID          int64             `json:"id"`
	Kind        string            `json:"kind"`
	Queue       string            `json:"queue"`
	State       string            `json:"state"`
	Tags        []string          `json:"tags,omitempty"`
	Attempt     int               `json:"attempt"`
	MaxAttempts int               `json:"max_attempts"`
	ScheduledAt time.Time         `json:"scheduled_at"`
	FinalizedAt *time.Time        `json:"finalized_at,omitempty"`
	Errors      []JobAttemptError `json:"errors,omitempty"`
}

JobSummary is the list-view shape of a river job.

type LogService

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

LogService adapts a logring Ring for the HTTP surface and MCP tools. A nil ring (logring not wired) yields Available=false and the surfaces answer with a clear degradation message instead of empty results.

func NewLogService

func NewLogService(ring *logring.Ring) *LogService

NewLogService wraps a ring.

func (*LogService) Available

func (s *LogService) Available() bool

Available reports whether a ring backs this service.

func (*LogService) ByRequestID

func (s *LogService) ByRequestID(requestID string, minLevel slog.Level, limit int) []logring.Entry

ByRequestID returns the captured entries for one request, newest last.

func (*LogService) Tail

func (s *LogService) Tail(minLevel slog.Level, limit int) []logring.Entry

Tail returns the most recent entries across all requests, newest last.

type Result

type Result struct {
	Pass    bool   `json:"pass"`
	Detail  string `json:"detail"`
	FixHint string `json:"fix_hint,omitempty"`
}

Result is the outcome of one check.

Jump to

Keyboard shortcuts

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