AdminService is the one write surface that changes what config is running, rather than what an object's state is. It carries four RPCs, ValidateConfig, ApplyConfig, GetConfig and ListAuditRecords, and it exists only when both -auth-config is set and the process was not started with -read-only.

All three RPCs act on the -config document only. Identities live in the separate document -auth-config loads (see Security); GetConfig never returns them and ApplyConfig never accepts them, because they are not part of the document either RPC reads or writes.

1. Admin-gated, and absent without auth

AdminService requires the admin role: viewer and operator identities are refused with a permission error, the same way CommandService refuses an unauthenticated caller. With no -auth-config set at all, the route is not mounted: there is no identity to require, so a config write over the API is never offered to an unauthenticated caller. That is the same posture the config file and the external command FIFO already have for a local operator. See Security for what -auth-config looks like and what the three roles mean.

-read-only unmounts AdminService for the same reason it unmounts CommandService: a process serving queries and the dashboard only has no write surface to gate, admin role or not.

2. ValidateConfig: check without touching anything

ValidateConfig(cue) runs a whole CUE document past the same loader a SIGHUP reload uses, without writing anything to disk and without touching the running config. Success returns an empty response. Rejection is a transport error (CodeInvalidArgument) carrying a ConfigDiagnostics detail: one ConfigDiagnostic per problem the loader found, each naming the file, line, column and message, exactly as a rejected SIGHUP logs them.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/ValidateConfig \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <admin identity token>' \
  -d '{"cue":"hosts: gw: {address: \"192.168.1.1\", check: {type: \"icmp\", host: address}}"}'

3. ApplyConfig: persist, then hot-swap

ApplyConfig(cue) runs the identical check and, on success, rewrites the served -config file with cue's bytes and swaps the running config through the same fanout a SIGHUP reload uses. Every consumer that reacts to a reload today, the scheduler, the sinks, the notifier's macro map, sees an apply exactly the same way. The file is rewritten before the swap: a process that stops between the two steps still converges on restart to the config it would have been running anyway.

Success and rejection follow the same shape as ValidateConfig: an empty response, or a CodeInvalidArgument error carrying a ConfigDiagnostics detail. A rejected apply changes nothing: the served file is left untouched and the running config keeps serving what it was serving before the call.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/ApplyConfig \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <admin identity token>' \
  -d '{"cue":"hosts: gw: {address: \"192.168.1.1\", check: {type: \"icmp\", host: address}}"}'

A missing cue field is also a CodeInvalidArgument transport error, but carries no ConfigDiagnostics detail: that is a malformed request rather than a rejected document. Any other applier failure, something other than a rejected CUE document, comes back as CodeInternal, also with no detail.

4. GetConfig: read the served document

GetConfig() takes no fields and returns the whole CUE document currently served from the -config file, the same text ValidateConfig and ApplyConfig would each start from. It requires the admin role, the same as the other two RPCs.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/GetConfig \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <admin identity token>' \
  -d '{}'
{"cue": "hosts: gw: {address: \"192.168.1.1\", check: {type: \"icmp\", host: address}}\n"}

A read failure comes back as CodeInternal, the same code an ApplyConfig failure other than a rejected document returns.

5. ListAuditRecords: read the audit trail

ListAuditRecords(since, limit) returns audit records newest first. since is optional: an unset value means everything still retained. limit of 0 means 100; values above 1000 are clamped to 1000. It requires the admin role, the same as the rest of AdminService.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.AdminService/ListAuditRecords \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <admin identity token>' \
  -d '{"limit":2}'
{
  "records": [
    {
      "at": "2026-09-07T10:32:11Z",
      "source": "AUDIT_SOURCE_API",
      "principal": {"name": "jdoe", "role": "ROLE_OPERATOR"},
      "peer": "10.0.4.12:53214",
      "rpc": "/jaque.v1.CommandService/AcknowledgeProblem",
      "commandJson": "{\"Target\":{\"object\":\"web-01/http\"},\"Sticky\":false,\"Author\":\"jdoe\",\"Comment\":\"investigating\"}",
      "outcome": "AUDIT_OUTCOME_ACCEPTED"
    },
    {
      "at": "2026-09-07T10:31:58Z",
      "source": "AUDIT_SOURCE_EXTCMD",
      "rpc": "SCHEDULE_SVC_DOWNTIME",
      "commandJson": "{\"Target\":{\"object\":\"web-01/http\"},\"Start\":\"2026-09-07T10:31:58Z\",\"End\":\"2026-09-07T11:31:58Z\",\"Author\":\"nagios\",\"Comment\":\"maintenance\"}",
      "outcome": "AUDIT_OUTCOME_ACCEPTED"
    }
  ]
}

A record with no principal is one nothing authenticated the sender for, the same as the command file's own audit lines. A read failure comes back as CodeInternal, the same code a GetConfig failure returns.

6. Profile demo narrows what ValidateConfig/ApplyConfig accept

Under -config-profile demo, both RPCs validate the submitted document against the narrowed schema instead of the full one: command checks, command contacts, exec and file sinks, and any non-empty commands block are rejected the same way an unsupported check type is: CodeInvalidArgument carrying a ConfigDiagnostics detail, same shape as the example in Errors below. See CLI flags for the full list of what the profile refuses.

7. Errors

Every AdminService failure comes back as a Connect transport error, never as an ok/error field inside the response body. The codes actually returned:

  • invalid_argument: a missing cue field, or a rejected CUE document (ValidateConfig/ApplyConfig only; GetConfig takes no fields and never returns this code, and ListAuditRecords never returns it either). A rejected document additionally carries a ConfigDiagnostics detail, one ConfigDiagnostic per problem the loader found.
  • unauthenticated: no session cookie or bearer token resolves to an identity.
  • permission_denied: the resolved identity's role is below admin.
  • internal: an applier failure other than a rejected CUE document, a GetConfig read failure, or a ListAuditRecords read failure.

A rejected ValidateConfig response body looks like this:

{
  "code": "invalid_argument",
  "message": "config: invalid:\nhosts.gw.check: unknown check type \"bogus\", must be one of [command dns http icmp legacy passive snmp tcp tls wasm]:\n    config.cue:3:2\n",
  "details": [
    {
      "type": "jaque.v1.ConfigDiagnostics",
      "value": "<base64-encoded protobuf>",
      "debug": {
        "diagnostics": [
          {
            "message": "hosts.gw.check: unknown check type \"bogus\", must be one of [command dns http icmp legacy passive snmp tcp tls wasm]",
            "file": "config.cue",
            "line": 3,
            "column": 2
          }
        ]
      }
    }
  ]
}

ApplyConfig rejects the same way, and leaves the served file and running config untouched.

8. Config stays code

Every RPC takes or returns a whole document, never a partial shape for one host or one check. Pipelines that render CUE and commit it remain the recommended way to manage a fleet's config in a real estate. This service is the admin convenience and the playground's upload path, alongside that workflow rather than in place of it. A caller that wants to add one object resolves the current config with GetConfig, edits its own copy, and calls ApplyConfig with the whole result.

9. Security considerations

An admin identity that can call ApplyConfig can rewrite the entire served config, a strictly larger blast radius than anything an operator identity can do through CommandService. Treat an admin session or token with the same care as shell access to the host: rotate -session-secret on any suspicion a session leaked, the same guidance Security gives for every other role.