Skip to content

Answer known questions locally; gate egress; approve every key use - #13

Merged
abhijitkrm merged 9 commits into
mainfrom
feat/router-guard
Oct 9, 2026
Merged

abhijitkrm merged 9 commits into
mainfrom
feat/router-guard

Conversation

@abhijitkrm

Copy link
Copy Markdown
Owner

Implements the plan agreed for v0.4.x. The end-to-end run on a throwaway testnet comes next, before any release.

Local router: no model call for known questions and incidents

  • Known questions: an intent catalog (YAML: built-ins plus ~/.cometcli/intents) answers them with read-only tools and a fixed template.
    • It covers jailed validators, validator count, peers, sync, missed blocks, consensus, proposals, pending votes, rewards, balance, upgrade plan and slashing params.
    • Repeats within 30s come from a cache.
  • What always goes to the model: open-ended prompts (why/how/explain), actions (unjail/restart/vote yes), long or multi-line prompts, anything unmatched, and any lookup that fails.
  • /incident: triage runs locally. When one known case is clearly the root cause and has a playbook, cometcli runs it, records the incident and reports.
    • Changes and transactions still ask.
    • A failed step hands over to the model, with the triage and the steps done attached.
    • A declined step stops.
  • Playbooks so far: node down, jailed for downtime, halt-height left set, and container memory limit.
  • Commands and config: /llm forces the model, /route explains the routing, /cost counts local answers, and agent.router: off disables routing.

Egress gate

  • Key material: a message carrying any of these is withheld whole, in the history too:

    • PEM/OpenSSH keys, consensus keys
    • BIP-39 mnemonics, keystores, file-keyring entries, private-key hex
    • API and bot tokens

    A prompt containing a mnemonic is refused before it's sent.

  • Strict egress (default on validators): the model sees bash/read/grep/node.logs/fleet output only as a local summary, with identifiers masked, repeats folded and errors first. The UI and the audit log keep the raw output. RPC/sentry profiles and general mode default to filtered.

Key access

  • Every signature is its own approval. It shows the account, which key and where it lives, the decoded messages, the fee and the stated reason.
  • It is never auto-approved, and each use is audited as a key event.
  • Telegram approvals of a key use expire in 5 minutes.
  • The local keyring no longer signs during gas simulation, so the key is untouched until approval.

Tests

  • Router: classification (local and model cases), cache, fallbacks, read-only enforcement, router off.
  • Local incidents: full playbook run, conditional skips, handoff with context, a declined step stopping, and an unknown case going to the model.
  • Egress: scanner true positives and false positives, summaries, the agent-level gate, strict mode.
  • Key use: always asked even under the most permissive auto-approval, a decline meaning nothing is signed (simulation checked unsigned), and the Telegram expiry.
  • Registry: every playbook and intent names a real tool, and intents use only read-only tools.

🤖 Generated with Claude Code

abhijitkrm and others added 9 commits October 9, 2026 12:16
Local router: every prompt is classified on the operator's machine first.
- an intent catalog (YAML, built-ins plus ~/.cometcli/intents) answers
  known questions — jailed validators, validator count, peers, sync,
  missed blocks, consensus, proposals, pending votes, rewards, balance,
  upgrade plan, slashing params — with read-only tools and a fixed
  template: no model call; repeats within 30s come from a cache
- open-ended prompts, actions, long or multi-line prompts and anything
  the catalog doesn't clearly match go to the model; a failed lookup too
- /incident triages locally; when one known case is clearly the root and
  has a playbook, cometcli runs it (changes and transactions still ask),
  records the incident and reports. A failed step hands over to the
  model with the triage and the steps done; a declined step stops
- playbooks for node down, jailed for downtime, halt-height left set and
  container memory limit; kb show lists them
- /llm forces the model, /route explains the routing, /cost counts local
  answers; agent.router: off disables it

Egress gate, in front of every provider call:
- key material (PEM/OpenSSH keys, consensus keys, BIP-39 mnemonics,
  keystores, file-keyring entries, private-key hex, API and bot tokens)
  withholds the whole message, in history too; a prompt with a mnemonic
  isn't sent at all
- strict egress (default on validators): bash/read/grep/node.logs/fleet
  output reaches the model only as a local summary — identifiers masked,
  repeats folded, errors first; the UI and audit keep the raw output
- keystore paths join the protected paths for the shell and file tools

