Skip to content

M2-37 Guide "connect your own agent" with a check run

No Date Ruling Reason
D-M2-37-1 2026-10-25 Page, language and nav: the guide is docs/guides/connect-agent.md, in English like the site, in a new nav section Guides after API; it addresses a developer who connects an own adapter. Sections in this order: Placeholders, Prerequisites, the ten steps with the bold titles of chapter 11 section 13 (without the final period), "How the repository checks this guide", Next. One place for the guides of the site; the step titles bind the page to chapter 11.
D-M2-37-2 2026-10-25 Python is the only executable path: the guide's code is the example adapter in sdk/python/examples/guide/; TypeScript gets one sentence in step 4 with links to sdk/typescript/README.md and its reference. No Mermaid block; the page links the chapters instead. One executable path keeps the check exact; every fence must be executable or compared.
D-M2-37-3 2026-10-25 Owner steps through the API: every owner step shows the operation; one sentence says that the admin screens of app and console call the same operations. The API is the path a developer can follow today and the one every screen uses.
D-M2-37-4 2026-10-25 Block grammar: every fence starts at column 0 with a language and title="..."; kinds request (http, <Owner\|Alex\|Sam>: <operationId>), response (json, Response <status>), shell (bash, Shell: <text>), adapter start and stop (bash), output (text), file (python, the repository path) and pin (toml, pyproject.toml (excerpt)); fences only in Prerequisites and the steps; there is no actor Agent. The grammar is described in services/server/test/guide/README.md. The contract between the text and the test; an untitled or unknown block cannot slip through unchecked.
D-M2-37-5 2026-10-25 Placeholders and secrets: the runner binds {server_url}, {adapter_dir}, {alex_id}, {sam_id}, {new_uuid} and {expires_at} (start time plus half of auth.api_token_max_ttl), listed exactly in the table of section Placeholders; every other name is captured once by a response or output block before its first use and compared afterwards; "{name}" keeps its JSON type; bash values are single-quoted and must have the form of a shell value. {secret:<file>} stands only in a response block: written to the adapter folder with mode 0600, never compared, never logged, redacted from every error. The page reads like a guide with placeholders while every value is still checked; a token never appears in the text or the logs (T-TK-02, SEC-LOG-2).
D-M2-37-6 2026-10-25 Matching: JSON bodies are expected subsets (objects by member, arrays element by element in any order with exactly one distinct match each, an element matching several is ambiguous); output lines are compared in order, an unbound placeholder captures \S+; a process that ends early or leaves unexpected lines fails. Every failure names the step, the block title and line and, for JSON, the JSON Pointer; nothing after it runs. Answers carry many members the guide need not show; the order of array elements is not part of the contract.
D-M2-37-7 2026-10-25 Actors: Owner is the first owner of the harness server and does a fresh step-up before each request, as the admin screens do (08 6.2); Alex (trigger with approve_actions on Support) and Sam (write) are created by the harness; the harness client adds the session token, DPoP and Idempotency-Key. No owner block can fail with step_up_required; the people of the white-label examples.
D-M2-37-8 2026-10-25 Example layout and output: one program in small step modules (keygen, agent, connect, heartbeat, commands, invocation, runtime, run, interactive, tests/test_agent.py), each shown whole in its step; one stdout line per confirmed action, Step <n>: <text>, with run, thread and component IDs but never content, prompt, token, key or a person's name; SDK logs to stderr at WARNING. The start lines come once per process in the order 4, 5, 6: on_ready prints the connection (getMe) and sends one sendHeartbeat (the gateway heartbeat is acknowledged before its agent part is applied, M2-20), the commands line follows the reconciliation, and prepare waits for all three. A run the inbox held before the start prints "resuming ... from the inbox" instead of the acceptance line; --stop-after accept stops the adapter from prepare after the acceptance line, so the entry stays accepted. Commands are prefixed with PYTHONPATH=sdk/python/src, because the SDK project is not installable before its release chain (M2-23/1). The guide shows the whole program; the output is deterministic and safe to print.
D-M2-37-9 2026-10-25 Rule-based runtime: fixed rules on the trigger text replace a model (case-insensitive, first match wins: follow-up, question, approval, long; the model command summarize; anything else a fixed answer); usage is fixed token counts without amount_micros. The guide costs nothing and its output is the same on every run (PLAN 3 point 5).
D-M2-37-10 2026-10-25 Runner in Go (services/server/test/guide, component testsupport) on the shared harness of M1-71 (harness.Start, NewUser, StepUp, AdminDB); ServerConfig raises ratelimit.*, points the scan at the scripted clamd (D-M2-37-11) and sets command.autocomplete_timeout to testkit.Patience plus a minute (http.request_timeout above it), as the autocomplete tests of M2-17 do, because the default would bound step 8 by the speed of the machine (red team finding, class fix with TestServerConfig_NoStepIsBoundByTheServersSpeed); commands run in the repository root with the test's environment minus every BOTHY_ variable and without bytecode files, each in its own process group; every wait ends at the test's deadline. The pin is read with a TOML subset parser (tables, strings, arrays of strings), so no new Go module is added. The owner and the people need the device-key and DPoP clients of the Go test support; no second harness.
D-M2-37-11 2026-10-25 Files: MessageCreate.attachment_file_ids is enabled on the base branch (M3-07 merged), so step 9 has the fifth part: Alex asks for a file, the example uploads a small text file through createUpload, uploadPart and completeUpload of the generated core (the hand layer has no file module), sends it with attachment_file_ids (the parameter of Outbox.send_message that M3-12 added in parallel), the heartbeat reports files_out, and getMessage shows the attachment's original_name. The test server scans with the scripted clamd of the M3-07 tests (clamdtest.Fake, file.scan.clamd_address). Alex's grant carries upload besides approve_actions, because a run acts with the rights of its trigger (SR9): without it createUpload answers 403 forbidden with reason acting_for_insufficient. The text states only what the server does; the file setup is the one the merged attachment tests use.
D-M2-37-12 2026-10-25 Examples follow every rule of hand-written code of sdk-python: SPDX header, ruff with D (tests excluded) and the formatter, strict mypy (files covers examples/guide), bandit (the root make lint excludes */examples/*/tests/* like tests/), pytest collects examples/guide/tests with examples/guide on the path, and coverage measures examples/guide next to bothy_sdk. The example is the code developers copy.
D-M2-37-13 2026-10-25 CI path selection: docs/guides/connect-agent.md and docs/spec/11-extensions.md select server and sdk-python; sdk/python/examples/ selects server through the existing prefix sdk/python/. The check run is a server test that reads these files.
D-M2-37-14 2026-10-25 Suggestions are shown in step 8, in Sam's post, right before the model command: the server answers an autocomplete request in a forum channel without thread_id with an empty list, because a forum has no main thread to bind the token to (R-AZ-26). Step 6 registers and lists the commands and says so. The brief placed the autocomplete in step 6, before any thread exists; the server follows the specification.
D-M2-37-15 2026-10-25 SDK additions (smallest public names, each with a test): AgentHandler.on_commands_reconciled (the commands line after the reconciliation), AgentHandler.on_autocomplete_sent (the suggestion line after the send), RuntimeStatus.reported_billing_mode and RuntimeStatus.billing_evidence (step 5 reports mode and evidence with every heartbeat, not only once). SDK fix: expectation_for compares the command_id of a command-triggered agent.invocation (M2-16 added it; the SDK still refused every model command). The example uses only public SDK names and writes no protocol code; the check run found the refusal.
D-M2-37-16 2026-10-25 A run from a model command loads no context: the example takes the rule from command_name and the options and prints that the run comes from the command. getRunContext answers internal_error for a run whose trigger_kind is not message until commands and interactions define their context (09 4.6, M2-13). The server follows its specification; the gap is reported for the ticket that defines the context of commands.