@open-everywhere/provider-github 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +50 -2
- package/agent/channels/github-catch-up.ts +134 -0
- package/agent/channels/github-engagement.ts +381 -0
- package/agent/channels/github-identity.ts +376 -0
- package/agent/channels/github-installations.ts +152 -0
- package/agent/channels/github-message-ops.ts +62 -0
- package/agent/channels/github-origin-codec.ts +42 -0
- package/agent/channels/github-post-arm.ts +422 -0
- package/agent/channels/github-reaction-feedback.ts +489 -0
- package/agent/channels/github-reaction-scope.ts +312 -0
- package/agent/channels/github-read.ts +471 -0
- package/agent/channels/github-stop.ts +175 -0
- package/agent/channels/github-supervisor.ts +645 -0
- package/agent/channels/github-webhook-verifier.ts +56 -0
- package/agent/channels/github.ts +1458 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/memory/memory.ts +265 -0
- package/agent/ops.ts +17 -0
- package/agent/provider-events.ts +114 -0
- package/agent/register.ts +130 -0
- package/agent/surface-parity.ts +116 -0
- package/dist/agent/channels/github-catch-up.d.ts +29 -0
- package/dist/agent/channels/github-catch-up.d.ts.map +1 -0
- package/dist/agent/channels/github-catch-up.js +105 -0
- package/dist/agent/channels/github-catch-up.js.map +1 -0
- package/dist/agent/channels/github-engagement.d.ts +223 -0
- package/dist/agent/channels/github-engagement.d.ts.map +1 -0
- package/dist/agent/channels/github-engagement.js +317 -0
- package/dist/agent/channels/github-engagement.js.map +1 -0
- package/dist/agent/channels/github-identity.d.ts +164 -0
- package/dist/agent/channels/github-identity.d.ts.map +1 -0
- package/dist/agent/channels/github-identity.js +295 -0
- package/dist/agent/channels/github-identity.js.map +1 -0
- package/dist/agent/channels/github-installations.d.ts +49 -0
- package/dist/agent/channels/github-installations.d.ts.map +1 -0
- package/dist/agent/channels/github-installations.js +114 -0
- package/dist/agent/channels/github-installations.js.map +1 -0
- package/dist/agent/channels/github-message-ops.d.ts +3 -0
- package/dist/agent/channels/github-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/github-message-ops.js +58 -0
- package/dist/agent/channels/github-message-ops.js.map +1 -0
- package/dist/agent/channels/github-origin-codec.d.ts +7 -0
- package/dist/agent/channels/github-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/github-origin-codec.js +36 -0
- package/dist/agent/channels/github-origin-codec.js.map +1 -0
- package/dist/agent/channels/github-post-arm.d.ts +54 -0
- package/dist/agent/channels/github-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/github-post-arm.js +339 -0
- package/dist/agent/channels/github-post-arm.js.map +1 -0
- package/dist/agent/channels/github-reaction-feedback.d.ts +97 -0
- package/dist/agent/channels/github-reaction-feedback.d.ts.map +1 -0
- package/dist/agent/channels/github-reaction-feedback.js +380 -0
- package/dist/agent/channels/github-reaction-feedback.js.map +1 -0
- package/dist/agent/channels/github-reaction-scope.d.ts +102 -0
- package/dist/agent/channels/github-reaction-scope.d.ts.map +1 -0
- package/dist/agent/channels/github-reaction-scope.js +296 -0
- package/dist/agent/channels/github-reaction-scope.js.map +1 -0
- package/dist/agent/channels/github-read.d.ts +135 -0
- package/dist/agent/channels/github-read.d.ts.map +1 -0
- package/dist/agent/channels/github-read.js +371 -0
- package/dist/agent/channels/github-read.js.map +1 -0
- package/dist/agent/channels/github-stop.d.ts +52 -0
- package/dist/agent/channels/github-stop.d.ts.map +1 -0
- package/dist/agent/channels/github-stop.js +145 -0
- package/dist/agent/channels/github-stop.js.map +1 -0
- package/dist/agent/channels/github-supervisor.d.ts +308 -0
- package/dist/agent/channels/github-supervisor.d.ts.map +1 -0
- package/dist/agent/channels/github-supervisor.js +339 -0
- package/dist/agent/channels/github-supervisor.js.map +1 -0
- package/dist/agent/channels/github-webhook-verifier.d.ts +5 -0
- package/dist/agent/channels/github-webhook-verifier.d.ts.map +1 -0
- package/dist/agent/channels/github-webhook-verifier.js +55 -0
- package/dist/agent/channels/github-webhook-verifier.js.map +1 -0
- package/dist/agent/channels/github.d.ts +139 -0
- package/dist/agent/channels/github.d.ts.map +1 -0
- package/dist/agent/channels/github.js +1232 -0
- package/dist/agent/channels/github.js.map +1 -0
- package/dist/agent/contributions.d.ts +12 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +31 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/memory/memory.d.ts +54 -0
- package/dist/agent/memory/memory.d.ts.map +1 -0
- package/dist/agent/memory/memory.js +224 -0
- package/dist/agent/memory/memory.js.map +1 -0
- package/dist/agent/ops.d.ts +3 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +16 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +112 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +4 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +116 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +110 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +156 -4
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// THE CONTRIBUTION MANIFEST for `@open-everywhere/provider-github` (OPE-375) — the curated
|
|
2
|
+
// catalogue of everything this package OFFERS an app's eve slot tree. `oe build` reads it;
|
|
3
|
+
// the app's `roster.config.ts` decides about it, and `roster.lock.json` records the review.
|
|
4
|
+
//
|
|
5
|
+
// 🔴 CURATED, NOT SCANNED, AND THAT IS MEASURED RATHER THAN STYLISTIC. A directory scan cannot
|
|
6
|
+
// enumerate contributions: core ships 47 files under `agent/channels/` of which 9 are channels, and
|
|
7
|
+
// 13 under `agent/connections/` of which 1 is a connection. Shape predicates do not rescue it either
|
|
8
|
+
// — only 8 of the 9 mounted core channels contain `defineChannel(`. A hand-kept list is the only
|
|
9
|
+
// honest catalogue, so keeping it current is a real authoring duty (`npm run lint:contributions`
|
|
10
|
+
// scan-compares the two kinds where a predicate IS sound — tools and hooks — and says plainly that
|
|
11
|
+
// the other four are unguarded).
|
|
12
|
+
//
|
|
13
|
+
// 🔴 THIS FILE IMPORTS NOTHING, DELIBERATELY. A provider bundle may reach core only through the
|
|
14
|
+
// `@open-everywhere/core/agent/adapter-api.js` barrel (the boundary lock, packages/core/scripts/
|
|
15
|
+
// boundary-test.ts), so a manifest that imported its own types would be a boundary red — and
|
|
16
|
+
// widening the barrel to admit a build-time type would be the wrong fix. Every manifest is therefore
|
|
17
|
+
// import-free DATA whose shape `roster-plan.ts` validates at build time, exactly as it must for a
|
|
18
|
+
// third-party package it has never seen. The types are in
|
|
19
|
+
// `@open-everywhere/core/agent/scaffold/roster-config.js` for READERS.
|
|
20
|
+
//
|
|
21
|
+
// ERASABLE SYNTAX ONLY (no `enum`, no `namespace`) — `oe build` loads this with a bare import.
|
|
22
|
+
|
|
23
|
+
export const CONTRIBUTIONS = {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
package: "@open-everywhere/provider-github",
|
|
26
|
+
register: "agent/register.js#registerGithubProvider",
|
|
27
|
+
contributions: [
|
|
28
|
+
// ── channels ─────────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
{ kind: "channel", name: "github", module: "agent/channels/github.js", summary: "GitHub — a PROPER conversational surface (not just an event pipe)." },
|
|
30
|
+
],
|
|
31
|
+
} as const;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// GENERATED by `npx tsx scripts/docs-generate.ts` from docs/deploy/surfaces/github.md — DO NOT HAND-EDIT.
|
|
2
|
+
// Edit the markdown under docs/ and re-run the generator; `npm run lint:docs` fails the
|
|
3
|
+
// build if this file and its sources ever disagree. See scripts/docs-generate.ts for why the
|
|
4
|
+
// manual is compiled in at all (docs/ does not exist on disk in a hosted deployment).
|
|
5
|
+
|
|
6
|
+
import type { DocEntry } from "@open-everywhere/core/agent/adapter-api.js";
|
|
7
|
+
|
|
8
|
+
export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
9
|
+
{
|
|
10
|
+
path: "docs/deploy/surfaces/github.md",
|
|
11
|
+
title: "GitHub",
|
|
12
|
+
summary: "Install Open Everywhere's GitHub App on a repo and the agent becomes a conversational teammate on issues and pull requests: @mention it in an issue/PR/review comment and it wakes with the repo checked out into its sandbox (and the PR diff…",
|
|
13
|
+
markdown: "# GitHub\n\nInstall Open Everywhere's GitHub App on a repo and the agent becomes a **conversational\nteammate on issues and pull requests**: @mention it in an issue/PR/review comment and it wakes\nwith the repo checked out into its sandbox (and the PR diff in context), does the work, and\nreplies as a comment. Opt the repo's memory **domain** into ambient mode and it also becomes a\n**quiet standing reviewer**: it reads every issue/PR-timeline comment, judges each against a\ndeliberately high bar (GitHub defaults to the *reserved* calibration tier), and comments only\nwhere it clearly adds something — every silence audited.\n\nThis is the GitHub *conversational-surface* runbook. It is **distinct from the\n[source-control plane](../../capabilities/source-control.md)** (the agent's own coding loop —\ncheckout/push/PR via the `OE_GITHUB_*` credentials): the two are orthogonal planes that can\nshare one GitHub App but use **separate env var sets** (bare `GITHUB_*` here). For the concept\nof a *surface*, see [Surfaces](../../extending/surfaces.md). Like\n[Linear](./linear.md)/[Notion](./notion.md), GitHub is ticket/comment-based rather than\nchat-based, so it's out of scope for the chat [support matrix](./support-matrix.md).\n\n## How it works — three legs on one webhook route\n\nEvery GitHub App webhook delivery hits **`POST /eve/v1/github`** and feeds up to three legs:\n\n1. **The @mention leg** (eve's native `githubChannel`). Typing `@<app-slug>` in an issue, PR,\n or review comment wakes an agent session anchored to that conversation, with the repo\n checked out and (for PRs) the diff in context; the reply posts back as a comment. Note App\n bots don't appear in GitHub's `@` autocomplete — the mention matcher is text-based, so\n **type the slug in full** (e.g. `@open-everywhere`). The `@mention` also **engages** that\n issue or pull request: later plain comments on it wake the agent with no new mention, until\n somebody asks it to stop following — see [Reply without re-tagging](#reply-without-re-tagging)\n below.\n2. **The event → routine fan-out.** `issues` / `pull_request` / CI events fire any subscribed\n [event routines](../../capabilities/proactivity-and-routines.md) into their (Slack) surfaces.\n Before it fires, OE **re-fetches the entity over its own credential** rather than trusting the\n webhook body — *payload-as-pointer*. The fired message states the read instant and marks\n itself the current, authoritative state; the webhook's own one-line summary sits above it and\n may be older (a queued delivery can still describe a PR that has since merged), so that\n summary is HISTORY, not the present. The vendor's own `updated` timestamp, when GitHub sends\n one, is aged against the read the same way a [Linear](./linear.md#reading-an-issue) comment is.\n **The re-fetch applies to an issue or a pull request**, the two things GitHub serves under a\n number. A `check_run`, a `release` or a `workflow_run` is identified by an opaque id instead, so\n there is nothing to re-read: those fire from the webhook's own one-line summary and claim to have\n read nothing.\n A **`workflow_run`** fire also carries the run's branch, workflow name, status and conclusion as\n filterable fields, so \"a red build on our `main`\" is a subscription rather than a routine that\n wakes on every run and reads the payload:\n `.scope.container == \"acme/web\" and .fields.conclusion == \"failure\" and .fields.branch == \"main\"`.\n **The container clause is required, not decoration** — a `github` subscription that narrows by\n nothing is refused when you arm it, because one source spans every repo the App is installed on.\n And `branch` is a ref *name*: a run triggered by a pull request from a fork carries the\n contributor's head ref, and a fork's own default branch is usually `main` too, so `branch` alone\n does not prove the run ran on your branch. See the\n [event inventory](../../capabilities/proactivity-and-routines.md#the-event-inventory--what-each-vendor-can-send-and-how-far-we-carry-it)\n for what every other event does and does not carry.\n3. **The ambient-supervisor leg** (opt-in). Every *non-mention* issue/PR-timeline comment on an\n opted-in repo is judged by a **standing per-repo supervisor session** — the same ambient\n judge Slack channels and Notion docs use — which replies into the triggering issue/PR or\n stays audibly silent (a `github.no_reply` audit row, reason included).\n\n### Reply without re-tagging\n\nAn `@mention` marks that issue or pull request **engaged**. From then on a plain comment there\nwakes the agent — no second `@mention`, and no domain opt-in: the invitation was the comment\nsomebody already wrote.\n\n- **It is per container, never per repo.** Engaging issue #42 says nothing about #43.\n- **Private repos only.** A plain follow-up wakes a full turn — the agent checks the repo out and\n spends on the model — so on a public repo it would let any passer-by buy one. The follow-up\n therefore does nothing on a public repo, or on one whose privacy the delivery does not state.\n It is a *stricter* floor than ambient mode's below: ambient mode admits a public repo you vouched\n internal, and this one does not — vouching a repo never opens an uninvited full turn on it. An\n `@mention` still works on a public repo, exactly as before.\n- **Inline review comments do not count as follow-ups.** A review thread shares its pull request's\n container, so a plain inline comment on an engaged pull request wakes nothing — one review would\n otherwise start a turn per comment. `@mention` the app inside the review thread to get an answer\n there.\n- **Asking it to stop works, and it is durable.** Say it in words — \"leave this to us\", \"stop\n following this one\" — and the agent calls `stand_down`, which stores the stop against that\n issue. It then answers nothing there, on either leg, until somebody `@mentions` it again. A new\n `@mention` is the way back in; there is no timeout.\n- **A container nobody ever mentioned it in is untouched** by all of this. It wakes only through\n the ambient supervisor below, which is off until a domain opts in.\n- **The agent's own comments never re-trigger it**, and neither do other bots' — an engaged issue\n admits comments from people only.\n\n### Ambient mode — reserved by default, fail-closed everywhere\n\n- **Opt-in is per domain** (`ambientSupervisor: true` on the repo's memory domain — the same\n flag that turns on Slack/Notion ambient). Until a domain opts in, non-mention comments do\n nothing.\n- **A positively private repo, or any repo the operator vouched internal — fail-closed.** On a\n private repo every commenter was invited. On an unvouched public repo any drive-by stranger\n could buy judged model turns (an uninvited spend + prompt-injection surface). So the ambient\n leg runs on a repo the delivery states is private, and on any repo the operator vouched\n internal. It **drops every other comment** fail-closed — a repo that is neither positively\n private nor vouched, which includes a repo whose privacy the delivery does not state. Vouch\n one repo by declaring the surface key `github/<owner>` or `github/<owner>/<repo>` internal in\n the trust register (see\n [Identity](../../how-it-works/identity.md#the-trust-registry--declaring-what-a-rooms-audience-is));\n `OE_TRUST=internal` is the deployment-wide blanket, not the per-repo act.\n- **GitHub's calibration tier defaults to `reserved`** — a deliberately *higher* bar than\n Slack/Notion, because an over-eager bot in a collaborative code space reads as spam. Tune it\n per domain with `engagementTier` (see the\n [configuration reference](../configuration-reference.md#memory-domain--silo)).\n- **Mentions never double-handle, and neither do engaged follow-ups.** The ambient classifier\n reuses the exact mention gate the native leg wakes on. On a SINGLE-WINDOW, operator-vouched\n repo it returns `direct`, and the unified window answers the mention. On every other repo it\n *drops* the mention, because the native leg owns it. An engaged follow-up goes to whichever\n leg owns mentions on that repo, so the two legs partition every comment with no overlap and\n no gap:\n - on a repo where the NATIVE leg answers mentions, the native leg owns the engaged follow-up\n too, and the ambient leg drops it;\n - on a SINGLE-WINDOW, operator-vouched repo the native leg stands down for every comment\n (`shouldSuppressNativeMention`), so the AMBIENT leg owns the engaged follow-up and answers it\n as a judged wake — even with `ambientSupervisor` off. That wake states\n `follow-up=\"thread-engaged\"`, as the Slack, Linear and Notion wakes do, so the agent reads the\n follow-up as part of a conversation it is already in. An @mention carries no `follow-up`\n attribute: it is owed an answer whatever the enrolment says. The classifier itself reads no\n engagement row; the split is made at the native suppression gate.\n- **A stood-down container is dropped here too.** Once somebody asks the agent to stop following\n an issue, the ambient leg stops answering it as well, whatever this opt-in says. That is the\n only thing that makes the stop a stop rather than a note.\n- **The supervisor is silent plumbing.** Its judged turns add no 👀 reaction (the mention leg\n keeps its ack reaction), never check the repo out, and a *failed* judge turn is audited —\n never posted as an error comment.\n- Review-thread **inline** comments (`pull_request_review_comment`) are out of ambient scope\n v1 (a different reply/threading model); PR *timeline* comments are covered (GitHub delivers\n them as `issue_comment`).\n\n## How the silo model maps\n\n| Open Everywhere concept | GitHub thing |\n| --- | --- |\n| `org` (tenant) | your deployment (`deploymentIdentity.org` in `agent/identity/surfaces.ts`, one org per deploy today) |\n| `workspace` (identity key) | the repo **owner** (org/user login) |\n| `surface` (partition key) | the **repo** (one repo, many issues/PRs over time) |\n| invocation + post-back anchor | the **issue / PR** number |\n| `mode` | `channel` (GitHub has no 1:1 DM analog) |\n| `visibility` | `public` only for an explicitly public repo; else `private` (fail-closed) |\n\n**Trust fails closed.** Only a *private* repo touched by an org **OWNER/MEMBER** binds\n`internal` (shared/org memory readable); public repos, outside collaborators, and missing\nsignals bind `external`. The operator can vouch a deployment with `OE_TRUST=internal` — the\nsame layer the other non-Slack platforms use.\n\n## Setup\n\n1. **Create (or reuse) a GitHub App** with a webhook pointed at\n `https://<your-deploy-url>/eve/v1/github`, a webhook secret, and repo permissions for\n Issues + Pull requests (read/write) and Contents (read — the mention leg's sandbox\n checkout). **Grant Issues *and* Pull requests, both at read/write — neither alone is\n enough.** GitHub serves an issue and a PR through the same `/issues/{n}` endpoints but\n routes permission by the resource the number names, so an App missing either family goes\n 403 on half of its own conversations: missing Issues and it cannot read a plain issue,\n missing Pull requests and its replies never land on a PR thread. Subscribe to\n **`issue_comment`** + **`pull_request_review_comment`** (mention\n turns; `issue_comment` also feeds the ambient leg) and, for the routine fan-out,\n `issues` / `pull_request` / `check_suite` / `check_run` / `workflow_run` (the Checks events\n only appear once the Checks permission is granted).\n You do **not** need to subscribe to `installation` / `installation_repositories` — GitHub\n sends both to every App automatically and they are absent from the \"Subscribe to events\"\n list for that reason (\"All GitHub Apps receive this event by default. You cannot manually\n subscribe to this event.\"). They arrive on the same webhook URL, so each org that installs\n the App registers itself with no extra configuration (see *Multi-org installs* below).\n2. **Install the App** on the repos the agent should live on.\n3. **Configure the environment** (the conversational surface uses the **bare** names — the\n `OE_GITHUB_*` set is the separate source-control plane, even when both point at one App):\n\n ```bash\n GITHUB_APP_ID=… # the App's numeric id\n GITHUB_APP_SLUG=… # the App slug — the bot answers to @<slug>\n GITHUB_APP_PRIVATE_KEY=… # PEM private key (mint installation tokens)\n GITHUB_WEBHOOK_SECRET=… # HMAC-verifies every delivery. REQUIRED — unset rejects all (fail closed)\n # DEV ONLY, and it is a SETTING not a variable: turn on `webhooks.allowUnverifiedGithub`\n # in agent/settings/settings.ts to accept unsigned deliveries locally.\n ```\n\n4. **For ambient mode**, add the repo to a memory domain (surface key\n `github/<owner>/<repo>`) and set `ambientSupervisor: true` on it — see\n [Memory](../../capabilities/memory.md). A domain can compose repos **with** Slack channels,\n Notion docs, and Linear teams: one flag, one shared silo, ambient everywhere in it.\n\n## Multi-org installs (self-registering)\n\nThe source-control plane mints tokens per **owner**, so it needs an `owner → installation id`\nentry for every account the App is installed on. You can set that by hand with\n`OE_GITHUB_INSTALLATIONS` (a JSON map) or the single-owner `OE_GITHUB_OWNER` /\n`OE_GITHUB_INSTALLATION_ID` pair — and you no longer have to.\n\nBecause GitHub delivers `installation` + `installation_repositories` to every App by default\n(no subscription exists to switch on), each install/uninstall registers\nitself: `created`/`unsuspend` records the owner's installation id, `deleted`/`suspend` clears it,\nand a repo-scope change refreshes it. An org installing your App is then self-service end to end —\nno env edit, no redeploy.\n\n**`OE_GITHUB_INSTALLATIONS` stays authoritative.** The discovered entries are composed *under* the\nenv map, so anything you pinned by hand always wins, a store outage degrades to exactly the\nenv-configured set, and a webhook can never re-point an owner you configured yourself. (By the same\nrule, uninstalling an org you *also* pinned in env leaves the env entry in place — its mints then\nfail loudly at GitHub, which is the honest signal; remove it from the env map to finish the removal.)\n\nInstall and uninstall both write an audit row — `surface.app_installed` / `surface.app_uninstalled`,\nwith the owner as the workspace — so the install history is queryable over the control plane's\naudit route.\n\n## Ticket actions (state / assignee / labels)\n\nThe agent can also mutate an issue/PR's own state, assignee, and labels —\n[Source control § Ticket actions](../../capabilities/source-control.md#ticket-actions-set_state--assign--add_label)\nis the canonical doc (these ride the **source-control plane**'s credential, the `OE_GITHUB_*`\nApp, not this page's bare `GITHUB_*` conversational set — grant it the Issues permission).\nThese three verbs are known to work on an **issue**. On a **pull request** they are expected to\nbe refused today, because their credential names only the Issues family — tracked as OPE-1021.\n\nIt can also **file a new issue** with `create_ticket` (`target` = `owner/repo`, defaulting to\nthis turn's own repo, or to the channel's sole granted repo) — same Issues permission, same\nper-channel repo grant, and it always goes through the approval gate first — a card, unless an\noperator has explicitly lowered that verb in the approver chain. See\n[Source control § Filing new work](../../capabilities/source-control.md#filing-new-work-create_ticket).\n\n## Reading the conversation\n\nThe agent reads the thread it was called into with the same two verbs it uses on every other\nsurface: `read_channel` reads the issue-or-PR conversation — the opening post, then the newest\ntimeline comments — and `read_thread` reads one PR **review thread**, the inline discussion on a\nline of the diff. Both take no target: they read the conversation this turn arrived in.\n\nThree things worth knowing:\n\n- **A plain issue reads exactly like a PR.** The read goes through `/repos/{repo}/issues/{number}`,\n which GitHub serves for both, so nothing is PR-only here. `get_pr_feedback` remains the richer\n read for a PR under review — reviews, inline comments and check runs, which a conversation read\n does not carry.\n- **Only a comment on the diff has a thread.** An issue comment and a PR timeline comment are at the\n top level of their conversation, so `read_thread` on one says so rather than reading the timeline\n under a thread read's name.\n- **A delegated worker needs the repo grant.** The seat reads its own conversation; the worker's\n `read_work_channel` / `read_work_thread` mount only when the calling scope grants at least one repo\n ([Connectors § A chat surface can be a work surface too](../../capabilities/connectors-and-providers.md#a-chat-surface-can-be-a-work-surface-too)).\n\nThe read runs on the source-control credential — the `OE_GITHUB_*` App installation token, falling\nback to `GITHUB_TOKEN` — so a repo the App is not installed on cannot be read.\n\n## Gotchas\n\n- **A reaction on the agent's comment is read, not delivered — and only in conversations it has\n commented in.** GitHub sends no reaction webhook, so the agent looks for reactions on its own\n comments when the *next* delivery names that conversation. Two bounds keep that cheap, and both\n change what you will see. It looks only in conversations it is *in* — ones this deployment's reply\n path has commented in, or ones somebody `@mention`ed it in — because a conversation it is not in\n can hold no reaction it would record. The `@mention` half is what covers the replies the framework\n posts on its own; what falls outside both is a conversation it only ever answered *ambiently*,\n with no `@mention` ever, and has not answered in since this shipped — that one is picked up again\n on its next reply there. And each repository runs roughly 60 such polls an hour at most; the\n counter is deliberately cheap rather than exact, so a burst of simultaneous deliveries can push an\n hour a little over. When a repository passes the ceiling, one **`github.reaction_poll_budget`**\n audit row is written for that hour and the rest of the hour's reactions are not read. Query it with\n `GET /control/v1/audit?since=<iso>&actions=github.reaction_poll_budget` — a busy repository showing these rows\n every hour is the signal that the ceiling, not the feature, is what you are seeing.\n- **A reaction the agent has already recorded is skipped, and a store it cannot read skips\n everything.** The dedupe is a durable claim per reaction, and it fails *closed*: an unreadable\n claim store is treated as \"already recorded\", so no duplicate row is ever written. The cost is\n that a store outage stops the feedback signal completely while it lasts, which on its own looks\n exactly like a quiet week. Each poll that could not read the store writes one\n **`github.reaction_claim_unreadable`** audit row naming how many reactions it skipped. Query it\n with `GET /control/v1/audit?since=<iso>&actions=github.reaction_claim_unreadable`. Rows here mean\n the operational store, not the feature, is what you are seeing; reactions are recorded on the next\n poll after it recovers. **The row covers a *partial* fault, not every one** — it is written to the\n same operational store the claim read failed against, so a store that is wholly unreachable loses\n this row too. That case is visible instead as the audit drop counter and an\n `[open-everywhere:audit] dropped a \"github.reaction_claim_unreadable\" entry …` warning in the\n logs. So: rows present means the claim table specifically; the warning means the store.\n- **Ambient is off until a domain opts in**, and it never runs on a repo that isn't positively\n private. If non-mention comments get no response, check the domain flag, then the repo's\n privacy — and read the record rather than guessing: the first delivery from a repo the\n supervisor was never asked about writes a **`github.ambient_skipped`** audit row naming the\n surface key, the reason (`no-domain` — nothing routes this repo — versus `domain-refused` —\n a domain does route it but hasn't opted in) and the exact edit to make. Query it with\n `GET /control/v1/audit?since=<iso>&actions=github.ambient_skipped`. A whole owner at once is\n `reach: [\"github/<owner>\"]` on the domain — or, better where it fits, a same-level glob\n `reach: [\"github/<owner>/payments-*\"]` plus `exclude` for the repos that must stay out. With\n 500 repos the whole-owner entry is tempting, but it confers the domain's bundles on every repo\n the org will ever create; the glob is the same ergonomics with strictly less reach.\n See the [configuration reference](../configuration-reference.md#memory-domain--silo) and\n [Access & scoping](../../how-it-works/access-and-scoping.md#reach-exact-keys-and-containers).\n- **The `reply` tool is the only way an answer reaches an issue.** The agent posts mid-turn by\n calling `reply`; its turn's closing text is DROPPED and never posted. A turn that delivered\n writes a `github.replied` row carrying `route: \"tool\"`.\n- **Silence is a first-class outcome.** Most comments correctly get no reply (reserved tier).\n Every judged-silent turn is a `github.no_reply` audit row — the surface is working, not\n broken. Tune via `engagementTier`, watching the live `no_reply` rate. A **different** quiet\n outcome is a `turn.supervisor_prose_dropped` row on the github partition: the agent wrote an\n answer as its closing text and never called `reply`, so nothing was posted. That row is a\n defect signal, not a tuning signal — read the session transcript it names.\n- **An @mention turn can judge silence too.** The `github.no_reply` row is not only an ambient\n outcome. A turn woken by a direct @mention can also decide the wake does not warrant an answer;\n it then comments NOTHING and writes the same row with `route: \"native\"` and the reason. Those\n rows are rare by construction — a member addressed the app — so treat one as worth reading, not\n as tuning noise.\n- **Mention the slug in full.** App bots aren't in GitHub's `@` autocomplete; the matcher is\n text-based (`@<slug>` followed by a non-word character).\n- **Webhook verification is fail-closed.** `GITHUB_WEBHOOK_SECRET` unset/wrong → every\n delivery is rejected 401 and every leg stays silent — including the installation leg, so a\n deploy with a wrong secret silently learns nothing. Check the secret first if installs\n aren't registering.\n\n## Status\n\nThe @mention leg and the routine fan-out are **live-verified**. The ambient leg is built,\nunit-tested, and driven by the `J-GH` end-to-end scenario in\n`packages/core/scripts/e2e/scenarios.ts` (`suite: \"core\"`, non-optional, judged), with\n`J-ENGAGED-github` covering the engaged-follow-up seam. `J-GH` drives the mention reply, the\npickup reaction, the **mention gate** (a non-mention comment gets no reply) with its\naudited-silence row, session resume, reply-targeting across two issues, and the signed-forge\nroute contract. The ambient *wake* on an opted-in repo is `J-ENGAGED-github`'s leg: a mention\nengages the issue, a plain follow-up wakes, and `stand_down` stops it. The repository holds the\nscenarios, not their results, so read live-verification status off those runs rather than off\nthis page.\n",
|
|
14
|
+
},
|
|
15
|
+
];
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// The github memory backing — a domain's memory living in a GitHub repo subtree, behind the
|
|
2
|
+
// MemoryProvider seam (docs/capabilities/memory.md "Git backing"). It PIGGYBACKS the GitHub App
|
|
3
|
+
// plane (scm/*: installations, scoped contents:write token minting), so a memory repo is just
|
|
4
|
+
// another repo the App is installed on — the second consumer of authenticated git-repo access
|
|
5
|
+
// after code checkout.
|
|
6
|
+
//
|
|
7
|
+
// LIVES HERE, NOT IN CORE (OPE-390 owner Direction 3). A connector-owned backing belongs to the
|
|
8
|
+
// provider package that owns the connector; core keeps only the FIRST-PARTY backings (postgres,
|
|
9
|
+
// sqlite), whose "connection" is the deployment identity. Core reaches nothing in here — this
|
|
10
|
+
// module registers itself into core's provider registry through the adapter-api barrel, exactly
|
|
11
|
+
// like the Notion backing already did.
|
|
12
|
+
//
|
|
13
|
+
// The MemorySilo supplies the location: `container` = "owner/repo", `path` = a subtree root.
|
|
14
|
+
// Within that, files are keyed by org + tier (so one repo can host many domains with at-rest
|
|
15
|
+
// separation, or you give each domain its own repo). The agent never sees any of this — the host
|
|
16
|
+
// materialises the resolved files into the sandbox.
|
|
17
|
+
//
|
|
18
|
+
// Construction is dependency-injected (a GitHubContents client + a per-repo token mint + the bot
|
|
19
|
+
// committer) so it's unit-testable offline; registerGitHubMemory() wires the real GitHub App plane
|
|
20
|
+
// AND declares the connector-memory capability that carries the prefetch invariant.
|
|
21
|
+
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import {
|
|
24
|
+
type MemoryProvider,
|
|
25
|
+
type MemorySilo,
|
|
26
|
+
type MemoryFiles,
|
|
27
|
+
type Partition,
|
|
28
|
+
type Tier,
|
|
29
|
+
type DomainMemoryGrants,
|
|
30
|
+
registerProvider,
|
|
31
|
+
registerConnectorMemory,
|
|
32
|
+
tierRelDir,
|
|
33
|
+
safeName,
|
|
34
|
+
seedFor,
|
|
35
|
+
makeSelector,
|
|
36
|
+
normalizeSelection,
|
|
37
|
+
GitHubContents,
|
|
38
|
+
type CommitId,
|
|
39
|
+
getGitHubApp,
|
|
40
|
+
MEMORY_WRITE_PERMISSIONS,
|
|
41
|
+
warmCredential,
|
|
42
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
43
|
+
|
|
44
|
+
const DEFAULT_BRANCH = process.env.OE_MEMORY_GIT_BRANCH || "main";
|
|
45
|
+
|
|
46
|
+
export interface GitHubMemoryDeps {
|
|
47
|
+
readonly contents: GitHubContents;
|
|
48
|
+
/** Mint a scoped contents:write token for one "owner/repo". */
|
|
49
|
+
mintToken(repo: string): Promise<string>;
|
|
50
|
+
/** The bot commit identity (open-everywhere[bot]). */
|
|
51
|
+
committer(): CommitId;
|
|
52
|
+
readonly branch?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireSilo(silo: MemorySilo | undefined): MemorySilo {
|
|
56
|
+
if (!silo?.container) {
|
|
57
|
+
throw new Error('github memory backing needs a silo with container "owner/repo" (memory/silo.ts).');
|
|
58
|
+
}
|
|
59
|
+
return silo;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The repo subtree a (silo, partition, tier) maps to: silo.path / <org> / <tier path>.
|
|
63
|
+
// Org is included so one repo can safely host multiple orgs/domains at rest.
|
|
64
|
+
function repoPath(silo: MemorySilo, p: Partition, tier: Tier): string {
|
|
65
|
+
return [silo.path, safeName(p.org), tierRelDir(p, tier)].filter((s) => s && s.length > 0).join("/");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function makeGitHubProvider(deps: GitHubMemoryDeps): MemoryProvider {
|
|
69
|
+
const branch = deps.branch ?? DEFAULT_BRANCH;
|
|
70
|
+
|
|
71
|
+
async function read(p: Partition, tier: Tier, siloArg?: MemorySilo): Promise<MemoryFiles> {
|
|
72
|
+
const silo = requireSilo(siloArg);
|
|
73
|
+
const repo = silo.container!;
|
|
74
|
+
const dir = repoPath(silo, p, tier);
|
|
75
|
+
const token = await deps.mintToken(repo);
|
|
76
|
+
const shas = await deps.contents.listFiles(repo, dir, branch, token, makeSelector(normalizeSelection(silo)));
|
|
77
|
+
const out: MemoryFiles = {};
|
|
78
|
+
for (const rel of Object.keys(shas)) {
|
|
79
|
+
const file = await deps.contents.getFile(repo, `${dir}/${rel}`, branch, token);
|
|
80
|
+
if (file) out[rel] = file.content;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function write(p: Partition, tier: Tier, files: MemoryFiles, message: string, siloArg?: MemorySilo): Promise<string | null> {
|
|
86
|
+
const silo = requireSilo(siloArg);
|
|
87
|
+
const repo = silo.container!;
|
|
88
|
+
const dir = repoPath(silo, p, tier);
|
|
89
|
+
const token = await deps.mintToken(repo);
|
|
90
|
+
const committer = deps.committer();
|
|
91
|
+
let last: string | null = null;
|
|
92
|
+
let changed = false;
|
|
93
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
94
|
+
const path = `${dir}/${rel}`;
|
|
95
|
+
const existing = await deps.contents.getFile(repo, path, branch, token);
|
|
96
|
+
if (existing && existing.content === content) continue; // unchanged → skip (no-op)
|
|
97
|
+
changed = true;
|
|
98
|
+
last = await deps.contents.putFile(repo, path, content, message, branch, token, committer, existing?.sha);
|
|
99
|
+
}
|
|
100
|
+
return changed ? last : null; // null when nothing changed (matches the provider contract)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id: "github",
|
|
105
|
+
readTier: read,
|
|
106
|
+
ensureTier: async (p, tier, siloArg) => {
|
|
107
|
+
const existing = await read(p, tier, siloArg);
|
|
108
|
+
if (Object.keys(existing).length > 0) return existing;
|
|
109
|
+
const seed = seedFor(p, tier);
|
|
110
|
+
await write(p, tier, seed, `seed ${tier.kind} memory`, siloArg);
|
|
111
|
+
return seed;
|
|
112
|
+
},
|
|
113
|
+
writeTier: write,
|
|
114
|
+
// Cheap version probe: a digest over the subtree LISTING (rel → blob sha) — one
|
|
115
|
+
// metadata call (listFiles), zero content downloads. Blob shas are content-addressed,
|
|
116
|
+
// so any file change/add/remove changes the digest. `null` for an empty/absent tier.
|
|
117
|
+
tierVersion: async (p, tier, siloArg) => {
|
|
118
|
+
const silo = requireSilo(siloArg);
|
|
119
|
+
const repo = silo.container!;
|
|
120
|
+
const dir = repoPath(silo, p, tier);
|
|
121
|
+
const token = await deps.mintToken(repo);
|
|
122
|
+
const shas = await deps.contents.listFiles(repo, dir, branch, token, makeSelector(normalizeSelection(silo)));
|
|
123
|
+
const entries = Object.entries(shas).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
124
|
+
if (entries.length === 0) return null;
|
|
125
|
+
const h = createHash("sha1");
|
|
126
|
+
for (const [rel, sha] of entries) h.update(`${rel}\n${sha}\n`);
|
|
127
|
+
return h.digest("hex");
|
|
128
|
+
},
|
|
129
|
+
// (OPE-64) `deleteFile` — the memory-admin MCP's per-file removal primitive
|
|
130
|
+
// (control/writes.ts `memory_delete_file`). Was deferred at the 2026-07-04
|
|
131
|
+
// memory-admin build because GitHubContents.deleteFile() (scm/contents.ts) returned
|
|
132
|
+
// void, not a commit sha; widened it there (its only caller is this module) rather
|
|
133
|
+
// than bundling a source-control-plane change into that build. `deleteFile` is
|
|
134
|
+
// otherwise NOT shared with `deleteTier` below (that one only needs "delete every
|
|
135
|
+
// listed path", never a version token back) — kept separate rather than making
|
|
136
|
+
// `deleteTier` a loop over this, so a single-file failure's error message stays
|
|
137
|
+
// about the one path the caller asked for.
|
|
138
|
+
deleteFile: async (p, tier, relPath, siloArg) => {
|
|
139
|
+
const silo = requireSilo(siloArg);
|
|
140
|
+
const repo = silo.container!;
|
|
141
|
+
const path = `${repoPath(silo, p, tier)}/${relPath}`;
|
|
142
|
+
const token = await deps.mintToken(repo);
|
|
143
|
+
const existing = await deps.contents.getFile(repo, path, branch, token);
|
|
144
|
+
if (!existing) return null; // already absent — idempotent, matches writeTier's "nothing changed"
|
|
145
|
+
const committer = deps.committer();
|
|
146
|
+
// Any failure here (permission, network, a stale sha — the file changed between
|
|
147
|
+
// the getFile above and this delete) THROWS (scm/contents.ts: only a 404 is
|
|
148
|
+
// swallowed, matching the "already absent" case, already handled above) and
|
|
149
|
+
// propagates to the caller uncaught — control/writes.ts's memory_delete_file
|
|
150
|
+
// catches it, surfaces a structured error, AND audits the failed attempt. Never a
|
|
151
|
+
// silent no-op reporting `deleted: true` while the file survives.
|
|
152
|
+
return deps.contents.deleteFile(repo, path, existing.sha, `delete ${relPath} from ${tier.kind} memory`, branch, token, committer);
|
|
153
|
+
},
|
|
154
|
+
deleteTier: async (p, tier, siloArg) => {
|
|
155
|
+
const silo = requireSilo(siloArg);
|
|
156
|
+
const repo = silo.container!;
|
|
157
|
+
const dir = repoPath(silo, p, tier);
|
|
158
|
+
const token = await deps.mintToken(repo);
|
|
159
|
+
const committer = deps.committer();
|
|
160
|
+
const shas = await deps.contents.listFiles(repo, dir, branch, token, makeSelector(normalizeSelection(silo)));
|
|
161
|
+
for (const [rel, sha] of Object.entries(shas)) {
|
|
162
|
+
await deps.contents.deleteFile(repo, `${dir}/${rel}`, sha, `delete ${tier.kind} memory`, branch, token, committer);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* THE PREFETCH INVARIANT for the github backing (OPE-390) — pure, so the bundle's own test can
|
|
170
|
+
* drive it without a live App.
|
|
171
|
+
*
|
|
172
|
+
* Domain-level GitHub access is THREE-layered: App installation = the mintable universe → the
|
|
173
|
+
* bundle `repos` grant (access/bundle.ts, additive union via the resolver) = what the domain may
|
|
174
|
+
* touch → a per-call token minted narrowed to the one named repo. Every scm tool enforces the
|
|
175
|
+
* MIDDLE layer fail-closed (S-PR-BOUNDARY live-proves the ungranted-repo refusal before any API
|
|
176
|
+
* call). This backing used to check only the OUTER layer — "is the App installed on the silo's
|
|
177
|
+
* repo" — and skip the grant layer entirely, which made a memory silo a way to read a repo the
|
|
178
|
+
* domain was never granted. One membership check against existing config closes it.
|
|
179
|
+
*/
|
|
180
|
+
export function githubMemoryContainerProblem(silo: MemorySilo, grants: DomainMemoryGrants): string | null {
|
|
181
|
+
const repo = silo.container;
|
|
182
|
+
if (typeof repo !== "string" || repo.length === 0) {
|
|
183
|
+
return `github-backed silos need container = "owner/repo" (memory/silo.ts)`;
|
|
184
|
+
}
|
|
185
|
+
if (grants.repos.includes(repo)) return null;
|
|
186
|
+
return (
|
|
187
|
+
`container "${repo}" is NOT in this domain's granted repos [${grants.repos.join(", ") || "none"}] — ` +
|
|
188
|
+
`a memory projection is read access, and the scm tools already refuse an ungranted repo fail-closed, so ` +
|
|
189
|
+
`add "${repo}" to a bundle bound to this domain (bundle.repos) or point the silo at a granted repo.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
type GitHubMemoryApp = Pick<NonNullable<ReturnType<typeof getGitHubApp>>, "tokensForRepos" | "committer">;
|
|
194
|
+
|
|
195
|
+
/** Narrow construction seam for the offline registration test. Production supplies nothing. */
|
|
196
|
+
export interface GitHubMemoryRegistrationDeps {
|
|
197
|
+
readonly app?: GitHubMemoryApp;
|
|
198
|
+
readonly contents?: GitHubContents;
|
|
199
|
+
readonly register?: typeof registerProvider;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Register the github memory backing from the configured GitHub App plane. No-op (returns false)
|
|
204
|
+
* when the App isn't configured — a deployment that doesn't use git-backed memory simply never
|
|
205
|
+
* references the "github" provider.
|
|
206
|
+
*
|
|
207
|
+
* The per-repo installation token is held by the SHARED always-warm seam (credentials/warm.ts,
|
|
208
|
+
* OPE-134): 1h `re-mint` rhythm, ~1 min margin, per-repo single-flight (a memory write touching
|
|
209
|
+
* several files no longer races itself to the token endpoint) and a LOUD
|
|
210
|
+
* `github.token_mint_failed` audit row when the mint fails — previously an App whose installation
|
|
211
|
+
* had been revoked made every memory read/write throw with nothing durable recording why.
|
|
212
|
+
*
|
|
213
|
+
* The App installation token is an APP-PRINCIPAL credential by construction — there is no user
|
|
214
|
+
* subject anywhere on this path — which is what the capability's `subject` field declares.
|
|
215
|
+
*/
|
|
216
|
+
export function registerGitHubMemory(deps: GitHubMemoryRegistrationDeps = {}): boolean {
|
|
217
|
+
const app = deps.app ?? getGitHubApp();
|
|
218
|
+
if (!app) return false;
|
|
219
|
+
const memoryToken = warmCredential<string>({
|
|
220
|
+
name: "github.memory-installation-token",
|
|
221
|
+
platform: "github",
|
|
222
|
+
auditAction: "github.token_mint_failed",
|
|
223
|
+
refresh: "re-mint",
|
|
224
|
+
refreshMarginMs: 60_000,
|
|
225
|
+
fallbackTtlSeconds: 3600,
|
|
226
|
+
key: (repo) => repo, // "owner/name" — non-secret
|
|
227
|
+
mint: async (repo) => {
|
|
228
|
+
const out = await app.tokensForRepos([repo], MEMORY_WRITE_PERMISSIONS);
|
|
229
|
+
const owner = repo.slice(0, repo.indexOf("/"));
|
|
230
|
+
const minted = out.tokens.get(owner);
|
|
231
|
+
if (!minted) {
|
|
232
|
+
throw new Error(`memory repo "${repo}" has no linked GitHub App installation (scm config).`);
|
|
233
|
+
}
|
|
234
|
+
const parsed = Date.parse(minted.expiresAt);
|
|
235
|
+
return { value: minted.token, expiresAtMs: Number.isFinite(parsed) ? parsed : undefined };
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const provider = makeGitHubProvider({
|
|
239
|
+
contents: deps.contents ?? new GitHubContents(),
|
|
240
|
+
committer: () => app.committer(),
|
|
241
|
+
mintToken: (repo: string) => memoryToken.get(repo),
|
|
242
|
+
});
|
|
243
|
+
(deps.register ?? registerProvider)(provider);
|
|
244
|
+
declareGitHubMemoryCapability();
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Declare the capability WITHOUT wiring the live provider — split out so the invariant is
|
|
250
|
+
* enforceable on a deployment whose App plane is unconfigured (config validation must still refuse
|
|
251
|
+
* an ungranted github silo, rather than going quiet because no credential exists yet) and so the
|
|
252
|
+
* bundle's offline test can register it without a GitHub App.
|
|
253
|
+
*/
|
|
254
|
+
export function declareGitHubMemoryCapability(): void {
|
|
255
|
+
registerConnectorMemory({
|
|
256
|
+
provider: "github",
|
|
257
|
+
connector: "github",
|
|
258
|
+
// The files ARE the source of truth here (a repo subtree), so this backing is writable —
|
|
259
|
+
// unlike the Notion projection, whose files are a cache and whose writes go through Notion.
|
|
260
|
+
mode: "read-write",
|
|
261
|
+
subject: "app-principal",
|
|
262
|
+
containerLabel: 'a repo, "owner/repo"',
|
|
263
|
+
assertContainerGranted: githubMemoryContainerProblem,
|
|
264
|
+
});
|
|
265
|
+
}
|
package/agent/ops.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// registerGithubOps — the ops-plane half of the github bundle's boot wiring: the telemetry
|
|
2
|
+
// credential-presence probe (OPE-495, the OPE-488 follow-up). Split into its own file so the
|
|
3
|
+
// STANDALONE ops MCP entry (repo-root scripts/ops-mcp.ts, which serves preflight/quickstart with
|
|
4
|
+
// no agent boot) can register it without pulling the whole channel graph — the same split slack
|
|
5
|
+
// and teams use.
|
|
6
|
+
//
|
|
7
|
+
// This file (not core) owns the credential-probe use of OE_GITHUB_APP_ID. The env var itself
|
|
8
|
+
// stays known to core too (packages/core/agent/scm/config.ts) — the SCM subsystem is a SEPARATE
|
|
9
|
+
// concern that stays in core by doctrine (see register.ts's header) — so this is not a
|
|
10
|
+
// zero-residue fingerprint, only the telemetry probe's own home.
|
|
11
|
+
|
|
12
|
+
import { registerSurfaceCredentialProbe } from "@open-everywhere/core/agent/adapter-api.js";
|
|
13
|
+
|
|
14
|
+
/** Register the github ops-plane arms (telemetry presence probe). */
|
|
15
|
+
export function registerGithubOps(): void {
|
|
16
|
+
registerSurfaceCredentialProbe("github", () => Boolean(process.env.OE_GITHUB_APP_ID));
|
|
17
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// GITHUB's event inventory (OPE-1612). See the core module's header
|
|
2
|
+
// (packages/core/agent/routines/provider-events.ts) for what the three routing states mean.
|
|
3
|
+
//
|
|
4
|
+
// Every `event` name below is a row of GitHub's own "Webhook events and payloads" index, which the
|
|
5
|
+
// `doc` field cites. The scope column is what `routines/github-normalizer.ts` proves: the `tenant`
|
|
6
|
+
// step is the repo OWNER and the `container` step is `owner/name` on EVERY row, so the rows below
|
|
7
|
+
// list only what each event adds BELOW the container — that is the part that varies, and the part a
|
|
8
|
+
// routine can address about the thing that happened.
|
|
9
|
+
//
|
|
10
|
+
// 🔴 THE `entity` STEP IS THE SUBJECT `normalizeGithubEvent` FOUND, and the chain it searches
|
|
11
|
+
// (`pull_request` → `issue` → `check_run` → `release` → `workflow_run`) is why `push`,
|
|
12
|
+
// `check_suite` and `workflow_job` are envelope-only: their subject object is under a key that chain
|
|
13
|
+
// does not name, so nothing below the repo is proven. That is a real gap, not a vendor limit, and
|
|
14
|
+
// the notes say so. `installation` is a different case and is `none`, not `envelope`: it names no
|
|
15
|
+
// repository at all, so the read-grant check refuses every delivery and nothing can ever fire.
|
|
16
|
+
//
|
|
17
|
+
// 🔴 THIS LIST IS REVIEWED, NOT EXHAUSTIVE. GitHub's index carries far more events than the rows
|
|
18
|
+
// below — `installation_repositories`, `star`, `fork`, `deployment_status` and many more. A row
|
|
19
|
+
// reaches this file when somebody read the event and decided what a routine could do with it. So an
|
|
20
|
+
// ABSENT event means "nobody has reviewed it here", never "the vendor does not send it"; adding one
|
|
21
|
+
// is a doc read plus a routing decision, not a guess.
|
|
22
|
+
|
|
23
|
+
import type { ProviderEventDeclaration } from "@open-everywhere/core/agent/adapter-api.js";
|
|
24
|
+
|
|
25
|
+
export const GITHUB_PROVIDER_EVENTS: ProviderEventDeclaration = {
|
|
26
|
+
source: "github",
|
|
27
|
+
doc: "https://docs.github.com/en/webhooks/webhook-events-and-payloads",
|
|
28
|
+
events: [
|
|
29
|
+
{
|
|
30
|
+
event: "issues",
|
|
31
|
+
routing: "mapped",
|
|
32
|
+
match: [],
|
|
33
|
+
scope: ["entity"],
|
|
34
|
+
note: "an issue was opened, closed, labelled, assigned…; the entity step is the issue number, so the fire re-fetches the issue itself",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
event: "issue_comment",
|
|
38
|
+
routing: "mapped",
|
|
39
|
+
match: [],
|
|
40
|
+
scope: ["entity", "sub"],
|
|
41
|
+
note: "a comment on an issue or on a pull request's timeline; the sub step is the comment id",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
event: "pull_request",
|
|
45
|
+
routing: "mapped",
|
|
46
|
+
match: [],
|
|
47
|
+
scope: ["entity"],
|
|
48
|
+
note: "a pull request was opened, synchronised, merged…; the entity step is the pull-request number",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
event: "pull_request_review",
|
|
52
|
+
routing: "mapped",
|
|
53
|
+
match: [],
|
|
54
|
+
scope: ["entity"],
|
|
55
|
+
note: "a review was submitted, edited or dismissed; the review itself is not addressable — the payload names it `review`, not `comment`",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
event: "pull_request_review_comment",
|
|
59
|
+
routing: "mapped",
|
|
60
|
+
match: [],
|
|
61
|
+
scope: ["entity", "sub"],
|
|
62
|
+
note: "an inline comment on a line of a pull request's diff; the sub step is the comment id",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
event: "push",
|
|
66
|
+
routing: "envelope",
|
|
67
|
+
match: [],
|
|
68
|
+
scope: [],
|
|
69
|
+
note: "commits were pushed to a ref; a push has no entity to fetch, and the ref is not mapped — a routine can match the repo and the sender only",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
event: "check_run",
|
|
73
|
+
routing: "mapped",
|
|
74
|
+
match: [],
|
|
75
|
+
scope: ["entity"],
|
|
76
|
+
note: "one check's state changed; the entity step is GitHub's opaque run id, so it narrows a subscription but is not re-fetchable",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
event: "check_suite",
|
|
80
|
+
routing: "envelope",
|
|
81
|
+
match: [],
|
|
82
|
+
scope: [],
|
|
83
|
+
note: "a whole suite of checks completed; unmapped — the payload's subject sits under `check_suite`, which the normalizer's subject chain does not name",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
event: "workflow_run",
|
|
87
|
+
routing: "mapped",
|
|
88
|
+
match: ["branch", "status", "conclusion", "workflow"],
|
|
89
|
+
scope: ["entity"],
|
|
90
|
+
note: "an Actions run started or finished; the four keys are what makes `a red build on our main` expressible — `.scope.container == \"acme/web\" and .fields.conclusion == \"failure\" and .fields.branch == \"main\"`. The container clause is REQUIRED, not decoration: a github subscription that narrows by nothing is refused at create time",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
event: "workflow_job",
|
|
94
|
+
routing: "envelope",
|
|
95
|
+
match: [],
|
|
96
|
+
scope: [],
|
|
97
|
+
note: "one job inside an Actions run changed state; unmapped — `workflow_run` is the run-level event and carries the same conclusion for the whole run",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
event: "release",
|
|
101
|
+
routing: "mapped",
|
|
102
|
+
match: [],
|
|
103
|
+
scope: ["entity"],
|
|
104
|
+
note: "a release was published or edited; the entity step is GitHub's opaque release id, so it narrows a subscription but is not re-fetchable",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
event: "installation",
|
|
108
|
+
routing: "none",
|
|
109
|
+
match: [],
|
|
110
|
+
scope: [],
|
|
111
|
+
note: "the App was installed, suspended or removed; the normalizer accepts it, but the payload names `repositories` and never `repository`, so it proves NO container step and the read-grant check refuses every delivery fail-closed — nothing can ever fire. The self-registering install leg reads it instead",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// registerGithubProvider — the ONE boot entry of @open-everywhere/provider-github. The app's
|
|
2
|
+
// adapter-wiring module (agent/lib/register-adapters.ts) imports this STATICALLY and calls it once
|
|
3
|
+
// at boot; every seam the github bundle implements registers here and nowhere else. Static imports
|
|
4
|
+
// only: eve bundles the agent definition and its whole import graph into one rolldown chunk — a
|
|
5
|
+
// literal dynamic import() code-splits and trips eve's "expected one authored module" invariant.
|
|
6
|
+
//
|
|
7
|
+
// SCOPE — CHAT SURFACE ONLY. The github bundle takes the conversational surface (channels/github.ts
|
|
8
|
+
// + the ambient supervisor + identity + post arm). The SCM subsystem STAYS IN CORE by doctrine (the
|
|
9
|
+
// single SCM implementation until a second SCM needs the abstraction — docs/extending/adapter-model.md):
|
|
10
|
+
// the githubEntityAdapter (scm/issue-actions.ts) and every PR/SCM tool (open_pull_request,
|
|
11
|
+
// subscribe_pr, checkout_repo, list_repos, add_label, assign, …) plus the config-repo PR mechanism
|
|
12
|
+
// (credentials/config-pr-github.ts) is NOT part
|
|
13
|
+
// of this bundle — they register from core exactly as before.
|
|
14
|
+
//
|
|
15
|
+
// What github registers (and what it deliberately does NOT):
|
|
16
|
+
// • identity mapper — bindGitHub (chat-surface plane; channels/github-identity.ts)
|
|
17
|
+
// • capability manifest — { midTurnPost: true, reactions: true, closingProseDropped: true }:
|
|
18
|
+
// the arm mints its own installation token and posts/edits/reacts on the
|
|
19
|
+
// issue/PR timeline itself, so every mid-turn verb runs through `reply` /
|
|
20
|
+
// `edit_reply` / `react` rather than a host-side supervisor post (OPE-811,
|
|
21
|
+
// OPE-862)
|
|
22
|
+
// • post arm — githubPostArm (mid-turn + unified-reply installation-token comment,
|
|
23
|
+
// editMessage, react, + redrive)
|
|
24
|
+
// • wake-origin codec — the stored-data github arm (channels/github-origin-codec.ts)
|
|
25
|
+
// • read adapter — githubReadAdapter (channels/github-read.ts, OPE-958): the issue-or-PR
|
|
26
|
+
// conversation for `read_channel` and a PR review thread for
|
|
27
|
+
// `read_thread`, on the brokered read-scoped token. The READ grain is
|
|
28
|
+
// the conversation, not the repo — a repo timeline is not a room
|
|
29
|
+
// anybody wrote in
|
|
30
|
+
// • memory backing — the github MemoryProvider + its connector-memory capability
|
|
31
|
+
// (agent/memory/memory.ts). MOVED here from core at OPE-390: a
|
|
32
|
+
// connector-owned backing belongs to the package that owns the
|
|
33
|
+
// connector; core keeps only the first-party postgres/sqlite ones.
|
|
34
|
+
// • surface message ops — githubMessageOps (postText/editMessage; OPE-899): the stop
|
|
35
|
+
// acknowledgement's transport (turn-anchor.ts's `postSurfaceText`).
|
|
36
|
+
// No postFiles (no file-attach API) and no setReaction (no raised hand
|
|
37
|
+
// in GitHub's fixed reaction set) — channels/github-message-ops.ts.
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
registerIdentityMapper,
|
|
41
|
+
registerCapabilityManifest,
|
|
42
|
+
registerSurfaceParity,
|
|
43
|
+
registerProviderEvents,
|
|
44
|
+
registerReactionSurface,
|
|
45
|
+
registerPostArm,
|
|
46
|
+
registerWakeOriginCodec,
|
|
47
|
+
registerProviderDocs,
|
|
48
|
+
registerReadAdapter,
|
|
49
|
+
registerSeatCatchUp,
|
|
50
|
+
registerWebhookVerifier,
|
|
51
|
+
registerSurfaceMessageOps,
|
|
52
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
53
|
+
import { PROVIDER_DOCS } from "./docs-generated.js";
|
|
54
|
+
import { GITHUB_SURFACE_PARITY } from "./surface-parity.js";
|
|
55
|
+
import { GITHUB_PROVIDER_EVENTS } from "./provider-events.js";
|
|
56
|
+
import { registerGithubOps } from "./ops.js";
|
|
57
|
+
import { registerGitHubMemory, declareGitHubMemoryCapability } from "./memory/memory.js";
|
|
58
|
+
import { bindGitHub } from "./channels/github-identity.js";
|
|
59
|
+
import { githubPostArm } from "./channels/github-post-arm.js";
|
|
60
|
+
import { githubWakeOriginCodec } from "./channels/github-origin-codec.js";
|
|
61
|
+
import { githubWebhookVerifier } from "./channels/github-webhook-verifier.js";
|
|
62
|
+
import { githubReadAdapter } from "./channels/github-read.js";
|
|
63
|
+
import { githubSeatCatchUpFetch, GITHUB_TIMESTAMP_PRECISION_MS } from "./channels/github-catch-up.js";
|
|
64
|
+
import { githubMessageOps } from "./channels/github-message-ops.js";
|
|
65
|
+
|
|
66
|
+
/** Perform ALL of the github bundle's chat-surface registrations. Idempotent (each register call
|
|
67
|
+
* replaces); cheap and side-effect-free beyond the registry writes. */
|
|
68
|
+
export function registerGithubProvider(): void {
|
|
69
|
+
registerIdentityMapper("github", bindGitHub);
|
|
70
|
+
// Reply surface (OPE-811): mid-turn post is the arm's own installation-token issue/PR-timeline
|
|
71
|
+
// comment (githubPostArm.dispatchReply) — no eve proactive helper needed ⇒ midTurnPost true.
|
|
72
|
+
// reactions (OPE-862): the arm's `react` member posts to GitHub's issue-reactions endpoint
|
|
73
|
+
// (mapping the model's bare emoji name onto GitHub's fixed content enum) ⇒ true.
|
|
74
|
+
// closingProseDropped (OPE-811): a container-supervisor turn runs the message-tool-only gate
|
|
75
|
+
// live, so its turn-final text is dropped and `reply` is the only route out.
|
|
76
|
+
registerCapabilityManifest("github", { midTurnPost: true, reactions: true, closingProseDropped: true });
|
|
77
|
+
// The OPE-1499 parity ledger: every capability the chat-surface contract names, answered
|
|
78
|
+
// `supported` or `{ unsupported: "<reason>" }`. Gates nothing at runtime — `lint:surface-parity`
|
|
79
|
+
// reds on an unanswered row, and the contract's §7.1/§7.1.1/§7.2 tables are generated from it.
|
|
80
|
+
registerSurfaceParity("github", GITHUB_SURFACE_PARITY);
|
|
81
|
+
// The OPE-1612 event inventory: every vendor event that could wake a routine, and how far the
|
|
82
|
+
// normalizer plane carries each one. Gates nothing at runtime — `lint:provider-events` reds a
|
|
83
|
+
// bundle that declares none or that promises a match key the normalizer does not declare, and
|
|
84
|
+
// the manual's inventory table is generated from it.
|
|
85
|
+
registerProviderEvents("github", GITHUB_PROVIDER_EVENTS);
|
|
86
|
+
registerPostArm(githubPostArm);
|
|
87
|
+
registerWakeOriginCodec("github", githubWakeOriginCodec);
|
|
88
|
+
// Read plane (OPE-958): the sixth surface's arm of the uniform chat read. It serves the SEAT's
|
|
89
|
+
// `read_channel` / `read_thread` and — under the domain's repo grant — the worker's
|
|
90
|
+
// `read_work_channel` / `read_work_thread`, because both run the same neutral engine over this
|
|
91
|
+
// one adapter.
|
|
92
|
+
registerReadAdapter(githubReadAdapter);
|
|
93
|
+
// THE SEAT CATCH-UP FETCH (OPE-1726): the timeline comments this issue/PR collected since the
|
|
94
|
+
// seat's last wake. Core's shared path applies the bounds, the admission policy and the cap.
|
|
95
|
+
// The surface DECLARES the unit its own message stamps carry, and core widens both catch-up
|
|
96
|
+
// bounds by it (OPE-1847). A wrong or missing unit bounds the seat out of its own window.
|
|
97
|
+
registerSeatCatchUp("github", githubSeatCatchUpFetch, { timestampPrecisionMs: GITHUB_TIMESTAMP_PRECISION_MS });
|
|
98
|
+
// Surface message ops (OPE-899): postText/editMessage over the SAME issue/PR-timeline comment
|
|
99
|
+
// transport the post arm uses, so the stop acknowledgement (turn-anchor.ts's `postSurfaceText`)
|
|
100
|
+
// now has a place to land on GitHub. No `postFiles` (no attach-a-file-to-a-comment API) and no
|
|
101
|
+
// `setReaction` (GitHub's fixed 8-value reaction set has no raised hand) — see
|
|
102
|
+
// channels/github-message-ops.ts's header.
|
|
103
|
+
registerSurfaceMessageOps(githubMessageOps);
|
|
104
|
+
// REACTION-FEEDBACK INGEST (OPE-1502) — a POLL, not a delivery: GitHub pushes no reaction webhook,
|
|
105
|
+
// so the front reads the reactions on the agent's own comments when a delivery names their
|
|
106
|
+
// conversation (channels/github-reaction-feedback.ts).
|
|
107
|
+
registerReactionSurface("github");
|
|
108
|
+
// Webhook-verifier plane (OPE-164): GitHub's `x-hub-signature-256` scheme, so a GENERIC hook can
|
|
109
|
+
// declare `source: "github"` and be verified the way GitHub signs. Changes no existing route —
|
|
110
|
+
// the native github webhook path keeps its own inline gate.
|
|
111
|
+
registerWebhookVerifier(githubWebhookVerifier);
|
|
112
|
+
// Shipped-manual plane (OPE-138): this bundle's own setup runbook (docs/deploy/surfaces/github.md,
|
|
113
|
+
// compiled into ./docs-generated.ts because docs/ is not on disk at runtime). De-select the bundle
|
|
114
|
+
// and the module leaves the import graph; registerProviderDocs re-checks the selection gate anyway.
|
|
115
|
+
registerProviderDocs("github", PROVIDER_DOCS);
|
|
116
|
+
// Ops plane (OPE-495): the telemetry credential-presence probe. Split into ./ops.js so the
|
|
117
|
+
// STANDALONE ops MCP entry (repo-root scripts/ops-mcp.ts, which serves preflight/quickstart with
|
|
118
|
+
// no agent boot) registers it without pulling the whole channel graph — the same split slack and
|
|
119
|
+
// teams use.
|
|
120
|
+
registerGithubOps();
|
|
121
|
+
|
|
122
|
+
// The MEMORY backing (OPE-390). Two calls on purpose:
|
|
123
|
+
// • registerGitHubMemory() wires the live provider off the GitHub App plane and is a no-op
|
|
124
|
+
// (false) when no App is configured — a deployment that never uses git-backed memory pays
|
|
125
|
+
// nothing and never resolves the "github" provider id.
|
|
126
|
+
// • declareGitHubMemoryCapability() runs REGARDLESS, because the prefetch invariant is a
|
|
127
|
+
// CONFIG check: a silo reaching past its domain's repo grants must go red at config load on
|
|
128
|
+
// a deployment whose App plane happens to be unconfigured too, not go quiet.
|
|
129
|
+
if (!registerGitHubMemory()) declareGitHubMemoryCapability();
|
|
130
|
+
}
|