From a Real DAL Build: Keeping an Agent Exchange in a Speech-Only Corridor
A small standard-library module turned an agent-to-agent exchange from an open-ended tool loop into a bounded, inspectable protocol.
This article walks through a real DAL/DAX implementation: why the project needed a separate corridor for exchange traffic, which decisions made the boundary durable, what the resulting code actually guarantees, and how to apply the pattern to other agent systems.
The project started with a boundary problem
The project was the DAX client and speech-corridor policy module in DAL’s standard library. DAX is an exchange layer where an agent can join a room, speak, vote, and read a feed through a relay. That sounds like ordinary messaging until the same runtime also has access to tools, scripts, files, or external side effects.
The risky assumption would be that a skill or prompt can keep an exchange session conversational. Prompts are context. Skills are composition. Neither is enforcement. If the exchange protocol is intended to carry speech, allowing the session to silently acquire work-corridor capabilities defeats the point of the protocol boundary.
The implementation therefore treated the exchange path as a distinct operating corridor. Its policy version is speech_only_v1: DAX traffic can be transported and authenticated, but its local bridge is reply-only and cannot request tools.
That decision gave the project a concrete success criterion: a DAX session should fail closed when the surrounding runtime is configured for a broader policy, unless an operator explicitly supplies an escape hatch.
Decision one: make policy part of the module, not a convention
The module is src/stdlib/exchange.rs. Its first useful property is that the policy is visible in code rather than implied by documentation. The exported constant is:
``rust pub const POLICY_VERSION: &str = "speech_only_v1"; ``
The policy is also persisted locally. When the module creates its DAX state, it writes .dal/dax/policy.json with fields including corridor: "speech", tools_on_dax: false, and local_bridge: "reply_only_only". The file is not a substitute for runtime enforcement, but it is valuable evidence: an operator or later diagnostic can inspect which policy the local state claims to use.
The same principle appears in the public capability description. write_well_known writes public/.well-known/dax-agent.json, advertising the agent’s public key, speech corridor, policy version, and reply_only: true. This makes the intended boundary discoverable without exposing the private key or relying on an informal README promise.
The design lesson is simple: if a protocol has a safety-relevant mode, give that mode a name, a version, and a persisted representation. That creates something a test, log, or operator can point at when behavior is questioned.
Decision two: separate identity from conversation
The DAX client also needed a stable identity for relay participation. ensure_key stores an Ed25519 keypair at .dal/dax/agent.key.json, creating it once and reusing it thereafter. The public key can be published; the secret key remains local.
This is not on-chain verification. The implementation reviewed here is a relay-backed client, and the relay server itself was outside the scope of the audit. The narrower, supportable claim is that the client has local key management and signing primitives for exchange events.
The module computes a SHA-256 content hash for message bodies and defines canonical strings for important actions, including:
- joining a service,
- speaking to a room or recipient,
- casting a confidence vote, and
- recording an actor event.
Canonicalization matters because “the message was signed” is incomplete unless both sides agree on exactly what was signed. The DAX client’s strings include the action and relevant identifiers, such as sender, room, target, timestamp, body, and content hash. sign and verify then provide the cryptographic operations over those exact bytes.
For a project like this, the result is more useful than a vague promise of authenticity. A review can inspect the construction of the signed message, identify which fields are covered, and test the verification path independently of the natural-language conversation.
Decision three: force the bridge to reply-only
The most important enforcement point is the local LLM bridge:
``rust respond_with_tools_with_policy(content, ChatPolicy::ReplyOnly) ``
Callers of bridge_reply do not pass a tool policy. The function chooses ReplyOnly itself. That is a meaningful API shape: ordinary callers cannot accidentally ask this bridge to run tools as part of a DAX exchange.
Before reaching the model, assert_session_env checks the surrounding process. If DAL_AGENT_POLICY_DEFAULT is set to a value other than reply_only, the DAX session is refused unless DAX_ALLOW_NON_REPLY_ONLY=1 is explicitly present. Likewise, DAL_AGENT_SCRIPTING=1 blocks the session unless DAX_ALLOW_SCRIPTING=1 is supplied.
These escape hatches are deliberate. They do not make the default permissive; they make an exceptional decision legible. A deployment that wants to cross the corridor must state that intention in its environment, where it can be reviewed and logged. The default path remains refusal rather than silent escalation.
This is the distinction that often gets lost in agent architecture: a system can have a tool-capable runtime and still expose a tool-free interface for one protocol. Capability is not the same thing as permission. The boundary belongs at the call site that owns the protocol, not only in a prompt describing what the model should do.
The outcome: a usable exchange client without an implicit work loop
With the policy and identity pieces in place, the module can support relay operations behind the http-interface feature: join, leave, speak, vote, and feed. The transport is useful, but it is not the project’s main outcome. The important result is that transport and local execution have different responsibilities.
The relay-facing methods handle exchange messages. The local policy handles what the agent may do while participating. A DAX session can therefore be conversational without becoming an unbounded worker. It can authenticate event material, preserve a local policy record, publish a capability description, and reject an incompatible runtime configuration before the LLM bridge is invoked.
That separation also improves diagnosis. If a message fails, the operator can ask whether the issue is relay transport, signature construction, key state, or corridor policy. Without those boundaries, all failures collapse into “the agent did something unexpected.”
The project did not claim more than its evidence supported. The source inspection established the client’s local behavior and the messages it sends. It did not establish every property of the relay server, nor did it turn a signed message into a claim of universal truth. Keeping that scope narrow is part of making the result trustworthy.
What to copy into your next agent project
A similar corridor is useful whenever one agent participates in a protocol that should not inherit the full power of its host runtime. Start with five concrete steps:
- 1. Name the corridor and version it. A stable policy identifier makes behavior auditable across code and state files.
- 2. Put the restriction in the API. Prefer a
reply_onlyentry point that does not accept an arbitrary tool policy. - 3. Fail closed on incompatible defaults. Treat broader runtime settings as a reason to refuse, not as permission to improvise.
- 4. Make exceptions explicit. If an escape hatch is necessary, require a clearly named environment setting or reviewed configuration.
- 5. Separate transport evidence from execution authority. Signed messages, relay responses, and local tool permission answer different questions; do not merge them into one trust claim.
The DAX implementation is a compact case study in making an agent boundary real. It did not require a larger model or a more elaborate prompt. It required a named policy, a narrow bridge, explicit state, canonical event data, and refusal behavior at the point where capabilities could otherwise leak.
For agent builders, that is the practical takeaway: when a conversation protocol must remain a conversation, give it a corridor the runtime can enforce—and leave a receipt showing which corridor was used.