Skip to main content

Nakagai edge: the connector runtime that holds your broker credentials

Project description

nakagai-edge

The edge connector for Nakag.ai: a user-run runtime that is the only place a broker credential is ever written to disk. Your agent talks to exactly one MCP endpoint, the edge, and never sees a token. The platform never sees one either.

Why an edge exists

Broker credentials could live on the platform host. That topology has two problems no amount of hardening fixes: one platform compromise exposes every user's brokerage, and if the platform holds the token and makes the broker call, the platform placed the trade. The fix is a custody split:

  • Control plane: the platform (api.nakag.ai). Source of truth for everything that is not a broker secret: settings, the mandate, the monitor watchlist (what an account watches) and the auto-execute allowlist (the separate, smaller list autopilot may trade unattended), strategy configs, the connector registry, guardrail policy, the approval queue and its signing key, and audit ingest. It issues signed decisions. It never dials a broker and never executes an edge-origin trade.
  • Data plane: the edge (this package, user-run). Sole holder of broker credentials, stored locally under mode-0600 token files. Serves MCP on 127.0.0.1 to the agent, dials brokers with local credentials, and dials the platform as just another connector using the agent's own token.

One-endpoint topology. Your agent points at one URL, http://127.0.0.1:8330/mcp/, and finds the whole surface there: the edge's own tools (the broker vocabulary, approvals, the brake, check-in and chat) beside the platform's own tools, which the edge promotes to first-class names when it starts. get_signals, get_mandate, get_roster, run_backtest and the rest are called by name, not through a generic escape hatch.

Quickstart

# In the Nakag.ai web app: Agents page -> "Add agent" -> get a 10-minute pairing code.

# One command: pairs, syncs the registry, (after you confirm at the prompt)
# opens a browser to log you into your broker, connects your agent, then serves.
uvx nakagai-edge setup <code> --platform https://api.nakag.ai

setup is idempotent: re-running it on a healthy edge just starts the server, and it is also the repair path when something has drifted. The individual steps remain available: nakagai-edge pair, then nakagai-edge sync, then nakagai-edge login <id>, then nakagai-edge run. nakagai-edge status reports pairing and policy freshness without doing anything.

Connect your agent

setup wires up any agent client it recognizes, and prints the endpoint for any it does not. Already set up? nakagai-edge connect does the wiring alone, without serving:

uvx nakagai-edge connect

The one client recognized today is Claude Code, detected by claude being on your PATH. It gets an MCP entry added with claude mcp add --scope user nakagai --transport http <url>, at user scope because a project-scoped entry needs a per-project approval, and the six skills below are copied into ~/.claude/skills/. A client that is not detected is not an obstacle: the snippet below is the whole contract.

--no-register withholds the wiring, never the endpoint. Both setup --no-register and connect --no-register still print the URL and the snippet, and touch no client config at all.

The contract is one URL, and it carries no credential:

http://127.0.0.1:8330/mcp/

Paste this into any MCP client:

{
  "mcpServers": {
    "nakagai": {
      "type": "http",
      "url": "http://127.0.0.1:8330/mcp/"
    }
  }
}

The edge holds your platform token and your broker credentials. Neither ever enters your agent's config.

What is on the endpoint

As it ships today: 16 tools the edge serves itself, plus 17 of the platform's promoted to first-class names, 33 in all. Six platform tools share a name with one the edge already serves (agent_checkin, call_connector, get_approval, get_connector_status, list_connector_tools, send_message). The local tool wins outright; nothing is ever exposed twice, and nothing is ever prefixed.

A promoted name is the same call typed a shorter way. It travels the same guarded door as call_connector: the same classification, the same approval policy, the same audit record, and the same refusal once cached policy goes stale. Promotion changed which names exist, not what any of them is allowed to do.

Promotion happens once, at startup, before the first client connects. If the platform is unreachable at that moment, the promoted names are absent for the life of that process and the log says so; call_connector("nakagai-mcp", ...) still reaches every one of them, and a restart picks them up. If the platform goes down after startup, the tools stay listed and a call comes back with an error naming the nakagai-mcp connector, because a name that fails legibly beats seventeen that silently vanish.

Skills

