M2-37: Guide "connect your own agent" with a check run¶
Status: ready | Depends on: M2-22, M2-30, M2-31 | Milestone: M2
Goal¶
After this ticket the documentation site has the developer guide "Connect your own agent" (docs/guides/connect-agent.md, English like the site) with the ten steps of 11 "Guide: connecting your own agent in ten steps", and the guide is executable: one Go test (services/server/test/guide) reads the page, starts a fresh server, sends every request block as the owner or as a person, runs every command block (key generation, the example adapter in sdk/python/examples/guide/ on the Python SDK of M2-29, the example's FakeServer tests), compares every answer and every output line with the text and every shown file with the file in the repository, and fails at the first step that differs from the text. The example adapter connects through the gateway with a consumer, reports its capabilities in the heartbeat, registers a control command and a model command with autocomplete, accepts invocations only after the SDK's token check and its own actual-mode comparison, fetches the context with trust marks, starts runs, fills the live box, answers, finishes with usage, handles a follow-up, a stop, a question, an approval card and (when the base branch already accepts attachments) a file, and survives a restart between acceptance and start. This is exit criterion 7 of milestone M2 ("The guide check run is green") and the proof for the owner's requirements 8 and 21 (PLAN 18.1) that an outside developer connects a generic agent through the public API alone (09 1 point 2, 11 1 point 3, ADR 0022, P-18). Why: a guide that nobody runs drifts away from API, SDK and server unnoticed, and the first developer who follows it hits a broken step; executing the text itself is the only way the text stays true.
Read first¶
docs/PLAN.md: section 3 "Way of working per ticket"; section 5 "Test strategy" (rows "Test doubles", "End to end"); section 8 "M2 Agent platform, extensions and terminal": Goal (P-18), Scope ("a guide for custom agents"), Out of scope ("Files in agent invocations and voice messages (M3)", "user interface (M4)"), Exit criteria 1, 4 and 7, Test strategy, Red team (first paragraph), Risks; block "agents and extensions" with the rows M2-29, M2-30 and M2-31 (what exists), M2-37 (this ticket), M2-09 to M2-20 and M2-22 (the server features the guide exercises); M2-14 (SR9); M3-07 (attachments); M4-28 and M6-04 (the admin screens that later call the same operations); 18.1 rows 8 and 21; section 19 P-09, P-10, P-11, P-18.- Dependencies: the header names M2-22, M2-30 and M2-31. Implied through them and therefore not in the header: M2-29, M2-27, M2-26, M2-24, M2-23, M2-20, M2-19, M2-18, M2-17, M2-16, M2-15, M2-14, M2-13, M2-12, M2-11, M2-10, M2-09, M2-08, M2-07, M2-06, M2-05, M2-04, M2-02, M2-01 and the M1 tickets M1-01 to M1-29, M1-31 to M1-37 (M1-37: setup path, used only as the merged suite of M2-31 uses it, Design note 10), M1-44 to M1-46, M1-48 to M1-58, M1-60 to M1-66, M1-68, M1-69 and M1-71. Not in the closure although touched: M3-07 (attachments, Design note 11).
- M2-29, M2-30 and M2-31: read their briefs (
docs/briefs/M2-29-python-sdk.md,M2-30-typescript-sdk.md,M2-31-agent-conformance-suite.md), their plan rows, the merged code (sdk/pythonwith its README and API reference page,sdk/typescript,services/server/test/agents,sdk/pythonmoduletesting) and their reports. Their merged names win (Boundaries). docs/spec/11-extensions.md: 1 "Principles" (point 3: no shortcut for own extensions); 2 "Agent or app"; 11 "SDK design" (11.1, 11.2 module table, 11.3); 12 "Versioning and compatibility" (compatibility matrix insdk/README.md); 13 "Guide: connecting your own agent in ten steps" (the outline this guide implements; its bold step titles are checked by the test); 16 (sdk.*).docs/spec/09-agents.md: 1 "Principles"; 2.1 "Records" (row "Access"); 2.2 "Features" (agent_operator_config.v1, heartbeatcapabilities); 2.3; 2.4 "Tool class"; 3.2 "Origin (messages.origin)"; 3.3 "Channel switches"; 3.4; 4.1 to 4.10 (connection and cursor, events to the adapter, invocation token with the adapter checks 1 to 10, 4.5.1, context withtrust, the operations table of 4.7, heartbeat, 4.9 "Order in the adapter: commitment first, then runtime", 4.10 "Usage at the end of a run"); 5.1, 5.2, 5.3; 7 "Follow-ups during a run"; 8 "Stop"; 10.2, 10.4, 10.5 (commands and autocomplete); 11.1 to 11.6 (components, approval buttons, questions); 15 "Reliability"; 16.1 "Test doubles" and 16.2 "Required scenarios" (AG-08, AG-09, AG-11, AG-12, AG-28); 19 (R-AG-07, R-AG-24); 21.docs/spec/08-identity.md: 4 "Initial installation"; 6.2 "Step-up flow"; 7.3 "DPoP"; 12 "API tokens for agents, apps and tools" (binding: DPoP required whileagentis inauth.dpop_required_for).docs/spec/10-backend-policy.md: 7 "Adapter check (I3)" (key_type = unknowncounts as a mismatch); 8 "Expected and reported"; 12.1 "Recording".docs/spec/06-api.md: 3.1 "DPoP"; 3.3 "Step-up"; 5 "Errors" (point 3,errors[].codenot_enabled); 7 "Idempotency"; 8 "Concurrency" (ETag,If-Match,412 version_conflict,428 precondition_required); 16 "Endpoints".docs/spec/04-sync-protocol.md(core contract, read only): 15 "Agent adapters" (15.1 to 15.5, 15.7).docs/spec/02-architecture.md: 6 "Code layout" (test/**is the componenttestsupport;identitytest; the list "Places that carry the product name").docs/spec/23-quality.md: 3 "Component contract" (row "Python project"), "Documentation in code" (ruffD, conventiongoogle); 4 "CI" (path selection of components).docs/spec/19-security.md: SEC-LOG-2, SEC-LOG-4, SEC-T10.docs/security/threat-model.md: T-TK-02, T-TK-05, T-AI-01, T-AI-03.docs/RISKS.md: RK-09, RK-18.docs/OPEN-QUESTIONS.md: OQ-17 (no publication of the SDK).docs/api/openapi.yaml: operationscreateSpace,createChannel,createGrant,createAgent,getAgent,createToken,getChannel,setChannelOperator,getChannelBehavior,putChannelBehavior,listChannelCommands,autocompleteCommand,invokeCommand,createThread,createMessage,getMessage,getRun,listThreadRuns,cancelRun,createInteraction(the requests of people) andsendHeartbeat,registerCommand,answerAutocomplete,getRunContext,startRun,resumeRun,renewInvocationToken,patchRun,awaitRunInput,finishRun,respondToInteraction,updateComponent,listServedChannels(the agent side, used through the SDK);createUpload,uploadPart,completeUpload(Design note 11); schemasAgentCreate,Agent,AgentRuntimeStatus,ApiTokenCreate,ApiTokenCreated,ApiToken,GrantCreate,ChannelView,ChannelOperator,OperatorSet,ChannelBehavior,ChannelBehaviorPut,ThreadCreate,ThreadCreated,MessageCreate,Message,Attachment,File,CommandList,CommandRegister,CommandOption,CommandOptionValue,CommandInvoke,CommandInvocation,AutocompleteQuery,AutocompleteResult,CommandChoice,InteractionCreate,Interaction,ComponentCreate,Heartbeat,BillingEvidence,RunStart,RunPatch,RunFinish,CostReport,RunCancel,RunSummary,Run,Problem(x-problem-codes),FieldError.docs/api/asyncapi.yaml: eventsagent.invocation,agent.followup,interaction.created,command.invoked,run.cancel_requested,command.autocomplete_requested, opcommand.autocomplete_result.docs/operations/configuration.md:auth.dpop_required_for,auth.api_token_max_ttl,auth.step_up_max_age,agent.invocation_token_ttl,agent.ack_timeout,agent.presence_timeout,agent.heartbeat_persist_interval,agent.cancel_grace,command.autocomplete_timeout,channel.history_backfill_limit_default,adapter.invocation_token_leeway,adapter.jwks_refresh_min_interval,sdk.http_timeout, theratelimit.*keys.- ADRs 0013, 0022, 0023, 0026, 0038, 0044, 0045, 0053, 0054.
docs/DECISIONS.md: D-W-54, D-W-100, section 10 (row formatD-M2-37-<n>). - Briefs whose interfaces the guide exercises:
docs/briefs/M2-02-channel-behavior-config.md(If-Matchonsettings_version),M2-07-agent-consumers.md,M2-09-run-lifecycle.md(start, finish, cancel, thecostsrule of Design note 6d),M2-10-run-document-live-box.md,M2-11-run-followups.md,M2-13-run-context.md,M2-15-components-interactions.md(cards, approval buttons, resume by interaction),M2-16-slash-commands.md,M2-17-command-autocomplete.md,M2-19-agent-messages.md,M2-20-heartbeat-presence.md(presence ingetAgent),M2-22-costs.md,M2-23-generated-clients.md(layout ofsdk/python, distributionbothy-sdk, import packagebothy_sdk,VERSION); M1 briefsM1-05-server-skeleton.md(testsupport),M1-12-postgres-migrations.md(pgtest),M1-17-api-server-stubs.md(api/operations.yaml),M1-33-identity-devices-dpop.md(identitytest),M1-44-identity-api-tokens.md,M1-48-authz-api.md,M1-51-agents-records.md,M1-53-chat-channels.md(forum channels),M1-56-messaging-send.md(thenot_enabledfields). - Repository files:
mkdocs.yml(nav,pymdownx.highlightrenders thetitleof a fence),tools/docs/mkdocs_markdown_structure.py,tools/docs/mkdocs_repo_links.py,tools/ci/components.pyandtools/ci/tests/test_components.py,sonar-project.properties,sdk/README.md,sdk/python/README.md,docs/api/README.md,docs/quality/gates.md.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. - Limits, windows and thresholds only through named configuration keys listed in
docs/operations/configuration.md; code comments and docs name the key, never the number. - Specification first: a change to behaviour, API, event, right or configuration updates
docs/spec/ordocs/api/in the same change. The core contract (docs/spec/00-glossary.md,03-data-model.md,04-sync-protocol.md,05-actions.md) is not edited by this ticket.
Product requirements implemented here:
- The guide covers the ten steps of 11 13 in that order, each under a heading whose title is the bold title of the step in 11 13 (PLAN row M2-37).
- One automated test runs every step against a fresh server and fails when a step differs from the text: a request or an answer, an output line, a shown file, a step title, the order of the steps or the SDK version pin (PLAN row M2-37, exit criterion 7).
- The example adapter uses only the public API through the Python SDK, never a private channel and never the generated core where the hand layer offers the operation (09 1 point 2, 11 1 point 3, 11 11.2, ADR 0022, ADR 0026); it never calls a model (PLAN 3 point 5).
- The agent token is bound to the adapter's key (DPoP) as
auth.dpop_required_forrequires by default, lives only in a file readable by the adapter, and never appears in any output (08 12, T-TK-02). - The adapter processes an event with a token only after the SDK's checks of 09 4.5 and its own actual-mode comparison (I3, 10 7), acknowledges after storing it in its inbox (09 4.3), starts its runtime only after
200onstartRun(R-AG-07), and hands context with trustunverifiedto the runtime only as marked data (09 4.6, ADR 0054, T-AI-01). - Output and logs of the example contain no message content, no prompt, no token and no secret (SEC-LOG-2, SEC-LOG-4).
- The example passes the scenarios AG-08 (adapter side), AG-09, AG-11, AG-12 and AG-28 of 09 16.2 against the SDK's FakeServer (09 16.1, step 10 of 11 13).
- A restart between acceptance and start resumes the run from the inbox and starts it exactly once (09 4.9, 09 15, RK-18).
Scope¶
Paths are relative to the repository root. This ticket creates no migration and no new top-level make target; the check runs inside make test (component server).
Create:
docs/guides/connect-agent.md: the guide (English), structure and blocks per Design notes 2 to 9.sdk/python/examples/README.md(English: what the examples are, how to run them, that the guide shows them and the test runs them).sdk/python/examples/guide/:keygen.py,agent.py,connect.py,heartbeat.py,commands.py,invocation.py,runtime.py,run.py,interactive.pyandtests/test_agent.py(Design notes 8 and 9). No other file in this directory except an empty package file if one turns out to be required.services/server/test/guide/:doc.go(package doc),parse.go(guide parser),validate.go(static checks),match.go(JSON and output matching),run.go(executor),parse_test.go,validate_test.go,match_test.go,mutation_test.go,guide_test.goandREADME.md(English: what the check proves, prerequisitesuvand a Docker-compatible runtime, how to run it, how to read a failure).
Change:
mkdocs.yml: nav sectionGuideswith the entryConnect your own agent: guides/connect-agent.md, placed afterAPI.sdk/python/pyproject.toml: the examples in lint, type check, tests and coverage (Design note 13).tools/ci/components.pyandtools/ci/tests/test_components.py(Design note 14).docs/spec/11-extensions.md13,docs/spec/09-agents.md2.1,docs/spec/02-architecture.md6 (Design note 15).docs/api/README.md,sdk/README.md,sdk/python/README.md: a link to the guide;docs/quality/gates.mdrowtest(Design note 15).sdk/python/src/bothy_sdk/...: only if the merged hand layer lacks a public function a step needs (Design note 8, last bullet), with tests, docstrings and a CHANGELOG line.services/server/test/harnessor the helpers ofservices/server/test/agents: additive changes only, only if the runner needs a helper they lack (Design note 10).docs/DECISIONS.mdsection 10: rowsD-M2-37-1and following (Design note 16).services/server/CHANGELOG.md,sdk/python/CHANGELOG.mdand the rootCHANGELOG.mdunder "Unreleased".
Out of scope¶
- A runnable TypeScript example: the guide names the TypeScript SDK and links its README and reference (Design note 2); the TypeScript SDK itself is M2-30.
- The admin screens for agents, tokens, grants and channels (M4-28, M6-04); they call the same operations the guide shows.
- Files in the guide while the base branch does not accept attachments (Design note 11); the file service and attachments themselves (M3-03 to M3-07).
- The conformance suite AG-01 to AG-30 against the real server (M2-31), the SDK modules and their own tests (M2-29), the reference adapters (M2-33 to M2-35), load tests (M2-36).
- Publication of the SDK packages and their final names (OQ-17, V1-06); the release chain of the SDK components (M7).
- Webhook and REST polling as transports for the example (09 4.2); the guide uses the gateway as 11 13 step 4 says.
- Changes to server behaviour; a deviation the check run finds in the server is fixed in its module only if it contradicts the specification, and is reported (Design note 1).
- Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M2-37-agent-guide; one writer per worktree. - Conventional Commits with the module or component as scope (for example
docs(guide): ...,test(server): ...,feat(sdk-python): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. Never publish a package and never reserve a name at PyPI or npm (OQ-17). - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests bind only
127.0.0.1and port 0; PostgreSQL runs only as a throwaway container on a random port throughpgtest(P-09, P-10). Without a Docker-compatible runtime the integration tests fail; they never skip. Withoutuvthe check run fails; it never skips. - Stop only processes you started, by PID. Temporary directories only via
mktemp -d, removed at the end. - The runner signals an adapter only through the process group it created for that adapter, and its temporary directories come from
t.TempDir(). - Never read or print secrets. No network calls except package downloads for pinned tools and Go modules; the locked Python packages of
sdk/pythoncount as pinned tools. - Never lower a threshold, disable a gate or delete a test.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100). Where the merged code of an M1 ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>. - The same holds for M2-29, M2-30 and M2-31 and the M2 tickets in their closure: their merged names (SDK modules and functions, the reference page, harness functions, file names) win over the expected names here; list each difference.
- No model call and no third-party service in any test or example: the example's runtime is the rule set of Design note 9. The only server is the fresh server the test starts.
- The check run never writes into the working tree except the ignored
sdk/python/.venvthatuvmanages; every adapter file lives in the temporary adapter directory. - No string in a committed file matches the API token format of 08 12 (
bothy_<kind>_...); the guide shows a token only as the placeholder of Design note 4. - Time only through real waits on events; no fixed sleeps. Every wait ends at the test deadline (
t.Deadline()), never at a number written into the test.
Design notes¶
-
Start check. Run
make checkon the fresh worktree; if it is red, stop withVerdict: BLOCKED base branch redand the failing gate. Confirm that the base branch has M2-29 (the SDK modulesauth,gateway,invocation,runs,components,commands,outbox,heartbeat,testingof 11 11.2 and the API reference page inmkdocs.yml), M2-30 (sdk/typescriptwith README and reference) and M2-31 (services/server/test/agentsrunning the Python fake agent against the real server). Then determine one fact and record it in the report: whetherMessageCreate.attachment_file_idsis enabled (M3-07, Design note 11). When the check run shows that the server differs from the specification, fix it in the owning module only if the specification is unambiguous, with a red test first, and report it; if the fix would touch the core contract, stop withVerdict: BLOCKED <reason>. -
The page. Ruling for docs/DECISIONS.md: the guide lives at
docs/guides/connect-agent.md(English file name like the other pages, English text like the site), in a new nav sectionGuides; it addresses a developer who connects an own adapter (01, role "Bot developer"). Sections in this order:# Connect your own agentwith a short introduction;## Placeholders(the table of Design note 4);## Prerequisites;## Step 1: Create the agentto## Step 10: Test and operate, each title exactly the bold title of the step in 11 13 without its final period;## How the repository checks this guide(three sentences: which test, what it compares, how to run it);## Next(links: 09, 11,sdk/README.mdwith the compatibility matrix, the Python API reference page of M2-29,sdk/typescript/README.md). The guide's code is Python only; TypeScript gets one sentence in step 4 naming the package and linkingsdk/typescript/README.mdand its reference. Ruling for docs/DECISIONS.md: one executable path keeps the check exact; the PLAN row namessdk/python/examples. No Mermaid block (every fence must be executable, note 3); link the sequence diagram of 09 4.9 instead. Every owner step shows the API operation; one sentence says that the admin screens of app and console call the same operations. Ruling for docs/DECISIONS.md: the API is the path a developer can follow today and the one every screen uses. -
Block grammar (the contract between text and test; Ruling for docs/DECISIONS.md). Every fenced block of the page starts at column 0 with three backticks, a language and a
title="..."attribute; an untitled block, a block with an unknown title form and an indented titled block fail the check. Kinds:- Request: language
http, title<Actor>: <operationId>with actorOwner,AlexorSam. First line<METHOD> /api/v1/<path> HTTP/1.1(the HTTP lexer of the site needs the version); then header lines (onlyContent-Type: application/jsonandIf-Match); then a blank line and an optional JSON body. The method and the path must equal the operation of thatoperationIdindocs/api/openapi.yaml, with any placeholder matching any path parameter. There is no actorAgent: the agent acts only through the example program. - Response: language
json, titleResponse <status>, directly after a request block. The status must be equal and the body is an expected subset (note 5). A request without a response block must answer with a2xxstatus. - Command: language
bash, titleShell: <text>. Run withbash -euo pipefail -cin the repository root after substitution; it must exit 0. Its stdout is recorded for a following output block. - Adapter start: language
bash, titleAdapter: start, exactly one command line; starts the adapter as a background process in its own process group. A start while a previous adapter may still run fails the static check, unless that previous start carried--stop-after accept(it stops by itself, note 7 step 10) or anAdapter: stopblock came after it; the runner then waits for the previous process to exit and requires exit code 0 and all of its stdout lines matched. - Adapter stop: language
bash, titleAdapter: stop, body exactly# Ctrl+C in the adapter's terminal. The runner sendsSIGINTto the adapter's process group and requires exit code 0. - Output: language
text, titleOutput. After aShell:block: the complete stdout, line by line. Otherwise: the next stdout lines of the running adapter, consumed in order. - File: language
python, title the repository path of a file undersdk/python/examples/guide/. The block content plus a final newline equals the file byte for byte; every file there (note 8) appears in exactly one block. - Pin: language
toml, titlepyproject.toml (excerpt). It parses as TOML, andproject.dependenciescontains exactly one entry for the SDK,bothy-sdk==<content of sdk/python/VERSION>. The prose says that until the packages are published (OQ-17) the SDK comes from a checkout of the repository.
Fenced blocks stand only in
## Prerequisitesand the ten step sections; a fence in any other section fails the check, so the maintainer section describes the grammar in prose. - Request: language
-
Placeholders, captures and secrets. A placeholder is
{name}withnamematching^[a-z][a-z0-9_]*$, or{secret:<file>}with a file name matching^[a-z0-9][a-z0-9._-]*$. Bound by the runner before the first block, and listed in the table## Placeholders(the check requires that the table's first column holds exactly these names):{server_url}(address of the instance; in the guide's prose an example likehttps://chat.example.com, in the test the public URL of the fresh server),{adapter_dir}(a folder only the adapter may read; in the test at.TempDir()with mode0700),{alex_id},{sam_id}(the people of the preconditions),{new_uuid}(a fresh UUIDv7 at each occurrence, forclient_msg_idandclient_interaction_id),{expires_at}(RFC 3339 in UTC; in the test the start time plus half of the effectiveauth.api_token_max_ttlof the test server). Every other name is captured exactly once, from a response block or an output block, before its first use; a later occurrence compares with the bound value; a use before the capture and a second capture fail the check. In a JSON body or an expected body, a string that is exactly"{name}"stands for the bound JSON value with its type (so"{history_backfill_limit}"becomes a number); everywhere else substitution is textual, and a value substituted into abashblock must match^[A-Za-z0-9._:/@+-]+$and is single-quoted.{secret:<file>}may appear only as a string in a response block: the runner captures the string, writes it to{adapter_dir}/<file>with mode0600, never logs or compares it and redacts it from every error message. -
Matching. JSON: an expected object matches when every expected member exists in the actual object and matches; an expected scalar matches an equal scalar of the same JSON type; an expected array matches when each expected element matches exactly one distinct actual element, order ignored, extra actual elements allowed; an element that matches more than one actual element fails as ambiguous (the guide then adds a distinguishing member). Output: each expected line must equal the next actual stdout line after substitution; an unbound placeholder in a line captures
\S+; a differing line fails at once with step, expected line and actual line; a process that ends before all expected lines arrived fails; a process whose stdout has lines no output block expects fails when it ends. Every failure names the step (Step <n>or the section), the block title and, for JSON, the JSON Pointer of the first difference; nothing after the failing block runs. -
Actors and authentication.
Owneris the first owner of the fresh server (Robin); before everyOwnerrequest the runner performs a fresh step-up with the device key, as the admin screens do before an admin action (08 6.2,auth.step_up_max_age), so no owner block can fail with401 step_up_required.Alexhastriggerandapprove_actionson space Support,Samonlywrite(granted in "Prerequisites"). Each person's requests carry their session token with DPoP and the server nonce and anIdempotency-Keywhere 06 "Idempotency" asks for one, all added by the harness client; the guide says in prose that the client adds authentication, DPoP andIdempotency-Key. A person's request never carries an invocation token. -
Scenario. The blocks per section, in this order; every response block names at least the members listed here, and the output lines follow the format
Step <n>: <text>of note 8. The exact English texts are the build session's, but each line below appears.- Prerequisites:
Owner: createSpace(nameSupport, captures{space_id});Owner: createChannel(kindforum, nameTickets, captures{channel_id});Owner: createGrantfor Alex (subject_kinduser, targetspace, leveltrigger, capabilitiesapprove_actions);Owner: createGrantfor Sam (levelwrite);Owner: getDiscovery(GET /api/v1/instance/discovery HTTP/1.1) withResponse 200capturinginstance_idas{instance_id}. Prose: an instance with an owner (link../operations/install-compose.md) and the two people; the owner hands the instance ID to the developer, who configures it in the adapter (step 4). - Step 1:
Owner: createAgentwith handlelotse, display nameLotse,adapter_kindcustomand a description;Response 201withid{agent_id},billing_modeorganization,tool_classchat. Prose: the backend policy starts asorganizationwith tool classchat;personalneedssetAgentBillingModewith a holder and is bound by I1 (link 10). - Step 2: file
keygen.py;Shell:running it with--adapter-dir {adapter_dir};Outputdpop_jkt: {dpop_jkt};Owner: createTokenon/api/v1/principals/{agent_id}/tokenswithname,dpop_jkt{dpop_jkt}andexpires_at{expires_at};Response 201withsecret{secret:agent.token}andtokenholdingdpop_jkt{dpop_jkt}. Prose: shown once, stored only in that file, DPoP binding required whileagentis inauth.dpop_required_for(08 12);uv runinstalls the locked SDK on first use, step 4 explains the installation. - Step 3:
Owner: createGrantfor the agent on the channel with levelwriteand capabilitiesupload;Owner: getChannelwithResponse 200capturingchannel.versionas{channel_version};Owner: setChannelOperatorwithIf-Match: "{channel_version}",modeagent,agent_id{agent_id}and a reason,Response 200withoperator.agent_id{agent_id};Owner: getChannelBehaviorcapturingsettings_versionandhistory_backfill_limit;Owner: putChannelBehaviorwithIf-Match: "{settings_version}"and the full body (thread_require_mentionfalse,history_backfill_limit"{history_backfill_limit}", a short English prompt,skillsempty, the other switches at their standard of 09 3.3),Response 200withthread_require_mentionfalse. Prose: the operator haswritewithout a grant (09 2.3); the grant addsuploadfor files;If-Matchper 06 8. - Step 4:
Shell:uv sync --project sdk/python --locked; filesagent.pyandconnect.py; one sentence on TypeScript (note 2);Adapter: startwithuv run --project sdk/python --locked python sdk/python/examples/guide/agent.py --server-url {server_url} --instance-id {instance_id} --agent-id {agent_id} --adapter-dir {adapter_dir} --consumer default;Outputwith the line that the adapter is connected aslotsewith consumerdefault. Prose: the developer takes the instance ID and the agent ID once from the owner and never reads them from the server at start, because the SDK checksissandaudof every invocation token against them (09 4.5 point 4). - Step 5: file
heartbeat.py;Outputwith the heartbeat line (stateonline, modeorganization, tool classchat); prose: the SDK sends the periodic heartbeats over the gateway, the example sends the first report once withsendHeartbeatand prints the server's answer (note 8);Owner: getAgentwithResponse 200holdingpresence.stateonlineandpresence.reported_tool_classchat. - Step 6: file
commands.py;Outputnaming both commands;Alex: listChannelCommandswithResponse 200capturing{summarize_command_id}(namesummarize,controlfalse) and{version_command_id}(nameversion,controltrue) fromitems;Alex: autocompleteCommandforsummarizewith optiontopicand a partial value;Response 200with one choice of the example's fixed list;Outputwith the line that suggestions were sent. - Step 7: file
invocation.py;Sam: createThread(a forum post withtitleandfirst_message, capturing{thread_id}; Sam has notrigger, so nothing runs);Alex: createMessagein{thread_id}asking Lotse for help;Outputwith the acceptance line capturing{run_id}and naming the checked token and the actual mode, then the context line listing the trust marks in thread order (Sam's messageunverified, Alex'strigger). - Step 8: files
runtime.pyandrun.py;Outputwith the lines started{run_id}, live box updated, answer sent, finished{run_id}withcompletedand usage reported;Alex: getRunwithResponse 200holdingstatecompleted,billing_mode_usedorganizationand the example's summary; then the model command:Alex: invokeCommandfor{summarize_command_id}withthread_id{thread_id}and optiontopic,Response 202withacceptedtrueandrun_id{command_run_id};Outputwith that run's lines;Alex: getRunholdingtrigger_kindcommandandstatecompleted. - Step 9: file
interactive.py; four parts, each starting with anAlex: createThreadpost whose text triggers one rule of note 9 and captures its own thread id. Follow-up: the run waits for a follow-up;Alex: createMessagein that thread;Outputwith follow-up absorbed and run completed. Question: the run posts a question card and waits (awaitRunInput); Alex answers with a message (09 7: a follow-up to a waiting run is the answer);Outputwith the answer received and the run resumed and completed. Approval: the run posts the approval card of 09 11.5 and prints the component id of "Allow once" as{once_component_id};Alex: createInteractionon/api/v1/components/{once_component_id}/interactionswithclient_interaction_id{new_uuid},Response 201;Outputwith the decisiononce, resumed, completed. Stop: the run keeps running until stopped;Alex: invokeCommandfor{version_command_id}in that thread,Response 202,Outputwith the answered control command while the run continues (09 10.4);Alex: cancelRunwithResponse 202;Outputwith the run stopped (user_cancelled);Alex: getRunwithstatecancelledandstate_reasonuser_cancelled. Files: note 11. - Step 10: file
tests/test_agent.py;Shell:uv run --project sdk/python --locked pytest sdk/python/examples/guide/tests(with--no-covonly if the project's pytest configuration measures coverage on every run, because a partial run cannot reach the threshold); prose on logs without content and secrets (proved by the runner's final checks, note 10); restart:Adapter: stopwith itsOutput;Adapter: startwith the command line of step 4 plus--stop-after accept;Outputwith the start lines of steps 4 to 6;Alex: createThreadcapturing{restart_thread_id};Outputwith the acceptance line capturing{restart_run_id}and the line that the adapter stopped after the acceptance (the process then exits 0 by itself);Adapter: startwith the command line of step 4 without the flag;Outputwith the start lines, the context line, started and finished{restart_run_id};Alex: listThreadRunsfor{restart_thread_id}holding{restart_run_id}withstatecompleted; versions: the pin block and one sentence on the compatibility matrix ofsdk/README.md; finallyAdapter: stopwith itsOutput, so the page ends with no running process.
- Prerequisites:
-
Example program (
sdk/python/examples/guide/, run as scripts from the repository root, sibling modules imported by name). Ruling for docs/DECISIONS.md: one program in small step modules, each shown whole in its step.keygen.py:--adapter-dir; creates the adapter's DPoP key with the SDK'sauthmodule, writes it asadapter.keywith mode0600(refuses an existing file), prints exactlydpop_jkt: <RFC 7638 thumbprint>.agent.py: flags--server-url,--instance-id,--agent-id,--adapter-dir,--consumer(defaultdefault),--stop-after(only valueaccept);--instance-idand--agent-idare required and are passed toAdapterConfig, so the SDK compares them as M2-29 Design note 8 says (Discovery.instance_idat start,issandaudof every invocation token) and checksready.principal.idagainst the agent ID (M2-29 Design note 6); refuses to start whenagent.tokenoradapter.keyis readable by group or others; keeps the SDK's inbox, outbox andjtistore in{adapter_dir}/state; wires the step modules; onSIGINTends cleanly and prints the stop line. Output order fixed per process: connect (4), heartbeat (5), commands (6), then events (inbox first). The SDK sends its first gateway heartbeat after a random delay and reconciles the commands right afterready(M2-29 Design notes 6 and 12), and the server sendsheartbeat_ackbefore it applies the memberagentand reports an error of that part later as anerrorframe (M2-20 Design note 10), so no gateway heartbeat confirms the report. Afterreadythe example therefore sends one heartbeat itself withsendHeartbeatthrough the SDK'sClient.call(body fromheartbeat.py) and prints the step 5 line on the200, fromHeartbeatAck.presence. It prints the step 6 line only after that line and after the SDK reports the command reconciliation done. Itsprepare()waits until both lines are printed, so inbox entries start only after them. The SDK's periodic gateway heartbeats and a reconciliation after a reconnect print nothing.connect.py(gateway with consumer, reconnect andackby the SDK),heartbeat.py(stateonline,reported_billing_modeorganization,billing_modes_supported[organization],billing_evidencewithkey_typenoneand no value,reported_tool_classchat,capabilitieswithfollowupssteerandcomponentstrue, andfiles_outonly in the file branch of note 11),commands.py(declarative list:summarizeas model command with optiontopicof typestringandautocompletetrue;versionas control command; registered for every channel fromlistServedChannels; autocomplete answers from a fixed list without any model call, 09 10.5 point 5),invocation.py(token check by the SDK, actual mode and tool class compared with the claims,billing_mode_mismatchortool_class_mismatchreported throughfinishRunwithout starting, context fetched and rendered with the SDK's function for marked data blocks, ADR 0054),run.py(start with the newest token, live-box patches through the SDK'srunshelpers with a status line, one step, a text excerpt and metrics on paths allowed by 09 5.3, the answer through theoutboxwithrun_id,finishRunwithsummaryandcostsholding oneCostReportwithproviderandmodelnaming the example runtime,currencyEUR, token counts and noamount_micros, so the server books it as M2-22 decides for an unknown price),interactive.py(follow-up statesdeliveredthenabsorbed, stop onrun.cancel_requestedwithfinishRuncancelledanduser_cancelled, question card and approval card withrun_idandawaitRunInput, resume with the token of the follow-up or interaction,respondToInteractionandupdateComponentshowing the decision, the control command answered as a message of the agent).- Output: one line per confirmed action on stdout, flushed, format
Step <n>: <text>where<n>is the step of the guide that explains the action (connect 4, the example's own heartbeat 5, commands and suggestions 6, acceptance and context 7, start, live box, answer and end 8, follow-up, question, approval, stop and control command 9, the stop onSIGINTand the stop after acceptance 10), printed only after the server confirmed the action (2xxfor a REST call; for a gateway op such ascommand.autocomplete_result, after the SDK reports it sent); never message content, prompt, token, key, display name or principal ID of a person; run, thread and component IDs are allowed. SDK logs go to stderr under the same rule. - Only public names of the merged SDK. If the hand layer lacks a public function a step needs (expected candidates: creating and storing a DPoP key with its thumbprint; a hook between acceptance and start for
--stop-after accept; a signal that the command reconciliation afterreadyis done), add the smallest public function to the hand-layer module of 11 11.2 it belongs to, with a docstring, a test and a CHANGELOG line, and list it in the report; never write cryptography or protocol code in the example.
-
Example runtime (
runtime.py). Ruling for docs/DECISIONS.md: fixed rules on the trigger text replace a model, so the guide costs nothing and its output is deterministic. Case-insensitive, first match wins: containsfollow-up: wait for a follow-up, then answer; containsquestion: ask with a card of two buttons and wait; containsapproval: ask with the approval card and wait; containslong: keep running until stopped; containsfile(only in the file branch): answer with a text file; a model commandsummarize: answer with a short summary naming the option value; anything else: a short fixed answer. The runtime never reads or echoes the prompt. -
Runner (
services/server/test/guide, packageguide, componenttestsupport; Go, because the owner and the people need the device-key and DPoP clients the Go test support offers).- Fresh server: reuse the harness the merged M2-31 suite uses for the real server (expected:
services/server/test/harnessof M1-71 withStart, the owner client and people with device keys and DPoP; otherwise the helpers ofservices/server/test/agentsonidentitytest). New PostgreSQL database throughpgtest, migrations asbothy migrate up --bootstrapruns them, the realinternal/wiregraph with roleall, listeners on127.0.0.1:0, first owner through the setup path of M1-37 (in the closure). Configuration: onlyratelimit.*raised (R-QA-11); no other key differs from the standard. Never build a second harness; add a missing helper there additively. - People: Alex and Sam created through the harness before the first block; their IDs bind
{alex_id}and{sam_id}. - Repository root: the directory above the package that holds
mkdocs.yml; the page is read fromdocs/guides/connect-agent.md, the step titles from 11 13 ofdocs/spec/11-extensions.md, the operations fromdocs/api/openapi.yaml, the version fromsdk/python/VERSION. - Processes: environment of the test without every variable that starts with
BOTHY_; working directory the repository root; stdout and stderr read line by line into buffers; each adapter in its own process group; at the end of the test, or on failure, any process still running getsSIGKILLon its own group. - Final checks after the last block: every step section had at least one executed block; every adapter process exited 0 with all stdout lines matched; no
{secret:...}value, no string matching a compact JWS (three base64url parts joined by dots, the first starting witheyJ) and nocontent_mdvalue of any request block of the page appears in any stdout or stderr of the adapters or theShell:blocks; the run{restart_run_id}has exactly one eventrun.state_changedwith staterunning(read through the harness's administrative database access, assertions only) and its thread has exactly one run. - Logging: the runner logs block by block (
t.Logf) with step and title, never a secret, never a request body with a secret.
- Fresh server: reuse the harness the merged M2-31 suite uses for the real server (expected:
-
Files (09 4.7 row "Send file", 11 13 step 9). If
MessageCreate.attachment_file_idsis enabled on the base branch (M3-07 merged), step 9 gets a fifth part: Alex posts a thread with the wordfile, the example uploads a small text file it creates in memory throughcreateUpload,uploadPartandcompleteUpload(from the generated core; the hand layer has no file module) and answers withattachment_file_ids, the heartbeat reportsfiles_outtrue, the output line captures{file_message_id}, andAlex: getMessageholdsattachmentswithfile.original_nameof that file; use the file setup the merged M3-07 tests use for the test server. Otherwise step 9 says in prose that this server version accepts no attachments yet and proves it:Alex: createMessagewithattachment_file_ids["{new_uuid}"],Response 400holdingcodevalidation_failedanderrorswithpointer/attachment_file_idsandcodenot_enabled. Ruling for docs/DECISIONS.md: the text states only what the server does, and the check fails as soon as attachments are enabled, so the ticket enabling them must extend the step (plan gap reported). -
Costs and SR9. Step 8 finishes with
costs(11 13 "run.finishwith usage"); M2-22 books them. Step 8 mentions the binding of a run to the rights of its trigger (SR9, the SDK sends the token along); M2-14, which builds it, is in the closure through M2-29. -
Lint, types, coverage. The examples are hand-written code of the component
sdk-python: SPDX header# SPDX-License-Identifier: Apache-2.0as first line, ruff withD(conventiongoogle, tests excluded) and the formatter, mypy with the project's strict configuration (addexamples/guideto its paths), bandit.sdk/python/pyproject.toml: pytest collectsexamples/guide/testsand putsexamples/guideon itspythonpath; coverage measuresexamples/guidenext to the SDK package, sosdk/python/coverage.xmlcovers the code Sonar sees undersdk/**; the thresholdquality.coverage.python_min_percent(added to 23 11 by M2-23) applies. The test file sits undertests/, whichsonar.test.inclusionsalready marks as test code. Ruling for docs/DECISIONS.md: examples follow every rule of hand-written code. -
CI path selection (23 4). Ruling for docs/DECISIONS.md: in
GATE_COMPONENTSoftools/ci/components.pythe prefixesdocs/guides/connect-agent.mdanddocs/spec/11-extensions.mdselectserverandsdk-python, andsdk/python/examples/additionally selectsserver, because the check run is a server test that reads these files; otherwise a change of the text or the example would skip the test that holds them. -
Specification and documentation text (English like each file). 11 13: one sentence after the introduction that the guide is
docs/guides/connect-agent.mdand runs as a test against a fresh server that fails when a step differs from the text; step 1 "The owner creates the agent (administration in the app or console or operationcreateAgent; ...)"; step 2 "bound to an adapter key (DPoP), required whileagentis inauth.dpop_required_for(default, 08 section 12)" instead of "recommended". Keep the bold step titles. 09 2.1 row "Access":dpop_jktrequired whileagentis inauth.dpop_required_for(default) instead of "recommended". 02 6:guide/in the list oftest/.docs/quality/gates.mdrowtest: the check run of the guide runs in the server's tests and needsuv.docs/api/README.md,sdk/README.md,sdk/python/README.md: one link each to the guide. The## How the repository checks this guidesection andservices/server/test/guide/README.mddescribe the grammar of note 3 for maintainers. -
Rulings for docs/DECISIONS.md, one row each
D-M2-37-<n>with date, decision, reason and place: page, language and nav (note 2); Python as the only executable path (note 2); owner steps through the API (note 2); the block grammar (note 3); placeholders and secret captures (note 4); the matching rules (note 5); actors and a fresh step-up before every owner request (note 6); the example's layout and output format (note 8); the rule-based runtime (note 9); the runner in Go with the shared harness (note 10); the files branch and its check (note 11); the scope of lint, types and coverage for examples (note 13); the CI path selection (note 14).
Tests (write them first)¶
Go unit tests (services/server/test/guide, no server; small synthetic pages in the test code plus mutated copies of the real page):
TestParse_StepHeadingsFollowChapter11: ten headings## Step 1:to## Step 10:with the bold titles of 11 13 pass; a renamed title, a missing step, a duplicated step, two swapped steps and an eleventh step each fail with the step number.TestParse_BlockKinds: one block of every kind of note 3 is recognised with its fields; an untitled fence, a fence with an unknown title form, an indented titled fence, a fence in## Next, anAgent:request, anAdapter: stopblock with another body and a secondAdapter: startwhile the first adapter may still run each fail with line number and reason.TestValidate_FileBlocksEqualExamples: equal content passes; one changed byte fails naming the file and the first differing line; a file ofsdk/python/examples/guide/not shown, a file shown twice and a title naming a missing file each fail.TestValidate_RequestLinesMatchOpenAPI:POST /api/v1/agents HTTP/1.1forcreateAgentpasses; an unknownoperationId, another method, another path and a missingHTTP/1.1each fail.TestValidate_Placeholders: use before capture, a second capture of the same name, an unknown placeholder, a{secret:...}outside a response block, and a placeholder table that lacks or adds a runner-bound name each fail.TestValidate_PinMatchesSDKVersion: the pin equal tosdk/python/VERSIONpasses; another version, a second SDK entry and invalid TOML fail.TestMatchJSON: equal scalars; missing member (pointer in the error); same value with another JSON type; nested objects; arrays with one unique match, no match and an ambiguous match; captures keep their JSON type; a bound placeholder compares.TestMatchLines: exact lines; capture of\S+; a differing line fails at once with expected and actual line; a process that ends early fails; extra unexpected lines fail at the end.TestSecretsNeverInErrors: a mismatch in a response block that captured{secret:agent.token}produces an error and a log without the secret value; the secret file has mode0600.TestMutations_Static: the real page with one mutation each (a changed line in a shown file, steps 5 and 6 swapped, step 9 removed, an unknownoperationId, a wrong path, an unbound placeholder, an untitled block, a changed pin) fails before any server starts, naming the step.
Go integration tests (PostgreSQL 18 via pgtest, the fresh server of note 10, uv and the locked sdk/python environment):
TestGuide(exit criterion 7): runs the whole page and the final checks of note 10; the log lists each step with its executed blocks.TestGuide_DeviationFailsAtStep1: the page with"billing_mode": "organization"in step 1's response block changed topersonalfails atStep 1, blockResponse 201, pointer/billing_mode; no block after it runs and no adapter process was started.
Python tests (sdk/python/examples/guide/tests/test_agent.py, pytest, the SDK's FakeServer on 127.0.0.1 port 0, shown in step 10):
test_ag08_invalid_token_starts_nothing(AG-08, adapter side): a missing token, a wrong signature,algother thanEdDSA, a foreignaudand anothertypeach start no runtime and end the run withfinishRunfailedandinvalid_invocation_token.test_ag09_double_delivery_starts_once(AG-09, adapter side): the same invocation delivered twice with different tokens gives exactly onestartRun, with the newer token (09 4.3 "Duplicate detection").test_ag11_billing_mode_mismatch_starts_no_runtime(AG-11): a token withbilling_modepersonalstarts no runtime and ends the runfailedwithbilling_mode_mismatch.test_ag12_patch_conflict_is_rebased(AG-12): a409 base_version_conflictfrom the FakeServer makes the example rebase on the returned document and patch again.test_ag28_expired_token_is_renewed(AG-28): an expired token is renewed throughrenewInvocationTokenbefore the start; the run starts once.test_runtime_rules: each rule of note 9 and the order of the rules.test_keygen_writes_private_key_file: mode0600, a thumbprint of 43 base64url characters, an existing file is not overwritten.test_refuses_readable_secret_files:agent.tokenoradapter.keywith group or other read rights stops the start with a message naming the file, not its content.test_output_has_no_content_or_secret: a FakeServer run with a canary in every message text and in the token; stdout and stderr contain neither.test_start_lines_order: against the FakeServer with a FakeClock, the start lines come in the order 4, 5, 6 for several seeds of the SDK's jitter, including one whose first gateway heartbeat comes before the command reconciliation ends; further gateway heartbeats and a reconnect with a newreadyadd no line.
Repository tools (tools/ci/tests/test_components.py): docs/guides/connect-agent.md and docs/spec/11-extensions.md select sdk-python and server; sdk/python/examples/guide/agent.py selects sdk-python and server; docs/spec/09-agents.md still selects nothing.
Red team focus (PLAN 8 "Red team": forging or reusing a token, faking the actual mode) is covered by test_ag08_invalid_token_starts_nothing, test_ag11_billing_mode_mismatch_starts_no_runtime, test_output_has_no_content_or_secret and the final checks of TestGuide; name them in the red team request, together with the question whether the guide teaches any insecure pattern (token in a command line, a world-readable key, logging content).
Commands that must pass, from the repository root:
cd services/server && go test -race -count=1 ./test/guide/...cd sdk/python && uv run --locked pytest examples/guide/tests(with--no-covunder the condition of Design note 7, step 10)cd tools && uv run --locked --quiet pytest ci/tests/test_components.pymake lint,make typecheck,make coverage,make licenses,make docs,make linkcheckmake check
Acceptance criteria¶
-
docs/guides/connect-agent.mdis in the site's nav underGuides, has the ten steps of 11 13 with their titles in order, and every block on it is executed or compared. -
TestGuideruns every step against a fresh server and is green (exit criterion 7); every adapter process ends with exit code 0 and every output line is matched. - A deviation between text and reality fails the check at that step: proven by
TestGuide_DeviationFailsAtStep1against the real server and byTestMutations_Static,TestMatchJSONandTestMatchLinesfor files, order, titles, placeholders, requests, answers and output lines. - The example adapter in
sdk/python/examples/guide/uses only public SDK names, never calls a model, binds its token to its DPoP key, starts runs only after the checks of 09 4.5 and I3, and resumes a run after a restart from its inbox exactly once. - No token, key, secret, prompt or message content appears in any output of the check run.
- The example passes AG-08 (adapter side), AG-09, AG-11, AG-12 and AG-28 against the FakeServer.
- Step 9 holds either the executable file part or the check that attachments are still
not_enabled, matching the base branch (Design note 11). - 11 13, 09 2.1 and 02 6 are updated as in Design note 15; the CI path selection runs the check for changes of the guide, chapter 11 and the examples.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why.
- READMEs of
services/server/test/guideandsdk/python/exampleswritten;sdk/README.md,sdk/python/README.md,docs/api/README.mdanddocs/quality/gates.mdupdated; the changes to 11, 09 and 02 made in the same change. - Rulings of Design note 16 recorded in
docs/DECISIONS.mdsection 10 (one row each with reason). services/server/CHANGELOG.md,sdk/python/CHANGELOG.mdand rootCHANGELOG.mdentries under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check, and the duration ofTestGuide. - Review and red team verdicts and the findings fixed.
- What is not proven.
- Follow-ups that could not be done in this scope, each with its reason, and spec gaps found; include the fact of Design note 1 (attachments), the branch of Design note 11 that was built, every name that differs from this brief, and every public function added to the SDK.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.