Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
70 changes: 41 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -29,52 +29,64 @@ 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
/plugin install convai-agent-authoring@convai # renamed from convai-character-authoring in 0.3.0
/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.
28 changes: 26 additions & 2 deletions scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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"),
Comment thread
ysinh marked this conversation as resolved.
Resolved
(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):
Expand All @@ -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")
Expand Down
68 changes: 68 additions & 0 deletions tests/test_public_documentation.py
Original file line number Diff line number Diff line change
@@ -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()
Loading