Direct-agent authentication (SDK ≥ 0.2.0)
From SDK 0.2.0, agents are expected to authenticate to the MACP runtime
directly with their own Bearer identity. The orchestrator /
control-plane no longer forges envelopes on behalf of agents. This matches
RFC-MACP-0004 §4 ("sender MUST be derived from authenticated identity")
and the architectural invariants spelled out in
ui-console/plans/direct-agent-auth.md.
For the runtime side of identity binding (token validators, sender derivation, dev-mode fallback), see Runtime API § Authentication and Runtime Deployment § Authentication.
This guide shows the initiator and non-initiator patterns.
Bootstrap from the orchestrator
Your orchestrator (examples-service, scenario compiler, CLI — whatever
produces a run) should pre-allocate a session_id and hand each agent
a bootstrap document that includes:
runtime.address— gRPC endpoint (e.g.runtime.example.com:50051)runtime.bearerToken— per-agent Bearer tokenruntime.tls—truein production;falseonly for local devrun.sessionId— UUID v4 pre-allocated by the orchestratorparticipant.participantId— this agent's bare sender identityinitiator.sessionStartPayload+initiator.kickoff— only on the initiator agent's bootstrap
The session_id must satisfy the runtime validator (UUID v4/v7 or
base64url ≥22 chars). Use macp_sdk.new_session_id() to generate one.
Initiator agent
The initiator owns SessionStart. It is the agent whose identity the
runtime records as session.initiator, and the only participant
authorised to commit (unless policy delegates otherwise).
from macp_sdk import (
AuthConfig,
DecisionSession,
MacpClient,
new_session_id,
)
# Pulled from bootstrap JSON
runtime_address = bootstrap["runtime"]["address"]
bearer_token = bootstrap["runtime"]["bearerToken"]
use_tls = bootstrap["runtime"]["tls"]
participant_id = bootstrap["participant"]["participantId"]
session_id = bootstrap["run"]["sessionId"]
start_payload = bootstrap["initiator"]["sessionStartPayload"]
auth = AuthConfig.for_bearer(bearer_token, expected_sender=participant_id)
client = MacpClient(
target=runtime_address,
# secure=True is default; local dev:
allow_insecure=not use_tls,
auth=auth,
)
client.initialize()
session = DecisionSession(client, session_id=session_id, auth=auth)
# 1. Unary Send(SessionStart) — the runtime binds session.initiator
# to this agent's Bearer identity.
session.start(
intent=start_payload["intent"],
participants=start_payload["participants"],
ttl_ms=start_payload["ttlMs"],
)
# 2. Open the bidi stream for subsequent events.
stream = session.open_stream()
# 3. Emit the kickoff envelope (the first mode-specific message).
session.propose("p1", "deploy-v1", rationale="canary checks passed")
# … run the event loop on stream.responses() …Non-initiator agent
Non-initiators never call .start(). They open a stream on a session
that may or may not yet exist; the runtime delivers SessionStart as the
first envelope once the initiator emits it.
auth = AuthConfig.for_bearer(bearer_token, expected_sender=participant_id)
client = MacpClient(target=runtime_address, allow_insecure=not use_tls, auth=auth)
client.initialize()
session = DecisionSession(client, session_id=session_id, auth=auth)
stream = session.open_stream()
for envelope in stream.responses():
if envelope.message_type == "Proposal":
session.evaluate(
proposal_id=..., recommendation="APPROVE", confidence=0.9
)
elif envelope.message_type == "Vote":
# ... aggregate and possibly emit session.vote() ...
...Why expected_sender matters
The runtime already derives the envelope sender from the authenticated
identity — a spoofed sender= fails at the runtime with
UNAUTHENTICATED. Setting expected_sender on the AuthConfig lets the
SDK catch the mistake locally and raise MacpIdentityMismatchError
before the envelope hits the wire. Clearer traceback, no wasted RTT,
and no ambiguity about whose identity the session was bound to.
auth = AuthConfig.for_bearer("tok-alice", expected_sender="alice")
session = DecisionSession(client, auth=auth)
session.vote("p1", "APPROVE", sender="mallory")
# ↑ MacpIdentityMismatchError(expected="alice", actual="mallory")Cancellation
Cancellation authority stays with the initiator (RFC-MACP-0001 §7.2)
unless a policy's commitment.authority delegates it. Two patterns:
- Option A (default): the initiator agent exposes a local HTTP
POST /agent/cancelendpoint. The orchestrator calls it, and the agent invokessession.cancel(reason=...)over its own gRPC channel. Since SDK 0.3.0 this is wired automatically byfrom_bootstrap— see Agent Framework → Cancel callback. If you hand-roll the agent outside the framework you can still stand up the endpoint yourself withmacp_sdk.agent.start_cancel_callback_server(...). - Option B (opt-in): the scenario's policy designates the orchestrator
as a commitment authority; it can then call
CancelSessiondirectly.
Either way, the SDK's session.cancel() call carries the agent's Bearer
identity, so the runtime enforces authority consistently.