DOCS
Connect
Start from the app, not the mechanism. Every connection is one app holding one credential for one door, and Korpus picks the way in that suits the app: a sign-in for anything with a browser, a trust for a job on GitHub Actions or AWS, a key for the rest.
Overview
| Access | me is your own access: your full rights, signed in through the browser, never a key and never a trust. Every other access is a delegation (nightly-sync, ci-bot): a name, a job and path rules that say what it may create, read, update and delete. An AI agent, an automation, a script and an SDK program are all delegations. Every write is signed @you via <delegation>. |
|---|---|
| Doors | The API is for programs: the web app, the SDK, the CLI, scripts, CI. MCP is for AI apps: Claude, ChatGPT, Claude Code, Cursor, LiteLLM, AgentCore. A delegation works at either door; one connection is for one. |
| Connection | One app, one credential, one door, for me or for one delegation. The Access page in the web app lists them for each access with the app, door, way in, when made and last used, and Revoke ends one at once. |
| Ways in | Sign in (OAuth, or a device code on a box with no browser), trust (a GitHub Actions or AWS identity traded for an hour-long token) and key (kp_…, a bearer or a client secret). |
| In the app | On Access, Connect opens the same tiles as below, with your username and delegation filled in. |
Addresses
Everything hangs off one host, fra.korpus.cloud here, and nothing else names a region.
| Web app | https://fra.korpus.cloud |
|---|---|
| API | https://api.fra.korpus.cloud/v1/<op>: POST with a JSON body (The API). |
| OAuth | https://api.fra.korpus.cloud/oauth/* and https://api.fra.korpus.cloud/.well-known/oauth-authorization-server: one issuer for both doors. |
| MCP, for me | https://mcp.fra.korpus.cloud/: the me of whoever signs in. Or for one account, https://mcp.fra.korpus.cloud/<username>/me. |
| MCP, for a delegation | https://mcp.fra.korpus.cloud/<username>/<delegation> |
| Trust audience | https://api.fra.korpus.cloud/<username>/<delegation>: what a platform token must name (Trust a workload). |
A username is chosen once and never changes, so none of these addresses ever moves. Every credential is bound to the door it was made for; at the other one it is refused (The wrong door).
Connect an app
Replace <username> with your username and <delegation> with the delegation. As me, the apps that sign in (Claude, ChatGPT, Claude Code, Cursor and any other MCP client) take https://mcp.fra.korpus.cloud/<username>/me in place of the delegation's address; everything else needs a delegation.
AI apps MCP
Claude Sign in
Add Korpus in Claude. Add a custom connector with this name and URL, then press Connect.
Settings → Connectors → Add custom connector
Name
korpus-<username>-<delegation>
Server URL
https://mcp.fra.korpus.cloud/<username>/<delegation>
ChatGPT Sign in
Add Korpus in ChatGPT. With developer mode on (under Advanced settings, on the plans that have it), create a connector with this name and URL. ChatGPT asks you to sign in.
Settings → Apps & Connectors → Create
Name
korpus-<username>-<delegation>
Server URL
https://mcp.fra.korpus.cloud/<username>/<delegation>
Claude Code Sign in
Add Korpus to Claude Code. In your project's folder:
claude mcp add --transport http korpus-<username>-<delegation> https://mcp.fra.korpus.cloud/<username>/<delegation>
Sign in from Claude Code. In Claude Code, type /mcp, choose korpus-<username>-<delegation> and Authenticate: your browser opens Korpus.
Another way: Key
Create a key first (Keys, below). It is shown once; replace $KORPUS_KEY with it.
Add Korpus to Claude Code. With the key as a header, read from KORPUS_KEY:
claude mcp add --transport http korpus-<username>-<delegation> https://mcp.fra.korpus.cloud/<username>/<delegation> --header "Authorization: Bearer $KORPUS_KEY"
.mcp.json
{"mcpServers":{"korpus-<username>-<delegation>":{"type":"http","url":"https://mcp.fra.korpus.cloud/<username>/<delegation>","headers":{"Authorization":"Bearer ${KORPUS_KEY}"}}}}Cursor and VS Code Sign in
Add Korpus to your editor. Your editor opens and asks to install the server, then signs in through your browser. Or add it to .cursor/mcp.json by hand.
.cursor/mcp.json
{"mcpServers":{"korpus-<username>-<delegation>":{"type":"http","url":"https://mcp.fra.korpus.cloud/<username>/<delegation>"}}}LiteLLM Key as client credentials
Create a key first (Keys, below). It is shown once; replace $KORPUS_KEY with it.
Add Korpus in LiteLLM. Add an MCP server at this URL with OAuth client credentials: the client id below, the key as the secret.
Server URL
https://mcp.fra.korpus.cloud/<username>/<delegation>
Token URL
https://api.fra.korpus.cloud/oauth/token
Client id
<username>/<delegation>
curl -s https://api.fra.korpus.cloud/oauth/token -d grant_type=client_credentials -d client_id=<username>/<delegation> -d client_secret="$KORPUS_KEY" --data-urlencode resource=https://mcp.fra.korpus.cloud/<username>/<delegation>
AgentCore Gateway Key as client credentials
Create a key first (Keys, below). It is shown once; replace $KORPUS_KEY with it.
Add Korpus in AgentCore Gateway. Add this URL as an MCP target with OAuth client credentials: the client id below, the key as the secret.
Server URL
https://mcp.fra.korpus.cloud/<username>/<delegation>
Token URL
https://api.fra.korpus.cloud/oauth/token
Client id
<username>/<delegation>
curl -s https://api.fra.korpus.cloud/oauth/token -d grant_type=client_credentials -d client_id=<username>/<delegation> -d client_secret="$KORPUS_KEY" --data-urlencode resource=https://mcp.fra.korpus.cloud/<username>/<delegation>
Another MCP client Key
Create a key first (Keys, below). It is shown once; replace $KORPUS_KEY with it.
Add Korpus to your app. Send the key as a bearer token, read from KORPUS_KEY.
Server URL
https://mcp.fra.korpus.cloud/<username>/<delegation>
Header
Authorization: Bearer $KORPUS_KEY
Programs API
SDK Sign in by device code
Sign in from the terminal. Install the Korpus CLI and sign in. It prints a link and a short code: open the link and allow <delegation>.
npm install -g @korpus-cloud/sdk
korpus login <delegation> --user <username>
Use it in a program. In the program, install the SDK; it finds the sign-in, no key:
npm install @korpus-cloud/sdk
Code
import { Korpus } from '@korpus-cloud/sdk';
const korpus = new Korpus({ delegation: '<delegation>', baseUrl: 'https://api.fra.korpus.cloud' });
const bundles = await korpus.listBundles();
console.log(bundles);Another way: Key
Create a key first (Keys, below). It is shown once; replace $KORPUS_KEY with it.
Use it in a program. Install the SDK. It reads the key from KORPUS_KEY, set where the program runs:
npm install @korpus-cloud/sdk
Code
import { Korpus } from '@korpus-cloud/sdk';
const korpus = new Korpus({ token: process.env.KORPUS_KEY, baseUrl: 'https://api.fra.korpus.cloud' });
const bundles = await korpus.listBundles();
console.log(bundles);CLI Sign in by device code
Sign in from the terminal. Install the Korpus CLI and sign in. It prints a link and a short code: open the link and allow <delegation>.
npm install -g @korpus-cloud/sdk
korpus login <delegation> --user <username>
Another way
Sign in with curl. Without Node, curl and jq do the same:
R=$(curl -s https://api.fra.korpus.cloud/oauth/device_authorization -d client_id=korpus-cli --data-urlencode resource=https://api.fra.korpus.cloud/<username>/<delegation>) echo "Open $(jq -r .verification_uri_complete <<<"$R") and allow <delegation>" while :; do T=$(curl -s https://api.fra.korpus.cloud/oauth/token -d grant_type=urn:ietf:params:oauth:grant-type:device_code -d client_id=korpus-cli -d device_code="$(jq -r .device_code <<<"$R")") case "$T" in *authorization_pending*|*slow_down*) sleep 5 ;; *) echo "$T"; break ;; esac done
GitHub Actions Trust
Trust the repository or the roles first (Trust a workload, below).
Add the step to your workflow. Let the job ask GitHub for a token, then add this step before anything that uses Korpus. Later steps find the hour-long token in KORPUS_ACCESS_TOKEN.
permissions
permissions: id-token: write contents: read
step
- name: Korpus token
run: |
JWT=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https%3A%2F%2Fapi.fra.korpus.cloud%2F<username>%2F<delegation>" | jq -r .value)
KORPUS_ACCESS_TOKEN=$(curl -s https://api.fra.korpus.cloud/oauth/token -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange -d subject_token_type=urn:ietf:params:oauth:token-type:jwt --data-urlencode "subject_token=$JWT" | jq -er .access_token)
echo "::add-mask::$KORPUS_ACCESS_TOKEN"
echo "KORPUS_ACCESS_TOKEN=$KORPUS_ACCESS_TOKEN" >> "$GITHUB_ENV"AWS Trust
Trust the repository or the roles first (Trust a workload, below).
Turn on outbound federation. Once per account. It prints the issuer the trust asks for; the role to trust is arn:aws:iam::<account>:role/korpus-<delegation>.
aws iam enable-outbound-web-identity-federation; aws iam get-outbound-web-identity-federation-info
Write the function. A Lambda function on the SDK: it mints the role's token, trades it and renews it, no key.
npm install @korpus-cloud/sdk @aws-sdk/client-sts
src/handler.ts
import { Korpus } from '@korpus-cloud/sdk';
// KORPUS_AUDIENCE and AWS_REGION are all it needs: the SDK mints the role's
// web identity token, trades it for an hour-long Korpus token and renews it.
const korpus = new Korpus();
export const handler = async () => {
const bundles = await korpus.listBundles();
return bundles.map((bundle) => bundle.name);
};Deploy it. Deploy it with the CDK; it makes the role korpus-<delegation>.
CDK
import { CfnOutput, Duration } from 'aws-cdk-lib';
import { ManagedPolicy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
// In the stack:
const role = new Role(this, 'KorpusRole', {
roleName: 'korpus-<delegation>', // the role Korpus trusts
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
});
role.addToPolicy(
new PolicyStatement({
actions: ['sts:GetWebIdentityToken'],
resources: ['*'],
conditions: {
'ForAllValues:StringEquals': { 'sts:IdentityTokenAudience': 'https://api.fra.korpus.cloud/<username>/<delegation>' },
NumericLessThanEquals: { 'sts:DurationSeconds': 300 },
},
}),
);
new NodejsFunction(this, 'KorpusFunction', {
entry: 'src/handler.ts',
runtime: Runtime.NODEJS_22_X,
role,
timeout: Duration.seconds(30),
environment: { KORPUS_AUDIENCE: 'https://api.fra.korpus.cloud/<username>/<delegation>' },
// The SDK loads @aws-sdk/client-sts when it first mints: ship it beside the bundle.
bundling: { nodeModules: ['@aws-sdk/client-sts'] },
});
new CfnOutput(this, 'KorpusRoleArn', { value: role.roleArn });Another way
Bundle the function. Bundle the handler, with @aws-sdk/client-sts beside it:
npx esbuild src/handler.ts --bundle --platform=node --format=esm --outfile=dist/index.mjs cd dist && npm init -y && npm install @aws-sdk/client-sts && cd ..
Apply it. Then terraform apply:
main.tf
locals {
korpus_audience = "https://api.fra.korpus.cloud/<username>/<delegation>"
}
data "aws_iam_policy_document" "korpus_assume" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
data "aws_iam_policy_document" "korpus" {
statement {
actions = ["sts:GetWebIdentityToken"]
resources = ["*"]
condition {
test = "ForAllValues:StringEquals"
variable = "sts:IdentityTokenAudience"
values = [local.korpus_audience]
}
condition {
test = "NumericLessThanEquals"
variable = "sts:DurationSeconds"
values = ["300"]
}
}
}
resource "aws_iam_role" "korpus" {
name = "korpus-<delegation>" # the role Korpus trusts
assume_role_policy = data.aws_iam_policy_document.korpus_assume.json
}
resource "aws_iam_role_policy" "korpus" {
role = aws_iam_role.korpus.id
policy = data.aws_iam_policy_document.korpus.json
}
resource "aws_iam_role_policy_attachment" "korpus_logs" {
role = aws_iam_role.korpus.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
data "archive_file" "korpus" {
type = "zip"
source_dir = "dist"
output_path = "korpus.zip"
}
resource "aws_lambda_function" "korpus" {
function_name = "korpus-<delegation>"
role = aws_iam_role.korpus.arn
runtime = "nodejs22.x"
handler = "index.handler"
timeout = 30
filename = data.archive_file.korpus.output_path
source_code_hash = data.archive_file.korpus.output_base64sha256
environment {
variables = { KORPUS_AUDIENCE = local.korpus_audience }
}
}
output "korpus_role_arn" {
value = aws_iam_role.korpus.arn
}Another way
Allow the role. Give the role this one permission:
Role policy
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"sts:GetWebIdentityToken","Resource":"*","Condition":{"ForAllValues:StringEquals":{"sts:IdentityTokenAudience":"https://api.fra.korpus.cloud/<username>/<delegation>"},"NumericLessThanEquals":{"sts:DurationSeconds":300}}}]}Mint and trade. In the workload, mint a token, then trade it:
JWT=$(aws sts get-web-identity-token --audience https://api.fra.korpus.cloud/<username>/<delegation> --signing-algorithm ES384 --duration-seconds 300 --query WebIdentityToken --output text)
KORPUS_ACCESS_TOKEN=$(curl -s https://api.fra.korpus.cloud/oauth/token -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange -d subject_token_type=urn:ietf:params:oauth:token-type:jwt --data-urlencode "subject_token=$JWT" | jq -er .access_token)
Another HTTP client Key
Create a key first (Keys, below). It is shown once; replace $KORPUS_KEY with it.
Point it at Korpus. Send the key as a bearer token with every request. To check it, ask what it may call:
API URL
https://api.fra.korpus.cloud/v1
Header
Authorization: Bearer $KORPUS_KEY
curl -s https://api.fra.korpus.cloud/v1 -H "Authorization: Bearer $KORPUS_KEY"
Not here? Anything that speaks MCP over HTTP takes the generic MCP recipe; anything that can send an HTTP header takes the API one. The ops either door serves are in The API.
Sign in
For an app that has a browser, or a person at a terminal. OAuth 2.1 with PKCE: the app is shown the address, you sign in to the web app and press Allow on a page that names the app and, for a delegation, its job and rules. A client with MCP OAuth support needs nothing configured: it discovers everything from the first 401. The result is an access token (1 hour) and a refresh token (30 days, rotated on every use; one presented twice ends the whole connection).
- Discovery. An unauthenticated MCP request answers
401withWWW-Authenticate: Bearer resource_metadata="…"(RFC 9728). The authorization server's metadata is athttps://api.fra.korpus.cloud/.well-known/oauth-authorization-server(RFC 8414). - Registration.
POST /oauth/register(RFC 7591), public clients only:token_endpoint_auth_methodisnone. Redirect URIs arehttps, orhttpon localhost, 127.0.0.1 or [::1]. An unused registration expires after 90 days. Or none: a client whoseclient_idis anhttpsURL is read from that Client ID Metadata Document (no redirects, at most 5 KB, believed only when it names itself and its redirect URIs pass the same rule) and remembered for a day. - Authorize.
GET /oauth/authorizewithresponse_type=code, PKCES256, scopemcpandresource, the address the token is for: it decides the door and the access. The request is good for 10 minutes; the code for 5. - Token.
POST /oauth/tokenwithauthorization_codeorrefresh_token.
Each approval is one connection under its access on the Access page. Revoke ends it at once, and the next call answers 401. One unused for 30 days ends with its refresh token; deleting the delegation ends all of its connections.
Device code
For a box with no browser (a server, a runner, a terminal over SSH): RFC 8628, for a delegation only, never me; the recipes here use it at the API door (resource names the door). The korpus command does all of it (The CLI); the connect recipes above print it, and the same by hand with curl and jq. By hand:
POST /oauth/device_authorizationwithclient_id=korpus-cli(or a registered client) andresource=https://api.fra.korpus.cloud/<username>/<delegation>. It answersdevice_code,user_code(BCDF-GHJK),verification_uriandverification_uri_complete(the web app's/devicepage),expires_in600 andinterval5.- You open that page anywhere, sign in, check the code, see the delegation's rules and press Allow. A code works once.
- The device polls
POST /oauth/tokenwithgrant_type=urn:ietf:params:oauth:grant-type:device_code,device_codeandclient_id:authorization_pendinguntil you decide,slow_downwhen it polls faster than the interval, then the same access and refresh tokens as a browser sign-in (oraccess_denied,expired_token).
Trust a workload
A job on GitHub Actions or AWS signs in as a delegation with nothing secret stored anywhere. You trust the repository or the roles once; the job trades the token its platform already gives it for a Korpus token that lasts an hour.
| Who | A delegation, never me. At the API door only: a trust gives no MCP connection. |
|---|---|
| Trusted | GitHub: one repository, held by its ids, pinned to an environment, a ref or a written pattern. AWS: IAM roles of one account. At most 10 per delegation. |
| Audience | Every platform token must be minted for the delegation's API address, https://api.fra.korpus.cloud/<username>/<delegation>: this deployment's host, your username and the delegation. A token for one deployment trades at no other. The Access page and add_trust print it. |
| Token | kat_…, one hour, no refresh token: trade a fresh platform token for the next. Send it as Authorization: Bearer to the API. KORPUS_ACCESS_TOKEN is a place to keep it for curl and your own scripts; the SDK does not read it, it trades for its own. |
| Order | Prefer it to a key, which works for anyone who has it. |
GitHub Actions
- Access → the delegation → Trust GitHub or AWS → GitHub Actions. Enter the repository (
owner/name) and, best, an environment. For a private repository, add its ids:gh api repos/owner/name --jq ".id, .owner.id". Or ask your AI app asme:add_trust provider=github repository=owner/name environment=production. - In the workflow, give the job
id-token: writeand add the step shown under GitHub Actions above. It masks the token and puts it inKORPUS_ACCESS_TOKENfor the later steps. - Protect the environment: required reviewers or branch rules. It becomes your gate: a job that may not deploy to it cannot get a Korpus token either.
With the SDK the step is not needed: new Korpus() finds the job's token and trades it itself, given the audience.
permissions:
id-token: write
env:
KORPUS_AUDIENCE: https://api.fra.korpus.cloud/<username>/<delegation>
steps:
- run: node sync.js # new Korpus() trades the job's token itselfA GitHub trust is always pinned. An environment is best; a ref (refs/heads/main, refs/tags/v*) pins one branch or tag pattern. To let every workflow of the repository sign in, pull requests from its branches included (GitHub gives no token to pull requests from forks), write it out: sub_pattern=repo:owner/name:*. Anyone who may push a branch could then act as the delegation.
AWS
IAM outbound identity federation lets a role ask AWS STS for a signed token about itself. Nothing is stored in the workload; the role's own credentials ask.
- Once per AWS account, the first command under AWS above; it prints the account issuer,
https://….tokens.sts.global.api.aws. - Access → the delegation → Trust GitHub or AWS → AWS. Paste the issuer and the role ARNs, one per line. One role acts as one of your delegations.
- Give each role the one permission shown under AWS, pinned to the delegation and to five-minute tokens.
In AWS CDK, the same policy on a Lambda function's role, the audience for the SDK, and the ARN to trust as a stack output (for another role, role.addToPolicy with the same statement, and KORPUS_AUDIENCE in its environment):
import { CfnOutput } from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
const audience = 'https://api.fra.korpus.cloud/<username>/<delegation>';
fn.addToRolePolicy(new iam.PolicyStatement({
actions: ['sts:GetWebIdentityToken'],
resources: ['*'],
conditions: {
'ForAllValues:StringEquals': { 'sts:IdentityTokenAudience': audience },
NumericLessThanEquals: { 'sts:DurationSeconds': 300 },
},
}));
fn.addEnvironment('KORPUS_AUDIENCE', audience); // the SDK reads it
new CfnOutput(this, 'KorpusRoleArn', { value: fn.role!.roleArn }); // the ARN to trustAsk regional STS for the token (GetWebIdentityToken is not on the global endpoint). With the SDK and @aws-sdk/client-sts installed, set KORPUS_AUDIENCE and new Korpus() mints and trades by itself, renewing before the hour ends.
The exchange
The same for both: RFC 8693 token exchange at the token endpoint, the platform token in $JWT: POST https://api.fra.korpus.cloud/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token_type=urn:ietf:params:oauth:token-type:jwt and the platform token as subject_token. No client, no secret.
{ "access_token": "kat_…",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600 }Refusals answer 400 with error: invalid_grant and an error_description that names the reason.
What Korpus checks
- The token's
audis one delegation's audience of this deployment, and a trust of that delegation names the token's issuer. Korpus asks only that issuer for its keys, and only after the match. - The signature, against the issuer's published keys;
exp; andiatwithin the last 10 minutes. - AWS: the token's
subis one of the trusted role ARNs, of the trusted account. GitHub:repository_idandrepository_owner_idare the trusted repository's, andsubmatches the environment, ref or pattern. - Each platform token trades once. A copy taken from a log is worth nothing once its job used it.
- The delegation still exists and the account is not blocked. Removing the trust or deleting the delegation stops the tokens it minted at their next request.
List and remove
A trust is one connection of the delegation: the Access page lists it with its setup again and Remove trust. As me over MCP or the API: list_connections and revoke_connection. Korpus records every exchange in its audit trail, with the trust and the workload (role ARN or GitHub subject) that made it.
Troubleshooting
aud must be one delegation's audience | Mint the token for the audience Korpus printed, exactly, and only that one. |
|---|---|
no trust … names issuer | AWS: the issuer pasted is another account's. GitHub: the trust is for another delegation. |
is not a trusted role | The role ARN differs (path included) from the one trusted. An assumed role's token names the role, not the session. |
another repository | The repository was deleted and made again under its name, or the ids given were wrong. Remove the trust and add it again. |
subject … does not match | The job runs outside the trusted environment or ref. |
too old / already exchanged | Mint a new platform token right before the exchange, one per exchange. |
Keys
The last resort, for a host that can only send a fixed header (n8n, the Claude API's mcp_servers, Managed Agents) or that fetches its own tokens with OAuth client credentials (AgentCore Gateway, LiteLLM, Strands). A key works for anyone who has it until it ends: keep it in a secret store, never in a repository.
| For | A delegation, never me. At one door: the key you make for the API is refused at MCP and the other way round. |
|---|---|
| Shape | kp_<deployment>_<43 random characters><6 checksum characters>. The deployment label (fra, dev.fra) tells the SDK and the CLI where it belongs, so a key needs no URL; the checksum lets a secret scanner tell one from noise. Stored only as a hash. |
| Made | On the Access page, Create key on the delegation, or create_key. Shown once. Expires after 30, 90, 365 days, and stops working after 90 days unused. At most 5 per delegation. |
| Bearer | Send it on every request as Authorization: Bearer kp_…. Keep KORPUS_KEY in the environment; a checked-in .mcp.json can read it from there. |
| Client credentials | POST https://api.fra.korpus.cloud/oauth/token with the client_credentials grant, client_id=<username>/<delegation> and the key as client_secret (in the body, or HTTP Basic), and resource naming the door. It answers an hour-long token and no refresh token; the host fetches the next itself. |
| Revoked | Revoke on the Access page, revoke_connection or deleting the delegation ends it at once. The log names the key by its first 8 characters. |
The wrong door
Every credential is bound to one door and one address: an API credential works at https://api.fra.korpus.cloud/v1 only, an MCP credential at its own MCP address only, and a delegation's at its own account's address, never me's or another delegation's. Anywhere else the answer is one error, 403 wrong_audience, and it names where the credential does work.
HTTP/1.1 403
{ "error": { "code": "wrong_audience",
"message": "this credential is for delegation \"nightly-sync\" at https://api.fra.korpus.cloud/v1; use that address",
"url": "https://api.fra.korpus.cloud/v1" } }Over MCP the same refusal is a JSON-RPC error -32003 with the HTTP status. The usual cause is a key made for one door and pasted at the other, or a username mistyped in an address. The fix is the url in the answer, or a new connection at the right door.
Delegations and rights
At most 50 delegations per account, 50 rules each; names are 1–32 characters of a-z 0-9 . _ -. me is built in, fixed, and holds your full rights; nothing creates, changes or deletes it. For a reviewer of the catalog, me also carries the review ops; a delegation never does.
Rules
{ "bundle": "<id> | *", "path": "<pattern>", "rights": ["create", "read", "update", "delete"] }readis always implied.- Patterns:
*alone is the whole bundle;*or**as the last segment is everything below that folder, at any depth (the two mean the same there);**elsewhere is any number of folders;*inside a segment matches within that segment. No.md. - Rules only subtract: your role in the bundle is checked first.
- Rules are read on every request, so a change holds from the next call.
| Pattern | Matches | Does not match |
|---|---|---|
notes/* | notes/a, notes/a/b | notes, other/notes/a |
**/test/* | test/a, x/y/test/a/b | test, x/tests/a |
adr/0* | adr/0007-queues | adr/0007/notes, adr/1001 |
AGENTS | AGENTS | x/AGENTS |
| Ops | Right needed on the path |
|---|---|
read_concept, read_concepts, read_asset | read |
list_folder, read_index, read_log, search_concepts, query, get_schema, get_collection, check_relations | a rule on the bundle; paths it may not read are left out, not refused |
create_concept, create_concepts, create_asset | create |
update_concept, update_concepts, update_asset, add_relation, remove_relation | update (on the from concept) |
delete_concept, delete_concepts, delete_asset | delete |
move_concept, move_concepts | read and delete on from, create on to |
A delegation never gets a managing op: no bundles created, renamed, deleted or exported, no relation types, collections or views, no shares with people, no keys, trusts or delegations. Listings (list_folder, search, index, log, query) leave out the paths it may not read instead of refusing. A refused write answers restricted and names what it may do instead; get_rights asks in advance. Questions: support@korpus.cloud.