Skip to content
Open
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
15 changes: 15 additions & 0 deletions .agents/setup
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ readonly pnpm_version="10.13.1"
readonly docs_theme_revision="450c498555135098c6a927adfdf13458be9be22a"
readonly docs_theme_icons_revision="71c3e482fe43e4cbc4f4327a26c8899b6bd4aaa7"
readonly docs_theme_dir="website/vendor/theme"
readonly lychee_version="0.24.2"
readonly lychee_linux_x64_sha256="73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5"

if [[ "$(node --version | cut -d. -f1)" != "v24" ]]; then
echo "Installing Node.js 24..."
Expand All @@ -18,6 +20,19 @@ if ! command -v pnpm >/dev/null 2>&1 || [[ "$(pnpm --version)" != "$pnpm_version
npm install --global "pnpm@${pnpm_version}"
fi

if ! command -v lychee >/dev/null 2>&1 || [[ "$(lychee --version)" != "lychee ${lychee_version}" ]]; then
echo "Installing lychee ${lychee_version}..."
lychee_archive="$(mktemp)"
lychee_dir="$(mktemp -d)"
curl -fsSL -o "$lychee_archive" \
"https://github.com/lycheeverse/lychee/releases/download/lychee-v${lychee_version}/lychee-x86_64-unknown-linux-musl.tar.gz"
echo "${lychee_linux_x64_sha256} ${lychee_archive}" | sha256sum -c -
tar -xzf "$lychee_archive" -C "$lychee_dir"
mkdir -p "$HOME/.local/bin"
install -m 0755 "$(find "$lychee_dir" -type f -name lychee -print -quit)" "$HOME/.local/bin/lychee"
rm -rf "$lychee_archive" "$lychee_dir"
fi

if [[ ! -f "${docs_theme_dir}/package.json" || ! -f "${docs_theme_dir}/vendor/icons/dist/index.js" ]]; then
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,19 @@ jobs:
git -C /tmp/docs-theme checkout 450c498555135098c6a927adfdf13458be9be22a
rm -rf website/vendor/theme && mkdir -p website/vendor
cp -r /tmp/docs-theme/packages/theme website/vendor/theme
git -C /tmp/docs-theme checkout 71c3e482fe43e4cbc4f4327a26c8899b6bd4aaa7
rm -rf website/vendor/theme/vendor/icons/dist
cp -r /tmp/docs-theme/packages/theme/vendor/icons/dist website/vendor/theme/vendor/icons/dist
- run: pnpm install --frozen-lockfile
- name: Install lychee
run: |
version=0.24.2
archive=/tmp/lychee.tar.gz
curl -fsSL -o "$archive" \
"https://github.com/lycheeverse/lychee/releases/download/lychee-v${version}/lychee-x86_64-unknown-linux-musl.tar.gz"
echo "73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5 $archive" | sha256sum -c -
tar -xzf "$archive" -C /tmp
sudo install -m 0755 /tmp/lychee-x86_64-unknown-linux-musl/lychee /usr/local/bin/lychee
- run: |
# Browser runtime sources remain in-tree, but are intentionally disabled
# until their sidecar/reactor architecture has a separate approved design.
Expand All @@ -60,6 +72,10 @@ jobs:
--filter='!@rivet-dev/agentos-runtime-browser' \
--filter='!@rivet-dev/agentos-playground'
fi
- name: Check documentation links and generated Markdown assets
run: |
pnpm --dir website build
pnpm exec tsx scripts/check-markdown-links.ts --built-site website/dist
# The Codex adapter is ordinary TypeScript, while its executable is a
# reproducibly built 56 MB WASI artifact. Keep the cheap PR lane source-only;
# nightly and publish workflows build the pinned executable explicitly.
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@ custom host-syscall imports. Treat that target as **native POSIX**;
- Runnable docs code must come from real checked example files via the docs
theme `<CodeSnippet>` mechanism. Inline code is fine only for shell commands,
config fragments, or non-runnable examples.
- Validate docs changes with `pnpm --dir website build` when the site changes.
- Whenever website, docs, README, or other Markdown content changes, run
`just check-site-links` before submitting the work. It builds the website and
verifies local and external source links plus client-generated Markdown
assets such as `/docs/apps.md`. Use `just check-site-links --no-external`
only for fast iteration, not as the final check.

## Tests

Expand Down
9 changes: 9 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,15 @@ docs:
docs-build:
pnpm --filter @rivet-dev/agentos-website build

# Check links in authored Markdown/MDX. Pass --no-external for a fast local-only check.
check-markdown-links *args:
pnpm exec tsx scripts/check-markdown-links.ts "$@"

# Build the website, then verify client-generated Markdown URLs and source links.
check-site-links *args:
pnpm --dir website build
pnpm exec tsx scripts/check-markdown-links.ts --built-site website/dist "$@"

test-bounded cmd='pnpm test':
#!/usr/bin/env bash
set -euo pipefail
Expand Down
17 changes: 17 additions & 0 deletions lychee.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Parsing and HTTP behavior are owned by lychee. The TypeScript launcher adds
# checkout-specific remaps for the website's /docs and /images routes.
no_progress = true
format = "detailed"
include_fragments = "anchor-only"
fallback_extensions = ["mdx", "md"]

timeout = 10
max_retries = 1
max_concurrency = 16
host_concurrency = 4
method = "get"
accept = ["100..=103", "200..=299", "401", "403", "429"]
user_agent = "agentOS-markdown-link-check/1.0"

include_mail = false
exclude_all_private = true
9 changes: 9 additions & 0 deletions packages/core/src/agent-os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3190,6 +3190,15 @@ export class AgentOs {
return this.#kernel.readFile(path);
}

async pread(
path: string,
offset: number,
length: number,
): Promise<Uint8Array> {
this._assertSafeAbsolutePath(path);
return this._vfs().pread(path, offset, length);
}

async writeFile(path: string, content: string | Uint8Array): Promise<void> {
this._assertWritableAbsolutePath(path);
return this.#kernel.writeFile(path, content);
Expand Down
1 change: 1 addition & 0 deletions scripts/check-layout.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const allowedTestHomes = [
/^software\/[^/]+\/test\/.+\.test\.ts$/,
/^toolchain\/conformance\/.+\.test\.ts$/,
/^packages\/[^/]+\/tests\/.+\.test\.ts$/,
/^benchmarks\/[^/]+\/.+\.test\.ts$/,
/^experiments\/[^/]+\/.+\.test\.ts$/,
/^scripts\/.+\.test\.ts$/,
];
Expand Down
5 changes: 4 additions & 1 deletion scripts/check-layout.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import test from "node:test";

const script = join(dirname(fileURLToPath(import.meta.url)), "check-layout.mjs");

test("allows experiment tests and ignores nested Claude worktrees", () => {
test("allows benchmark and experiment tests and ignores nested Claude worktrees", () => {
const root = mkdtempSync(join(tmpdir(), "agentos-layout-"));
try {
const nestedTest = join(
Expand All @@ -20,6 +20,9 @@ test("allows experiment tests and ignores nested Claude worktrees", () => {
const experimentTest = join(root, "experiments/gigacode/gate.test.ts");
mkdirSync(dirname(experimentTest), { recursive: true });
writeFileSync(experimentTest, "export {};\n");
const benchmarkTest = join(root, "benchmarks/agentos-apps/src/load.test.ts");
mkdirSync(dirname(benchmarkTest), { recursive: true });
writeFileSync(benchmarkTest, "export {};\n");

const bin = join(root, "bin");
mkdirSync(bin);
Expand Down
144 changes: 144 additions & 0 deletions scripts/check-markdown-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env tsx

import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");

function usage(): never {
console.error(
"Usage: pnpm exec tsx scripts/check-markdown-links.ts [--no-external] [--built-site path] [path ...]",
);
process.exit(2);
}

function sourceMarkdownFiles(requestedPaths: string[]): string[] {
const output = execFileSync(
"git",
["ls-files", "--cached", "--others", "--exclude-standard", "--", "*.md", "*.mdx"],
{ cwd: repoRoot, encoding: "utf8" },
);
const requested = requestedPaths.map((path) => resolve(repoRoot, path));
const generatedDocs = resolve(repoRoot, "website/public/docs");
return output
.split("\n")
.filter(Boolean)
.map((path) => resolve(repoRoot, path))
.filter((path) => path !== generatedDocs && !path.startsWith(`${generatedDocs}/`))
.filter(
(path) =>
requested.length === 0 ||
requested.some((root) => path === root || path.startsWith(`${root}/`)),
)
.map((path) => relative(repoRoot, path))
.sort();
}

function fileUrl(path: string): string {
return pathToFileURL(path).href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function checkBuiltMarkdownAssets(siteDir: string): boolean {
const failures: string[] = [];
const visit = (directory: string) => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = resolve(directory, entry.name);
if (entry.isDirectory()) {
visit(path);
continue;
}
if (entry.name !== "index.html") continue;
const html = readFileSync(path, "utf8");
const island = html.match(
/component-export="DocsPageDropdown"[^>]*\sprops="([^"]+)"/,
);
if (!island) continue;
const props = JSON.parse(
island[1].replace(/&quot;/g, '"').replace(/&amp;/g, "&"),
) as { markdownPath?: [number, string] };
const advertisedPath = props.markdownPath?.[1];
if (!advertisedPath) {
failures.push(`${relative(repoRoot, path)}: missing markdownPath prop`);
continue;
}
const markdownAsset = resolve(siteDir, `${advertisedPath}.md`);
if (
!markdownAsset.startsWith(`${siteDir}/`) ||
!existsSync(markdownAsset)
) {
failures.push(`/${advertisedPath}.md`);
}
}
};
visit(siteDir);
for (const path of failures.sort()) {
console.error(`Built docs page points to missing Markdown asset: ${path}`);
}
if (failures.length > 0) {
console.error(`\nFound ${failures.length} missing built Markdown assets.\n`);
}
return failures.length === 0;
}

function main() {
let offline = false;
let builtSite: string | undefined;
const requestedPaths: string[] = [];
const argv = process.argv.slice(2);
for (let index = 0; index < argv.length; index++) {
const arg = argv[index];
if (arg === "--no-external") offline = true;
else if (arg === "--built-site") {
const path = argv[++index];
if (!path) usage();
builtSite = resolve(repoRoot, path);
}
else if (arg === "--help" || arg === "-h" || arg.startsWith("-")) usage();
else requestedPaths.push(arg);
}

if (builtSite && !existsSync(builtSite)) {
throw new Error(`built site does not exist: ${relative(repoRoot, builtSite)}`);
}
const builtSitePassed = builtSite ? checkBuiltMarkdownAssets(builtSite) : true;

const files = sourceMarkdownFiles(requestedPaths);
if (files.length === 0) throw new Error("no Markdown source files found");

const rootUrl = fileUrl(repoRoot);
const args = [
"--config",
resolve(repoRoot, "lychee.toml"),
"--root-dir",
repoRoot,
"--remap",
`^${rootUrl}/docs/(.*)$ ${pathToFileURL(resolve(repoRoot, "website/src/content/docs/docs")).href}/$1`,
"--remap",
`^${rootUrl}/images/(.*)$ ${pathToFileURL(resolve(repoRoot, "website/public/images")).href}/$1`,
"--remap",
`^${rootUrl}/registry/?$ ${pathToFileURL(resolve(repoRoot, "website/src/pages/registry/index.astro")).href}`,
"--remap",
`^${rootUrl}/use-cases/?$ ${pathToFileURL(resolve(repoRoot, "website/src/pages/use-cases.astro")).href}`,
];
if (offline) args.push("--offline");
args.push("--files-from", "-");

const result = spawnSync("lychee", args, {
cwd: repoRoot,
input: `${files.join("\n")}\n`,
stdio: ["pipe", "inherit", "inherit"],
});
if (result.error) {
if ((result.error as NodeJS.ErrnoException).code === "ENOENT") {
throw new Error(
"lychee is not installed; see https://github.com/lycheeverse/lychee#installation",
);
}
throw result.error;
}
process.exitCode = builtSitePassed ? (result.status ?? 1) : 1;
}

main();
2 changes: 1 addition & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"docs:gen:ts": "typedoc",
"gen:registry": "node scripts/gen-registry.mjs",
"check-types": "astro check",
"build": "node scripts/gen-registry.mjs && pnpm run docs:gen:ts && astro build",
"build": "node scripts/gen-registry.mjs && pnpm run docs:gen:ts && astro build && node scripts/generate-page-markdown.mjs",
"preview": "astro preview",
"astro": "astro",
"docs:index": "node --input-type=module -e \"import('@rivet-dev/docs-theme/scripts/index-docs').then(m => m.indexDocs())\""
Expand Down
2 changes: 1 addition & 1 deletion website/public/docs/docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ agentOS uses the same authentication system as [Rivet Actors](/docs/actors/authe

The server declares the credential shape and validates it in `onBeforeConnect` (throw to reject); the client passes credentials as `params`.

See [Actor Authentication](/docs/actors/authentication) for JWT validation, role-based access control, external auth providers, and token caching.
See [Actor Authentication](/docs/actors/authentication) for JWT validation, role-based access control, external auth providers, and token caching.
2 changes: 1 addition & 1 deletion website/public/docs/docs/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ ts=2026-… level=debug message="querying: api.anthropic.com. A"

Most sidecar log activity is on the session/ACP path. A bare `AgentOs.create()` or a single `exec()` emits almost nothing — create a session (and send a prompt) to see request-handling logs.

Use **agent logs** to see what the agent did (tool calls, model errors), and **runtime logs** to see what the sidecar did around it (request timing, DNS, lifecycle).
Use **agent logs** to see what the agent did (tool calls, model errors), and **runtime logs** to see what the sidecar did around it (request timing, DNS, lifecycle).
60 changes: 60 additions & 0 deletions website/scripts/generate-page-markdown.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
writeFileSync,
} from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const websiteRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolve(websiteRoot, "..");
const distDir = join(websiteRoot, "dist");

function parseFrontmatter(raw) {
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!match) return { data: {}, body: raw };
const data = {};
for (const line of match[1].split(/\r?\n/)) {
const value = line.match(/^(\w+):\s*(.*)$/);
if (value) data[value[1]] = value[2].trim().replace(/^["']|["']$/g, "");
}
return { data, body: match[2] };
}

function cookbookMarkdown(slug) {
const raw = readFileSync(join(repoRoot, "examples", slug, "README.md"), "utf8");
const { data, body } = parseFrontmatter(raw);
const title = data.title || slug;
const description = data.description ? `\n${data.description}\n` : "";
const withoutSource = body.replace(/\n#{1,6}\s+Source\b[\s\S]*$/i, "\n").trimEnd();
return `# ${title}\n${description}\n${withoutSource}\n\n## Source\n\n[View source on GitHub](https://github.com/rivet-dev/agentos/tree/main/examples/${slug})\n`;
}

function visit(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) {
visit(path);
continue;
}
if (entry.name !== "index.html") continue;
const html = readFileSync(path, "utf8");
if (!html.includes('component-export="DocsPageDropdown"')) continue;

const route = relative(distDir, dirname(path)).replace(/\\/g, "/");
const output = join(distDir, `${route}.md`);
mkdirSync(dirname(output), { recursive: true });
if (route.startsWith("docs/")) {
const source = join(websiteRoot, "public", "docs", `${route}.md`);
if (!existsSync(source)) throw new Error(`missing generated Markdown source: ${source}`);
copyFileSync(source, output);
} else if (route.startsWith("cookbooks/")) {
writeFileSync(output, cookbookMarkdown(route.slice("cookbooks/".length)));
}
}
}

visit(distDir);
2 changes: 1 addition & 1 deletion website/src/content/docs/docs/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ The executor is the untrusted half of the VM. It runs the guest code and reaches

## Agents & sessions

An agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) is just another guest process running inside a VM, behind the same boundary as any other code. A **session** keeps that agent alive across many prompts and streams its output back to your app as events.
An agent (such as [Pi](https://github.com/earendil-works/pi)) is just another guest process running inside a VM, behind the same boundary as any other code. A **session** keeps that agent alive across many prompts and streams its output back to your app as events.

<svg viewBox="0 0 700 210" role="img" aria-label="A client sends a prompt to an agent running inside a VM. The agent streams events back to the client and persists a transcript." style="width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;">
<defs>
Expand Down
Loading
Loading