diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4074969..d1f2e1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,8 @@ jobs: - run: npm install -g @anthropic-ai/claude-code - name: Shared skills are in sync with packages run: python3 scripts/build.py --check + - name: Public documentation regression tests + run: python3 -m unittest discover -s tests - name: Release checks env: CATALOG_AVAILABLE: ${{ steps.catalog.outputs.available }} diff --git a/README.md b/README.md index 21bd167..6c90f69 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ shared/ agent-author/ # create / edit / delete, narrative, functions agent-version-review/ # diff, release, promote, roll back agent-knowledge/ # connect existing knowledge documents - review-cases.json # starter prompts + the 5 positive / 3 negative cases submitted to OpenAI + review-cases.json # starter prompts and synthetic verification scenarios packages/ openai/ # OpenAI portable plugin (ChatGPT + Codex) plugin.json mcp.json skills/ claude/ # Claude Code plugin .claude-plugin/plugin.json .mcp.json skills/ - grok-bot/ # Grok Bot plugin (Cursor plugin format, static OAuth client grok-bot) + grok-bot/ # Grok Bot plugin (Cursor plugin format) .cursor-plugin/plugin.json mcp.json assets/ skills/ LICENSE (Apache-2.0) .claude-plugin/marketplace.json # this repo is a Claude Code marketplace .cursor-plugin/marketplace.json # and a Cursor / Grok Bot marketplace (packages/grok-bot) @@ -29,20 +29,25 @@ scripts/validate.py # release checks (versions, prod URL, scopes, sec `packages/*/skills/` are generated: edit `shared/skills/` and run `python3 scripts/build.py`. CI fails if they drift. -## OpenAI listing status (0.2.9) +## Install -0.2.9 was approved and published in the ChatGPT plugin directory on 2026-10-27. Earlier submissions (0.1.x and early 0.2.x) were rejected without a specific reason; see CHANGELOG.md for what changed. Lessons that apply to other directories: +### ChatGPT and Codex -- The review scans the live `tools/list` of `https://mcp-api.convai.com/mcp`, not only this package. Every tool the reviewer sees must work for the reviewer's account and sign-in method, and tool descriptions, the listing and the skills must not mention tools the server does not register. The hosted server therefore hides API-key-only tools from OAuth callers and does not expose function execution or external tool-server management. -- The portal identifies ChatGPT to the authorization server with an OAuth client ID metadata document URL; the authorization server must recognize that client and its exact redirect URI before Connect works. -- The reviewer account must already hold every plan entitlement the test cases need; the review cannot include a purchase. -- Listing text says "agents", not "AI characters". Explain in the submission context that Convai is the product brand, how to sign in (email and password on the Convai sign-in page, then Approve), and what the plugin deliberately does not do. Never put reviewer credentials in this repository. +Install [Convai Agent Authoring](https://chatgpt.com/plugins/plugin_asdk_app_6ac4cf6467048191a51888a2600180da) +from your application's plugin directory and sign in with your Convai account +when prompted. Review the requested permissions before approving access. You do +not need an API key, the Convai CLI, or a local MCP server. -After a change, build with `python3 scripts/build.py --zip` and upload the versioned OpenAI ZIP in dist/ as an update to the existing plugin. See the [submission guide](https://developers.openai.com/plugins/deploy/submission) for portal steps. +The published OpenAI package is **0.2.9**, with package ID +`convai-character-authoring`. The **0.3.0** rename to `convai-agent-authoring` in +this repository does not establish that the renamed package is published. -## Install +For a first read, ask: "List my Convai agents." Choose an agent from the returned +list and ask for its saved configuration. Specify the intended Workspace when +applicable. Installing the plugin does not grant additional Workspace, role, or +plan permissions. -**Claude Code** +### Claude Code ```text /plugin marketplace add Conv-AI/ConvAI-Agent-Plugin @@ -50,31 +55,38 @@ After a change, build with `python3 scripts/build.py --zip` and upload the versi /mcp # convai -> Authenticate ``` -**Claude (web, Desktop)**: Settings -> Connectors -> Convai (after directory listing), or add a +### Claude web and Desktop + +Use Settings -> Connectors -> Convai when available in your directory, or add a custom connector with URL `https://mcp-api.convai.com/mcp` and OAuth Client ID `claude`. -**ChatGPT and Codex**: install *Convai Agent Authoring* from the plugin directory and sign in to Convai when prompted. +## Capabilities and limits + +The plugin supports authorized agent inspection, character authoring, narrative +editing, version review and release, existing knowledge-document associations, +and requested conversation-history reads. Confirm changes before writing. + +It does not upload knowledge documents, execute function code, or start live +voice conversations. Hosted OAuth cannot discover the knowledge library; connect +an existing document only when its authorized ID is known. Use the connected +tools' schemas and advertised Workspace support rather than assuming every +operation is available. + +## Privacy and support + +Review Convai's [Privacy Policy](https://convai.com/privacy-policy) and +[Terms of Service](https://convai.com/tos) before connecting your account. +Use the [Convai Developer Forum](https://forum.convai.com) for support. ## Develop ```bash python3 scripts/build.py && python3 scripts/validate.py +python3 -m unittest discover -s tests claude plugin validate packages/claude --strict && claude plugin validate . ``` -## Test the Grok Bot package - -Grok Bot uses Cursor's plugin format and MCP client: the manifest is `.cursor-plugin/plugin.json` and the MCP config is the root `mcp.json`. Convai Auth has no dynamic client registration, so -`packages/grok-bot/mcp.json` sets the static client `grok-bot` (`auth.CLIENT_ID`). Auth must -register it with Cursor's fixed callbacks: `https://www.cursor.com/agents/mcp/oauth/callback` -(cloud agents, Grok Bot) and `http://localhost:8787/callback` (desktop). - -1. Build a staging copy: `python3 scripts/build.py --mcp-url https://mcp-api-stg.convai.com/mcp --out dist/stg` - (writes `dist/stg/grok-bot`). -2. Local sign-in check with the same client, in Cursor desktop: put the `convai` entry from - `dist/stg/grok-bot/mcp.json` in `.cursor/mcp.json`, open MCP settings, Connect, sign in to - Convai (stg) and Allow. Run "List my Convai agents". -3. In Grok Bot: install the plugin from this repository (or the `dist/stg/grok-bot` folder - pushed to a test branch), click Authenticate, sign in and run the same prompt. -4. If Auth answers "redirect_uri is not registered", the rejected URL in the request is the - callback to add to the `grok-bot` client. +Build local archives with `python3 scripts/build.py --zip`. Building an archive +does not publish a directory listing or change a hosted MCP deployment. +See [CHANGELOG.md](CHANGELOG.md) for package changes. Release coordination and +non-production verification are maintained separately from these public instructions. diff --git a/scripts/validate.py b/scripts/validate.py index c9e3588..a1ea741 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -34,8 +34,18 @@ ) NON_PROD_HOST = re.compile( r"(?:-stg|-preview|-dev)\.convai\.com" - r"|localhost(?!:8080\b)" - r"|127\.0\.0\.1" + r"|\blocalhost\b" + r"|127\.0\.0\.1", + re.I, +) +INTERNAL_REFERENCE = re.compile( + r"convaixr\.atlassian\.net" + r"|conv-ai\.slack\.com" + r"|console\.cloud\.google\.com" + r"|(?:[a-z0-9-]+\.)*internal\.convai\.com" + r"|\bprj-cs-[a-z0-9-]+" + r"|\bCORE-\d+\b", + re.I, ) FRONTMATTER = re.compile(r"\A---\n(.*?)\n---\n", re.S) @@ -51,6 +61,19 @@ def check(cond: bool, msg: str) -> None: errors.append(msg) +def public_documentation_errors(text: str) -> list[str]: + """Keep private operational references out of the public entry point.""" + return [ + f"README.md: {message}" + for pattern, message in ( + (SECRET, "looks like it contains a credential"), + (NON_PROD_HOST, "references a non-production host"), + (INTERNAL_REFERENCE, "references internal operational information"), + ) + if pattern.search(text) + ] + + def schema_identifiers(node, found: set[str]) -> None: """Collect property, required and string enum or const names from a JSON schema.""" if isinstance(node, dict): @@ -76,6 +99,7 @@ def main() -> int: help="convai-api-mcp operation-catalog.v1.json", ) args = parser.parse_args() + errors.extend(public_documentation_errors((ROOT / "README.md").read_text())) openai = load("packages/openai/plugin.json") claude = load("packages/claude/.claude-plugin/plugin.json") diff --git a/tests/test_public_documentation.py b/tests/test_public_documentation.py new file mode 100644 index 0000000..bc14003 --- /dev/null +++ b/tests/test_public_documentation.py @@ -0,0 +1,68 @@ +import unittest + +from scripts.validate import public_documentation_errors + + +class PublicDocumentationTests(unittest.TestCase): + def test_public_setup_and_synthetic_examples_are_allowed(self): + for text in ( + "https://mcp-api.convai.com/mcp", + "https://login.convai.com", + "https://github.com/Conv-AI/ConvAI-Agent-Plugin", + "support@convai.com", + "OAuth public client ID chatgpt, no client secret", + "character:read character:write", + "11111111-1111-4111-8111-111111111111", + "cvpat_...", + ): + with self.subTest(text=text): + self.assertEqual(public_documentation_errors(text), []) + + def test_nonproduction_hosts_are_rejected(self): + for host in ( + "https://fixture-stg.convai.com/mcp", + "https://fixture-preview.convai.com", + "https://fixture-dev.convai.com", + "HTTPS://FIXTURE-STG.CONVAI.COM/mcp", + "http://localhost/mcp", + "http://localhost:8080/mcp", + "http://localhost:8081/mcp", + "HTTP://LOCALHOST:8080/mcp", + "http://127.0.0.1:8080/mcp", + ): + with self.subTest(host=host): + self.assertIn( + "README.md: references a non-production host", + public_documentation_errors(host), + ) + + def test_internal_references_are_rejected(self): + for text in ( + "https://convaixr.atlassian.net/wiki/example", + "https://conv-ai.slack.com/archives/example", + "https://console.cloud.google.com/logs/query", + "https://admin.internal.convai.com", + "CORE-1234", + "prj-cs-test-example", + ): + with self.subTest(text=text): + self.assertIn( + "README.md: references internal operational information", + public_documentation_errors(text), + ) + + def test_credential_material_is_rejected_without_echoing_it(self): + for text in ( + "cvpat_" + "0" * 12, + "cvoat_" + "0" * 12, + "-----BEGIN PRIVATE KEY-----", + "AIza" + "0" * 32, + ): + with self.subTest(kind=text[:5]): + result = public_documentation_errors(text) + self.assertIn("README.md: looks like it contains a credential", result) + self.assertTrue(all(text not in message for message in result)) + + +if __name__ == "__main__": + unittest.main()