Skip to content

Discord operator profile

reach.discord.operator@1 is an explicit Linux development profile for a small server: a credential-bearing Discord edge, a networkless native documents worker, and a local operator tool for reviewed server setup. It does not activate reach.discord@1 or any public deployment profile. Document search is the default; an explicit private profile can instead select bounded local-model conversation and, separately, catalogue research. Independently enabled owner publications use a local queue. These options supply no Ward enrollment, durable Phylactery, or restart continuation.

The two processes have different dependencies and custody. The edge runs discord.py 2.7.1 on Python 3.14.8 with its own lockfile. The worker runs Python 3.15.0 in the explicit OCI development image and executes a real native Graph with a generation-local memory Run ledger. A host supervisor relays bounded JSON over their standard streams; neither container receives an engine socket, checkout, HOME or the other's credentials.

For document search, the worker receives a compiled policy and an immutable export of selected public documents, then returns deterministic excerpts and source links. The optional conversation profile adds one frozen static persona and model binding to that worker. Its model HTTP remains in a closed host transport.

The existing generic Reach policy selects people, groups, conversations and actions. Discord administrator permission does not grant a native Sigil, shell or host operation. Administrative writes are separate local operator commands; Discord chat cannot approve a setup plan.

Server setup uses a fresh snapshot and an exact, expiring plan. It can create bounded roles, categories and channels, and patch explicitly named existing channel ids. It does not delete, ban, assign memberships, change the everyone role or grant administrator. A private operations category and read-only orientation channels belong to the proposed developer layout.

SDK connection management and command dispatch remain at the edge. Administrative/message writes use a single-attempt REST adapter because the pinned SDK retries some POST failures. The edge retains content-free, fsynced attempt metadata. An unknown result fences repetition; the journal witnesses transport effects and is not a canonical Run or conversation ledger. An uncertain create is inspected, never blindly retried or automatically deleted.

Each launch creates a new generation. Old events are refused; process failure stops both peers. The operator effect journal survives, while memory Runs and conversations do not. This is an explicit development limitation, not Phylactery recovery or production hosting acceptance.

Prepare the server and private workspace

Start with Prepare Reachy: create a private application, install its bot with bot and applications.commands, copy its stable IDs, and validate the policy. That tutorial ends with the bot offline. The steps here deliberately continue into this separate operator profile. The public Discord contract and the Designed home and VPS profiles keep their existing boundaries.

Use a Linux host with the repository's host tooling installed as described in CONTRIBUTING and a working rootless Podman engine. Docker is an explicit alternative supported by the harness; its build requirements remain those in CONTRIBUTING. Run the following commands from the repository root, in one terminal. They run the host wrapper; the native documents worker itself runs only inside its OCI image.

Choose a new absolute directory outside every Git checkout. Its existing parent must be a real directory owned by you. Replace the example path and synthetic USER_ID below before continuing. USER_ID must be the destination server's actual owner for server planning; it is also used as the optional inspection allowlist entry at startup.

REACH_ROOT="/absolute/private/path/reach-operator-01"
USER_ID="555555555555555555"
ENGINE="podman"
umask 077
mkdir -m 700 "$REACH_ROOT"
cp -n examples/reach/policy.toml "$REACH_ROOT/policy.toml"

Keep this terminal's variables for the later commands. Set ENGINE="docker" before building if you intentionally use Docker. The harness rejects relative paths, symlink ancestors and paths inside a Git checkout, including ignored directories. It requires private owner-only state folders and token/image/plan files. Token, image record and plan outputs must not exist already; use a new filename for each new artifact. An exported corpus needs a new or empty directory.

Open $REACH_ROOT/policy.toml with your local text editor. Replace every synthetic application, bot, server, channel, user and role ID using the table in preparation. For a first launch, keep one existing text channel in allowed_channel_ids, allow your own user ID through the first rule, and keep context_message_limit = 0. Remove the example developers rule until you have recorded a real role ID, or replace it with the actual intended role ID. The first matching user-or-role rule owns the permitted actions; a later rule cannot supply an action missing from that first match. Keep answer_docs enabled for the user who will try the bot and inspect it.

The server plan below has a fixed developer layout. Its Reach channel is exactly the first ID in allowed_channel_ids: it preserves that channel's ID and may rename it to the-reach, move it into DEVELOPMENT, and update its topic. If it is currently named general, the plan renames it before creating the separate community general channel. Verify that this is the intended existing text channel before planning. Other policy channels are not added to the server layout by this CLI.

