Skip to content

M2-49 Reference app for code hosting (GitHub)

No Date Ruling Reason
D-M2-49-1 2026-10-25 plugins/github-app is an ESM Node.js program (package github-app, private, Apache-2.0, version equal to VERSION) with one runtime dependency, bothy-sdk as file:../../sdk/typescript; HTTP, HMAC and files with node: modules, the two GitHub calls by hand; dev dependencies at the SDK's exact versions plus @octokit/webhooks-examples 7.6.1; every script that imports the SDK builds it first (build:sdk); coverage with quality.coverage.typescript_min_percent. Place: plugins/github-app/package.json, vitest.config.ts. The fewest third-party packages in the security path (SEC-SC-1); the root Makefile runs plugins/github-app before sdk/typescript, and npm runs no pre-scripts under ignore-scripts.
D-M2-49-2 2026-10-25 Configuration in one JSON object github_app with the twelve keys of 11 16 (place "GitHub app", added to PLACES of tools/configdoc/configdoc.py); unknown members, wrong types and bad durations abort the start with key and file, never a value; secret files must be regular files of the process user with mode 0400 or 0600, the GitHub secret and token are read at each use, the app token once; state_dir is created with 0700, an open one is refused. Place: plugins/github-app/src/config.ts. SEC-SEC-1 applied to the app; rotation without restart.
D-M2-49-3 2026-10-25 Three routes, checked in this order: path (404), method (405), stopping or unauthenticated (503), Content-Type (415), body bound (413, before reading by Content-Length, cut while streaming), signature (401), headers and JSON (400), duplicate, classification with seen identifier and job in one store write (202 or 204); plain HTTP behind the TLS proxy of the instance. Place: src/http/server.ts, src/chat/receive.ts. The app stays small; TLS and slow clients are the proxy's.
D-M2-49-4 2026-10-25 X-Hub-Signature-256 must match ^sha256=[0-9a-f]{64}$; the HMAC digest and the decoded header are compared only through crypto.timingSafeEqual on 32 bytes; reasons missing, malformed, mismatch; X-Hub-Signature is never read; tested with the values of the GitHub documentation. Place: src/github/signature.ts. 11 10.1 row "Input"; RK-83.
D-M2-49-5 2026-10-25 Closed event list: ping, issues (opened, closed, reopened), issue_comment (created), pull_request (opened, closed, reopened), pull_request_review (submitted); everything else 204 with unsupported, an unknown repository 204 with repository_not_configured, a missing field 400 with invalid_payload; the job stores the parsed fields, never the payload. Place: src/github/events.ts. The list covers the life of an issue or pull request; more events are a later change with their fixtures.
D-M2-49-6 2026-10-25 Every command and interaction is answered 202 and acted on through the API; requests of the server are verified with the SDK's verifyWebhook and the keys of one WebhookKeyCache; no v1 secret. The cache uses an SDK client without the app's token: getWebhookKeys is public (instance.discovery.read, security: []) and no scope of 05 2.6 covers it, so the server answers the app's token 403 there (found by the live test), while the SDK's own example builds the cache on a client without a token. Place: src/chat/receive.ts, src/chat/sdk-adapter.ts, src/app.ts. In a forum the server has no reply thread for a 200 (M2-25 no_thread), and GitHub calls do not fit into app.interaction_timeout.
D-M2-49-7 2026-10-25 State in state_dir: state.json and one file per job, each written to a temporary file, synced, renamed, the directory synced; a lock file created atomically with the PID inside (a temporary file linked to the name); a second store of the same process is refused before any file is touched; a lock whose PID is not alive (also the own PID, left by an earlier process of a restarted container) is taken over with a Warn line, only under the takeover mutex lock.takeover and after reading the lock again, so a live lock is never removed (residual: a mutex left by a start that died during a takeover, removed by two starts at the same moment); one worker in creation order; the job's progress holds every client_msg_id and Idempotency-Key before the first attempt; a GitHub write is marked attempted before it is sent and never repeated; a crash between the job file and the state file restores the seen key from the job. Place: src/store/, src/worker/. 04 5.1 and 5.2 make server writes repeatable; GitHub's create calls are not, and a second issue is worse than a missing comment.
D-M2-49-8 2026-10-25 Key <owner>/<name>#<number> in lower case; titles #<n> <title> and PR #<n> <title>; a 404 on a mapped thread drops the entry and opens one new post (thread_unreachable), a second 404 ends the job. Place: src/domain/keys.ts, src/worker/jobs.ts. One thread per issue or pull request (11 10.1 row "Output"); an app cannot move or retitle threads.
D-M2-49-9 2026-10-25 Foreign text: logins in code spans and bodies in fenced blocks whose delimiter is one backtick longer than the longest run of the text; a link only for an absolute https address without whitespace, angle brackets or parentheses; messages cut in UTF-8 bytes at a code point with the fence closed and "(cut, full text on GitHub)"; titles without Cc and bidi controls, cut in code points; a 400 on the title retries once with the bare title, a too_long reloads the limits once. Place: src/domain/render.ts. 03 content format rules 2 and 3, SEC-IN-4, SEC-OUT-1.
D-M2-49-10 2026-10-25 /issue has required_level trigger and always opens its own forum post with a single-use form (custom_id issue:<draft>, field body, and repository when a forum has several); the issue gets the form text and "Created from chat.", nothing about the person; 3xx and 4xx answer failed with a new card, 5xx, a time-out, a transport error or a restart after attempted answer failed with "Check the repository" and no second POST; a GitHub token file that fails its check sends nothing and answers failed with a new card; a 409 to a repeated respondToInteraction counts as done. Place: src/worker/jobs.ts. interaction.create needs trigger (05 4.10); a thread belongs to exactly one issue.
D-M2-49-11 2026-10-25 Mirroring only with messages:read, events:subscribe and a subscription; at receipt the app's own message, a non-human origin, a non-text kind, a deleted message and an unmapped thread are skipped in this order; the comment carries the display name in a code span and the text in a fenced block; the issues opened and issue_comment created events of objects the app created are not posted, checked at receipt and again in the job. Place: src/domain/mirror.ts, src/chat/receive.ts, src/worker/jobs.ts. The loop protection of 11 10.1; the echo can arrive while the job that created the object still runs.
D-M2-49-12 2026-10-25 Answers of the server: 403 scope_missing ends the job with one Warn line; 401 ends the job and the endpoints answer 503 until getMe succeeds again (tried once per sdk.retry_backoff_cap); 403, 400, 409, other 4xx end the job with their code; 422 idempotency_key_reuse is an Error line; 429 and 5xx and transport errors keep the job queued after the SDK's retries, and the worker pauses sdk.retry_backoff_cap. Place: src/worker/jobs.ts, src/app.ts. 06 5, 07 2.10.
D-M2-49-13 2026-10-25 GitHub client: the two POST calls with Accept: application/vnd.github+json, Authorization: Bearer, X-GitHub-Api-Version: 2022-11-28, User-Agent: github-app/<VERSION>; time limit sdk.http_timeout; redirect: "manual"; the body read only on 201; errors carry the status only. Place: src/github/client.ts. No Octokit in the security path; a followed redirect would carry the token elsewhere.
D-M2-49-14 2026-10-25 Logs: JSON lines with time, level, a fixed msg code and only the attributes event, action, delivery_id, webhook_id, job_id, job_kind, step, operation, status, outcome, reason, code, number, thread_id, channel_id; other fields are dropped; the SDK client's messages become sdk.client. Place: src/log.ts. SEC-LOG-2, SEC-LOG-4, SEC-SEC-3; a canary test holds it.
D-M2-49-15 2026-10-25 Manifest: required scopes exactly messages:write, commands:write, components:write; optional messages:read and events:subscribe with event message.created; delivery webhook with events_url and interactions_url on one host; command issue; components [form]; one write grant request; 11 10.1 row "Scopes" names events:subscribe as well. Place: plugins/github-app/manifest.json, docs/spec/11-extensions.md 10.1. 05 2.6 row "Event subscriptions" and the manifest rule of M2-24 require events:subscribe for events.
D-M2-49-16 2026-10-25 Fixtures: the examples of @octokit/webhooks-examples 7.6.1 (MIT), which collects the payloads of the GitHub webhook documentation, changed only in repository, numbers and IDs; three variants the package does not hold (issues closed, a merged pull request, a comment on a pull request) are minimal files under test/fixtures/github/; test/fixtures/README.md names source, version and date. Place: plugins/github-app/test/. D-W-10, RK-83; nothing is copied into the repository.
D-M2-49-17 2026-10-25 The live test runs inside npm test (Vitest project live, the globalSetup of the SDK) against sdkserver, extended additively: start member app_front {host} opens a TLS front with an in-memory authority whose roots and a test resolver the apps module's egress clients get (harness.Options.AppsEgress), and adds the host and port to webhook.allowed_hosts and egress.allowed_hosts; op app_backend {port}; op create_forum {name}; op call {as, operation_id, path_params, query, body} with as owner or alex (the merged world has no owner Robin), answers written to files of mode 0600. No Go test is added. tools/ci/components.py lets a change under services/server/ or sdk/typescript/ select plugins, and make bootstrap-ci installs every tracked plugins/*/package-lock.json. Place: services/server/test/sdkserver/, plugins/github-app/test/live/, tools/ci/components.py, Makefile. PLAN 8 Exit criterion 9: the reference apps pass against the real server, inside make check.