Documentation
¶
Overview ¶
Package api is a thin HTTP client for Ronja's /api/v2 surface.
It is deliberately hand-rolled rather than generated from the OpenAPI spec: the CLI touches a handful of endpoints, and a generated client would drag in a code-generation step and a dependency for no benefit at this size.
Index ¶
- Constants
- Variables
- func CodeOf(err error) string
- func DescribeLifecycle(lifecycle string) string
- func IsTimeout(err error) bool
- func SameAccess(a, b DataAppAccess) bool
- func StatusOf(err error) int
- type Client
- func (c *Client) ApplyMigrations(ctx context.Context, databaseID string, in MigrationSetInput, ...) (*MigrationSetResult, error)
- func (c *Client) CheckoutDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
- func (c *Client) CheckoutWorkflow(ctx context.Context, id string) (*Workflow, error)
- func (c *Client) CommitDataAppDraft(ctx context.Context, draftID string) (*DataAppCommitOutcome, error)
- func (c *Client) CommitWorkflowDraft(ctx context.Context, draftID, confirmHeadVersionID string) error
- func (c *Client) CreateDataApp(ctx context.Context, in CreateDataAppInput) (*DataApp, error)
- func (c *Client) CreateWorkflow(ctx context.Context, in CreateWorkflowInput) (*Workflow, error)
- func (c *Client) DatabaseSQL(ctx context.Context, databaseID string, in DatabaseSQLInput, ...) (*DatabaseSQLResult, error)
- func (c *Client) DeleteDataApp(ctx context.Context, id string) error
- func (c *Client) DeleteDataAppFile(ctx context.Context, id, path string) (*DataAppFileDeleteResponse, error)
- func (c *Client) DeleteWorkflow(ctx context.Context, id string) error
- func (c *Client) DeleteWorkflowFile(ctx context.Context, id, path string, baseSha256 *string) (*FileDeleteResponse, error)
- func (c *Client) DiscardDataAppDraft(ctx context.Context, draftID string) error
- func (c *Client) DiscardWorkflowDraft(ctx context.Context, draftID string) error
- func (c *Client) Do(ctx context.Context, method, path string, body, out any) error
- func (c *Client) DoRaw(ctx context.Context, method, path string, header http.Header, body []byte, ...) (*RawResponse, error)
- func (c *Client) GetDataApp(ctx context.Context, id string) (*DataApp, error)
- func (c *Client) GetDataAppDraft(ctx context.Context, id string) (*DataApp, error)
- func (c *Client) GetDataAppFile(ctx context.Context, id, path string) (*DataAppFile, error)
- func (c *Client) GetFeature(ctx context.Context, id string) (*Feature, error)
- func (c *Client) GetTable(ctx context.Context, id string) (*Table, error)
- func (c *Client) GetText(ctx context.Context, path string) (string, error)
- func (c *Client) GetWorkflow(ctx context.Context, id string) (*Workflow, error)
- func (c *Client) GetWorkflowDraft(ctx context.Context, id string) (*Workflow, error)
- func (c *Client) GetWorkflowFile(ctx context.Context, id, path string) (*WorkflowFile, error)
- func (c *Client) GetWorkflowRun(ctx context.Context, runID string) (*RunResponse, error)
- func (c *Client) HeadVersionID(ctx context.Context, parentID string) (string, error)
- func (c *Client) ListDataAppFiles(ctx context.Context, id string) ([]DataAppFile, error)
- func (c *Client) ListWorkflowFiles(ctx context.Context, id string) ([]WorkflowFile, error)
- func (c *Client) ListWorkflowVersions(ctx context.Context, parentID string) ([]Workflow, error)
- func (c *Client) Me(ctx context.Context) (*Me, error)
- func (c *Client) PollDevice(ctx context.Context, start *DeviceStart) (*DeviceToken, error)
- func (c *Client) PublishWorkflowDraft(ctx context.Context, draftID string) error
- func (c *Client) PutDataAppFile(ctx context.Context, id, path, content string) (*DataAppFileSaveResponse, error)
- func (c *Client) PutWorkflowFile(ctx context.Context, id, path, content string, baseSha256 *string) (*FileSaveResponse, error)
- func (c *Client) Query(ctx context.Context, in QueryInput, timeout time.Duration) (*QueryResult, error)
- func (c *Client) RequestDataAppReview(ctx context.Context, draftID string) error
- func (c *Client) RequestWorkflowReview(ctx context.Context, draftID string) error
- func (c *Client) RunWorkflow(ctx context.Context, workflowID string, parameterValues map[string]any) (*WorkflowRun, error)
- func (c *Client) StartDevice(ctx context.Context, clientName string) (*DeviceStart, error)
- func (c *Client) UpdateDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
- func (c *Client) UpdateWorkflow(ctx context.Context, id string, patch WorkflowPatch) error
- func (c *Client) ValidateDataApp(ctx context.Context, draftID string) (*DataApp, error)
- func (c *Client) ValidateWorkflowFiles(ctx context.Context, in ValidateInput) (*ValidateResult, error)
- type CommitDraftInput
- type CompileDiagnostic
- type CompileError
- type CreateDataAppInput
- type CreateWorkflowInput
- type DataApp
- type DataAppAccess
- type DataAppCommitOutcome
- type DataAppFile
- type DataAppFileDeleteResponse
- type DataAppFileSaveResponse
- type DataAppPatch
- type DatabaseSQLInput
- type DatabaseSQLResult
- type DeviceStart
- type DeviceToken
- type Error
- type Feature
- type FileDeleteResponse
- type FileSaveResponse
- type Me
- type Migration
- type MigrationResult
- type MigrationSetInput
- type MigrationSetResult
- type QueryInput
- type QueryResult
- type RawResponse
- type RunResponse
- type StepDTO
- type Table
- type ValidateBindings
- type ValidateFile
- type ValidateFinding
- type ValidateInput
- type ValidateResult
- type Workflow
- type WorkflowApprovalGate
- type WorkflowFile
- type WorkflowParameter
- type WorkflowPatch
- type WorkflowRun
- type WorkflowRunOutput
- type WorkflowRunTableOutput
Constants ¶
const ( ErrAuthorizationPending = "authorization_pending" ErrSlowDown = "slow_down" ErrExpiredToken = "expired_token" ErrAccessDenied = "access_denied" ErrInvalidGrant = "invalid_grant" )
The device-authorization flow. Mirrors backend/api/v2/authentication/ api_cli_auth.go — the poll error codes are a wire contract, so they are matched by exact string.
const ( DefaultMinPollInterval = 5 * time.Second DefaultPollBackoff = 2 * time.Second DefaultMaxPollInterval = 20 * time.Second // DefaultMaxPollFailureWindow bounds an UNBROKEN RUN of transient poll // failures (a dropped connection, a 429, a 5xx from a pod restarting // mid-deploy) by ELAPSED TIME rather than by a count. // // A count was the wrong unit: it silently means "count x interval", so the // real tolerance moved whenever the cadence did. Five retries at the 5s // floor is ~25 seconds, which is less than an ordinary rolling deploy takes // — a login would die on a perfectly healthy instance mid-release. Elapsed // time says what is actually meant, and stays honest under backoff. // // Generous is nearly free here: a login has ~10 minutes of life, and the // flow deadline bounds total wall time independently of this. What this // still rules out is the case it exists for — a permanently broken instance // hidden behind a silent loop for the full ten minutes. DefaultMaxPollFailureWindow = 90 * time.Second // DefaultRateLimitBackoff is added to the interval on a 429, which is the // global rate limiter answering. It is the one transient failure where // retrying at the same cadence is actively wrong: the server is saying // there is too much traffic, and an unchanged cadence is a promise to keep // producing exactly as much. DefaultRateLimitBackoff = 5 * time.Second )
Default device-flow poll timing. The server advertises its own interval and enforces it, so these are a floor, a backoff step and a ceiling rather than the primary cadence.
const ( // MigrationApplied — the DDL ran and a ledger row was written. MigrationApplied = "applied" // MigrationSkipped — already in the ledger with identical SQL. MigrationSkipped = "skipped" // MigrationPending — not in the ledger; a real apply would apply it. MigrationPending = "pending" // MigrationDrifted — in the ledger under DIFFERENT SQL. A real apply // refuses the whole set. MigrationDrifted = "drifted" )
Migration outcomes, as reported per migration. The first two are what a real apply returns; the last two only ever come back from a dry run.
const ( LifecycleLive = "live" LifecycleDraft = "draft" LifecycleVersion = "version" LifecycleArchived = "archived" LifecycleProposed = "proposed" )
Workflow lifecycle values, mirrored from rdb.WorkflowLifecycle (backend/ronja/rdb/table_workflow.go). The CLI branches on these by exact string, so they are a wire contract in the same sense the device-flow poll codes are.
const ( RunStatusRunning = "running" RunStatusDone = "done" RunStatusError = "error" )
Workflow run statuses, mirrored from rdb.WorkflowRunStatus (backend/ronja/rdb/table_workflow.go:78). The enum is exactly these three, and the poll loop's terminal condition is "not running", so a status added server-side would end the poll rather than hang it.
const ( RunHealthDone = "done" RunHealthDegraded = "degraded" RunHealthFailed = "failed" )
Derived run health, mirrored from workflow.RunHealth (backend/api/v2/workflow/run_steps.go). Never stored: the server computes it on read from the run status plus the step rows. Degraded is the one worth knowing about — a run that finished but had a best-effort step fail, which a bare "done" would hide.
const DefaultDatabaseSQLTimeout = slowRequestTimeout
DefaultDatabaseSQLTimeout bounds one `db sql` call when the caller names no deadline.
The slow bound, matching Query: a statement against a real Postgres database is real work, and an aggregate over a large table legitimately outlasts a read timeout. It remains only a DEFAULT — --timeout moves it, and a value past the client's own ceiling is honoured (see httpFor).
const DefaultMigrationTimeout = 20 * time.Minute
DefaultMigrationTimeout bounds one migration set.
Deliberately far longer than a query default. The server runs the whole set in ONE transaction holding a per-database advisory lock, under a 20-minute ceiling of its own; a client that gave up sooner would abandon a migration that is still committing and leave the operator with no idea whether it landed. Better to wait for the answer than to guess at it.
const DefaultQueryTimeout = slowRequestTimeout
DefaultQueryTimeout bounds one query when the caller names no deadline.
The slow bound rather than the read bound, for the usual reason — a query is real work server-side. It is only a DEFAULT, though: a large query legitimately routes to Batch compute and takes minutes, so `ronja query --timeout` can move it, and Query honours a value past the client's own ceiling. See httpFor.
const DefaultRawTimeout = slowRequestTimeout
DefaultRawTimeout bounds one raw call when the caller names no deadline.
The slow bound rather than the read bound: an arbitrary POST can be anything, including the endpoints that legitimately do real work server-side, and killing one of those at 30s reports a healthy instance as a broken one.
It is only a DEFAULT, for the same reason DefaultQueryTimeout is: `ronja api` can be pointed at an endpoint that runs for minutes, and a bound nobody can move is a bound that eventually kills a healthy request. `--timeout` moves it, including past the client's own ceiling — see httpFor.
const QueryFormatCSVMeta = "csv_meta"
QueryFormatCSVMeta asks for the envelope the CLI is built around: a CSV string plus the row count, the truncation flag and the failure field.
The CLI always sends it. The endpoint has other formats, and picking between them would be a flag nobody can answer — CSV is the one shape that is both human-legible at a terminal and directly consumable by every data tool, and the "meta" half is what makes truncation and a failed query distinguishable from an empty result.
const SeverityError = "error"
SeverityError is mirrored from rworkflow's SeverityError. The CLI branches on it by exact string — an error finding is what makes `wf push` refuse — so it is a wire contract.
The server's finding CODES (invalid_path, unresolved_ref, secret_dropped, …) are deliberately not mirrored as constants. The CLI never branches on a code: it prints Message and Path, so an unknown code is displayed as faithfully as a known one, and a mirrored list would only be one more thing to keep in step with backend/resource/rworkflow/validate_files.go.
const StatusConflict = http.StatusConflict
StatusConflict is the status both optimistic-concurrency refusals answer with — a file whose stored content is not what the write asserted, and a commit whose parent has moved since the draft was seeded.
An alias for net/http's constant rather than a second spelling of 409: named because the CLI branches on it in two commands and the branch is load-bearing — a 409 must never be routed into `wf publish`'s needs-review fallback, which is keyed on 400 — but the VALUE has one definition.
const UserAgent = "ronja-cli"
UserAgent identifies the CLI in server logs.
Variables ¶
var ( ErrDeviceDenied = errors.New("the request was declined in the browser") ErrDeviceExpired = errors.New("the code expired before it was approved") )
ErrDeviceDenied and ErrDeviceExpired are the terminal outcomes a caller is expected to explain to a human, as opposed to genuine failures.
Functions ¶
func CodeOf ¶
CodeOf extracts the server's machine-readable error code, or "" if the error was not an HTTP error.
func DescribeLifecycle ¶
DescribeLifecycle renders a lifecycle value for a human, including the ones the CLI refuses to work with.
func IsTimeout ¶
IsTimeout reports a request that died on a DEADLINE rather than on an answer.
The distinction is what makes a write recoverable: a 4xx means the server considered the request and refused it, while a timeout means we stopped listening — the write may well have committed, and the caller has to go and look rather than assume either way.
Both forms are checked because they arrive differently: a per-request context deadline surfaces as context.DeadlineExceeded, while the http.Client's own Timeout surfaces as a *url.Error that merely reports Timeout() — and which of the two fired is not something a caller should have to reason about.
func SameAccess ¶
func SameAccess(a, b DataAppAccess) bool
SameAccess compares two access sets for sync purposes, order-insensitively.
Types ¶
type Client ¶
type Client struct {
BaseURL string
Token string
HTTP *http.Client
// MinPollInterval floors the cadence PollDevice uses, so a server that
// advertises nothing (or zero) still gets polled politely.
MinPollInterval time.Duration
// PollBackoff is added to the interval each time the server answers
// slow_down.
PollBackoff time.Duration
// MaxPollInterval caps that backoff, so a run of slow_down answers cannot
// stretch the cadence past the point where the flow expires unpolled.
MaxPollInterval time.Duration
// MaxPollFailureWindow bounds an unbroken run of transient failures by
// elapsed time before PollDevice gives up. See isTransient.
MaxPollFailureWindow time.Duration
// RateLimitBackoff is added to the interval on a 429.
RateLimitBackoff time.Duration
}
func (*Client) ApplyMigrations ¶
func (c *Client) ApplyMigrations(ctx context.Context, databaseID string, in MigrationSetInput, timeout time.Duration) (*MigrationSetResult, error)
ApplyMigrations applies (or, with DryRun, plans) an ordered migration set.
func (*Client) CheckoutDataApp ¶
func (c *Client) CheckoutDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
CheckoutDataApp forks a live data app into the caller's own draft, optionally overlaying a metadata + allowlist patch in the same call.
Idempotent on the draft: a caller who already has one gets it back. The overlay is what makes a first push one request instead of two, and the server re-checks every bound id in it against the caller's access.
func (*Client) CheckoutWorkflow ¶
CheckoutWorkflow gets-or-creates the caller's own draft of a live workflow. Idempotent: calling it twice returns the same draft.
func (*Client) CommitDataAppDraft ¶
func (c *Client) CommitDataAppDraft(ctx context.Context, draftID string) (*DataAppCommitOutcome, error)
CommitDataAppDraft applies a draft onto its parent — or, for a never-published app the caller may not publish itself, submits it for an admin's approval. Takes the DRAFT id. Read the returned outcome before reporting a publish.
func (*Client) CommitWorkflowDraft ¶
func (c *Client) CommitWorkflowDraft(ctx context.Context, draftID, confirmHeadVersionID string) error
CommitWorkflowDraft applies a draft onto its parent. Takes the DRAFT id.
confirmHeadVersionID is empty for an ordinary commit, which sends no body at all — the shape this route took before the CAS existed. Supply it only after a 409 has named the version being overwritten, and only on a deliberate override (`wf publish --overwrite-remote`).
func (*Client) CreateDataApp ¶
CreateDataApp creates a data app inside a feature. The row it returns is LIVE and empty — see CreateDataAppInput.
func (*Client) CreateWorkflow ¶
CreateWorkflow creates a workflow inside a feature. The row it returns is a parentless DRAFT — hidden until published — which is why the first push needs no separate checkout.
func (*Client) DatabaseSQL ¶
func (c *Client) DatabaseSQL(ctx context.Context, databaseID string, in DatabaseSQLInput, timeout time.Duration) (*DatabaseSQLResult, error)
DatabaseSQL runs one statement against a managed database.
func (*Client) DeleteDataApp ¶
DeleteDataApp marks a data app for deletion. Unlike DiscardDataAppDraft this removes the APP, so it is only ever reached through an explicit opt-in.
The one case the CLI needs it for is a parentless draft — an app created by a first push and then abandoned, typically because that push failed to compile. Discard cannot express it (there is no parent for the row to fall back to), and without this the only remaining answer is the web UI, which is no answer at all for a caller that has nothing but a terminal.
func (*Client) DeleteDataAppFile ¶
func (c *Client) DeleteDataAppFile(ctx context.Context, id, path string) (*DataAppFileDeleteResponse, error)
DeleteDataAppFile removes one file. The server refuses to delete the entrypoint; its message is passed through rather than second-guessed.
func (*Client) DeleteWorkflow ¶
DeleteWorkflow soft-deletes a workflow — the row survives a 30-day TTL before the janitor purges it. Unlike DiscardWorkflowDraft this removes the WORKFLOW, so it is only ever reached through an explicit opt-in.
The one case the CLI needs it for is a parentless draft — a workflow created by a first push and then abandoned. Discard cannot express it (there is no parent for the row to fall back to), and without this the only remaining answer is the web UI, which is no answer at all for a caller that has nothing but a terminal.
func (*Client) DeleteWorkflowFile ¶
func (c *Client) DeleteWorkflowFile(ctx context.Context, id, path string, baseSha256 *string) (*FileDeleteResponse, error)
DeleteWorkflowFile removes one file from a draft. The server refuses to delete the entrypoint; its message is passed through rather than second- guessed, because the remedy (change the entrypoint first) is its to state.
baseSha256 asserts what is being deleted, and nil sends NO BODY AT ALL rather than `{}` — the route took none before preconditions existed, and an unconditional delete has no reason to start sending one.
func (*Client) DiscardDataAppDraft ¶
DiscardDataAppDraft deletes a draft, throwing its uncommitted changes away. The parent live app is untouched.
func (*Client) DiscardWorkflowDraft ¶
DiscardWorkflowDraft deletes a draft, throwing its uncommitted changes away. The parent live workflow is untouched.
func (*Client) Do ¶
Do issues a request against /api/v2 and decodes a JSON response into out. Pass a nil body to send none, and a nil out to discard the response.
func (*Client) DoRaw ¶
func (c *Client) DoRaw(ctx context.Context, method, path string, header http.Header, body []byte, timeout time.Duration) (*RawResponse, error)
DoRaw issues an arbitrary request against a path relative to the instance root and returns the response without reading it.
header is applied verbatim; the defaults below fill in only what it does not already carry, so a caller can override any of them — including Authorization, which is how someone tests a different credential without re-logging-in.
timeout bounds the whole call, the response body INCLUDED — the deadline rides on Body.Close, not on this function's return. Zero or less means no client-side deadline at all, which the command surfaces as `--timeout 0`.
func (*Client) GetDataApp ¶
GetDataApp reads a data app's metadata. 404s for an id the caller cannot reach — the read gate does not distinguish absent from invisible.
func (*Client) GetDataAppDraft ¶
GetDataAppDraft reads the CALLER'S OWN open draft of a data app, returning (nil, nil) when they have none.
The endpoint answers 200 with a JSON `null` body rather than 404 in that case, so decoding into a **DataApp is not a stylistic choice: decoding into a struct would turn "no draft" into a zero-value DataApp with an empty id, and every caller would then act on a draft that does not exist.
func (*Client) GetDataAppFile ¶
GetDataAppFile reads one file back by path, answering 404 when the row holds none.
It lives on the write side because it exists for the write side: a PUT that died on a TIMEOUT may still have committed, and the only way to find out which is to ask. Note that it is subject to the same silent draft resolution as ListDataAppFiles — pass the id you mean.
func (*Client) GetFeature ¶
GetFeature reads a feature's metadata. 404s for an id the caller cannot reach.
func (*Client) GetTable ¶
GetTable reads a table's identity so an id can be shown with its name.
Best-effort by contract: the caller is expected to fall back to printing the bare id. A token scoped away from the data surface, or a table the caller cannot read, fails here — and neither is a reason to refuse to tell someone which tables a run would overwrite.
func (*Client) GetText ¶
GetText fetches a path relative to the instance ROOT (not /api/v2) and returns it verbatim. The agent-readable docs surface — /llms.txt, /docs/api/endpoints.md, /docs/api/skills.md — lives there and is served unauthenticated, so this deliberately does not require a token.
func (*Client) GetWorkflow ¶
GetWorkflow reads a workflow's metadata. 404s for an id the caller cannot reach — the read gate does not distinguish absent from invisible.
func (*Client) GetWorkflowDraft ¶
GetWorkflowDraft reads the CALLER'S OWN open draft of a workflow, returning (nil, nil) when they have none.
The endpoint answers 200 with a JSON `null` body rather than 404 in that case (see the route's own description in backend/api/v2/workflow/handler.go), so decoding into a **Workflow is not a stylistic choice: decoding into a struct would turn "no draft" into a zero-value Workflow with an empty id, and every caller would then act on a draft that does not exist.
func (*Client) GetWorkflowFile ¶
GetWorkflowFile reads one file back by path, answering 404 when the row holds none.
It lives on the write side because it exists for the write side: a PUT that died on a TIMEOUT may still have committed, and the only way to find out which is to ask. Nothing else reads a single file — the rest of the CLI wants the whole set — so this is a repair tool, not a read API.
func (*Client) GetWorkflowRun ¶
GetWorkflowRun reads one run: the row, its steps and its derived health.
Note the route is /workflow/run/:runID — keyed by the RUN, not nested under the workflow — so polling needs nothing but the id the run started with.
func (*Client) HeadVersionID ¶ added in v0.26.9
HeadVersionID resolves the CAS anchor for a live workflow: the id of its most recently committed version, or — when it has never been versioned — the workflow's OWN id.
The parent-id fallback is not a guess. It mirrors rworkflow.resolveHeadVersionID exactly: an unversioned workflow's drafts are seeded from the live row itself, so the live row IS the anchor, and sending anything else as confirmHeadVersionID would be refused.
Deliberately NOT parsed out of the 409 message. The HTTP error body is the flat app-wide `{"error": "<prose>"}` shape with no structured payload, and a client that regexes an id out of prose starts silently overwriting the wrong version the day somebody rewords it.
func (*Client) ListDataAppFiles ¶
ListDataAppFiles reads every file of ONE data-app row.
⚠️ The id you pass is NOT necessarily the row you get. Unlike the workflow equivalent, GET /dataapp/:id/files SILENTLY RESOLVES TO THE CALLER'S OWN OPEN DRAFT when one exists (api/v2/dataapp/handler.go), and nothing in the response says which row answered — the files carry a dataAppID, but a caller that did not think to look will never notice.
That matters because a baseline is a claim about a specific row. Recording draft bytes under the live app's identity makes `status` compare the wrong two things: it reports drift that is not there, or worse hides drift that is, and a push then acts on the difference. So every caller in this CLI resolves the draft explicitly with GetDataAppDraft and passes the id it actually means — never a live id in the hope that the server picks the same row it would have.
func (*Client) ListWorkflowFiles ¶
ListWorkflowFiles reads every file of ONE workflow row — note that a draft has its own complete file set, so the id passed here decides whether you get the live code or the draft's.
The route returns a bare JSON array ([]*rdb.WorkflowFile), not an envelope.
func (*Client) ListWorkflowVersions ¶ added in v0.26.9
ListWorkflowVersions reads a workflow's committed version history, newest first — the server orders it (committed_at DESC, id DESC), which is the SAME total order rworkflow.resolveHeadVersionID resolves the head by, so element 0 is the head rather than merely a recent version.
It lives on the write side, like GetWorkflowFile, because it exists for the write side: the only caller is the commit CAS override, which needs the id of the version it is about to overwrite. The route answers a bare JSON array of rows and stamps no `url` on them (it is a LIST route), so treat these as identity and timing only.
func (*Client) Me ¶
Me identifies the caller. It is the CLI's login verification: the endpoint returns the user, and their tenant and role when one is selected.
Note that /api/v2/authentication is admin-scoped, so a SCOPED token cannot call it — which is why `login` mints a full-access Personal Access Token. A PAT still never exceeds its user's live role.
func (*Client) PollDevice ¶
func (c *Client) PollDevice(ctx context.Context, start *DeviceStart) (*DeviceToken, error)
PollDevice waits for a human to approve the flow, then returns the minted token. It blocks until approval, denial, expiry, or ctx cancellation.
The server's advertised interval is the starting cadence; a slow_down response backs it off, because the server enforces the throttle and an over-eager client would otherwise never make progress.
TRANSIENT failures are tolerated rather than fatal, and the tolerance is measured in ELAPSED TIME (MaxPollFailureWindow), not in retries. A login has a ~10 minute budget during which a human is walking to a browser, and in that window a rolling deploy, a 429 from the global rate limiter or a dropped connection are all entirely ordinary. Aborting on the first one would burn a flow the user then has to restart for no reason — and a retry COUNT quietly made the real tolerance a function of the cadence, so it came out shorter than an ordinary deploy takes. The RFC-defined outcomes (access_denied, expired_token, invalid_grant) are still terminal — those are the server's considered answer, not a blip. The deadline remains the backstop so this always terminates.
func (*Client) PublishWorkflowDraft ¶
PublishWorkflowDraft takes a PARENTLESS draft live. Takes the draft's own id, which for a parentless draft is also the workflow's stable identity.
func (*Client) PutDataAppFile ¶
func (c *Client) PutDataAppFile(ctx context.Context, id, path, content string) (*DataAppFileSaveResponse, error)
PutDataAppFile creates or replaces one file, returning the saved row plus the recompile's diagnostics when it failed. See DataAppFileSaveResponse.
SLOW: every write recompiles the whole bundle with esbuild and uploads a fresh bundle to S3, so this is seconds of real work rather than a row write.
func (*Client) PutWorkflowFile ¶
func (c *Client) PutWorkflowFile(ctx context.Context, id, path, content string, baseSha256 *string) (*FileSaveResponse, error)
PutWorkflowFile creates or replaces one file.
This is the hard-validating path: unreachable `{{ ref }}` / `{{ write }}` / `{{ agent }}` / `{{ codex }}` markers roll the write back with a 400, and the file keeps its previous content. Secrets are the soft tier and come back as Warnings instead.
baseSha256 is the optional compare-and-swap precondition described on putWorkflowFileInput. A failed precondition is a 409, and the write is rolled back — the file certainly keeps its previous content, exactly like a 400.
The other SLOW call: every save re-derives the workflow's binding columns in the same transaction, so a file write is a row write with marker resolution attached — not something to hold to a read's deadline.
func (*Client) Query ¶
func (c *Client) Query(ctx context.Context, in QueryInput, timeout time.Duration) (*QueryResult, error)
Query runs one SQL statement and returns the csv_meta envelope.
timeout bounds the request. Zero or less means no client-side deadline at all, which the command surfaces as `--timeout 0` — a query that has been routed to Batch compute can legitimately outlast any number anyone would think to type, and being told to go and look for a result that is still coming is not an improvement on waiting for it.
func (*Client) RequestDataAppReview ¶
RequestDataAppReview submits the caller's own edit draft for admin review.
Unlike the workflow equivalent this lives on the data-app handler itself rather than the governance one, so the path has no /draft/ segment.
func (*Client) RequestWorkflowReview ¶
RequestWorkflowReview submits the caller's own edit draft for admin review.
NOTE the different prefix: this route lives on the GOVERNANCE handler, whose group is scoped `admin` rather than the core workflow handler's `automation`. The CLI's full-access PAT covers both; a scoped token would half-work, which is why the failure is reported as-is rather than swallowed.
func (*Client) RunWorkflow ¶
func (c *Client) RunWorkflow(ctx context.Context, workflowID string, parameterValues map[string]any) (*WorkflowRun, error)
RunWorkflow starts a run and returns IMMEDIATELY with the created run row at status "running" — execution is asynchronous, and GetWorkflowRun is how you learn what happened.
Two refusals arrive here as 400s with the server's own message: an approval gate (which no HTTP caller can satisfy — only an agent session produces an approval) and the per-tenant credit kill-stop. Both are passed through rather than paraphrased.
parameterValues is always sent, as {} when there are none. NOT `omitempty`: that omits the key, which is the null map it is meant to avoid — the server decodes a missing key to a nil map exactly as it does an explicit null.
func (*Client) StartDevice ¶
StartDevice opens a device-authorization flow. No authentication required.
func (*Client) UpdateDataApp ¶
func (c *Client) UpdateDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
UpdateDataApp patches a data app's metadata and allowlists.
Returns the row that RECEIVED the edit, which is not necessarily the one addressed: patching a live app auto-forks a draft, exactly as a file write does. Callers that care which row they changed must read the returned id.
func (*Client) UpdateWorkflow ¶
UpdateWorkflow patches a workflow's metadata.
The endpoint answers with nothing, and it DROPS the save-time warnings the file path returns (the server's own comment says to plumb a new endpoint rather than overload this one) — so a caller that needs the row's current state re-reads it afterwards.
func (*Client) ValidateDataApp ¶
ValidateDataApp recompiles a draft and stamps it validated, which CommitDataAppDraft refuses without. Takes the DRAFT id.
This is the authoritative answer to "does this compile": it always recompiles from the row's current files, so a draft left un-validated by a failed file write is re-checked here rather than trusted.
SLOW for the same reason PutDataAppFile is.
func (*Client) ValidateWorkflowFiles ¶
func (c *Client) ValidateWorkflowFiles(ctx context.Context, in ValidateInput) (*ValidateResult, error)
ValidateWorkflowFiles dry-runs a candidate workflow. Findings are data: a candidate full of errors still comes back as a successful call, and only a malformed request (no featureID, a feature the caller cannot read) is an error here.
One of the two SLOW calls: the whole folder goes up in one body and every marker in it is resolved against the caller's access, so this is doing real work rather than reading a row.
type CommitDraftInput ¶ added in v0.26.9
type CommitDraftInput struct {
ConfirmHeadVersionID string `json:"confirmHeadVersionID,omitempty"`
}
CommitDraftInput is the OPTIONAL body of POST /workflow/:id/commit, mirroring rworkflow.CommitDraftInput (backend/resource/rworkflow/model.go).
An empty body is the normal case and means "no override": the commit then requires the parent to still be at the version this draft was seeded from, and is refused with a 409 when it is not.
ConfirmHeadVersionID acknowledges the specific committed version this commit will OVERWRITE. It must equal the head AT COMMIT TIME, not merely be non-empty — a caller who learns the head, deliberates, and commits while a third version lands gets another 409. That re-refusal is the feature, which is why nothing here retries.
type CompileDiagnostic ¶
type CompileDiagnostic struct {
Message string `json:"message"`
Line int `json:"line"`
Column int `json:"column"`
File string `json:"file,omitempty"`
}
CompileDiagnostic is one author-readable build error, mirroring dataappbundle.Diagnostic.
type CompileError ¶
type CompileError struct {
Message string `json:"message"`
Diagnostics []CompileDiagnostic `json:"diagnostics"`
}
CompileError is a failed bundle compile, mirroring the api/v2/dataapp CompileDiagnostics response type.
type CreateDataAppInput ¶
type CreateDataAppInput struct {
FeatureID string `json:"featureID"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
DataAppAccess
}
CreateDataAppInput is the body of POST /dataapp, mirroring the subset of rdataapp.CreateInput the CLI writes.
What this call PRODUCES matches the workflow equivalent as of migration 000495: a hidden PARENTLESS DRAFT, visible only to its drafter, which the first commit promotes to live IN PLACE — so the id this returns is the app's id forever and is safe to record in ronja.json before any publish. (Before 000495 the row was born LIVE and empty, serving the pending page to the whole feature until someone published it.)
The allowlists are sent at creation because a data app with none can query nothing, and the alternative (create, then patch) leaves a window where the app exists in a state its author never asked for.
type CreateWorkflowInput ¶
type CreateWorkflowInput struct {
FeatureID string `json:"featureID"`
Title string `json:"title"`
Entrypoint string `json:"entrypoint"`
// Parameters is the declared parameter set, when the folder manages one.
// omitempty because a folder that does not manage parameters must create a
// workflow with none rather than assert an empty declaration.
Parameters []WorkflowParameter `json:"parameters,omitempty"`
}
CreateWorkflowInput is the body of POST /workflow.
The server's input type is the whole rdb.Workflow, and this deliberately is not: everything else on that row is either derived (the binding columns are stamped from the code at file save), stamped by the server (drafter attribution is scrubbed from HTTP input on purpose), or not the CLI's to set. Sending three fields makes it obvious that a first push creates an EMPTY workflow whose bindings then come from its files.
type DataApp ¶
type DataApp struct {
ID string `json:"id"`
FeatureID string `json:"featureID"`
Name string `json:"name"`
Description string `json:"description"`
// Entrypoint names the file the bundle compiles from. Reported, never
// pushed: rdataapp.Patch has no entrypoint field, so a data app's is fixed
// at "App.tsx" from the moment the row is created.
Entrypoint string `json:"entrypoint"`
// BundleFileKey is the compiled bundle in S3, empty on an app that has never
// compiled. Read as a FACT ABOUT THE PAST only — a failed compile leaves the
// previous key in place, so a non-empty value never means "the current source
// compiles". ValidatedAt is the field that answers that.
BundleFileKey string `json:"bundleFileKey"`
// ValidatedAt is cleared by every file mutation and re-stamped only by a
// clean compile. It is the single gate POST :id/commit enforces, so it is
// also what `ronja app publish` checks before it tries.
ValidatedAt *time.Time `json:"validatedAt"`
DataAppAccess
// ParentDataAppID is empty on a live row AND on a parentless draft (an app
// created by POST /dataapp and not yet published, since migration 000495),
// and set on an edit shadow or a committed version snapshot. So it — not
// the lifecycle, which reads "draft" for both — is what tells a never-
// published app apart from somebody's open edits to a live one.
ParentDataAppID string `json:"parentDataAppID"`
CommittedAt *time.Time `json:"committedAt"`
// Lifecycle is trigger-computed server-side: live|draft|version|archived|proposed.
Lifecycle string `json:"lifecycle"`
// SubmittedForReviewAt is set once a drafter asks an admin to commit their
// draft; null on every other row, and on a draft they have not submitted.
SubmittedForReviewAt *time.Time `json:"submittedForReviewAt"`
DrafterUserID string `json:"drafterUserID"`
CreatedBy string `json:"createdBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// URL is the absolute frontend /apps/<id> page, stamped by the SERVER
// (api/v2/dataapp.DataAppView) on the single-row routes. Never built here —
// see the same field on Workflow for why, and for why an empty value is
// normal, silent, and must not be filled in with a guess.
URL string `json:"url,omitempty"`
}
DataApp is a HAND-MIRROR of the subset of rdb.DataApp the CLI reads.
Same rule and same hazard as Workflow in workflow.go: a json tag that does not exist on the wire decodes to the zero value SILENTLY, so a typo here shows up as an empty column of output — or as an allowlist the CLI believes is empty and then pushes as empty, revoking the app's access to its own tables. Every tag below is copied from backend/ronja/rdb/table_data_app.go; keep them in step with that file, and see dataapp_test.go for the fixture that checks them.
The lifecycle vocabulary is shared with workflows (rdb.DataAppLifecycle carries the same five values as rdb.WorkflowLifecycle), so the Lifecycle* constants and DescribeLifecycle are reused rather than duplicated.
Mirroring decisions specific to this row:
- The server's optional.V[T] marshals as the bare value or JSON `null`, and unmarshalling `null` into a non-pointer is a documented no-op — so optional STRINGS are plain strings ("" is exactly what "unset" means for an id) while optional TIMESTAMPS are pointers, because "has this been validated at all" is the question the whole publish path turns on.
- The allowlist slices carry NO omitempty server-side (they are plain []string columns), so they arrive as [] rather than absent. Normalize still runs, because a nil from a hand-written fixture or an older instance would otherwise range differently from an empty one.
func (*DataApp) IdentityID ¶
IdentityID is the STABLE id of the data app this row belongs to: the parent for a draft or a version snapshot, the row itself for a live app.
This is what a manifest binding records. A draft's own id is per-user and disappears on commit, so a committed file naming one would point at a row that stops existing the moment its author publishes.
func (*DataApp) IsValidated ¶
IsValidated reports whether the row's current source has compiled cleanly since its last file edit — the gate POST :id/commit enforces.
type DataAppAccess ¶
type DataAppAccess struct {
AllowedTableIDs []string `json:"allowedTableIDs"`
AllowedSecretIDs []string `json:"allowedSecretIDs"`
AllowedAgentIDs []string `json:"allowedAgentIDs"`
AllowedWorkflowIDs []string `json:"allowedWorkflowIDs"`
AllowedCodexIDs []string `json:"allowedCodexIDs"`
AllowedMetricIDs []string `json:"allowedMetricIDs"`
// Capabilities are the coarse switches (query_ronja, run_agent, …) the
// minted script-token carries alongside the id allowlists.
Capabilities []string `json:"capabilities"`
}
DataAppAccess is a data app's capability surface: what its bundle may reach.
One type serving three roles on purpose — it is embedded in the row, it is the manifest's `access` block, and it is what the patch sends. A second struct would only add a conversion that silently drops whatever field was added to one side and not the other, and this is the type where a dropped field means a REVOKED capability rather than a cosmetic gap.
Its json tags are therefore part of the ronja.json FILE FORMAT as well as the wire, and renaming one is a breaking change to committed manifests.
These are grants, not descriptions: an id here is a capability the app's viewers inherit, which is why the server re-checks every one of them against the pusher's own access (governance.ValidateDataAppScope) rather than trusting the file.
func (DataAppAccess) IsEmpty ¶
func (a DataAppAccess) IsEmpty() bool
IsEmpty reports an access set that grants nothing.
func (DataAppAccess) Normalized ¶
func (a DataAppAccess) Normalized() DataAppAccess
Normalized returns a copy with nil lists replaced by empty ones and every list sorted and de-duplicated.
Sorting is what makes SameAccess a comparison of MEANING rather than of spelling. An allowlist is a set — the server stores it as one and the token mint reads it as one — so a manifest listing two tables in the other order is the same grant, and treating it as drift would make `app status` report a change forever and `app push` send a patch that changes nothing.
type DataAppCommitOutcome ¶
type DataAppCommitOutcome struct {
DataAppID string `json:"dataAppID"`
FirstPublish bool `json:"firstPublish"`
Proposed bool `json:"proposed"`
}
DataAppCommitOutcome is the body of POST /dataapp/:id/commit, mirroring rdataapp.CommitOutcome.
It exists because the route has TWO successful endings and they are not the same event: a commit either publishes, or — for a non-admin publishing a brand-new app into a shared feature — raises a proposal an admin must approve. `proposed` is the only thing on the wire that tells them apart, so a caller that ignores this body necessarily reports one of them wrongly.
type DataAppFile ¶
type DataAppFile struct {
ID string `json:"id"`
DataAppID string `json:"dataAppID"`
Path string `json:"path"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
DataAppFile mirrors rdb.DataAppFile — one source file of one data-app row. There is no content hash on the wire, so the CLI hashes Content itself.
type DataAppFileDeleteResponse ¶
type DataAppFileDeleteResponse struct {
DataAppID string `json:"dataAppID"`
CompileError *CompileError `json:"compileError,omitempty"`
}
DataAppFileDeleteResponse is the body of DELETE :id/files/*path. DataAppID is the row the delete landed on — deleting from a live app auto-forks a draft.
type DataAppFileSaveResponse ¶
type DataAppFileSaveResponse struct {
DataAppFile
CompileError *CompileError `json:"compileError,omitempty"`
}
DataAppFileSaveResponse is the body of PUT :id/files/*path.
The compile failure arrives IN a 200 rather than as an error, and that is the server being accurate rather than lenient: the file write COMMITS before the bundle is recompiled, so the save really did happen. Pushing a multi-file app walks through states that do not compile by construction — App.tsx importing a module that is one request away — so a client that treated this as a failure could not push more than one file.
What it does NOT mean is that the app is fine. validated_at was cleared by the write and only a clean compile re-stamps it, so ValidateDataApp is the verdict and CommitDataAppDraft refuses until it passes.
type DataAppPatch ¶
type DataAppPatch struct {
Name *string `json:"name,omitempty"`
AllowedTableIDs *[]string `json:"allowedTableIDs,omitempty"`
AllowedSecretIDs *[]string `json:"allowedSecretIDs,omitempty"`
AllowedAgentIDs *[]string `json:"allowedAgentIDs,omitempty"`
AllowedWorkflowIDs *[]string `json:"allowedWorkflowIDs,omitempty"`
AllowedCodexIDs *[]string `json:"allowedCodexIDs,omitempty"`
AllowedMetricIDs *[]string `json:"allowedMetricIDs,omitempty"`
Capabilities *[]string `json:"capabilities,omitempty"`
}
DataAppPatch is the body of PUT /dataapp/:id, mirroring the subset of rdataapp.Patch the CLI writes.
Every field is a POINTER because every field of the server's Patch is an optional.V: an absent key means "unchanged", while an explicit value — including an empty list — REPLACES what is there. Both are things a folder legitimately means ("I do not manage the allowlists" and "this app is allowed nothing"), and a plain slice with omitempty collapses them into the first, making it impossible to remove the last table from an app.
There is deliberately no Entrypoint: rdataapp.Patch has none, because a data app's entrypoint is stamped at create and never moves.
func (DataAppPatch) Empty ¶
func (p DataAppPatch) Empty() bool
Empty reports a patch that would change nothing, so the caller can skip the round trip rather than send `{}`.
func (*DataAppPatch) SetAccess ¶
func (p *DataAppPatch) SetAccess(access DataAppAccess)
SetAccess fills in every allowlist field from a declared set.
All seven or none: the allowlists are one decision, and patching a subset would let a folder that manages access leave a list it happens not to use pointing at whatever the row had.
type DatabaseSQLInput ¶
type DatabaseSQLInput struct {
SQL string `json:"sql"`
// Params are positional bound parameters for $1, $2 … placeholders, carried
// as RAW JSON rather than []any.
//
// That is load-bearing, not fussiness. Decoding a parameter list into []any
// turns every number into a float64, which silently loses precision above
// 2^53 — 9007199254740993 becomes ...992, and 1234567890123456789 becomes
// ...800. Re-encoding cannot recover what the decode threw away, so the wrong
// number would reach the database with no error at any layer. Passing the
// user's bytes through untouched means a bigint id survives the trip.
//
// Sent only when non-empty: the field is optional server-side, and an
// explicit null adds nothing.
Params []json.RawMessage `json:"params,omitempty"`
MaxRows int `json:"maxRows,omitempty"`
}
DatabaseSQLInput is the POST :id/sql body.
type DatabaseSQLResult ¶
type DatabaseSQLResult struct {
DatabaseID string `json:"databaseID"`
// Result is the CSV, header row included. Empty for a statement with no
// result set — a plain INSERT, say — which is not the same as no rows.
Result string `json:"result"`
RowCount int `json:"rowCount"`
// Truncated reports that the row cap cut the result off, so Result is a
// prefix rather than the answer.
Truncated bool `json:"truncated"`
DurationMs int64 `json:"durationMs"`
}
DatabaseSQLResult is the POST :id/sql response.
No omitempty anywhere: this struct is emitted verbatim by `ronja db sql --json`, and a field that vanishes when it is zero makes the payload a different shape depending on the answer.
type DeviceStart ¶
type DeviceToken ¶
type DeviceToken struct {
Token string `json:"token"`
TokenID string `json:"tokenID"`
TokenName string `json:"tokenName"`
// UserID and TenantID are unused by the CLI today. They are kept because
// this struct is a faithful mirror of the server's response, and a mirror
// that quietly omits fields is one nobody can check against the source.
UserID string `json:"userID"`
TenantID string `json:"tenantID"`
// ExpiresAt is RFC 3339. CLI-minted tokens are time-bounded, and a
// credential that stops working with no warning is a bad surprise.
ExpiresAt string `json:"expiresAt"`
}
type Error ¶
Error is a non-2xx response. Code carries the machine-readable `error` field when the server sent one — the CLI branches on it for the device-flow poll, where "authorization_pending" is an expected, non-fatal answer.
type Feature ¶
type Feature struct {
ID string `json:"id"`
Name string `json:"name"`
// Scope is private|workspace|organization. Compare against
// scopeOrganization; an unknown value is treated as not-shared by callers,
// which fails toward attempting the commit and letting the server decide.
Scope string `json:"scope"`
}
Feature is a HAND-MIRROR of the sliver of rdb.Feature the CLI reads: what a feature is called, and how private it is.
Scope is the only field anything branches on, and it decides whether a commit is admin-only. Workflows get it for free — the server stamps featureScope onto every workflow row at read time — but rdb.DataApp carries no such column, so `ronja app publish` has to ask. That asymmetry is the entire reason this type exists; it is not a general feature client and should not grow into one, since discovery belongs on plain HTTP.
Tags copied from backend/ronja/rdb/table_feature.go.
type FileDeleteResponse ¶
type FileDeleteResponse struct {
Warnings []string `json:"warnings,omitempty"`
}
FileDeleteResponse mirrors the handler's FileDeleteResponse. Deleting the last file that referenced a secret drops that binding, which is exactly the kind of change nobody asked for that a warning exists to surface.
type FileSaveResponse ¶
type FileSaveResponse struct {
WorkflowFile
Warnings []string `json:"warnings,omitempty"`
}
FileSaveResponse mirrors the workflow handler's FileSaveResponse: the saved file at the response root, plus soft save-time notices.
Warnings is the SOFT tier of the save path — an unreachable secret is filtered out of the binding rather than refused — and it is the only place those notices exist, so the CLI passes them through verbatim rather than paraphrasing.
type Me ¶
type Me struct {
User struct {
ID string `json:"id"`
Email string `json:"email"`
// Serialized from optional.V[string], so these are `null` rather than
// absent when unset; encoding/json leaves the zero value in place.
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
} `json:"user"`
Tenant *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"tenant,omitempty"`
Role *struct {
Name string `json:"name"`
PrivilegeLevel int `json:"privilegeLevel"`
} `json:"role,omitempty"`
IsRonjaStaff bool `json:"isRonjaStaff"`
}
Me mirrors the /authentication/me response (MeResponse in backend/api/v2/authentication/api_login.go). Tenant and Role are absent for a user who has not selected an organization.
This is a HAND-MIRROR of server types, so the field names have to come from the structs that are actually serialized, not from what a CLI would like to receive:
- User is a *rdb.User — backend/ronja/rdb/table_user.go is the source of truth. It has first_name / last_name and NO "name" field.
- Role is a *rdb.Role — backend/ronja/rdb/table_roles.go — which is keyed on name and has NO id column.
Keeping this in step with those files is load-bearing (see CLAUDE.md): a field that does not exist on the wire decodes to the zero value silently, so the mistake shows up as a blank line of output rather than an error.
type Migration ¶
type Migration struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
// SQL is sent VERBATIM. The server's ledger hash covers exactly these bytes,
// so trimming or normalising here — a trailing newline is the obvious
// temptation — would make a file hash differently from the way it was
// applied, and every applied migration would then report as drifted.
SQL string `json:"sql"`
}
Migration is one named migration in a set.
type MigrationResult ¶
type MigrationResult struct {
Name string `json:"name"`
Outcome string `json:"outcome"`
Seq int `json:"seq"`
}
MigrationResult is one migration's verdict.
type MigrationSetInput ¶
type MigrationSetInput struct {
Migrations []Migration `json:"migrations"`
DryRun bool `json:"dryRun,omitempty"`
}
MigrationSetInput is the POST :id/migrations body.
type MigrationSetResult ¶
type MigrationSetResult struct {
DatabaseID string `json:"databaseID"`
DryRun bool `json:"dryRun"`
Results []MigrationResult `json:"results"`
}
MigrationSetResult is the POST :id/migrations response.
func (*MigrationSetResult) Drifted ¶
func (r *MigrationSetResult) Drifted() []MigrationResult
Drifted returns the migrations the server reported as drifted.
Only a DRY RUN can produce these: a real apply refuses the whole set and returns an error instead, which is why `db migrate push` never needs to inspect this and `db migrate status` always does.
func (*MigrationSetResult) Pending ¶
func (r *MigrationSetResult) Pending() []MigrationResult
Pending returns the migrations a real apply would apply.
type QueryInput ¶
type QueryInput struct {
SQL string `json:"sql"`
MaxRows int `json:"maxRows,omitempty"`
Format string `json:"format"`
}
QueryInput is the request body.
MaxRows omitted means the server's own default cap; the server also clamps anything above its ceiling, so an over-large value is not an error the CLI has to pre-empt.
type QueryResult ¶
type QueryResult struct {
// Result is the CSV, header row included.
Result string `json:"result"`
RowCount int `json:"rowCount"`
// Truncated reports that the row cap cut the result off, so what is in
// Result is a prefix rather than the answer.
Truncated bool `json:"truncated"`
// Error is the query's own failure — a syntax error, an unresolvable
// {{ ref }}, a permission refusal. Non-empty means the query did not run.
Error string `json:"error"`
SQL string `json:"sql"`
InstanceKind string `json:"instanceKind"`
}
QueryResult is the csv_meta envelope.
The trap worth stating loudly: a FAILED query comes back as HTTP 200 with Error set. Nothing in the transport layer will flag it, so every caller has to check Error before trusting Result — a query that fails and one that returns no rows are otherwise indistinguishable.
No omitempty anywhere: this struct is emitted verbatim by `ronja query --json`, and a field that vanishes when it is zero makes the payload a different shape depending on the answer.
func (*QueryResult) Failed ¶
func (q *QueryResult) Failed() bool
Failed reports a query the server accepted and could not run.
type RawResponse ¶
type RawResponse struct {
Status int
Header http.Header
Body io.ReadCloser
}
RawResponse is one undecoded HTTP response. The caller owns Body and MUST close it — closing is also what releases the request's deadline.
Header was added back when a caller finally needed it: `ronja api --retry` honours Retry-After, and a retry that ignores the interval the server just named is the one behaviour a rate limiter is entitled to treat as abuse. Still deliberately narrow — everything else about the response is the body.
type RunResponse ¶
type RunResponse struct {
WorkflowRun
// Steps is never null: a legacy or step-less run hydrates as [].
Steps []StepDTO `json:"steps"`
// Health is done|degraded|failed — see the constants above.
Health string `json:"health"`
}
RunResponse mirrors workflow.RunResponse — the GET /workflow/run/:runID wire shape.
The server embeds *rdb.WorkflowRun, so every run field sits at the JSON ROOT alongside steps and health rather than under a "run" key. The embedded (untagged) WorkflowRun below reproduces exactly that flattening — do not give it a json tag, or the CLI stops decoding every field of the run it polls.
func (*RunResponse) Running ¶
func (r *RunResponse) Running() bool
Running reports a run still in flight — the poll loop's continue condition.
Written as "== running" rather than "not done and not error" on purpose. The enum is exactly three values today, so the two spellings agree; they differ only on a status added server-side later, and there the failure modes are not symmetric. Ending the poll on an unrecognised status reports it (a caller sees "status: cancelled" and knows more than the CLI does), while treating it as still-running hangs until --timeout on a run that already finished.
type StepDTO ¶
type StepDTO struct {
ID string `json:"id"`
ParentStepID *string `json:"parentStepId"`
Seq int `json:"seq"`
Name string `json:"name"`
// Status is a run-span status; "failed" is the one the health derivation
// keys on.
Status string `json:"status"`
StartedAt string `json:"startedAt"`
EndedAt *string `json:"endedAt"`
DurationMs *int64 `json:"durationMs"`
AICallCount int64 `json:"aiCallCount"`
QueryCount int64 `json:"queryCount"`
TokensUsed int64 `json:"tokensUsed"`
Error *string `json:"error"`
Logs *string `json:"logs"`
}
StepDTO mirrors workflow.StepDTO — one observable step of a run, projected from the container-step run spans.
ParentStepID keeps the server's parentStepId spelling (a frontend back-compat name for what is now parent_span_id). TokensUsed is 0 in production today.
type Table ¶
Table is a minimal mirror of rdb.ModelV2: an id and a name, which is all the CLI wants — a table id in a refusal message is correct but unreadable.
Name is optional server-side, hence the pointer: an unnamed table is a real state (a freshly created one), not a decode failure.
type ValidateBindings ¶
type ValidateBindings struct {
InputTableIDs []string `json:"inputTableIDs"`
OutputTableIDs []string `json:"outputTableIDs"`
SecretIDs []string `json:"secretIDs"`
QuerySecretIDs []string `json:"querySecretIDs"`
AgentIDs []string `json:"agentIDs"`
CodexIDs []string `json:"codexIDs"`
}
ValidateBindings mirrors rworkflow.ValidateBindings — what the candidate WOULD bind to if saved as-is. Every slice is always present on the wire (no omitempty server-side), so unlike Workflow's binding columns these need no normalization.
type ValidateFile ¶
ValidateFile is one candidate file, mirroring rworkflow.ValidateFile.
type ValidateFinding ¶
type ValidateFinding struct {
Severity string `json:"severity"`
Code string `json:"code"`
Message string `json:"message"`
Path string `json:"path"`
// Marker is the canonical marker form for the offending binding, when the
// finding is about one. A hint for locating the problem, not necessarily
// the literal text in the file.
Marker string `json:"marker,omitempty"`
}
ValidateFinding mirrors rworkflow.ValidateFinding: one problem, attributed to the source that caused it.
Path is a file path, or a synthetic source ("parameters[0].optionsQuery", "pipPackages", "inputTableIDs") when the problem does not belong to a file — so a client grouping by Path gets a sensible bucket either way.
func (ValidateFinding) IsError ¶
func (f ValidateFinding) IsError() bool
IsError reports whether a finding would block a save.
type ValidateInput ¶
type ValidateInput struct {
FeatureID string `json:"featureID"`
Entrypoint string `json:"entrypoint"`
Parameters []WorkflowParameter `json:"parameters,omitempty"`
Files []ValidateFile `json:"files"`
PipPackages []string `json:"pipPackages,omitempty"`
}
ValidateInput mirrors rworkflow.ValidateFilesInput — a whole candidate workflow, with no workflow ID anywhere in it. That absence is the point: the endpoint persists nothing and needs nothing to exist, which is what makes "iterate an existing script to green before the first push" possible.
Parameters and PipPackages are omitempty because the CLI does not persist either in the manifest today: a workflow's declared parameters live server-side on the draft, and sending an empty list would read as "this candidate declares no parameters" rather than "I am not telling you about them". The server treats an absent list as none, which is the honest answer for a folder that has no record of them.
type ValidateResult ¶
type ValidateResult struct {
Findings []ValidateFinding `json:"findings"`
Resolved ValidateBindings `json:"resolved"`
}
ValidateResult mirrors rworkflow.ValidateResult.
func (*ValidateResult) Errors ¶
func (r *ValidateResult) Errors() []ValidateFinding
Errors returns the blocking findings, in the order the server reported them.
func (*ValidateResult) OK ¶
func (r *ValidateResult) OK() bool
OK reports whether the candidate would save cleanly. Warnings do not make it false — a save succeeds through them, and so does a push.
type Workflow ¶
type Workflow struct {
ID string `json:"id"`
WorkspaceID string `json:"workspaceID"`
FeatureID string `json:"featureID"`
Title string `json:"title"`
Description string `json:"description"`
Entrypoint string `json:"entrypoint"`
// Kind is the output-channel discriminator: unspecified|function|report|pipeline.
Kind string `json:"kind"`
Parameters []WorkflowParameter `json:"parameters"`
Variables map[string]any `json:"variables"`
UseDedicatedCompute bool `json:"useDedicatedCompute"`
// ApprovalGate is null on a workflow with no gate. `wf test` (Phase 4)
// preflights Enabled: a gated workflow cannot be run over HTTP at all,
// because only an agent session can produce an approval.
ApprovalGate *WorkflowApprovalGate `json:"approvalGate"`
// ParentWorkflowID is empty for a live row AND for a parentless draft (a
// brand-new workflow nobody has published yet). Non-empty on an edit draft
// and on a committed version snapshot.
ParentWorkflowID string `json:"parentWorkflowID"`
CommittedAt *time.Time `json:"committedAt"`
Hidden bool `json:"hidden"`
SecretIDs []string `json:"secretIDs"`
ExplicitSecretIDs []string `json:"explicitSecretIDs"`
InputTableIDs []string `json:"inputTableIDs"`
OutputTableIDs []string `json:"outputTableIDs"`
AgentIDs []string `json:"agentIDs"`
CodexIDs []string `json:"codexIDs"`
QuerySecretIDs []string `json:"querySecretIDs"`
PipPackages []string `json:"pipPackages"`
// Lifecycle is trigger-computed server-side: live|draft|version|archived|proposed.
Lifecycle string `json:"lifecycle"`
// SubmittedForReviewAt is set once a drafter asks an admin to commit their
// draft; null on every other row, and on a draft they have not submitted.
SubmittedForReviewAt *time.Time `json:"submittedForReviewAt"`
DrafterUserID string `json:"drafterUserID"`
// FeatureScope is the joined feature's scope (private|workspace|
// organization), stamped at read time — the field to branch on for privacy,
// not the legacy scope/private columns.
FeatureScope string `json:"featureScope"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// URL is the absolute frontend page for this row, stamped by the SERVER
// (api/v2/workflow.WorkflowView) on the single-row routes. Never built here:
// the instance URL a profile records is the API origin, and the deploy
// template puts the backend on api.* and the frontend on app.*, so a link
// assembled from it points at a host that serves no pages. In local dev the
// two differ even more sharply.
//
// EMPTY IS NORMAL AND SILENT. The field is omitempty server-side and an
// instance with no configured frontend origin emits none at all (see
// backend/lib/deeplink), which is the ordinary state of a dev box. A caller
// with no url prints no link — it never falls back to deriving one, and it
// never treats absence as an error.
//
// Absent on the LIST routes (GET /workflow/query, GET :id/versions) and on
// rows the server refuses to link, so read it off the response you have
// rather than assuming every Workflow value carries one.
URL string `json:"url,omitempty"`
}
Workflow is a HAND-MIRROR of the subset of rdb.Workflow the CLI reads.
Same rule as Me in auth.go, and the same hazard: a json tag that does not exist on the wire decodes to the zero value SILENTLY, so a typo here shows up as an empty column of output or — worse — as "no approval gate" on a workflow that has one. Every tag below is copied from backend/ronja/rdb/table_workflow.go; keep them in step with that file.
Two mirroring decisions worth knowing about:
- The server's optional.V[T] marshals as the bare value or JSON `null`. Unmarshalling `null` into a non-pointer is a documented no-op in encoding/json, so optional STRINGS are plain strings here ("" is exactly what "unset" means for an id) while optional TIMESTAMPS are pointers, because "is there a submitted-for-review stamp at all" is a question the CLI genuinely asks.
- The ID slices carry `omitempty` server-side, so an empty binding set is ABSENT from the JSON rather than `[]`. Normalize() replaces the resulting nils with empty slices so callers can range and compare without nil-checking every field.
This is a SUBSET, not the whole row: rdb.Workflow carries roughly twenty more columns (schedule and run bookkeeping, logic summaries, governance stamps) that no CLI command reads. Adding a field here is cheap and adding one that nothing consumes is dead weight nobody keeps in step — so the rule is "everything the CLI reads, plus what makes a read field legible", and the fixture test in workflow_test.go is what checks those tags against the wire.
func (*Workflow) IdentityID ¶
IdentityID is the STABLE id of the workflow this row belongs to: the parent for an edit draft or a version snapshot, the row itself for a live workflow or a parentless draft.
This is what a manifest binding records. A draft's own id is per-user and disappears on commit, so a committed file naming one would point at a row that stops existing the moment its author publishes.
type WorkflowApprovalGate ¶
type WorkflowApprovalGate struct {
Enabled bool `json:"enabled"`
}
WorkflowApprovalGate is a SUBSET of rdb.WorkflowApprovalGate: what the CLI acts on, which is whether the gate is switched on at all.
The rest of the server's shape — the approvers, the author's message, the context parameter names, and the whole email/Slack delivery tree — is deliberately NOT mirrored. A gate the CLI cannot run is a gate the CLI has nothing to say about, and a mirrored field nobody reads is a field nobody keeps in step with backend/ronja/rdb/table_workflow.go. Add them back when a command genuinely needs them (a `wf status` that names the approvers and says where the request goes is the obvious candidate), not before.
type WorkflowFile ¶
type WorkflowFile struct {
ID string `json:"id"`
WorkflowID string `json:"workflowID"`
Path string `json:"path"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
WorkflowFile mirrors rdb.WorkflowFile — one source file of one workflow row. There is no content hash on the wire, so the CLI hashes Content itself.
type WorkflowParameter ¶
type WorkflowParameter struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
Type string `json:"type"` // "string" | "number" | "date" | "select"
DefaultValue any `json:"defaultValue,omitempty"`
Required bool `json:"required"`
Options []string `json:"options,omitempty"`
OptionsQuery string `json:"optionsQuery,omitempty"`
}
WorkflowParameter mirrors rdb.WorkflowParameter. OptionsQuery is SQL carrying `{{ ref('tableID') }}` markers, which is why validation attributes findings from it to a synthetic parameters[N].optionsQuery path.
type WorkflowPatch ¶
type WorkflowPatch struct {
Title string `json:"title,omitempty"`
Entrypoint string `json:"entrypoint,omitempty"`
Parameters *[]WorkflowParameter `json:"parameters,omitempty"`
}
WorkflowPatch is the body of PUT /workflow/:id, mirroring the subset of rworkflow.Patch the CLI writes.
Two fields, both omitempty: every field of the server's Patch is an optional.V, so an absent key means "unchanged" while an explicit empty string would CLEAR the value. A folder whose manifest has lost its title must not be able to blank the workflow's.
Entrypoint is here because the manifest owns it: without this the row keeps running whatever file it was created with, and an entrypoint RENAME is impossible — the server refuses to delete the file the row still names. Ordering matters and belongs to the caller: rworkflow's Update refuses an entrypoint the workflow has no file for, so the new file must be written BEFORE this patch, and the old one deleted after it. Parameters is a POINTER for the reason the string fields are omitempty: the server's Patch.Parameters is an optional slice, so an absent key leaves the declaration alone while an explicit [] CLEARS it. Both are things a folder legitimately means — "I don't manage parameters" and "I declare none" — and a plain slice with omitempty collapses them into the first, making it impossible to remove the last parameter from a workflow.
func (WorkflowPatch) Empty ¶
func (p WorkflowPatch) Empty() bool
Empty reports a patch that would change nothing, so the caller can skip the round trip rather than send `{}`.
type WorkflowRun ¶
type WorkflowRun struct {
ID string `json:"id"`
WorkflowID string `json:"workflowID"`
ParameterValues map[string]any `json:"parameterValues"`
// Status is running|done|error — see the constants above.
Status string `json:"status"`
Outputs []WorkflowRunOutput `json:"outputs"`
TableOutputs []WorkflowRunTableOutput `json:"tableOutputs"`
// Logs is the run's FULL captured output, on the row itself. There is no
// streaming endpoint, which is why the CLI tails this at the end rather
// than following it live.
Logs string `json:"logs"`
Error *string `json:"error"`
ExecutedBy string `json:"executedBy"`
ExecutedAt time.Time `json:"executedAt"`
CompletedAt *time.Time `json:"completedAt"`
// ExecRunID / TraceID link the run to the generic execution telemetry and
// to the cross-primitive trace. Both nullable; the CLI carries them so a
// caller can follow either one into the web UI.
ExecRunID *string `json:"execRunID"`
TraceID *string `json:"traceID"`
// TriggeringRunKind / TriggeringRunID name the run that triggered this one.
// Always absent for a CLI-started run — it is a top-level execution — and
// mirrored anyway so the shape is the server's, not a CLI-shaped subset.
TriggeringRunKind *string `json:"triggeringRunKind"`
TriggeringRunID *string `json:"triggeringRunID"`
// ProcessingStartedAt is stamped by the Python harness the moment user code
// begins, so the gap from ExecutedAt is queue + container start. Absent when
// the container never reported in.
ProcessingStartedAt *time.Time `json:"processingStartedAt"`
// Kind is the workflow's output channel, denormalized at run-create time:
// unspecified|function|report|pipeline.
Kind string `json:"kind"`
// Body is the inline return value of a function-kind run, capped server-side
// at 8 KB.
Body *string `json:"body"`
// Telemetry counters, hydrated from the joined exec_run. Zero for legacy
// runs and for runs that made no AI call or query.
TokensUsed int64 `json:"tokensUsed"`
AICallCount int64 `json:"aiCallCount"`
QueryCount int64 `json:"queryCount"`
}
WorkflowRun mirrors rdb.WorkflowRun — one execution of one workflow row.
Mirrored in FULL rather than as a subset (unlike Workflow), because this struct is what `--json` prints: the CLI reads maybe six of these fields, but dropping the rest would quietly narrow the machine-readable output of a command whose whole purpose is to report what a run did.
The optional.V columns are pointers wherever "absent" is a real answer (a run still going has no completedAt; a successful one has no error). TenantID is deliberately not mirrored: it is the server's bookkeeping, and nothing at the command line has a use for it.
type WorkflowRunOutput ¶
type WorkflowRunOutput struct {
FileKey string `json:"fileKey"`
// Format is html|csv|xlsx|json|pdf|docx|xml.
Format string `json:"format"`
FileID string `json:"fileID,omitempty"`
// Name is the author-supplied filename, empty for unnamed outputs.
Name string `json:"name,omitempty"`
}
WorkflowRunOutput mirrors rdb.WorkflowRunOutput — one file a run produced.
type WorkflowRunTableOutput ¶
type WorkflowRunTableOutput struct {
ModelID string `json:"modelID"`
LogicalName string `json:"logicalName"`
DisplayName string `json:"displayName"`
// WriteMode is replace|append.
WriteMode string `json:"writeMode"`
FileCount int `json:"fileCount"`
}
WorkflowRunTableOutput mirrors rdb.WorkflowRunTableOutput — one TABLE a run wrote to. This is the thing `--write-live` exists for: WriteMode "replace" means the run replaced the table's contents.