For setup, grant the bot Manage Channels and Manage Roles in the destination server and check its role hierarchy in Discord. The planner requires those existing permissions or an already present Administrator permission; it never grants Administrator itself. Prefer the two specific permissions. The bot still needs access to the intended channel for ordinary operation. Creating Core, Developer or Contributor does not assign those roles to anyone; an authorized human manages membership in Discord and records any resulting rule role IDs in the private policy. Discord server authority does not grant native or host authority.

Capture the bot token locally

On your application's Bot page in the Developer Portal, obtain the bot token yourself. Keep it out of chat, screenshots, tracked files, command history and environment variables. This is the credential step omitted by offline preparation. Run:

uv run --locked python scripts/reach_operator.py token \
  --file "$REACH_ROOT/discord-token"

The command prompts through getpass in your local terminal and writes a private 0600 file. Paste the token only at that hidden prompt. The token value is never a CLI argument; later --token-file arguments contain only the file's path. No ordinary Discord user token is used. If the token file already exists, the command refuses to overwrite it. A replacement credential needs a deliberately selected new file, followed by explicit use of that file at startup.

Export public documents and build both images

Export the committed public documents selected by examples/reach/public-docs.txt. The maintained selection names 19 public pages, including the Lich's identity, Transcendence and Inner Tongue owners alongside setup and extension material:

uv run --locked python scripts/reach_operator.py export-corpus \
  --revision HEAD \
  --output "$REACH_ROOT/corpus-01"

The result reports the resolved commit. Each exported file has a manifest hash and a source link pinned to that commit. Dirty working-tree changes and private local material do not enter this export. The worker verifies the manifest and file bytes. To change the documents, export a new committed revision into a new corpus directory and select it on a later explicit launch. Do not edit the exported files in place.

Build the paired development images and retain their exact image identities:

uv run --locked python scripts/reach_operator.py build \
  --engine "$ENGINE" \
  --images "$REACH_ROOT/images-01.json"

This creates the Python 3.15.0 native check image and the separately locked Python 3.14.8 Discord edge image. The image record contains their sha256: identities; startup selects these exact local images with pulling disabled. A failed build or unavailable engine stops the step. There is no host worker fallback. This build command does not run the bot or apply server changes.

The edge's credential-bearing process talks to Discord. The native worker has --network=none and receives the compiled shared policy and corpus read-only. The edge never receives the corpus or Core runtime; the worker never receives the token or constructs a Discord client. The development check image includes the SDK as a test dependency. Both containers have a read-only root, bounded resources, and a private temporary filesystem. Neither receives the checkout, host HOME, an engine socket, provider credentials or published ports. The host wrapper owns engine calls and relays bounded JSON between their standard streams.

Review an exact server plan

Stop any running instance before planning or applying against the same state directory. The harness holds one exclusive instance lock across each operation. Planning contacts Discord to verify the application, bot, server owner and current server snapshot. It writes a private plan; it does not create or patch Discord resources.

uv run --locked python scripts/reach_operator.py admin-plan \
  --engine "$ENGINE" \
  --policy "$REACH_ROOT/policy.toml" \
  --token-file "$REACH_ROOT/discord-token" \
  --state-dir "$REACH_ROOT/state" \
  --images "$REACH_ROOT/images-01.json" \
  --owner-id "$USER_ID" \
  --output "$REACH_ROOT/server-plan-01.json"

The terminal reports the plan's digest and path. Open the plan JSON locally and review its layout, snapshot, existing, operations, and expiry. In particular, confirm all application, bot, guild and owner IDs, the existing Reach channel ID, and every planned patch. The CLI offers this fixed layout rather than an arbitrary layout-file option:

Resource Intended layout
Roles Core, Developer, Contributor, each created with zero permission bits.
START Text channels start-here and announcements; everyone may view/read history, while sending is denied to everyone and allowed to Core.
DEVELOPMENT Text channels dev, help, and the existing channel selected as the-reach.
COMMUNITY Text channel general; voice channels Coworking and Meeting.
OPERATIONS Text channel ops; category and channel deny everyone view access and explicitly allow the named owner, Core, and the bot to view, send and read history.

