@open-everywhere/provider-github 0.8.0 → 0.9.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/agent/app-manifest.ts +39 -17
- package/agent/channels/github-catch-up.ts +9 -4
- package/agent/channels/github-credentials.ts +41 -0
- package/agent/channels/github-engagement.ts +1 -1
- package/agent/channels/github-event-reach.ts +9 -4
- package/agent/channels/github-post-arm.ts +15 -12
- package/agent/channels/github-reaction-feedback.ts +2 -2
- package/agent/channels/github.ts +58 -14
- package/agent/docs-generated.ts +42 -23
- package/agent/memory/memory.ts +13 -12
- package/agent/ops.ts +9 -6
- package/dist/agent/app-manifest.d.ts +16 -3
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +32 -17
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/github-catch-up.d.ts.map +1 -1
- package/dist/agent/channels/github-catch-up.js +9 -5
- package/dist/agent/channels/github-catch-up.js.map +1 -1
- package/dist/agent/channels/github-credentials.d.ts +10 -0
- package/dist/agent/channels/github-credentials.d.ts.map +1 -0
- package/dist/agent/channels/github-credentials.js +40 -0
- package/dist/agent/channels/github-credentials.js.map +1 -0
- package/dist/agent/channels/github-engagement.d.ts +1 -1
- package/dist/agent/channels/github-engagement.js +1 -1
- package/dist/agent/channels/github-event-reach.d.ts +3 -2
- package/dist/agent/channels/github-event-reach.d.ts.map +1 -1
- package/dist/agent/channels/github-event-reach.js +8 -4
- package/dist/agent/channels/github-event-reach.js.map +1 -1
- package/dist/agent/channels/github-post-arm.d.ts.map +1 -1
- package/dist/agent/channels/github-post-arm.js +15 -14
- package/dist/agent/channels/github-post-arm.js.map +1 -1
- package/dist/agent/channels/github-reaction-feedback.d.ts +1 -1
- package/dist/agent/channels/github-reaction-feedback.js +2 -2
- package/dist/agent/channels/github-reaction-feedback.js.map +1 -1
- package/dist/agent/channels/github.d.ts +33 -0
- package/dist/agent/channels/github.d.ts.map +1 -1
- package/dist/agent/channels/github.js +57 -13
- package/dist/agent/channels/github.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +42 -23
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/memory/memory.d.ts +1 -0
- package/dist/agent/memory/memory.d.ts.map +1 -1
- package/dist/agent/memory/memory.js +12 -12
- package/dist/agent/memory/memory.js.map +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +9 -6
- package/dist/agent/ops.js.map +1 -1
- package/package.json +6 -2
package/agent/app-manifest.ts
CHANGED
|
@@ -108,11 +108,34 @@ export function githubAppManifest(opts: {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** The env names a created App fills, for each shape. The OE_
|
|
112
|
-
* bare
|
|
111
|
+
/** The env names a created App fills, for each shape — its SECRETS only. The OE_ key is the
|
|
112
|
+
* source-control plane's; the bare key is the conversational surface's (docs/deploy/surfaces/github.md).
|
|
113
|
+
* One App serves both. Which App it is (id, slug, client id, bot user id) is the `github.*` settings
|
|
114
|
+
* since OPE-2650, so no id is an env name here: see {@link githubAppSettings}. */
|
|
113
115
|
export function githubAppEnvNames(variant: GithubAppVariant): string[] {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
// ONE private-key name for both variants (OPE-2650 folded OE_GITHUB_APP_PRIVATE_KEY into it): the key
|
|
117
|
+
// and the webhook secret are operator secrets, so `secret_set` moves them from .env.local into the store.
|
|
118
|
+
return ["GITHUB_APP_PRIVATE_KEY", "GITHUB_WEBHOOK_SECRET"];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The PUBLIC settings a created App fills, setting path → value (OPE-2650). The source-control plane and
|
|
122
|
+
* the conversation surface read the one `github.appId`/`github.appSlug`. `github.conversation` arms the
|
|
123
|
+
* conversation surface, so it is `true` for the `full` App only: a `source-control` App subscribes to no
|
|
124
|
+
* comment event and must start no conversation. */
|
|
125
|
+
export function githubAppSettings(app: {
|
|
126
|
+
readonly variant: GithubAppVariant;
|
|
127
|
+
readonly id: string;
|
|
128
|
+
readonly slug: string;
|
|
129
|
+
readonly clientId: string;
|
|
130
|
+
readonly botUserId?: string;
|
|
131
|
+
}): Record<string, string | boolean> {
|
|
132
|
+
return {
|
|
133
|
+
"github.appId": app.id,
|
|
134
|
+
"github.appSlug": app.slug,
|
|
135
|
+
"github.appClientId": app.clientId,
|
|
136
|
+
...(app.botUserId === undefined ? {} : { "github.botUserId": app.botUserId }),
|
|
137
|
+
"github.conversation": app.variant === "full",
|
|
138
|
+
};
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
interface Conversion {
|
|
@@ -128,7 +151,7 @@ const API = "https://api.github.com";
|
|
|
128
151
|
const HEADERS = { accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28" };
|
|
129
152
|
|
|
130
153
|
/**
|
|
131
|
-
* Turn the redirect's code into this deployment's env. One POST to GitHub's conversion endpoint
|
|
154
|
+
* Turn the redirect's code into this deployment's env and settings. One POST to GitHub's conversion endpoint
|
|
132
155
|
* (it needs no credential: the code is the proof, and it is single-use and lives one hour), then a
|
|
133
156
|
* public read of the App's bot user for the commit e-mail. That second read is fail-soft: without
|
|
134
157
|
* it the plane falls back to the App id (docs/capabilities/source-control.md).
|
|
@@ -159,25 +182,23 @@ export async function exchangeGithubManifestCode(opts: {
|
|
|
159
182
|
.map(([k]) => k);
|
|
160
183
|
if (missing.length > 0) throw new Error(`GitHub's reply lacked ${missing.join(", ")}`);
|
|
161
184
|
const env: Record<string, string> = {
|
|
162
|
-
|
|
163
|
-
OE_GITHUB_APP_SLUG: slug!,
|
|
164
|
-
OE_GITHUB_APP_PRIVATE_KEY: pem!,
|
|
165
|
-
OE_GITHUB_APP_CLIENT_ID: clientId!,
|
|
185
|
+
GITHUB_APP_PRIVATE_KEY: pem!,
|
|
166
186
|
GITHUB_WEBHOOK_SECRET: webhookSecret!,
|
|
167
187
|
};
|
|
168
|
-
|
|
169
|
-
env.GITHUB_APP_ID = id!;
|
|
170
|
-
env.GITHUB_APP_SLUG = slug!;
|
|
171
|
-
env.GITHUB_APP_PRIVATE_KEY = pem!;
|
|
172
|
-
}
|
|
188
|
+
let botUserId: string | undefined;
|
|
173
189
|
try {
|
|
174
190
|
const bot = await opts.fetchFn(`${API}/users/${encodeURIComponent(`${slug}[bot]`)}`, { headers: HEADERS });
|
|
175
191
|
const botId = bot.ok ? str(((await bot.json()) as { id?: unknown }).id) : undefined;
|
|
176
|
-
if (botId !== undefined && /^\d+$/.test(botId))
|
|
192
|
+
if (botId !== undefined && /^\d+$/.test(botId)) botUserId = botId;
|
|
177
193
|
} catch {
|
|
178
194
|
// Fail-soft, see above.
|
|
179
195
|
}
|
|
180
|
-
return {
|
|
196
|
+
return {
|
|
197
|
+
env,
|
|
198
|
+
settings: githubAppSettings({ variant: opts.variant, id: id!, slug: slug!, clientId: clientId!, ...(botUserId === undefined ? {} : { botUserId }) }),
|
|
199
|
+
appName: str(body.name) ?? slug!,
|
|
200
|
+
installUrl: `https://github.com/apps/${encodeURIComponent(slug!)}/installations/new`,
|
|
201
|
+
};
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
const isVariant = (v: string): v is GithubAppVariant => GITHUB_APP_VARIANTS.some((x) => x.name === v);
|
|
@@ -204,7 +225,8 @@ export const GITHUB_APP_CREATE_FLOW: SurfaceAppCreateFlow = {
|
|
|
204
225
|
// App's one `installation` delivery. Before the redeploy it does not hold the App's webhook secret, so
|
|
205
226
|
// it refuses that delivery with a 401 and never learns the installation.
|
|
206
227
|
after: [
|
|
207
|
-
"
|
|
228
|
+
"Commit the returned settings to agent/settings/settings.ts as `github: { appId, appSlug, appClientId, botUserId, conversation }` (public values, safe in Git). Keep any other github key the file already sets.",
|
|
229
|
+
"Store each written name in the deployment's credential store: call the ops tool secret_set with names [\"GITHUB_APP_PRIVATE_KEY\", \"GITHUB_WEBHOOK_SECRET\"] (they are operator secrets, OPE-2650), then redeploy with the settings change.",
|
|
208
230
|
"Only after that redeploy, install the app on the repositories the agent works in: open the install link. Installing first sends the app's installation delivery to a deployment that does not hold the new webhook secret yet, and it refuses that delivery.",
|
|
209
231
|
"Optional: on the app's settings page, under Display information, upload the project mark as its logo. The manifest has no logo field.",
|
|
210
232
|
],
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
catchUpSender,
|
|
26
|
+
getSettings,
|
|
26
27
|
resolveSurfaceReadToken,
|
|
27
28
|
type NormalizedMessage,
|
|
28
29
|
type PrFetchFn,
|
|
@@ -88,12 +89,16 @@ export interface GitHubCatchUpDeps {
|
|
|
88
89
|
readonly selfLogin?: string;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// The GitHub App's comment author is `<app-slug>[bot]`; the deployment states its own slug in the
|
|
93
|
+
// `github.appSlug` setting (OPE-2650), and an unset one degrades to "our posts look like a bot's",
|
|
94
|
+
// which the admission policy still admits under `wide`. A getter, so each read sees the registered
|
|
95
|
+
// settings rather than the value at module load.
|
|
91
96
|
const liveDeps: GitHubCatchUpDeps = {
|
|
92
97
|
fetchFn: liveGithubReadFetch,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
get selfLogin(): string | undefined {
|
|
99
|
+
const slug = getSettings().github.appSlug;
|
|
100
|
+
return slug ? `${slug}[bot]` : undefined;
|
|
101
|
+
},
|
|
97
102
|
};
|
|
98
103
|
|
|
99
104
|
/** The fetch, with its seams exposed so the contract test drives it with no network. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// WHERE THE GITHUB BUNDLE'S SECRETS COME FROM (OPE-2650) — the App's private key and the webhook
|
|
2
|
+
// signing secret.
|
|
3
|
+
//
|
|
4
|
+
// Both are OPERATOR SECRETS: `oe secret set GITHUB_APP_PRIVATE_KEY`, `oe secret set
|
|
5
|
+
// GITHUB_WEBHOOK_SECRET`. Core reads them: the db credential store first, the env var of the same name
|
|
6
|
+
// as its warned on-ramp, each store lookup remembered for thirty seconds. The private key has ONE name
|
|
7
|
+
// for the scm plane and this bundle (core's `githubAppPrivateKey`); OPE-2650 folded the
|
|
8
|
+
// OE_GITHUB_APP_PRIVATE_KEY spelling into it.
|
|
9
|
+
//
|
|
10
|
+
// 🔴 EVE MUST BE HANDED BOTH. eve's `githubChannel` resolves `credentials.privateKey` and
|
|
11
|
+
// `credentials.webhookSecret`, and with neither it reads the GITHUB_APP_PRIVATE_KEY and
|
|
12
|
+
// GITHUB_WEBHOOK_SECRET env vars itself, so it would never see a stored value: the native route would
|
|
13
|
+
// 401 every delivery on a deployment that holds the secrets in the store only.
|
|
14
|
+
// {@link githubEveCredentials} is that hand-over. Functions, not values: the read is async, and a value
|
|
15
|
+
// read at module load would pin whatever was true at boot.
|
|
16
|
+
|
|
17
|
+
import { githubAppPrivateKey, operatorSecretOrUnset } from "@open-everywhere/core/agent/adapter-api.js";
|
|
18
|
+
|
|
19
|
+
/** The operator secret holding the webhook HMAC secret. */
|
|
20
|
+
const GITHUB_WEBHOOK_SECRET_NAME = "GITHUB_WEBHOOK_SECRET";
|
|
21
|
+
|
|
22
|
+
/** The webhook signing secret, or `undefined` when this deployment holds none (a fault reads as none,
|
|
23
|
+
* logged once — the route then fails closed). */
|
|
24
|
+
export async function githubWebhookSecret(): Promise<string | undefined> {
|
|
25
|
+
return await operatorSecretOrUnset(GITHUB_WEBHOOK_SECRET_NAME);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The `credentials` block both `githubChannel(...)` instances carry. Each arm throws eve's OWN message
|
|
29
|
+
* when nothing resolves, byte for byte. */
|
|
30
|
+
export const githubEveCredentials = {
|
|
31
|
+
privateKey: async (): Promise<string> => {
|
|
32
|
+
const key = await githubAppPrivateKey();
|
|
33
|
+
if (!key) throw new Error("githubChannel: GITHUB_APP_PRIVATE_KEY is required.");
|
|
34
|
+
return key;
|
|
35
|
+
},
|
|
36
|
+
webhookSecret: async (): Promise<string> => {
|
|
37
|
+
const secret = await githubWebhookSecret();
|
|
38
|
+
if (!secret) throw new Error("githubChannel: GITHUB_WEBHOOK_SECRET is required.");
|
|
39
|
+
return secret;
|
|
40
|
+
},
|
|
41
|
+
} as const;
|
|
@@ -286,7 +286,7 @@ async function liveMarkEngageSeen(org: string, key: string, atIso: string): Prom
|
|
|
286
286
|
* Is this comment MACHINE-AUTHORED — a GitHub App, a bot account, or this deployment's own reply?
|
|
287
287
|
*
|
|
288
288
|
* 🔴 ONE PREDICATE, AND IT MUST FAIL CLOSED ON AN UNRESOLVED SLUG. `githubBotMentioned` needs the
|
|
289
|
-
* app slug to recognise a mention at all, so before this seam a deployment with no `
|
|
289
|
+
* app slug to recognise a mention at all, so before this seam a deployment with no `github.appSlug`
|
|
290
290
|
* simply never woke natively. A plain comment now reaches the seam, and without the slug the app
|
|
291
291
|
* cannot tell its OWN reply from a person's — so "unknown" answers `true` and nothing is delivered,
|
|
292
292
|
* the same posture `classifyGitHubAmbientComment` takes with its own no-slug drop.
|
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
// dev-only `webhooks.allowUnverifiedGithub` hatch on. With neither, it 401s every delivery and no GitHub
|
|
7
7
|
// routine can fire. This probe asks the same two questions the gate asks, and reads the secret's PRESENCE only.
|
|
8
8
|
|
|
9
|
+
// The secret is an operator secret since OPE-2650, so the presence read is async and asks the store
|
|
10
|
+
// first, the env var as its on-ramp — the same read the gate makes.
|
|
11
|
+
|
|
9
12
|
import { getSettings } from "@open-everywhere/core/agent/adapter-api.js";
|
|
13
|
+
import { githubWebhookSecret } from "./github-credentials.js";
|
|
10
14
|
|
|
11
|
-
/** True when the native GitHub route can admit a delivery into the routine fan-out.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
/** True when the native GitHub route can admit a delivery into the routine fan-out. `secret` is a test
|
|
16
|
+
* seam; the live default is the gate's own read. */
|
|
17
|
+
export async function githubEventsReach(secret: () => Promise<string | undefined> = githubWebhookSecret): Promise<boolean> {
|
|
18
|
+
const value = await secret();
|
|
19
|
+
return (value !== undefined && value.length > 0) || getSettings().webhooks.allowUnverifiedGithub;
|
|
15
20
|
}
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
mintInstallationToken,
|
|
22
22
|
CONVERSATION_WRITE_PERMISSIONS,
|
|
23
23
|
loadGitHubAppConfig,
|
|
24
|
+
githubAppPrivateKey,
|
|
24
25
|
githubAuthMode,
|
|
26
|
+
getSettings,
|
|
25
27
|
resolveGitHubInstallationId,
|
|
26
28
|
warmCredential,
|
|
27
29
|
markNotApplied,
|
|
@@ -108,15 +110,16 @@ export function _resetGitHubInstallationTokenCache(): void {
|
|
|
108
110
|
installationToken.reset();
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
/** GitHub App credentials for the unified reply arm
|
|
112
|
-
*
|
|
113
|
-
|
|
113
|
+
/** GitHub App credentials for the unified reply arm. The App id is the `github.appId` setting (the scm
|
|
114
|
+
* plane's client id preferred as the JWT issuer when set), and the private key has ONE home since
|
|
115
|
+
* OPE-2650: the operator secret GITHUB_APP_PRIVATE_KEY, for both the scm plane and this surface. */
|
|
116
|
+
async function githubAppCreds(): Promise<{ appId: string; privateKeyPem: string; installations: Record<string, string> } | null> {
|
|
114
117
|
const scm = loadGitHubAppConfig();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const privateKeyPem =
|
|
118
|
-
if (!
|
|
119
|
-
return { appId, privateKeyPem, installations: {} };
|
|
118
|
+
const appId = scm ? (scm.clientId ?? scm.appId) : getSettings().github.appId;
|
|
119
|
+
if (!appId) return null;
|
|
120
|
+
const privateKeyPem = await githubAppPrivateKey();
|
|
121
|
+
if (!privateKeyPem) return null;
|
|
122
|
+
return { appId, privateKeyPem, installations: scm?.installations ?? {} };
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
/** Exported for the reaction-feedback poll (channels/github-reaction-feedback.ts, OPE-1502), which
|
|
@@ -127,19 +130,19 @@ export async function installationTokenFor(origin: GitHubOrigin): Promise<string
|
|
|
127
130
|
if (githubAuthMode() === "token") {
|
|
128
131
|
throw markNotApplied(new Error("reply couldn't post to GitHub — this deployment signs in with a personal access token, and the GitHub conversation surface needs the GitHub App."));
|
|
129
132
|
}
|
|
130
|
-
const creds = githubAppCreds();
|
|
133
|
+
const creds = await githubAppCreds();
|
|
131
134
|
if (!creds) {
|
|
132
135
|
// DEFINITIVE (OPE-825): no App credentials ⇒ no request ever left this host.
|
|
133
|
-
throw markNotApplied(new Error("reply couldn't post to GitHub — no App credentials (set
|
|
136
|
+
throw markNotApplied(new Error("reply couldn't post to GitHub — no App credentials (set the github.appId setting, and store the operator secret GITHUB_APP_PRIVATE_KEY)."));
|
|
134
137
|
}
|
|
135
138
|
// The wake's own (webhook-supplied) installation id first; then the SCM plane's composed map —
|
|
136
|
-
//
|
|
139
|
+
// the `github.installations` setting authoritative, `installation`-webhook-discovered installs augmenting it
|
|
137
140
|
// (scm/installations.ts), so an org that self-installed resolves without an env edit.
|
|
138
141
|
const installationId = origin.installationId !== undefined ? String(origin.installationId) : await resolveGitHubInstallationId(origin.owner, creds.installations);
|
|
139
142
|
if (!installationId) {
|
|
140
143
|
// DEFINITIVE (OPE-825): unresolved before any comment request is built — nothing left this host.
|
|
141
144
|
throw markNotApplied(new Error(
|
|
142
|
-
`reply couldn't post to GitHub — no installation id for owner "${origin.owner}" (the wake carried none,
|
|
145
|
+
`reply couldn't post to GitHub — no installation id for owner "${origin.owner}" (the wake carried none, the github.installations setting has no entry, and no installation webhook has registered one).`,
|
|
143
146
|
));
|
|
144
147
|
}
|
|
145
148
|
// Key by (installation, repo): the minted token is scoped to exactly [origin.repo], so a sibling
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
//
|
|
88
88
|
// ── IDENTITY
|
|
89
89
|
//
|
|
90
|
-
// "The agent's own comment" is `<
|
|
90
|
+
// "The agent's own comment" is `<github.appSlug>[bot]` — the login GitHub gives an App's comments,
|
|
91
91
|
// and the same self-identity channels/github-supervisor.ts drops its own replies by. 🔴 IT FAILS
|
|
92
92
|
// CLOSED: with no slug configured the arm polls nothing at all, because it cannot tell the agent's
|
|
93
93
|
// comments from anybody else's and reading every comment's reactions would record strangers' reactions
|
|
@@ -421,7 +421,7 @@ function tokenFor(target: GitHubReactionPollTarget): Promise<string> {
|
|
|
421
421
|
});
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
/** Production deps for the poll. `botLogin` is `<
|
|
424
|
+
/** Production deps for the poll. `botLogin` is `<github.appSlug>[bot]` — absent when the slug is
|
|
425
425
|
* unset, which is exactly the fail-closed case {@link routeGitHubReactionFeedback} refuses on. */
|
|
426
426
|
export function liveGitHubReactionFeedbackDeps(appSlug: string | undefined, fetchFn: PrFetchFn = liveGithubReadFetch): GitHubReactionFeedbackDeps {
|
|
427
427
|
const slug = appSlug?.trim();
|
package/agent/channels/github.ts
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
// 4. The INSTALLATION leg (installationDelivery below): the App's `installation` /
|
|
70
70
|
// `installation_repositories` events register (or revoke) the owner→installation_id entry the
|
|
71
71
|
// SCM plane mints tokens with, so a new org installing the App is self-service end-to-end
|
|
72
|
-
// instead of needing
|
|
72
|
+
// instead of needing a `github.installations` settings edit + redeploy. The settings map stays
|
|
73
73
|
// AUTHORITATIVE; discovered installs only augment it (scm/installations.ts).
|
|
74
74
|
//
|
|
75
75
|
// Point the GitHub App webhook at POST /eve/v1/github (content-type application/json),
|
|
@@ -175,9 +175,52 @@ import {
|
|
|
175
175
|
dispatchNativeWakeWithRetry,
|
|
176
176
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
177
177
|
import { githubAuthMode, withEndsTurnFinalChannel } from "@open-everywhere/core/agent/adapter-api.js";
|
|
178
|
+
import { githubEveCredentials, githubWebhookSecret } from "./github-credentials.js";
|
|
178
179
|
|
|
179
180
|
const ROUTE = "/eve/v1/github";
|
|
180
|
-
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The name the conversation surface answers to: the `github.appSlug` setting (was `GITHUB_APP_SLUG`,
|
|
184
|
+
* OPE-2650), or `undefined`. A function, read per call: a module-level read would run before the app
|
|
185
|
+
* registers its settings. `undefined` keeps the fail-closed loop guard — every ambient comment drops,
|
|
186
|
+
* and no @mention wakes.
|
|
187
|
+
*
|
|
188
|
+
* 🔴 THE SURFACE ARMS ON ITS OWN SETTING, `github.conversation` (OPE-2650). The source-control plane and
|
|
189
|
+
* this surface share ONE App id, ONE slug and ONE private key (the operator secret
|
|
190
|
+
* GITHUB_APP_PRIVATE_KEY). None of them says whether this App should start conversations: a
|
|
191
|
+
* `source-control` App holds all three too. So the arm is a reviewed line in Git. Off (shipped), a
|
|
192
|
+
* `source-control` App, or a token-mode deployment that moved to one and kept its repository webhook,
|
|
193
|
+
* starts no conversation from a comment. `surface_app_create` returns `github.conversation: true` for
|
|
194
|
+
* the `full` App only.
|
|
195
|
+
*/
|
|
196
|
+
export function githubConversationBotName(): string | undefined {
|
|
197
|
+
const github = getSettings().github;
|
|
198
|
+
if (!github.conversation) return undefined;
|
|
199
|
+
return github.appSlug || undefined;
|
|
200
|
+
}
|
|
201
|
+
const botName = githubConversationBotName;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Which App eve's channel speaks as, for BOTH githubChannel instances below (OPE-2650). The slug and
|
|
205
|
+
* the App id come from settings, and the private key and the webhook secret come from the operator
|
|
206
|
+
* secret store (`githubEveCredentials`, ./github-credentials.ts). All are lazy fields.
|
|
207
|
+
*
|
|
208
|
+
* 🔴 A FIELD LEFT UNDEFINED FALLS BACK TO ENV INSIDE EVE (`GITHUB_APP_SLUG`, `GITHUB_APP_ID`,
|
|
209
|
+
* `GITHUB_APP_PRIVATE_KEY`, `GITHUB_WEBHOOK_SECRET`), so every one is always passed. eve caches a
|
|
210
|
+
* resolved slug per instance and treats "" as unset. The App id resolver throws when the setting is
|
|
211
|
+
* unset, which is eve's own "is required" refusal.
|
|
212
|
+
*/
|
|
213
|
+
export const GITHUB_CHANNEL_APP = {
|
|
214
|
+
botName: (): string => botName() ?? "",
|
|
215
|
+
credentials: {
|
|
216
|
+
...githubEveCredentials,
|
|
217
|
+
appId: (): string => {
|
|
218
|
+
const id = getSettings().github.appId;
|
|
219
|
+
if (id === "") throw new Error("githubChannel: the github.appId setting is required.");
|
|
220
|
+
return id;
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
} as const;
|
|
181
224
|
|
|
182
225
|
function log(msg: string): void {
|
|
183
226
|
console.log(`[open-everywhere:routines] ${msg}`);
|
|
@@ -679,7 +722,7 @@ async function ambientDelivery(body: string, eventType: string, send: GitHubSend
|
|
|
679
722
|
// WINDOW-ADMISSION FLOOR = the operator VOUCH (identity/trust.ts — the SAME allowlist that gates
|
|
680
723
|
// the shared memory), NOT the repo-private flag. `ev.repoPrivate` is now a logged SIGNAL only.
|
|
681
724
|
const vouched = githubRepoVouched(ev.owner, ev.repo);
|
|
682
|
-
const cls = classifyGitHubAmbientComment(ev.comment,
|
|
725
|
+
const cls = classifyGitHubAmbientComment(ev.comment, botName(), { treatMentionAsDirect: singleWindow, vouched });
|
|
683
726
|
if (cls.addressing === "drop") {
|
|
684
727
|
slog(`comment on ${surfaceKey}#${ev.issueNumber} dropped: ${cls.reason} (private=${ev.repoPrivate === true}, vouched=${vouched})`);
|
|
685
728
|
return;
|
|
@@ -906,15 +949,16 @@ async function installationDelivery(body: string, eventType: string): Promise<vo
|
|
|
906
949
|
|
|
907
950
|
// --- supervisor-aware events (delegating every non-supervisor turn to eve's defaults) --
|
|
908
951
|
|
|
909
|
-
// A PRISTINE second githubChannel instance (same config axes that shape the default events:
|
|
910
|
-
//
|
|
952
|
+
// A PRISTINE second githubChannel instance (same config axes that shape the default events: the
|
|
953
|
+
// shared GITHUB_CHANNEL_APP slug, App id and credentials, and api/progress unset, matching the real channel
|
|
954
|
+
// below) is the source of
|
|
911
955
|
// eve's DEFAULT handlers. defineChannel compiles authored events into `adapter[<event>]`
|
|
912
956
|
// entries of shape `(data, adapterCtx) => authored(data, enriched(adapterCtx), callbackCtx)`;
|
|
913
957
|
// calling one from inside OUR authored handler (same ALS step scope) re-runs the enrichment
|
|
914
958
|
// harmlessly and rebuilds the callback context — i.e. the default runs EXACTLY as if we had
|
|
915
959
|
// never authored the key. This is how the #623 destructive-replacement axis is used safely:
|
|
916
960
|
// the mention path's checkout/reply/error behaviour is eve's own code, not a replica.
|
|
917
|
-
const pristine = githubChannel({
|
|
961
|
+
const pristine = githubChannel({ ...GITHUB_CHANNEL_APP });
|
|
918
962
|
type AdapterEventFn = (data: unknown, adapterCtx: unknown) => unknown;
|
|
919
963
|
const defaultEvent = (name: string): AdapterEventFn | undefined =>
|
|
920
964
|
(pristine as unknown as { adapter?: Record<string, AdapterEventFn> }).adapter?.[name];
|
|
@@ -991,13 +1035,13 @@ function nativeGitHubAuditPartition(state: GitHubChannelState): ChatAuditPartiti
|
|
|
991
1035
|
// can (a) keep the SAME mention semantics (githubBotMentioned mirrors eve's gate) and
|
|
992
1036
|
// (b) enrich the dispatched auth with the repo owner/name/privacy + actor association the
|
|
993
1037
|
// identity mapper needs — eve's own defaultGitHubAuth omits repo.private, which would peg
|
|
994
|
-
// every surface to fail-closed private/external. botName
|
|
995
|
-
//
|
|
1038
|
+
// every surface to fail-closed private/external. botName and the App id come from settings,
|
|
1039
|
+
// and the private key and the webhook secret from the operator secret store (GITHUB_CHANNEL_APP).
|
|
996
1040
|
// Exported for the provider integration test (drives the REAL ingress handler directly with a
|
|
997
1041
|
// seeded over-budget scope). The channel below wires it as `onComment`.
|
|
998
1042
|
export const githubOnComment = async (ctx: GitHubInboundContext, comment: GitHubComment): Promise<GitHubInboundResult> => {
|
|
999
1043
|
// Same wake rule as eve's built-in gate (that helper isn't publicly exported).
|
|
1000
|
-
const isMention = githubBotMentioned(comment.body,
|
|
1044
|
+
const isMention = githubBotMentioned(comment.body, botName(), comment.author);
|
|
1001
1045
|
// 🔴 THE §2.5 ENGAGED SEAM (OPE-1501) IS THE SECOND DOOR OF THIS GATE. Before it, a comment that
|
|
1002
1046
|
// was not an @mention returned here and the native leg answered exactly once per mention. Now a
|
|
1003
1047
|
// PLAIN comment in a container an @mention already ENGAGED is admitted too, and the seam's own
|
|
@@ -1016,7 +1060,7 @@ export const githubOnComment = async (ctx: GitHubInboundContext, comment: GitHub
|
|
|
1016
1060
|
// The author gate, in the seam's own vocabulary. `githubBotMentioned` already refuses a
|
|
1017
1061
|
// self-authored mention; a PLAIN comment reaches the seam with no such check, so an engaged
|
|
1018
1062
|
// container would otherwise admit the app's own reply and loop on itself.
|
|
1019
|
-
authorIsBot: githubMachineAuthored(comment.author?.login ?? ctx.sender.login, comment.author?.type ?? ctx.sender.type,
|
|
1063
|
+
authorIsBot: githubMachineAuthored(comment.author?.login ?? ctx.sender.login, comment.author?.type ?? ctx.sender.type, botName()),
|
|
1020
1064
|
// 🔴 THE REACH FACT (see `githubEngagedFollowUpReach`): an INLINE review comment shares its
|
|
1021
1065
|
// pull request's anchor, so the non-mention arm refuses one. A mention inside a review thread
|
|
1022
1066
|
// still wakes — it never reaches that arm.
|
|
@@ -1166,7 +1210,7 @@ export const githubOnComment = async (ctx: GitHubInboundContext, comment: GitHub
|
|
|
1166
1210
|
};
|
|
1167
1211
|
|
|
1168
1212
|
const native: GitHubChannel = githubChannel({
|
|
1169
|
-
|
|
1213
|
+
...GITHUB_CHANNEL_APP,
|
|
1170
1214
|
// 🔴 QUEUE, NEVER STEER (OPE-412) — eve's default `steer` cancels the turn in flight when a
|
|
1171
1215
|
// message arrives. Same reasoning as the Slack channel; see slack.ts for the full note.
|
|
1172
1216
|
turnPolicy: "queue",
|
|
@@ -1378,7 +1422,7 @@ const native: GitHubChannel = githubChannel({
|
|
|
1378
1422
|
// delegate the request UNCHANGED to the native handler for the @mention/session behaviour.
|
|
1379
1423
|
// The request body can only be read once, so we buffer it and hand the native handler a fresh
|
|
1380
1424
|
// Request carrying the same body + headers (it re-verifies the signature itself over that
|
|
1381
|
-
// body — same GITHUB_WEBHOOK_SECRET).
|
|
1425
|
+
// body — the same GITHUB_WEBHOOK_SECRET operator secret, handed to eve as `githubEveCredentials`).
|
|
1382
1426
|
type HttpHandler = HttpRouteDefinition<GitHubChannelState>["handler"];
|
|
1383
1427
|
function wrapRoute(inner: HttpHandler): HttpHandler {
|
|
1384
1428
|
return async (req, args) => {
|
|
@@ -1387,7 +1431,7 @@ function wrapRoute(inner: HttpHandler): HttpHandler {
|
|
|
1387
1431
|
// unauthenticated delivery is rejected for the whole route (the native handler would
|
|
1388
1432
|
// 401 it too). The dev escape hatch keeps the fan-out testable without a secret.
|
|
1389
1433
|
const rejection = gateGithubDelivery(
|
|
1390
|
-
|
|
1434
|
+
await githubWebhookSecret(),
|
|
1391
1435
|
body,
|
|
1392
1436
|
req.headers.get("x-hub-signature-256"),
|
|
1393
1437
|
getSettings().webhooks.allowUnverifiedGithub,
|
|
@@ -1441,7 +1485,7 @@ function wrapRoute(inner: HttpHandler): HttpHandler {
|
|
|
1441
1485
|
const pollTarget = parseGitHubReactionPollTarget(body, eventType);
|
|
1442
1486
|
if (pollTarget) {
|
|
1443
1487
|
args.waitUntil(
|
|
1444
|
-
routeGitHubReactionFeedback(pollTarget, liveGitHubReactionFeedbackDeps(
|
|
1488
|
+
routeGitHubReactionFeedback(pollTarget, liveGitHubReactionFeedbackDeps(botName())).then(
|
|
1445
1489
|
(outcomes) => {
|
|
1446
1490
|
if (outcomes.length > 0) slog(`reaction feedback on ${pollTarget.owner}/${pollTarget.repo}#${pollTarget.number}: ${outcomes.length} recorded`);
|
|
1447
1491
|
},
|
package/agent/docs-generated.ts
CHANGED
|
@@ -23,8 +23,9 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
23
23
|
"",
|
|
24
24
|
"This is the GitHub *conversational-surface* runbook. It is **distinct from the",
|
|
25
25
|
"[source-control plane](../../capabilities/source-control.md)** (the agent's own coding loop —",
|
|
26
|
-
"checkout/push/PR
|
|
27
|
-
"
|
|
26
|
+
"checkout/push/PR): the two are orthogonal planes, and since OPE-2650 they are ONE GitHub App. They",
|
|
27
|
+
"read the App's ids from the same `github.*` settings and sign with the one operator secret",
|
|
28
|
+
"`GITHUB_APP_PRIVATE_KEY`. The `github.conversation` setting arms this surface. For the concept",
|
|
28
29
|
"of a *surface*, see [Surfaces](../../extending/surfaces.md). Like",
|
|
29
30
|
"[Linear](./linear.md)/[Notion](./notion.md), GitHub is ticket/comment-based rather than",
|
|
30
31
|
"chat-based, so it's out of scope for the chat [support matrix](./support-matrix.md).",
|
|
@@ -175,9 +176,9 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
175
176
|
" `https://<your-deploy-url>/eve/v1/github`, a generated webhook secret, and the permissions and",
|
|
176
177
|
" events below. Check the name and confirm. GitHub App names are unique across GitHub, so change",
|
|
177
178
|
" the name there if GitHub says it is taken. GitHub then sends the App's id, private key and",
|
|
178
|
-
" webhook secret back to the local page
|
|
179
|
-
" no
|
|
180
|
-
" then lists the names written and the App's
|
|
179
|
+
" webhook secret back to the local page. The page writes only the secrets into `.env.local`. You",
|
|
180
|
+
" copy no key, and no secret passes through the chat. `surface_app_create { \"surface\": \"github\", \"action\": \"status\" }`",
|
|
181
|
+
" then lists the names written, the App's install link, and the App's public ids as `settings`.",
|
|
181
182
|
"",
|
|
182
183
|
" - `owner: \"<org login>\"` creates the App under an organization. Without it, the App belongs to",
|
|
183
184
|
" your own account.",
|
|
@@ -225,20 +226,38 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
225
226
|
" **By hand.** Without the ops server, create the App at GitHub → **Settings → Developer settings",
|
|
226
227
|
" → GitHub Apps → New GitHub App**. Point the webhook at `https://<your-deploy-url>/eve/v1/github`,",
|
|
227
228
|
" set a webhook secret, grant the permissions in the table and subscribe to its events. Then set the",
|
|
228
|
-
" environment in step 2 yourself.",
|
|
229
|
+
" settings and the environment in step 2 yourself.",
|
|
229
230
|
"",
|
|
230
231
|
" On the App's settings page, under **Display information**, upload [`brand/png/mark-square-1024.png`](../../../brand/png/mark-square-1024.png), the project mark, as its logo. The manifest has no logo field. Skip this when you set [`identity.name`](../configuration-reference.md) and chose your own icon: with no name configured the agent presents as the project, so the project mark is its default avatar.",
|
|
231
|
-
"2. **
|
|
232
|
-
"
|
|
233
|
-
" `
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
" `
|
|
237
|
-
" `
|
|
232
|
+
"2. **Commit the settings and store the secrets.** The App's public values are settings in Git",
|
|
233
|
+
" (OPE-2650). The `status` reply returns them as `settings`: `github.appId`, `github.appSlug`,",
|
|
234
|
+
" `github.appClientId`, `github.conversation` and, when GitHub's bot-user lookup answers,",
|
|
235
|
+
" `github.botUserId`. Commit them to your app's `agent/settings/settings.ts`. One App serves both",
|
|
236
|
+
" planes, so the source-control plane and this conversational surface read the same settings.",
|
|
237
|
+
" `github.conversation` arms this surface: the full App returns `true`, and a source-control App",
|
|
238
|
+
" returns `false`, so its comments start no conversation:",
|
|
239
|
+
"",
|
|
240
|
+
" ```ts",
|
|
241
|
+
" // agent/settings/settings.ts",
|
|
242
|
+
" export const SETTINGS_OVERRIDES = defineSettings({",
|
|
243
|
+
" github: {",
|
|
244
|
+
" appId: \"123456\", // the App's numeric id",
|
|
245
|
+
" appSlug: \"acme-agent\", // the App slug — the bot answers to @<slug>",
|
|
246
|
+
" appClientId: \"Iv1.…\", // the App's public OAuth client id",
|
|
247
|
+
" botUserId: \"987654\", // the bot user's id, for commit attribution",
|
|
248
|
+
" conversation: true, // the full App: @mentions and comments start sessions",
|
|
249
|
+
" },",
|
|
250
|
+
" });",
|
|
251
|
+
" ```",
|
|
252
|
+
"",
|
|
253
|
+
" **The private key and the webhook secret are operator secrets** (OPE-2650). The tool writes both",
|
|
254
|
+
" into `.env.local`. Store them with the ops tool `secret_set { names: [\"GITHUB_APP_PRIVATE_KEY\",",
|
|
255
|
+
" \"GITHUB_WEBHOOK_SECRET\"] }`, or `npx oe secret set <NAME>` by hand, then redeploy. One",
|
|
256
|
+
" private-key name serves both planes: OPE-2650 folded `OE_GITHUB_APP_PRIVATE_KEY` into",
|
|
257
|
+
" `GITHUB_APP_PRIVATE_KEY`.",
|
|
238
258
|
"",
|
|
239
259
|
" ```bash",
|
|
240
|
-
"
|
|
241
|
-
" GITHUB_APP_SLUG=… # the App slug — the bot answers to @<slug>",
|
|
260
|
+
" # Operator secrets: `npx oe secret set <NAME>`, value on stdin. An env var is only the on-ramp.",
|
|
242
261
|
" GITHUB_APP_PRIVATE_KEY=… # PEM private key (mint installation tokens)",
|
|
243
262
|
" GITHUB_WEBHOOK_SECRET=… # HMAC-verifies every delivery. REQUIRED — unset rejects all (fail closed)",
|
|
244
263
|
" # DEV ONLY, and it is a SETTING not a variable: turn on `webhooks.allowUnverifiedGithub`",
|
|
@@ -258,9 +277,9 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
258
277
|
"## Multi-org installs (self-registering)",
|
|
259
278
|
"",
|
|
260
279
|
"The source-control plane mints tokens per **owner**, so it needs an `owner → installation id`",
|
|
261
|
-
"entry for every account the App is installed on. You can set that by hand with",
|
|
262
|
-
"`
|
|
263
|
-
"
|
|
280
|
+
"entry for every account the App is installed on. You can set that by hand with the",
|
|
281
|
+
"`github.installations` setting (an object such as `{ acme: \"4242\" }`, ids as numeric strings) —",
|
|
282
|
+
"and you no longer have to.",
|
|
264
283
|
"",
|
|
265
284
|
"Because GitHub delivers `installation` + `installation_repositories` to every App by default",
|
|
266
285
|
"(no subscription exists to switch on), each install/uninstall registers",
|
|
@@ -268,11 +287,11 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
268
287
|
"and a repo-scope change refreshes it. An org installing your App is then self-service end to end —",
|
|
269
288
|
"no env edit, no redeploy.",
|
|
270
289
|
"",
|
|
271
|
-
"**`
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"rule, uninstalling an org you *also* pinned in
|
|
275
|
-
"fail loudly at GitHub, which is the honest signal
|
|
290
|
+
"**`github.installations` stays authoritative.** The discovered entries are composed *under* the",
|
|
291
|
+
"setting, so anything you pinned by hand always wins. A store outage degrades to exactly the",
|
|
292
|
+
"configured set, and a webhook can never re-point an owner you configured yourself. (By the same",
|
|
293
|
+
"rule, uninstalling an org you *also* pinned in the setting leaves its entry in place. Its mints then",
|
|
294
|
+
"fail loudly at GitHub, which is the honest signal. Remove it from the setting to finish the removal.)",
|
|
276
295
|
"",
|
|
277
296
|
"Install and uninstall both write an audit row — `surface.app_installed` / `surface.app_uninstalled`,",
|
|
278
297
|
"with the owner as the workspace — so the install history is queryable over the control plane's",
|