AgentID auth, external runner, activity protocol, approval flow #240
Draft
cyruszhang wants to merge 20 commits intomainfrom
Draft
AgentID auth, external runner, activity protocol, approval flow #240cyruszhang wants to merge 20 commits intomainfrom
cyruszhang wants to merge 20 commits intomainfrom
Conversation
Phase 0 of external-agent migration. Gateway accepts `Authorization: AIP <token>` and verifies via aip-identity-verify against a configured IdP. Legacy X-Agent-ID path is unchanged; AIP stays disabled when env isn't set. Wired in at the dashboard server and standalone gateway CLI. Tests + design doc included.
Constructed with aip_client (from aip-identity-sdk), transport sends `Authorization: AIP <token>` per request; AIPClient.get_token(audience) handles caching + refresh. Audience defaults to gateway base URL. Legacy X-Agent-ID path unchanged. New `[aip]` extra; lazy import.
Renames AIP → AgentID across gateway, SDK transport, and runner config.
Upstream packages aip-identity-{verify,sdk} 0.1.x → agent-id-{service,
client}-sdk 0.2.0; classes are `Verifier`/`AIPClient` and errors are
`TokenExpiredError` / `TokenInvalidError` / `ProviderUntrustedError` /
`SignatureInvalidError`. Optional extras renamed `[aip]` → `[agentid]`.
X-Agent-ID is removed end-to-end. Gateway accepts only `Authorization:
Bearer <token>` verified by AgentID; missing/malformed → 401, no
verifier → 503. Transport never emits X-Agent-ID. Legacy dojozero-agent
CLI + robust_agent.py demo will 401 — follow-up.
Verifier wires activity reporting: when DOJOZERO_AGENTID_ACTIVITY_API_-
KEY + AGENT_TOKEN are set, auth.verify is auto-emitted per request.
Adds dojozero-agent-runner package skeleton + RunnerConfig (env-driven,
loads persona/LLM YAMLs, AgentID identity required); core loop is a
stub. Env vars `DOJOZERO_AIP_*` → `DOJOZERO_AGENTID_*`, documented in
.env.example and deploy/.env.template. "Protocol" wording dropped.
Adds the runner core (_runner.py + _llm.py + _tools.py): loads Identity.from_env(), builds Client for Bearer auth, opens DojoClient.connect_trial with agentid_client, constructs an agentscope ReActAgent with the persona prompt + matched LLM, registers SDK-backed tools (get_balance, get_current_odds, place_bet), polls events and reacts until trial_ended. Phase 1 canary: degen × Qwen (qwen3-max). DojoClient.connect_trial gains agentid_client / agentid_audience params, threaded through GatewayTransport. Daemon resolves auth via AGENTID_AGENT_ID env first (Identity + Client), falls back to legacy api_key when unset; the daemon's "no auth configured" startup error points at AgentID env vars first, legacy second. agent-id-client-sdk is lazily imported so dojozero-client still installs without it. dojozero-client 0.3.0 → 0.4.0 (breaking; matches the gateway-side Bearer-only switch). 7 new runner tests cover event formatting and SDK-backed tool behavior.
Wires four hand-emitted Tier-1 emitters: auth.deny on Bearer verify failure, session.start on register, session.end on unregister + a lifespan sweep for stranded sessions, transfer.value on accepted bets. Per-request auth.verify auto-emit OFF (report_auto_verify=False) — projected ~2.7M/day flood replaced by hand-emitted high-signal categories. All best-effort; failures log WARN, never block auth. 14 new tests. Known issue: payload fields don't match aip-activity's server schemas (would 422); reconciliation tracked in agent-identity/design/2026-05-04-activity-discovery.en.md §9.
Reconciles gateway/_activity.py with aip-activity's server schemas.
Before, every emitted event would 422 — fields were structurally
wired but didn't match server expectations. emit_auth_deny: rename
reason→error_class. emit_session_start: persona/model/sport_type
into attributes dict, scenario="trial". emit_session_end: roll-ups
into summary dict, outcome="completed". emit_transfer_value: canonical
{amount_bucket, currency, direction, amount?, purpose, transaction_id,
linked_tier2="dojozero.bet_decision"}; hub-specific richness deferred
to the linked Tier-2 event. New _amount_bucket() helper mirrors
aip-activity's AMOUNT_BUCKETS whitelist. 28 tests (was 14).
Make DojoZero a first-adopter publisher of the AgentID activity protocol.
The gateway now serves all four discovery artefacts so aip-activity can
verify Tier-2 events from this hub without any out-of-band registration.
- New HubPublisher module: loads an Ed25519 keypair from
DOJOZERO_HUB_SIGNING_KEY_PEM, signs the manifest via
agent_id_service_sdk.sign_manifest, and serves JWKS / categories /
schemas. Hard-coded v1 catalog: dojozero.bet_decision (transaction_id,
decision_kind, selection, stake/confidence bucket, rationale_hash,
model, game_id, sport).
- Four .well-known routes on the gateway:
GET /.well-known/agent-id-jwks
GET /.well-known/agent-id-activity-manifest (application/jose)
GET /.well-known/agent-id-activity-categories
GET /.well-known/agent-id-activity-schemas/{verb}/{version}
All return 503 when DOJOZERO_HUB_* env is unset (publisher disabled).
- Wired HubPublisher.from_env() into both gateway-construction sites
(CLI and dashboard_server/_trial_manager).
- 16 tests including a full HubManifestFetcher round-trip that proves
wire compatibility with aip-activity's discovery client.
- Bump agent-id-service-sdk>=0.3.0 with local path source for dev.
The agent's invocation of place_bet is a tool call; emit canonical Tier-1
tool.use alongside the existing transfer.value so consumers can join the
"what tool did the agent run" stream to the "what value moved" stream.
- New emit_tool_use() in gateway/_activity.py: payload is
{tool_name, args_hash, tool_invocation_id, duration_ms?, success,
linked_transfer_id?}. args_hash is sha256 over canonical-JSON form so
raw args never leave the gateway. Returns the invocation id so callers
still link forward when the verifier is None.
- _hash_args + new_tool_invocation_id helpers; sorted-key JSON form
makes the same args hash the same regardless of dict order.
- POST /bets emits tool.use on both success and failure paths, with
duration_ms from time.monotonic() around the broker call. Success
case sets linked_transfer_id = bet_id (= transfer.value.transaction_id);
failure case omits it (no transfer happened).
- 8 new tests covering hash stability, payload shape, success/failure
outcomes, caller-supplied invocation ids, duration clamping, and
swallowed emission failures.
LLM token usage / latency happens entirely on the runner side and is
invisible to the gateway today. Wire it through: runner wraps the chat
model, posts usage to the gateway after each call, gateway forwards as
a canonical Tier-1 model.call. The hub stays the single emission source.
- emit_model_call() in gateway/_activity.py: payload is
{model, tokens_in, tokens_out, latency_ms?, cost_usd?, purpose?,
outcome?, linked_tool_invocation_id?}; negatives clamped fail-safe.
- POST /activity/model-call: agent-Bearer-authed, requires registration,
forwards via emit_model_call.
- ModelCallReport pydantic model with camelCase aliases.
- TrialConnection.report_model_call() in dojozero-client; failures
swallowed (observability never blocks the agent).
- EmittingChatModel wraps any agentscope ChatModelBase: time.monotonic
latency, extracts usage from ChatResponse, reports success/error
outcome. Streaming pass-through with one-time INFO log (canary uses
stream=False).
- Runner wraps create_model(...) in EmittingChatModel(_, connection)
before constructing the ReActAgent.
- 10 new tests (5 emit_model_call, 5 EmittingChatModel); 1091 passing
repo-wide.
Every accepted bet now emits both Tier-1 transfer.value and Tier-2 dojozero.bet_decision joined by transaction_id. Closes Phase 4. - emit_bet_decision() validates against the JSON Schema HubPublisher serves; sha256 of rationale (raw text never leaves), confidence bucketed low/medium/high. Unknown markets log + skip. - BetRequest + client place_bet gain optional model/confidence/rationale, forwarded only to the Tier-2 event. - POST /bets emits transfer.value then bet_decision; sport from state.metadata, game_id from broker._event when available. - 14 tests including a round-trip that validates payloads against the published schema.
The gateway was reinventing Ed25519 → JWK encoding and shipping a dojo0 hub-keygen subcommand that did literally nothing the SDK CLI doesn't. Pull both into the SDK so every adopter shares one implementation; document the mint command in the deployment doc. - _hub_publisher.py: drop private _public_key_to_jwk; use agent_id_service_sdk.public_key_to_jwk instead. - Delete dojo0 hub-keygen subcommand + tests. Operators run `python -m agent_id_service_sdk.keygen --out hub.pem`. - docs/external_agent_migration.md: new "Hub publisher" section with the four DOJOZERO_HUB_* env vars and a pointer to the SDK keygen as the canonical mint flow.
- pyproject: SDK 0.3.0 resolves directly from PyPI now that it's published; lockfile re-resolved. - New tests/test_e2e_hub_discovery.py (10 integration tests, gated behind --run-integration). Stands up the gateway with a real HubPublisher and runs HubManifestFetcher against it via httpx ASGITransport — no network, no subprocesses. Catches drift between the signer side and the verifier side. - Test imports app.main from aip-activity via pytest.importorskip so it skips cleanly when aip-activity isn't installed; opt-in via `uv pip install -e ../aip-activity`. - Not in GitHub CI — aip-activity is on Alibaba GitLab, not reachable from GitHub Actions. Aone is the planned CI home.
Reuse the hub publisher's Ed25519 key (the one already signing the manifest) for outer-envelope auth on each activity POST. Sunsets the static DOJOZERO_AGENTID_ACTIVITY_API_KEY path with no migration required — DojoZero is the only adopter so far. - _agentid.py: drop DOJOZERO_AGENTID_ACTIVITY_API_KEY env read. Construct HubPublisher.from_env() and thread its (private_key, kid, service_id) into the SDK Verifier as hub_signing_key / _kid / _service_id. The activity_endpoint and agent_token_for_emit pass-through is unchanged. - Activity reporting now requires the hub publisher to be configured (DOJOZERO_HUB_SERVICE_ID + DOJOZERO_HUB_SIGNING_KEY_PEM). Without those, emission stays disabled — same behavior as before just triggered by a different env var. - test_gateway_agentid: replace the bearer-key wiring test with a hub-publisher wiring test (mints a fresh keypair, asserts the Verifier received the right private_key/kid/service_id). - Bump agent-id-service-sdk pin to >=0.4.0; local-path source override during 0.4.0 development. 1106 unit + 10 E2E green.
…SDK)
Before: the E2E discovery test imported app.schemas.namespace_ownership
from aip-activity, forcing every contributor (and any CI environment) to
have aip-activity installed in DojoZero's venv. With the helper lifted
into agent-id-service-sdk 0.4.1, the cross-repo coupling disappears.
- test_e2e_hub_discovery.py: import verify_namespace_ownership +
NamespaceOwnershipError from agent_id_service_sdk. Drop the
pytest.importorskip("app.main") and the type-ignore'd lazy imports
inside TestNamespaceOwnership.
- Update the test docstring: GitHub CI can now run this freely; the
Aone-only constraint applied to the (deferred) Pass 2 ingest test,
not Pass 1 discovery.
- Bump SDK pin to >=0.4.1. 1106 unit + 10 E2E green.
The gateway no longer needs an IdP-issued JWT to declare its privacy posture — that was a leftover from the pre-§5.0 era when X-AgentID-Token was the only way to carry the claim. Read the posture from env, build the privacy dict, hand it to the SDK Verifier; SDK signs it into every HubJWS envelope. - _agentid.py reads DOJOZERO_HUB_PRIVACY_DEFAULT_LEVEL (default 'summary') and optional DOJOZERO_HUB_PRIVACY_OVERRIDES (JSON map). Builds hub_privacy_claim and passes to Verifier. - Drop DOJOZERO_AGENTID_AGENT_TOKEN env read; agent_token_for_emit kwarg no longer set on the Verifier. - Update test_gateway_agentid wiring test for the new privacy env vars and the absence of the token. Add monkeypatch.delenv on DOJOZERO_HUB_* in the minimum-config test so a populated dev .env doesn't leak. - Bump SDK pin to >=0.4.2. 1106 unit + 10 E2E green.
Hub identity is process-wide and origin-wide, not trial-scoped. The four .well-known/* routes were attached to per-trial gateway apps, which meant the hub's public surface only existed while a trial was running — and got duplicated across every concurrent trial. Move them to the dashboard server's persistent app where they belong. - New gateway/_hub_routes.py: register_hub_routes(app) helper. Reads app.state.hub_publisher at request time so callers populate it during their own lifespan setup. - dashboard_server/_server.py: builds HubPublisher.from_env() in lifespan, calls register_hub_routes(app). Routes available before any trial is scheduled, persist after they finish. - gateway/_server.py: drop the four route handlers, the hub_publisher field on GatewayState, and the hub_publisher kwarg on create_gateway_app. Per-trial gateways no longer carry the hub surface. - cli.py + dashboard_server/_trial_manager.py: drop hub_publisher threading; trial gateways are created without it. - Tests: hub-publisher and E2E discovery tests pivot to mounting register_hub_routes on a minimal FastAPI app — same code path the dashboard uses, no trial-app construction needed just to exercise the four routes. 1106 unit + 10 E2E green.
Run the runner against a live trial without the DojoZero shell setup. - _config.build_config(): programmatic entry point that both env and CLI loaders reduce to. RunnerConfig.identity carries a pre-loaded AgentID Identity (portal-zip or CLI profile); falls back to Identity.from_env() only when None. - __main__.py: argparse CLI with --portal-zip / --agent-profile / --dashboard-url and friends; flags override env. - --dashboard-url derives the trial-prefixed gateway URL and defaults the AgentID audience to the dashboard origin — keeps the JWT `aud` matching what the gateway expects. gateway register_agent: accept AgentID Bearer JWT as preferred auth, fall back to legacy apiKey for pre-AgentID clients. Mismatched apiKey/Bearer identity → 403.
Identity (mutually exclusive): --agent-profile <name> profile dir --agent-zip <path> zip (none) AGENTID_* env Brain: --agent-brain <path> single YAML (sys_prompt + model:) --agent-prompt + --agent-model piecewise / per-field override Removes --persona/--llm matrix lookup and the older flag set (--portal-zip, --identity, --sys-prompt-file, --model-config, ...). Adds `dojo0 agents build-brains`: expands agents/personas × agents/llms into per-combination YAMLs in ./agent-brains/. Authoring matrix stays; expansion is a build step. Requires agent-id-client-sdk 0.2.1. 35 tests pass.
Agents registering with request_approval=true route bets through
an IdP-mediated approval. Hub submits to /agentid/approvals,
polls, verifies the decision JWT vs IdP JWKS, mints + consumes a
single-use grant, then places the bet atomically.
Gateway: new _approvals.py + /bets/pending/{id} route. Bet
content sourced from local stash, never from JWT ctx. Adds
approval.requested/granted/denied Tier-1 emissions.
Client: place_bet returns BetResult | PendingApproval; new
check_pending_bet returns BetResult on approved-and-placed in one
round-trip. connect_trial gains request_approval.
Runner: --request-approval flag (env DOJOZERO_REQUEST_APPROVAL).
In approval mode, registers check_pending_bet tool. Grants stay
hub-internal; agent only sees approval_id.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Five related but separable threads in one PR — they share fate (the
runner uses Bearer auth, the activity protocol assumes Bearer auth)
and the conversion was done as one continuous arc.
1. AgentID Bearer authentication
Replaces the legacy
X-Agent-IDheader withAuthorization: Bearer <JWT>, verified at the gateway against the configured AgentIDprovider's JWKS. Cross-hub identity instead of per-trial API keys.
_agentid.pybuilds aVerifierfromDOJOZERO_AGENTID_*env config; every authenticated request resolves
agent_idfromthe JWT
sub. 503 on misconfiguration (no silent fallback).dojozero-client.GatewayTransportaccepts anagentid_clientand anagentid_audience; tokens are minted perrequest and sent as
Authorization: Bearer ....X-Agent-IDis no longer accepted by the gateway or sentby the client. The legacy
dojozero-agentCLI and the demorobust_agent.pywill get 401 against an AgentID-enabled gatewayand need a follow-up to migrate (out of scope here).
2. Phase 1 canary runner
New
dojozero-agent-runnerpackage — a self-contained external agentrunner that connects to a DojoZero gateway as an agent over Bearer
auth. Canary configuration: degen × qwen-max.
_runner.py: opensDojoClient.connect_trialwith anagent_id_client_sdk.Clientfor AgentID identity, builds anagentscope
ReActAgentfor the configured persona × LLM, pollsevents, reacts until
trial_ended._llm.py: factory forChatModelBase+ matchingFormatterBaseacross openai / dashscope / anthropic / gemini / grok.
_tools.py: registersget_balance,get_current_odds,place_bet,get_betsas ReAct tools backed by the SDK._emitting_model.py: wraps theChatModelBaseso each LLM callreports
model.callback to the gateway (best-effort; observabilitynever blocks the agent loop).
Deliberately lighter than the in-process
BettingAgent— no eventthrottling, retry queues, or memory compression. The canary's job is
to prove the externalization contract works end-to-end; performance
work lands only when a success-criteria gate fails.
3. AgentID activity protocol
Makes DojoZero a first-adopter hub of the activity-discovery design
(
agent-identity/design/2026-05-04-activity-discovery.en.md).Hub discovery surface — four public
.well-known/*endpoints onthe gateway:
GET /.well-known/agent-id-jwks— Ed25519 JWKS (RFC 7517/8037)GET /.well-known/agent-id-activity-manifest— JWS-signed manifestGET /.well-known/agent-id-activity-categories— Tier-2 catalogGET /.well-known/agent-id-activity-schemas/{verb}/{version}— JSON Schemaaip-activity verifies these via
HubManifestFetcher; namespaceownership is enforced by eTLD+1 of
service_id(no central registry).Activity emitters (gateway-side, all best-effort):
auth.deny,session.start,session.end,transfer.value,tool.use(onPOST /bets)model.callviaPOST /activity/model-calldojozero.bet_decision, joined totransfer.valueviatransaction_id. Carries decision_kind, selection, stake/confidencebuckets, sha256(rationale), model, sport, game_id. The hub-flavored
richness the canonical Tier-1 schema can't carry.
Per-request
auth.verifyauto-emit is off — at full DojoZeroload it would produce ~2.7M near-identical events/day. Tier-1
categories with real signal are hand-emitted instead.
4. Agent runner: --agent-* flag set, identity vs brain decoupled
Reworked the runner CLI around the "identity is durable, brain is
iterated often" split. Identity (profile dir or zip) and brain
(sys_prompt + model config YAML) are now orthogonal inputs.
Identity (mutually exclusive):
--agent-profile profile dir under $AGENTID_HOME/agents/
--agent-zip zip with agent.json + private_key
(none) AGENTID_* env (k8s pod default)
Brain:
--agent-brain single YAML, sys_prompt + model:
--agent-prompt + --agent-model piecewise / per-field override
Removes the legacy --persona / --llm matrix lookup (DojoZero-repo-
bound) plus --portal-zip, --identity, --private-key,
--sys-prompt-file, --model-config and DOJOZERO_* env equivalents.
New
dojo0 agents build-brainsexpands agents/personas × agents/llmsinto per-combination YAMLs in ./agent-brains/. The runner consumes
these via --agent-brain; matrix authoring stays where it is,
expansion is a build step.
Requires agent-id-client-sdk>=0.2.1 (adds AgentConfig — the brain
loader). Now on PyPI.
New section 5
5. Approval flow (spec §7.6.7 grant tokens)
Agents registering with --request-approval route every bet through
an IdP-mediated approval. Hub submits to /agentid/approvals, polls,
verifies the decision JWT vs IdP JWKS, mints + consumes a single-use
hub-internal grant, then places the bet atomically.
Gateway:
verification.
PendingApprovalResponse.
verifies + mints + consumes + places in one shot, returning
BetResponse.
(per design §9.5).
Client (dojozero-client):
approve-and-placed, PendingBetStatus otherwise.
Runner:
place_bet's docstring for the pending contract.
Bet content sourced from the local stash, never from the JWT's ctx —
a buggy IdP can't change what gets placed. Grant tokens stay
hub-internal; agent only ever sees approval_id.
aip-idp: zero changes (existing /agentid/approvals + portal endpoints
already do everything). DojoZero is the first hub adopter of this
spec section.
Configuration
DOJOZERO_AGENTID_TRUSTED_PROVIDERSDOJOZERO_AGENTID_AUDIENCEaud)DOJOZERO_AGENTID_ACTIVITY_API_KEYDOJOZERO_AGENTID_AGENT_TOKENDOJOZERO_HUB_SERVICE_IDDOJOZERO_HUB_SIGNING_KEY_PEMpython -m agent_id_service_sdk.keygen --out hub.pemDOJOZERO_HUB_NAMESPACEdojozero)DOJOZERO_HUB_SIGNING_KIDhub-key-1)DOJOZERO_AGENT_PROFILEDOJOZERO_AGENT_ZIPDOJOZERO_AGENT_BRAINDOJOZERO_AGENT_PROMPTDOJOZERO_AGENT_MODELDOJOZERO_REQUEST_APPROVALDOJOZERO_AGENTID_APPROVAL_ENDPOINTSee
docs/external_agent_migration.mdfor the full env-var table.Tests
publisher, hub-keypair handling.
tests/test_e2e_hub_discovery.py—stands up the gateway with a real
HubPublisherand runsHubManifestFetcheragainst it viahttpx.ASGITransport. Catchesdrift between signer and verifier across the full chain (manifest
JWS, JWKS encoding, eTLD+1 ownership, schema URL resolution,
fetcher caching). Gated behind
--run-integration; opt-in viauv pip install -e ../aip-activity. Not in GitHub CI — Aone isthe planned home (DojoZero on github.com but aip-activity on
Alibaba GitLab; cross-repo checkout works in Aone, not GitHub).
Dependencies
agent-id-service-sdk>=0.3.0(PyPI) —Verifier,HubManifestFetcher,build_manifest/sign_manifest,generate_signing_keypair,public_key_to_jwk. Symmetric sign +verify means gateway and aip-activity agree on wire format by
construction.
agent-id-client-sdk>=0.2.0— runner-side identity / token minting.Test plan
uv run pytest packages/— full unit suite greenuv pip install -e ../aip-activity && pytest packages/dojozero/tests/test_e2e_hub_discovery.py --run-integration— E2E greenDOJOZERO_HUB_*+DOJOZERO_AGENTID_*secrets, deploy gatewaycurl https://<pre-gateway>/.well-known/agent-id-jwksreturns JWKS with the expectedkidcurl https://<pre-gateway>/.well-known/agent-id-activity-manifestreturns a compact JWS withContent-Type: application/josetool.use,transfer.value, anddojozero.bet_decisionjoined bytransaction_idmodel.callafter each LLM invocation; rows show in aip-activitydojo0 agents build-brains --personas degen --llms-filter Qwenwrites ./agent-brains/dojozero-degen-qwen.yamlOut of scope (follow-up)
dojozero-agentCLI / demorobust_agent.pyoff
X-Agent-ID(will get 401 against AgentID-enabled gateway).compression) — only when a success-criteria gate fails without it.