Existing Discord Administrator access still follows Discord's own permission semantics. The private operations overwrites do not revoke it. The setup does not change the everyone role, assign members, moderate, ban, delete channels or delete roles. Creating voice channels does not provide a voice connection, transcription or other media feature.

A matching name lets the planner retain a resource without writing only when its desired fields and permission overwrites already match exactly. Duplicate names, incompatible role permissions or incompatible channel settings refuse planning. The first policy channel is different because its explicit ID allows the reviewed plan to patch that exact resource. The planner cannot rename or reconfigure arbitrary name-matched channels. Keep the existing channel ID in policy after setup, even if the plan changes its name or category.

A plan expires after fifteen minutes. Copy the digest you actually reviewed into PLAN_DIGEST; the placeholder below must be replaced with that exact 64-character lowercase hexadecimal value. Applying is the explicit authorization for the reviewed Discord writes:

PLAN_DIGEST="REPLACE_WITH_THE_REVIEWED_PLAN_DIGEST"
uv run --locked python scripts/reach_operator.py admin-apply \
  --engine "$ENGINE" \
  --policy "$REACH_ROOT/policy.toml" \
  --token-file "$REACH_ROOT/discord-token" \
  --state-dir "$REACH_ROOT/state" \
  --images "$REACH_ROOT/images-01.json" \
  --plan "$REACH_ROOT/server-plan-01.json" \
  --expect-digest "$PLAN_DIGEST"

The digest covers the exact plan, including the original snapshot. Apply obtains a fresh snapshot and refuses if it has changed, the plan expired or the reviewed digest differs. It checks expiry before each write, checks each applied effect against the observed server, and stops on expiry, rejection or uncertainty while retaining earlier completed results. A stopped apply can leave earlier operations in place; it is not a transaction or a rollback. If a plan simply expired or became stale before any write, create a new plan at a new output path and review its new digest. If an effect may already have occurred, follow the uncertainty procedure below first.

Start Reachy and try its commands

On the first launch, explicitly register commands for this policy's guild and allow your admitted user to run the read-only inspection command:

uv run --locked python scripts/reach_operator.py run \
  --engine "$ENGINE" \
  --policy "$REACH_ROOT/policy.toml" \
  --token-file "$REACH_ROOT/discord-token" \
  --state-dir "$REACH_ROOT/state" \
  --images "$REACH_ROOT/images-01.json" \
  --corpus-dir "$REACH_ROOT/corpus-01" \
  --sync-commands \
  --admin-user "$USER_ID"

Keep this process in the foreground. It verifies the logged-in bot and application, guild membership and all policy channel IDs before connecting. --sync-commands performs guild-scoped registration only. It never performs a global command sync. On later launches with unchanged commands, omit that flag; repeat it deliberately when guild registration needs updating. --admin-user may be repeated for additional inspection users, and each must still pass the same Reach admission policy. It grants no setup apply command and no host permission.

In an allowed channel, use an admitted account to try:

  • /reach status for this connection's readiness.
  • /reach ask with a documentation question, or a direct bot mention followed by the question.
  • /reach admin-inspect for counts of server roles and channels, if your user ID was included with --admin-user and your policy permits answer_docs.

Slash responses are ephemeral. A mention answer is sent to its allowed channel with mentions disabled. Replies and owner publications suppress automatic link previews; links remain clickable. An unlisted person, channel, trigger or action is refused or ignored. The edge and native worker both evaluate the same compiled policy; Discord Administrator status is no bypass. Default answers contain deterministic public-document excerpts and pinned source links. Opting into local conversation changes /reach ask and direct mentions to model replies under a separately permitted converse action. Administrative writes remain the separate local admin-plan/admin-apply workflow; Discord chat cannot approve a plan.

Press Ctrl+C in the foreground terminal to stop. The supervisor retires both peers and removes only its validated container identities. A child exit, EOF or process failure also stops both. The SDK may reconnect its Gateway connection within a running generation, but the harness does not install a service, auto-restart failed processes, or resume native Runs. A new invocation is a new generation, refuses pre-generation events, and starts with empty memory Runs and conversations. Keep the same state directory so the durable transport attempt journal continues to fence uncertain effects.

The optional community onboarding profile starts from a public welcome channel, a separate public introduction channel, an existing bot-owned welcome message, and two zero-permission baseline roles. The welcome channel may stay read-only. Its persistent button opens a private form for optional interests and introduction text; only explicit publication confirmation creates or updates that member's one public introduction. Those choices grant no Contributor, Maintainer, Admin, native identity, or capabilities.