Six skills ship inside the wheel:

  • connect-edge: connect a local edge to a hosted platform, and diagnose the known failure modes.
  • pair-agent: pair a new agent with the hosted platform, directly or through an edge, and run the first-session protocol.
  • verify-edge: the health ladder, from the local edge up to the platform relay, with an opt-in write-path drill through approvals.
  • daily-brief: signals, open risk, portfolio and pending approvals in one pass.
  • halt: stop trading authority now, and say precisely what is and is not stopped.
  • check-the-evidence: pull a play's proving record before endorsing it, and say so plainly when there is none.

A client that reads skills as files gets them installed by connect (Claude Code: ~/.claude/skills/). Any MCP client can read exactly the same text off the endpoint instead: nakagai://skills lists them with their descriptions, nakagai://skills/{name} is one skill's full text, and each is offered as an MCP prompt under its own name.

An edit of yours is never overwritten. connect records a hash of what it wrote, so a later run replaces only a file that still matches, and a skill you have tuned is left alone and reported as left alone.

Live chat with your agent

nakagai-edge run serves tools. It does not make you reachable. For that, run:

nakagai-edge listen

It holds the platform's chat channel open and prints one JSON object per owner message on stdout, {"seq", "text", "from", "at", "cursor"}. Point your agent at those lines and have it answer with the send_message tool. While it runs, the web app's chat pane reports "Agent connected", because the platform counts an agent as present only while a poll is genuinely held.

Notes that matter:

  • One listener per edge. A second one refuses to start. Two would both receive every message and both answer it.
  • Dedupe on seq. Delivery is at-least-once and send_message carries no idempotency key, so a re-delivery you answer twice posts twice.
  • A first-ever run starts from now. It will not replay your history. After that the read position is kept in cache/channel-cursor.json, so a gap between runs is picked up on the next start. --replay (default 20) bounds that to the newest N messages of the gap, since the recent end is the part still worth answering; it says on stderr how many it skipped.
  • Only owner messages are printed. Signals, briefings, and approval events are dropped rather than fed to an agent.
  • Chat is never mandate-gated. The kill switch halts trading authority, not speech: a halted agent must still be able to tell you that it is halted.

The write path

  1. Intent. The agent calls a write tool through the edge's MCP surface. The edge's guardrails classify it first, fail-closed, so an intent that would already be denied never leaves the edge.
  2. Pending approval. A write matching the approval policy is enqueued to the platform.
  3. A human approves in the web app, or the mandate does.
  4. Signed grant. On approve, the platform signs an Ed25519 artifact: {approval_id, agent_id, connector_id, tool, args_hash, account, expires_at}.
  5. Edge verifies and executes. The edge checks the signature, recomputes args_hash from its own copy of the arguments, checks expiry, and re-runs guardrails against its own synced policy before the broker is ever dialed.
  6. Execution report. The edge ships the outcome back and the approval record closes.

The platform never holds a broker credential at any point in this chain. It authorizes; the edge acts.

The capability layer

A broker connector is a downstream MCP server with its own name for everything: its own tool for placing an order, its own key for an account, its own field for a position's quantity. When those names live in the edge, a second broker is not a config change, and the failure is not a loud one. The brake stops seeing positions while every display goes on reporting them as guarded.

So the edge knows seven things a broker can be asked to do, and nothing about how any particular broker spells them: list_accounts, get_balance, list_positions, get_quote, list_orders, place_order, cancel_order.

Code owns the meaning and the type of every canonical field. quantity is a number, symbol is upper-cased, side is buy or sell. A connector's map owns location only: which downstream tool, which argument keys, which response paths. That split is the safety property. A wrong map produces a visibly wrong number or an extraction failure; it can never make quantity mean notional.

The vocabulary is closed on purpose. These seven cover shares. Options, futures and crypto each need their own notional math and their own envelope reasoning before they can be let in. Four option contracts at $2.50 compute as $10 of notional against a $2,000 per-order cap, when the real exposure is $1,000: an option's notional is contracts times premium times the 100 multiplier. A broker that adds futures tomorrow is refused by default rather than waved through under a notional nobody checked.

Adding a broker is data, not code. A connector declares its map in the registry, so a new brokerage is a registry entry rather than a release of this package:

- id: alien-broker
  kind: mcp-http
  role: broker
  capabilities:
    list_positions:
      tool: holdings
      args: {account: acct}
      items: [holdings]
      fields:
        symbol: [ticker]
        quantity: [qty]
        avg_price: [cost]
    place_order:
      tool: submit
      args:
        symbol: ticker
        side: action
        quantity: qty
        price: limit
        stop: trigger
        account: acct
      values:
        side:
          buy: [BUY]
          sell: [SELL]
      market_args: {kind: MARKET}

A place_order map has to name all five order keys: symbol, side, quantity, price and stop. The edge reads an executed entry back through them to build the ledger record the brake watches, so a map missing one places real orders that are then supervised by nothing, absent from get_open_risk while the Portfolio page still lists them. A connector declaring an incomplete place_order is refused when the registry is parsed, by name and by which keys are missing, rather than found later by a position that had no stop watching it. A connector that places no orders at all simply declares no place_order.

The order inside values.side is load-bearing. The list is every spelling this connector recognizes when it reads a side back off an order, and the first entry is the single spelling the edge sends when it places one or builds a stop's exit. So list them all, and put first the one that is correct whether the order opens or closes. A broker with separate verbs mapped as buy: [BUY_TO_OPEN, BUY_TO_COVER] sends BUY_TO_OPEN for every buy, including the one meant to cover a short.

The agent gets seven named tools it learns once and uses against any broker. connector_id is optional only while exactly one enabled broker declares the capability. Enable a second and the edge stops filling it in: the call comes back naming both candidates and the agent has to say which brokerage it meant. Letting registry order decide which broker received an order is not something an agent can review or an owner can predict, so this is the one place the layer gets louder rather than quieter as brokers are added.

call_connector remains the raw escape hatch: a broker tool outside the vocabulary is still reachable by its own name, through the same guardrails, the same approval queue, and the same audit record.

Three read-only classifications, each of which fails silently. An unclassified tool counts as a write (unknown_is_write, fail closed), and check_accounts denies a write that names no account whenever account tiers exist. That pair is right for an agent and wrong for the edge acting on its own behalf, so any tool the edge dials for itself has to be classified read-only, either by the downstream server's own readOnlyHint or by the owner's read_only_tools glob:

  • A connector's get_quote tool. Otherwise the brake goes blind: no price, no breach, no fire, and every display still saying guarded.
  • A connector's list_accounts tool. Otherwise account inference enqueues an approval instead of answering the question it was asked.
  • Anything else the edge dials on its own behalf, for the same reason. The map moved the tool names out of the edge; it did not move this requirement, which now has to hold once per connector rather than once in total.

The bundle schema gate. The edge refuses a policy bundle whose schema_version it does not understand. ConnectorSpec reaches the platform through PyPI and pydantic ignores unknown fields, so an edge running ahead of the platform would parse the older bundle cleanly and simply not find what the newer shape carries. Losing the capability map that builds a stop's exit order records every supervised position as unguarded while every display still calls it guarded. Refusing beats half-understanding. A refused bundle leaves the previous registry untouched and does not stamp freshness, so the cached policy goes on aging and everything is refused once the TTL lapses. nakagai-edge sync reports the refusal on the spot and nakagai-edge status carries a schema_error until a sync succeeds; the fix is to upgrade whichever side is behind, or pin an older nakagai-edge.

Two things a registry entry must get right. Both are the connector author's job and both fail silently:

  • Every broker connector must declare place_order.values.side. There is no default buy/sell vocabulary any more. There used to be a Robinhood-flavored one, and it would have quietly mistranslated the next broker's spelling. Without it an entry's side cannot be classified and the position is recorded blocked with the anomaly "unclassifiable order side": visible, unguarded, and never acted on.
  • A connector whose responses are enveloped must root its scalar capabilities with items:. Robinhood wraps everything in {"data": ..., "guide": ...}, so its get_balance needs items: [data] and unprefixed field paths beneath it. Without that the raw figures ship with the envelope still on and the Portfolio page renders blank.

The brake

Every out-of-sample number in Nakagai's evidence store was measured on a strategy that exits. Live, the agent places an entry and goes to sleep. The brake is what exits.

