Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ jobs:
- name: No word runs into a tag
run: pnpm check:spacing

- name: The markdown twins are real
# Generated from the built HTML, so they cannot drift from the pages.
# What they can do is come out empty if the conversion breaks, and an
# empty twin tells a reader the page says almost nothing.
run: |
for page in index results; do
file="dist/$page.md"
if [ ! -f "$file" ]; then
echo "$file is missing, so the build did not produce a twin." >&2
exit 1
fi
if [ "$(wc -c < "$file")" -lt 1000 ]; then
echo "$file is too small to be the page it claims to be." >&2
exit 1
fi
if grep -qE '</[a-z]+>' "$file"; then
echo "$file still carries HTML tags, so the conversion is wrong:" >&2
grep -nE '</[a-z]+>' "$file" | head -3 >&2
exit 1
fi
done
echo "Both twins are present and readable."

- name: No client JavaScript ships
# The site's Content-Security-Policy says script-src 'none'. If a build
# ever emits a script this fails here rather than in a browser console
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build": "astro build && node scripts/make-markdown.mjs",
"check:spacing": "node scripts/check-spacing.mjs",
"preview": "astro preview",
"typecheck": "astro check",
Expand Down
5 changes: 5 additions & 0 deletions public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Every judgment comes back as a probability rather than a verdict. Thresholds liv
- What it is not: a sandbox. It sees what a call says it will do and cannot stop a server doing something the call did not describe.
- What leaves the machine: the arguments and results being screened go to the configured model backend. Secret-shaped strings are replaced first, and screening can be turned off per server.

## This site, as markdown