For membership, humans receive the configured Magus baseline unless they already hold an explicitly configured elevated community role; bot accounts receive the common Bot role. When using these exemptions, keep one community role per human—Magus, Contributor, or Maintainer—with Admin as a separate add-on. An authorized human promotes a member by replacing the previous community role. Onboarding itself never removes roles or grants an elevated role.

This automation requires Server Members Intent. It uses no member/presence cache and backfills at most 1,000 existing members once per process generation. A private draft expires after five minutes or restart.

Select it with --onboarding-profile /absolute/private/onboarding.json on run. The strict JSON object contains guild_id, channel_id, introduction_channel_id, panel_message_id, magus_role_id, and bot_role_id. Optional elevated_human_role_ids is an array of at most two exact role ID strings for Contributor and Maintainer; omission defaults to an empty array and preserves the baseline behavior of existing profiles. IDs must be distinct from one another, the everyone role, Magus and Bot. Keep Admin outside this list. Role names do not select exemptions. Startup requires each exempted role to exist and be unmanaged; these read-only checks confer no authority to assign it. Join and startup backfill skip Magus for humans holding either configured role, including after restart. Existing duplicate community memberships require a separate reviewed human cleanup.

The two channel IDs must differ. The host freezes this bounded private file and mounts it only into the Discord edge. Startup verifies both public text channels, welcome author, baseline role authority and hierarchy before registering controls. Attaching the controls to the welcome message is a separate reviewed operator edit. Role additions preserve other memberships; public introductions use the content-free effect journal to retain their message identity across restarts. Unknown creation or update outcomes fence further publication until operator reconciliation. These are Discord presentation and membership effects, not Ward enrollment or native Graph runs.

Enable bounded surrounding history only when intended

The default policy keeps context_message_limit = 0 and the run command omits --history. Direct mentions and slash questions do not require general surrounding-history access. Leave Message Content, Server Members and Presence privileged intents off for that default.

To opt into history, stop the instance, choose a positive context_message_limit (at most 50) and context_max_age_seconds (at most 172800, or two days) in the private policy, grant Read Message History in the allowed channel, and enable Message Content for the application in the Developer Portal. Then add --history to the same run command. The policy setting and flag must agree; a positive limit without the flag, or the flag with a zero limit, refuses startup. Member-list and presence intents remain unnecessary.

To use /reach context, also add summarize_context to the first matching rule's actions, for example actions = ["answer_docs", "summarize_context"]. Enabling history alone does not authorize that action. The result is selected context excerpts, explicitly labelled as having no AI summary. History can also inform document matching for admitted questions.

The edge fetches only bounded recent messages from the admitted channel before the triggering event, skips webhooks, other bots and empty text, truncates each message to 2,000 characters, and caps the history payload at 16 KiB. The worker checks its conversation, age, count and byte bounds. Conversation mode may also include this bot's own recent replies with its configured display name. This is per-request context, not durable native conversation history. Do not enable it in a channel whose surrounding content you did not intend to admit.

Enable local conversation

Nadir conversation evolution

The explicit operator conversation profile may opt into one-to-one DMs. In the private policy.toml, add the exact sender to top-level allowed_dm_user_ids and include that sender in user_ids on the first matching rule with converse. Both are required: a guild role rule cannot admit a DM, and the allowlist alone grants no action. The following IDs are synthetic; replace them locally and update the existing matching rule rather than appending a competing rule:

allowed_dm_user_ids = ["111111111111111111"]

[[rules]]
name = "conversation-owner"
user_ids = ["111111111111111111"]
role_ids = []
actions = ["converse"]

A DM has no guild, inherited roles or parent conversation. Only converse is admitted there; allowed_triggers continues to govern guild mentions and slash commands. DMs require the conversation profile and remain separate from guild channels and other people's DMs. The edge verifies the observed one-to-one recipient before sending. That message binding grants no channel administration authority.

An admitted mention or DM receives a short acknowledgement before work starts. The acknowledgement and each final answer part have separate stable delivery identities; an acknowledgement is not completion. Answers are frozen into at most six parts of at most 1,900 UTF-16 units each before the first final send. Text exceeding that delivery budget is refused instead of silently cut off. A failed or uncertain part stops later parts. Slash answers use the same bounded splitting with ephemeral followups.

