feat(mcp): 11 audit-fix MCP tools — approvals, break-glass, bootstrap, audit-category (MED-13)

Audit 2026-05-10 MED-13 closure.

WHAT.

11 new MCP tools rounding out the operator surface for workflows
that previously had GUI + CLI coverage but no MCP equivalent:

Approval workflow (4):
  certctl_approval_list      GET    /v1/approvals                  approval.read
  certctl_approval_get       GET    /v1/approvals/{id}             approval.read
  certctl_approval_approve   POST   /v1/approvals/{id}/approve     approval.approve
  certctl_approval_reject    POST   /v1/approvals/{id}/reject      approval.reject

Break-glass credential admin (4):
  certctl_breakglass_list           GET    /v1/auth/breakglass/credentials
  certctl_breakglass_set_password   POST   /v1/auth/breakglass/credentials
  certctl_breakglass_unlock         POST   /v1/auth/breakglass/credentials/{actor_id}/unlock
  certctl_breakglass_remove         DELETE /v1/auth/breakglass/credentials/{actor_id}
  All gated auth.breakglass.admin; surface invisible (404 not 403)
  when CERTCTL_BREAKGLASS_ENABLED=false.

Bootstrap (2):
  certctl_bootstrap_status     GET   /v1/auth/bootstrap   (auth-exempt; safe probe)
  certctl_bootstrap_consume    POST  /v1/auth/bootstrap   (auth-exempt; one-shot mint)

Audit category filter (1):
  certctl_audit_list_with_category   GET   /v1/audit?category=<cat>   audit.read

WHY.

certctl_bootstrap_consume is the load-bearing day-0 primitive: a
fresh server with no admin actors lets the holder of CERTCTL_BOOTSTRAP_TOKEN
mint a fresh admin API key. Exposing it via MCP without a security
gate would let a downstream caller mint admin from any chat
transcript / log surface that captured the bootstrap token. The
tool description carries an explicit cautious-wording comment:

  CAUTION: NEVER WIRE THIS TO AUTONOMOUS OPERATION. A leaked
  bootstrap token from any log, telemetry, or chat-transcript
  surface lets a downstream caller mint a fresh admin API key
  bypassing every other access-control gate. Run this manually,
  exactly once, from a trusted shell.

Similarly certctl_breakglass_set_password's description flags
that the password crosses the MCP transport in plaintext; the
server-side handler hashes with Argon2id before persisting + the
audit row redacts, but client-side logging must NEVER capture the
payload.

HOW.

internal/mcp/tools_audit_fix.go (NEW):
  registerAuditFixTools(s, c) — declares the 11 tools via
  gomcp.AddTool. Each tool routes through the existing Client.Get/
  Post/Delete helpers; the server-side rbacGate wrappers (or
  auth-exempt allowlist, for bootstrap) handle authorization.

internal/mcp/types.go:
  Adds 5 input structs:
    ApprovalIDInput              (get/approve/reject)
    BreakglassActorIDInput       (unlock/remove)
    BreakglassSetPasswordInput   (set_password — flagged plaintext)
    BootstrapConsumeInput        (token + key_name; cautious comment)
    AuditListWithCategoryInput   (category + optional limit/since/until/actor_id)
  Each tagged with jsonschema descriptions for LLM tool discovery.

internal/mcp/tools.go:
  RegisterTools now calls registerAuditFixTools after the existing
  Bundle 2 Phase 9 registrar.

internal/mcp/tools_per_tool_test.go:
  allHappyPathCases extended with 11 new entries. The existing
  TestMCP_AllTools_HappyPath dispatches each tool via the in-memory
  MCP transport against a 2xx mock backend and asserts the
  wrapper-layer fence wraps the response; TestMCP_AllTools_ErrorPath
  dispatches against a 5xx mock and asserts MCP_ERROR fence.
  TestMCP_RegisterTools_DispatchableToolCount confirms every new
  tool is dispatchable by name.

VERIFY.

- go vet ./internal/mcp/...                                       PASS
- go test -short -count=1
  -run 'TestMCP_AllTools_HappyPath|TestMCP_AllTools_ErrorPath|
        TestMCP_RegisterTools_DispatchableToolCount'
  ./internal/mcp/...                                              PASS
- go test -short -count=1 ./internal/mcp/...                      PASS (0.3s)

Refs: cowork/auth-bundles-audit-2026-05-10.md MED-13
      cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md item 4
This commit is contained in:
shankar0123
2026-05-10 23:37:06 +00:00
parent 532cae249d
commit ca31232ad2
5 changed files with 300 additions and 0 deletions
+46
View File
@@ -689,3 +689,49 @@ type AuthListSessionsInput struct {
type AuthRevokeSessionInput struct {
ID string `json:"id" jsonschema:"Session ID (e.g. ses-abc123). Server-side own-bypass: caller may revoke their own session even without auth.session.revoke."`
}
// =============================================================================
// Audit 2026-05-10 MED-13 — input shapes for the 11 new MCP tools
// (approvals + breakglass + bootstrap + audit category filter).
// =============================================================================
// ApprovalIDInput is the id-only input for approval get/approve/reject.
type ApprovalIDInput struct {
ID string `json:"id" jsonschema:"Approval request ID (e.g. aprq-abc123). Returned by certctl_approval_list."`
}
// BreakglassActorIDInput is the actor-id-only input for the unlock + remove tools.
type BreakglassActorIDInput struct {
ActorID string `json:"actor_id" jsonschema:"Break-glass actor ID (e.g. bg-admin1). Listed by certctl_breakglass_list."`
}
// BreakglassSetPasswordInput is the body for certctl_breakglass_set_password.
//
// SECURITY: the password field crosses the MCP transport in
// plaintext. The server-side handler hashes with Argon2id before
// persisting; the audit row redacts the password column. Never log
// this payload at the client side.
type BreakglassSetPasswordInput struct {
ActorID string `json:"actor_id" jsonschema:"Break-glass actor ID (e.g. bg-admin1). New row created if not present."`
Password string `json:"password" jsonschema:"Plaintext password (hashed server-side with Argon2id). Choose >=14 chars from a strong-entropy source; this is the SSO-bypass credential."`
RoleID string `json:"role_id" jsonschema:"Role ID granted on successful break-glass login (e.g. r-admin). Typically r-admin for production break-glass."`
}
// BootstrapConsumeInput is the body for certctl_bootstrap_consume.
//
// SECURITY: NEVER wire this tool into autonomous operation. A leaked
// bootstrap token mints a fresh admin API key bypassing every other
// access-control gate. Run manually, once, from a trusted shell.
type BootstrapConsumeInput struct {
Token string `json:"token" jsonschema:"The pre-shared CERTCTL_BOOTSTRAP_TOKEN value (one-shot, constant-time-compared server-side, never logged)."`
KeyName string `json:"key_name" jsonschema:"Human-readable name for the new admin API key (e.g. 'day-zero-admin'). Subsequently visible in certctl_auth_list_keys."`
}
// AuditListWithCategoryInput is the input for the category-filtered audit list.
type AuditListWithCategoryInput struct {
Category string `json:"category,omitempty" jsonschema:"Audit category filter. One of: auth, pki, config, system, security. Empty returns unfiltered (equivalent to GET /v1/audit)."`
Limit int `json:"limit,omitempty" jsonschema:"Maximum rows to return. Server default applies when 0."`
Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp lower bound (inclusive). Optional."`
Until string `json:"until,omitempty" jsonschema:"RFC3339 timestamp upper bound (exclusive). Optional."`
ActorID string `json:"actor_id,omitempty" jsonschema:"Filter by originating actor ID. Optional."`
}