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
37 changes: 29 additions & 8 deletions docs-v2/api/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,25 @@ async function getDocs(origin: string): Promise<Doc[]> {
const TOOLS = [
{
name: 'search_anchor_docs',
description: 'Search the Anchor (anchor-lang) docs. Returns matching pages with title, path, url, snippet.',
description:
'Search the Anchor (anchor-lang) docs. Returns matching pages with title, path, url, snippet.',
inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
},
{
name: 'read_anchor_doc',
description: 'Read the full Markdown of an Anchor docs page by its path (from search results), e.g. "v2/fundamentals/pda".',
description:
'Read the full Markdown of an Anchor docs page by its path (from search results), e.g. "v2/fundamentals/pda".',
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
},
{
name: 'query_docs_filesystem_anchor',
description:
'Run a read-only shell command (rg, grep, find, tree, ls, cat, head, sed, awk, jq, pipes, &&) against an in-memory filesystem of the Anchor docs rooted at /. Pages are at /<path>.mdx. No network, no persisted writes.',
inputSchema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
inputSchema: {
type: 'object',
properties: { command: { type: 'string' } },
required: ['command'],
},
},
]

Expand All @@ -53,14 +59,20 @@ function search(docs: Doc[], query: string, limit = 8) {
}

async function shell(docs: Doc[], command: string): Promise<string> {
const files = Object.fromEntries(docs.map((d) => [`/${d.id}.mdx`, `Title: ${d.title}\n${d.description}\n\n${d.body}`]))
const files = Object.fromEntries(
docs.map((d) => [`/${d.id}.mdx`, `Title: ${d.title}\n${d.description}\n\n${d.body}`]),
)
//this is a fake shell to make it easier for the agent to interact, nothing too fancy
const { stdout, stderr, exitCode } = await new Bash({
files,
cwd: '/',
executionLimits: { maxCommandCount: 200, maxLoopIterations: 10_000 },
}).exec(command)
return `exit: ${exitCode}` + (stdout ? `\n--- stdout ---\n${stdout}` : '') + (stderr ? `\n--- stderr ---\n${stderr}` : '')
return (
`exit: ${exitCode}` +
(stdout ? `\n--- stdout ---\n${stdout}` : '') +
(stderr ? `\n--- stderr ---\n${stderr}` : '')
)
}

const ok = (id: unknown, result: unknown) =>
Expand Down Expand Up @@ -93,20 +105,29 @@ export async function POST(request: Request) {
const q = String(args.query ?? '')
const hits = search(docs, q)
const text = hits.length
? hits.map((d) => `## ${d.title}\npath: ${d.id}\nurl: ${d.url}\n${d.description || d.body.slice(0, 200)}`).join('\n\n')
? hits
.map(
(d) =>
`## ${d.title}\npath: ${d.id}\nurl: ${d.url}\n${d.description || d.body.slice(0, 200)}`,
)
.join('\n\n')
: `No results for "${q}".`
return ok(m.id, { content: [{ type: 'text', text }] })
}
if (name === 'read_anchor_doc') {
const path = String(args.path ?? '').replace(/^\/+|\/+$|\.mdx?$/g, '')
const d = docs.find((x) => x.id === path || x.id === `${path}/index`)
return ok(m.id, {
content: [{ type: 'text', text: d ? `# ${d.title}\n${d.url}\n\n${d.body}` : `Not found: ${path}` }],
content: [
{ type: 'text', text: d ? `# ${d.title}\n${d.url}\n\n${d.body}` : `Not found: ${path}` },
],
isError: !d,
})
}
if (name === 'query_docs_filesystem_anchor')
return ok(m.id, { content: [{ type: 'text', text: await shell(docs, String(args.command ?? '')) }] })
return ok(m.id, {
content: [{ type: 'text', text: await shell(docs, String(args.command ?? '')) }],
})

return fail(m.id, -32602, `Unknown tool: ${name}`)
}
Expand Down
7 changes: 5 additions & 2 deletions docs-v2/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import { darkTheme, lightTheme } from './src/lib/shiki-themes'

type DevMiddleware = (
req: { url?: string },
res: { setHeader(name: string, value: string): void; end(data: Uint8Array): void },
res: {
setHeader(name: string, value: string): void
end(data: Uint8Array): void
},
next: () => void,
) => void | Promise<void>

Expand Down Expand Up @@ -87,7 +90,7 @@ function resolvePagefindAsset(url: string): string | null {
}