When the platform grants an entry it also signs an exit warrant scoped to that position: reduce-only, capped at the entry quantity, single-use, and expiring. The edge watches the position against its approved stop and places a market exit when the level is confirmed broken, with no agent and no model awake. The warrant is renewed on the ordinary sync cadence.

It is armed by default, because the stop it enforces is one you already approved when you stamped the entry. Two properties are deliberate and worth knowing:

  • It fires on stale policy and through a platform outage. Every other path in the edge refuses when policy goes stale, because every other path exists to restrain the agent. The brake's authority is in the signed warrant, and firing only reduces exposure.
  • The kill switch does not stop it. The kill switch halts the agent. Killing the agent must not strip the stops off your open positions.
nakagai-edge brake status              # what is watched, and its risk in R
nakagai-edge brake off                 # disarm, locally, with no network
nakagai-edge brake off --position <id> # release one position
nakagai-edge brake on                  # re-arm

The brake does not promise the level. A gap opens a position under its stop and the exit goes off at the market, below it. That is what a stop is.

A connector must declare a place_order capability with market_args before its positions can be supervised, and the two halves of that fail differently. A connector that declares place_order but no market_args still gets a ledger record: there is no exit order to build, so the position is recorded unguarded, listed that way by get_open_risk, and shown that way on the Portfolio page. A connector that declares no place_order at all leaves no ledger record to make, so its positions are absent from get_open_risk entirely; the Portfolio page still shows them unguarded, because a position with no record cannot be marked guarded. The Portfolio page is the surface that sees both, so it is the one to check before assuming a stop is being watched.

Failure modes

  • Platform unreachable. The edge caches the bootstrap bundle with a policy TTL (default 15 minutes). Reads may continue on the cached policy while the TTL holds; once it expires, everything is refused. Writes are impossible by construction the whole time: a write needs a live round trip to the platform's approval queue. An edge that started while the platform was down serves its own 16 tools and none of the promoted ones, since the tool list is built once at startup; restart it once the platform answers.

  • Revocation. Revoking an agent takes effect on the agent's next platform call: the bearer token 401s. Writes were already gated on a live platform round trip, so revocation closes them structurally.

  • A newer release exists. On start, the edge checks the package index and logs a line naming the newer version. That is the whole behavior: it never updates itself, and it never refuses to start. This daemon is the sole holder of your broker credentials, so replacing it is your decision, not a web index's. The check is bounded and advisory, and no network means no line. Upgrade when you choose to with uvx nakagai-edge@latest run.

Development

uv sync
uv run pytest

A handful of integration tests exercise the edge against the Nakag.ai platform package and skip automatically when it is not installed. The import closure of nakagai_edge itself is intentionally small (no pandas, numpy, or pyarrow) and enforced by tests/test_import_closure.py.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nakagai_edge-0.2.0.tar.gz (268.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nakagai_edge-0.2.0-py3-none-any.whl (144.1 kB view details)

Uploaded Python 3

File details

Details for the file nakagai_edge-0.2.0.tar.gz.

File metadata

  • Download URL: nakagai_edge-0.2.0.tar.gz
  • Upload date:
  • Size: 268.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nakagai_edge-0.2.0.tar.gz
Algorithm Hash digest
SHA256 31f11285412e164f74c8858679e38d572995dda6d5db0b46ef445a494fa6a43a
MD5 001226027d285f4b9b5e63474ce0c0ae
BLAKE2b-256 15081eae2a32ada61954b21fb5285d14528c1e59264b41f5b508fcb5f24cd7ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for nakagai_edge-0.2.0.tar.gz:

Publisher: ci.yml on loubylabs/nakagai-edge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nakagai_edge-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: nakagai_edge-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 144.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nakagai_edge-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e05bf704cf353c8a9a6871a1b09adf6f5520953d15f8836cc1950988902a96f6
MD5 2346b4150ba0c2c90804020b0263a3de
BLAKE2b-256 7be5871830d2ae9714fec4822718961d76c7b5f0c329b318f4f80cb00ac17e0a

See more details on using hashes here.

Provenance

The following attestation bundles were made for nakagai_edge-0.2.0-py3-none-any.whl:

Publisher: ci.yml on loubylabs/nakagai-edge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page