ont-run
v0.0.16
Published
Ontology-enforced API framework for AI coding agents
Maintainers
Readme
ont-run
Vibe code with confidence.
A web framework designed for the era of coding agents. You define the ontology—what operations exist and who can perform them. AI writes the implementation.
https://github.com/user-attachments/assets/93fbf862-aca3-422d-8f0c-1fc1e4510d88
// ontology.config.ts
import { defineOntology, z } from 'ont-run';
import getTicket from './resolvers/getTicket.js';
import assignTicket from './resolvers/assignTicket.js';
export default defineOntology({
name: 'support-desk',
accessGroups: {
public: { description: 'Unauthenticated users' },
support: { description: 'Support agents' },
admin: { description: 'Administrators' },
},
functions: {
getTicket: {
description: 'Get ticket details',
access: ['support', 'admin'],
entities: ['Ticket'],
inputs: z.object({ ticketId: z.string().uuid() }),
resolver: getTicket,
},
assignTicket: {
description: 'Assign ticket to an agent',
access: ['admin'], // If AI tries to add 'public' here, review is triggered
entities: ['Ticket'],
inputs: z.object({ ticketId: z.string().uuid(), assignee: z.string() }),
resolver: assignTicket,
},
},
// ...
});How It Works
Coding agents (Claude Code, Cursor, etc.) can edit resolver implementations freely. But changes to the ontology—functions, access groups, inputs—trigger a review that agents can't bypass. It's built into the framework.
| Layer | Contains | Who modifies |
|-------|----------|--------------|
| Ontology | Functions, access groups, inputs, entities | Humans only (via ont-run review) |
| Implementation | Resolver code | AI agents freely |
Your job shifts: instead of writing every line, you encode what the system can do and who can do it. You review a visual map of capabilities—not 7,000 lines of code.
The Enforcement
If Claude tries to let public call assignTicket (currently admin-only):
- The ontology config changes
ont.lockdetects the mismatch- Server refuses to start
- You review the visual diff and approve or reject
WARN Ontology has changed:
~ assignTicket
Access: [admin] -> [admin, public]
WARN Run `npx ont-run review` to approve these changes.Why not just prompts? The ontology is the source of truth. The framework won't run if it changes without human review.
Installation
npx ont-run init my-apiThis creates a new project with the ont-run framework configured.
Quick Start
1. Initialize
npx ont-run init my-api
cd my-api2. Review the initial ontology
npm run reviewOpens a browser showing all functions and access groups. Click Approve to generate ont.lock.
3. Start the server
The generated project includes a full-stack setup with Vite + React Router for the frontend and Hono for the backend.
npm run devYour API is running at http://localhost:3000.
Writing Resolvers
Resolvers are where AI writes the implementation:
// resolvers/assignTicket.ts
import type { ResolverContext } from 'ont-run';
export default async function assignTicket(
ctx: ResolverContext,
args: { ticketId: string; assignee: string }
) {
// AI can modify this freely—no review required
const ticket = await db.tickets.update({
where: { id: args.ticketId },
data: { assigneeId: args.assignee },
});
return { id: ticket.id, assignedTo: args.assignee };
}The resolver context provides:
ctx.env— Current environment namectx.envConfig— Environment configurationctx.logger— Logger instancectx.accessGroups— Access groups for the request
Configuration Reference
import { defineOntology, z } from 'ont-run';
import getUser from './resolvers/getUser.js';
export default defineOntology({
name: 'my-api',
environments: {
dev: { debug: true },
prod: { debug: false },
},
// Auth returns access groups (and optional user identity)
auth: async (req: Request) => {
const token = req.headers.get('Authorization');
if (!token) return { groups: ['public'] };
const user = await verifyToken(token);
return {
groups: user.isAdmin ? ['admin', 'user', 'public'] : ['user', 'public'],
user: { id: user.id, email: user.email }, // Optional: for row-level access
};
},
accessGroups: {
public: { description: 'Unauthenticated users' },
user: { description: 'Authenticated users' },
admin: { description: 'Administrators' },
},
entities: {
User: { description: 'A user account' },
Ticket: { description: 'A support ticket' },
},
functions: {
getUser: {
description: 'Get user by ID',
access: ['user', 'admin'],
entities: ['User'],
inputs: z.object({ userId: z.string().uuid() }),
resolver: getUser,
},
},
});Row-Level Access Control
The framework handles group-based access (user → group → function) out of the box. For row-level ownership (e.g., "users can only edit their own posts"), use userContext():
import { defineOntology, userContext, z } from 'ont-run';
import editPost from './resolvers/editPost.js';
export default defineOntology({
// Auth must return user identity for userContext to work
auth: async (req) => {
const user = await verifyToken(req);
return {
groups: ['user'],
user: { id: user.id, email: user.email },
};
},
functions: {
editPost: {
description: 'Edit a post',
access: ['user', 'admin'],
entities: ['Post'],
inputs: z.object({
postId: z.string(),
title: z.string(),
// currentUser is injected at runtime, hidden from API callers
currentUser: userContext(z.object({
id: z.string(),
email: z.string(),
})),
}),
resolver: editPost,
},
},
});In the resolver, you receive the typed user object:
// resolvers/editPost.ts
export default async function editPost(
ctx: ResolverContext,
args: { postId: string; title: string; currentUser: { id: string; email: string } }
) {
const post = await db.posts.findById(args.postId);
// Row-level check: only author or admin can edit
if (args.currentUser.id !== post.authorId && !ctx.accessGroups.includes('admin')) {
throw new Error('Not authorized to edit this post');
}
return db.posts.update(args.postId, { title: args.title });
}Key points:
userContext()fields are injected fromauth()result'suserfield- They're hidden from public API/MCP schemas (callers don't see or provide them)
- They're type-safe in resolvers
- The review UI shows a badge for functions using user context
The Lockfile
ont.lock is the enforcement mechanism. It contains a hash of your ontology:
What gets hashed (requires review):
- Function names and descriptions
- Access group assignments
- Input schemas
What doesn't get hashed (AI can change freely):
- Resolver implementations
- Environment configurations
- Auth function implementation
CLI Commands
npx ont-run init [dir] # Initialize a new project
npx ont-run review # Review and approve ontology changesAPI Endpoints
All functions are exposed as POST endpoints:
# Call a function
curl -X POST http://localhost:3000/api/getUser \
-H "Authorization: your-token" \
-H "Content-Type: application/json" \
-d '{"userId": "123e4567-e89b-12d3-a456-426614174000"}'
# List available functions
curl http://localhost:3000/api
# Health check
curl http://localhost:3000/healthThe Bigger Picture
As the cost of software production trends toward the cost of compute, every business will encode itself as a software system—through autonomous agents and process orchestration.
ont-run is the enforcement layer that keeps AI agents aligned with your business rules as they automate your operations.
License
MIT
