From d3ecc0ac43b387377075938fbf3286f08a61999a Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Thu, 24 Sep 2026 14:29:43 +0300 Subject: [PATCH] fix(scaffold): a cli.service can name its stop signal; io.pilot.postgres 17.5.1 runs its server under the app postgres.start ran pg_ctl, which setsid()s the postmaster out of the adapter's process group: it outlived the app's SIGTERM, the supervisor's SIGKILL and daemon death on all 4 platforms. It is now a cli.service that runs the postgres binary itself in the foreground (no pg_ctl, no /bin/sh, so the port/datadir params can no longer reach a shell), TCP only (no Unix socket, so a long datadir no longer hits the sun_path limit). cli.service gains stop_signal (SIGTERM default, SIGINT, SIGQUIT): PostgreSQL's SIGTERM is a smart shutdown that waits for every client; SIGINT is its fast, clean one. postgres.exec gets a bare-name tools allowlist without pg_ctl, pg_upgrade and pg_createsubscriber (each daemonizes a postmaster), which also closes the '../' escape to any host binary; a passthrough postgres is a service. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/CLI-ADAPTER.md | 12 +++++ internal/publish/submission.go | 2 + internal/scaffold/config.go | 9 ++++ internal/scaffold/templates/main.go.tmpl | 4 +- internal/scaffold/templates/service.go.tmpl | 27 +++++++++-- .../scaffold/zz_cli_service_config_test.go | 2 + .../scaffold/zz_cli_service_lifetime_test.go | 38 +++++++++++++++ submissions/io.pilot.postgres/submission.json | 48 +++++++++++++------ 8 files changed, 121 insertions(+), 21 deletions(-) diff --git a/docs/CLI-ADAPTER.md b/docs/CLI-ADAPTER.md index 67eef85..d5ad413 100644 --- a/docs/CLI-ADAPTER.md +++ b/docs/CLI-ADAPTER.md @@ -74,6 +74,18 @@ down; on Linux it also gets SIGTERM from the kernel when the adapter dies. A starts, and a process that exits before it is ready (`--version`, a config error) returns its output like a plain command. +`stop_signal` (`SIGTERM`, the default, or `SIGINT` or `SIGQUIT`) replaces +SIGTERM in that clean stop. Use it when SIGTERM is not the server's fast clean +stop. PostgreSQL's SIGTERM is a "smart" shutdown that waits for every client to +disconnect, so one open connection would run out the 8 s and end in a SIGKILL +and crash recovery. Its SIGINT is a fast shutdown with a checkpoint. + +A server must never be started through a wrapper that daemonizes it. For +example, `pg_ctl start` forks the postmaster into its own session. Run the +server binary itself in the foreground. For PostgreSQL that is +`postgres -D -p -h 127.0.0.1`, with `stop_signal: SIGINT`. +Leave the wrapper out of a passthrough route's `cli.tools`. + On a passthrough route, `service.tools` names the tools that start a server (needs `cli.tools`). There are no `${field}` params, so readiness comes from the argv: with `ready_port_flag`, a call that passes that flag (`--port N` or diff --git a/internal/publish/submission.go b/internal/publish/submission.go index fa636cf..c3d6257 100644 --- a/internal/publish/submission.go +++ b/internal/publish/submission.go @@ -247,6 +247,7 @@ type SubCLIService struct { ForceArgs []string `json:"force_args,omitempty"` Tools []string `json:"tools,omitempty"` ReadyPortFlag string `json:"ready_port_flag,omitempty"` + StopSignal string `json:"stop_signal,omitempty"` } // SubParam is one structured input parameter (vs the old free-text field). @@ -742,6 +743,7 @@ func (s Submission) ToConfig() *scaffold.Config { ForceArgs: sv.ForceArgs, Tools: sv.Tools, ReadyPortFlag: sv.ReadyPortFlag, + StopSignal: sv.StopSignal, } } default: diff --git a/internal/scaffold/config.go b/internal/scaffold/config.go index 93e8555..82e3ad8 100644 --- a/internal/scaffold/config.go +++ b/internal/scaffold/config.go @@ -949,6 +949,10 @@ type CLIService struct { // `--port=N`), the server is ready once 127.0.0.1:N accepts (and the call // fails fast if something already does) instead of after ReadyAfter. ReadyPortFlag string `yaml:"ready_port_flag,omitempty"` + // StopSignal is sent to stop the server: SIGTERM (default), SIGINT or + // SIGQUIT; SIGKILL follows after a grace period. Use it when SIGTERM is + // not the server's fast clean stop (PostgreSQL: SIGINT). + StopSignal string `yaml:"stop_signal,omitempty"` } // ReadyAfterOrDefault / ReadyTimeoutOrDefault feed the generated Spec. @@ -1621,6 +1625,11 @@ func validateCLIService(i int, m Method, sv *CLIService) []error { if mt, err := time.ParseDuration(m.TimeoutFor()); err == nil && rt >= mt { errs = append(errs, fmt.Errorf("%s.ready_timeout %s must be below the method timeout %s", at, rt, mt)) } + switch sv.StopSignal { + case "", "SIGTERM", "SIGINT", "SIGQUIT": + default: + errs = append(errs, fmt.Errorf("%s.stop_signal %q must be SIGTERM, SIGINT or SIGQUIT", at, sv.StopSignal)) + } if m.CLI.Passthrough { if len(sv.Tools) == 0 { errs = append(errs, fmt.Errorf("%s on a passthrough route needs tools (the args[0] names that start a server)", at)) diff --git a/internal/scaffold/templates/main.go.tmpl b/internal/scaffold/templates/main.go.tmpl index 890020c..7936244 100644 --- a/internal/scaffold/templates/main.go.tmpl +++ b/internal/scaffold/templates/main.go.tmpl @@ -733,7 +733,7 @@ func jsonScalar(v json.RawMessage) string { func registerHandlers(d *ipc.Dispatcher, r *backend.Runner, c *backend.CloudClient, version, backendURL string) { {{- range .Methods}} {{- if .CLI}} - d.Register("{{.Name}}", forwardExec(r, backend.Spec{ {{if .CLI.Passthrough}}Passthrough: true{{if .CLI.Tools}}, Tools: {{printf "%#v" .CLI.Tools}}{{end}}{{else}}Args: {{printf "%#v" .CLI.Args}}, ParamsAsFlags: {{.CLI.ParamsAsFlags}}{{end}}{{with .CLI.Service}}, Service: &backend.ServiceSpec{ReadyTCP: {{printf "%q" .ReadyTCP}}, ReadyAfter: dur({{printf "%q" .ReadyAfterOrDefault}}), ReadyTimeout: dur({{printf "%q" .ReadyTimeoutOrDefault}}), LogFile: {{printf "%q" .LogFile}}, ForceArgs: {{printf "%#v" .ForceArgs}}, Tools: {{printf "%#v" .Tools}}, ReadyPortFlag: {{printf "%q" .ReadyPortFlag}}}{{end}} }, dur("{{.TimeoutFor}}"))) // {{.Duration}} (local) + d.Register("{{.Name}}", forwardExec(r, backend.Spec{ {{if .CLI.Passthrough}}Passthrough: true{{if .CLI.Tools}}, Tools: {{printf "%#v" .CLI.Tools}}{{end}}{{else}}Args: {{printf "%#v" .CLI.Args}}, ParamsAsFlags: {{.CLI.ParamsAsFlags}}{{end}}{{with .CLI.Service}}, Service: &backend.ServiceSpec{ReadyTCP: {{printf "%q" .ReadyTCP}}, ReadyAfter: dur({{printf "%q" .ReadyAfterOrDefault}}), ReadyTimeout: dur({{printf "%q" .ReadyTimeoutOrDefault}}), LogFile: {{printf "%q" .LogFile}}, ForceArgs: {{printf "%#v" .ForceArgs}}, Tools: {{printf "%#v" .Tools}}, ReadyPortFlag: {{printf "%q" .ReadyPortFlag}}, StopSignal: {{printf "%q" .StopSignal}}}{{end}} }, dur("{{.TimeoutFor}}"))) // {{.Duration}} (local) {{- else}} d.Register("{{.Name}}", cloudHandler(c, "{{.HTTP.Verb}}", "{{.HTTP.Path}}", {{if eq .HTTP.Path $.ProvisionPath}}true{{else}}false{{end}}, dur("{{.TimeoutFor}}"))) // {{.Duration}} (cloud) {{- end}} @@ -827,7 +827,7 @@ func decodeCall(spec backend.Spec, raw json.RawMessage) (backend.Call, error) { {{- else}} func registerHandlers(d *ipc.Dispatcher, r *backend.Runner, version string) { {{- range .Methods}} - d.Register("{{.Name}}", forwardExec(r, backend.Spec{ {{if .CLI.Passthrough}}Passthrough: true{{if .CLI.Tools}}, Tools: {{printf "%#v" .CLI.Tools}}{{end}}{{else}}Args: {{printf "%#v" .CLI.Args}}, ParamsAsFlags: {{.CLI.ParamsAsFlags}}{{end}}{{with .CLI.Service}}, Service: &backend.ServiceSpec{ReadyTCP: {{printf "%q" .ReadyTCP}}, ReadyAfter: dur({{printf "%q" .ReadyAfterOrDefault}}), ReadyTimeout: dur({{printf "%q" .ReadyTimeoutOrDefault}}), LogFile: {{printf "%q" .LogFile}}, ForceArgs: {{printf "%#v" .ForceArgs}}, Tools: {{printf "%#v" .Tools}}, ReadyPortFlag: {{printf "%q" .ReadyPortFlag}}}{{end}} }, dur("{{.TimeoutFor}}"))) // {{.Duration}} + d.Register("{{.Name}}", forwardExec(r, backend.Spec{ {{if .CLI.Passthrough}}Passthrough: true{{if .CLI.Tools}}, Tools: {{printf "%#v" .CLI.Tools}}{{end}}{{else}}Args: {{printf "%#v" .CLI.Args}}, ParamsAsFlags: {{.CLI.ParamsAsFlags}}{{end}}{{with .CLI.Service}}, Service: &backend.ServiceSpec{ReadyTCP: {{printf "%q" .ReadyTCP}}, ReadyAfter: dur({{printf "%q" .ReadyAfterOrDefault}}), ReadyTimeout: dur({{printf "%q" .ReadyTimeoutOrDefault}}), LogFile: {{printf "%q" .LogFile}}, ForceArgs: {{printf "%#v" .ForceArgs}}, Tools: {{printf "%#v" .Tools}}, ReadyPortFlag: {{printf "%q" .ReadyPortFlag}}, StopSignal: {{printf "%q" .StopSignal}}}{{end}} }, dur("{{.TimeoutFor}}"))) // {{.Duration}} {{- end}} d.Register("{{.Namespace}}.help", helpHandler(version)) } diff --git a/internal/scaffold/templates/service.go.tmpl b/internal/scaffold/templates/service.go.tmpl index d6f8225..c0e480d 100644 --- a/internal/scaffold/templates/service.go.tmpl +++ b/internal/scaffold/templates/service.go.tmpl @@ -67,6 +67,22 @@ type ServiceSpec struct { // binary's first launch can take seconds (macOS assesses it; Rosetta // translates it) before it listens. ReadyPortFlag string + // StopSignal asks the server to stop: "SIGTERM" (default), "SIGINT" or + // "SIGQUIT". Some servers treat SIGTERM as a slow, polite stop that waits + // for clients (PostgreSQL's "smart" shutdown); SIGINT is their fast, + // clean one. SIGKILL still follows after serviceStopGrace. + StopSignal string +} + +// stopSignal maps ServiceSpec.StopSignal to a signal (validated at generation). +func stopSignal(name string) syscall.Signal { + switch name { + case "SIGINT": + return syscall.SIGINT + case "SIGQUIT": + return syscall.SIGQUIT + } + return syscall.SIGTERM } // appliesTo reports whether this call starts a server. Every call of an @@ -92,6 +108,7 @@ type service struct { name string cmd *exec.Cmd out *tailBuffer + sig syscall.Signal // asks it to stop (ServiceSpec.StopSignal) done chan struct{} // closed once Wait has returned err error // Wait's result, valid after done } @@ -135,7 +152,7 @@ func (r *Runner) startService(ctx context.Context, sv *ServiceSpec, args []strin name = filepath.Base(argv[0]) } - s := &service{name: name, out: &tailBuffer{limit: serviceTailBytes}, done: make(chan struct{})} + s := &service{name: name, sig: stopSignal(sv.StopSignal), out: &tailBuffer{limit: serviceTailBytes}, done: make(chan struct{})} cmd := exec.Command(r.base[0], argv...) cmd.Env = r.childEnv() cmd.Stdout, cmd.Stderr = s.out, s.out @@ -229,9 +246,9 @@ func (r *Runner) startService(ctx context.Context, sv *ServiceSpec, args []strin } } -// stop SIGTERMs the service, then SIGKILLs it after grace, and waits for it. +// stop sends the service its stop signal, then SIGKILL after grace, and waits. func (s *service) stop(grace time.Duration) { - _ = s.cmd.Process.Signal(syscall.SIGTERM) + _ = s.cmd.Process.Signal(s.sig) select { case <-s.done: return @@ -259,7 +276,7 @@ func StopServices() { return } for _, s := range list { - _ = s.cmd.Process.Signal(syscall.SIGTERM) + _ = s.cmd.Process.Signal(s.sig) } deadline := time.After(serviceStopGrace) wait: @@ -274,7 +291,7 @@ wait: select { case <-s.done: default: - log.Printf("backend: service %s (pid %d) ignored SIGTERM for %s; killing it", s.name, s.cmd.Process.Pid, serviceStopGrace) + log.Printf("backend: service %s (pid %d) ignored %s for %s; killing it", s.name, s.cmd.Process.Pid, s.sig, serviceStopGrace) _ = s.cmd.Process.Kill() select { case <-s.done: diff --git a/internal/scaffold/zz_cli_service_config_test.go b/internal/scaffold/zz_cli_service_config_test.go index 79088d4..880a442 100644 --- a/internal/scaffold/zz_cli_service_config_test.go +++ b/internal/scaffold/zz_cli_service_config_test.go @@ -38,6 +38,8 @@ methods: {"service.tools on enumerated", ` cli: {args: ["x"], service: {tools: ["a"]}}`, "tools only applies to a passthrough route"}, {"ok ready_port_flag", ` cli: {passthrough: true, tools: ["a"], service: {tools: ["a"], ready_port_flag: "--port"}}`, ""}, {"ready_port_flag not a flag", ` cli: {passthrough: true, tools: ["a"], service: {tools: ["a"], ready_port_flag: "port"}}`, "must be a flag such as --port"}, + {"ok stop_signal", ` cli: {args: ["x"], service: {stop_signal: "SIGINT"}}`, ""}, + {"bad stop_signal", ` cli: {args: ["x"], service: {stop_signal: "SIGKILL"}}`, "stop_signal \"SIGKILL\" must be SIGTERM, SIGINT or SIGQUIT"}, {"ready_port_flag on enumerated", ` cli: {args: ["x"], service: {ready_port_flag: "--port"}}`, "ready_port_flag only applies to a passthrough route"}, } for _, tc := range cases { diff --git a/internal/scaffold/zz_cli_service_lifetime_test.go b/internal/scaffold/zz_cli_service_lifetime_test.go index 22e3ca5..4356de7 100644 --- a/internal/scaffold/zz_cli_service_lifetime_test.go +++ b/internal/scaffold/zz_cli_service_lifetime_test.go @@ -107,6 +107,7 @@ func TestCLIServiceLifetime(t *testing.T) { script := "#!/bin/sh\n" + "case \"$1\" in\n" + " serve) echo \"$$ $*\" > \"$2\"; trap 'echo term > \"$2.term\"; kill $! 2>/dev/null; exit 0' TERM; sleep 300 & wait;;\n" + + " servei) echo \"$$ $*\" > \"$2\"; trap 'echo int > \"$2.int\"; kill $! 2>/dev/null; exit 0' INT; trap 'echo term > \"$2.term\"; kill $! 2>/dev/null; exit 0' TERM; sleep 300 & wait;;\n" + " quick) echo quick-out; exit 3;;\n" + " listen) shift; exec env " + slowListenerEnv + "=1 '" + os.Args[0] + "' -test.run='^TestHelperSlowListener$' -- \"$@\";;\n" + " *) echo '{}';;\n" + @@ -128,6 +129,11 @@ methods: cli: args: ["serve", "${pidfile}"] service: {ready_after: "300ms", ready_timeout: "10s", force_args: ["--daemonize", "no"]} + - name: svctool.startint + summary: "Start a server whose fast clean stop is SIGINT." + cli: + args: ["servei", "${pidfile}"] + service: {ready_after: "300ms", ready_timeout: "10s", stop_signal: "SIGINT"} - name: svctool.listen summary: "Start a server that must answer on a port." cli: @@ -459,4 +465,36 @@ methods: t.Errorf("process group %d not empty after SIGTERM: %v", apid, left) } }) + + // stop_signal: a service declared with SIGINT (PostgreSQL's fast + // shutdown; its SIGTERM is a "smart" one that waits for every client) gets + // SIGINT, not SIGTERM, when the adapter stops. + t.Run("stop-signal", func(t *testing.T) { + dir := shortDir(t) + sock := filepath.Join(dir, "app.sock") + a := spawn(t, sock) + apid := a.Process.Pid + pf := filepath.Join(dir, "i.pid") + if _, err := call(sock, "svctool.startint", map[string]string{"pidfile": pf}); err != nil { + t.Fatalf("startint: %v", err) + } + pid, _ := readPid(t, pf) + _ = a.Process.Signal(syscall.SIGTERM) + st, _ := a.Process.Wait() + if st == nil || !st.Success() { + t.Errorf("adapter exit after SIGTERM = %v, want 0", st) + } + if procAlive(pid) { + t.Errorf("server %d alive after the adapter's SIGTERM exit", pid) + } + if _, err := os.Stat(pf + ".int"); err != nil { + t.Errorf("server was not stopped with its stop_signal SIGINT (no i.pid.int)") + } + if _, err := os.Stat(pf + ".term"); err == nil { + t.Errorf("server got SIGTERM although its stop_signal is SIGINT") + } + if left := groupMembers(apid); len(left) > 0 { + t.Errorf("process group %d not empty after SIGTERM: %v", apid, left) + } + }) } diff --git a/submissions/io.pilot.postgres/submission.json b/submissions/io.pilot.postgres/submission.json index 538147c..f2b4a0e 100644 --- a/submissions/io.pilot.postgres/submission.json +++ b/submissions/io.pilot.postgres/submission.json @@ -1,6 +1,6 @@ { "id": "io.pilot.postgres", - "version": "17.5.0", + "version": "17.5.1", "description": "PostgreSQL 17.5.0 as a native CLI for agents: stand up and manage a local Postgres server (initdb / start / stop / createdb) and run SQL with psql — aligned-table or CSV results, backslash schema introspection, database listing, and the full client/server toolchain (pg_dump, pg_restore, pg_isready, ...) via a verbatim-argv passthrough. Connects to a local or remote server via a libpq connection string or PG* env.", "email": "apps@pilotprotocol.network", "backend": { @@ -53,7 +53,7 @@ }, { "name": "postgres.start", - "description": "Start a local PostgreSQL server from an initialized `datadir`, listening on 127.0.0.1:`port` and on a Unix socket in `datadir`. Backgrounds the server and waits until it accepts connections; logs go to `/postgres.log`. After this, connect with uri `host=127.0.0.1 port= user=postgres dbname=postgres`. This is `pg_ctl -D -o '-p -k -h 127.0.0.1' -l /postgres.log -w start`.", + "description": "Start a local PostgreSQL server from an initialized `datadir`, listening on 127.0.0.1:`port` (TCP only, no Unix socket). Returns once the server accepts connections; logs go to `/postgres.log`. The server runs under this app (a foreground child, never daemonized) and stops when the app stops, restarts, or is uninstalled, so it cannot outlive the app; call postgres.start again after that. Fails if something already listens on 127.0.0.1:`port`. After this, connect with uri `host=127.0.0.1 port= user=postgres dbname=postgres`. This is `postgres -D -p -h 127.0.0.1 -c unix_socket_directories= -c logging_collector=on -c log_directory= -c log_filename=postgres.log`.", "latency": "med", "params": [ { @@ -71,21 +71,33 @@ ], "cli": { "args": [ - "pg_ctl", + "postgres", "-D", "${datadir}", - "-o", - "-p ${port} -k ${datadir} -h 127.0.0.1", - "-l", - "${datadir}/postgres.log", - "-w", - "start" - ] + "-p", + "${port}", + "-h", + "127.0.0.1", + "-c", + "unix_socket_directories=", + "-c", + "logging_collector=on", + "-c", + "log_directory=${datadir}", + "-c", + "log_filename=postgres.log" + ], + "service": { + "ready_tcp": "127.0.0.1:${port}", + "ready_timeout": "45s", + "log_file": "${datadir}/postgres.log", + "stop_signal": "SIGINT" + } } }, { "name": "postgres.stop", - "description": "Stop a running local server (fast shutdown) for the given `datadir`. This is `pg_ctl -D -m fast -w stop`.", + "description": "Stop a running local server (fast shutdown) for the given `datadir`. A server started by postgres.start also stops (fast shutdown) whenever the app stops. This is `pg_ctl -D -m fast -w stop`.", "latency": "med", "params": [ { @@ -299,7 +311,7 @@ }, { "name": "postgres.exec", - "description": "Run any PostgreSQL client/server tool shipped in this bundle with a verbatim argv — the full surface beyond the curated methods. Payload is {\"args\":[, ...]} where the first element is the tool name (psql, initdb, pg_ctl, createdb, dropdb, pg_isready, pg_dump, pg_restore, pg_dumpall, vacuumdb, pg_basebackup, postgres) and the rest are its flags; optional {\"stdin\":\"...\"} is piped to the tool. Examples: {\"args\":[\"psql\",\"-d\",\"postgresql://127.0.0.1:5599/postgres\",\"-A\",\"-t\",\"-c\",\"select 1\"]}; {\"args\":[\"pg_dump\",\"-h\",\"127.0.0.1\",\"-p\",\"5599\",\"-U\",\"postgres\",\"mydb\"]}; {\"args\":[\"psql\",\"-d\",\"\"],\"stdin\":\"select 1;\\nselect 2;\"}. Connection can also come from PG* env vars passed through to the child.", + "description": "Run any PostgreSQL client/server tool shipped in this bundle with a verbatim argv — the full surface beyond the curated methods. Payload is {\"args\":[, ...]} where the first element is one of the bundled tools (psql, initdb, createdb, dropdb, createuser, dropuser, pg_isready, pg_dump, pg_restore, pg_dumpall, vacuumdb, reindexdb, clusterdb, pg_basebackup, pg_config, pg_controldata, pg_amcheck, pg_verifybackup, pg_waldump, pgbench, postgres) and the rest are its flags; optional {\"stdin\":\"...\"} is piped to the tool. Examples: {\"args\":[\"psql\",\"-d\",\"postgresql://127.0.0.1:5599/postgres\",\"-A\",\"-t\",\"-c\",\"select 1\"]}; {\"args\":[\"pg_dump\",\"-h\",\"127.0.0.1\",\"-p\",\"5599\",\"-U\",\"postgres\",\"mydb\"]}; {\"args\":[\"psql\",\"-d\",\"\"],\"stdin\":\"select 1;\\nselect 2;\"}. Connection can also come from PG* env vars passed through to the child. A `postgres` server started here runs like postgres.start: under this app, returned once it accepts connections on 127.0.0.1:<-p port> (or, with no -p, once it has stayed up for 1s; a --version/config-error run just returns its output), and stopped (fast shutdown) when the app stops. pg_ctl is not available here because `pg_ctl start` detaches the server from the app; use postgres.start/stop/status, or SQL (SELECT pg_reload_conf(), pg_promote()).", "latency": "med", "params": [ { @@ -316,7 +328,15 @@ } ], "cli": { - "passthrough": true + "passthrough": true, + "tools": ["psql", "initdb", "createdb", "dropdb", "createuser", "dropuser", "pg_isready", "pg_dump", "pg_restore", "pg_dumpall", "vacuumdb", "reindexdb", "clusterdb", "pg_basebackup", "pg_config", "pg_controldata", "pg_amcheck", "pg_verifybackup", "pg_waldump", "pgbench", "postgres"], + "service": { + "tools": ["postgres"], + "ready_port_flag": "-p", + "ready_after": "1s", + "ready_timeout": "45s", + "stop_signal": "SIGINT" + } } }, { @@ -345,7 +365,7 @@ "listing": { "display_name": "PostgreSQL", "tagline": "Run and query PostgreSQL from an agent — local server lifecycle + psql, any libpq target", - "app_description": "# PostgreSQL (psql + local server) — native CLI for agents\n\nThis app installs the official **PostgreSQL 17.5.0** toolchain on the host and fronts it as typed methods. The bundle is a relocatable build of PostgreSQL 17.5.0 (from conda-forge, compiled from the upstream PostgreSQL sources) carrying the **complete client + server suite**: `psql`, `initdb`, `pg_ctl`, `postgres`, `createdb`, `dropdb`, `pg_isready`, `pg_dump`, `pg_restore`, `pg_dumpall`, `vacuumdb`, `pg_basebackup`. Every binary is sha-pinned and staged at install; a tiny `pg` dispatcher in the bundle routes each method to the right tool.\n\n## Two ways to use it\n\n**A) Talk to an existing PostgreSQL server.** Point the query methods at any reachable server with a libpq `uri` (or `PG*` env) — no local server needed:\n- `postgres.query` / `postgres.query_csv` — run SQL, get an aligned table or CSV.\n- `postgres.command` — backslash introspection (`\\dt`, `\\d table`, `\\du`, `\\l`, …).\n- `postgres.list` — list databases. `postgres.version` / `postgres.psql_help` — client version and full `--help`.\n\n**B) Run a database locally on this machine.** The app can provision and manage its own cluster — useful for an agent that needs a throwaway or embedded Postgres:\n\n1. **Configure (one-time):** `postgres.initdb` `{ \"datadir\": \"/path/to/pgdata\" }` — creates the cluster (superuser `postgres`, local `trust` auth).\n2. **Start:** `postgres.start` `{ \"datadir\": \"/path/to/pgdata\", \"port\": \"5599\" }` — boots the server on `127.0.0.1:5599` (+ a Unix socket in the datadir), waits until ready, logs to `/postgres.log`.\n3. **Create a database:** `postgres.createdb` `{ \"port\": \"5599\", \"dbname\": \"appdb\" }`.\n4. **Use it:** `postgres.query` with `uri = \"host=127.0.0.1 port=5599 user=postgres dbname=appdb\"`.\n5. **Health / teardown:** `postgres.ready` `{ \"port\": \"5599\" }`, `postgres.status` `{ \"datadir\": \"...\" }`, `postgres.stop` `{ \"datadir\": \"...\" }`.\n\n## Configuration\n\nPostgreSQL is configuration-rich; the knobs this app exposes:\n\n- **`datadir`** — where the cluster lives. Pick a writable absolute path (e.g. `$HOME/.pilot/pgdata` or a tmp dir). One cluster can hold many databases.\n- **`port`** — TCP port for the local server (default convention `5599`). The server also listens on a Unix socket inside `datadir`.\n- **Auth** — local clusters are initialized with `trust` (no password) for convenience; for any networked use, set a password (`postgres.exec` → `psql ... -c \"ALTER ROLE postgres PASSWORD '...'\"`) and supply it via the `uri` or `PGPASSWORD`.\n- **Non-root for the server** — `postgres.initdb`/`postgres.start` run the PostgreSQL **server**, which refuses to run as the OS `root` user (a PostgreSQL safety rule). On a normal host the pilot daemon runs as your user, so this just works; only fully-root environments (e.g. some containers) need a non-root user. The query methods against a remote server are unaffected and run anywhere.\n- **`uri`** — a full libpq connection string, either `postgresql://user:secret@host:5432/dbname?sslmode=require` or `host=... port=... dbname=... user=... sslmode=...`.\n- **`PG*` env** — `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGSSLMODE`, `PGOPTIONS`, `PGCONNECT_TIMEOUT`, `PGAPPNAME`, `PGCLIENTENCODING` are passed through to the child, so `postgres.exec` can connect with no inline credentials.\n- **Anything else** — full `postgresql.conf`/server flags are reachable via `postgres.exec` (e.g. `pg_ctl ... -o \"-c shared_buffers=256MB\"`), and per-session settings via SQL `SET`.\n\n## Good to know\n\n- Output returns verbatim where it is already clean; on a non-zero exit (SQL error, server down) the reply is `{stdout, stderr, exit}` so the caller sees everything the tool produced.\n- Runs on **macOS and Linux** (arm64 + amd64); binaries are fetched from the Pilot artifact registry and sha-pinned on install. Free and open source under the **PostgreSQL License**.\n- `postgres.help` lists every method with its latency class; this is the self-describing discovery contract.\n\n## `psql --help`\n```\npsql is the PostgreSQL interactive terminal.\n\nUsage:\n psql [OPTION]... [DBNAME [USERNAME]]\n\nGeneral options:\n -c, --command=COMMAND run only single command (SQL or internal) and exit\n -d, --dbname=DBNAME database name to connect to\n -f, --file=FILENAME execute commands from file, then exit\n -l, --list list available databases, then exit\n -v, --set=, --variable=NAME=VALUE\n set psql variable NAME to VALUE\n (e.g., -v ON_ERROR_STOP=1)\n -V, --version output version information, then exit\n -X, --no-psqlrc do not read startup file (~/.psqlrc)\n -1 (\"one\"), --single-transaction\n execute as a single transaction (if non-interactive)\n -?, --help[=options] show this help, then exit\n --help=commands list backslash commands, then exit\n --help=variables list special variables, then exit\n\nInput and output options:\n -a, --echo-all echo all input from script\n -b, --echo-errors echo failed commands\n -e, --echo-queries echo commands sent to server\n -E, --echo-hidden display queries that internal commands generate\n -L, --log-file=FILENAME send session log to file\n -n, --no-readline disable enhanced command line editing (readline)\n -o, --output=FILENAME send query results to file (or |pipe)\n -q, --quiet run quietly (no messages, only query output)\n -s, --single-step single-step mode (confirm each query)\n -S, --single-line single-line mode (end of line terminates SQL command)\n\nOutput format options:\n -A, --no-align unaligned table output mode\n --csv CSV (Comma-Separated Values) table output mode\n -F, --field-separator=STRING\n field separator for unaligned output (default: \"|\")\n -H, --html HTML table output mode\n -P, --pset=VAR[=ARG] set printing option VAR to ARG (see \\pset command)\n -R, --record-separator=STRING\n record separator for unaligned output (default: newline)\n -t, --tuples-only print rows only\n -T, --table-attr=TEXT set HTML table tag attributes (e.g., width, border)\n -x, --expanded turn on expanded table output\n -z, --field-separator-zero\n set field separator for unaligned output to zero byte\n -0, --record-separator-zero\n set record separator for unaligned output to zero byte\n\nConnection options:\n -h, --host=HOSTNAME database server host or socket directory\n -p, --port=PORT database server port\n -U, --username=USERNAME database user name\n -w, --no-password never prompt for password\n -W, --password force password prompt (should happen automatically)\n\nFor more information, type \"\\?\" (for internal commands) or \"\\help\" (for SQL\ncommands) from within psql, or consult the psql section in the PostgreSQL\ndocumentation.\n\nReport bugs to .\nPostgreSQL home page: \n```\n", + "app_description": "# PostgreSQL (psql + local server) — native CLI for agents\n\nThis app installs the official **PostgreSQL 17.5.0** toolchain on the host and fronts it as typed methods. The bundle is a relocatable build of PostgreSQL 17.5.0 (from conda-forge, compiled from the upstream PostgreSQL sources) carrying the **complete client + server suite**: `psql`, `initdb`, `pg_ctl`, `postgres`, `createdb`, `dropdb`, `pg_isready`, `pg_dump`, `pg_restore`, `pg_dumpall`, `vacuumdb`, `pg_basebackup`. Every binary is sha-pinned and staged at install; a tiny `pg` dispatcher in the bundle routes each method to the right tool.\n\n## Two ways to use it\n\n**A) Talk to an existing PostgreSQL server.** Point the query methods at any reachable server with a libpq `uri` (or `PG*` env) — no local server needed:\n- `postgres.query` / `postgres.query_csv` — run SQL, get an aligned table or CSV.\n- `postgres.command` — backslash introspection (`\\dt`, `\\d table`, `\\du`, `\\l`, …).\n- `postgres.list` — list databases. `postgres.version` / `postgres.psql_help` — client version and full `--help`.\n\n**B) Run a database locally on this machine.** The app can provision and manage its own cluster — useful for an agent that needs a throwaway or embedded Postgres:\n\n1. **Configure (one-time):** `postgres.initdb` `{ \"datadir\": \"/path/to/pgdata\" }` — creates the cluster (superuser `postgres`, local `trust` auth).\n2. **Start:** `postgres.start` `{ \"datadir\": \"/path/to/pgdata\", \"port\": \"5599\" }` — boots the server on `127.0.0.1:5599` (TCP only), waits until ready, logs to `/postgres.log`. The server runs under the app and stops when the app stops (daemon restart, upgrade, uninstall); start it again afterwards.\n3. **Create a database:** `postgres.createdb` `{ \"port\": \"5599\", \"dbname\": \"appdb\" }`.\n4. **Use it:** `postgres.query` with `uri = \"host=127.0.0.1 port=5599 user=postgres dbname=appdb\"`.\n5. **Health / teardown:** `postgres.ready` `{ \"port\": \"5599\" }`, `postgres.status` `{ \"datadir\": \"...\" }`, `postgres.stop` `{ \"datadir\": \"...\" }`.\n\n## Configuration\n\nPostgreSQL is configuration-rich; the knobs this app exposes:\n\n- **`datadir`** — where the cluster lives. Pick a writable absolute path (e.g. `$HOME/.pilot/pgdata` or a tmp dir). One cluster can hold many databases.\n- **`port`** — TCP port for the local server (default convention `5599`), bound to `127.0.0.1`. No Unix socket is created, so any `datadir` path length works.\n- **Auth** — local clusters are initialized with `trust` (no password) for convenience; for any networked use, set a password (`postgres.exec` → `psql ... -c \"ALTER ROLE postgres PASSWORD '...'\"`) and supply it via the `uri` or `PGPASSWORD`.\n- **Non-root for the server** — `postgres.initdb`/`postgres.start` run the PostgreSQL **server**, which refuses to run as the OS `root` user (a PostgreSQL safety rule). On a normal host the pilot daemon runs as your user, so this just works; only fully-root environments (e.g. some containers) need a non-root user. The query methods against a remote server are unaffected and run anywhere.\n- **`uri`** — a full libpq connection string, either `postgresql://user:secret@host:5432/dbname?sslmode=require` or `host=... port=... dbname=... user=... sslmode=...`.\n- **`PG*` env** — `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGSSLMODE`, `PGOPTIONS`, `PGCONNECT_TIMEOUT`, `PGAPPNAME`, `PGCLIENTENCODING` are passed through to the child, so `postgres.exec` can connect with no inline credentials.\n- **Anything else** — full `postgresql.conf` settings are reachable with SQL `ALTER SYSTEM SET ...` (then `postgres.stop` + `postgres.start`, or `SELECT pg_reload_conf()`), or by starting the server through `postgres.exec` (`[\"postgres\",\"-D\",\"\",\"-p\",\"5599\",\"-h\",\"127.0.0.1\",\"-c\",\"shared_buffers=256MB\"]`), and per-session settings via SQL `SET`.\n\n## Good to know\n\n- Output returns verbatim where it is already clean; on a non-zero exit (SQL error, server down) the reply is `{stdout, stderr, exit}` so the caller sees everything the tool produced.\n- Runs on **macOS and Linux** (arm64 + amd64); binaries are fetched from the Pilot artifact registry and sha-pinned on install. Free and open source under the **PostgreSQL License**.\n- `postgres.help` lists every method with its latency class; this is the self-describing discovery contract.\n\n## `psql --help`\n```\npsql is the PostgreSQL interactive terminal.\n\nUsage:\n psql [OPTION]... [DBNAME [USERNAME]]\n\nGeneral options:\n -c, --command=COMMAND run only single command (SQL or internal) and exit\n -d, --dbname=DBNAME database name to connect to\n -f, --file=FILENAME execute commands from file, then exit\n -l, --list list available databases, then exit\n -v, --set=, --variable=NAME=VALUE\n set psql variable NAME to VALUE\n (e.g., -v ON_ERROR_STOP=1)\n -V, --version output version information, then exit\n -X, --no-psqlrc do not read startup file (~/.psqlrc)\n -1 (\"one\"), --single-transaction\n execute as a single transaction (if non-interactive)\n -?, --help[=options] show this help, then exit\n --help=commands list backslash commands, then exit\n --help=variables list special variables, then exit\n\nInput and output options:\n -a, --echo-all echo all input from script\n -b, --echo-errors echo failed commands\n -e, --echo-queries echo commands sent to server\n -E, --echo-hidden display queries that internal commands generate\n -L, --log-file=FILENAME send session log to file\n -n, --no-readline disable enhanced command line editing (readline)\n -o, --output=FILENAME send query results to file (or |pipe)\n -q, --quiet run quietly (no messages, only query output)\n -s, --single-step single-step mode (confirm each query)\n -S, --single-line single-line mode (end of line terminates SQL command)\n\nOutput format options:\n -A, --no-align unaligned table output mode\n --csv CSV (Comma-Separated Values) table output mode\n -F, --field-separator=STRING\n field separator for unaligned output (default: \"|\")\n -H, --html HTML table output mode\n -P, --pset=VAR[=ARG] set printing option VAR to ARG (see \\pset command)\n -R, --record-separator=STRING\n record separator for unaligned output (default: newline)\n -t, --tuples-only print rows only\n -T, --table-attr=TEXT set HTML table tag attributes (e.g., width, border)\n -x, --expanded turn on expanded table output\n -z, --field-separator-zero\n set field separator for unaligned output to zero byte\n -0, --record-separator-zero\n set record separator for unaligned output to zero byte\n\nConnection options:\n -h, --host=HOSTNAME database server host or socket directory\n -p, --port=PORT database server port\n -U, --username=USERNAME database user name\n -w, --no-password never prompt for password\n -W, --password force password prompt (should happen automatically)\n\nFor more information, type \"\\?\" (for internal commands) or \"\\help\" (for SQL\ncommands) from within psql, or consult the psql section in the PostgreSQL\ndocumentation.\n\nReport bugs to .\nPostgreSQL home page: \n```\n", "license": "PostgreSQL", "homepage": "https://www.postgresql.org/docs/17/", "source_url": "https://github.com/postgres/postgres",