Skip to content

Commit e6eb4ec

Browse files
committed
fix(notifications): route Discord actions through audited webhooks
1 parent 1e62742 commit e6eb4ec

11 files changed

Lines changed: 331 additions & 394 deletions

apps/gittensory-ui/src/lib/selfhost-env-reference.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
1111
},
1212
{
1313
name: "AI_EMBED_API_KEY",
14-
firstReference: "src/server.ts:425",
14+
firstReference: "src/server.ts:428",
1515
},
1616
{
1717
name: "AI_EMBED_BASE_URL",
18-
firstReference: "src/server.ts:422",
18+
firstReference: "src/server.ts:425",
1919
},
2020
{
2121
name: "AI_EMBED_MODEL",
@@ -43,7 +43,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
4343
},
4444
{
4545
name: "BACKUP_ACKNOWLEDGED",
46-
firstReference: "src/server.ts:364",
46+
firstReference: "src/server.ts:367",
4747
},
4848
{
4949
name: "BROWSER_WS_ENDPOINT",
@@ -79,27 +79,27 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
7979
},
8080
{
8181
name: "CRON_INTERVAL_MS",
82-
firstReference: "src/server.ts:885",
82+
firstReference: "src/server.ts:904",
8383
},
8484
{
8585
name: "DATABASE_PATH",
86-
firstReference: "src/server.ts:247",
86+
firstReference: "src/server.ts:250",
8787
},
8888
{
8989
name: "DATABASE_URL",
9090
firstReference: "src/selfhost/preflight.ts:201",
9191
},
9292
{
9393
name: "DISCORD_REPO_WEBHOOKS",
94-
firstReference: "src/selfhost/discord-notify.ts:31",
94+
firstReference: "src/services/notify-discord.ts:41",
9595
},
9696
{
9797
name: "DISCORD_WEBHOOK_URL",
98-
firstReference: "src/selfhost/discord-notify.ts:40",
98+
firstReference: "src/services/notify-discord.ts:78",
9999
},
100100
{
101101
name: "FOREGROUND_LIVENESS_ENABLED",
102-
firstReference: "src/selfhost/foreground-liveness.ts:34",
102+
firstReference: "src/selfhost/foreground-liveness.ts:41",
103103
},
104104
{
105105
name: "GITHUB_APP_ID",
@@ -111,11 +111,11 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
111111
},
112112
{
113113
name: "GITHUB_CACHE_TTL_SECONDS",
114-
firstReference: "src/server.ts:493",
114+
firstReference: "src/server.ts:496",
115115
},
116116
{
117117
name: "GITTENSORY_REPO_CONFIG_DIR",
118-
firstReference: "src/server.ts:281",
118+
firstReference: "src/server.ts:284",
119119
},
120120
{
121121
name: "GITTENSORY_VERSION",
@@ -127,11 +127,11 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
127127
},
128128
{
129129
name: "MAINTENANCE_ADMISSION_ENABLED",
130-
firstReference: "src/selfhost/maintenance-admission.ts:123",
130+
firstReference: "src/selfhost/maintenance-admission.ts:129",
131131
},
132132
{
133133
name: "MIGRATIONS_DIR",
134-
firstReference: "src/server.ts:377",
134+
firstReference: "src/server.ts:380",
135135
},
136136
{
137137
name: "OBSERVABILITY_SMOKE_POLL_MS",
@@ -191,7 +191,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
191191
},
192192
{
193193
name: "ORB_BROKER_URL",
194-
firstReference: "src/server.ts:934",
194+
firstReference: "src/server.ts:953",
195195
},
196196
{
197197
name: "ORB_COLLECTOR_TOKEN",
@@ -207,7 +207,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
207207
},
208208
{
209209
name: "ORB_RELAY_MODE",
210-
firstReference: "src/server.ts:936",
210+
firstReference: "src/server.ts:955",
211211
},
212212
{
213213
name: "OTEL_EXPORTER_OTLP_ENDPOINT",
@@ -239,11 +239,11 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
239239
},
240240
{
241241
name: "PGVECTOR_ENABLED",
242-
firstReference: "src/server.ts:227",
242+
firstReference: "src/server.ts:230",
243243
},
244244
{
245245
name: "PORT",
246-
firstReference: "src/server.ts:684",
246+
firstReference: "src/server.ts:703",
247247
},
248248
{
249249
name: "PUBLIC_API_ORIGIN",
@@ -259,7 +259,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
259259
},
260260
{
261261
name: "QDRANT_URL",
262-
firstReference: "src/server.ts:512",
262+
firstReference: "src/server.ts:515",
263263
},
264264
{
265265
name: "QUEUE_BACKGROUND_CONCURRENCY",
@@ -271,7 +271,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
271271
},
272272
{
273273
name: "REVIEW_AUDIT_DIR",
274-
firstReference: "src/server.ts:557",
274+
firstReference: "src/server.ts:560",
275275
},
276276
{
277277
name: "SELFHOST_BUNDLE_ALL",
@@ -307,23 +307,23 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
307307
},
308308
{
309309
name: "SETUP_OUTPUT_PATH",
310-
firstReference: "src/server.ts:801",
310+
firstReference: "src/server.ts:820",
311311
},
312312
];
313313

314314
export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
315315
"| Name | First reference |",
316316
"| --- | --- |",
317317
"| `AI_COMBINE` | `src/selfhost/ai.ts:936` |",
318-
"| `AI_EMBED_API_KEY` | `src/server.ts:425` |",
319-
"| `AI_EMBED_BASE_URL` | `src/server.ts:422` |",
318+
"| `AI_EMBED_API_KEY` | `src/server.ts:428` |",
319+
"| `AI_EMBED_BASE_URL` | `src/server.ts:425` |",
320320
"| `AI_EMBED_MODEL` | `src/selfhost/ai.ts:832` |",
321321
"| `AI_ON_MERGE` | `src/selfhost/ai.ts:938` |",
322322
"| `AI_PROVIDER` | `src/selfhost/ai-config.ts:43` |",
323323
"| `ANTHROPIC_AI_BASE_URL` | `src/selfhost/ai.ts:836` |",
324324
"| `ANTHROPIC_AI_MODEL` | `src/selfhost/ai.ts:57` |",
325325
"| `ANTHROPIC_API_KEY` | `src/selfhost/ai.ts:835` |",
326-
"| `BACKUP_ACKNOWLEDGED` | `src/server.ts:364` |",
326+
"| `BACKUP_ACKNOWLEDGED` | `src/server.ts:367` |",
327327
"| `BROWSER_WS_ENDPOINT` | `src/selfhost/stubs/puppeteer.ts:11` |",
328328
"| `CLAUDE_AI_EFFORT` | `src/selfhost/ai.ts:108` |",
329329
"| `CLAUDE_AI_MODEL` | `src/selfhost/ai.ts:49` |",
@@ -332,20 +332,20 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
332332
"| `CODEX_AI_MODEL` | `src/selfhost/ai.ts:53` |",
333333
"| `CODEX_AI_TIMEOUT_MS` | `src/selfhost/ai.ts:112` |",
334334
"| `CODEX_HOME` | `src/selfhost/ai.ts:274` |",
335-
"| `CRON_INTERVAL_MS` | `src/server.ts:885` |",
336-
"| `DATABASE_PATH` | `src/server.ts:247` |",
335+
"| `CRON_INTERVAL_MS` | `src/server.ts:904` |",
336+
"| `DATABASE_PATH` | `src/server.ts:250` |",
337337
"| `DATABASE_URL` | `src/selfhost/preflight.ts:201` |",
338-
"| `DISCORD_REPO_WEBHOOKS` | `src/selfhost/discord-notify.ts:31` |",
339-
"| `DISCORD_WEBHOOK_URL` | `src/selfhost/discord-notify.ts:40` |",
340-
"| `FOREGROUND_LIVENESS_ENABLED` | `src/selfhost/foreground-liveness.ts:34` |",
338+
"| `DISCORD_REPO_WEBHOOKS` | `src/services/notify-discord.ts:41` |",
339+
"| `DISCORD_WEBHOOK_URL` | `src/services/notify-discord.ts:78` |",
340+
"| `FOREGROUND_LIVENESS_ENABLED` | `src/selfhost/foreground-liveness.ts:41` |",
341341
"| `GITHUB_APP_ID` | `src/selfhost/orb-collector.ts:59` |",
342342
"| `GITHUB_APP_PRIVATE_KEY` | `src/selfhost/orb-collector.ts:166` |",
343-
"| `GITHUB_CACHE_TTL_SECONDS` | `src/server.ts:493` |",
344-
"| `GITTENSORY_REPO_CONFIG_DIR` | `src/server.ts:281` |",
343+
"| `GITHUB_CACHE_TTL_SECONDS` | `src/server.ts:496` |",
344+
"| `GITTENSORY_REPO_CONFIG_DIR` | `src/server.ts:284` |",
345345
"| `GITTENSORY_VERSION` | `src/selfhost/health.ts:29` |",
346346
"| `HOME` | `src/selfhost/ai.ts:274` |",
347-
"| `MAINTENANCE_ADMISSION_ENABLED` | `src/selfhost/maintenance-admission.ts:123` |",
348-
"| `MIGRATIONS_DIR` | `src/server.ts:377` |",
347+
"| `MAINTENANCE_ADMISSION_ENABLED` | `src/selfhost/maintenance-admission.ts:129` |",
348+
"| `MIGRATIONS_DIR` | `src/server.ts:380` |",
349349
"| `OBSERVABILITY_SMOKE_POLL_MS` | `scripts/smoke-observability-traces.mjs:8` |",
350350
"| `OBSERVABILITY_SMOKE_TIMEOUT_MS` | `scripts/smoke-observability-traces.mjs:6` |",
351351
"| `OLLAMA_AI_API_KEY` | `src/selfhost/ai.ts:829` |",
@@ -360,27 +360,27 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
360360
"| `ORB_AIR_GAP` | `src/selfhost/orb-collector.ts:161` |",
361361
"| `ORB_ANONYMIZE` | `src/selfhost/orb-collector.ts:174` |",
362362
"| `ORB_APP_ID` | `src/selfhost/orb-collector.ts:59` |",
363-
"| `ORB_BROKER_URL` | `src/server.ts:934` |",
363+
"| `ORB_BROKER_URL` | `src/server.ts:953` |",
364364
"| `ORB_COLLECTOR_TOKEN` | `src/selfhost/orb-collector.ts:205` |",
365365
"| `ORB_COLLECTOR_URL` | `src/selfhost/orb-collector.ts:172` |",
366366
"| `ORB_ENROLLMENT_SECRET` | `src/selfhost/orb-collector.ts:165` |",
367-
"| `ORB_RELAY_MODE` | `src/server.ts:936` |",
367+
"| `ORB_RELAY_MODE` | `src/server.ts:955` |",
368368
"| `OTEL_EXPORTER_OTLP_ENDPOINT` | `src/selfhost/otel.ts:47` |",
369369
"| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `src/selfhost/otel.ts:45` |",
370370
"| `OTEL_SERVICE_ENVIRONMENT` | `src/selfhost/otel.ts:60` |",
371371
"| `OTEL_SERVICE_NAME` | `src/selfhost/otel.ts:59` |",
372372
"| `OTEL_TRACES_EXPORTER` | `src/selfhost/otel.ts:40` |",
373373
"| `OTEL_TRACES_SAMPLER` | `src/selfhost/otel.ts:74` |",
374374
"| `OTEL_TRACES_SAMPLER_ARG` | `src/selfhost/otel.ts:76` |",
375-
"| `PGVECTOR_ENABLED` | `src/server.ts:227` |",
376-
"| `PORT` | `src/server.ts:684` |",
375+
"| `PGVECTOR_ENABLED` | `src/server.ts:230` |",
376+
"| `PORT` | `src/server.ts:703` |",
377377
"| `PUBLIC_API_ORIGIN` | `src/selfhost/preflight.ts:192` |",
378378
"| `QDRANT_API_KEY` | `src/selfhost/qdrant-vectorize.ts:50` |",
379379
"| `QDRANT_DIM` | `src/selfhost/qdrant-vectorize.ts:71` |",
380-
"| `QDRANT_URL` | `src/server.ts:512` |",
380+
"| `QDRANT_URL` | `src/server.ts:515` |",
381381
"| `QUEUE_BACKGROUND_CONCURRENCY` | `src/selfhost/queue-common.ts:102` |",
382382
"| `REDIS_URL` | `src/selfhost/preflight.ts:144` |",
383-
"| `REVIEW_AUDIT_DIR` | `src/server.ts:557` |",
383+
"| `REVIEW_AUDIT_DIR` | `src/server.ts:560` |",
384384
"| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.mjs:13` |",
385385
"| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.mjs:5` |",
386386
"| `SELFHOST_SETUP_TOKEN` | `src/selfhost/preflight.ts:186` |",
@@ -389,5 +389,5 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
389389
"| `SENTRY_RELEASE` | `src/selfhost/otel.ts:62` |",
390390
"| `SENTRY_SERVER_NAME` | `src/selfhost/sentry.ts:383` |",
391391
"| `SENTRY_TRACES_SAMPLE_RATE` | `src/selfhost/sentry.ts:171` |",
392-
"| `SETUP_OUTPUT_PATH` | `src/server.ts:801` |",
392+
"| `SETUP_OUTPUT_PATH` | `src/server.ts:820` |",
393393
].join("\n");

scripts/gen-selfhost-env-reference.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const DEFAULT_OUTPUT_PATH = "apps/gittensory-ui/src/lib/selfhost-env-refe
88
export const DEFAULT_SOURCE_ROOTS = [
99
"src/selfhost",
1010
"src/server.ts",
11+
"src/services/notify-discord.ts",
1112
"scripts/build-selfhost.mjs",
1213
"scripts/migrate-selfhost-sqlite-to-postgres.ts",
1314
"scripts/smoke-observability-traces.mjs",
@@ -69,13 +70,25 @@ function collectEnvReads(source, fileName) {
6970
const name = bindingElementName(element);
7071
if (name) addRead(name, element.propertyName ?? element.name);
7172
}
73+
} else if (ts.isCallExpression(node) && isStaticEnvHelperCall(node)) {
74+
addRead(node.arguments[1].text, node.arguments[1]);
7275
}
7376
ts.forEachChild(node, visit);
7477
};
7578
visit(sourceFile);
7679
return reads;
7780
}
7881

82+
function isStaticEnvHelperCall(node) {
83+
return (
84+
ts.isIdentifier(node.expression) &&
85+
node.expression.text === "envString" &&
86+
node.arguments.length >= 2 &&
87+
isEnvContainer(node.arguments[0]) &&
88+
ts.isStringLiteralLike(node.arguments[1])
89+
);
90+
}
91+
7992
function bindingElementName(element) {
8093
const candidate = element.propertyName ?? element.name;
8194
if (ts.isIdentifier(candidate) || ts.isStringLiteralLike(candidate)) return candidate.text;

src/review/guardrail-config.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,27 @@ export const ENGINE_DECISION_GUARDRAIL_GLOBS = [
5050
"src/review/outcomes-wire.ts", // the pr_outcome + reversal telemetry that feeds self-tuning
5151
];
5252

53+
// Self-host runtime and persistence surface. These paths do not necessarily decide the verdict directly, but they
54+
// decide whether the review stack can boot, store, queue, notify, migrate, and export safely. Keep this scoped to
55+
// operational chokepoints so ordinary feature/review logic can still auto-merge when it is clean.
56+
export const SELFHOST_RUNTIME_GUARDRAIL_GLOBS = [
57+
"src/selfhost/**",
58+
"src/server.ts",
59+
"src/db/**",
60+
"migrations/**",
61+
"Dockerfile",
62+
"docker-compose*.yml*",
63+
"docker-compose*.yaml*",
64+
"compose*.yml*",
65+
"compose*.yaml*",
66+
"systemd/**",
67+
];
68+
5369
/**
5470
* Resolve hard-guardrail path globs. Kept async to avoid touching the processor call graph, but this no longer
5571
* reads external policy storage; self-host review policy belongs in container-private `.gittensory.yml`, and these
5672
* engine-level guardrails are always-on invariants.
5773
*/
5874
export async function loadHardGuardrailGlobs(_env: Env, _repoFullName: string): Promise<string[]> {
59-
return [...DEFAULT_CRUCIAL_GUARDRAIL_GLOBS, ...CONFIG_AS_CODE_GUARDRAIL_GLOBS, ...ENGINE_DECISION_GUARDRAIL_GLOBS];
75+
return [...DEFAULT_CRUCIAL_GUARDRAIL_GLOBS, ...CONFIG_AS_CODE_GUARDRAIL_GLOBS, ...ENGINE_DECISION_GUARDRAIL_GLOBS, ...SELFHOST_RUNTIME_GUARDRAIL_GLOBS];
6076
}

src/review/outcomes-wire.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
// once a repo's merge precision actually drops below the floor over a real sample.
2525

2626
import { recordAuditEvent } from "../db/repositories";
27-
import { notifyDiscordReview } from "../selfhost/discord-notify";
2827
import { incr } from "../selfhost/metrics";
2928
import type { GitHubWebhookPayload } from "../types";
3029
import { errorMessage, nowIso } from "../utils/json";
@@ -338,21 +337,10 @@ export async function recordPrOutcome(
338337
),
339338
);
340339

341-
// Per-repo Discord notification on the FINAL disposition (self-host; no-op unless a webhook is configured).
342-
// Reason = the AI's recorded gate verdict for this PR, falling back to the plain disposition.
343-
const fallbackReason = merged
344-
? "Pull request merged into the base branch."
345-
: botWasActor
346-
? "Closed by Gittensory after review."
347-
: "Pull request closed without merging.";
348-
await notifyDiscordReview({
349-
repoFullName,
350-
prNumber: pr.number,
351-
author: pr.user?.login ?? "unknown",
352-
outcome: decision,
353-
reason: await resolveDispositionReason(env, targetId, fallbackReason),
354-
url: `https://github.com/${repoFullName}/pull/${pr.number}`,
355-
});
340+
// Discord/Slack action notifications are emitted by the action executor, which knows the exact bot action that
341+
// was attempted and can audit the delivery. This outcome recorder only stores realized ground truth. Emitting
342+
// another webhook from the GitHub `pull_request.closed` event duplicated bot-action notifications and could
343+
// route through stale/global self-host webhook config.
356344
}
357345

358346
// ── 2) reversals — a human undid a bot action ────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)