Model-authored answers, research plans and clarification questions follow the reply language explicitly requested in the current question, otherwise that question's language. Earlier history, quoted evidence and persona preferences do not choose a different reply language. Fixed transport acknowledgements, readiness/refusal messages and context-limit notices use English; this profile does not implement automatic localization of operational copy.

Adaptive context is separately opt-in: add "adaptive_context": true to the private conversation JSON shown below. Omission retains the single-call behavior. Without catalogue research, the model returns only an answer, read_more or clarify text decision, never an executable tool call. Without catalogue research, at most three model calls share one turn deadline; each uses a fresh generation-local native Run. Only a settled request for more context admits the exact next round, preserving the event, reply reference and earlier evidence. Terminal results cannot open another round.

History still requires the explicit policy/--history opt-in above. The edge reads only the same conversation, at most three pages of six recent messages, or sixteen per page when research is enabled, within the policy's total count and age limits and a shared 16 KiB text budget. A same-place reply anchor consumes those same count and byte budgets. Disabled history, exhaustion, a spent budget and unavailable history remain distinct observations; a missing needed source can produce a concrete clarification question. DM history admits only that sender and this bot. Cached excerpts last for the bounded request; they supply no durable memory, private Archive ingestion, cross-place identity or publication authority.

Optional reactions and GIFs

The private conversation JSON may enable expressions alongside adaptive_context:

{
  "adaptive_context": true,
  "expressions": {
    "enabled": true,
    "acknowledge": true,
    "gifs": []
  }
}

These are fields to merge into the complete profile below. Omission keeps expressions disabled. After a successful textual acknowledgement, the edge may add 👀 to the exact incoming mention or admitted DM. A completed answer may select one additional reaction from 👀, 👍, ❤️, 😂, ✨ and 🤔. These effects never target another message or observe unmentioned channel traffic. A cosmetic failure preserves the text answer; a failed or uncertain final part prevents later decorations. Slash responses remain text-only and ephemeral.

To offer GIF choices, add at most eight entries to gifs, each with a unique lowercase id, a short description, and an exact HTTPS GIPHY media url of the form https://media.giphy.com/media/VERIFIED_ID/giphy.gif. Replace VERIFIED_ID with a real verified media ID. The model receives only IDs and descriptions; the edge resolves the selected ID against the generation's frozen catalogue. Arbitrary model URLs, live GIF search, attachments, custom emoji and animation generation are not part of this option.

After all text parts succeed, the edge may send one selected GIF as a separate silent reply with previews enabled and mentions disabled. Ordinary text still suppresses previews. Reaction and GIF slots have stable attempt identities, so changing a choice cannot bypass an uncertain earlier effect. The bot needs Discord's Add Reactions and Embed Links permissions where used; this option does not grant or change permissions. Discord client settings and provider availability still determine whether the GIF animates.

This option requires an already running, warm local Qwen3.8-27B at the fixed literal endpoint http://127.0.0.1:28000. The host transport checks fresh /health and /v1/models observations for the exact model. It cannot start, activate, evict or replace an engine. An absent or unsuitable model refuses the turn; document search and another model are not fallbacks.

The default --model-transport host reads that loopback endpoint on the operator host. When the already-running model is inside the native Podman pod, explicitly select --model-transport podman. Its bounded transport executes only a fixed Python HTTP helper in the exact lychd-night-qwen38 container and reads the same loopback endpoint there. Request text crosses standard input, never shell arguments. The helper cannot select a different container, URL, command, model, or runtime transition; it publishes no host port. Both roads retain the exact health/model and generation-payload checks.

Create a private 0600 file at $REACH_ROOT/conversation-01.json with your local editor. This synthetic example uses the admitted defaults:

{
  "schema_version": 1,
  "persona_name": "Example",
  "persona_instructions": "Answer clearly and state uncertainty when evidence is missing.",
  "animator_id": "night-qwen38",
  "model_id": "Qwen3.8-27B",
  "max_context": 16384,
  "max_tokens": 384,
  "temperature": 0.6,
  "max_input_chars": 20000,
  "timeout_seconds": 60
}

