From db8d03f80db2e33ceee73652b2bbd5dbd9450ecd Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Mon, 14 Sep 2026 20:00:35 +1000 Subject: [PATCH] feat(config): publish profile workspace schemas --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 2 + bun.lock | 4 +- .../schemas/v1/project-workspace.schema.json | 441 +++++++ .../schemas/v1/user-workspace.schema.json | 1057 +++++++++++++++++ .../docs/docs/reference/configuration.mdx | 38 +- package.json | 8 +- scripts/generate-workspace-schemas.ts | 95 ++ src/models/workspace-config.ts | 173 +-- .../unit/models/workspace-json-schema.test.ts | 123 ++ 10 files changed, 1858 insertions(+), 84 deletions(-) create mode 100644 docs/public/schemas/v1/project-workspace.schema.json create mode 100644 docs/public/schemas/v1/user-workspace.schema.json create mode 100644 scripts/generate-workspace-schemas.ts create mode 100644 tests/unit/models/workspace-json-schema.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed4367f3..b720d070 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: with: bun-version-file: package.json - run: bun install --frozen-lockfile --ignore-scripts + - run: bun run schema:check - run: bun run build typecheck: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a326ce2..adcd124f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ - Claude Code global profiles using isolated `CLAUDE_CONFIG_DIR` roots, additive MCP configuration, strict settings, native marketplace/plugin lifecycle, generated launchers, and ownership-safe cleanup. +- Versioned user and project workspace JSON Schemas generated from the runtime + Zod models, with CI drift enforcement and YAML Language Server setup docs. ## [1.0.0] - 2026-03-13 diff --git a/bun.lock b/bun.lock index d208e73c..8778bd49 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ "micromatch": "^4.0.8", "read-cmd-shim": "^4.0.0", "simple-git": "^3.30.0", - "zod": "^3.22.4", + "zod": "^3.25.28", }, "devDependencies": { "@biomejs/biome": "^1.9.0", @@ -25,8 +25,10 @@ "@types/js-yaml": "^4.0.9", "@types/micromatch": "^4.0.10", "@types/node": "^20.11.5", + "ajv": "8.18.0", "shx": "^0.4.0", "typescript": "^5.3.3", + "zod-to-json-schema": "3.25.2", }, }, }, diff --git a/docs/public/schemas/v1/project-workspace.schema.json b/docs/public/schemas/v1/project-workspace.schema.json new file mode 100644 index 00000000..e3e35307 --- /dev/null +++ b/docs/public/schemas/v1/project-workspace.schema.json @@ -0,0 +1,441 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://allagents.dev/schemas/v1/project-workspace.schema.json", + "title": "AllAgents project workspace", + "description": "Configuration for a project .allagents/workspace.yaml. Global profiles are not accepted.", + "$ref": "#/definitions/AllAgentsProjectWorkspace", + "definitions": { + "AllAgentsProjectWorkspace": { + "type": "object", + "properties": { + "version": { + "type": "number" + }, + "setup": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "run": { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/setup/items/anyOf/0" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aix", + "android", + "darwin", + "freebsd", + "haiku", + "linux", + "openbsd", + "sunos", + "win32", + "cygwin", + "netbsd" + ] + }, + "minItems": 1 + }, + "architectures": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "arm", + "arm64", + "ia32", + "loong64", + "mips", + "mipsel", + "ppc", + "ppc64", + "riscv64", + "s390", + "s390x", + "x64" + ] + }, + "minItems": 1 + } + }, + "required": [ + "run" + ], + "additionalProperties": false + } + ] + } + }, + "workspace": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "dest": { + "type": "string" + } + }, + "additionalProperties": true + } + ] + } + } + }, + "required": [ + "files" + ], + "additionalProperties": true + }, + "repositories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "source": { + "type": "string" + }, + "repo": { + "type": "string" + }, + "description": { + "type": "string" + }, + "skills": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "managed": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "clone", + "sync" + ] + } + ] + }, + "branch": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": true + } + }, + "plugins": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/plugins/items/anyOf/0" + }, + "clients": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "universal", + "claude", + "copilot", + "codex", + "pi", + "omp", + "cursor", + "opencode", + "gemini", + "factory", + "ampcode", + "vscode", + "openclaw", + "windsurf", + "cline", + "continue", + "roo", + "kilo", + "trae", + "augment", + "zencoder", + "junie", + "openhands", + "kiro", + "replit", + "kimi" + ] + } + }, + "install": { + "type": "string", + "enum": [ + "file", + "native" + ] + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + } + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "properties": { + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "exclude" + ], + "additionalProperties": true + } + ] + }, + "ref": { + "type": "string" + } + }, + "required": [ + "source" + ], + "additionalProperties": false + } + ] + } + }, + "clients": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + }, + { + "type": "string", + "pattern": "^(?:universal|claude|copilot|codex|pi|omp|cursor|opencode|gemini|factory|ampcode|vscode|openclaw|windsurf|cline|continue|roo|kilo|trae|augment|zencoder|junie|openhands|kiro|replit|kimi):(?:file|native)$" + }, + { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + }, + "install": { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/plugins/items/anyOf/1/properties/install", + "default": "file" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + } + ] + } + }, + "vscode": { + "type": "object", + "properties": { + "output": { + "type": "string" + } + }, + "additionalProperties": true + }, + "syncMode": { + "type": "string", + "enum": [ + "symlink", + "copy" + ] + }, + "mcpProxy": { + "type": "object", + "properties": { + "clients": { + "type": "array", + "items": { + "type": "string" + } + }, + "servers": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "proxy": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "proxy" + ], + "additionalProperties": true + } + } + }, + "required": [ + "clients" + ], + "additionalProperties": true + }, + "mcpServers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "url": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ] + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsProjectWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + ] + } + }, + "disabledSkills": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabledSkills": { + "type": "array", + "items": { + "type": "string" + } + }, + "profiles": { + "not": {} + } + }, + "required": [ + "repositories", + "plugins", + "clients" + ], + "additionalProperties": true + } + } +} diff --git a/docs/public/schemas/v1/user-workspace.schema.json b/docs/public/schemas/v1/user-workspace.schema.json new file mode 100644 index 00000000..20c15212 --- /dev/null +++ b/docs/public/schemas/v1/user-workspace.schema.json @@ -0,0 +1,1057 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://allagents.dev/schemas/v1/user-workspace.schema.json", + "title": "AllAgents user workspace", + "description": "Configuration for ~/.allagents/workspace.yaml, including global profiles.", + "$ref": "#/definitions/AllAgentsUserWorkspace", + "definitions": { + "AllAgentsUserWorkspace": { + "type": "object", + "properties": { + "version": { + "type": "number" + }, + "setup": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "run": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/setup/items/anyOf/0" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aix", + "android", + "darwin", + "freebsd", + "haiku", + "linux", + "openbsd", + "sunos", + "win32", + "cygwin", + "netbsd" + ] + }, + "minItems": 1 + }, + "architectures": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "arm", + "arm64", + "ia32", + "loong64", + "mips", + "mipsel", + "ppc", + "ppc64", + "riscv64", + "s390", + "s390x", + "x64" + ] + }, + "minItems": 1 + } + }, + "required": [ + "run" + ], + "additionalProperties": false + } + ] + } + }, + "workspace": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "dest": { + "type": "string" + } + }, + "additionalProperties": true + } + ] + } + } + }, + "required": [ + "files" + ], + "additionalProperties": true + }, + "repositories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "source": { + "type": "string" + }, + "repo": { + "type": "string" + }, + "description": { + "type": "string" + }, + "skills": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "managed": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "clone", + "sync" + ] + } + ] + }, + "branch": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": true + }, + "default": [] + }, + "plugins": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/0" + }, + "clients": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "universal", + "claude", + "copilot", + "codex", + "pi", + "omp", + "cursor", + "opencode", + "gemini", + "factory", + "ampcode", + "vscode", + "openclaw", + "windsurf", + "cline", + "continue", + "roo", + "kilo", + "trae", + "augment", + "zencoder", + "junie", + "openhands", + "kiro", + "replit", + "kimi" + ] + } + }, + "install": { + "type": "string", + "enum": [ + "file", + "native" + ] + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + } + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "properties": { + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "exclude" + ], + "additionalProperties": true + } + ] + }, + "ref": { + "type": "string" + } + }, + "required": [ + "source" + ], + "additionalProperties": false + } + ] + }, + "default": [] + }, + "clients": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + }, + { + "type": "string", + "pattern": "^(?:universal|claude|copilot|codex|pi|omp|cursor|opencode|gemini|factory|ampcode|vscode|openclaw|windsurf|cline|continue|roo|kilo|trae|augment|zencoder|junie|openhands|kiro|replit|kimi):(?:file|native)$" + }, + { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/install", + "default": "file" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + } + ] + }, + "default": [] + }, + "vscode": { + "type": "object", + "properties": { + "output": { + "type": "string" + } + }, + "additionalProperties": true + }, + "syncMode": { + "type": "string", + "enum": [ + "symlink", + "copy" + ] + }, + "mcpProxy": { + "type": "object", + "properties": { + "clients": { + "type": "array", + "items": { + "type": "string" + } + }, + "servers": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "proxy": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "proxy" + ], + "additionalProperties": true + } + } + }, + "required": [ + "clients" + ], + "additionalProperties": true + }, + "mcpServers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "url": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ] + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + ] + } + }, + "disabledSkills": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabledSkills": { + "type": "array", + "items": { + "type": "string" + } + }, + "profiles": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "clients": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "claude" + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/install", + "default": "file" + }, + "launcher": { + "type": "string", + "pattern": "^(?!(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\.|$))(?!.*\\.$)[a-z0-9][a-z0-9._-]{0,63}$" + }, + "settings": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 1 + }, + "effortLevel": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "fallbackModel": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, + "outputStyle": { + "type": "string", + "minLength": 1 + }, + "autoMemoryEnabled": { + "type": "boolean" + }, + "spinnerTipsEnabled": { + "type": "boolean" + }, + "autoUpdatesChannel": { + "type": "string", + "enum": [ + "stable", + "latest" + ] + } + }, + "additionalProperties": false, + "default": {} + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "opencode" + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/install" + }, + "launcher": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/launcher" + }, + "settings": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 1 + }, + "small_model": { + "type": "string", + "minLength": 1 + }, + "default_agent": { + "type": "string", + "minLength": 1 + }, + "username": { + "type": "string", + "minLength": 1 + }, + "share": { + "type": "string", + "enum": [ + "manual", + "auto", + "disabled" + ] + }, + "autoupdate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "const": "notify" + } + ] + }, + "snapshot": { + "type": "boolean" + }, + "subagent_depth": { + "type": "integer", + "minimum": 0 + }, + "logLevel": { + "type": "string", + "enum": [ + "DEBUG", + "INFO", + "WARN", + "ERROR" + ] + }, + "disabled_providers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "enabled_providers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "default": {} + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "copilot" + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/install" + }, + "launcher": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/launcher" + }, + "settings": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 1 + }, + "theme": { + "type": "string", + "enum": [ + "default", + "github", + "dim", + "high-contrast", + "colorblind" + ] + }, + "autoUpdate": { + "type": "boolean" + }, + "autoUpdatesChannel": { + "type": "string", + "enum": [ + "stable", + "prerelease" + ] + }, + "banner": { + "type": "string", + "enum": [ + "always", + "once", + "never" + ] + }, + "askUser": { + "type": "boolean" + }, + "includeCoAuthoredBy": { + "type": "boolean" + }, + "stream": { + "type": "boolean" + }, + "streamerMode": { + "type": "boolean" + }, + "toolSearch": { + "type": "boolean" + }, + "updateTerminalTitle": { + "type": "boolean" + }, + "respectGitignore": { + "type": "boolean" + }, + "disableAllHooks": { + "type": "boolean" + }, + "experimental": { + "type": "boolean" + }, + "bashEnv": { + "type": "boolean" + }, + "keepAlive": { + "type": "string", + "enum": [ + "on", + "off", + "busy" + ] + }, + "commandHistoryMaxSize": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + }, + "compactPaste": { + "type": "boolean" + }, + "mouse": { + "type": "boolean" + }, + "terminalProgress": { + "type": "boolean" + }, + "remote": { + "type": "string", + "enum": [ + "on", + "off" + ] + }, + "remoteExport": { + "type": "boolean" + }, + "ide.autoConnect": { + "type": "boolean" + }, + "shellShortcut": { + "type": "boolean" + }, + "customAgents.defaultLocalOnly": { + "type": "boolean" + }, + "storeTokenPlaintext": { + "type": "boolean" + }, + "disabledMcpServers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "enabledMcpServers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "default": {} + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "codex" + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/install" + }, + "launcher": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/launcher" + }, + "settings": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 1 + }, + "model_reasoning_effort": { + "type": "string", + "enum": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "model_reasoning_summary": { + "type": "string", + "enum": [ + "auto", + "concise", + "detailed", + "none" + ] + }, + "model_verbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "approval_policy": { + "type": "string", + "enum": [ + "on-request", + "never" + ] + }, + "sandbox_mode": { + "type": "string", + "enum": [ + "read-only", + "workspace-write", + "danger-full-access" + ] + }, + "web_search": { + "type": "string", + "enum": [ + "disabled", + "cached", + "indexed", + "live" + ] + }, + "personality": { + "type": "string", + "enum": [ + "none", + "friendly", + "pragmatic" + ] + } + }, + "additionalProperties": false, + "default": {} + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "pi", + "omp" + ] + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/install" + }, + "launcher": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/launcher" + }, + "settings": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "default": {} + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "universal", + "cursor", + "gemini", + "factory", + "ampcode", + "vscode", + "openclaw", + "windsurf", + "cline", + "continue", + "roo", + "kilo", + "trae", + "augment", + "zencoder", + "junie", + "openhands", + "kiro", + "replit", + "kimi" + ] + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/install" + }, + "launcher": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/0/properties/launcher" + }, + "settings": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/clients/items/anyOf/4/properties/settings", + "default": {} + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + ] + }, + "minItems": 1 + }, + "plugins": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/0" + }, + { + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/0" + }, + "ref": { + "type": "string" + }, + "install": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/install" + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "properties": { + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "exclude" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "source" + ], + "additionalProperties": false + } + ] + }, + "default": [] + }, + "mcpServers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "url": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string", + "pattern": "^\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}$" + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ] + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/profiles/additionalProperties/properties/mcpServers/additionalProperties/anyOf/0/properties/headers/additionalProperties" + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/AllAgentsUserWorkspace/properties/plugins/items/anyOf/1/properties/clients/items" + } + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + ] + } + } + }, + "required": [ + "clients" + ], + "additionalProperties": false + }, + "propertyNames": { + "pattern": "^(?!(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\.|$))(?!.*\\.$)[a-z0-9][a-z0-9._-]{0,63}$" + } + } + }, + "additionalProperties": true + } + } +} diff --git a/docs/src/content/docs/docs/reference/configuration.mdx b/docs/src/content/docs/docs/reference/configuration.mdx index 7033eec9..7c70be06 100644 --- a/docs/src/content/docs/docs/reference/configuration.mdx +++ b/docs/src/content/docs/docs/reference/configuration.mdx @@ -53,6 +53,38 @@ clients: - cursor ``` +### JSON Schema and editor validation + +AllAgents publishes separate, versioned schemas for the two workspace scopes: + +- [Project workspace schema](https://allagents.dev/schemas/v1/project-workspace.schema.json) + for `.allagents/workspace.yaml`. It rejects the global `profiles` field. +- [User workspace schema](https://allagents.dev/schemas/v1/user-workspace.schema.json) + for `~/.allagents/workspace.yaml`. It includes global profiles and strict + client-specific settings. + +Add the matching YAML Language Server directive as the first line of each file: + +```yaml +# yaml-language-server: $schema=https://allagents.dev/schemas/v1/project-workspace.schema.json +repositories: [] +plugins: [] +clients: [] +``` + +```yaml +# yaml-language-server: $schema=https://allagents.dev/schemas/v1/user-workspace.schema.json +profiles: + review: + clients: + - name: claude +``` + +The `v1` URLs are stable. A future breaking schema contract will use a new +versioned directory. Runtime Zod validation remains authoritative; regenerate +the committed schemas with `bun run schema:generate` after changing workspace +input models, and verify drift with `bun run schema:check`. + ### Client Install Modes A bare client name uses file sync. Native mode delegates compatible plugin @@ -550,6 +582,10 @@ plugins: - superpowers@obra/superpowers ``` -This file follows the same `plugins` format as the project-level workspace.yaml but does not support `workspace`, `repositories`, or `clients` fields. User-scoped plugins sync to user-level directories (`~/.claude/`, `~/.codex/`, etc.) and are available across all projects. +The user file accepts the same ordinary workspace fields as a project file, and +also accepts global `profiles`. Its `repositories`, `plugins`, and `clients` +arrays default to empty, so it may contain only profiles. User-scoped ordinary +plugins sync to user-level directories (`~/.claude/`, `~/.codex/`, etc.); +profile lifecycle remains explicit and independent. Sync state for user-scoped plugins is tracked in `~/.allagents/sync-state.json`. diff --git a/package.json b/package.json index 67f2daa8..666cd952 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "test:e2e": "bun test tests/e2e", "smoke:mcp-oauth": "bun run scripts/smoke-mcp-oauth.ts", "dev:mcp-server": "bun run scripts/dev-mcp-server.ts", + "schema:generate": "bun run scripts/generate-workspace-schemas.ts", + "schema:check": "bun run scripts/generate-workspace-schemas.ts --check", "typecheck": "tsc --noEmit", "lint": "biome lint src", "lint:fix": "biome lint --write src", @@ -67,7 +69,7 @@ "micromatch": "^4.0.8", "read-cmd-shim": "^4.0.0", "simple-git": "^3.30.0", - "zod": "^3.22.4" + "zod": "^3.25.28" }, "devDependencies": { "@biomejs/biome": "^1.9.0", @@ -75,8 +77,10 @@ "@types/js-yaml": "^4.0.9", "@types/micromatch": "^4.0.10", "@types/node": "^20.11.5", + "ajv": "8.18.0", "shx": "^0.4.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "zod-to-json-schema": "3.25.2" }, "overrides": { "chalk": "^4.1.2" diff --git a/scripts/generate-workspace-schemas.ts b/scripts/generate-workspace-schemas.ts new file mode 100644 index 00000000..bc556e29 --- /dev/null +++ b/scripts/generate-workspace-schemas.ts @@ -0,0 +1,95 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + ProjectWorkspaceConfigSchema, + UserWorkspaceConfigSchema, +} from '../src/models/workspace-config.js'; + +const SCHEMA_VERSION = 'v1'; +const SCHEMA_ROOT = resolve( + import.meta.dir, + '..', + 'docs', + 'public', + 'schemas', + SCHEMA_VERSION, +); +const PUBLIC_ROOT = `https://allagents.dev/schemas/${SCHEMA_VERSION}`; + +const WORKSPACE_SCHEMAS = [ + { + fileName: 'project-workspace.schema.json', + definitionName: 'AllAgentsProjectWorkspace', + title: 'AllAgents project workspace', + description: + 'Configuration for a project .allagents/workspace.yaml. Global profiles are not accepted.', + schema: ProjectWorkspaceConfigSchema, + }, + { + fileName: 'user-workspace.schema.json', + definitionName: 'AllAgentsUserWorkspace', + title: 'AllAgents user workspace', + description: + 'Configuration for ~/.allagents/workspace.yaml, including global profiles.', + schema: UserWorkspaceConfigSchema, + }, +] as const; + +export interface GeneratedWorkspaceSchema { + readonly fileName: string; + readonly path: string; + readonly url: string; + readonly content: string; +} + +export function generateWorkspaceSchemas(): readonly GeneratedWorkspaceSchema[] { + return WORKSPACE_SCHEMAS.map((entry) => { + const generated = zodToJsonSchema(entry.schema, { + name: entry.definitionName, + target: 'jsonSchema7', + effectStrategy: 'input', + removeAdditionalStrategy: 'strict', + }); + const { $schema, ...body } = generated; + const document = { + $schema, + $id: `${PUBLIC_ROOT}/${entry.fileName}`, + title: entry.title, + description: entry.description, + ...body, + }; + return { + fileName: entry.fileName, + path: resolve(SCHEMA_ROOT, entry.fileName), + url: document.$id, + content: `${JSON.stringify(document, null, 2)}\n`, + }; + }); +} + +async function writeSchemas(check: boolean): Promise { + const schemas = generateWorkspaceSchemas(); + if (check) { + const drifted: string[] = []; + for (const schema of schemas) { + const current = await readFile(schema.path, 'utf8').catch(() => null); + if (current !== schema.content) drifted.push(schema.path); + } + if (drifted.length > 0) { + throw new Error( + `Generated workspace schemas are stale:\n${drifted.map((path) => `- ${path}`).join('\n')}\nRun bun run schema:generate.`, + ); + } + return; + } + + await mkdir(SCHEMA_ROOT, { recursive: true }); + await Promise.all( + schemas.map((schema) => writeFile(schema.path, schema.content, 'utf8')), + ); +} + +if (import.meta.main) { + await writeSchemas(process.argv.includes('--check')); +} diff --git a/src/models/workspace-config.ts b/src/models/workspace-config.ts index 130eed91..8880e09c 100644 --- a/src/models/workspace-config.ts +++ b/src/models/workspace-config.ts @@ -126,39 +126,24 @@ export type InstallMode = z.infer; * "claude:native" → colon shorthand, parsed to { name: "claude", install: "native" } * { name, install } → explicit object form */ +const CLIENT_INSTALL_SHORTHAND_PATTERN = new RegExp( + `^(?:${ClientTypeSchema.options.join('|')}):(?:${InstallModeSchema.options.join('|')})$`, +); + +const ClientInstallShorthandSchema = z + .string() + .regex( + CLIENT_INSTALL_SHORTHAND_PATTERN, + `Expected CLIENT:INSTALL with a known client and one of: ${InstallModeSchema.options.join(', ')}`, + ) + .transform((value) => { + const [name, install] = value.split(':') as [ClientType, InstallMode]; + return { name, install }; + }); + export const ClientEntrySchema = z.union([ - z.string().transform((s, ctx) => { - const colonIdx = s.indexOf(':'); - if (colonIdx === -1) { - // Bare string — validate as client type - const result = ClientTypeSchema.safeParse(s); - if (!result.success) { - for (const issue of result.error.issues) ctx.addIssue(issue); - return z.NEVER; - } - return result.data; - } - // Colon shorthand — split on first colon - const name = s.slice(0, colonIdx); - const mode = s.slice(colonIdx + 1); - const nameResult = ClientTypeSchema.safeParse(name); - if (!nameResult.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Invalid client type: '${name}'`, - }); - return z.NEVER; - } - const modeResult = InstallModeSchema.safeParse(mode); - if (!modeResult.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Invalid install mode: '${mode}'. Valid modes: ${InstallModeSchema.options.join(', ')}`, - }); - return z.NEVER; - } - return { name: nameResult.data, install: modeResult.data }; - }), + ClientTypeSchema, + ClientInstallShorthandSchema, z.object({ name: ClientTypeSchema, install: InstallModeSchema.default('file'), @@ -414,24 +399,9 @@ export const ProfileSecretReferenceSchema = z */ export const ProfileNameSchema = z .string() - .min(1) - .max(64) .regex( - /^[a-z0-9][a-z0-9._-]{0,63}$/, - 'Expected 1-64 lowercase ASCII characters starting with a letter or number', - ) - .refine((name) => name !== '.' && name !== '..', { - message: "'.' and '..' are not valid profile or launcher names", - }) - .refine((name) => !name.endsWith('.'), { - message: 'Profile and launcher names cannot end with a dot', - }) - .refine( - (name) => - !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name), - { - message: 'Reserved device basenames are not allowed', - }, + /^(?!(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$))(?!.*\.$)[a-z0-9][a-z0-9._-]{0,63}$/, + 'Expected 1-64 portable lowercase ASCII characters starting with a letter or number', ); export type ProfileName = z.infer; @@ -545,38 +515,81 @@ export type CodexProfileSettings = z.infer< >; /** - * Profile clients deliberately use object form only. Unsupported clients still - * parse with empty settings so orchestration can report an adapter capability - * error instead of misclassifying a valid public client name as bad syntax. + * Profile clients deliberately use object form only. Each adapter owns a + * structural settings schema so runtime validation and generated JSON Schema + * expose the same input contract. Clients without an adapter accept only an + * empty settings object, allowing orchestration to report capability errors. */ -export const ProfileClientSchema = z - .object({ - name: ClientTypeSchema, - install: InstallModeSchema.default('file'), - launcher: ProfileNameSchema.optional(), - settings: z.record(z.unknown()).default({}), - }) - .strict() - .superRefine((client, context) => { - const settingsSchema = - client.name === 'claude' - ? ClaudeProfileSettingsSchema - : client.name === 'opencode' - ? OpenCodeProfileSettingsSchema - : client.name === 'copilot' - ? CopilotProfileSettingsSchema - : client.name === 'codex' - ? CodexProfileSettingsSchema - : EmptyProfileSettingsSchema; - const result = settingsSchema.safeParse(client.settings); - if (result.success) return; - for (const issue of result.error.issues) { - context.addIssue({ - ...issue, - path: ['settings', ...issue.path], - }); - } - }); +const ProfileClientCommonShape = { + install: InstallModeSchema.default('file'), + launcher: ProfileNameSchema.optional(), +} as const; + +export const ProfileClientSchema = z.union([ + z + .object({ + name: z.literal('claude'), + ...ProfileClientCommonShape, + settings: ClaudeProfileSettingsSchema.default({}), + }) + .strict(), + z + .object({ + name: z.literal('opencode'), + ...ProfileClientCommonShape, + settings: OpenCodeProfileSettingsSchema.default({}), + }) + .strict(), + z + .object({ + name: z.literal('copilot'), + ...ProfileClientCommonShape, + settings: CopilotProfileSettingsSchema.default({}), + }) + .strict(), + z + .object({ + name: z.literal('codex'), + ...ProfileClientCommonShape, + settings: CodexProfileSettingsSchema.default({}), + }) + .strict(), + z + .object({ + name: z.enum(['pi', 'omp']), + ...ProfileClientCommonShape, + settings: EmptyProfileSettingsSchema.default({}), + }) + .strict(), + z + .object({ + name: z.enum([ + 'universal', + 'cursor', + 'gemini', + 'factory', + 'ampcode', + 'vscode', + 'openclaw', + 'windsurf', + 'cline', + 'continue', + 'roo', + 'kilo', + 'trae', + 'augment', + 'zencoder', + 'junie', + 'openhands', + 'kiro', + 'replit', + 'kimi', + ]), + ...ProfileClientCommonShape, + settings: EmptyProfileSettingsSchema.default({}), + }) + .strict(), +]); export type ProfileClient = z.infer; diff --git a/tests/unit/models/workspace-json-schema.test.ts b/tests/unit/models/workspace-json-schema.test.ts new file mode 100644 index 00000000..effbc348 --- /dev/null +++ b/tests/unit/models/workspace-json-schema.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from 'bun:test'; +import { readFile } from 'node:fs/promises'; +import Ajv from 'ajv'; +import { load } from 'js-yaml'; +import { generateWorkspaceSchemas } from '../../../scripts/generate-workspace-schemas.js'; +import { + ProjectWorkspaceConfigSchema, + UserWorkspaceConfigSchema, +} from '../../../src/models/workspace-config.js'; + +function parsedYaml(source: string): unknown { + return load(source); +} +describe('published workspace JSON Schemas', () => { + test('validates real user and project workspace YAML by scope', async () => { + const schemaEntries = generateWorkspaceSchemas(); + const generated = new Map( + await Promise.all( + schemaEntries.map(async (schema) => [ + schema.fileName, + JSON.parse(await readFile(schema.path, 'utf8')), + ] as const), + ), + ); + for (const schema of schemaEntries) { + expect(generated.get(schema.fileName).$id).toBe(schema.url); + } + const ajv = new Ajv({ allErrors: true, strict: false }); + const validateUser = ajv.compile( + generated.get('user-workspace.schema.json'), + ); + const validateProject = ajv.compile( + generated.get('project-workspace.schema.json'), + ); + + const userWorkspace = parsedYaml(` +profiles: + review: + clients: + - name: claude + launcher: claude-review + settings: + model: sonnet + effortLevel: xhigh + - name: codex + install: native + settings: + approval_policy: on-request + plugins: + - source: owner/review-tools + ref: stable + skills: + exclude: [legacy] + mcpServers: + review: + command: review-mcp + env: + REVIEW_TOKEN: \${REVIEW_TOKEN} +`); + const projectWorkspace = parsedYaml(` +repositories: [] +plugins: + - source: owner/project-tools + ref: main +clients: + - name: claude + install: native +`); + const projectWithProfiles = parsedYaml(` +repositories: [] +plugins: [] +clients: [] +profiles: + review: + clients: + - name: claude +`); + const userWithUnknownSettings = parsedYaml(` +profiles: + review: + clients: + - name: claude + settings: + unknownSetting: true +`); + const userWithInvalidClientShorthand = parsedYaml(` +clients: + - claude:bogus +`); + const userWithInvalidProfileName = parsedYaml(` +profiles: + ../escape: + clients: + - name: claude +`); + + expect(validateUser(userWorkspace)).toBe(true); + expect(UserWorkspaceConfigSchema.safeParse(userWorkspace).success).toBe( + true, + ); + expect(validateProject(projectWorkspace)).toBe(true); + expect( + ProjectWorkspaceConfigSchema.safeParse(projectWorkspace).success, + ).toBe(true); + expect(validateProject(projectWithProfiles)).toBe(false); + expect( + ProjectWorkspaceConfigSchema.safeParse(projectWithProfiles).success, + ).toBe(false); + expect(validateUser(userWithUnknownSettings)).toBe(false); + expect( + UserWorkspaceConfigSchema.safeParse(userWithUnknownSettings).success, + ).toBe(false); + expect(validateUser(userWithInvalidClientShorthand)).toBe(false); + expect( + UserWorkspaceConfigSchema.safeParse(userWithInvalidClientShorthand) + .success, + ).toBe(false); + expect(validateUser(userWithInvalidProfileName)).toBe(false); + expect( + UserWorkspaceConfigSchema.safeParse(userWithInvalidProfileName).success, + ).toBe(false); + }); +});