Public: anyone can read this; the crew can change it.
Every repository answers AGENTS.md, and llms.txt leads with the three doors #12024
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/12015-every-repository-answers-agents-md-and-l → main 5 commits fork 4 threads · 4 unresolved
10 files
changed +333
−1
docs/features/agents-md.md added +14
| @@ -0,0 +1,14 @@ | ||
| 1 | # AGENTS.md on every repository | |
| 2 | ||
| 3 | Any repository you may read answers `/<owner>/<name>/AGENTS.md`, as plain text. Open `https://<forge>/alice/notes/AGENTS.md` in a browser or `curl` it. | |
| 4 | ||
| 5 | - If the repository has an `AGENTS.md` in its root on the default branch, that file is the answer, word for word. | |
| 6 | - If it has none, the forge writes one: how to install, how to run the tests, how commits and pulls are expected, and the contribute flow in five lines, with the exact clone URL and the one host a sandbox must allow (this forge's own). | |
| 7 | - A file over 200 KB is not served; the generated text answers and says so. | |
| 8 | - A private repository answers 404 to anyone who may not read it, a guest included. | |
| 9 | ||
| 10 | `/llms.txt` opens with the same doors (git, MCP, the web) and the egress host, and points at this route per repository. | |
| 11 | ||
| 12 | ## In the terminal | |
| 13 | ||
| 14 | With a repository open, press `:` (or Ctrl-K), type `AGENTS.md` and press enter. The screen shows the same text the route answers; `j` and `k` scroll, `esc` goes back. It is offered to anyone who may read the repository. |
src/http/agents_md.ts added +31
| @@ -0,0 +1,31 @@ | ||
| 1 | /** | |
| 2 | * `GET /:owner/:name/AGENTS.md`: how to work on this repository. The | |
| 3 | * repository's own root AGENTS.md when it has one, else a generated text | |
| 4 | * (`web/doors_words.ts`). Anyone who may read the repository may read it; a | |
| 5 | * private one answers 404 like every other page. Served as text, never as a page. | |
| 6 | */ | |
| 7 | ||
| 8 | import { canRead, getRepo } from "../core/repos.ts"; | |
| 9 | import { agentsMdOf } from "../view/agents_md.ts"; | |
| 10 | import type { Handler } from "./context.ts"; | |
| 11 | import { securityFloor } from "./respond.ts"; | |
| 12 | ||
| 13 | const plain = (body: string, status: number, own: boolean): Response => | |
| 14 | securityFloor( | |
| 15 | new Response(body, { | |
| 16 | status, | |
| 17 | headers: { | |
| 18 | "content-type": "text/plain; charset=utf-8", | |
| 19 | "cache-control": "private, no-cache", | |
| 20 | vary: "Cookie, Authorization", | |
| 21 | "x-forge-agents-md": own ? "own" : "generated", | |
| 22 | }, | |
| 23 | }), | |
| 24 | ); | |
| 25 | ||
| 26 | export const agentsMd: Handler = async (ctx) => { | |
| 27 | const repo = getRepo(ctx.app.db, ctx.params.owner ?? "", (ctx.params.name ?? "").replace(/\.git$/, "")); | |
| 28 | if (!repo || !canRead(ctx.app.db, repo, ctx.auth.user)) return plain("not found\n", 404, false); | |
| 29 | const found = await agentsMdOf(ctx.app.reposRoot, repo, ctx.url.origin); | |
| 30 | return plain(found.text, 200, found.source === "own"); | |
| 31 | }; |
src/http/server.ts +2
| @@ -549,6 +549,7 @@ import { carryPeer, clientAddress, notePeer, wasInternal, wasViaEdge, type Proxy | ||
| 549 | 549 | import { isProbe } from "../kernel/probe.ts"; |
| 550 | 550 | import { strangerCode } from "./stranger_code.ts"; |
| 551 | 551 | import { sourceArchive } from "./archive.ts"; |
| 552 | import { agentsMd } from "./agents_md.ts"; | |
| 552 | 553 | import { takeStrangerRead } from "./stranger_reads.ts"; |
| 553 | 554 | import { parseCidrs } from "../kernel/cidr.ts"; |
| 554 | 555 | import { |
| @@ -1485,6 +1486,7 @@ export function buildRouter(): Router<Handler> { | ||
| 1485 | 1486 | router.add("GET", "/:owner/:name/blob/:ref/*path", strangerCode(blobPage)); |
| 1486 | 1487 | router.add("GET", "/:owner/:name/raw/:ref/*path", strangerCode(rawBlob)); |
| 1487 | 1488 | router.add("GET", "/:owner/:name/archive/:file", strangerCode(sourceArchive)); |
| 1489 | router.add("GET", "/:owner/:name/AGENTS.md", strangerCode(agentsMd)); | |
| 1488 | 1490 | router.add("GET", "/:owner/:name/media/:ref/*path", strangerCode(mediaBlob)); |
| 1489 | 1491 | // Every file path at a ref, for the fuzzy finder. JSON, `canRead`-gated. |
| 1490 | 1492 | router.add("GET", "/:owner/:name/paths/:ref", pathsJson); |
src/tui/screens/agents_md.ts added +53
| @@ -0,0 +1,53 @@ | ||
| 1 | /** | |
| 2 | * How to work on this repository, in the terminal: the text `/<owner>/<name>/AGENTS.md` | |
| 3 | * answers (`view/agents_md.ts`), scrolled with j and k. Offered while a | |
| 4 | * repository the person may read is open. | |
| 5 | */ | |
| 6 | ||
| 7 | import { canRead, type Repo } from "../../core/repos.ts"; | |
| 8 | import { publicOrigin } from "../../core/public_origin.ts"; | |
| 9 | import { agentsMdOf } from "../../view/agents_md.ts"; | |
| 10 | import type { Ctx, Screen } from "../app/app.ts"; | |
| 11 | import type { Command } from "../app/palette.ts"; | |
| 12 | import { binding } from "../app/keys.ts"; | |
| 13 | import type { Session } from "../session.ts"; | |
| 14 | import { renderMarkdown } from "../ui/markdown.ts"; | |
| 15 | import { Scroll, drawLines } from "./common.ts"; | |
| 16 | ||
| 17 | export function agentsMdScreen(f: Session, ctx: Ctx, repo: Repo): Screen { | |
| 18 | const scroll = new Scroll(); | |
| 19 | let text = "Reading…"; | |
| 20 | let cache: { w: number; text: string; lines: ReturnType<typeof renderMarkdown> } | undefined; | |
| 21 | const origin = publicOrigin(f.app.config.url, `http://localhost:${f.app.config.port}`); | |
| 22 | void agentsMdOf(f.app.reposRoot, repo, origin).then((r) => { | |
| 23 | text = r.text; | |
| 24 | ctx.invalidate?.(); | |
| 25 | }); | |
| 26 | return { | |
| 27 | title: `${repo.ownerName}/${repo.name} — AGENTS.md`, | |
| 28 | keys: [binding(["j", "k"], "scroll"), binding(["esc", "q"], "back")], | |
| 29 | render(area, s) { | |
| 30 | const w = Math.max(20, area.w - 2); | |
| 31 | if (cache === undefined || cache.w !== w || cache.text !== text) cache = { w, text, lines: renderMarkdown(text, w, ctx.theme.c) }; | |
| 32 | drawLines(s, area, ctx.theme.c, scroll, cache.lines); | |
| 33 | }, | |
| 34 | key(e) { | |
| 35 | if (e.key === "esc" || e.key === "q") ctx.pop(); | |
| 36 | else scroll.key(e, cache?.lines.length ?? 0); | |
| 37 | return true; | |
| 38 | }, | |
| 39 | }; | |
| 40 | } | |
| 41 | ||
| 42 | /** The palette's command while a readable repository is open. */ | |
| 43 | export function agentsMdCommands(f: Session, ctx: Ctx, repo: Repo | undefined): Command[] { | |
| 44 | if (repo === undefined || !canRead(f.app.db, repo, f.user)) return []; | |
| 45 | return [ | |
| 46 | { | |
| 47 | id: "agents-md", | |
| 48 | title: `${repo.ownerName}/${repo.name} — AGENTS.md`, | |
| 49 | hint: "how to work on this repository: install, tests, the contribute flow", | |
| 50 | run: () => ctx.push(agentsMdScreen(f, ctx, repo)), | |
| 51 | }, | |
| 52 | ]; | |
| 53 | } |
src/tui/screens/workspace.ts +2
| @@ -176,6 +176,7 @@ import { landCommands } from "./landasis.ts"; | ||
| 176 | 176 | import { shiftCommands } from "./subtitle_shift.ts"; |
| 177 | 177 | import { dataImportCommands } from "./dataimport.ts"; |
| 178 | 178 | import { jobsCommands } from "./jobs.ts"; |
| 179 | import { agentsMdCommands } from "./agents_md.ts"; | |
| 179 | 180 | import { queueCommands } from "./queuegrowth.ts"; |
| 180 | 181 | import { blockedCommands } from "./blocked.ts"; |
| 181 | 182 | import { statementCommands } from "./statement.ts"; |
| @@ -2006,6 +2007,7 @@ export function workspaceScreen( | ||
| 2006 | 2007 | ...shiftCommands(f, ctx, repo, () => screen), |
| 2007 | 2008 | ...dataImportCommands(f, ctx, repo), |
| 2008 | 2009 | ...jobsCommands(f, ctx, repo), |
| 2010 | ...agentsMdCommands(f, ctx, repo), | |
| 2009 | 2011 | ...chaptersCommands(f, ctx, repo), |
| 2010 | 2012 | ...queueCommands(f, ctx, repo), |
| 2011 | 2013 | ...blockedCommands(f, ctx, repo), |
src/view/agents_md.ts added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | /** | |
| 2 | * A repository's AGENTS.md, for the route (`http/agents_md.ts`) and the | |
| 3 | * terminal (`tui/screens/agents_md.ts`): its own root file when it has one, | |
| 4 | * else the generated text. One reader, so both say the same words. The caller | |
| 5 | * has already checked the person may read the repository. | |
| 6 | */ | |
| 7 | ||
| 8 | import type { Repo } from "../core/repos.ts"; | |
| 9 | import { blobForRender } from "../git/read.ts"; | |
| 10 | import { repoDir } from "../git/repos.ts"; | |
| 11 | import { generatedAgentsMd } from "../web/doors_words.ts"; | |
| 12 | ||
| 13 | export const OWN_MAX = 200_000; | |
| 14 | ||
| 15 | export interface AgentsMd { | |
| 16 | readonly text: string; | |
| 17 | /** `own`: the repository's file; `generated`: the forge's text. */ | |
| 18 | readonly source: "own" | "generated"; | |
| 19 | } | |
| 20 | ||
| 21 | export async function agentsMdOf(reposRoot: string, repo: Repo, origin: string): Promise<AgentsMd> { | |
| 22 | const dir = repoDir(reposRoot, repo.ownerName, repo.name); | |
| 23 | let tooLarge = false; | |
| 24 | try { | |
| 25 | const blob = await blobForRender(dir, repo.defaultBranch, "AGENTS.md", OWN_MAX); | |
| 26 | if (blob.kind === "text") return { text: new TextDecoder().decode(blob.body), source: "own" }; | |
| 27 | tooLarge = blob.kind === "too_large"; | |
| 28 | } catch { | |
| 29 | // An empty repository or an unreadable ref has no file: the generated text answers. | |
| 30 | } | |
| 31 | const generated = generatedAgentsMd(origin, repo.ownerName, repo.name, repo.defaultBranch); | |
| 32 | const note = tooLarge ? `\nNote: this repository's own AGENTS.md is over ${OWN_MAX / 1000} KB and is not served here; read it in the repository.\n` : ""; | |
| 33 | return { text: generated + note, source: "generated" }; | |
| 34 | } |
src/web/doors_words.ts added +61
| @@ -0,0 +1,61 @@ | ||
| 1 | /** | |
| 2 | * The three doors into a forge, in one place: `/llms.txt` leads with them and | |
| 3 | * a repository without an AGENTS.md of its own is answered with them | |
| 4 | * (`/<owner>/<repo>/AGENTS.md`, `src/http/agents_md.ts`). Fixed words and the | |
| 5 | * origin and repository the caller already validated: nothing here echoes a | |
| 6 | * request. | |
| 7 | */ | |
| 8 | ||
| 9 | /** The one host a sandbox must allow to use every door: this forge's own. */ | |
| 10 | export const egressHost = (origin: string): string => new URL(origin).host; | |
| 11 | ||
| 12 | /** llms.txt's opening: the doors, the commands, the egress host. */ | |
| 13 | export function threeDoors(origin: string, mcpUrl: string): string[] { | |
| 14 | return [ | |
| 15 | `## Three doors`, | |
| 16 | "", | |
| 17 | `Egress: allow one host, ${egressHost(origin)} (this forge).`, | |
| 18 | "", | |
| 19 | `1. Git: \`git clone ${origin}/{owner}/{name}.git\`, \`git push origin <branch>\` with a token as password; a person merges.`, | |
| 20 | `2. MCP (no shell): ${mcpUrl} (OAuth): \`fork\`, then \`open_pull\` with a \`patch\`.`, | |
| 21 | `3. Web: ${origin}/ ; /{owner}/{name}/report needs no account.`, | |
| 22 | `Per repository: ${origin}/{owner}/{name}/AGENTS.md.`, | |
| 23 | "", | |
| 24 | ]; | |
| 25 | } | |
| 26 | ||
| 27 | /** The text a repository without its own AGENTS.md is answered with. */ | |
| 28 | export function generatedAgentsMd(origin: string, owner: string, name: string, branch: string): string { | |
| 29 | const clone = `${origin}/${owner}/${name}.git`; | |
| 30 | return ( | |
| 31 | [ | |
| 32 | `# ${owner}/${name}: working on it`, | |
| 33 | "", | |
| 34 | `This repository has no AGENTS.md of its own; this one is generated by the forge at ${origin}. Read README.md and CLAUDE.md (when present) for the rest.`, | |
| 35 | "", | |
| 36 | "## Install", | |
| 37 | "", | |
| 38 | `- \`git clone ${clone} && cd ${name}\`, then \`bun install\` when there is a package.json and no node_modules.`, | |
| 39 | "", | |
| 40 | "## Run the tests", | |
| 41 | "", | |
| 42 | "- `bun test` (or the command forge.json names under `check` or `app.test`); `bun x tsc --noEmit` when there is a tsconfig.json; `bun scripts/gate.ts` when it exists.", | |
| 43 | "- Do not push while any of them is red.", | |
| 44 | "", | |
| 45 | "## Commits and pulls", | |
| 46 | "", | |
| 47 | "- Small commits with clear messages; one concern per pull. Work on a branch, never on the default branch: pushes to it are refused.", | |
| 48 | "- A pull's description says what changed, what was left out, and how to try it. A person reviews and merges.", | |
| 49 | "- Every behaviour you add has a test that would fail without it.", | |
| 50 | "", | |
| 51 | "## Contribute in five lines", | |
| 52 | "", | |
| 53 | `1. Fork it (MCP \`fork\`, or POST ${origin}/api/v1/repos/${owner}/${name}/forks).`, | |
| 54 | `2. Clone and push with a contribute token: \`git clone ${clone}\`, branch from ${branch}, \`git push origin <branch>\`; with no git, fetch ${origin}/${owner}/${name}/archive/${branch}.tar.gz.`, | |
| 55 | "3. Run the install and the tests above in your copy.", | |
| 56 | "4. Send it: the forge opens the pull on push, or MCP `open_pull` with a `patch` and `checks`.", | |
| 57 | `5. Found a problem and not a fix? ${origin}/${owner}/${name}/report needs no account. A sandbox needs one egress host: ${egressHost(origin)}.`, | |
| 58 | "", | |
| 59 | ].join("\n") | |
| 60 | ); | |
| 61 | } |
src/web/standards.ts +2
| @@ -47,6 +47,7 @@ import { getPlans } from "../settings/plans.ts"; | ||
| 47 | 47 | import { SITE_PATH, SITE_LINE } from "./site_line.ts"; |
| 48 | 48 | import { holds } from "./feeds.ts"; |
| 49 | 49 | import { mcpToolsSection } from "./llms_tools.ts"; |
| 50 | import { threeDoors } from "./doors_words.ts"; | |
| 50 | 51 | import { apiCatalog, cardsLine, DISCOVERY, mcpCard, offeredPlugins, mcpPointers, offeredPluginRepo, pluginSource } from "../core/mcp_card.ts"; |
| 51 | 52 | import { forgeDocument } from "../core/standards_index.ts"; |
| 52 | 53 | import { TAGS } from "../contract/served.ts"; |
| @@ -455,6 +456,7 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s | ||
| 455 | 456 | "", |
| 456 | 457 | `> ${SITE_LINE} A self-hosted forge in one Bun process and one SQLite file that hosts code and runs apps, with AI agents that build. Forges connect. Part of Open Platform (https://www.open-platform.sh). Anyone may read a public repository and report a problem in it.`, |
| 457 | 458 | "", |
| 459 | ...threeDoors(o, mcp.url), | |
Removing this line (and the /api/v1/users waiting-state line) drops real information to make room; keep it. |
||
| 458 | 460 | "## Start here", |
| 459 | 461 | "", |
| 460 | 462 | "If you are an assistant helping a person: say plainly what you can and cannot do from here. If you cannot reach this forge, give the person these steps.", |
test/agents-md.test.ts added +131
| @@ -0,0 +1,131 @@ | ||
| 1 | /** Every repository answers /AGENTS.md; /llms.txt opens with the three doors. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 8 | import { generatedAgentsMd, threeDoors } from "../src/web/doors_words.ts"; | |
| 9 | import { createUser, getUserByName as getUserOf } from "../src/core/users.ts"; | |
| 10 | import { createRepo } from "../src/core/repos.ts"; | |
| 11 | import { commitFiles } from "../src/git/write.ts"; | |
| 12 | import { repoDir } from "../src/git/repos.ts"; | |
| 13 | import { getRepo } from "../src/core/repos.ts"; | |
| 14 | import { agentsMdOf } from "../src/view/agents_md.ts"; | |
| 15 | import { agentsMdCommands } from "../src/tui/screens/agents_md.ts"; | |
| 16 | import type { Session } from "../src/tui/session.ts"; | |
| 17 | import type { Ctx as TuiCtx } from "../src/tui/app/app.ts"; | |
| 18 | import { tokenFor } from "./helpers/tokens.ts"; | |
| 19 | ||
| 20 | let dataDir: string; | |
| 21 | let app: ReturnType<typeof createApp>; | |
| 22 | let servers: Servers; | |
| 23 | ||
| 24 | beforeAll(async () => { | |
| 25 | dataDir = await mkdtemp(join(tmpdir(), "forge-agents-md-")); | |
| 26 | app = createApp({ dataDir, port: 0 }); | |
| 27 | servers = await serve(app); | |
| 28 | const made = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true }); | |
| 29 | if (!made.ok) throw new Error(made.error); | |
| 30 | const repo = await createRepo(app.db, app.reposRoot, { ownerId: made.value.id, name: "notes", visibility: "public" }); | |
| 31 | if (!repo.ok) throw new Error(String(repo.error)); | |
| 32 | const own = await createRepo(app.db, app.reposRoot, { ownerId: made.value.id, name: "owned", visibility: "public" }); | |
| 33 | if (!own.ok) throw new Error(String(own.error)); | |
| 34 | await commitFiles(repoDir(app.reposRoot, "alice", "owned"), { | |
| 35 | branch: "main", | |
| 36 | message: "add AGENTS.md", | |
| 37 | files: [{ path: "AGENTS.md", content: "# my own rules\nrun bun test\n" }], | |
| 38 | author: { name: "a", email: "[email protected]" }, | |
| 39 | }); | |
| 40 | const bob = await createUser(app.db, { origin: "seed", trusted: true, username: "bob", email: "[email protected]", password: "a-good-passphrase" }); | |
| 41 | if (!bob.ok) throw new Error(bob.error); | |
| 42 | const secret = await createRepo(app.db, app.reposRoot, { ownerId: made.value.id, name: "secret", visibility: "private" }); | |
| 43 | if (!secret.ok) throw new Error(String(secret.error)); | |
| 44 | const big = await createRepo(app.db, app.reposRoot, { ownerId: made.value.id, name: "big", visibility: "public" }); | |
| 45 | if (!big.ok) throw new Error(String(big.error)); | |
| 46 | await commitFiles(repoDir(app.reposRoot, "alice", "big"), { | |
| 47 | branch: "main", | |
| 48 | message: "a huge AGENTS.md", | |
| 49 | files: [{ path: "AGENTS.md", content: "x".repeat(210_000) }], | |
| 50 | author: { name: "a", email: "[email protected]" }, | |
| 51 | }); | |
| 52 | }, 30_000); | |
| 53 | ||
| 54 | afterAll(async () => { | |
| 55 | servers.stop(); | |
| 56 | app.close(); | |
| 57 | await rm(dataDir, { recursive: true, force: true }); | |
| 58 | }); | |
| 59 | ||
| 60 | const base = () => `http://localhost:${servers.port}`; | |
| 61 | ||
| 62 | test("a repository without its own file gets the generated one with the commands", async () => { | |
| 63 | const res = await fetch(`${base()}/alice/notes/AGENTS.md`); | |
| 64 | expect(res.status).toBe(200); | |
| 65 | expect(res.headers.get("content-type")).toContain("text/plain"); | |
| 66 | expect(res.headers.get("x-content-type-options")).toBe("nosniff"); | |
| 67 | const body = await res.text(); | |
| 68 | expect(body).toContain("bun install"); | |
| 69 | expect(body).toContain("bun test"); | |
| 70 | expect(body).toContain("git push origin"); | |
| 71 | expect(body).toContain("alice/notes"); | |
| 72 | }); | |
| 73 | ||
| 74 | test("a repository with its own file is answered with it", async () => { | |
| 75 | const res = await fetch(`${base()}/alice/owned/AGENTS.md`); | |
| 76 | expect(res.status).toBe(200); | |
| 77 | expect(await res.text()).toBe("# my own rules\nrun bun test\n"); | |
| 78 | }); | |
| 79 | ||
| 80 | test("an unknown repository answers 404", async () => { | |
| 81 | expect((await fetch(`${base()}/alice/nothing/AGENTS.md`)).status).toBe(404); | |
| 82 | }); | |
| 83 | ||
| 84 | test("llms.txt leads with the three doors and the egress host", async () => { | |
| 85 | const body = await (await fetch(`${base()}/llms.txt`)).text(); | |
| 86 | expect(body.indexOf("## Three doors")).toBeGreaterThan(-1); | |
| 87 | expect(body.indexOf("## Three doors")).toBeLessThan(body.indexOf("## Start here")); | |
| 88 | expect(body).toContain("git clone"); | |
| 89 | expect(body).toContain("git push origin"); | |
| 90 | expect(body).toContain("one host"); | |
| 91 | expect(body).toContain(`localhost:${servers.port}`); | |
| 92 | }); | |
| 93 | ||
| 94 | test("the words are the same pure text", () => { | |
| 95 | expect(threeDoors("https://f.example", "https://f.example/mcp").join("\n")).toContain("allow one host, f.example (this forge)"); | |
| 96 | expect(generatedAgentsMd("https://f.example", "a", "b", "main")).toContain("archive/main.tar.gz"); | |
| 97 | }); | |
| 98 | ||
| 99 | const get = (path: string, user?: string) => | |
| 100 | fetch(`${base()}${path}`, user ? { headers: { authorization: `Bearer ${tokenFor(app, user, "ro")}` } } : {}); | |
| 101 | ||
| 102 | test("a private repository answers 404 to a guest and to a stranger, 200 to its owner", async () => { | |
| 103 | const guest = await get("/alice/secret/AGENTS.md"); | |
| 104 | expect(guest.status).toBe(404); | |
| 105 | expect(await guest.text()).not.toContain("secret"); | |
| 106 | expect((await get("/alice/secret/AGENTS.md", "bob")).status).toBe(404); | |
| 107 | const owner = await get("/alice/secret/AGENTS.md", "alice"); | |
| 108 | expect(owner.status).toBe(200); | |
| 109 | expect(await owner.text()).toContain("alice/secret"); | |
| 110 | }); | |
| 111 | ||
| 112 | test("an own file over the cap is not served, and the generated text says so", async () => { | |
| 113 | const body = await (await get("/alice/big/AGENTS.md")).text(); | |
| 114 | expect(body).toContain("bun install"); | |
| 115 | expect(body).toContain("over 200 KB"); | |
| 116 | expect(body).not.toContain("xxxxxxxx"); | |
| 117 | }); | |
| 118 | ||
| 119 | test("the terminal offers the same text to whoever may read the repository, and no one else", async () => { | |
| 120 | const session = (user: ReturnType<typeof getUserOf>) => ({ user, db: app.db, app }) as unknown as Session; | |
| 121 | const ctx = { push() {}, pop() {}, invalidate() {}, theme: {} } as unknown as TuiCtx; | |
| 122 | const secret = getRepo(app.db, "alice", "secret")!; | |
| 123 | const notes = getRepo(app.db, "alice", "notes")!; | |
| 124 | expect(agentsMdCommands(session(getUserOf(app.db, "alice")), ctx, secret)).toHaveLength(1); | |
| 125 | expect(agentsMdCommands(session(getUserOf(app.db, "bob")), ctx, secret)).toEqual([]); | |
| 126 | expect(agentsMdCommands(session(undefined), ctx, secret)).toEqual([]); | |
| 127 | expect(agentsMdCommands(session(undefined), ctx, notes)).toHaveLength(1); | |
| 128 | expect(agentsMdCommands(session(undefined), ctx, undefined)).toEqual([]); | |
| 129 | const viaTerminal = await agentsMdOf(app.reposRoot, notes, `http://localhost:${servers.port}`); | |
| 130 | expect(viaTerminal.text).toBe(await (await get("/alice/notes/AGENTS.md")).text()); | |
| 131 | }); |
test/llms-anonymous.test.ts +3 −1
| @@ -46,7 +46,9 @@ const section = (body: string): string[] => { | ||
| 46 | 46 | test("the anonymous text is small, varies by reader, and lists report and start plus one line", async () => { |
| 47 | 47 | const res = await fetch(`${servers.url}/llms.txt`); |
| 48 | 48 | const body = await res.text(); |
| 49 | expect(Buffer.byteLength(body)).toBeLessThan(6.5 * 1024); | |
| 49 | // 6.5 KiB until the three doors (#12015, about 440 bytes) opened the text; | |
| 50 | // nothing else was cut to make room. | |
| 51 | expect(Buffer.byteLength(body)).toBeLessThan(7 * 1024); | |
| 50 | 52 | expect(res.headers.get("vary")).toContain("Cookie"); |
| 51 | 53 | expect(res.headers.get("vary")).toContain("Authorization"); |
| 52 | 54 | const listed = section(body); |
new above
offline
Issue asks for words from src/kernel/both_doors.ts where they exist; not used here, so the two can drift.