endpoints

package
v0.143.0 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2026 License: Apache-2.0 Imports: 82 Imported by: 0

README

API end-points of DataTug agent

When DataTug agent is started with a serve command it listens on HTTP port (by default 8989).

datatug serve -p=./example

Who is answered

A serve route answers a request only when it comes from one of the server's own pages or from a tool on the same machine, and an answer names a source by its ID. One check stands in front of every route (the routes of the table below, the page at / and the answer to every OPTIONS request; see RequestGuard in request_guard.go):

  • the Host of the request is one that the server was started for: localhost, 127.0.0.1 or ::1, or the host given with --host, on the port that the server listens on;
  • the Origin of the request, when it has one, is on the list of the OPTIONS handler (IsSupportedOrigin: exactly https://datatug.app, and the host datatug.localhost over http or https with no port or a numeric one, for local UI development; nothing else, no flag adds one);
  • unless the server was built without an access secret (tests only), the request carries the session token (Authorization: Bearer), except a preflight and POST /datatug/session/exchange, which trades the one-time code of a link for the token (see pkg/agentaccess); a request without it is a 401;
  • the Sec-Fetch-Site of the request, when it has one, is same-origin, same-site or none, or its Origin is on the list.

A request that has neither an Origin nor a Sec-Fetch-Site (the CLI, curl, a script) is answered when its Host passes. Any other request is refused with a 403 whose body is one fixed sentence: it holds nothing the request said, and it names no Access-Control-Allow-Origin. An answer names an origin in Access-Control-Allow-Origin only when it is on the list.

A server that listens on a wildcard address (--host 0.0.0.0) answers Host: 0.0.0.0:<port> and the loopback names, and no other name: the host a client reaches it by has to be given with --host.

The live connection to a database server is a capability that is off by default (see GET /dbserver-databases below): datatug serve --allow-live-connections.

Record lookup

POST /datatug/records/lookup reads at most one row of a table of a registered source by its primary key, through the same policy-enforced executor as every other read (a denied principal gets 403 ACCESS_DENIED and no data). It is not registered in write-only mode and needs no write capability.

Request: {"project","env","source","schema","table","key":{"<pkCol>":<value>}}. schema may be ""; no other field may be left out; unknown fields are refused. project, env and source are plain names and the source is looked up in the registry of the environment (there is no default source); schema and table hold no ://, path separator or control character. A refused field is a 400 that does not echo its value, and nothing is looked up.

key names exactly the declared primary-key columns of the table, read live from the source (a source that was never scanned works), with a string, or a number or string that is an exact integer literal for an integer column (an optional - and digits, no +, space, leading zero, fraction or exponent; it must fit int64). An integer is read from its literal text, never through a float, so 9007199254740993 opens that record and not its neighbour. Values are bound as query parameters; identifiers come from the live schema only. A key that does not fit (unknown, non-key, missing or extra column, null, object, array, boolean, wrong type, a key type that is not integer or string, a table with no primary key) is 400 INVALID_REQUEST with field: "key" and a message that does not quote the request, and no query is issued.

The access decision (the session's policies, on the collection) is made before the source is opened or its schema read, on both routes: a principal the policy refuses gets one 403 ACCESS_DENIED whether the table exists or not and whatever the key looks like. Only the default schema of the driver (or no schema) names a table; any other schema is 404 NOT_FOUND with field: "schema", so the queried object is always the object the policy decided on. A key column that appears twice in key is a bad key.

An integer key above 2^53 - 1 reads up to 64 candidates (SQLite compares numbers as floats) and keeps the row whose key is exactly equal. If none is exact and the candidates cannot be compared (a key column the policy masks, or returned as a float or bytes) or the 64 are all used (keys above about 2^60), the answer is 500 INTERNAL, never 404 and never a neighbour.

Response 200: the contract Result of semantic/related/rows and exec/run_query (recordset with one row of typed values, limitations, provenance). Errors are the contract envelope: NOT_FOUND with field project, source, table or key (record not found), 400 INVALID_REQUEST / MISSING_PARAMETER, ACCESS_DENIED, SOURCE_UNAVAILABLE, TIMEOUT. The body, the key and the row are never logged.

POST /datatug/records/schema takes the same body without key and answers {"source","schema","table","columns":[{"name","type"}],"primaryKey":[...],"referencedBy":[...],"foreignKeys":[...]}. It reads no row. foreignKeys lists the outgoing foreign keys of the table, read live from the source's own catalog by its driver (pragma_foreign_key_list for SQLite, pg_catalog for PostgreSQL; identifiers come from the schema only and nothing of the request is interpolated): {"name","columns":[...],"refTable":{"schema","name"}, "refColumns":[...]}, with columns and refColumns in matching positions (composite keys keep their order). refTable.schema is the schema the reference is in: the one the source reports for another schema, else the schema of the request, else the default schema of the driver (main for SQLite, public for PostgreSQL), so it is never empty on those drivers. A key whose referenced table is in another schema than the default one, or that the principal may not read (the same access decision as for the table asked about), is left out, as is the same table in referencedBy: an allowed table never reveals the name or columns of a denied one. A key whose columns and refColumns differ in length is left out. SQLite names no constraint: its name is synthesized (<table>_fk_<id>) and is not a constraint name of the database. A table with no foreign key, or a source that reports none, answers [], never null. Both routes make the same access decision first, so a denied principal gets the identical 403 and learns nothing of the schema.

A key value that is a boolean or a non-integer number is a 400 bad-key on the lookup: the lookup binds strings and exact integers only (a float is never compared for equality, and the structured SQLite read binds no boolean). datatug daemon url --record refuses those kinds with exit 2 for the same reason.

Endpoints

Method Path Description
Executor
POST /exec/execute Executes a batch of commands
GET /exec/select Executes a single non mutating SELECT command
POST /records/lookup One record by the typed values of its declared primary key; read-only, see Record lookup
POST /records/schema The live schema (columns and types, primary key, referencing tables) of one table of a registered source, read from the source and not from a scan
Entities
GET /entities/all_entities
GET /entities/entity
POST /entities/create_entity Refuses an ID that is taken (400)
PUT /entities/save_entity
DELETE /entities/delete_entity 404 when the project has no such entity; 500 when the delete fails
Projects
POST /projects/create_project 501: not implemented (behind --allow-writes as every write)
Queries
GET /queries/get_query A failure to walk the queries tree of the project is queries of project "<id>" could not be loaded, as for all_queries
GET /queries/all_queries root is shared (the default) or personal; any other value is a 400 that does not quote it
POST /queries/create_query Legacy create-or-replace; needs --allow-writes and a project-write grant for the serving principal
PUT /queries/update_query Legacy create-or-replace; same authorization as create_query
DELETE /queries/delete_query Needs --allow-writes and a project-write grant
POST /queries/capture Save as project query: an atomic, revision-checked create (ifNoneMatch) or update (ifMatch) of a TugQL query pair; same authorization
Recordsets
GET /data/recordsets
GET /data/recordset_definition
GET /data/recordset_data 501: not implemented
POST /data/recordset_add_rows count, when given, is a whole number from 0 to 1000; any other is a 400
PUT /data/recordset_update_rows
DELETE /data/recordset_delete_rows
Folders
PUT /folders/create_folder
DELETE /folders/delete_folder 404 when the project has no such folder; 500 when the delete fails
DB servers
GET /dbserver-summary The db server of the project that the driver, host and port of the query name
GET /dbserver-databases The databases of a SQL Server server (driver=sqlserver) that the served project records. It connects to the server under the identity of the person who runs datatug serve, so it answers 403 unless datatug serve was started with --allow-live-connections. A project that is not served, a server that is not a plain reference and a server that the project does not record are refused with a 400 before anything is connected to; the connection and the query end after 15 seconds, or with the request, with could not list the databases of db server "<id>"
POST /dbserver-add
DELETE /dbserver-delete 404 when the project does not record the server; 500 when the delete fails
Boards
GET /boards/board
POST /boards/create_board
PUT /boards/save_board
DELETE /boards/delete_board 404 when the project has no such board; 500 when the delete fails

The routes of the table above that load a board, an entity, a recordset definition, a folder or a db server, and environment-summary, projects/projects_summary, projects/project_summary, projects/project_full, catalog-tables, queries/all_queries, the semantic routes, the resolution of the source of exec/select, exec/execute_commands and exec/run_query (the connection descriptor of a PostgreSQL catalog included), and the document of a saved query of exec/run_query, answer with a sentence that they build from the kind of what was asked for and its ID (board "b1" not found, could not delete board "b1") when the project store, a driver or a file of the project cannot be read, and read only what the served project records. What the store or the driver said, which quotes the paths of the server, is in the log of the server and is not in the answer.

A source is named by its ID in every answer, for every scheme. Where a route answers that the data file of a source is not there, or that a source could not be opened (the semantic routes, exec/run_query, exec/select and exec/execute_commands), the sentence is built at the route from the ID of the source:

Answer (503, SOURCE_UNAVAILABLE) Built from
the data file of source "<id>" does not exist (run datatug demo to fetch the demo project's data fixtures) the ID
source "<id>" could not be opened the ID

The text of pkg/dbcopy, which holds the display form of the source (the path of its file), is in the log of the server. The fixed sentences that name no source (the preview of PostgreSQL sources is off, a read through policies is not available, a URL that turns the read-only session off) are answered as they are, and so is the failure of a PostgreSQL source, whether it could not be opened or lost its connection: the sentence the adapter chose for the failure and the hint of where its connection string is read from (the server could not be reached; the PostgreSQL connection string is read from the environment variable <NAME>), which hold no host, port, database, user or password. The log says the same. The CLI's own commands (datatug query run, datatug db copy) show the sentences of pkg/dbcopy: a person at their own terminal sees their own paths.

Endpoint: POST /execute

Executes a batch of commands

Endpoint: GET /select

Executes a single non mutating SELECT command

Documentation

Index

Constants

View Source
const DevUIHost = "datatug.localhost"

DevUIHost is the one host, besides https://datatug.app, whose pages may call the agent: the local web UI development host (http or https, with or without a port).

Variables

View Source
var ErrInvalidQueriesRoot = errors.New("invalid queries root")

ErrInvalidQueriesRoot is handleError's INVALID_REQUEST trigger (field "root") for an unrecognized ?root= value — the same {code,field} shape api.ErrUnknownStoreID/ErrAmbiguousStore already give ?storage=.

Functions

func AgentInfo

func AgentInfo(w http.ResponseWriter, r *http.Request)

AgentInfo is GET agent-info's exact contract envelope (api-contract.md "Endpoint table"): principal with explicit roles/groups, securityContextId, the projects this process serves, and its capabilities. It replaces the old ad-hoc {version, uptimeMinutes, principal} shape (pkg/api.GetAgentInfo/AgentInfo) — Task 12 item 3.

version reports buildinfo.Get("datatug").Version — see buildInfoFunc. The contract (api-contract.md "Endpoint table", `agent-info` row) defines only `version` on this envelope; commit and build date are deliberately not added here, even though buildinfo.Info carries them, to avoid introducing fields the contract doesn't define.

func ExecuteCloudWatchCheck added in v0.117.0

func ExecuteCloudWatchCheck(ctx context.Context, localProject, operationID string, claim watch.Claim, bundle watchdaemon.CloudExecutionBundle, bindings map[string]string) (core.CheckRun, string, error)

ExecuteCloudWatchCheck evaluates an immutable hosted graph using the existing Core evaluator and accepted-query runner. Local source bindings are explicit operator grants and remain subject to the ordinary secured execution path.

func ExecuteWatchCheck added in v0.114.0

func ExecuteWatchCheck(ctx context.Context, project, environment, checkID, checkVersion, fingerprint, operationID string, inputs map[string]detective.InputValue) (detective.CheckRun, string, error)

ExecuteWatchCheck is a trusted local daemon boundary. It preserves Detective's project asset admission, revision pinning and secured saved-query path. This function is never registered as an unauthenticated HTTP route.

func IsSupportedOrigin

func IsSupportedOrigin(origin string) bool

IsSupportedOrigin reports whether origin is one the agent answers: exactly https://datatug.app (no port), or the exact host datatug.localhost with the scheme http or https and no port or a numeric one. Nothing else is on the list: not localhost or 127.0.0.1, not a subdomain of datatug.app, not another product's UI (founder decision 2026-10-15). The match is exact on scheme, host and port, never a prefix or a suffix test, and no flag or environment variable adds an origin.

It is the one list of origins: the OPTIONS handler, the request guard in front of every route and the answers' Access-Control-Allow-Origin all use it. An origin is a scheme, a host and maybe a port, and nothing else (the Origin of a browser never has a path, a query, a fragment or a user), so a text that has more is not on the list whatever it starts or ends with.

func Ping

func Ping(w http.ResponseWriter, _ *http.Request)

Ping return "pong" - is a simplest

func PrepareLocalVisibilityItems added in v0.133.0

func PrepareLocalVisibilityItems(ctx context.Context, scope datatug.ProjectLifecycleScope) (projectvisibility.Runtime, error)

PrepareLocalVisibilityItems uses the same owning services as their normal list endpoints. A registered project or private history record alone never grants access to another actor's working context.

func RegisterDatatugHandlers

func RegisterDatatugHandlers(
	pathPrefix string,
	router *httprouter.Router,
	mode RegisterMode,
	wrap wrapper,
	contextProvider func(r *http.Request) (context.Context, error),
	handler Handler,
)

RegisterDatatugHandlers registers datatug HTTP handlers with every write route closed (Capabilities{}) — kept for callers (this package's own tests) that do not need write access. Real `datatug serve` startup uses RegisterDatatugHandlersWithCapabilities.

func RegisterDatatugHandlersWithCapabilities added in v0.20.0

func RegisterDatatugHandlersWithCapabilities(
	pathPrefix string,
	router *httprouter.Router,
	mode RegisterMode,
	wrap wrapper,
	contextProvider func(r *http.Request) (context.Context, error),
	handler Handler,
	caps Capabilities,
)

RegisterDatatugHandlersWithCapabilities is RegisterDatatugHandlers plus an explicit Capabilities gate for this process's mutation routes.

Types

type Capabilities added in v0.20.0

type Capabilities struct {
	AllowWrites bool
	// AllowLiveConnections opens the routes that connect to a database server that the served
	// project records, under the identity of the person who runs the server (today
	// dbserver-databases, which lists the databases of a SQL Server server). It defaults false:
	// without it such a route answers 403 before anything is connected to (see
	// requireLiveConnections, and `datatug serve --allow-live-connections`).
	AllowLiveConnections bool
	// ServedHost and ServedPort are the host and the port that the server listens on. The Host
	// of a request must be a loopback name or address, or ServedHost, on ServedPort (see
	// RequestGuard). A ServedPort of 0 accepts any port.
	ServedHost string
	ServedPort int
	// Access, when set, is the agent's shared-token access (pkg/agentaccess): every route but a
	// preflight and the exchange of a one-time code requires its session token.
	Access *agentaccess.Access
}

Capabilities gates whether this process's mutation routes register at all (api-contract.md "Security and errors": "this read journey must not expose an unauthenticated mutation endpoint as a side effect" — REQ:principal-selection: "Unneeded write endpoints MUST fail closed"). AllowWrites defaults false: a `datatug serve` process serving the Phase 1 read journey registers every mutation route behind requireWriteCapability (write_capability.go), which refuses with ACCESS_DENIED before any project file is touched, unless the caller explicitly opted in (see cmd_serve.go's --allow-writes flag).

func (Capabilities) RequestGuard added in v0.57.0

func (c Capabilities) RequestGuard() RequestGuard

RequestGuard is the guard of a server started with these capabilities: the host and port it serves on are the ones of Capabilities.

type ErrorResponse

type ErrorResponse struct {
	Error string `json:"error"`
	Code  string `json:"code,omitempty"`
	Field string `json:"field,omitempty"`
}

ErrorResponse defines format of error response body. Code is set for a structured refusal (currently only "ACCESS_DENIED"); Field is reserved for a future per-field validation error and is not populated yet.

type Handler

type Handler = func(
	w http.ResponseWriter,
	r *http.Request,
	requestDTO apicore.RequestDTO,
	verifyOptions verify.RequestOptions,
	successStatusCode int,
	getContext apicore.ContextProvider,
	handler apicore.Worker,
)

Handler is responsible for creating context and call `handler()` func that should use provided context along with `requestDTO` that was populated from request body Its is exposed publicly so it can be replaced with custom implementation

type ProjectAgentEndpoints

type ProjectAgentEndpoints struct {
}

ProjectAgentEndpoints defines project endpoints

type ProjectEndpoints

type ProjectEndpoints interface {
	CreateProject(w http.ResponseWriter, r *http.Request)
	DeleteProject(w http.ResponseWriter, r *http.Request)
}

ProjectEndpoints defines project endpoints

type RegisterMode

type RegisterMode = int
const (
	RegisterWriteOnlyHandlers RegisterMode = iota
	RegisterAllHandlers
)

type RequestGuard added in v0.57.0

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

RequestGuard is the one check that stands in front of every route of the server. A route answers a request only when it comes from one of the server's own pages or from a tool on the same machine:

  • its Host is one that the server was started for: a loopback name or address (localhost, 127.0.0.1, ::1) or the host the person configured, with the port the server listens on. A page of another site that makes the browser reach this server by a name of its own (DNS rebinding) carries that name in Host;
  • its Origin, when the request has one, is on the list that the OPTIONS handler uses (IsSupportedOrigin);
  • its Sec-Fetch-Site, when the request has one, is same-origin, same-site or none, or its Origin is on the list. A request that carries neither header (the CLI, curl, a script) is answered when its Host passes.

When the server has an access secret, a request that is not a preflight or a code exchange must also carry the session token (`Authorization: Bearer`), or it is a 401.

A refusal is a 403 with a fixed sentence: no echo of what the request said, and no Access-Control-Allow-Origin. Every other answer to a request whose Origin is on the list, an error as much as a success, carries Access-Control-Allow-Origin for that Origin and Vary: Origin (writeCORSOrigin).

func NewRequestGuard added in v0.57.0

func NewRequestGuard(servedHost string, servedPort int) RequestGuard

NewRequestGuard builds the guard of a server that listens on servedHost and servedPort. The loopback names and addresses are always accepted, whatever servedHost is. A servedPort of 0 says that the port is not known and any port is accepted.

func (RequestGuard) Wrap added in v0.57.0

Wrap returns next behind the guard: the request is refused (see RequestGuard) or answered by next.

type VerifyRequest

type VerifyRequest struct {
	MinContentLength int64
	MaxContentLength int64
	AuthRequired     bool
}

VerifyRequest implements VerifyRequestOptions

func (VerifyRequest) AuthenticationRequired

func (v VerifyRequest) AuthenticationRequired() bool

AuthenticationRequired specifies if authentication is mandatory

func (VerifyRequest) MaximumContentLength

func (v VerifyRequest) MaximumContentLength() int64

MaximumContentLength defines max content length, if < 0 no limit

func (VerifyRequest) MinimumContentLength

func (v VerifyRequest) MinimumContentLength() int64

MinimumContentLength defines min content length

Source Files

Jump to

Keyboard shortcuts

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