Repository navigation
Add external package invocation API #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
2ddc0fb
a29cb24
759dd2a
63f2641
8ce8ec4
f5f5264
0932ac2
d52596b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| # External package invocation API | ||
|
|
||
| Use this API when a trusted external service should invoke a saved package | ||
| export inside Kody without going through the interactive MCP transport. | ||
|
|
||
| The first intended caller is a stable Discord Gateway proxy: | ||
|
|
||
| 1. the external proxy owns the long-lived Discord websocket | ||
| 2. the proxy normalizes gateway events | ||
| 3. the proxy calls Kody's package invocation API | ||
| 4. Kody executes the saved package export with package context, user context, | ||
| package storage, and normal secret/capability rules | ||
|
|
||
| Kody remains the package runtime and storage brain. The external service owns | ||
| the socket lifecycle. | ||
|
|
||
| ## Endpoint | ||
|
|
||
| `POST /api/package-invocations/:packageIdOrKodyId/:exportName` | ||
|
|
||
| Examples: | ||
|
|
||
| - `/api/package-invocations/pkg_123/dispatch-message-created` | ||
| - `/api/package-invocations/discord-gateway/dispatch-message-created` | ||
|
|
||
| The path accepts either: | ||
|
|
||
| - the saved package id | ||
| - the package `kody.id` | ||
|
|
||
| The export name is normalized to package export form, so | ||
| `dispatch-message-created` resolves as `./dispatch-message-created`. | ||
|
|
||
| ## Authentication | ||
|
|
||
| Authentication uses a private bearer token configured with the Worker secret | ||
| `PACKAGE_INVOCATION_TOKENS`. | ||
|
|
||
| The secret value is a JSON object that maps a token id to: | ||
|
|
||
| - `token` — the actual bearer secret | ||
| - `userId` | ||
| - `email` | ||
| - `displayName` | ||
| - package scope (`packageIds` and/or `packageKodyIds`) | ||
| - optional `exportNames` | ||
| - optional `sources` | ||
|
|
||
| Example: | ||
|
|
||
| ```json | ||
| { | ||
| "discord-gateway-prod": { | ||
| "token": "replace-with-a-long-random-secret", | ||
| "userId": "user_123", | ||
| "email": "me@example.com", | ||
| "displayName": "Kent", | ||
| "packageKodyIds": ["discord-gateway"], | ||
| "exportNames": ["./dispatch-message-created"], | ||
| "sources": ["discord-gateway"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The token is not a global backdoor: | ||
|
|
||
| - package lookup is still user-owned | ||
| - package access is scoped by the token config | ||
| - export access can be restricted | ||
| - `source` metadata can be restricted | ||
| - execution still uses normal package runtime machinery | ||
|
|
||
| ## Request body | ||
|
|
||
| ```json | ||
| { | ||
| "params": { | ||
| "messageId": "123", | ||
| "content": "hello" | ||
| }, | ||
| "idempotencyKey": "discord:message-create:123", | ||
| "source": "discord-gateway", | ||
| "topic": "discord.message.created" | ||
| } | ||
| ``` | ||
|
|
||
| Fields: | ||
|
|
||
| - `params` — JSON object passed to the package export as runtime params | ||
| - `idempotencyKey` — required stable key for replay protection | ||
| - `source` — optional source label for auditing and token scoping | ||
| - `topic` — optional event topic label for downstream logic and logs | ||
|
|
||
|
Comment on lines
+80
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The same friction shows up in the curl example at lines 196–205, where 🤖 Prompt for AI Agents |
||
| ## Idempotency | ||
|
|
||
| Kody persists package invocation idempotency in D1. | ||
|
|
||
| The identity key is: | ||
|
|
||
| - user | ||
| - token id | ||
| - package id | ||
| - export name | ||
| - idempotency key | ||
|
|
||
| Behavior: | ||
|
|
||
| - same request + same idempotency key => stored response replayed | ||
| - same idempotency key + different payload => `409 idempotency_mismatch` | ||
| - duplicate while first invocation is still active => | ||
| `409 invocation_in_progress` | ||
|
|
||
| This makes duplicate Discord gateway deliveries safe when the proxy retries. | ||
|
|
||
| ## Response shape | ||
|
|
||
| Success: | ||
|
|
||
| ```json | ||
| { | ||
| "ok": true, | ||
| "package": { | ||
| "id": "pkg_123", | ||
| "kodyId": "discord-gateway" | ||
| }, | ||
| "exportName": "./dispatch-message-created", | ||
| "source": "discord-gateway", | ||
| "topic": "discord.message.created", | ||
| "idempotency": { | ||
| "key": "discord:message-create:123", | ||
| "replayed": false | ||
| }, | ||
| "result": { | ||
| "reply": "handled" | ||
| }, | ||
| "logs": [] | ||
| } | ||
| ``` | ||
|
|
||
| Replay responses return the same stored body with `idempotency.replayed: true`. | ||
|
|
||
| Failures return: | ||
|
|
||
| ```json | ||
| { | ||
| "ok": false, | ||
| "error": { | ||
| "code": "package_not_found", | ||
| "message": "Saved package \"discord-gateway\" was not found for this user." | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Execution failures return sanitized structured errors and logs. The route does | ||
| not expose Worker secrets directly. | ||
|
|
||
| ## Runtime behavior | ||
|
|
||
| The API reuses the existing published package bundle path: | ||
|
|
||
| - resolve the saved package for the configured user | ||
| - resolve the requested package export | ||
| - load the published `module` artifact if present | ||
| - rebuild and persist the module artifact on cache miss | ||
| - execute through `runBundledModuleWithRegistry` | ||
|
|
||
| Execution includes: | ||
|
|
||
| - package context | ||
| - repo context when a published source exists | ||
| - writable package storage bound to `package:<packageId>` | ||
| - user context from the scoped token config | ||
|
|
||
| ## Rate limiting and auditing | ||
|
|
||
| The endpoint is shaped for standard Cloudflare edge rate limiting: | ||
|
|
||
| - path is stable and narrow | ||
| - caller metadata includes `source` and `topic` | ||
| - each request is audit-logged with hashed email/IP metadata | ||
|
|
||
| Prefer Cloudflare WAF/rate limiting rules in front of this path rather than | ||
| adding bespoke in-Worker rate limiting first. | ||
|
|
||
| ## Discord gateway proxy pattern | ||
|
|
||
| Recommended flow: | ||
|
|
||
| 1. keep the Discord Gateway websocket in a stable external process | ||
| 2. receive `MESSAGE_CREATE` | ||
| 3. derive a stable idempotency key from the gateway event | ||
| 4. call the Kody endpoint | ||
| 5. use Kody's structured result to decide what to post back to Discord | ||
|
|
||
| Example request: | ||
|
|
||
| ```bash | ||
| curl --fail --silent \ | ||
| -X POST \ | ||
| -H "Authorization: Bearer $PACKAGE_INVOCATION_TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| "https://kody.example.com/api/package-invocations/discord-gateway/dispatch-message-created" \ | ||
| -d '{ | ||
| "params": { | ||
| "messageId": "123", | ||
| "channelId": "456", | ||
| "content": "hello" | ||
| }, | ||
| "idempotencyKey": "discord:message-create:123", | ||
| "source": "discord-gateway", | ||
| "topic": "discord.message.created" | ||
| }' | ||
| ``` | ||
|
|
||
| ## Related | ||
|
|
||
| - [Packages and manifests](./packages-and-manifests.md) | ||
| - [Environment variables](./environment-variables.md) | ||
| - [Setup manifest](./setup-manifest.md) | ||
Uh oh!
There was an error while loading. Please reload this page.