- [Overview](https://agentchaperone.dev/index.md): what it screens, what it is not, how it compares, and the shadow-to-enforce path
- [Measured results](https://agentchaperone.dev/results.md): the same tables as the HTML page

## Docs

- [README](https://raw.githubusercontent.com/agent-chaperone/agent-chaperone/main/README.md): install, the two screens, the commands, and the measured results
Expand Down
169 changes: 169 additions & 0 deletions scripts/make-markdown.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* A markdown twin of every built page.
*
* An assistant fetching a page gets markup it has to strip before it can read
* anything, and what it strips is the part carrying the meaning: which text was
* a heading, which was a table cell, which was a link. Serving the same content
* as markdown hands it the structure instead of making it infer one.
*
* Generated from the built HTML rather than written by hand, because a twin
* that drifts from the page is worse than no twin: it says something the site
* does not, with the site's authority. Nothing here is a second source of
* truth; the pages remain the only one.
*
* Run after `astro build`. Writes dist/index.md beside dist/index.html.
*/

import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

const DIST = 'dist';
const SITE = 'https://agentchaperone.dev';

/** Entities the pages actually produce, decoded so the markdown reads as text. */
const ENTITIES = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
'&mdash;': '-',
'&ndash;': '-',
};

function decode(text) {
return text.replace(/&[a-z#0-9]+;/gi, (found) => ENTITIES[found] ?? found);
}

/** Tags stripped to their text, with the markdown that carries the same meaning. */
function inline(html) {
return decode(
html
.replace(/<code[^>]*>(.*?)<\/code>/gis, (_, inner) => `\`${inner.replace(/<[^>]*>/g, '')}\``)
.replace(/<(?:b|strong)[^>]*>(.*?)<\/(?:b|strong)>/gis, '**$1**')
.replace(/<(?:i|em)[^>]*>(.*?)<\/(?:i|em)>/gis, '_$1_')
// A link keeps its destination. Relative ones are made absolute, since a
// reader of the markdown has no page to resolve them against.
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gis, (_, href, text) => {
const clean = text.replace(/<[^>]*>/g, '').trim();
if (clean === '' || clean === '#') {
return '';
}
const url = href.startsWith('/') ? `${SITE}${href}` : href;
return url.startsWith('#') ? clean : `[${clean}](${url})`;
})
.replace(/<[^>]*>/g, ''),
)
.replace(/[ \t]+/g, ' ')
.trim();
}

function cells(row, tag) {
return [...row.matchAll(new RegExp(`<${tag}[^>]*>(.*?)</${tag}>`, 'gis'))].map((m) =>
inline(m[1]).replace(/\|/g, '\\|'),
);
}

/** One table, as a markdown table. The tables here carry the numbers. */
function table(html) {
const rows = [...html.matchAll(/<tr[^>]*>(.*?)<\/tr>/gis)].map((m) => m[1]);
if (rows.length === 0) {
return '';
}
const head = cells(rows[0], 'th');
const body = rows.slice(head.length > 0 ? 1 : 0).map((row) => cells(row, 'td'));
const width = Math.max(head.length, ...body.map((r) => r.length), 1);
const pad = (r) => [...r, ...Array(width - r.length).fill('')];
const out = [];
out.push(`| ${pad(head.length > 0 ? head : Array(width).fill('')).join(' | ')} |`);
out.push(`| ${Array(width).fill('---').join(' | ')} |`);
for (const row of body) {
if (row.length > 0) {
out.push(`| ${pad(row).join(' | ')} |`);
}
}
return out.join('\n');
}

function convert(html) {
const main = /<main[^>]*>(.*?)<\/main>/is.exec(html)?.[1] ?? '';
const out = [];

// Block elements in the order they appear, so the twin reads in page order.
const blocks = main.matchAll(/<(h1|h2|h3|h4|p|pre|ul|ol|table)\b[^>]*>(.*?)<\/\1>/gis);

for (const [, tag, inner] of blocks) {
if (tag === 'pre') {
const code = decode(inner.replace(/<[^>]*>/g, '')).trim();
out.push(`\`\`\`\n${code}\n\`\`\``);
continue;
}
if (tag === 'table') {
const rendered = table(inner);
if (rendered !== '') {
out.push(rendered);
}
continue;
}
if (tag === 'ul' || tag === 'ol') {
const items = [...inner.matchAll(/<li[^>]*>(.*?)<\/li>/gis)]
.map((m) => inline(m[1]))
.filter((one) => one !== '');
if (items.length > 0) {
out.push(
items.map((one, at) => (tag === 'ol' ? `${at + 1}. ${one}` : `- ${one}`)).join('\n'),
);
}
continue;
}
const text = inline(inner);
if (text === '') {
continue;
}
const level = { h1: '# ', h2: '## ', h3: '### ', h4: '#### ' }[tag] ?? '';
out.push(`${level}${text}`);
}

return out.join('\n\n');
}

function main() {
const pages = readdirSync(DIST).filter((name) => name.endsWith('.html') && name !== '404.html');
let written = 0;
for (const page of pages) {
const html = readFileSync(join(DIST, page), 'utf8');
const title = /<title>(.*?)<\/title>/is.exec(html)?.[1] ?? '';
const description = /<meta name="description" content="([^"]*)"/i.exec(html)?.[1] ?? '';
const path = page === 'index.html' ? '/' : `/${page.replace(/\.html$/, '')}`;
const body = convert(html);

if (body.length < 200) {
// A twin that is nearly empty means the conversion missed the content, and
// shipping it would tell a reader this page says almost nothing.
throw new Error(`${page}: the markdown twin came out empty, so the conversion is wrong`);
}

const front = [
`<!-- Generated from ${path} by scripts/make-markdown.mjs. The page is the source. -->`,
'',
body,
'',
'---',
'',
`Source: ${SITE}${path}`,
decode(description) === '' ? '' : decode(description),
]
.filter((one) => one !== undefined)
.join('\n');

writeFileSync(join(DIST, page.replace(/\.html$/, '.md')), `${front}\n`);
written += 1;
console.log(`${path} -> ${page.replace(/\.html$/, '.md')} (${body.length} chars, "${title}")`);
}
if (written === 0) {
throw new Error('no pages were converted, which means dist/ was not built');
}
}

main();
11 changes: 11 additions & 0 deletions src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ const updated = RUN.date;
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{/* The same content as markdown, for a reader that would rather not strip
markup to find the structure. */}
<link
rel="alternate"
type="text/markdown"
href={
canonical === 'https://agentchaperone.dev/'
? 'https://agentchaperone.dev/index.md'
: `${canonical}.md`
}
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="agent-chaperone" />
<meta property="og:title" content={ogTitle ?? title} />
Expand Down
46 changes: 40 additions & 6 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,49 @@
"key": "Strict-Transport-Security",
"value": "max-age=63072000; includeSubDomains; preload"
},
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "no-referrer" },
{ "key": "X-Frame-Options", "value": "DENY" },
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "no-referrer"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "Permissions-Policy",
"value": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()"
},
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
}
]
},
{
"source": "/(.*).md",
"headers": [
{
"key": "Content-Type",
"value": "text/markdown; charset=utf-8"
},
{
"key": "Cache-Control",
"value": "public, max-age=0, must-revalidate"
}
]
},
{
"source": "/_astro/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
],
"redirects": [
Expand Down Expand Up @@ -60,6 +90,10 @@
"destination": "https://github.com/agent-chaperone/agent-chaperone/blob/main/ROADMAP.md",
"permanent": true
},
{ "source": "/benchmark", "destination": "/results", "permanent": true }
{
"source": "/benchmark",
"destination": "/results",
"permanent": true
}
]
}
Loading