Key access:
- every signature is approved on its own with the account, which key
  and where, the decoded messages, fee and the agent's stated reason;
  never auto-approved, audited as a key event; Telegram approvals of a
  key use expire in 5 minutes
- the local keyring no longer signs during gas simulation — the key is
  untouched until the operator approves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- shell guard: refuse deleting a node's data dir or stores (state reset,
  takes priv_validator_state.json with it), copying or dd onto
  priv_validator_state.json, interpreter scripts that touch it, and
  <chain>d start / init / rollback — a model with every approval granted
  ran all of these on a drill node
- key_seed.json (evmd testnet's operator mnemonic) is protected key material
- strict egress masks peer topology (node.peers, sec.exposure, fleet.status)
- playbook conditions are checked against fresh signals: a halted node
  is jailed while the playbook runs
- container-memory-limit and node-crash-loop explain
  val-in-set-while-syncing, so the playbook isn't handed to the model
- the key use of a playbook step says which case and step it's for;
  raw-output steps are summarized by line count in the report
- config set agent.router / agent.egress; profile values merge
- init offers only persistent keyring backends (test is in-memory)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fee denom and gas price come from the node's own app.toml (host home,
  or inside the container) when the profile doesn't set them: a profile
  made with profile add couldn't unjail ('no fee denom')
- profile add: --signer-mode, --container-home, --container-keyring, so
  container signing works without init
- val-pvs-ahead no longer fires on healthy validators of a fast chain:
  the chain height is read with the state file, and the case needs
  evidence the node isn't signing
- triage collectors get 60s on SSH nodes (20s timed out across the WAN)
- gov propose defaults to a text proposal
- failed steps of local answers and playbooks render as errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- node.service stops docker nodes with a 60s grace period: docker's
  default 10s SIGKILLed evmd on every stop (exit 137)
- node-killed-sigkill (a forced or timed-out stop) gets a playbook, so
  /incident revives such a node locally: the live run went to the model
- wait.until fails when the condition isn't reached (non-zero exit; a
  playbook no longer carries on as if it held), and signal waits honor
  --validator instead of watching the profile's own validator
- -p explains that it never approves transactions or key use and names
  the exact cometcli command to run in a terminal (the model invented one)
- a playbook stops, rather than handing off to the model, when -p can't
  approve a step
- gov propose: integer flags no longer block the text default

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- upgrade.releases: releases newer than the running image (version and
  source repo from its labels), the upgrade handler names each one
  registers, and a choice when there are several
- upgrade.build: build the target on the node with the Dockerfile that
  built the running image (beside the compose project, else found under
  the home dir); optionally adds a no-op handler (RunMigrations) when the
  release registers none for the upgrade; checks version and handler in
  the built binary
- upgrade.ready: the new image registers the plan name, the running
  binary doesn't, the deployed compose file is known
- gov.propose --kind upgrade on docker nodes needs --image and refuses
  unless upgrade.ready passes: a name the new binary doesn't register
  halts the chain for good, one the old binary has switches nothing
- upgrade.switch: only after the 'UPGRADE NEEDED' halt, and only to an
  image that registers the halted plan — sed the image in the deployed
  compose file (backup kept), compose down, up -d, same data dir

Found on primium-1: every v0.6.x release registers only
v0.5.0-to-v0.6.0, which v0.6.0 has too, so v0.6.1 can't be reached by a
governed upgrade without a handler of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
upgrade.build takes an image template — {tag} is the release tag,
{version} the tag without v — e.g. primium-{tag} → primium-v0.7.3.
profile add --image-name saves it as the profile's naming scheme. The
build is the official tag as-is; the upgrade-handler patch is for forks
only and stays opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node's Dockerfile was written for the release it runs; a newer release
may need a newer Go (cosmos/evm v0.7.x needs 1.25.9, the deployed
Dockerfile pins 1.25.8). The build reads go.mod's go/toolchain line and,
when the builder image is older, builds from a copy with the Go image
raised — the original Dockerfile is untouched — and says so in a line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
upgrade.build reads the release notes: a state- or consensus-breaking
release always asks (RequireApproval, whatever --yes or autopilot say)
and the prompt says every validator must switch together at an upgrade
height. The Go builder bump is decided before the prompt and shown in
it. upgrade.releases marks breaking releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abhijitkrm
abhijitkrm merged commit 4ae0353 into main Oct 9, 2026
2 checks passed
@abhijitkrm
abhijitkrm deleted the feat/router-guard branch October 9, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant