Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,57 @@

<!-- version list -->

## v0.20.0 (2026-05-28)

### Features

- Allow dashboard access by github team
([`50d5d3d`](https://github.com/pilipilisbot/github-agent-bridge/commit/50d5d3d652052f9cbd5741d7cdc25c0787e4f182))


## v0.19.2 (2026-05-27)

### Bug Fixes

- Accept github app bot trigger actors
([`7478871`](https://github.com/pilipilisbot/github-agent-bridge/commit/747887182f28ba64dacd86523a178c4844d4a060))

- Route dashboard login and use configured gh
([`ba393ca`](https://github.com/pilipilisbot/github-agent-bridge/commit/ba393caad9d90ace3fee580ba97606cdbda138c2))

### Testing

- Cover github app bot actor normalization
([`9a45ea2`](https://github.com/pilipilisbot/github-agent-bridge/commit/9a45ea2834e4598c15d55213b0fca1f9ecde3e75))


## v0.19.1 (2026-05-26)

### Bug Fixes

- Preserve default bot login policy
([`80dfee9`](https://github.com/pilipilisbot/github-agent-bridge/commit/80dfee9481dc8cdc011bc144ee50552ea3e21e43))

- Support forwarded github notifications
([`70f8e13`](https://github.com/pilipilisbot/github-agent-bridge/commit/70f8e13c4c28af9d5e9b4db3ed3925315bc53f4a))

- Treat approved reviews as non-actionable
([`e26c41b`](https://github.com/pilipilisbot/github-agent-bridge/commit/e26c41b78a0e7f3d0a3452b8d7e91cc56ab81bb2))


## v0.19.0 (2026-05-26)

### Documentation

- Add mobile version screenshot
([`f5cae8d`](https://github.com/pilipilisbot/github-agent-bridge/commit/f5cae8dbce8f3e4d2542202390acf442f96680c1))

### Features

- Show bridge version in dashboard
([`c2711d6`](https://github.com/pilipilisbot/github-agent-bridge/commit/c2711d63d1bab18f4df7a90600841da5f24439fa))


## v0.18.7 (2026-05-25)

### Bug Fixes
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ The bridge is conservative by default. `policy.json` decides what is trusted, wh

```json
{
"source": {
"from": ["notifications@github.com", "giscebot@gisce.net"]
},
"botLogins": ["pilipilisbot"],
"trustedOrgs": ["your-org"],
"enabledRepos": ["your-org/your-repo"],
"orgRoutes": {
Expand Down
19 changes: 19 additions & 0 deletions dashboard/src/main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import {
ActorFilter,
ProductMeta,
StatusBadge,
buildJobQuery,
groupSessionEvents,
groupTranscriptEntries,
selectedJobIdFromPath,
shouldRefreshJobForSessionEvent,
} from "./main";

describe("dashboard routing and API query helpers", () => {
Expand All @@ -33,6 +35,14 @@ describe("dashboard routing and API query helpers", () => {
expect(selectedJobIdFromPath("/jobs/not-a-number")).toBeNull();
expect(selectedJobIdFromPath("/jobs/45/activity")).toBeNull();
});

it("refreshes job data only for session events that can change job state", () => {
expect(shouldRefreshJobForSessionEvent("claimed")).toBe(true);
expect(shouldRefreshJobForSessionEvent("dispatch_finished")).toBe(true);
expect(shouldRefreshJobForSessionEvent("done")).toBe(true);
expect(shouldRefreshJobForSessionEvent("openclaw_stdout")).toBe(false);
expect(shouldRefreshJobForSessionEvent("openclaw_stderr")).toBe(false);
});
});

describe("status badges", () => {
Expand All @@ -48,6 +58,15 @@ describe("status badges", () => {
});
});

describe("product metadata", () => {
it("shows the bridge version and upstream repository link", () => {
render(<ProductMeta about={{ service: "github-agent-bridge-dashboard", version: "0.18.7", repository_url: "https://github.com/pilipilisbot/github-agent-bridge" }} />);

expect(screen.getByText("v0.18.7")).toBeInTheDocument();
expect(screen.getByRole("link", { name: /github/i })).toHaveAttribute("href", "https://github.com/pilipilisbot/github-agent-bridge");
});
});

describe("actor filter", () => {
it("filters actors, selects a suggestion, and clears the selection", async () => {
const user = userEvent.setup();
Expand Down
41 changes: 37 additions & 4 deletions dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ type Percentiles = {
p99: number | null;
};

type About = {
service: string;
version: string;
repository_url: string;
};

type Job = {
id: number;
work_key: string;
Expand Down Expand Up @@ -463,6 +469,10 @@ function transcriptKey(item: TranscriptEntry) {
return `${item.timestamp ?? ""}:${item.role}:${item.kind}:${item.title}:${item.text}`;
}

function shouldRefreshJobForSessionEvent(eventType: string) {
return ["claimed", "dispatch_started", "dispatch_finished", "done", "blocked", "denied", "waiting_approval"].includes(eventType);
}

function selectedJobIdFromPath(pathname = window.location.pathname) {
const match = pathname.match(/^\/jobs\/(\d+)\/?$/);
return match ? Number(match[1]) : null;
Expand All @@ -478,6 +488,7 @@ function App() {
const selectedJobId = jobRouteId;
const metrics = useQuery({ queryKey: ["metrics"], queryFn: () => api<{ metrics: MetricsSummary }>("/api/metrics/summary"), enabled: !isJobDetailRoute });
const me = useQuery({ queryKey: ["me"], queryFn: () => api<{ user: UserProfile }>("/api/me"), refetchInterval: false });
const about = useQuery({ queryKey: ["about"], queryFn: () => api<About>("/api/about") });
const actorOptions = useQuery({ queryKey: ["job-actors"], queryFn: () => api<{ actors: JobActor[] }>("/api/jobs/actors"), enabled: !isJobDetailRoute });
const jobs = useQuery({ queryKey: ["jobs", filters, jobLimit], queryFn: () => api<{ jobs: Job[] }>(buildJobQuery(filters, jobLimit)), enabled: !isJobDetailRoute });
const processes = useQuery({ queryKey: ["processes"], queryFn: () => api<ProcessesResponse>("/api/processes"), enabled: !isJobDetailRoute });
Expand Down Expand Up @@ -512,8 +523,10 @@ function App() {
queryClient.setQueryData<{ events: SessionEvent[] }>(["job-session-events", selectedJobId], (current) => ({
events: appendUniqueById(current?.events ?? [], event),
}));
queryClient.invalidateQueries({ queryKey: ["job", selectedJobId] });
queryClient.invalidateQueries({ queryKey: ["jobs"] });
if (shouldRefreshJobForSessionEvent(event.event_type)) {
queryClient.invalidateQueries({ queryKey: ["job", selectedJobId] });
queryClient.invalidateQueries({ queryKey: ["jobs"] });
}
});
source.addEventListener("transcript_entry", (message) => {
const payload = parseSseData<{ job_id: number; entry: TranscriptEntry }>(message);
Expand All @@ -523,7 +536,9 @@ function App() {
}));
});
source.onerror = () => {
source.close();
queryClient.invalidateQueries({ queryKey: ["job", selectedJobId] });
queryClient.invalidateQueries({ queryKey: ["job-session-events", selectedJobId] });
queryClient.invalidateQueries({ queryKey: ["job-session-transcript", selectedJobId] });
};
return () => source.close();
}, [selectedJobId, queryClient]);
Expand Down Expand Up @@ -558,7 +573,7 @@ function App() {
<div className="mx-auto flex w-full max-w-[1440px] items-center justify-between gap-3 px-4 py-4 md:px-6">
<div className="min-w-0">
<h1 className="truncate text-xl font-semibold">GitHub Agent Bridge</h1>
<p className="text-sm text-slate-300">Read-only operational dashboard</p>
<ProductMeta about={about.data} />
</div>
<UserMenu user={me.data?.user} loading={me.isLoading} />
</div>
Expand Down Expand Up @@ -631,6 +646,22 @@ function App() {
);
}

function ProductMeta({ about }: { about: About | undefined }) {
const version = about?.version ? `v${about.version}` : "version loading";
return (
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-300">
<span>Read-only operational dashboard</span>
<span className="font-mono text-xs text-slate-400">{version}</span>
{about?.repository_url ? (
<a className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:underline" href={safeExternalUrl(about.repository_url)} rel="noreferrer" target="_blank">
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
GitHub
</a>
) : null}
</p>
);
}

function JobDetailPage({ jobId, detail, onRefresh }: { jobId: number; detail: React.ReactNode; onRefresh: () => void }) {
return (
<div className="grid min-w-0 gap-3 sm:gap-4">
Expand Down Expand Up @@ -1444,11 +1475,13 @@ function RefreshButton({ onClick, compactOnMobile = false }: { onClick: () => vo

export {
ActorFilter,
ProductMeta,
StatusBadge,
buildJobQuery,
groupSessionEvents,
groupTranscriptEntries,
selectedJobIdFromPath,
shouldRefreshJobForSessionEvent,
};

const root = document.getElementById("root");
Expand Down
16 changes: 10 additions & 6 deletions docs/dashboard-github-oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ loopback-only unless it is behind HTTPS and an authenticated reverse proxy.
6. Create the app, then copy the **Client ID**.
7. Generate a **Client secret** and copy it into the private environment file.

The dashboard currently requests `read:user read:org`. `read:org` is required
when access is granted by `GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_ORGS`,
especially for private organization membership.
The dashboard requests `read:user` by default. It also requests `read:org` when
access is granted by `GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_ORGS` or
`GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_TEAMS`, especially for private
organization and team membership.

## Configure the Dashboard Environment

Expand All @@ -59,6 +60,7 @@ GITHUB_OAUTH_CLIENT_ID=replace-with-github-oauth-client-id
GITHUB_OAUTH_CLIENT_SECRET=replace-with-github-oauth-client-secret
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_USERS=your-github-login
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_ORGS=
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_TEAMS=
EOF
chmod 600 ~/.config/github-agent-bridge/env
```
Expand All @@ -77,12 +79,14 @@ Use at least one authorization allowlist:
- `GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_USERS`: comma-separated GitHub logins.
- `GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_ORGS`: comma-separated GitHub
organizations whose members may access the dashboard.
- `GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_TEAMS`: comma-separated GitHub teams in
`org/team-slug` form whose members may access the dashboard.

If both allowlists are empty, any authenticated GitHub user is accepted. That is
If all allowlists are empty, any authenticated GitHub user is accepted. That is
only appropriate for isolated local development.

Team-level allowlists and per-repository dashboard scopes are part of the issue
#4 architecture but are not implemented in the current dashboard backend.
Per-repository dashboard scopes are part of the issue #4 architecture but are
not implemented in the current dashboard backend.

## Start the Service

Expand Down
1 change: 1 addition & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ GITHUB_AGENT_BRIDGE_DASHBOARD_SECRET_KEY=replace-with-random-secret
GITHUB_OAUTH_CLIENT_ID=replace-with-github-oauth-client-id
GITHUB_OAUTH_CLIENT_SECRET=replace-with-github-oauth-client-secret
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_USERS=your-github-login
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_TEAMS=
EOF
```

Expand Down
14 changes: 14 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ GITHUB_OAUTH_CLIENT_ID=replace-with-github-oauth-client-id
GITHUB_OAUTH_CLIENT_SECRET=replace-with-github-oauth-client-secret
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_USERS=alice,bob
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_ORGS=example-org
GITHUB_AGENT_BRIDGE_DASHBOARD_ALLOWED_TEAMS=example-org/platform
```

See [`dashboard-github-oauth.md`](dashboard-github-oauth.md) for the GitHub
Expand Down Expand Up @@ -221,6 +222,19 @@ before it is returned to the authenticated dashboard. The process activity panel
uses persisted process samples for a compact CPU history line chart when monitor
samples exist, and falls back to the live executor snapshot otherwise.

When publishing the dashboard through nginx, disable buffering for the proxied
dashboard location so SSE events flush immediately:

```nginx
location / {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
}
```

## Operational SLOs

| Signal | Target |
Expand Down
46 changes: 43 additions & 3 deletions docs/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ gab --policy ~/.config/github-agent-bridge/policy.json enqueue-comment-url ...
```json
{
"source": {
"from": "notifications@github.com",
"from": ["notifications@github.com", "giscebot@gisce.net"],
"requiredAuth": ["spf=pass", "dkim=pass", "dmarc=pass"],
"requiredUrlPrefix": "https://github.com/",
"messageIdDomain": "github.com"
},
"botLogins": ["pilipilisbot"],
"trustedRepos": ["your-org/your-repo"],
"trustedOrgs": ["your-org"],
"enabledRepos": ["your-org/your-repo"],
Expand Down Expand Up @@ -97,6 +98,7 @@ gab --policy ~/.config/github-agent-bridge/policy.json enqueue-comment-url ...
| `orgRoutes` | object | `{}` | Per-owner delivery routes used when no `repoRoutes` entry matches. |
| `repoRoles` | object | `{}` | Exact per-repo operating role. Takes precedence over `orgRoles`. |
| `orgRoles` | object | `{}` | Per-owner operating role used when no `repoRoles` entry matches. |
| `botLogins` | array of strings | `["pilipilisbot"]` | GitHub login names that should count as addressed bots when classifying mentions, assignments, and review requests. |
| `actions` | object | built-in action defaults | Maps classified notification actions to policy decisions. |
| `promptOverrides` | object | `{}` | Optional Markdown files that replace selected packaged prompt resources. |
| `feedbackLearning` | object | `{ "enabled": true, "minConfidence": 0.5, "autoApproveConfidence": 0.8 }` | Controls candidate capture, autonomous learning, and prompt threshold for feedback rules. |
Expand All @@ -109,7 +111,7 @@ Unknown top-level keys are ignored by the current implementation.

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `from` | string | `notifications@github.com` | Required substring in the decoded email `From` header. |
| `from` | string or array of strings | `notifications@github.com` | Required substring in the decoded email `From` header. Use an array when GitHub notifications are forwarded or rewritten by a trusted mail gateway while GitHub reply headers and message ids are preserved. |
| `requiredUrlPrefix` | string | `https://github.com/` | At least one extracted URL must start with this prefix. |
| `messageIdDomain` | string | `github.com` | Required substring in the email `Message-ID`. |
| `requiredAuth` | array of strings | currently documented only | Intended SPF/DKIM/DMARC requirements. See note below. |
Expand All @@ -123,14 +125,52 @@ Current auth behavior:
Source trust fails when any of these are false:

```text
source.from is in From header
any configured source.from value is in From header
AND auth is OK
AND at least one GitHub URL has source.requiredUrlPrefix
AND Message-ID contains source.messageIdDomain
```

If source trust fails, the decision is always `deny`.

Example with Google Groups or similar forwarded GitHub notifications:

```json
{
"source": {
"from": ["notifications@github.com", "giscebot@gisce.net"],
"requiredUrlPrefix": "https://github.com/",
"messageIdDomain": "github.com"
}
}
```

The parser still requires GitHub-specific headers, a GitHub reply address, GitHub message id content, and normal source trust before forwarded messages are accepted.

## `botLogins`

`botLogins` defines the GitHub accounts that count as the addressed bot for mention, assignment, and review-request classification.

Default:

```json
{
"botLogins": ["pilipilisbot"]
}
```

Configured names are case-insensitive and may include or omit the leading `@`.

Example:

```json
{
"botLogins": ["pilipilisbot", "giscebot"]
}
```

With this policy, comments that mention `@giscebot`, assignments to `@giscebot`, and review requests from `@giscebot` are classified the same way as the default `@pilipilisbot` notifications. Set an explicit empty array only if the deployment should rely on GitHub footer text such as “You are receiving this because you were mentioned” instead of login matching.

## `trustedRepos`

Exact repositories trusted for `trustedAuto` actions.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/issue-50/version-link-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion policy.example.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"source": {
"from": "notifications@github.com",
"from": [
"notifications@github.com",
"giscebot@gisce.net"
],
"requiredAuth": [
"spf=pass",
"dkim=pass",
Expand All @@ -10,6 +13,9 @@
"messageIdDomain": "github.com"
},
"trustedRepos": [],
"botLogins": [
"pilipilisbot"
],
"trustedOrgs": [
"your-org"
],
Expand Down
Loading