The profile is a static instruction and exact model binding, not Mirror identity, Persona lineage or lasting memory. The supervisor freezes its validated bytes for one generation and mounts that snapshot read-only into the worker. Only its validated expression catalogue reaches the Discord edge through readiness; the persona and model frames do not. Changes require a new explicit launch.

Add converse to the first matching rule's actions. Keep answer_docs if you use admin-inspect; keep summarize_context if you use the excerpt-only context command. To admit recent surrounding conversation, first enable history using the preceding section. Then run:

uv run --locked python scripts/reach_operator.py run \
  --engine "$ENGINE" \
  --policy "$REACH_ROOT/policy.toml" \
  --token-file "$REACH_ROOT/discord-token" \
  --state-dir "$REACH_ROOT/state" \
  --images "$REACH_ROOT/images-01.json" \
  --corpus-dir "$REACH_ROOT/corpus-01" \
  --conversation-profile "$REACH_ROOT/conversation-01.json" \
  --model-endpoint http://127.0.0.1:28000 \
  --history

Omit --history when the policy limit is zero. The corpus is still required and supplies bounded public-document excerpts where relevant. The model receives recent messages and excerpts as attributed untrusted data, with oldest history omitted first to meet the input bound.

For each admitted context round, the native worker creates a generation-local Run, executes reach.operator_converse@4, and acquires an exact Dispatcher Chat lease without tools. An ordinary Run makes one bounded model request; the optional research branch below makes one planning and one synthesis request in the same Run. Adaptive context can admit up to three Runs for one turn. The relay supplies no retry, proxy, redirect, arbitrary endpoint or model override. Its reply grants no publication or host authority. Model output remains bounded; this profile does not qualify general answer quality.

The public slash-command group defaults to reach. Select an alias with --command-name example and deliberately repeat --sync-commands when changing guild registration. Names accept a lowercase initial letter followed by lowercase letters, digits or underscores, at most 32 characters. The alias changes the displayed commands, not the Reach Composition or permissions.

Opt into public catalogue research

Research requires adaptive_context and a separate research object in the private conversation profile. It offers verified local documents as doc:<manifest-path> selectors and at most eight operator-authored public web entries. Each web entry has a unique lowercase id, title, exact canonical HTTPS url, and optional bounded focus phrases. The operator authorizes each complete public locator before launch; a question, pasted link or model decision cannot add a destination. The research profile law keeps that standing authority separate from native Bridge consent and Discord identity.

An admitted mention or one-to-one DM may select at most four sources: at most three documents and two web entries. The first model call returns an interpreted plan of at most 500 characters. The edge must deliver that plan as a separate reply to the same incoming message and acknowledge successful delivery before any selected document read or web acquisition. Slash commands cannot acknowledge a research plan and therefore cannot acquire sources through this branch. The usual initial acknowledgement and optional 👀 reaction remain separate from this interpreted plan.

The native research station obtains its exact no-tool Chat lease, waits for the delivered-plan gate, then consumes bounded source excerpts and makes one synthesis call. A source provides at most 4,000 characters; oldest history is omitted before source projections are shortened to fit the combined persona and JSON input budget. Original and projected digests remain explicit. Source bodies are transient data attached only to detached outbound model messages; checkpoint state retains the plan and selectors, and neither native history nor cached Context retains the acquired bodies. The final model selects citation IDs; the host renders links only through admitted successful sources and appends explicit unavailable-source notices. A blocked page does not become a page that was read. If every selected web source fails, the host returns a fixed clarification and its unavailable-source notices, without adopting the model's unsupported comparison. Persona, humour, multipart replies and optional GIFs retain the same evidence and delivery bounds.

The current research assembly requires Docker for the edge, Core worker and separate source worker, with --model-transport podman for the already-running native model. Build the paired images using ENGINE="docker"; images built in Podman's store do not supply that Docker record. The source image adds Bubblewrap and a dedicated public CPython 3.15 runtime closure to the exact Core image. Prepare that closure outside every checkout, with no credentials or private material, then build a new private image record:

uv run --locked python scripts/reach_research_image.py \
  --core-images "$REACH_ROOT/images-01.json" \
  --python-root /absolute/public/runtime/python \
  --output "$REACH_ROOT/research-images-01.json"

Replace the synthetic runtime path with the complete dedicated interpreter tree. The builder bounds and fingerprints it, verifies copied bytes, and retains an exact research image ID. It does not qualify a new host or runtime merely by building the image.

