vlt / affordances
  • Quickstart
  • Concepts
  • Guides
  • Reference
  • For agents
  • Why Affordances
  • Quickstart
  • Benchmarks

Concepts
  • Overview
  • Actions
  • State
  • Expose
  • Guards and confirm
  • Deltas
  • Plugins

Guides
  • Overview
  • React and WebMCP
  • MCP
  • Lint
  • Security

Plugins
  • Overview

Reference
  • Overview
  • Protocol
  • Options
  • CLI
  • ESLint
  • oxlint
  • Rules
  • require-affordance
  • static-action-name
  1. Guides
  2. /
  3. Lint

Lint

Two rules that keep the manifest buildable and every server-action control visible to agents.

@affordances/lint is an ESLint plugin, written against the ESLint rule API so oxlint's JS plugin support can load it too.

Terminal
vlt install -D @affordances/lintnpm install -D @affordances/lintpnpm add -D @affordances/lintyarn add -D @affordances/lintbun add -D @affordances/lintdeno add -D npm:@affordances/lint

ESLint

eslint.config.js
import affordances from '@affordances/lint'

export default [affordances.configs.recommended]

oxlint

.oxlintrc.json
{
  "jsPlugins": ["@affordances/lint"],
  "rules": {
    "affordances/require-affordance": "warn",
    "affordances/static-action-name": "error"
  }
}

Rules

RuleRecommendedWhat it checks
affordances/require-affordancewarnControls that call a server action are inside <Affordance>
affordances/static-action-nameerrordefineAction has a name, and it's a string literal

require-affordance

The rule tracks identifiers imported from an actions module (./actions, ../cart/actions, @/app/cart/actions). It reports a JSX action, formAction, onClick, or onSubmit attribute that uses one, unless an <Affordance> element encloses it:

TSX
import { addToCart } from './actions'

// reported: addToCart is a server action, but this control isn't inside <Affordance>
<button formAction={addToCart}>Add</button>

// fine
<Affordance action={addToCart}>
  <button formAction={addToCart}>Add</button>
</Affordance>

It follows the forms people write: action={x}, formAction={x.bind(null, id)}, onClick={() => x(input)}, and onClick={async () => { await x(input) }}.

static-action-name

Codegen reads action names without running your code, so they have to be literals:

TypeScript
defineAction({ name: 'addToCart' }) // fine
defineAction({ name: `add${kind}` }) // reported
defineAction({ description: '…' }) // reported: no name

PreviousMCPNextSecurity

On this page

  • ESLint
  • oxlint
  • Rules
  • require-affordance
  • static-action-name
Edit this page

Make your app agent-operable

Give every page a graph and every server action an agent path: HTTP, WebMCP, and MCP, with the same session and the same checks. One command sets it up.

Read the quickstart