diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3bd2392..c0bcf31a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,11 @@ jobs: steps: - uses: actions/checkout@v4 + # The Vercel adapter used by the docs and website supports Node 22. + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 diff --git a/deploy/tabularis/README.md b/deploy/tabularis/README.md index 48d87c6c..8c4a6813 100644 --- a/deploy/tabularis/README.md +++ b/deploy/tabularis/README.md @@ -20,3 +20,40 @@ The registry's strict authoring endpoint rejects unknown properties; lenient ing The frontend follow-up shows compact locale-aware counts, exact accessible card labels, and distinct loading/empty/unavailable/populated states. It does not change counting APIs or perform tracked downloads. It can ship independently of theme-package support. Keep the theme rollout gated: packaged platform acceptance, real runtime/tooling version assignments, old-client/profile protection, staged kind admission, and published author journeys are still separate requirements. Neither this operator delta nor the statistics UI enables themes. + +## Declarative AI assistant providers + +`ai-assistant-kind.json` is a complete operator kind preset for Tabularis. It +adds the manifest extension contract, a Gemini example and an optional public +catalogue page. Packages contain data only; Tabularis owns HTTP, credentials, +proxies, templates and response parsing. ChatGPT account login remains built in. + +Operator rollout: + +1. Deploy the supporting Tabularium version with separate `id` and display + `name` fields, and back up the current kind configuration. +2. Create the kind through authenticated `POST /api/admin/kinds` using the preset + body, or copy it into the kind editor. For an existing kind, review the + current configuration before applying `PUT /api/admin/kinds/ai-assistant`. + Leave driver, theme and global extension settings intact. +3. Validate Gemini's complete standalone manifest against the merged core and + kind schema using `/api/manifest/validate`, including the HTTPS icon, native + `generateContent` recipe, model-resource URL and discovery filter. Confirm + that missing contract fields and executable extensions are rejected. +4. Keep the existing providers embedded in Tabularis. OpenAI, Anthropic, Ollama, + OpenRouter, Requesty, MiniMax, OpenAI Compatible and API Route need no external + replacement packages. ChatGPT account login remains built in too. +5. Assign the actual first supporting Tabularis runtime version when the + application release is determined. Publish a successor Gemini release with + `min_runtime_version` before enabling its production listing. Version 1.0.1 + is a development prerelease without that gate, not evidence of compatibility. +6. Register `TabularisDB/tabularis-gemini-plugin` as slug `gemini`, kind + `ai-assistant`. Ingest its standalone `.tabularium` and `universal` ZIP, + checking release version, SHA-256, size and release-integrity metadata. +7. Verify kind filtering, icons, install/select, model discovery, generation, + update/remove/reinstall and offline availability of embedded providers in + both transports. Source deployment alone does not apply the kind or publish + the provider. + +The preset is not applied automatically by deploying this repository. Registry +configuration and provider publication remain explicit rollout operations. diff --git a/deploy/tabularis/ai-assistant-kind.json b/deploy/tabularis/ai-assistant-kind.json new file mode 100644 index 00000000..c9d79b15 --- /dev/null +++ b/deploy/tabularis/ai-assistant-kind.json @@ -0,0 +1,292 @@ +{ + "key": "ai-assistant", + "label": "AI assistants", + "labelTranslations": { + "it": "Assistenti AI" + }, + "description": "Declarative AI providers for Tabularis. No plugin code is executed.", + "publicPageEnabled": true, + "publicPageCopy": { + "hero": "Choose your AI assistant", + "intro": "Use the providers built into Tabularis, or install additional providers such as Gemini. Credentials and settings stay in the application; external packages can be updated independently." + }, + "extensionsSchema": { + "ai_assistant": { + "type": "object", + "additionalProperties": false, + "required": true, + "properties": { + "api_version": { + "type": "integer", + "const": 1 + }, + "authentication": { + "type": "string", + "enum": ["api-key", "none"] + }, + "api_key_env": { + "type": "string", + "pattern": "^[A-Z_][A-Z0-9_]*$", + "maxLength": 100 + }, + "default_models": { + "type": "array", + "maxItems": 1000, + "items": { + "type": "string", + "maxLength": 2048 + } + }, + "optional_api_key": { + "type": "boolean" + }, + "api_key_header": { + "type": "string", + "maxLength": 100 + }, + "api_key_prefix": { + "type": "string", + "maxLength": 100 + }, + "direct": { + "type": "boolean" + }, + "generate": { + "type": "object", + "additionalProperties": false, + "required": ["requests", "response"], + "properties": { + "requests": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { + "type": "string", + "maxLength": 2048 + }, + "append_path": { + "type": "string", + "maxLength": 2048 + }, + "headers": { + "type": "object", + "maxProperties": 16, + "additionalProperties": { + "type": "string", + "maxLength": 2048 + } + }, + "body": { + "type": "object" + }, + "authenticated": { + "type": "boolean" + } + } + } + }, + "response": { + "type": "object", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { + "type": "string", + "maxLength": 2048 + }, + "select": { + "type": "object", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { + "type": "string", + "maxLength": 256 + }, + "equals": {}, + "contains": {}, + "starts_with": { + "type": "string", + "maxLength": 256 + }, + "missing_matches": { + "type": "boolean" + }, + "prefixes": { + "type": "array", + "maxItems": 16, + "items": { + "type": "string", + "maxLength": 100 + } + } + } + }, + "value_path": { + "type": "string", + "maxLength": 2048 + } + } + } + } + }, + "models": { + "type": "object", + "additionalProperties": false, + "required": ["sources"], + "properties": { + "sources": { + "type": "array", + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["request", "items_path", "id_path"], + "properties": { + "request": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { + "type": "string", + "maxLength": 2048 + }, + "append_path": { + "type": "string", + "maxLength": 2048 + }, + "headers": { + "type": "object", + "maxProperties": 16, + "additionalProperties": { + "type": "string", + "maxLength": 2048 + } + }, + "body": { + "type": "object" + }, + "authenticated": { + "type": "boolean" + } + } + }, + "items_path": { + "type": "string", + "maxLength": 2048 + }, + "id_path": { + "type": "string", + "maxLength": 2048 + }, + "filters": { + "type": "array", + "maxItems": 16, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { + "type": "string", + "maxLength": 256 + }, + "equals": {}, + "contains": {}, + "starts_with": { + "type": "string", + "maxLength": 256 + }, + "missing_matches": { + "type": "boolean" + }, + "prefixes": { + "type": "array", + "maxItems": 16, + "items": { + "type": "string", + "maxLength": 100 + } + } + } + } + } + } + } + }, + "cache_seconds": { + "type": "integer", + "minimum": 0, + "maximum": 86400 + }, + "fallback": { + "type": "boolean" + } + } + }, + "auto_select_priority": { + "type": "integer", + "minimum": 0, + "maximum": 1000 + }, + "require_explicit_model": { + "type": "boolean", + "default": false, + "description": "Require a saved or explicitly selected model instead of choosing the first discovered model." + } + }, + "allOf": [ + { + "required": ["api_version", "authentication", "generate", "models"] + } + ] + }, + "settings": { + "type": "array", + "maxItems": 32, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key", "label", "type"], + "properties": { + "key": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "label": { + "type": "string", + "maxLength": 2048 + }, + "description": { + "type": "string", + "maxLength": 2048 + }, + "type": { + "type": "string", + "enum": ["string", "number", "boolean", "select"] + }, + "default": {}, + "required": { + "type": "boolean" + }, + "options": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048 + } + } + } + } + } + }, + "customExample": { + "json": "{\n \"id\": \"gemini\",\n \"name\": \"Google Gemini\",\n \"version\": \"1.0.1\",\n \"kind\": \"ai-assistant\",\n \"description\": \"Google Gemini assistant using the native generateContent API.\",\n \"ai_assistant\": {\n \"api_version\": 1,\n \"authentication\": \"api-key\",\n \"api_key_env\": \"GEMINI_API_KEY\",\n \"api_key_header\": \"x-goog-api-key\",\n \"api_key_prefix\": \"\",\n \"default_models\": [\n \"models/gemini-flash-latest\"\n ],\n \"generate\": {\n \"requests\": [\n {\n \"url\": \"https://generativelanguage.googleapis.com/v1beta/{{model}}:generateContent\",\n \"body\": {\n \"systemInstruction\": {\n \"parts\": [\n {\n \"text\": \"{{system_prompt}}\"\n }\n ]\n },\n \"contents\": [\n {\n \"role\": \"user\",\n \"parts\": [\n {\n \"text\": \"{{prompt}}\"\n }\n ]\n }\n ]\n }\n }\n ],\n \"response\": {\n \"path\": \"/candidates/0/content/parts\",\n \"select\": {\n \"path\": \"/thought\",\n \"equals\": false,\n \"missing_matches\": true\n },\n \"value_path\": \"/text\"\n }\n },\n \"models\": {\n \"sources\": [\n {\n \"request\": {\n \"url\": \"https://generativelanguage.googleapis.com/v1beta/models\"\n },\n \"items_path\": \"/models\",\n \"id_path\": \"/name\",\n \"filters\": [\n {\n \"path\": \"/supportedGenerationMethods\",\n \"contains\": \"generateContent\"\n }\n ]\n }\n ],\n \"cache_seconds\": 86400\n }\n },\n \"settings\": [],\n \"license\": \"Apache-2.0\",\n \"icon\": \"https://raw.githubusercontent.com/TabularisDB/tabularis-gemini-plugin/main/icon.svg\"\n}" + } +} diff --git a/packages/manifest/test/tabularis-ai-assistant.test.ts b/packages/manifest/test/tabularis-ai-assistant.test.ts new file mode 100644 index 00000000..343a1ef4 --- /dev/null +++ b/packages/manifest/test/tabularis-ai-assistant.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'bun:test' +import { buildSchema, ManifestSchema, validateManifest, type ExtensionsDelta } from '../src/index' +import preset from '../../../deploy/tabularis/ai-assistant-kind.json' + +const schema = buildSchema({ + coreSchema: ManifestSchema as never, + kindOverrides: { 'ai-assistant': preset.extensionsSchema as ExtensionsDelta }, + kind: 'ai-assistant', +}) + +describe('Tabularis AI assistant kind preset', () => { + it('accepts the native Gemini example with a stable id and display name', () => { + const manifest = JSON.parse(preset.customExample.json) as Record + expect(validateManifest(manifest, schema).ok).toBe(true) + expect(manifest.id).toBe('gemini') + expect((manifest.ai_assistant as Record).api_key_header).toBe('x-goog-api-key') + }) + + it('requires the provider contract and excludes executable extension fields', () => { + const manifest = JSON.parse(preset.customExample.json) as Record + for (const key of ['executable', 'interpreter', 'ui_extensions']) { + expect(validateManifest({ ...manifest, [key]: 'forbidden' }, schema).ok).toBe(false) + } + const missing = { ...manifest } + delete missing.ai_assistant + expect(validateManifest(missing, schema).ok).toBe(false) + const contract = manifest.ai_assistant as Record + const incomplete = { ...contract } + delete incomplete.generate + expect(validateManifest({ ...manifest, ai_assistant: incomplete }, schema).ok).toBe(false) + }) +})