cli

package
v0.0.0-...-8b65ef4 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: Apache-2.0 Imports: 107 Imported by: 0

Documentation

Overview

Package cli — admin_bootstrap.go: ensure the admin endpoint has at least one valid bearer token at server boot (v2.3-3a task #28).

The first time the server comes up (admin_tokens table empty), we mint a system-owned superuser token, write the plaintext to `<datadir>/bootstrap_token` (mode 0600), and log a banner pointing at the file. Subsequent boots are a no-op — operators rotate the token via `agent-center admin token create / revoke`.

Why a file and not stdout: the operator launching the server may not even be the same user as the one who later runs `agent-center admin token list`; a file under datadir keeps the secret reachable across processes / sessions while still under the same uid.

Package cli — admin_client.go: HTTP-over-unix-socket Client that talks to the center process's admin endpoint.

Per conventions § 0.4 "AppService is the only entry": every CLI command outside the `server` boot path and a couple of schema-migration tools MUST round-trip through this Client rather than reach into the BC's Repositories / Services directly. The Client mirrors the admin endpoint surface registered in internal/admin/api/server.go (~79 methods grouped by BC).

Mirror of internal/workerdaemon/AdminClient (Phase C); we don't reuse that one because the CLI needs a much wider method surface and the worker-daemon transport intentionally exposes only the 5 methods the daemon itself needs.

Sub-files by BC:

  • admin_client_workforce.go (workers, proposals, agents, projects)
  • admin_client_conversation.go (conv/msg/channel/participant/derivation)
  • admin_client_taskruntime.go (task/exec/IR/artifact/dispatch/kill)
  • admin_client_discussion.go (issue lifecycle + bind/link)
  • admin_client_secret.go (user_secret CRUD)
  • admin_client_identity.go (identity register / find)
  • admin_client_observability.go (query / inspect / fleet / stats / logs)
  • admin_client_cognition.go (supervisor spawn / invocation / decision)

All sub-files share the doJSON / doPOST / doGET helpers defined here.

Package cli — admin_client_admintoken.go: Client methods for the AdminToken management surface (v2.3-3a task #28). Mirrors internal/admin/api/admintoken.go 1:1.

Package cli — admin_client_conversation.go: Client methods for the Conversation BC admin surface (ConvRepo / MsgRepo / MessageWriter / ChannelMgmtSvc / ParticipantMgmtSvc / CarryOverSvc / ConvRefRepo). Mirrors internal/admin/api/conversation.go 1:1.

19 endpoints registered in internal/admin/api/server.go under the `/admin/conversation/` prefix. Naming: methods on Client are named <Resource><Verb> to match the admin route segments (e.g. `ChannelCreate` for `POST /admin/conversation/channel/create`). Read methods return typed DTO structs whose JSON tags match the JSON keys emitted by the admin endpoint's projection helpers (convMap, messageMap, refsToMap) exactly.

v2.3-1 (task #24) closed the two prior v2.2 mismatches:

  • `POST /admin/conversation/participant/leave` now exists → ParticipantLeave method below; `channel leave` CLI goes through Client uniformly (no more direct-service fallback).
  • `GET /admin/conversation/msg/find-recent` now exists → MessageFindRecent method below; `conversation read --tail=N` and `conversation tail` no longer trim client-side off the 200-cap find-by-conversation-id helper.

Package cli — admin_client_observability.go: Client methods for the Observability BC admin surface (event find / query / inspect / fleet / stats / logs). Mirrors internal/admin/api/observability.go 1:1.

Logs/Open is special: the admin endpoint streams the gzipped blob body as `Content-Type: application/gzip` with the blob ref in `X-Blob-Ref`. The Client returns the raw body bytes (gzip-compressed) and the ref; handlers stream those bytes to stdout the same way the pre-refactor LogsSvc.Open path did.

Package cli — admin_client_secret.go: Client methods for the SecretManagement BC admin surface (user_secret CRUD + Resolve). Mirrors internal/admin/api/secret.go 1:1.

SecretResolve is v2.3-3b (task #29) and gated server-side by the `secret:resolve` scope. CLI tokens generally don't carry this scope — the method is here for test parity with the worker-daemon AdminClient (and so any future CLI command like `secret reveal` can plug in).

Package cli — admin_client_testhelper.go: test scaffolding that spins up an in-process admin endpoint and returns a Client pointing at it.

Use this in CLI handler tests instead of the legacy newTestApp() path (which wires Services directly on the App). v2.2 Phase B per docs/plans/v2.2-audits/v22-B-cli-refactor-audit.md: handlers must route through Client, not the Service fields.

Usage:

app, cleanup := setupAdminServerForTests(t)
defer cleanup()
// app.Client is wired; app.DB / app.WorkerRepo / ... are also
// populated because the helper builds a full App + serves it.

The helper is exported (lowercase but referenced from _test.go files in this same package) and lives in a non-test file so it can be referenced from package-level documentation/examples. It compiles into the production binary as dead code — harmless because the listener never starts until the helper is called.

Package cli — admin_client_workforce.go: Client methods for the Workforce BC admin surface (workers, proposals, projects). Mirrors internal/admin/api/workforce.go 1:1.

Naming: methods on Client are named <Resource><Verb> to match the admin route segments (e.g. `WorkerEnroll` for `POST /admin/workforce/worker/enroll`). Read methods return typed DTO structs whose JSON tags match the JSON keys emitted by the admin endpoint's projection helpers (`workerMap`, `proposalMap`, etc.).

handlers_install.go — `agent-center install` subcommand family. v2.4-D-A1 (task #35): skeleton + version detection + branch routing. A2 (task #36) implements the systemd / launchd install path; A5 (#39) implements the upgrade flow. A1 ships:

  • `agent-center install center [--prefix=...] [--user-mode]` — installs server on local machine (host A).
  • `agent-center install worker --bootstrap=... --token=... [...]` — installs worker daemon on local machine (host B).

Both commands detect existing installs and branch:

  • **Fresh**: no prior install at prefix → A2 will write binaries, units, start service.
  • **SameVersion**: install dir for this exact version already exists → idempotent no-op, exit 0 with "already installed" message.
  • **Upgrade**: different version exists → A5 will atomic-swap symlink + restart service + rollback on failure.

A1 implements the routing + clear error messages. The real install / upgrade work is stubbed (returns "not implemented in A1; coming in A2/A5") so the CLI shape is observable + testable before the implementation lands.

handlers_list_centers.go — `agent-center list-local-centers` (v2.7.1 #211).

Lists every center deployment installed on this machine so an operator (or a Tester running multiple instances in parallel) can see, at a glance, which instances exist, where they live, their ports, whether they run as a managed service or foreground, and whether the web console is currently reachable. Discovery is filesystem-based: it scans the install parent dir for `<base>` (the default instance) + `<base>.<instance>` prefixes that contain an etc/config.yaml.

handlers_list_workers.go — `agent-center list-local-workers` (v2.8 #170).

Symmetric to `list-local-centers` (#211): lists every worker deployment installed on this machine — both production workers (under the prod install parent, e.g. ~/.agent-center.<x>) and test-instance workers (under ~/.agent-center-test/<id>/worker-<n>), each tagged with its `namespace` (prod|test) so a single discovery surface serves both operators and the test-instance tooling (#255). Discovery is filesystem-based: a directory qualifies when it holds an etc/config.yaml that is a WORKER config (a `worker:` section / worker.db sqlite path, never a center's web_console).

handlers_test_instance.go — `agent-center install/uninstall/list test-instance` (v2.8 #255).

A one-command test/dev sandbox: spin up an isolated agent-center topology (1 center + N workers) under a namespace physically separate from the production install (`~/.agent-center-test/<id>/` vs `~/.agent-center`), with dynamically-allocated free ports (skipping :7000, macOS AirPlay #161) and per-instance launchd labels — so multiple sandboxes coexist and a Tester can drive UI/acceptance without hand-allocating ports or hand-editing config.

Design constraints (locked in #255 design):

  • Reuse the REAL install codepath: install drives installCenterFresh / installWorkerFresh in-process, so the generated config is the real thing (incl. blob_store, #159) — never hand-rolled (constraint #1). This file only chooses the namespace/ports/labels and orchestrates.
  • Workers auto-enroll on launchd start using the center's bootstrap token (scope=*) + pinned fingerprint — no org/user/seed needed. Tenant seeding (signin user, org/project/channel, --with-agent) is the #257 follow-up.
  • Cleanup is confined to the test namespace: uninstall only ever removes the `~/.agent-center-test/<id>/` subtree + the matching launchd labels, and `--id` is slug-validated so a caller cannot escape the root with `..` or an absolute path. The production `~/.agent-center` is never touched.

handlers_uninstall.go — `agent-center uninstall center|worker`.

v2.5.1 patch (#agent-center:5f6288e6, @oopslink ask msg=74fb3fa6). Inverts `install center|worker`: stop + unload the service unit, remove the install artefacts, leave the operator's data alone by default. `--purge` opts in to wiping `var/` + `logs/` and the install prefix itself.

Default-preserve rationale: var/ holds the sqlite database + master_key + worker-token + bootstrap_token — wiping those by accident is hard to undo. The expected reinstall-on-preserved-var path (uninstall → reinstall same prefix → existing data is reused; see ensureMasterKeyFile + sqlite Open auto-migration) is verified end-to-end in v2.5.1.

handlers_upgrade.go — `agent-center upgrade center|worker`.

v2.5.2 patch (@oopslink ask in #agent-center msg=8e5ea457). The upgrade path itself was already wired in v2.4-D-A5 (atomic symlink swap + health probe + auto-rollback under `install center` auto- detect); this file just exposes it as an explicit subcommand so operators can say "I want to upgrade" out loud instead of relying on the install handler's silent fresh-vs-upgrade branch.

Behaviour difference from `install center`:

  • Fresh prefix → `install` walks the fresh path. `upgrade` rejects with "no existing install at <prefix>; run `install center` first".
  • Same version → both walk the idempotent no-op path.
  • Different ver → both walk the atomic-swap upgrade path.

install_errors.go — friendly error wrapping for `agent-center install` failure modes. v2.4-D-A6 (task #40). Covers the install-command- reachable subset of deployment doc § 5's 12-row failure matrix.

**Out of A6 scope** (visible at runtime, not at install time):

  • "Cannot reach center at <host>:7300" — worker daemon's enroll attempt logs this to stderr after install completes
  • "Token already used" / "Token expired" / "fingerprint mismatch" / "worker name already enrolled" — same: emerge from worker daemon first enroll attempt
  • "DB migration failed" — server boot path; A5 health probe surfaces it via /admin/health timeout → rollback

**In A6 scope** (install-command-detectable):

  • "Port already in use" — pre-flight TCP bind on the configured web port + admin TCP port (if set)
  • "Need sudo" — wraps EACCES from systemd unit write with the friendly text suggesting `sudo` or `--user-mode`
  • "Disk full" — wraps ENOSPC from binary copy with the friendly "free up space" hint
  • "Same version already installed" — A1 already handles via state detection; this file adds the friendly recovery hint

install_fs.go — filesystem + service activation for `install center` / `install worker`. v2.4-D-A2 (task #36).

Responsibility split:

  • A2 (this file): write versioned binaries + service unit + config + atomic symlink swap (current → versions/<v>); activate service.
  • A5 (next ST): wrap the same flow with upgrade semantics — DB migration apply + service restart + health probe + auto-rollback.

On install success we print the operator-facing summary + URL + (for center) the bootstrap token. The version selection is the running binary's own version (installerVersion()).

install_platform.go — generate systemd unit (Linux) / launchd plist (Mac) for the center + worker services. v2.4-D-A2 (task #36).

Mac launchd path is the only one that must actually work for v2.4 PM acceptance (mac arm64 only per @oopslink 2026-05-26 scope narrow); systemd path is implemented + has unit tests for the unit-file rendering but is not validated end-to-end. Marked clearly so future iterations can validate on Linux without re-deriving the layout.

install_upgrade.go — atomic version-swap upgrade for `install center` and `install worker`. v2.4-D-A5 (task #39).

Sequence on `agent-center install center|worker` when an existing install is detected at a different version:

  1. Read `<prefix>/current` to capture the rollback target.
  2. Copy new-version binaries into `<prefix>/versions/<newver>/`.
  3. Write VERSION file.
  4. Atomic symlink swap `<prefix>/current` → `<prefix>/versions/<newver>`. (Service unit + config file unchanged — they point at `<prefix>/current/...` which is now the new version.)
  5. Restart the service via systemctl/launchctl.
  6. Health probe: poll `/admin/health` over unix socket (center) or `<launchctl|systemctl> is-active` (worker) for up to 10s.
  7. On any failure between (4)-(6): swap symlink BACK to the rollback target + restart + return error.

Config + unit files are NOT rewritten on upgrade — preserves operator edits and matches the "same command does install + upgrade" UX. DB migration: the new server binary auto-runs migrations on startup via the existing migrate.Up() path; we don't pre-apply, just rely on the boot path + health probe to confirm the migration finished cleanly.

Package cli implements the agent-center CLI router + handler registry.

Per plan-1 § 3.1.4: stdlib `flag` + hand-rolled sub-command router (R8 spike decision). The router supports nested verbs (e.g. `worker proposal accept`) by recursively matching positional args against a static command tree.

test_instance_agent.go — `install test-instance --with-agent` (v2.8 #261).

Phase 2 of the test-instance tenant layer. Builds on --with-seed (#257): after seeding a usable tenant, it org-enrolls the workers INTO the seeded org (via the org-scoped mint-enroll flow, using the owner's session) so they control-connect — resolving the #255 finding where admin-endpoint-enrolled workers stay workforce-registered but control-disconnect with 409 worker_not_org_enrolled. It then creates a real agent bound to a connected worker and dispatches a simple task so the agent runs and produces real tool_use/result events (closing the v2.7.1 #216 / §8 caveat source).

REORDER vs #255: workers are installed HERE (after the seed), with org-bound enroll tokens — not during provisionTestInstance (which, for --with-agent, installs the center alone). All driving is via the REAL center HTTP API.

test_instance_seed.go — `install test-instance --with-seed` (v2.8 #257).

After the test sandbox's center is healthy, drive a usable tenant through the REAL center HTTP API (the same endpoints a human/agent uses — never a SQL shortcut): signup (creates the owner user + org + auto-signin JWT) → create one project → create one channel. The resulting signin credentials + entity ids are folded into the access pack so a consumer can log into the UI and navigate entities with zero round-trips (closes the manual-seed handoff pain that every acceptance round hit).

Scope (#257 phase 1): tenant seed only. Worker org-enrollment + `--with-agent` (real agent producing tool events on a control-connected worker) is phase 2 — the #255 workers remain workforce-registered but not org-connected until then (see the access pack's workers_note).

Index

Constants

View Source
const (
	FormatTable = "table"
	FormatJSON  = "json"
	FormatText  = "text"
	FormatHuman = "human" // alias of FormatTable; not advertised.
)

Output format constants. P11 § 3.8: the CLI exposes three formats — `table` (default, human-readable), `json` (stable schema, snake_case keys; safe for scripting), and `text` (one canonical identifier per line; safe for `xargs`).

`human` is retained as a backwards-compatible alias of `table` so pre-§3.8 scripts that pass `--format=human` keep working. It is not advertised in help strings and `NormalizeFormat` collapses it onto `table`.

View Source
const (
	ExitOK                 = 0
	ExitBusinessError      = 1
	ExitUsage              = 2
	ExitVersionConflict    = 16
	ExitNotFound           = 17
	ExitInvalidTransition  = 18
	ExitInvariantViolation = 19
	ExitNotImplemented     = 64
	ExitSIGINT             = 130
)

Exit codes per 03-cli § 5.

View Source
const BootstrapTokenFilename = "bootstrap_token"

BootstrapTokenFilename is the on-disk name of the bootstrap token file (joined with the resolved datadir).

View Source
const DefaultInstance = "default"

DefaultInstance is the singleton / back-compat center deployment name (v2.7.1 #211). `install center` with no --instance = this; it keeps the legacy prefix (~/.agent-center) + launchd label (com.agent-center.center) so existing operators are unaffected.

Variables

View Source
var ErrClientNotConfigured = errors.New("admin client: not configured " +
	"(server.admin_socket_path missing or server not running)")

ErrClientNotConfigured is returned from Client methods when the Client wasn't constructed (e.g. CLI invoked without an admin socket configured).

View Source
var ErrServerUnreachable = errors.New("admin server unreachable")

ErrServerUnreachable is wrapped around network errors that indicate the admin socket isn't accepting connections. Handlers translate this into a user-facing "is the server running?" hint.

Functions

func EnsureBootstrapToken

func EnsureBootstrapToken(ctx context.Context, app *App, datadir string, logger func(string)) error

EnsureBootstrapToken mints a system token + writes plaintext to disk iff the admin_tokens table is currently empty.

datadir may be empty — in that case we fall back to filepath.Dir of the configured sqlite path (cfg.Server.SqlitePath). logger is the boot-banner writer (typically a fmt.Fprintf wrapper on stderr).

Returns nil on success or when bootstrap is unnecessary (table non-empty). Failure to write the file is fatal — the operator MUST have a way to authenticate, and silently swallowing the error would produce a deployment where the admin endpoint exists but nobody can reach it.

func EnsureSocketExists

func EnsureSocketExists(c *Client) error

EnsureSocketExists is a courtesy preflight: if the configured socket doesn't exist on disk, return a friendly error pointing at how to start the server. Network-level failures (connect refused after the socket exists) surface as ErrServerUnreachable during the first call.

Returns nil when the socket file is present OR when c is nil (handlers that can run without the client should be unaffected).

func FormatJSONError

func FormatJSONError(reason, message string) string

FormatJSONError renders an error response.

func GlobalConfigPath

func GlobalConfigPath() string

GlobalConfigPath returns the resolved config path published by BuildRouter (--config flag or AGENT_CENTER_CONFIG env). Empty when neither is set.

func IsClientNotConfigured

func IsClientNotConfigured(err error) bool

IsClientNotConfigured reports whether err signals an absent Client.

func IsServerUnreachable

func IsServerUnreachable(err error) bool

IsServerUnreachable reports whether err looks like the server isn't accepting connections (socket missing, ECONNREFUSED, etc.).

func IsValidFormat

func IsValidFormat(in string) bool

IsValidFormat reports whether the input is an accepted format string.

func NormalizeFormat

func NormalizeFormat(in string) (string, bool)

NormalizeFormat maps an input format string to one of the three canonical values. Empty input defaults to `table`. `human` aliases to `table`. Returns ok=false when the input is unrecognised.

func OpenAndMigrate

func OpenAndMigrate(cfg config.Config) (*sql.DB, error)

OpenAndMigrate is a convenience that opens the DB pointed to by cfg and runs migrations. The caller is responsible for closing the DB.

func ParseFormat

func ParseFormat(v string) string

ParseFormat returns "human" or "json" (or yaml in the future); defaults to "human" for empty input.

func ResolvedBuildBranch

func ResolvedBuildBranch() string

ResolvedBuildBranch returns the linker-injected branch, or "unknown".

func ResolvedBuildBuiltAt

func ResolvedBuildBuiltAt() string

ResolvedBuildBuiltAt returns the linker-injected build timestamp, or "unknown".

func ResolvedBuildCommit

func ResolvedBuildCommit() string

ResolvedBuildCommit returns the linker-injected commit (the same value the install command uses via installerCommit), exported so server-side surfaces (e.g. /api/system/version) can echo it. Falls back to "unknown".

func ResolvedBuildVersion

func ResolvedBuildVersion() string

ResolvedBuildVersion returns the linker-injected version string if main() called SetInstallBuildVersion, otherwise "dev". Mirrors installerVersion() but exported so other server-side surfaces (e.g. /api/health) can echo the same value the install command printed. v2.4-D-X1 fix B10.

func SetGlobalConfigPath

func SetGlobalConfigPath(p string)

SetGlobalConfigPath is called by BuildRouter to publish the global --config flag value to the system command handlers.

func SetInstallBuildBranch

func SetInstallBuildBranch(b string)

SetInstallBuildBranch threads main.buildBranch from the binary's main(). No-op for the empty/"unknown" sentinel so `go run` stays branch-agnostic.

func SetInstallBuildBuiltAt

func SetInstallBuildBuiltAt(t string)

SetInstallBuildBuiltAt threads main.buildBuiltAt from the binary's main().

func SetInstallBuildCommit

func SetInstallBuildCommit(c string)

SetInstallBuildCommit threads main.buildCommit into the install command (v2.7.1 #234). No-ops for the "unknown" sentinel so `go run` stays commit-agnostic. Tests mutate installBuildCommit directly with a restore-on-defer pattern.

func SetInstallBuildVersion

func SetInstallBuildVersion(v string)

SetInstallBuildVersion lets the binary's main() thread the linker- injected buildVersion into the install command. Called only when buildVersion is non-empty and not the "dev" sentinel; the empty case stays "dev" for `go run` / unversioned builds. Tests don't call this — they mutate installBuildVersion directly with a restore-on-defer pattern.

func StripGlobalFlags

func StripGlobalFlags(args []string, cfgPath string) []string

StripGlobalFlags removes the global --config / -c flags from args because they're handled out-of-band by BuildRouter.

Types

type AdminTokenCreateRequest

type AdminTokenCreateRequest struct {
	Owner     string   `json:"owner"`
	Scopes    []string `json:"scopes"`
	CreatedBy string   `json:"created_by"`
}

AdminTokenCreateRequest is the body for /admin/admintoken/create.

type AdminTokenCreateResponse

type AdminTokenCreateResponse struct {
	ID        string `json:"id"`
	Plaintext string `json:"plaintext"`
}

AdminTokenCreateResponse is the success body — id + plaintext. The plaintext is the operator's only chance to see the bearer; the server never echoes it again.

type AdminTokenDTO

type AdminTokenDTO struct {
	ID            string   `json:"id"`
	Owner         string   `json:"owner"`
	Scopes        []string `json:"scopes"`
	CreatedAt     string   `json:"created_at"`
	CreatedBy     string   `json:"created_by"`
	Version       int      `json:"version"`
	RevokedAt     string   `json:"revoked_at,omitempty"`
	RevokedBy     string   `json:"revoked_by,omitempty"`
	RevokedReason string   `json:"revoked_reason,omitempty"`
	LastUsedAt    string   `json:"last_used_at,omitempty"`
}

AdminTokenDTO mirrors the JSON envelope returned by list/show. It intentionally omits plaintext + value_hash per ADR-aligned policy.

type AdminTokenRevokeRequest

type AdminTokenRevokeRequest struct {
	ID     string `json:"id"`
	Reason string `json:"reason"`
}

AdminTokenRevokeRequest is the body for /admin/admintoken/revoke.

type AdminTransportConfig

type AdminTransportConfig struct {
	SocketPath      string
	TCPListenAddr   string
	TLSCertPath     string
	TLSKeyPath      string
	FingerprintPath string
	Hostname        string
}

AdminTransportConfig captures the v2.3-7a (task #27) admin listener configuration: optional unix socket + optional TCP+TLS address with auto-managed cert + fingerprint files. At least one of SocketPath or TCPListenAddr must be non-empty.

type AdminTransportInfo

type AdminTransportInfo struct {
	TLSFingerprint   string
	TLSCertNotAfter  time.Time
	TLSCertGenerated bool
	TLSExpiryWarn    bool
	TLSExpiryDays    int
}

AdminTransportInfo is what runAdminEndpoint returns to the caller (boot banner code in handlers_system.go uses this to print the cert fingerprint, expiry, etc.).

type App

type App struct {
	Config config.Config

	// Client is the admin transport. Populated in CLI mode; may be nil
	// in server mode (the server doesn't dial itself).
	Client *Client

	// DB / Clock / IDGen / Service / Repo fields below are wired only
	// in server mode (NewApp). CLI mode (NewClientApp) leaves them nil.
	DB       *sql.DB
	Clock    clock.Clock
	IDGen    idgen.Generator
	DBHealth *persistence.DBHealthMonitor

	// RuntimeImportValidationKey signs AI Runtime Preview/Apply tokens. It is
	// derived from the restart-stable server master key.
	RuntimeImportValidationKey []byte

	WorkerRepo workforce.WorkerRepository
	// PMProjectRepo is the new-model (pm) project repo used by the
	// operator-scoped CLI project READ handlers (list/show). v2.7 #131
	// PR-3 — the LOCAL list path uses its operator-global ListAll.
	PMProjectRepo pm.ProjectRepository
	ConvRepo      conversation.ConversationRepository
	MsgRepo       conversation.MessageRepository
	EventRepo     *obsqlite.EventRepo
	Sink          *observability.EventSink
	Authorization *authorization.Service

	// Usage BC (v2.15.0 I28/F2): usage_events + model_prices repos backing the
	// report_usage agent-tool.
	UsageEventRepo *usagesql.UsageEventRepo
	ModelPriceRepo *usagesql.ModelPriceRepo

	EnrollSvc *wfservice.WorkerEnrollService
	// WorkerConfigSvc backs the operator per-CLI capability toggle (v2.7 #147).
	WorkerConfigSvc *wfservice.WorkerConfigService

	// v2.7 ProjectManager BC AppService facade (ADR-0046) — backs the nested
	// /api/projects/{project_id}/... routes + produces the outbox events the
	// server-runtime relay projects into Conversation/Agent.
	PMService *pmservice.Service

	// CodeRepoService is the v2.18.4 BE-1 workspace CodeRepo AppService (issue-f980c8de)
	// — workspace Repos CRUD + encrypted credential storage + the merge-check resolver.
	CodeRepoService *coderepservice.Service

	// OrchService is the T768 orchestration engine AppService backing the 18 agent MCP
	// graph/node/edge tools (admin-api HandlerDeps.OrchService). The SAME instance is
	// injected into PMService for graph-backed plan dispatch.
	OrchService *orch.Service

	// LiveState is the per-agent live executor snapshot store (v2.19.0): the SAME
	// instance is wired into the admin heartbeat handler (writer) and the webconsole
	// .../agents/{id}/concurrency endpoint (reader).
	LiveState concurrency.LiveStateStore

	// AgentService is the v2.7 Agent BC AppService facade (C3).
	AgentService *agentsvc.Service

	// AgentRepo is the raw Agent repository (v2.7 D2-f s4). The worker boot-resume
	// admin endpoint enumerates a worker's agents (ListByWorker) — a worker-level
	// read with no fitting AppService method, so the repo is exposed directly.
	AgentRepo agentpkg.Repository

	// AgentActivityRepo is the append-only Agent activity-event repository (C2).
	// The admin controller→center feedback surface (v2.7 D2-c-i activity sink)
	// reads it back in tests; writes go through AgentService.AppendActivity.
	AgentActivityRepo agentpkg.ActivityEventRepository
	TeamRuleAuditRepo *ruleregistrysqlite.AuditRepo

	// EnvControlSvc is the v2.7 Environment BC control-channel AppService
	// (D1, ADR-0050, task #102) — backs the additive /admin/environment/...
	// worker control endpoints.
	EnvControlSvc *envservice.EnvControl

	// ControlStreamBus is the v2.7 D5 slice-1 center-side SSE down-push bus. A
	// single shared instance: the projector's ControlLog publishes appended
	// commands here (after commit, best-effort), and the
	// /admin/environment/worker/commands/stream endpoint subscribes workers to
	// it. Same WorkerControlEvent log backs both push + poll.
	ControlStreamBus *controlstream.Bus

	// RuntimeFsDispatcher is the I5 (issue-921db054) agent-runtime-browser correlator
	// — ONE shared instance: the webconsole runtime endpoints Register+await a req_id
	// here, and the admin /admin/environment/agent/runtime-fs/response endpoint
	// Resolves the worker's reply against it. Both servers must hold the SAME pointer.
	RuntimeFsDispatcher *runtimefs.Dispatcher

	MessageWriter      *convservice.MessageWriter
	ChannelMgmtSvc     *convservice.ChannelManagementService
	ParticipantMgmtSvc *convservice.ParticipantManagementService
	CarryOverSvc       *convservice.CarryOverService
	ConvRefRepo        conversation.ConversationMessageReferenceRepository
	ReadStateRepo      conversation.UserConversationReadStateRepository
	ReadStateSvc       *convservice.ReadStateService
	InboxSvc           *convservice.AgentInboxService
	FollowStateRepo    conversation.UserConversationFollowStateRepository
	FollowStateSvc     *convservice.FollowStateService

	// WakeGuard is the ONE process-singleton wake-chain circuit breaker (I7-D1).
	// It holds the rate/cycle/depth anti-storm runtime state shared across ALL
	// agent→agent down-pushes. Hoisted onto App (formerly a webconsole-wiring
	// local) so the reply-guardrail (T341) gates agent-authored reply nudges
	// through the SAME instance as wake delivery — one shared budget, no
	// ungoverned ping-pong. Config is resolved LIVE from center settings.
	WakeGuard *wakeguard.Guard
	// ReplyNudgeSvc is the server-side reply-guardrail (T341, 方案 A). At
	// turn-end + TrueIdle the worker asks it which directed replies an idle agent
	// still owes; it derives them from the message log + read-state, gates
	// agent-authored ones through WakeGuard, and returns bounded re-inject prompts.
	ReplyNudgeSvc *convservice.ReplyNudgeService

	// OutboxRepo is the cross-BC outbox emitter (v2.7 D2-e-ii). The MessageWriter
	// uses it to emit `conversation.message_added` in the same tx as the message
	// append (the conversational-wake trigger). (The retired request_input →
	// agent.awaiting_input emit no longer exists; AgentWorkItem was removed in F7.)
	OutboxRepo outbox.Repository

	// SecretManagement (P11 § 3.7b)
	UserSecretRepo secretmgmt.UserSecretRepository
	UserSecretSvc  *secretservice.UserSecretService
	// UserSecretResolveSvc gates plaintext via SecretResolutionService.
	// v2.3-3b (task #29): wired alongside UserSecretSvc when master key is
	// loaded, so the admin endpoint can serve secret:resolve to worker
	// daemons that hold a `secret:resolve`-scoped admin token.
	UserSecretResolveSvc *secretservice.SecretResolutionService

	// AdminToken (v2.3-3a task #28) — bearer tokens that gate the admin
	// endpoint. Server mode wires both fields; CLI mode (NewClientApp)
	// leaves them nil — the CLI talks to a server that already has them.
	AdminTokenRepo admintoken.Repository
	AdminTokenSvc  *admintokensvc.Service

	// v2.6: Identity BC services.
	IdentitySignupSvc           *identity.SignupService
	IdentitySigninSvc           *identity.SigninService
	IdentitySignoutSvc          *identity.SignoutService
	IdentityAuthSvc             *identity.AuthService
	IdentityPasscodeChangeSvc   *identity.PasscodeChangeService
	IdentityRepo                identity.IdentityRepository
	IdentityOrgRepo             identity.OrganizationRepository
	IdentityOrgCreateSvc        *identity.OrganizationCreateService
	IdentityOrgLifecycleSvc     *identity.OrganizationLifecycleService
	IdentityMemberRepo          identity.MemberRepository
	IdentityMemberAddSvc        *identity.MemberAddService
	IdentityMemberCreateUserSvc *identity.MemberCreateUserService
	IdentityMemberRoleChangeSvc *identity.MemberRoleChangeService
	IdentityMemberDisableSvc    *identity.MemberDisableService
	IdentityMemberRemoveSvc     *identity.MemberRemoveService
	IdentityAgentProvisionSvc   *identity.AgentIdentityProvisionService
	IdentityOrgUpdateSvc        *identity.OrganizationUpdateService
	IdentityInvitationRepo      identity.InvitationRepository

	// Observability Phase 4
	QuerySvc               *query.Service
	FleetSvc               *query.FleetSnapshotService
	StatsSvc               *query.StatsService
	LogsSvc                *query.LogsService
	BlobStore              blobstore.BlobStore
	InsightSvc             *insight.Service
	InsightObservationRepo *insight.ObservationRepo
}

App carries everything CLI handlers need.

Two construction modes (v2.2 Phase B per conventions § 0.4):

  • **Server mode**: NewApp opens the DB and wires every Service / Repo field. Used by `agent-center server` (handlers_system.go) which IS the process that owns the DB and serves the admin endpoint.
  • **CLI mode**: NewClientApp builds a lightweight App with only Config + Clock + Client populated; every Service / Repo field is nil. CLI handlers go through Client to talk to a running server (the only legitimate AppService entry point per § 0.4).

Handlers that need to run in BOTH modes (rare; mostly only server boot + schema migrations) must consult whichever fields the active mode populates. The pattern across handlers_*.go (post-Phase B migration) is: prefer `a.Client.<Method>` over `a.<Svc>.<Method>`.

func NewApp

func NewApp(cfg config.Config, db *sql.DB, clk clock.Clock) (*App, error)

NewApp wires the full dependency graph from a Config. The DB must already be open + migrated.

func NewClientApp

func NewClientApp(cfg config.Config, client *Client) *App

NewClientApp constructs a lightweight CLI-mode App. The DB / Service / Repo fields are intentionally nil — handlers MUST go through Client (v2.2 Phase B; conventions § 0.4 "AppService is the only entry").

Use this for every CLI command except `agent-center server` (which IS the server and uses NewApp + an open DB).

func (*App) AdminCommands

func (a *App) AdminCommands() []*Command

AdminCommands returns the admin command subtree per 03-cli-subcommands § 8.x. Phase 7 lands `backup`; v2.3-3a (task #28) adds the `token` group (create / list / revoke).

func (*App) WorkerCommands

func (a *App) WorkerCommands() []*Command

WorkerCommands returns the `worker` command tree (excluding the worker daemon `worker run` placeholder which lives in handlers_system.go).

v2.2 Phase B (per docs/plans/v2.2-audits/v22-B-cli-refactor-audit.md): every handler in this file now routes through a.Client (admin endpoint) when a Client is configured. A transitional fallback to direct Service / Repo access remains for the test path that constructs an App via newTestApp() without a Client; that fallback dies once the next phase of v2.2-B converts test scaffolding to use setupAdminServerForTests (see admin_client_testhelper.go).

type ChannelArchiveRequest

type ChannelArchiveRequest struct {
	Name       string `json:"name"`
	ArchivedBy string `json:"archived_by"`
}

ChannelArchiveRequest mirrors api archiveChannelReq.

type ChannelCreateRequest

type ChannelCreateRequest struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	CreatedBy   string `json:"created_by"`
}

ChannelCreateRequest mirrors api createChannelReq.

type ChannelCreateResponse

type ChannelCreateResponse struct {
	ConversationID string `json:"conversation_id"`
	EventID        string `json:"event_id"`
}

ChannelCreateResponse mirrors the success projection.

type Client

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

Client is the CLI-side admin transport. It dials either a unix-domain socket (default; cfg.Server.AdminSocketPath) or a TCP+TLS endpoint with SSH-style fingerprint pinning (v2.3-7b, task #27) — both go through the same code path; the kind is captured at construct-time.

Construct one per CLI invocation via NewClient (unix) or NewClientFromTarget (any). Zero value is invalid; methods that try to use it return ErrClientNotConfigured.

func NewClient

func NewClient(socketPath string, timeout time.Duration) *Client

NewClient returns a Client targeting the given unix socket path. timeout is applied per request; pass 0 for the default 30s.

Preserved unchanged for backward compat with v2.2 callers + tests. v2.3-7b callers wanting TCP+TLS should use NewClientFromTarget.

func NewClientFromTarget

func NewClientFromTarget(target clienttransport.Target, fingerprint string, timeout time.Duration) (*Client, error)

NewClientFromTarget constructs a Client from a parsed transport target (unix or tcp) + optional fingerprint (mandatory for tcp). v2.3-7b (task #27). Returns an error rather than panicking when the fingerprint is missing/malformed for tcp — security-sensitive path.

func (*Client) AdminTokenCreate

AdminTokenCreate POSTs /admin/admintoken/create. The caller must hold a bearer with `admin:token` scope.

func (*Client) AdminTokenList

func (c *Client) AdminTokenList(ctx context.Context) ([]AdminTokenDTO, error)

AdminTokenList GETs /admin/admintoken/list.

func (*Client) AdminTokenRevoke

func (c *Client) AdminTokenRevoke(ctx context.Context, req AdminTokenRevokeRequest) error

AdminTokenRevoke POSTs /admin/admintoken/revoke.

func (*Client) CarryOverFindByChildConv

func (c *Client) CarryOverFindByChildConv(ctx context.Context, childConvID string) ([]ConversationMessageReferenceDTO, error)

CarryOverFindByChildConv GETs /admin/conversation/carry-over/find-by-child-conv?child_conversation_id=…

func (*Client) CarryOverFindBySourceMsg

func (c *Client) CarryOverFindBySourceMsg(ctx context.Context, sourceMsgID string) ([]ConversationMessageReferenceDTO, error)

CarryOverFindBySourceMsg GETs /admin/conversation/carry-over/find-by-source-msg?source_message_id=…

func (*Client) ChannelArchive

func (c *Client) ChannelArchive(ctx context.Context, req ChannelArchiveRequest) (EventIDResponse, error)

ChannelArchive POSTs /admin/conversation/channel/archive.

func (*Client) ChannelCreate

func (c *Client) ChannelCreate(ctx context.Context, req ChannelCreateRequest) (ChannelCreateResponse, error)

ChannelCreate POSTs /admin/conversation/channel/create.

func (*Client) CollaborationEffectEvidence

func (c *Client) CollaborationEffectEvidence(ctx context.Context, effectID, projectID string) (collaborationeffect.EvidenceResult, error)

func (*Client) CollaborationEffectsQuery

func (c *Client) CollaborationEffectsQuery(ctx context.Context, filter collaborationeffect.Filter) (collaborationeffect.QueryResult, error)

func (*Client) ConvRefFindByChildConvID

func (c *Client) ConvRefFindByChildConvID(ctx context.Context, childConvID string) ([]ConversationMessageReferenceDTO, error)

ConvRefFindByChildConvID GETs /admin/conversation/conv-ref/find-by-child-conv-id?child_conversation_id=…

func (*Client) ConvRefFindBySourceMsgID

func (c *Client) ConvRefFindBySourceMsgID(ctx context.Context, sourceMsgID string) ([]ConversationMessageReferenceDTO, error)

ConvRefFindBySourceMsgID GETs /admin/conversation/conv-ref/find-by-source-msg-id?source_message_id=…

func (*Client) ConversationArchive

func (c *Client) ConversationArchive(ctx context.Context, req ConversationArchiveRequest) (EventIDResponse, error)

ConversationArchive POSTs /admin/conversation/message-writer/archive.

func (*Client) ConversationClose

func (c *Client) ConversationClose(ctx context.Context, req ConversationCloseRequest) (EventIDResponse, error)

ConversationClose POSTs /admin/conversation/message-writer/close.

func (*Client) ConversationFind

func (c *Client) ConversationFind(ctx context.Context, kind, status string) ([]ConversationDTO, error)

ConversationFind GETs /admin/conversation/conv/find?kind=…&status=…

func (*Client) ConversationFindByID

func (c *Client) ConversationFindByID(ctx context.Context, id string) (ConversationDTO, error)

ConversationFindByID GETs /admin/conversation/conv/find-by-id?id=…

func (*Client) ConversationFindByName

func (c *Client) ConversationFindByName(ctx context.Context, name string) (ConversationDTO, error)

ConversationFindByName GETs /admin/conversation/conv/find-by-name?name=…

func (*Client) ConversationOpen

ConversationOpen POSTs /admin/conversation/message-writer/open.

func (*Client) EventFind

func (c *Client) EventFind(ctx context.Context, filter EventFindFilter) ([]EventDTO, error)

EventFind GETs /admin/observability/event/find with optional filters. Pass empty strings / 0 to omit a filter; the helper drops them.

func (*Client) EventFindByID

func (c *Client) EventFindByID(ctx context.Context, id string) (EventDTO, error)

EventFindByID GETs /admin/observability/event/find-by-id?id=…

func (*Client) FleetSnapshotRaw

func (c *Client) FleetSnapshotRaw(ctx context.Context, projectID string, out any) error

FleetSnapshotRaw GETs /admin/observability/fleet/snapshot?project_id=… and decodes into out (typically query.FleetSnapshot).

func (*Client) InspectRaw

func (c *Client) InspectRaw(ctx context.Context, kind, id string, out any) error

InspectRaw GETs /admin/observability/query/inspect?kind=…&id=… and decodes into out (typically query.InspectResult).

func (*Client) LogsOpen

func (c *Client) LogsOpen(ctx context.Context, kind, id string) ([]byte, string, error)

LogsOpen GETs /admin/observability/logs/open?kind=…&id=… and returns the gzipped body bytes plus the blob ref (from the X-Blob-Ref response header). Caller is responsible for decoding / streaming.

We deliberately return []byte rather than an io.ReadCloser because the underlying socket connection is closed when this method returns; streaming would require a different transport-level helper.

func (*Client) MessageAppend

func (c *Client) MessageAppend(ctx context.Context, req MsgAppendRequest) (MsgAppendResponse, error)

MessageAppend POSTs /admin/conversation/msg/append.

func (*Client) MessageFindByConversationID

func (c *Client) MessageFindByConversationID(ctx context.Context, convID string) ([]MessageDTO, error)

MessageFindByConversationID GETs /admin/conversation/msg/find-by-conversation-id?conversation_id=… Server hard-codes MessageFilter{Limit: 200}. For arbitrary-tail queries use MessageFindRecent below (v2.3-1).

func (*Client) MessageFindByID

func (c *Client) MessageFindByID(ctx context.Context, id string) (MessageDTO, error)

MessageFindByID GETs /admin/conversation/msg/find-by-id?id=…

func (*Client) MessageFindRecent

func (c *Client) MessageFindRecent(ctx context.Context, convID string, n int) ([]MessageDTO, error)

MessageFindRecent GETs /admin/conversation/msg/find-recent?conversation_id=…&n=… (v2.3-1). n=0 lets the server pick the default (50). Returns messages oldest-first, mirroring MessageRepo.FindRecent's contract.

func (*Client) ParticipantInvite

func (c *Client) ParticipantInvite(ctx context.Context, req ParticipantInviteRequest) (EventIDResponse, error)

ParticipantInvite POSTs /admin/conversation/participant/invite.

func (*Client) ParticipantKick

func (c *Client) ParticipantKick(ctx context.Context, req ParticipantKickRequest) (EventIDResponse, error)

ParticipantKick POSTs /admin/conversation/participant/kick.

func (*Client) ParticipantLeave

func (c *Client) ParticipantLeave(ctx context.Context, req ParticipantLeaveRequest) (EventIDResponse, error)

ParticipantLeave POSTs /admin/conversation/participant/leave (v2.3-1).

func (*Client) ProjectFindAll

func (c *Client) ProjectFindAll(ctx context.Context, _ string) ([]ProjectDTO, error)

ProjectFindAll GETs /admin/workforce/project/find-all. v2.5.5 dropped the by-kind filter; the param is kept on the signature for callers that haven't been updated yet, but is no longer transmitted.

func (*Client) ProjectFindByID

func (c *Client) ProjectFindByID(ctx context.Context, id string) (ProjectDTO, error)

ProjectFindByID GETs /admin/workforce/project/find-by-id?id=…

func (*Client) QueryRaw

func (c *Client) QueryRaw(ctx context.Context, req QueryRequest, out any) error

QueryRaw POSTs /admin/observability/query/query and decodes the success body into `out` (any).

Handlers typically decode into a query.QueryResult; we don't take a compile dependency on the query package here so accept any.

func (*Client) SecretCreate

func (c *Client) SecretCreate(ctx context.Context, req SecretCreateRequest) (SecretCreateResponse, error)

SecretCreate POSTs /admin/secret/user-secret/create.

func (*Client) SecretFindAll

func (c *Client) SecretFindAll(ctx context.Context, kind, state string) ([]UserSecretDTO, error)

SecretFindAll GETs /admin/secret/user-secret/find-all?kind=…&state=…

func (*Client) SecretFindByID

func (c *Client) SecretFindByID(ctx context.Context, id string) (UserSecretDTO, error)

SecretFindByID GETs /admin/secret/user-secret/find-by-id?id=…

func (*Client) SecretFindByName

func (c *Client) SecretFindByName(ctx context.Context, name string) (UserSecretDTO, error)

SecretFindByName GETs /admin/secret/user-secret/find-by-name?name=…

func (*Client) SecretResolve

func (c *Client) SecretResolve(ctx context.Context, name string) ([]byte, error)

SecretResolve POSTs /admin/secret/user-secret/resolve. Returns the decoded plaintext bytes; caller is responsible for wiping them after use (ADR-0026 § 5). Requires `secret:resolve` scope on the bearer.

func (*Client) SecretRevoke

func (c *Client) SecretRevoke(ctx context.Context, req SecretRevokeRequest) (EventIDResponse, error)

SecretRevoke POSTs /admin/secret/user-secret/revoke.

func (*Client) SecretRotate

func (c *Client) SecretRotate(ctx context.Context, req SecretRotateRequest) (EventIDResponse, error)

SecretRotate POSTs /admin/secret/user-secret/rotate.

func (*Client) SocketPath

func (c *Client) SocketPath() string

SocketPath returns the configured socket path (diagnostics). Empty when the Client was constructed from a TCP target.

func (*Client) StatsAggregateRaw

func (c *Client) StatsAggregateRaw(ctx context.Context, scope, since string, out any) error

StatsAggregateRaw GETs /admin/observability/stats/aggregate?scope=…&since=… and decodes into out (typically query.StatsResult). `since` is the raw query parameter value (handlers pre-format duration vs RFC3339).

func (*Client) Token

func (c *Client) Token() string

Token exposes the configured bearer (for diagnostics / tests).

func (*Client) WithToken

func (c *Client) WithToken(t string) *Client

WithToken sets the bearer token attached to every subsequent request. Returns the receiver to allow chaining at construction sites.

Empty strings clear the token (so tests can deliberately exercise the unauthenticated path).

func (*Client) WorkerEnroll

func (c *Client) WorkerEnroll(ctx context.Context, req WorkerEnrollRequest) (WorkerEnrollResponse, error)

WorkerEnroll POSTs /admin/workforce/worker/enroll.

func (*Client) WorkerFindAll

func (c *Client) WorkerFindAll(ctx context.Context) ([]WorkerDTO, error)

WorkerFindAll GETs /admin/workforce/worker/find-all.

func (*Client) WorkerFindByID

func (c *Client) WorkerFindByID(ctx context.Context, id string) (WorkerDTO, error)

WorkerFindByID GETs /admin/workforce/worker/find-by-id?id=…

func (*Client) WorkerFindByStatus

func (c *Client) WorkerFindByStatus(ctx context.Context, status string) ([]WorkerDTO, error)

WorkerFindByStatus GETs /admin/workforce/worker/find-by-status?status=…

type ClientError

type ClientError struct {
	Method  string
	Path    string
	Status  int
	Code    string // server-side error code (from the JSON envelope)
	Message string // server-side message
	Body    string // raw body for unrecognised shapes
}

ClientError carries non-2xx admin endpoint responses.

func (*ClientError) Error

func (e *ClientError) Error() string

Error implements error.

func (*ClientError) IsConflict

func (e *ClientError) IsConflict() bool

IsConflict reports whether the error is a 409 response.

func (*ClientError) IsNotFound

func (e *ClientError) IsNotFound() bool

IsNotFound reports whether the error is a 404 response.

type Command

type Command struct {
	Name        string
	Summary     string
	LongHelp    string
	Subcommands []*Command
	Run         Handler

	// Flags can be registered up-front via this hook. The hook gets a
	// fresh *flag.FlagSet to register on; the returned Handler receives
	// the parsed-arg slice.
	Flags func(fs *flag.FlagSet) Handler

	// Group is the root-level grouping label used by `agent-center help`
	// to keep ~30 top-level commands legible. Empty defaults to "Other".
	// Only top-level commands are grouped — nested subcommands inherit
	// implicitly.
	Group string

	// Examples are concrete, runnable invocations rendered under
	// `Examples:` in `--help` output. Per P11 § 3.9: each leaf command
	// should ship at least the happy path + one `--format=json` variant
	// so users discover scripting paths without reading docs.
	Examples []string

	// HelpFlags is an optional display-only flag registrar. Used by
	// `lazyApp` wrappers where `Run` is set (real handler) but flag
	// metadata still needs to render in `--help`. printNodeHelp falls
	// back to HelpFlags when Flags is nil. If both are nil, no Flags:
	// section renders.
	HelpFlags func(fs *flag.FlagSet)
}

Command is a node in the command tree. A node is either a group (has Subcommands) or a leaf (has Run). It cannot be both.

func AdminBlobMigratePlaceholder

func AdminBlobMigratePlaceholder() *Command

AdminBlobMigratePlaceholder returns the `admin blob-migrate` stub.

func AgentRuntimeCommand

func AgentRuntimeCommand() *Command

AgentRuntimeCommand is the `worker agent-runtime` entry (T854 D6, design §4.5/§5): ONE agent's self-contained runtime, run as its own OS process. The worker's launcher fork/execs this subcommand (os.Executable() worker agent-runtime --agent-id X --sock-dir D) per agent and rebuilds it on exit.

It shares the worker's config/token bootstrap (it does NOT re-enroll — it loads the token the worker persisted), self-builds its center client, runs Boot self-recovery, then serves control commands the worker proxies over the unix socket in --sock-dir. The flag surface mirrors `worker run` so an operator/launcher can pass the same admin-target/token/fingerprint, plus --agent-id and --sock-dir.

func AgentSupervisorCommand

func AgentSupervisorCommand() *Command

AgentSupervisorCommand is the v2.7 (D2-f s1) persistent per-agent SUPERVISOR entry. It is a thin, long-lived process that OWNS the agent's claude (claude == its child) so a worker-daemon crash/restart does NOT kill claude: the supervisor setsids into its own session/group to ESCAPE a killpg of the daemon's group, holds claude's stdin open, and continuously drains claude's stdout to a persistent offset cursor (events.jsonl).

SCOPE: additive + NOT wired into the daemon (no socket, no attach/reattach — those are s2/s3). System-audience (operators do not invoke it directly), so it lives under the `worker` group alongside mcp-host / run / shim.

MINIMAL KEY SURFACE: the supervisor receives only the daemon-generated mcp-config FILE PATH via --mcp-config-path. It NEVER takes or holds the worker token; the daemon generates the mcp-config (which carries the token) and the supervisor just points claude at the file.

func BootstrapCommand

func BootstrapCommand() *Command

BootstrapCommand returns the `bootstrap` admin command tree. v1 surface:

  • bootstrap check-systemd → validate the worker user-systemd unit contains KillMode=process (ADR-0018 hard requirement). Designed to be invoked by install-worker.sh AND from the worker daemon at startup so manual tampering is caught.

func ExecutorCommand

func ExecutorCommand() *Command

ExecutorCommand is the F1 (agent-concurrent-execution §4/§11.2) per-task EXECUTOR entry. It is the forked, pure-compute worker the orchestrator (监工) spawns for ONE task: it reads its goal from <agent-root>/executors/<id>/input.json, runs the model-routed agent CLI inside its own isolated git worktree, streams progress, and writes output.json + status — all over the F2 file protocol.

CRITICAL (the executor's defining property): it NEVER connects to the center or mcp and holds NO credentials. Unlike mcp-host / agent-supervisor it takes no admin URL, no worker token, and is launched (by executor.Spawner) with a sanitized, mcp-free environment. Binding is purely its --agent-root + --executor-id. System-audience (the orchestrator spawns it, operators do not), so it lives under the `worker` group alongside mcp-host / agent-supervisor.

func InstallCenterCommand

func InstallCenterCommand() *Command

InstallCenterCommand is the `install center` leaf — the operator's one command for "spin up an agent-center server on this machine".

func InstallCommand

func InstallCommand() *Command

InstallCommand is the parent group; printing help when invoked without a subcommand. Per the v2.4 first-mile spec, the operator types `agent-center install center` or `... install worker`.

func InstallTestInstanceCommand

func InstallTestInstanceCommand() *Command

InstallTestInstanceCommand spins up an isolated 1-center + N-worker sandbox.

func InstallWorkerCommand

func InstallWorkerCommand() *Command

InstallWorkerCommand is the `install worker` leaf — the operator's one command for "join this machine to the cluster as a worker".

func ListLocalCentersCommand

func ListLocalCentersCommand() *Command

ListLocalCentersCommand surfaces all local center deployments (v2.7.1 #211).

func ListLocalWorkersCommand

func ListLocalWorkersCommand() *Command

ListLocalWorkersCommand surfaces all local worker deployments (v2.8 #170).

func ListTestInstancesCommand

func ListTestInstancesCommand() *Command

ListTestInstancesCommand lists the sandboxes under ~/.agent-center-test. Named with the `list-` prefix for symmetry with list-local-centers (#211) / list-local-workers (#170).

func MCPHostCommand

func MCPHostCommand() *Command

MCPHostCommand is the per-agent stdio MCP server entry (v2.7 b3-i, ADR-0049). One process == one agent: it bridges MCP tool calls from a claude process to the center's admin agent-tool endpoints. The daemon spawns it via --mcp-config with per-server env; it is system-audience (operators do not invoke it directly), so it lives under the `worker` group alongside the other daemon-internal entries (run / shim).

Binding comes ENTIRELY from env (process-fixed):

  • AC_MCP_AGENT_ID operating agent id, injected into every admin call body; NEVER taken from tool args (required).
  • AC_MCP_ADMIN_URL admin endpoint: unix:/path or tcp://host:port (required).
  • AC_MCP_WORKER_TOKEN worker bearer token (owner worker:<id>).
  • AC_MCP_SERVER_FINGERPRINT pinned cert fingerprint, required when AC_MCP_ADMIN_URL is tcp://...
  • AC_MCP_RUNTIME_SOCKET optional local runtime control socket for runtime-authoritative supervisor execution state.
  • AC_MCP_TIER_TOOLS optional bool, default true. Set false only for clients that already own deferred MCP discovery.
  • AC_MCP_GENERATION optional non-negative int identifying the launching supervisor generation for plan-rule snapshot audits.

func MigrateGroupCommand

func MigrateGroupCommand() *Command

MigrateGroupCommand is the parent of `migrate up` + `migrate v1-to-v2`. It carries no Run handler — invoking `migrate` alone prints help.

func MigrateOrphanConditionsCommand

func MigrateOrphanConditionsCommand() *Command

MigrateOrphanConditionsCommand implements `agent-center migrate orphan-conditions`.

func MigrateUpCommand

func MigrateUpCommand() *Command

MigrateUpCommand replaces the v1-era top-level `migrate` leaf. It runs pending migrations against the configured SQLite file. Behavior is preserved verbatim from the v1 form (target=N supported).

func MigrateV1ToV2Command

func MigrateV1ToV2Command() *Command

MigrateV1ToV2Command implements `agent-center migrate v1-to-v2`.

Per P12 S12 audit (docs/plans/phase-12-audits/s12-migration-tool-audit.md):

  • --dry-run reports planned ops (bridge row counts; current vs target version)
  • --apply runs them: bridge tables → JSON archive → drop via 0025 → Up to targetSchemaVersion (currently 28 — v2.0 GA was 25, v2.1-C added 0026, v2.1-E added 0027, v2.3-3a added 0028 admin_tokens)
  • Idempotent: if already at v2 (currentVer >= targetSchemaVersion), exits 0 with "already at v2"
  • Refuses to run silently — neither flag → usage error

func ServerCommand

func ServerCommand() *Command

ServerCommand returns the `server` mode command. It needs to construct its own deps (open DB, run migrations) because it's the entry point before any other command runs.

func SystemCommands

func SystemCommands(buildVersion, buildCommit string) []*Command

SystemCommands returns top-level mode + admin commands.

`server` / `migrate` actually run; `supervisor` / `worker` / `admin blob-migrate` are placeholder stubs per plan-1 § 3.1.4 — they exist so the CLI surface is stable and exit cleanly with reason `not_implemented_in_phase_1`.

func UninstallCenterCommand

func UninstallCenterCommand() *Command

UninstallCenterCommand removes a center install. Mirrors the install center flag surface so operators don't have to think about where things landed.

func UninstallCommand

func UninstallCommand() *Command

UninstallCommand is the parent group for `uninstall center|worker`.

func UninstallTestInstanceCommand

func UninstallTestInstanceCommand() *Command

UninstallTestInstanceCommand tears down a sandbox: bootout the launchd labels + remove the namespace subtree. Confined to ~/.agent-center-test/<id>/.

func UninstallWorkerCommand

func UninstallWorkerCommand() *Command

UninstallWorkerCommand removes a worker install. --worker-id is required since multi-worker installs share a parent prefix.

func UpgradeCenterCommand

func UpgradeCenterCommand() *Command

UpgradeCenterCommand is the explicit upgrade-only entry point for the center. Shares the install-center flag surface to keep muscle memory + the existing flag docs unchanged.

func UpgradeCommand

func UpgradeCommand() *Command

UpgradeCommand is the parent group for `upgrade center|worker`.

func UpgradeWorkerCommand

func UpgradeWorkerCommand() *Command

UpgradeWorkerCommand mirrors UpgradeCenterCommand for the worker install. Requires --worker-id so the right worker subtree is targeted on multi-worker hosts.

func WorkerRecoverDeliveryCommand

func WorkerRecoverDeliveryCommand() *Command

WorkerRecoverDeliveryCommand is the operator-facing manual recovery delivery path. It runs on the worker/operator machine, optionally commits/pushes a retained worktree, then reports a manual_recovery delivery through the same report_delivery agent-tool the runtime uses. The center persists/audits the result; it never performs git operations.

func WorkerRunCommand

func WorkerRunCommand() *Command

WorkerRunCommand is the `worker run` daemon entry (v2.7 (b) cutover). The worker daemon now ships INSIDE the unified `agent-center` binary so its os.Executable() can route the `worker agent-supervisor` and `worker mcp-host` subcommands the daemon spawns (the spawn-bug fix; the retired standalone `agent-center-worker- daemon` was flag-only and could not route them).

§ 0.4 is honored by construction: the daemon talks to the center ONLY via the admin endpoint (AdminClient) and never opens the SQLite file — so this CLI subcommand holds no DB handle. The flag set is kept STRICTLY in parity with the (retiring) standalone binary so operator behavior and Tester runbooks are unchanged; the real bootstrap lives in workerdaemon.RunDaemon (single source, shared with the thin standalone wrapper).

func WorkerShimPlaceholder

func WorkerShimPlaceholder() *Command

WorkerShimPlaceholder is the `worker shim` entry point. The shim runtime is daemon-internal; this is a thin CLI hook. Daemon spawns shim via this entry; users do not directly invoke it (audience=Sys per 03-cli § 8.3).

type ConversationArchiveRequest

type ConversationArchiveRequest struct {
	ConversationID string `json:"conversation_id"`
	Version        int    `json:"version"`
	ArchivedBy     string `json:"archived_by"`
}

ConversationArchiveRequest is the POST body for message-writer/archive.

type ConversationCloseRequest

type ConversationCloseRequest struct {
	ConversationID string `json:"conversation_id"`
	Version        int    `json:"version"`
	Reason         string `json:"reason"`
	Message        string `json:"message"`
}

ConversationCloseRequest is the POST body for message-writer/close.

type ConversationDTO

type ConversationDTO struct {
	ID                   string           `json:"id"`
	Kind                 string           `json:"kind"`
	Name                 string           `json:"name"`
	Description          string           `json:"description"`
	Status               string           `json:"status"`
	ParentConversationID string           `json:"parent_conversation_id"`
	CreatedBy            string           `json:"created_by"`
	CreatedAt            string           `json:"created_at"`
	UpdatedAt            string           `json:"updated_at"`
	Version              int              `json:"version"`
	ArchivedAt           string           `json:"archived_at,omitempty"`
	ArchivedBy           string           `json:"archived_by,omitempty"`
	Participants         []ParticipantDTO `json:"participants"`
}

ConversationDTO mirrors admin api convMap.

type ConversationMessageReferenceDTO

type ConversationMessageReferenceDTO struct {
	ID                   string `json:"id"`
	ChildConversationID  string `json:"child_conversation_id"`
	SourceConversationID string `json:"source_conversation_id"`
	SourceMessageID      string `json:"source_message_id"`
	CreatedBy            string `json:"created_by"`
	CreatedAt            string `json:"created_at"`
}

ConversationMessageReferenceDTO mirrors admin api refsToMap entries.

type ConversationOpenRequest

type ConversationOpenRequest struct {
	Kind                 string                            `json:"kind"`
	Name                 string                            `json:"name"`
	Description          string                            `json:"description"`
	ParentConversationID string                            `json:"parent_conversation_id"`
	Participants         []conversation.ParticipantElement `json:"participants"`
	CreatedBy            string                            `json:"created_by"`
}

ConversationOpenRequest is the POST body for message-writer/open.

type ConversationOpenResponse

type ConversationOpenResponse struct {
	ConversationID string `json:"conversation_id"`
	EventID        string `json:"event_id"`
}

ConversationOpenResponse mirrors the success projection.

type ErrorReason

type ErrorReason struct {
	Reason  string `json:"reason"`
	Message string `json:"message"`
}

ErrorReason captures the structured error returned to the user via JSON. Aligns with 03-cli § 6.

type EventDTO

type EventDTO struct {
	ID            string         `json:"id"`
	EventType     string         `json:"event_type"`
	Actor         string         `json:"actor"`
	Refs          map[string]any `json:"refs"`
	Payload       map[string]any `json:"payload"`
	CorrelationID string         `json:"correlation_id"`
	DecisionID    string         `json:"decision_id"`
	OccurredAt    string         `json:"occurred_at"`
}

EventDTO mirrors admin api eventMap.

type EventFindFilter

type EventFindFilter struct {
	EventType      string
	TaskID         string
	ExecutionID    string
	IssueID        string
	ConversationID string
	WorkerID       string
	Limit          int
}

EventFindFilter is the wire-side filter for EventFind.

type EventIDResponse

type EventIDResponse struct {
	EventID string `json:"event_id"`
}

EventIDResponse is the generic single-event-id success shape used by many admin write endpoints (`{"event_id": "..."}`).

type ExitCode

type ExitCode int

ExitCode pairs a numeric exit value with optional structured info that the router can post-process (e.g. JSON-format mode).

func HandleClientError

func HandleClientError(w io.Writer, format string, err error) ExitCode

HandleClientError translates an admin Client error to the same (reason, exit-code) shape as HandleDomainError. This is the post-v2.2-B handler error path: domain errors come back over the wire wrapped in *ClientError, with the server-side code in ClientError.Code mapping to the same reason strings handlers used pre-migration.

Mapping rules:

  • server-side codes (not_found, version_conflict, already_exists, terminal, invalid_transition, invalid_input) map to exit-code equivalents from the legacy domain table.
  • ErrServerUnreachable / ErrClientNotConfigured surface a friendly "is the server running?" hint with ExitBusinessError.
  • everything else falls through to ExitBusinessError with the raw error message preserved in the message field.

func HandleDomainError

func HandleDomainError(w io.Writer, format string, err error) ExitCode

HandleDomainError formats and prints err using format ("human" or "json") and returns the matching exit code. Falls back to internal_error / ExitBusinessError for unknown error types.

func MapDomainError

func MapDomainError(err error) (reason string, code ExitCode, ok bool)

MapDomainError translates a workforce/conversation domain error to a (reason, exit-code) pair suitable for PrintError. Returns (false, …) if the error is not a known sentinel — caller emits a generic `internal_error` line and exits ExitBusinessError.

func PrintError

func PrintError(w io.Writer, format, reason, message string, code ExitCode) ExitCode

PrintError writes a formatted error to stderr in the requested format and returns the exit code.

type Handler

type Handler func(ctx context.Context, args []string, out, err io.Writer) ExitCode

Handler is the signature of a leaf command implementation.

The framework parses positional + flag arguments before calling the handler. Handler implementations write business output to `out` (stdout) and diagnostics to `err` (stderr) — they MUST NOT call os.Exit; return an Exit struct instead.

type InstallState

type InstallState int

InstallState reflects the outcome of detectExistingInstall.

const (
	InstallStateUnknown InstallState = iota
	// InstallStateFresh = no prior install at prefix; A2 will write
	// everything fresh.
	InstallStateFresh
	// InstallStateSameVersion = `<prefix>/current` already points at
	// this binary's version; the install is idempotent.
	InstallStateSameVersion
	// InstallStateUpgrade = `<prefix>/current` exists but points at a
	// different version; A5 will perform symlink swap.
	InstallStateUpgrade
)

func (InstallState) String

func (s InstallState) String() string

String for diagnostics.

type MessageDTO

type MessageDTO struct {
	ID               string `json:"id"`
	ConversationID   string `json:"conversation_id"`
	SenderIdentityID string `json:"sender_identity_id"`
	ContentKind      string `json:"content_kind"`
	Content          string `json:"content"`
	Direction        string `json:"direction"`
	InputRequestRef  string `json:"input_request_ref"`
	PostedAt         string `json:"posted_at"`
}

MessageDTO mirrors admin api messageMap.

type MsgAppendRequest

type MsgAppendRequest struct {
	ConversationID   string `json:"conversation_id"`
	SenderIdentityID string `json:"sender_identity_id"`
	ContentKind      string `json:"content_kind"`
	Content          string `json:"content"`
	Direction        string `json:"direction"`
	InputRequestRef  string `json:"input_request_ref"`
}

MsgAppendRequest is the POST body for /admin/conversation/msg/append.

type MsgAppendResponse

type MsgAppendResponse struct {
	MessageID string `json:"message_id"`
	EventID   string `json:"event_id"`
}

MsgAppendResponse mirrors the success projection.

type ParticipantDTO

type ParticipantDTO struct {
	IdentityID string `json:"identity_id"`
	Role       string `json:"role"`
	JoinedAt   any    `json:"joined_at"`
	JoinedBy   string `json:"joined_by"`
	LeftAt     any    `json:"left_at,omitempty"`
	LeftReason string `json:"left_reason,omitempty"`
}

ParticipantDTO mirrors one entry of the admin api convMap "participants".

type ParticipantInviteRequest

type ParticipantInviteRequest struct {
	ConversationName string `json:"conversation_name"`
	IdentityID       string `json:"identity_id"`
	Role             string `json:"role"`
	InvitedBy        string `json:"invited_by"`
}

ParticipantInviteRequest mirrors api inviteParticipantReq.

type ParticipantKickRequest

type ParticipantKickRequest struct {
	ConversationName string `json:"conversation_name"`
	IdentityID       string `json:"identity_id"`
	KickedBy         string `json:"kicked_by"`
	Reason           string `json:"reason"`
}

ParticipantKickRequest mirrors api kickParticipantReq.

type ParticipantLeaveRequest

type ParticipantLeaveRequest struct {
	ConversationName string `json:"conversation_name"`
	IdentityID       string `json:"identity_id"`
	Reason           string `json:"reason"`
}

ParticipantLeaveRequest mirrors api leaveParticipantReq (v2.3-1). IdentityID may be empty — server defaults to the actor.

type ProjectDTO

type ProjectDTO struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	Description    string `json:"description"`
	OrganizationID string `json:"organization_id"`
	Version        int    `json:"version"`
	CreatedAt      string `json:"created_at"`
}

ProjectDTO mirrors admin api projectMap. v2.7 #131 PR-3: repointed to the pm.Project model — tags dropped (pm.Project has none), organization_id surfaced.

type QueryRequest

type QueryRequest struct {
	Resource    string `json:"resource"`
	Status      string `json:"status"`
	ProjectID   string `json:"project_id"`
	WorkerID    string `json:"worker_id"`
	TaskID      string `json:"task_id"`
	ExecutionID string `json:"execution_id"`
	IssueID     string `json:"issue_id"`
	Opener      string `json:"opener"`
	EventType   string `json:"event_type"`
	Limit       int    `json:"limit"`
	Cursor      string `json:"cursor"`
}

QueryRequest mirrors api queryReq (POST body for /observability/query/query).

type Router

type Router struct {
	Root *Command
	Out  io.Writer
	Err  io.Writer
}

Router carries shared application state (DB, services, etc.) and the root command tree. Construct via NewRouter then add commands via Add.

func BuildRouter

func BuildRouter(buildVersion, buildCommit string, args []string) (*Router, string, error)

BuildRouter constructs the full command tree.

The router opens the DB lazily — only when a resource command is run (i.e. workforce / conversation handlers). `version` / `--help` / `supervisor` / `worker run` / `admin blob-migrate` placeholders all skip DB construction.

Returns the router + the resolved config path so main.go can strip the --config flag from args before dispatching.

func NewRouter

func NewRouter(binaryName string) *Router

NewRouter builds an empty router rooted at the binary name.

func (*Router) Add

func (r *Router) Add(path []string, cmd *Command) error

Add inserts a sub-command tree at the given path (e.g. ["worker", "proposal"]). The leaf Command is appended as the last element's Subcommands.

func (*Router) HelpCommand

func (r *Router) HelpCommand() *Command

HelpCommand is the `agent-center help [topic]` synthetic command. It is registered by BuildRouter so users can discover the rest of the tree without remembering exact subcommand names.

func (*Router) Run

func (r *Router) Run(ctx context.Context, args []string) ExitCode

Run executes the router with the given args (typically os.Args[1:]). Returns the exit code the caller should pass to os.Exit. Help / version requests are part of `args` (e.g. `--help`).

type SecretCreateRequest

type SecretCreateRequest struct {
	Name      string `json:"name"`
	Kind      string `json:"kind"`
	Plaintext string `json:"plaintext"`
}

SecretCreateRequest mirrors api secretCreateReq.

type SecretCreateResponse

type SecretCreateResponse struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	EventID string `json:"event_id"`
}

SecretCreateResponse mirrors api success body (id + name + event_id).

type SecretResolveResponse

type SecretResolveResponse struct {
	ID              string `json:"id"`
	Name            string `json:"name"`
	PlaintextBase64 string `json:"plaintext_base64"`
}

SecretResolveResponse mirrors the server's secret resolve envelope. PlaintextBase64 is std base64 of the raw bytes (NOT URL-safe).

type SecretRevokeRequest

type SecretRevokeRequest struct {
	ID      string `json:"id"`
	Reason  string `json:"reason"`
	Message string `json:"message"`
	Version int    `json:"version"`
}

SecretRevokeRequest mirrors api secretRevokeReq.

type SecretRotateRequest

type SecretRotateRequest struct {
	ID           string `json:"id"`
	NewPlaintext string `json:"new_plaintext"`
	Version      int    `json:"version"`
}

SecretRotateRequest mirrors api secretRotateReq.

type UserSecretDTO

type UserSecretDTO struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	Kind           string `json:"kind"`
	State          string `json:"state"`
	CreatedAt      string `json:"created_at"`
	CreatedBy      string `json:"created_by"`
	Version        int    `json:"version"`
	RevokedAt      string `json:"revoked_at,omitempty"`
	RevokedBy      string `json:"revoked_by,omitempty"`
	RevokedReason  string `json:"revoked_reason,omitempty"`
	RevokedMessage string `json:"revoked_message,omitempty"`
	RotatedAt      string `json:"rotated_at,omitempty"`
	LastUsedAt     string `json:"last_used_at,omitempty"`
}

UserSecretDTO mirrors admin api secretMap.

type WebConsoleEnrollWiring

type WebConsoleEnrollWiring struct {
	BootstrapHost string // e.g. "192.168.1.10:7300" or "127.0.0.1:7300"
	Fingerprint   string // SSH-style sha256:HH:HH:...
}

WebConsoleEnrollWiring carries the values the AddWorkerModal needs to render a working install command for the worker box. Both are known by ServerCommand after the admin TCP listener boots: the fingerprint comes from AdminTransportInfo, the bootstrap host is derived from the admin_tcp_listen config + the operator-facing hostname (or 127.0.0.1 when the listener is loopback-only).

type WorkerDTO

type WorkerDTO struct {
	WorkerID        string               `json:"worker_id"`
	Status          string               `json:"status"`
	Capabilities    []string             `json:"capabilities"`
	Version         int                  `json:"version"`
	EnrolledAt      string               `json:"enrolled_at"`
	LastHeartbeatAt string               `json:"last_heartbeat_at,omitempty"`
	SystemInfo      workforce.SystemInfo `json:"system_info,omitempty"`
}

WorkerDTO mirrors admin api workerMap.

type WorkerEnrollRequest

type WorkerEnrollRequest struct {
	WorkerID     string   `json:"worker_id"`
	Capabilities []string `json:"capabilities"`
}

WorkerEnrollRequest is the POST body for /admin/workforce/worker/enroll.

type WorkerEnrollResponse

type WorkerEnrollResponse struct {
	WorkerID string `json:"worker_id"`
	EventID  string `json:"event_id"`
	Version  int    `json:"version"`
}

WorkerEnrollResponse is the success body.

type WriterAlias

type WriterAlias = io.Writer

WriterAlias is io.Writer; aliased so cmd/agent-center can refer to it without re-importing io.

Jump to

Keyboard shortcuts

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