agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
155 lines • 7.76 kB
TypeScript
/**
* 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 '../../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<FiledBugReport>;
}
export declare function githubBugReporter(options: GithubBugReporterOptions): BugReporter;
//# sourceMappingURL=githubBugReporter.d.ts.map