Create a separate owner-only authority directory containing exactly one 0600 profile.json, and a distinct 0700 research-state directory; neither may contain or nest inside the other. The strict ResearchRuntimeProfile names source_kind = "operator_public_catalogue", public_catalogue_authorized = true, policy/revocation generations, an aware expiry, a catalogue exactly matching the conversation profile's research object, exact subject/channel/DM selectors, a numeric resolver address and its custody statement, and cumulative attempt/contact ceilings. Keep these files outside every checkout. Add the following selections to the conversation run command, replacing the image placeholder with the exact research value from the new record:

--engine docker --model-transport podman
--research-profile "$REACH_ROOT/research-authority/profile.json"
--research-state-dir "$REACH_ROOT/research-state"
--research-image sha256:REPLACE_WITH_THE_RECORDED_RESEARCH_IMAGE_ID

The source worker receives only the public catalogue authority, its private attempt partition and bounded event/source metadata. Discord credentials, model credentials, conversation text, checkout mounts and engine sockets do not enter it. It reserves each stable event/source attempt before contact and rechecks current authority at network and consumption boundaries. Redirects, retries, discovered links, general web search, browser rendering and durable source storage are outside this profile. Restart uncertainty cannot authorize another source attempt.

With research enabled, the native request deadline is 300 seconds, the edge waits at most 310 seconds for that result, and the whole adaptive turn has a 480-second ceiling. The research relay has 150 seconds, including a 20-second delivered-plan gate and a 70-second source-worker bound plus cleanup. Ordinary conversation retains its 90-second request and 180-second turn limits; each model call retains the selected profile timeout. These ceilings do not extend event eligibility: the default 300-second event lifetime can still refuse late consumption or settlement. Live Discord/model/source deployment and host-specific extraction qualification remain separate operator evidence obligations in State of Work.

Reuse from another chat transport

The native research Pattern is shared Reach application code. The conversation session also owns context-round admission, the three-page ceiling and the adaptive paging/clarification loop. The operator client owns the bounded private-pipe protocol, including progress delivery acknowledgements. Discord supplies its SDK pager, audience verification, progress sender and final rendering. Another transport consumes those shared contracts rather than copying the conversation controller.

Acknowledging the interpreted plan records delivery to the admitted audience, not a human verdict. The standing research authority remains separate. The current source profile and attempt partition belong to one exact Reach policy; a multi-account successor must qualify its authority and attempt identities and explicitly migrate retained claims. The extraction does not make bare subject/event IDs portable authority.

Provider-neutral acceptance cases exercise the same native Pattern, broker, pipe client and context driver with a synthetic second transport. They qualify reuse of that choreography, not a Telegram adapter. Workbench ownership keeps the finite research path distinct from tool execution and inspection UI integration.

Publish owner-supplied updates

Publication is independent of conversation. A trusted local producer supplies the finished text and is responsible for authorizing its exact content and audience before queueing it. Neither a chat message nor a model result can enqueue a publication. This queue is a local operator interface, not a Ward grant, general scheduler or automatic link from completed coding work.

Add these arguments to the selected run command, using a private queue outside every checkout:

--publications-dir "$REACH_ROOT/publications" --publication-channel 444444444444444444

Replace the synthetic channel ID with an intended text channel in the configured guild. Repeat --publication-channel for each exact destination. The edge verifies the guild channel binding at startup; chat policy does not broaden this separate host-selected destination set. Only the edge receives the queue's writable mount. The native worker and model receive no queue access.

Using a local editor, save the finished message as a private 0600 UTF-8 file outside every checkout, for example $REACH_ROOT/update-01.txt. Queue it from a separate local terminal with the same REACH_ROOT value:

uv run --locked python -m lychd.interface.discord.publications queue \
  --queue-dir "$REACH_ROOT/publications" \
  --body-file "$REACH_ROOT/update-01.txt" \
  --channel-id 444444444444444444 \
  --publication-id 12345678-1234-4234-8234-123456789abc \
  --ttl-seconds 3600

Use a fresh UUID for each separately authorized publication; retain it for inspection. The message body stays out of argv and environment variables. The queue admits at most 32 pending entries, 1,900 characters per body and a lifetime of at most one hour. A running edge scans the queue and makes one REST attempt with mentions disabled. It does not ask the model to rewrite or decide whether to publish.

