openvibers/openvibe.actor Commits
Agent layer: /.well-known/agent.json and Markdown twins of the public pages (#172)
Changes
@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.229.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"devDependencies": {@@@ -27,6 +27,12 @@"node": ">=22.0.0"}},++ "node_modules/@acemir/cssom": {++ "version": "0.9.32",++ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",++ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",++ "license": "MIT"++ },"node_modules/@electric-sql/pglite": {"version": "0.5.8","resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",@@@ -116,6 +122,19 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/boolbase": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",++ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",++ "license": "ISC",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -218,6 +237,39 @@"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==","license": "MIT"},++ "node_modules/css-select": {++ "version": "7.0.0",++ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",++ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0",++ "css-what": "^8.0.0",++ "domhandler": "^6.0.1",++ "domutils": "^4.0.2",++ "nth-check": "^3.0.1"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },++ "node_modules/css-what": {++ "version": "8.0.0",++ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",++ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/debug": {"version": "2.6.9","resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",@@@ -255,6 +307,73 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/dom-serializer": {++ "version": "3.1.1",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",++ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0",++ "entities": "^8.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/domelementtype": {++ "version": "3.0.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",++ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ }++ },++ "node_modules/domhandler": {++ "version": "6.0.1",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",++ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^3.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/domutils": {++ "version": "4.0.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",++ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^3.0.0",++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },"node_modules/dotenv": {"version": "16.6.1","resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",@@@ -305,6 +424,18 @@"node": ">= 0.8"}},++ "node_modules/entities": {++ "version": "8.1.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",++ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -563,6 +694,110 @@"url": "https://github.com/sponsors/EvanHahn"}},++ "node_modules/html-escaper": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",++ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",++ "license": "MIT"++ },++ "node_modules/htmlparser2": {++ "version": "10.1.0",++ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",++ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",++ "funding": [++ "https://github.com/fb55/htmlparser2?sponsor=1",++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3",++ "domutils": "^3.2.2",++ "entities": "^7.0.1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",++ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.2",++ "entities": "^4.2.0"++ },++ "funding": {++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {++ "version": "4.5.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domelementtype": {++ "version": "2.3.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",++ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause"++ },++ "node_modules/htmlparser2/node_modules/domhandler": {++ "version": "5.0.3",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",++ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^2.3.0"++ },++ "engines": {++ "node": ">= 4"++ },++ "funding": {++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domutils": {++ "version": "3.2.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",++ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^2.0.0",++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3"++ },++ "funding": {++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/entities": {++ "version": "7.0.1",++ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",++ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/http-errors": {"version": "2.0.1","resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",@@@ -708,6 +943,30 @@"safe-buffer": "^5.0.1"}},++ "node_modules/linkedom": {++ "version": "0.18.14",++ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",++ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",++ "license": "ISC",++ "dependencies": {++ "@acemir/cssom": "^0.9.32",++ "css-select": "^7.0.0",++ "html-escaper": "^3.0.3",++ "htmlparser2": "^10.1.0",++ "uhyphen": "^0.2.0"++ },++ "engines": {++ "node": ">=16"++ },++ "peerDependencies": {++ "canvas": ">= 2"++ },++ "peerDependenciesMeta": {++ "canvas": {++ "optional": true++ }++ }++ },"node_modules/lodash.defaults": {"version": "4.2.0","resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",@@@ -846,6 +1105,22 @@"node": ">= 0.6"}},++ "node_modules/nth-check": {++ "version": "3.0.1",++ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",++ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/nth-check?sponsor=1"++ }++ },"node_modules/object-inspect": {"version": "1.13.4","resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",@@@ -915,12 +1190,13 @@}},"node_modules/openvibe-shared": {-- "version": "4.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",-- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",++ "version": "4.34.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",++ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==","license": "MIT","dependencies": {-- "jsonwebtoken": "^9.0.0"++ "jsonwebtoken": "^9.0.0",++ "linkedom": "^0.18.13"},"bin": {"openvibe-build-assets": "scripts/build-assets.js"@@@ -1363,6 +1639,12 @@"node": ">= 0.6"}},++ "node_modules/uhyphen": {++ "version": "0.2.0",++ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",++ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",++ "license": "ISC"++ },"node_modules/unpipe": {"version": "1.0.0","resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",@@@ -30,7 +30,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.229.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"engines": {nnew file mode 100644@@@ -0,0 +1,17 @@++'use strict';++// What openvibe.actor tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.++// Every field is something this site really serves; /docs is the API reference and the JSON API is /api/v1.++// A task page is behind a person's token, so it is never part of this surface.++module.exports = {++ id: 'actor',++ name: 'OpenVibe.Actor',++ purpose: 'A router for agent work: a task in plain words goes to the best agent system for it, a second model family checks the answer, and the task records its cost and why that agent.',++ home: 'https://openvibe.actor',++ site: 'openvibe.actor',++ api: '/api/v1',++ docs: '/docs',++ policy: {++ auth: ['none', 'Network OIDC', 'bearer'],++ rateLimit: '240 API requests a minute per address; 300 page requests a minute; 60 sign-in attempts per 15 minutes',++ },++};nnew file mode 100644@@@ -0,0 +1,23 @@++'use strict';++// The Markdown twins of openvibe.actor's public pages (openvibe-shared/agent-layer), rendered by the same++// page builders the routes use (createPageRoutes().publicViews + render/layout.js's renderPage), so a twin++// cannot drift from its page. Anonymous only. Named agent-layer-pages because server/http/agent-pages.js++// already means a person's own agents, and a task page is behind a token: it is not here.++const { renderPage } = require('./render/layout');++++function createAgentLayerPages(ctx, publicViews) {++ const views = publicViews;++ const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);++++ async function renderPublicPage(path) {++ if (path === '/') return twin(await views.home(), path);++ if (path === '/agents') return twin(views.agents(), path);++ if (path === '/docs') return twin(views.docs(), path);++ if (path === '/updates') return twin(views.updates(), path);++ return null;++ }++++ return { renderPublicPage };++}++++module.exports = { createAgentLayerPages };@@@ -197,7 +197,22 @@ async function createApp(opts = {}) {app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.ACTOR_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false }));app.use(createAgentPages(ctx));app.use(createConsolePages(ctx, opts.bot || {}));-- app.use(createPageRoutes(ctx));++ const pageRoutes = createPageRoutes(ctx);++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public++ // pages. The pages themselves keep their own routes — a signed-in visitor's account bar and task box are++ // theirs — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.++ // A task page is behind a person's token: renderPublicPage deliberately returns null for it.++ const agentLayer = express();++ require('openvibe-shared/agent-layer').mount(agentLayer, {++ descriptor: require('./agent-descriptor'),++ render: require('./agent-layer-pages').createAgentLayerPages(ctx, pageRoutes.publicViews).renderPublicPage,++ });++ app.use((req, res, next) => {++ const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');++ if (req.path !== '/.well-known/agent.json' && !twin) return next();++ return agentLayer(req, res, next);++ });++ app.use(pageRoutes);app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/">giving Actor a task</a>, <a href="/agents">the agents</a> or <a href="/docs">the API</a>.</p>` }));// eslint-disable-next-line no-unused-vars@@@ -56,6 +56,7 @@ function createDiscoveryRoutes(ctx) {r.get('/robots.txt', (_req, res) => {res.type('text/plain').set('Cache-Control', TEXT).send('# openvibe.actor: the public pages are for search and AI crawlers; tasks, sign-in and the API are not.\n'++ + '# Agent manifest: https://openvibe.actor/.well-known/agent.json; the public pages have a .md twin.\n'+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));});@@@ -91,8 +91,8 @@ function createPageRoutes(ctx) {const tier = config.allowance;// ── The task box ─────────────────────────────────────────-- function taskForm(req, { text = '', mode = 'balanced', problem = null } = {}) {-- if (!signedIn(req)) {++ function taskForm(viewer, { text = '', mode = 'balanced', problem = null } = {}) {++ if (!(viewer.kind === 'user' && viewer.subject)) {// A template picked while signed out waits in the box and comes back after sign-in.const next = text ? `/?${new URLSearchParams({ task: text, mode })}` : '/#task';return html`<div class="task-box task-box-guest">@@@ -120,18 +120,19 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valureturn (Array.isArray(list) ? list : []).filter((t) => t && t.id && t.name);}-- async function home(req, res, { status = 200, form = {} } = {}) {++ async function homePage({ viewer = { kind: 'anonymous' }, query = {}, form = {} } = {}) {const agents = catalog.list(config);const tools = await callableTools();++ const signed = viewer.kind === 'user' && viewer.subject;// A template link (/?task=…&mode=…#task) fills the task box; the page itself stays the canonical /.-- if (!form.text && typeof req.query.task === 'string') form = { text: req.query.task.slice(0, 2000), mode: MODES.includes(req.query.mode) ? req.query.mode : 'balanced' };++ if (!form.text && typeof query.task === 'string') form = { text: query.task.slice(0, 2000), mode: MODES.includes(query.mode) ? query.mode : 'balanced' };const working = agents.filter((a) => a.available).length;const faq = faqItems(agents);const hero = showcase.hero({eyebrow: 'OpenVibe.Actor · your agent for everything · alpha',title: 'Ask once.', accent: 'The right agent does it.',lede: 'Give Actor a task in plain words. It picks the agent that fits: OpenVibe\'s own, acting through OpenVibe Search and Tools, or another platform\'s, like OpenAI\'s web agent. It runs the task, has a second model check the answer, and shows you what it cost and why that agent. Like OpenRouter, for agents.',-- actions: signedIn(req) ? [{ label: 'Give it a task', href: '#task', primary: true }, { label: 'Your tasks', href: '/tasks' }] : [{ label: 'Sign in to start', href: '/auth/login?next=%2F', primary: true }, { label: 'See the agents', href: '/agents' }],++ actions: signed ? [{ label: 'Give it a task', href: '#task', primary: true }, { label: 'Your tasks', href: '/tasks' }] : [{ label: 'Sign in to start', href: '/auth/login?next=%2F', primary: true }, { label: 'See the agents', href: '/agents' }],note: `Open source (AGPL-3.0). Free tier: ${tier.tasksPerDay} tasks a day, up to ${usd(tier.perTaskUsd)} each. The list below says exactly what works today.`,aside: { html: String(routerCard(agents)) },});@@@ -141,8 +142,8 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu[MODES.length, 'ways to choose', '#route'],[tier.tasksPerDay, 'free tasks a day', '#faq'],].filter(([n]) => n > 0);-- page(req, res, {-- index: true, cache: signedIn(req) && !req.query.task ? null : req.query.task ? cache.htmlHeaders({ private: true }) : PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: signed && !query.task ? null : query.task ? cache.htmlHeaders({ private: true }) : PUBLIC_CACHE,jsonLd: [...homeJsonLd(config),seo.jsonLd.itemList('Agents OpenVibe.Actor routes tasks to', agents.map((a) => ({ name: a.name, url: `${config.baseUrl}/agents#${a.id}`, description: (catalog.byId(a.id) || {}).blurb }))),seo.jsonLd.faq(faq.map(([q, a]) => ({ q, a })))],@@@ -151,7 +152,7 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valubody: html`${raw(hero)}<section class="sc-sec hm-stats-sec" aria-label="OpenVibe.Actor in numbers"><ul class="hm-stats">${stats.map(([n, label, href]) => html`<li><a href="${href}"><b>${n}</b><span>${label}</span></a></li>`)}</ul></section><section class="sc-sec" id="task" aria-labelledby="h-task"><h2 id="h-task">Give Actor a task</h2>--${taskForm(req, form)}</section>++${taskForm(viewer, form)}</section><section class="sc-sec" aria-labelledby="h-templates"><h2 id="h-templates">Start from one of these</h2><p class="sc-lede">Tasks that work today. Pick one and it lands in the task box, ready to run or change.</p><ul class="hm-templates">${TEMPLATES.map((g) => html`<li class="hm-tgroup"><p class="hm-tgroup-head"><b>${g.group}</b></p>@@@ -231,7 +232,12 @@ ${raw(showcase.code({<div class="hm-faq">${faq.map(([q, a]) => html`<div class="hm-qa"><h3>${q}</h3><p>${a}</p></div>`)}</div></section>${raw(showcase.network({ service: 'actor' }))}${raw(showcase.cta({ title: 'Build on it', text: 'Every task, its live stream, its webhooks and the router are an API. Apps get actor.task.* grants on OpenVibe.Services.', actions: [{ label: 'Read the API', href: '/docs', primary: true }, { label: 'See the agents', href: '/agents' }] }))}`,-- }, status);++ };++ }++++ /** Render the home page through the route's own response shape (the POST failure path reuses it). */++ async function home(req, res, { status = 200, form = {} } = {}) {++ return page(req, res, await homePage({ viewer: req.viewer, query: req.query, form }), status);}/** Questions people ask first, answered from the running config and the catalog. */@@@ -293,7 +299,7 @@ ${a.available ? '' : html`<p class="muted small">${a.reason}</p>`}title: 'Your tasks', crumbs: [{ label: 'Your tasks' }], styles: [showcase.STYLESHEET],body: html`<h1>Your tasks</h1><p class="muted">Today: ${today.tasks} of ${tier.tasksPerDay} free tasks, ${usd(today.usd)} of ${usd(tier.perDayUsd)}. Tasks are kept ${store.RETENTION_DAYS} days.</p>--${taskForm(req)}++${taskForm(req.viewer)}${table(['Task', 'State', 'Agent', 'Cost', 'Started'], pageRows.rows.map((t) => {const result = t.result ? (typeof t.result === 'string' ? JSON.parse(t.result) : t.result) : null;return [html`<a href="/tasks/${t.id}">${t.task.length > 90 ? `${t.task.slice(0, 89)}…` : t.task}</a>`, badge(t.state, STATE_KIND[t.state]), result ? (catalog.byId(result.agent) || {}).name || result.agent : '—', usd(t.cost_usd == null ? null : Number(t.cost_usd)), time(t.created_at)];@@@ -489,10 +495,10 @@ ${table(['Agent', 'Could take it', 'Estimated cost', 'Why not'], (p.candidates |});// ── The agents ───────────────────────────────────────────-- r.get('/agents', (req, res) => {++ function agentsPage() {const agents = catalog.list(config);-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,title: 'The agents',description: 'Every agent system OpenVibe.Actor routes a task to — OpenVibe\'s own runtime, OpenAI\'s web agent, an open model on OpenVibe hardware, OpenVibe.Codes — with what each can do, its trust class and its price per million tokens.',crumbs: [{ label: 'Agents' }],@@@ -520,11 +526,12 @@ ${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[m<p>Before you see an answer, a model of another family reads it: DeepSeek's answers are checked by OpenAI's smallest model and OpenAI's by DeepSeek. In private mode only OpenVibe's own model may read the task, so it checks its own answer and the task says so. A failed check sends the task to the next agent that can do it.</p><h2 id="trust">Trust</h2><p><strong>first-party</strong> runs on OpenVibe's own hardware; <strong>external</strong> means the task's text goes to that provider (the OpenVibe runtime is OpenVibe's code, but its model runs at DeepSeek). Private mode takes only first-party agents.</p>`,-- });-- });++ };++ }++ r.get('/agents', (req, res) => page(req, res, agentsPage()));// ── The API reference ────────────────────────────────────-- r.get('/docs', (req, res) => {++ function docsPage() {const b = config.baseUrl;const create = `curl -s ${b}/api/v1/tasks \\-H "authorization: Bearer $TOKEN" \\@@@ -537,8 +544,8 @@ ${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[mid: 3event: outputdata: {"task_id":"tsk_…","seq":3,"kind":"output","agent":"openvibe-runtime","step":"tool_call","tool":"run_tool","input":"ssl {\\"target\\":\\"example.com\\"}"}`;-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,title: 'API',description: 'The OpenVibe.Actor API: create a task, watch it live over server-sent events, read the result with its cost and explanation, cancel it; list the agents and dry-run the router.',crumbs: [{ label: 'API' }],@@@ -580,18 +587,25 @@ ${table(['Code', 'Meaning'], [[html`<code>actor.check.unavailable</code>`, 'The answer could not be checked, so it was not delivered.'],[html`<code>actor.task.timeout</code> / <code>actor.task.interrupted</code>`, 'It ran too long, or Actor restarted while it ran.'],])}`,-- });-- });++ };++ }++ r.get('/docs', (req, res) => page(req, res, docsPage()));// ── Limits and the update log ────────────────────────────r.get('/limits.json', (req, res) => {res.set('Cache-Control', cache.htmlHeaders({ maxAge: 300 }));res.json({ service: 'actor', free_tier: { tasks_per_day: tier.tasksPerDay, per_task_usd: tier.perTaskUsd, per_day_usd: tier.perDayUsd }, task_timeout_seconds: Math.round(config.taskTimeoutMs / 1000), retention_days: store.RETENTION_DAYS });});-- r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Actor', body: raw(frame.updatesBody({ service: 'actor', siteName: 'OpenVibe.Actor' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));++ const updatesPage = () => ({ agent: true, index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Actor', body: raw(frame.updatesBody({ service: 'actor', siteName: 'OpenVibe.Actor' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) });++ r.get('/updates', (req, res) => page(req, res, updatesPage()));// ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt ──r.use(createDiscoveryRoutes(ctx));++++ // The public pages the agent layer's Markdown twins (server/agent-pages.js) render with the same builders.++ // A task page is behind a person's token and is deliberately absent.++ r.publicViews = { home: homePage, agents: agentsPage, docs: docsPage, updates: updatesPage };++return r;}@@@ -88,6 +88,10 @@ function renderPage(o) {description: o.description || 'Give a task in plain words. Actor picks the best agent for it, ours or another platform\'s, checks the answer and shows the cost and why.',canonical,robots: o.index ? 'index, follow' : 'noindex, nofollow',++ // A page with a Markdown twin (o.agent) advertises it in the head and the Link header; the twin itself++ // is server/agent-pages.js.++ agentLayer: o.agent === true,++ llms: `${String(o.config.baseUrl).replace(/\/+$/, '')}/llms.txt`,jsonLd: (o.jsonLd || []).filter(Boolean),home: '/',navLinks: NAV,@@@ -126,6 +130,10 @@ function send(res, status, o) {const personal = o.viewer && o.viewer.kind === 'user';if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));res.set('Vary', 'Cookie');++ if (o.agent) {++ const site = String(o.config.baseUrl).replace(/\/+$/, '');++ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${site}${String(o.path || '/').split('?')[0]}`, `${site}/llms.txt`).header);++ }res.status(status).type('html').send(renderPage(o));}
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.229.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.229.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -27,6 +27,12 @@
@@ -27,6 +27,12 @@
"node": ">=22.0.0"
"node": ">=22.0.0"
}
}
},
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.32",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",
+ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",
+ "license": "MIT"
+ },
"node_modules/@electric-sql/pglite": {
"node_modules/@electric-sql/pglite": {
"version": "0.5.8",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
@@ -116,6 +122,19 @@
@@ -116,6 +122,19 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/boolbase": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
+ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -218,6 +237,39 @@
@@ -218,6 +237,39 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/css-select": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
+ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0",
+ "css-what": "^8.0.0",
+ "domhandler": "^6.0.1",
+ "domutils": "^4.0.2",
+ "nth-check": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
+ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/debug": {
"node_modules/debug": {
"version": "2.6.9",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -255,6 +307,73 @@
@@ -255,6 +307,73 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/dom-serializer": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
+ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0",
+ "entities": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
+ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
+ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
+ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^3.0.0",
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"node_modules/dotenv": {
"version": "16.6.1",
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -305,6 +424,18 @@
@@ -305,6 +424,18 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -563,6 +694,110 @@
@@ -563,6 +694,110 @@
"url": "https://github.com/sponsors/EvanHahn"
"url": "https://github.com/sponsors/EvanHahn"
}
}
},
},
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmlparser2/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-errors": {
"node_modules/http-errors": {
"version": "2.0.1",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -708,6 +943,30 @@
@@ -708,6 +943,30 @@
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1"
}
}
},
},
+ "node_modules/linkedom": {
+ "version": "0.18.14",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",
+ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",
+ "license": "ISC",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.32",
+ "css-select": "^7.0.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.1.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lodash.defaults": {
"node_modules/lodash.defaults": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -846,6 +1105,22 @@
@@ -846,6 +1105,22 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/nth-check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
+ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/object-inspect": {
"node_modules/object-inspect": {
"version": "1.13.4",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -915,12 +1190,13 @@
@@ -915,12 +1190,13 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",
+ "version": "4.34.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
+ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "jsonwebtoken": "^9.0.0"
+ "jsonwebtoken": "^9.0.0",
+ "linkedom": "^0.18.13"
},
},
"bin": {
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
"openvibe-build-assets": "scripts/build-assets.js"
@@ -1363,6 +1639,12 @@
@@ -1363,6 +1639,12 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "license": "ISC"
+ },
"node_modules/unpipe": {
"node_modules/unpipe": {
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.229.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.229.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+'use strict';
+// What openvibe.actor tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
+// Every field is something this site really serves; /docs is the API reference and the JSON API is /api/v1.
+// A task page is behind a person's token, so it is never part of this surface.
+module.exports = {
+ id: 'actor',
+ name: 'OpenVibe.Actor',
+ purpose: 'A router for agent work: a task in plain words goes to the best agent system for it, a second model family checks the answer, and the task records its cost and why that agent.',
+ home: 'https://openvibe.actor',
+ site: 'openvibe.actor',
+ api: '/api/v1',
+ docs: '/docs',
+ policy: {
+ auth: ['none', 'Network OIDC', 'bearer'],
+ rateLimit: '240 API requests a minute per address; 300 page requests a minute; 60 sign-in attempts per 15 minutes',
+ },
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+'use strict';
+// The Markdown twins of openvibe.actor's public pages (openvibe-shared/agent-layer), rendered by the same
+// page builders the routes use (createPageRoutes().publicViews + render/layout.js's renderPage), so a twin
+// cannot drift from its page. Anonymous only. Named agent-layer-pages because server/http/agent-pages.js
+// already means a person's own agents, and a task page is behind a token: it is not here.
+const { renderPage } = require('./render/layout');
+
+function createAgentLayerPages(ctx, publicViews) {
+ const views = publicViews;
+ const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);
+
+ async function renderPublicPage(path) {
+ if (path === '/') return twin(await views.home(), path);
+ if (path === '/agents') return twin(views.agents(), path);
+ if (path === '/docs') return twin(views.docs(), path);
+ if (path === '/updates') return twin(views.updates(), path);
+ return null;
+ }
+
+ return { renderPublicPage };
+}
+
+module.exports = { createAgentLayerPages };
@@ -197,7 +197,22 @@ async function createApp(opts = {}) {
@@ -197,7 +197,22 @@ async function createApp(opts = {}) {
app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.ACTOR_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false }));
app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.ACTOR_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false }));
app.use(createAgentPages(ctx));
app.use(createAgentPages(ctx));
app.use(createConsolePages(ctx, opts.bot || {}));
app.use(createConsolePages(ctx, opts.bot || {}));
- app.use(createPageRoutes(ctx));
+ const pageRoutes = createPageRoutes(ctx);
+ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public
+ // pages. The pages themselves keep their own routes — a signed-in visitor's account bar and task box are
+ // theirs — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.
+ // A task page is behind a person's token: renderPublicPage deliberately returns null for it.
+ const agentLayer = express();
+ require('openvibe-shared/agent-layer').mount(agentLayer, {
+ descriptor: require('./agent-descriptor'),
+ render: require('./agent-layer-pages').createAgentLayerPages(ctx, pageRoutes.publicViews).renderPublicPage,
+ });
+ app.use((req, res, next) => {
+ const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');
+ if (req.path !== '/.well-known/agent.json' && !twin) return next();
+ return agentLayer(req, res, next);
+ });
+ app.use(pageRoutes);
app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/">giving Actor a task</a>, <a href="/agents">the agents</a> or <a href="/docs">the API</a>.</p>` }));
app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/">giving Actor a task</a>, <a href="/agents">the agents</a> or <a href="/docs">the API</a>.</p>` }));
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars
@@ -56,6 +56,7 @@ function createDiscoveryRoutes(ctx) {
@@ -56,6 +56,7 @@ function createDiscoveryRoutes(ctx) {
r.get('/robots.txt', (_req, res) => {
r.get('/robots.txt', (_req, res) => {
res.type('text/plain').set('Cache-Control', TEXT).send(
res.type('text/plain').set('Cache-Control', TEXT).send(
'# openvibe.actor: the public pages are for search and AI crawlers; tasks, sign-in and the API are not.\n'
'# openvibe.actor: the public pages are for search and AI crawlers; tasks, sign-in and the API are not.\n'
+ + '# Agent manifest: https://openvibe.actor/.well-known/agent.json; the public pages have a .md twin.\n'
+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
});
});
@@ -91,8 +91,8 @@ function createPageRoutes(ctx) {
@@ -91,8 +91,8 @@ function createPageRoutes(ctx) {
const tier = config.allowance;
const tier = config.allowance;
// ── The task box ─────────────────────────────────────────
// ── The task box ─────────────────────────────────────────
- function taskForm(req, { text = '', mode = 'balanced', problem = null } = {}) {
- if (!signedIn(req)) {
+ function taskForm(viewer, { text = '', mode = 'balanced', problem = null } = {}) {
+ if (!(viewer.kind === 'user' && viewer.subject)) {
// A template picked while signed out waits in the box and comes back after sign-in.
// A template picked while signed out waits in the box and comes back after sign-in.
const next = text ? `/?${new URLSearchParams({ task: text, mode })}` : '/#task';
const next = text ? `/?${new URLSearchParams({ task: text, mode })}` : '/#task';
return html`<div class="task-box task-box-guest">
return html`<div class="task-box task-box-guest">
@@ -120,18 +120,19 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu
@@ -120,18 +120,19 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu
return (Array.isArray(list) ? list : []).filter((t) => t && t.id && t.name);
return (Array.isArray(list) ? list : []).filter((t) => t && t.id && t.name);
}
}
- async function home(req, res, { status = 200, form = {} } = {}) {
+ async function homePage({ viewer = { kind: 'anonymous' }, query = {}, form = {} } = {}) {
const agents = catalog.list(config);
const agents = catalog.list(config);
const tools = await callableTools();
const tools = await callableTools();
+ const signed = viewer.kind === 'user' && viewer.subject;
// A template link (/?task=…&mode=…#task) fills the task box; the page itself stays the canonical /.
// A template link (/?task=…&mode=…#task) fills the task box; the page itself stays the canonical /.
- if (!form.text && typeof req.query.task === 'string') form = { text: req.query.task.slice(0, 2000), mode: MODES.includes(req.query.mode) ? req.query.mode : 'balanced' };
+ if (!form.text && typeof query.task === 'string') form = { text: query.task.slice(0, 2000), mode: MODES.includes(query.mode) ? query.mode : 'balanced' };
const working = agents.filter((a) => a.available).length;
const working = agents.filter((a) => a.available).length;
const faq = faqItems(agents);
const faq = faqItems(agents);
const hero = showcase.hero({
const hero = showcase.hero({
eyebrow: 'OpenVibe.Actor · your agent for everything · alpha',
eyebrow: 'OpenVibe.Actor · your agent for everything · alpha',
title: 'Ask once.', accent: 'The right agent does it.',
title: 'Ask once.', accent: 'The right agent does it.',
lede: 'Give Actor a task in plain words. It picks the agent that fits: OpenVibe\'s own, acting through OpenVibe Search and Tools, or another platform\'s, like OpenAI\'s web agent. It runs the task, has a second model check the answer, and shows you what it cost and why that agent. Like OpenRouter, for agents.',
lede: 'Give Actor a task in plain words. It picks the agent that fits: OpenVibe\'s own, acting through OpenVibe Search and Tools, or another platform\'s, like OpenAI\'s web agent. It runs the task, has a second model check the answer, and shows you what it cost and why that agent. Like OpenRouter, for agents.',
- actions: signedIn(req) ? [{ label: 'Give it a task', href: '#task', primary: true }, { label: 'Your tasks', href: '/tasks' }] : [{ label: 'Sign in to start', href: '/auth/login?next=%2F', primary: true }, { label: 'See the agents', href: '/agents' }],
+ actions: signed ? [{ label: 'Give it a task', href: '#task', primary: true }, { label: 'Your tasks', href: '/tasks' }] : [{ label: 'Sign in to start', href: '/auth/login?next=%2F', primary: true }, { label: 'See the agents', href: '/agents' }],
note: `Open source (AGPL-3.0). Free tier: ${tier.tasksPerDay} tasks a day, up to ${usd(tier.perTaskUsd)} each. The list below says exactly what works today.`,
note: `Open source (AGPL-3.0). Free tier: ${tier.tasksPerDay} tasks a day, up to ${usd(tier.perTaskUsd)} each. The list below says exactly what works today.`,
aside: { html: String(routerCard(agents)) },
aside: { html: String(routerCard(agents)) },
});
});
@@ -141,8 +142,8 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu
@@ -141,8 +142,8 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu
[MODES.length, 'ways to choose', '#route'],
[MODES.length, 'ways to choose', '#route'],
[tier.tasksPerDay, 'free tasks a day', '#faq'],
[tier.tasksPerDay, 'free tasks a day', '#faq'],
].filter(([n]) => n > 0);
].filter(([n]) => n > 0);
- page(req, res, {
- index: true, cache: signedIn(req) && !req.query.task ? null : req.query.task ? cache.htmlHeaders({ private: true }) : PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: signed && !query.task ? null : query.task ? cache.htmlHeaders({ private: true }) : PUBLIC_CACHE,
jsonLd: [...homeJsonLd(config),
jsonLd: [...homeJsonLd(config),
seo.jsonLd.itemList('Agents OpenVibe.Actor routes tasks to', agents.map((a) => ({ name: a.name, url: `${config.baseUrl}/agents#${a.id}`, description: (catalog.byId(a.id) || {}).blurb }))),
seo.jsonLd.itemList('Agents OpenVibe.Actor routes tasks to', agents.map((a) => ({ name: a.name, url: `${config.baseUrl}/agents#${a.id}`, description: (catalog.byId(a.id) || {}).blurb }))),
seo.jsonLd.faq(faq.map(([q, a]) => ({ q, a })))],
seo.jsonLd.faq(faq.map(([q, a]) => ({ q, a })))],
@@ -151,7 +152,7 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu
@@ -151,7 +152,7 @@ ${MODES.map((m) => html`<label class="mode"><input type="radio" name="mode" valu
body: html`${raw(hero)}
body: html`${raw(hero)}
<section class="sc-sec hm-stats-sec" aria-label="OpenVibe.Actor in numbers"><ul class="hm-stats">${stats.map(([n, label, href]) => html`<li><a href="${href}"><b>${n}</b><span>${label}</span></a></li>`)}</ul></section>
<section class="sc-sec hm-stats-sec" aria-label="OpenVibe.Actor in numbers"><ul class="hm-stats">${stats.map(([n, label, href]) => html`<li><a href="${href}"><b>${n}</b><span>${label}</span></a></li>`)}</ul></section>
<section class="sc-sec" id="task" aria-labelledby="h-task"><h2 id="h-task">Give Actor a task</h2>
<section class="sc-sec" id="task" aria-labelledby="h-task"><h2 id="h-task">Give Actor a task</h2>
-${taskForm(req, form)}</section>
+${taskForm(viewer, form)}</section>
<section class="sc-sec" aria-labelledby="h-templates"><h2 id="h-templates">Start from one of these</h2>
<section class="sc-sec" aria-labelledby="h-templates"><h2 id="h-templates">Start from one of these</h2>
<p class="sc-lede">Tasks that work today. Pick one and it lands in the task box, ready to run or change.</p>
<p class="sc-lede">Tasks that work today. Pick one and it lands in the task box, ready to run or change.</p>
<ul class="hm-templates">${TEMPLATES.map((g) => html`<li class="hm-tgroup"><p class="hm-tgroup-head"><b>${g.group}</b></p>
<ul class="hm-templates">${TEMPLATES.map((g) => html`<li class="hm-tgroup"><p class="hm-tgroup-head"><b>${g.group}</b></p>
@@ -231,7 +232,12 @@ ${raw(showcase.code({
@@ -231,7 +232,12 @@ ${raw(showcase.code({
<div class="hm-faq">${faq.map(([q, a]) => html`<div class="hm-qa"><h3>${q}</h3><p>${a}</p></div>`)}</div></section>
<div class="hm-faq">${faq.map(([q, a]) => html`<div class="hm-qa"><h3>${q}</h3><p>${a}</p></div>`)}</div></section>
${raw(showcase.network({ service: 'actor' }))}
${raw(showcase.network({ service: 'actor' }))}
${raw(showcase.cta({ title: 'Build on it', text: 'Every task, its live stream, its webhooks and the router are an API. Apps get actor.task.* grants on OpenVibe.Services.', actions: [{ label: 'Read the API', href: '/docs', primary: true }, { label: 'See the agents', href: '/agents' }] }))}`,
${raw(showcase.cta({ title: 'Build on it', text: 'Every task, its live stream, its webhooks and the router are an API. Apps get actor.task.* grants on OpenVibe.Services.', actions: [{ label: 'Read the API', href: '/docs', primary: true }, { label: 'See the agents', href: '/agents' }] }))}`,
- }, status);
+ };
+ }
+
+ /** Render the home page through the route's own response shape (the POST failure path reuses it). */
+ async function home(req, res, { status = 200, form = {} } = {}) {
+ return page(req, res, await homePage({ viewer: req.viewer, query: req.query, form }), status);
}
}
/** Questions people ask first, answered from the running config and the catalog. */
/** Questions people ask first, answered from the running config and the catalog. */
@@ -293,7 +299,7 @@ ${a.available ? '' : html`<p class="muted small">${a.reason}</p>`}
@@ -293,7 +299,7 @@ ${a.available ? '' : html`<p class="muted small">${a.reason}</p>`}
title: 'Your tasks', crumbs: [{ label: 'Your tasks' }], styles: [showcase.STYLESHEET],
title: 'Your tasks', crumbs: [{ label: 'Your tasks' }], styles: [showcase.STYLESHEET],
body: html`<h1>Your tasks</h1>
body: html`<h1>Your tasks</h1>
<p class="muted">Today: ${today.tasks} of ${tier.tasksPerDay} free tasks, ${usd(today.usd)} of ${usd(tier.perDayUsd)}. Tasks are kept ${store.RETENTION_DAYS} days.</p>
<p class="muted">Today: ${today.tasks} of ${tier.tasksPerDay} free tasks, ${usd(today.usd)} of ${usd(tier.perDayUsd)}. Tasks are kept ${store.RETENTION_DAYS} days.</p>
-${taskForm(req)}
+${taskForm(req.viewer)}
${table(['Task', 'State', 'Agent', 'Cost', 'Started'], pageRows.rows.map((t) => {
${table(['Task', 'State', 'Agent', 'Cost', 'Started'], pageRows.rows.map((t) => {
const result = t.result ? (typeof t.result === 'string' ? JSON.parse(t.result) : t.result) : null;
const result = t.result ? (typeof t.result === 'string' ? JSON.parse(t.result) : t.result) : null;
return [html`<a href="/tasks/${t.id}">${t.task.length > 90 ? `${t.task.slice(0, 89)}…` : t.task}</a>`, badge(t.state, STATE_KIND[t.state]), result ? (catalog.byId(result.agent) || {}).name || result.agent : '—', usd(t.cost_usd == null ? null : Number(t.cost_usd)), time(t.created_at)];
return [html`<a href="/tasks/${t.id}">${t.task.length > 90 ? `${t.task.slice(0, 89)}…` : t.task}</a>`, badge(t.state, STATE_KIND[t.state]), result ? (catalog.byId(result.agent) || {}).name || result.agent : '—', usd(t.cost_usd == null ? null : Number(t.cost_usd)), time(t.created_at)];
@@ -489,10 +495,10 @@ ${table(['Agent', 'Could take it', 'Estimated cost', 'Why not'], (p.candidates |
@@ -489,10 +495,10 @@ ${table(['Agent', 'Could take it', 'Estimated cost', 'Why not'], (p.candidates |
});
});
// ── The agents ───────────────────────────────────────────
// ── The agents ───────────────────────────────────────────
- r.get('/agents', (req, res) => {
+ function agentsPage() {
const agents = catalog.list(config);
const agents = catalog.list(config);
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
title: 'The agents',
title: 'The agents',
description: 'Every agent system OpenVibe.Actor routes a task to — OpenVibe\'s own runtime, OpenAI\'s web agent, an open model on OpenVibe hardware, OpenVibe.Codes — with what each can do, its trust class and its price per million tokens.',
description: 'Every agent system OpenVibe.Actor routes a task to — OpenVibe\'s own runtime, OpenAI\'s web agent, an open model on OpenVibe hardware, OpenVibe.Codes — with what each can do, its trust class and its price per million tokens.',
crumbs: [{ label: 'Agents' }],
crumbs: [{ label: 'Agents' }],
@@ -520,11 +526,12 @@ ${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[m
@@ -520,11 +526,12 @@ ${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[m
<p>Before you see an answer, a model of another family reads it: DeepSeek's answers are checked by OpenAI's smallest model and OpenAI's by DeepSeek. In private mode only OpenVibe's own model may read the task, so it checks its own answer and the task says so. A failed check sends the task to the next agent that can do it.</p>
<p>Before you see an answer, a model of another family reads it: DeepSeek's answers are checked by OpenAI's smallest model and OpenAI's by DeepSeek. In private mode only OpenVibe's own model may read the task, so it checks its own answer and the task says so. A failed check sends the task to the next agent that can do it.</p>
<h2 id="trust">Trust</h2>
<h2 id="trust">Trust</h2>
<p><strong>first-party</strong> runs on OpenVibe's own hardware; <strong>external</strong> means the task's text goes to that provider (the OpenVibe runtime is OpenVibe's code, but its model runs at DeepSeek). Private mode takes only first-party agents.</p>`,
<p><strong>first-party</strong> runs on OpenVibe's own hardware; <strong>external</strong> means the task's text goes to that provider (the OpenVibe runtime is OpenVibe's code, but its model runs at DeepSeek). Private mode takes only first-party agents.</p>`,
- });
- });
+ };
+ }
+ r.get('/agents', (req, res) => page(req, res, agentsPage()));
// ── The API reference ────────────────────────────────────
// ── The API reference ────────────────────────────────────
- r.get('/docs', (req, res) => {
+ function docsPage() {
const b = config.baseUrl;
const b = config.baseUrl;
const create = `curl -s ${b}/api/v1/tasks \\
const create = `curl -s ${b}/api/v1/tasks \\
-H "authorization: Bearer $TOKEN" \\
-H "authorization: Bearer $TOKEN" \\
@@ -537,8 +544,8 @@ ${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[m
@@ -537,8 +544,8 @@ ${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[m
id: 3
id: 3
event: output
event: output
data: {"task_id":"tsk_…","seq":3,"kind":"output","agent":"openvibe-runtime","step":"tool_call","tool":"run_tool","input":"ssl {\\"target\\":\\"example.com\\"}"}`;
data: {"task_id":"tsk_…","seq":3,"kind":"output","agent":"openvibe-runtime","step":"tool_call","tool":"run_tool","input":"ssl {\\"target\\":\\"example.com\\"}"}`;
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
title: 'API',
title: 'API',
description: 'The OpenVibe.Actor API: create a task, watch it live over server-sent events, read the result with its cost and explanation, cancel it; list the agents and dry-run the router.',
description: 'The OpenVibe.Actor API: create a task, watch it live over server-sent events, read the result with its cost and explanation, cancel it; list the agents and dry-run the router.',
crumbs: [{ label: 'API' }],
crumbs: [{ label: 'API' }],
@@ -580,18 +587,25 @@ ${table(['Code', 'Meaning'], [
@@ -580,18 +587,25 @@ ${table(['Code', 'Meaning'], [
[html`<code>actor.check.unavailable</code>`, 'The answer could not be checked, so it was not delivered.'],
[html`<code>actor.check.unavailable</code>`, 'The answer could not be checked, so it was not delivered.'],
[html`<code>actor.task.timeout</code> / <code>actor.task.interrupted</code>`, 'It ran too long, or Actor restarted while it ran.'],
[html`<code>actor.task.timeout</code> / <code>actor.task.interrupted</code>`, 'It ran too long, or Actor restarted while it ran.'],
])}`,
])}`,
- });
- });
+ };
+ }
+ r.get('/docs', (req, res) => page(req, res, docsPage()));
// ── Limits and the update log ────────────────────────────
// ── Limits and the update log ────────────────────────────
r.get('/limits.json', (req, res) => {
r.get('/limits.json', (req, res) => {
res.set('Cache-Control', cache.htmlHeaders({ maxAge: 300 }));
res.set('Cache-Control', cache.htmlHeaders({ maxAge: 300 }));
res.json({ service: 'actor', free_tier: { tasks_per_day: tier.tasksPerDay, per_task_usd: tier.perTaskUsd, per_day_usd: tier.perDayUsd }, task_timeout_seconds: Math.round(config.taskTimeoutMs / 1000), retention_days: store.RETENTION_DAYS });
res.json({ service: 'actor', free_tier: { tasks_per_day: tier.tasksPerDay, per_task_usd: tier.perTaskUsd, per_day_usd: tier.perDayUsd }, task_timeout_seconds: Math.round(config.taskTimeoutMs / 1000), retention_days: store.RETENTION_DAYS });
});
});
- r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Actor', body: raw(frame.updatesBody({ service: 'actor', siteName: 'OpenVibe.Actor' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));
+ const updatesPage = () => ({ agent: true, index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Actor', body: raw(frame.updatesBody({ service: 'actor', siteName: 'OpenVibe.Actor' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) });
+ r.get('/updates', (req, res) => page(req, res, updatesPage()));
// ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt ──
// ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt ──
r.use(createDiscoveryRoutes(ctx));
r.use(createDiscoveryRoutes(ctx));
+
+ // The public pages the agent layer's Markdown twins (server/agent-pages.js) render with the same builders.
+ // A task page is behind a person's token and is deliberately absent.
+ r.publicViews = { home: homePage, agents: agentsPage, docs: docsPage, updates: updatesPage };
+
return r;
return r;
}
}
@@ -88,6 +88,10 @@ function renderPage(o) {
@@ -88,6 +88,10 @@ function renderPage(o) {
description: o.description || 'Give a task in plain words. Actor picks the best agent for it, ours or another platform\'s, checks the answer and shows the cost and why.',
description: o.description || 'Give a task in plain words. Actor picks the best agent for it, ours or another platform\'s, checks the answer and shows the cost and why.',
canonical,
canonical,
robots: o.index ? 'index, follow' : 'noindex, nofollow',
robots: o.index ? 'index, follow' : 'noindex, nofollow',
+ // A page with a Markdown twin (o.agent) advertises it in the head and the Link header; the twin itself
+ // is server/agent-pages.js.
+ agentLayer: o.agent === true,
+ llms: `${String(o.config.baseUrl).replace(/\/+$/, '')}/llms.txt`,
jsonLd: (o.jsonLd || []).filter(Boolean),
jsonLd: (o.jsonLd || []).filter(Boolean),
home: '/',
home: '/',
navLinks: NAV,
navLinks: NAV,
@@ -126,6 +130,10 @@ function send(res, status, o) {
@@ -126,6 +130,10 @@ function send(res, status, o) {
const personal = o.viewer && o.viewer.kind === 'user';
const personal = o.viewer && o.viewer.kind === 'user';
if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));
if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));
res.set('Vary', 'Cookie');
res.set('Vary', 'Cookie');
+ if (o.agent) {
+ const site = String(o.config.baseUrl).replace(/\/+$/, '');
+ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${site}${String(o.path || '/').split('?')[0]}`, `${site}/llms.txt`).header);
+ }
res.status(status).type('html').send(renderPage(o));
res.status(status).type('html').send(renderPage(o));
}
}