/** * githubBugReporter — file a bug report, with the run attached, into GitHub. * * import { exportBugReport, githubBugReporter } from 'agentfootprint/observe'; * * const reporter = githubBugReporter({ * issueRepo: 'acme/checkout-agent', // where the ISSUE goes * evidenceRepo: 'acme/agent-evidence', // where the ZIP goes (default: issueRepo) * }); // token: GITHUB_TOKEN, or `token` * * const { issueUrl, zipUrl } = await reporter.file(report); * * Two HTTP calls and no SDK: `PUT /repos/{evidenceRepo}/contents/{path}` commits * the zip, `POST /repos/{issueRepo}/issues` files the issue with a manifest * table and a link to the committed bundle. Plain `fetch`, zero dependencies, * `apiBase` for GitHub Enterprise Server — so this works unchanged on a network * that never reaches github.com. * * ## TWIN TARGETS: the issue and the evidence may live in different repos * * The case this exists for: a field tester finds a bug in a LIBRARY. The issue * belongs in the library's public repo, where the maintainers and the next * person to hit it will find it. The evidence — a real run, with real prompts, * real tool arguments and real retrieved documents — does not. So the zip goes * into a PRIVATE repo the maintainers can read, and the issue links it and says * plainly that the evidence is private. * * ```ts * githubBugReporter({ * issueRepo: 'footprintjs/agentfootprint', // public — the conversation * evidenceRepo: 'acme/af-bug-evidence', // private — the run * }); * ``` * * ## DEFAULT-TARGET DOCTRINE * * **File into the application's OWN repo.** That is the default (`evidenceRepo` * defaults to `issueRepo`) and it is the right default: the run belongs to the * organisation that produced it. Sending a run's evidence across an * organisational boundary — to a vendor, to an upstream library, to anyone * whose access your company did not grant — is a HUMAN act with consequences a * library cannot weigh. This adapter will do it, because a field tester filing * upstream is a real and valuable thing; it will not do it quietly. The * consent manifest (`describeBugReport`) exists so a person sees exactly what * would leave before it does, and this reporter refuses to commit evidence to a * PUBLIC repo unless the caller says `acknowledgePublicEvidence: true` out loud. * * ## Provisioning the token: fine-grained, and scoped to two repos * * Use a **fine-grained personal access token** (GitHub → Settings → Developer * settings → Fine-grained tokens), scoped to ONLY `issueRepo` and * `evidenceRepo`, with exactly two permissions — **Contents: read and write** * (to commit the zip) and **Issues: read and write** (to file the issue) — and * an expiry date. Put it in the server's environment as `GITHUB_TOKEN`, or pass * it as `token`. * * The contrast matters: a CLASSIC PAT's `repo` scope is coarse — it grants * read/write across every repository the account can reach, so a leaked * bug-report token is a leaked key to the whole account. With a fine-grained * token scoped as above, the blast radius of a leak is filing bug reports and * committing files to one evidence repo, and nothing else. GitHub App * installation tokens (short-lived, org-installed, revocable centrally) are the * next rung for an organisation that wants one; this adapter does not mint * them — hand it the token your app already obtained. * * ## Secrecy (the two-clause law) * * The token appears in no message, no error and no log this adapter can * produce, and neither does the bundle's content. A failed request is reported * as **the status and GitHub's own `message` field** — never the request, never * the headers, never the body that carried the token, never a byte of * evidence. Transport failures are re-wrapped rather than rethrown, because a * `fetch` implementation is free to put the request (headers included) into the * error it throws. Nothing here writes to a console. Pinned by a suite that * forces every failure path and greps the message, the stack and the JSON * projection for the token. * * @example A server route (the app's own repo, the default target) * ```ts * const reporter = githubBugReporter({ issueRepo: 'acme/checkout-agent' }); * app.post('/bug-report', async (req, res) => { * const report = exportBugReport(recordings.get(req.body.runId), req.body.fields); * res.json(await reporter.file(report)); * }); * ``` */ import type { BugReport } from '../../lib/bug-report/index.js'; export interface GithubBugReporterOptions { /** `owner/name` of the repo the ISSUE is filed in. Required. */ readonly issueRepo: string; /** * `owner/name` of the repo the evidence ZIP is committed to. Defaults to * {@link issueRepo}. Point it at a PRIVATE repo when the issue itself is * public — see the twin-target section above. */ readonly evidenceRepo?: string; /** * The GitHub token. Falls back to the `GITHUB_TOKEN` environment variable. * It needs **Contents: read/write on `evidenceRepo`** and **Issues: * read/write on `issueRepo`**; filing an issue on a public repo needs only a * valid account token. This is a secret: it is sent as an `Authorization` * header and appears in no message this adapter can throw. */ readonly token?: string; /** Directory inside `evidenceRepo` for the bundles. Default `'bug-reports'`. */ readonly dir?: string; /** Labels applied to the issue. Default: none. */ readonly labels?: readonly string[]; /** Branch to commit the evidence to. Default: the repo's default branch. */ readonly branch?: string; /** API root. Default `https://api.github.com`; for GitHub Enterprise Server * it is `https://github.your-company.com/api/v3`. */ readonly apiBase?: string; /** * Commit evidence to a PUBLIC repo deliberately. * * Off by default: a bundle carries a real run — prompts, tool arguments, * retrieved documents — and a public repo publishes it to the internet * permanently. Set this only when the evidence is synthetic, or when a human * has read the manifest and decided. */ readonly acknowledgePublicEvidence?: boolean; /** Refuse a zip larger than this before uploading. Default 24 MB. */ readonly maxZipBytes?: number; /** Test seam — inject `fetch`. Bypasses the network entirely. */ readonly _fetch?: typeof fetch; } /** What a filed report leaves behind. */ export interface FiledBugReport { /** The issue, on `issueRepo`. */ readonly issueUrl: string; /** The committed bundle's blob page, on `evidenceRepo`. */ readonly zipUrl: string; /** Path inside `evidenceRepo`. */ readonly zipPath: string; /** `owner/name` the evidence went to. */ readonly evidenceRepo: string; /** * Whether the evidence repo's visibility was actually READ before committing. * * `false` means the metadata call failed — usually a token that can write * contents but not read repository metadata. The commit proceeds (a * permissions quirk must not block a bug report) and this field says the * guard did not run, so nobody mistakes an unchecked upload for a checked one. */ readonly checkedVisibility: boolean; /** The answer, when the check ran. */ readonly evidenceRepoPrivate?: boolean; } /** Files a finished {@link BugReport}. */ export interface BugReporter { file(report: BugReport): Promise; } export declare function githubBugReporter(options: GithubBugReporterOptions): BugReporter; //# sourceMappingURL=githubBugReporter.d.ts.map