After an attempt, a private queue receipt retains the publication ID, body digest, outcome and any returned remote ID. A known settlement removes queued body and request files. An unknown result retains them and blocks automatic repetition of that publication; the REST journal also continues to fence uncertainty independently.

Inspect the uncertain external result before taking any further authorized action. A new UUID cannot bypass that uncertainty. The receipt witnesses this local transport path, without establishing a durable native outbox, cross-process producer authority or application-level release acceptance.

Handle refusals and uncertain effects

The harness intentionally emits fixed diagnostics such as reach_operator_refused, and its children expose bounded diagnostic codes. A refusal does not print credentials, Discord bodies or a detailed foreign exception. Check the selected file paths, owner-only permissions, policy, image record, expected digest and engine first. Do not work around a refusal by widening mounts, passing the token in an environment variable, or starting the native worker on the host.

The isolated Core also attempts a bounded structural diagnostic for each admitted Run to stderr. It correlates the worker generation, request and Run with the returned status, fixed reason and processing stage. A failed station is named only when a retained native node event explicitly records its failure; absent or incomplete capture remains visible. These records exclude question, answer, source and provider bodies and arbitrary exception text. The supervisor accepts this record only from its Core child, separately from the Edge's fixed diagnostic codes. Diagnostic writes drop on backpressure so a stalled log consumer cannot withhold the Run's answer.

Each running supervisor retains typed structural observations in the owner-only $REACH_ROOT/state/traces/trace.sqlite3 archive. Platform message IDs link adaptive request rounds; each request links its native Run and exact model/research call identities. History counts, graph stations, model HTTP status and timings, source outcomes, acknowledgements and individual final delivery parts remain searchable after restart. An HTTP status is present only when observed; missing data never means success. No question, answer, history, source body, URL, provider body, arbitrary exception text or credential enters this archive.

The archive retains at most 30 days and 100,000 observations, with a 128 MiB database page ceiling and bounded rollback-journal overhead. Queries return a bounded retained suffix, omissions and capture counters. Structural producer sequences expose dropped observations separately from native sequences, whose missing token events are expected. A missing session close remains an unclosed observation, not evidence that every Run failed. Corruption, unsafe paths or unknown schemas refuse without erasing the archive. Preserve the original files before manual diagnosis; there is no automatic reset or repair command.

Inspect the most recent retained Run, failures, or one Discord message from the repository:

uv run --offline --locked python scripts/reach_trace.py show --archive-dir "$REACH_ROOT/state/traces" --latest
uv run --offline --locked python scripts/reach_trace.py recent --archive-dir "$REACH_ROOT/state/traces" --failures
uv run --offline --locked python scripts/reach_trace.py show --archive-dir "$REACH_ROOT/state/traces" --platform-event-id MESSAGE_ID

show also accepts an exact --run-id, --request-id or --call-id; query accepts generation, aware --since/--until times and a result limit of 1–1000 (default 100). These are local, read-only metadata queries. Capture is durable_best_effort: queue overflow, interruption or disk failure can leave gaps, and retained observations never authorize replay or settle an effect. The archive is never mounted into either child. Core and Edge container logging is disabled to avoid incidentally retaining raw private wire bodies in the container engine.

The RunLedger and event bus themselves remain in memory. The diagnostic archive does not provide Oculus ingestion, native Run recovery, conversation memory, or a replacement for the journals below.

Transport attempts live in $REACH_ROOT/state/effects. Their private JSON records contain method, target class, hashed effect/payload/scope identities, status, and an optional remote ID or fixed code. They do not retain the token, message text or response body. attempting after an interruption and unknown both mean that Discord may already have performed the write. The journal refuses unresolved effects in the same scope even under a newly generated plan.

To investigate, stop the instance and retain its plan and journal. Inspect the actual Discord server and relevant audit trail manually, comparing the reviewed resource intent and recorded remote IDs with what exists. Do not bypass the fence by rerunning an uncertain create, deleting a possible new resource, editing/removing journal records, choosing an empty state directory, or regenerating a plan.

This profile has no public journal reset or automated reconciliation command. Inspection may establish what happened, but further recovery requires explicit manual reconciliation outside the automatic apply path. Leave the effect unknown if the evidence stays inconclusive. The journal witnesses attempts; it does not retain native Runs or durable conversations.

Preparation · Architecture · State of Work