export default defineConfig({
site: 'https://www.anchor-lang.com',
site: 'https://v2.anchor-lang.com',
base: DOCS_BASE,
trailingSlash: 'always',
outDir: './dist/docs',
Expand Down
2 changes: 1 addition & 1 deletion docs-v2/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { DocsConfig, IconMap, Site, SocialLink } from '@/types'
export const SITE: Site = {
title: 'Anchor Docs',
description: 'Anchor is the leading development framework for building Solana programs.',
href: 'https://www.anchor-lang.com',
href: 'https://v2.anchor-lang.com',
author: 'solana-foundation',
locale: 'en-US',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ in the `owner` field of the `Account{:rs}` type defined by the Token Program. Th
Program or Token Extension Program, as specified in the `owner` field of the base Solana
[`Account{:rs}`](https://github.com/anza-xyz/agave/blob/master/sdk/account/src/lib.rs#L44-L56) type.

When working with token accounts, "owner" typically refers to the authority that can spend the
tokens, not the program that owns the account.
When working with token accounts, "owner" typically refers to the authority that can spend the tokens, not the program that owns the account.

</Callout>

## What is an associated token account?
Expand Down
10 changes: 5 additions & 5 deletions docs-v2/src/content/docs/v2/reference/feature-flags.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

The `guardrails{:toml}` feature enables a small set of runtime checks that catch caller bugs the borrow checker cannot see statically:

- `check_program_id{:rs}` on dispatch rejects misrouted instructions

Check warning on line 30 in docs-v2/src/content/docs/v2/reference/feature-flags.mdx

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (misrouted)
- `is_writable{:rs}` enforcement in data-account `load_mut{:rs}` paths returns an error when the account is not marked `mut{:rs}`
- executable checks on `Program<T>{:rs}` reject non-program accounts before CPI

Expand Down Expand Up @@ -70,11 +70,11 @@

The remaining helpers re-expose v1 names so existing call sites compile without rewrites:

| Helper | Form |
| --- | --- |
| `error!{:rs}`, `err!{:rs}`, `pubkey!{:rs}` | Macros |
| `Pubkey{:rs}` | Type alias for `Address{:rs}` |
| `AccountViewCompat{:rs}` | Extension trait on pinocchio's `AccountView{:rs}` |
| Helper | Form |
| ------------------------------------------ | ------------------------------------------------- |
| `error!{:rs}`, `err!{:rs}`, `pubkey!{:rs}` | Macros |
| `Pubkey{:rs}` | Type alias for `Address{:rs}` |
| `AccountViewCompat{:rs}` | Extension trait on pinocchio's `AccountView{:rs}` |

<Callout variant="warning" title="`debug!{:rs}` cost">
`debug!{:rs}` is meaningfully more expensive than the native `msg!{:rs}` because of the heap allocation and fmt-trait dispatch. The feature is off by default to keep production binaries from accidentally shipping the heavier path.
Expand All @@ -94,7 +94,7 @@

## `testing{:toml}` (off)

The `testing{:toml}` feature exposes the `anchor_lang_v2::testing{:rs}` module, which provides stack-backed mock pinocchio types (such as `AccountView{:rs}` and the SBF input buffer) that the `lang-v2/tests/{:dir}` integration tests and Miri witnesses depend on. Enabling `testing{:toml}` also turns on `const-rent{:toml}` so host-side tests can exercise rent-dependent helpers without needing SVM sysvar support:

Check warning on line 97 in docs-v2/src/content/docs/v2/reference/feature-flags.mdx

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (Miri)

```console showLineNumbers=false
$ <blue>cargo</blue> test <dim>-p</dim> anchor-lang-v2 <dim>--features</dim> testing
Expand Down
18 changes: 9 additions & 9 deletions docs-v2/src/content/docs/v2/security/raw-account-access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ A program that drops `#[derive(Accounts)]{:rs}` owns every check itself. The pin

When you do go raw, put back each check Anchor would have run:

| Check Anchor runs | Put it back with |
| --- | --- |
| Account count | An `accounts.len() < N{:rs}` guard before any unchecked indexing |
| Discriminator | Compare the first byte before trusting the rest |
| Owner | Compare `view.owner(){:rs}` against the program id |
| Writable | Check `view.is_writable(){:rs}` before a mutable borrow |
| Signer | Check `signer.is_signer(){:rs}` |
| Borrow tracking | Ensure no other `&mut{:rs}` to the account is live |
| PDA derivation | `find_program_address{:rs}`, or a `const{:rs}` bump for literal seeds |
| Check Anchor runs | Put it back with |
| ----------------- | --------------------------------------------------------------------- |
| Account count | An `accounts.len() < N{:rs}` guard before any unchecked indexing |
| Discriminator | Compare the first byte before trusting the rest |
| Owner | Compare `view.owner(){:rs}` against the program id |
| Writable | Check `view.is_writable(){:rs}` before a mutable borrow |
| Signer | Check `signer.is_signer(){:rs}` |
| Borrow tracking | Ensure no other `&mut{:rs}` to the account is live |
| PDA derivation | `find_program_address{:rs}`, or a `const{:rs}` bump for literal seeds |

An init path can lean on `create_account_signed{:rs}`, which enforces the PDA match and the program owner as it creates the account. A read-after-init path has no such helper, so re-check owner and discriminator by hand.

Expand Down
117 changes: 108 additions & 9 deletions docs-v2/src/pages/og/[...slug].png.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { BANNER_GRAPHICS, getBannerGraphic, type BannerGraphic } from '@/lib/banner-graphics'
import { docSlugFromId, getAllDocs, type Doc } from '@/lib/docs'
import { darkTheme, lightTheme } from '@/lib/shiki-themes'
import type { MetaFile } from '@/types'
import { ImageResponse } from '@vercel/og'
import React from 'react'
import sharp from 'sharp'
import { codeToTokens } from 'shiki'
import type { BundledLanguage } from 'shiki'

Expand All @@ -13,6 +15,11 @@ const size = {
height: 630,
}

const bannerPanel = {
width: 1200,
height: 210,
}

const dmSansRegular = readFile(resolve(process.cwd(), 'src/assets/fonts/DMSans-Regular.ttf'))
const dmSansMedium = readFile(resolve(process.cwd(), 'src/assets/fonts/DMSans-Medium.ttf'))
const cascadiaCodeRegular = readFile(
Expand Down Expand Up @@ -47,16 +54,25 @@ interface Props {

export async function GET({ props }: { props: Props }) {
const { doc } = props
const banner = resolveBannerGraphic(doc)
const theme = resolveTheme(doc.id)
const [title, description, wordmark, dmSansRegularData, dmSansMediumData, cascadiaCodeData] =
await Promise.all([
renderInlineText(doc.data.title, theme),
doc.data.description ? renderInlineText(doc.data.description, theme) : null,
svgDataUrl(theme.dark ? wordmarkDark : wordmarkLight),
dmSansRegular,
dmSansMedium,
cascadiaCodeRegular,
])
const [
title,
description,
bannerImage,
wordmark,
dmSansRegularData,
dmSansMediumData,
cascadiaCodeData,
] = await Promise.all([
renderInlineText(doc.data.title, theme),
doc.data.description ? renderInlineText(doc.data.description, theme) : null,
bannerDataUrl(resolve(process.cwd(), 'public', banner.src.replace(/^\//, '')), banner),
svgDataUrl(theme.dark ? wordmarkDark : wordmarkLight),
dmSansRegular,
dmSansMedium,
cascadiaCodeRegular,
])
const breadcrumb = breadcrumbParts(doc.id)

return new ImageResponse(
Expand All @@ -78,9 +94,29 @@ export async function GET({ props }: { props: Props }) {
React.createElement(
'div',
{
style: {
width: '100%',
height: bannerPanel.height,
display: 'flex',
overflow: 'hidden',
borderBottom: `1px solid ${theme.border}`,
},
},
React.createElement('img', {
src: bannerImage,
alt: banner.description,
style: {
width: '100%',
height: '100%',
},
}),
),
React.createElement(
'div',
{
style: {
width: '100%',
height: size.height - bannerPanel.height,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
Expand Down Expand Up @@ -188,6 +224,29 @@ export async function GET({ props }: { props: Props }) {
)
}

async function bannerDataUrl(path: string, banner: BannerGraphic): Promise<string> {
const metadata = await sharp(path).metadata()

if (!metadata.width || !metadata.height) {
throw new Error(`Unable to read banner dimensions for ${banner.src}`)
}

const crop = coverCrop(
metadata.width,
metadata.height,
bannerPanel.width,
bannerPanel.height,
banner,
)
const data = await sharp(path)
.extract(crop)
.resize(bannerPanel.width, bannerPanel.height)
.jpeg({ quality: 86 })
.toBuffer()

return `data:image/jpeg;base64,${data.toString('base64')}`
}

async function svgDataUrl(svg: Promise<string>): Promise<string> {
const data = await svg
return `data:image/svg+xml;base64,${Buffer.from(data).toString('base64')}`
Expand Down Expand Up @@ -305,6 +364,38 @@ async function renderInlineCode(
)
}

function resolveBannerGraphic(doc: Doc): BannerGraphic {
if (typeof doc.data.banner === 'string' && doc.data.banner !== 'random') {
return getBannerGraphic(doc.data.banner)
}

return BANNER_GRAPHICS[hashString(doc.id) % BANNER_GRAPHICS.length]
}

function coverCrop(
sourceWidth: number,
sourceHeight: number,
targetWidth: number,
targetHeight: number,
banner: BannerGraphic,
) {
const [xPercent, yPercent] = banner.objectPosition
.split(' ')
.map((value) => Number.parseFloat(value) / 100)

const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight)
const width = Math.min(sourceWidth, Math.round(targetWidth / scale))
const height = Math.min(sourceHeight, Math.round(targetHeight / scale))
const left = Math.round(clamp((sourceWidth - width) * xPercent, 0, sourceWidth - width))
const top = Math.round(clamp((sourceHeight - height) * yPercent, 0, sourceHeight - height))

return { left, top, width, height }
}

function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}

function resolveTheme(id: string) {
const dark = id === 'v1' || id.startsWith('v1/')

Expand Down Expand Up @@ -333,6 +424,14 @@ function resolveTheme(id: string) {
}
}

function hashString(input: string): number {
let hash = 0
for (const char of input) {
hash = (hash * 31 + char.charCodeAt(0)) >>> 0
}
return hash
}

function titleSize(title: string): number {
if (title.length > 70) return 50
if (title.length > 52) return 56
Expand Down
Loading