First governed action in ten minutes
Let an agent update one file on GitHub without holding your GitHub token, and finish with a receipt graded confirmed.
You are going to let an agent update one file on one branch of a GitHub repository. The agent never holds your GitHub token. You finish with a receipt, and its grade is confirmed.
The shape is always the same. You define the action once. Your agent invokes it by name. Anlyon holds the credential, makes the call, and returns the receipt.
The run uses a repository you own and a branch made for this run. A file update on its own branch moves no money and is easy to undo. That makes it a good place to start.
What you need
- An Anlyon account at console.anlyon.com.
- A GitHub account, and the GitHub CLI logged in as you.
- Node.js 20 or later, Python 3.10 or later, or
curl.
Step 1: Create two API keys
Open API keys
Sign in to the console. Go to Settings, then API Keys.
Create an operator key
Pick one environment. Create a key with secrets:write, secrets:read and actions:write. This key stores the credential and defines the action. secrets:read lets the key name a secret. No scope lets a key read a secret’s value. Your agent does not run with this key.
Create an agent key
In the same environment, create a second key with actions:invoke, actions:read and approvals:read. Your agent runs with this one. It can invoke an action and read the receipt. It cannot change an action or read a credential.
A key looks like anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. A key belongs to exactly one environment.
Two keys are the point of the exercise. The key that decides where a credential may be sent should not be the key a model is driving.
export ANLYON_OPERATOR_KEY=anlyon_live_...
export ANLYON_AGENT_KEY=anlyon_live_...
Step 2: Prepare a repository and a branch
The GitHub action updates an existing text file on a branch you name. It refuses the repository’s default branch. So you need a repository you own, a branch made for this run, and one file on that branch.
Set GH_REPO to a repository you own. The commands below create the agent-edits branch from the default branch and add one file to it. They do not change the default branch.
export GH_OWNER=your-github-login
export GH_REPO=your-repository
export GH_BRANCH=agent-edits
export GH_PATH=notes/status.txt
DEFAULT_BRANCH=$(gh api "repos/$GH_OWNER/$GH_REPO" --jq .default_branch)
BASE_SHA=$(gh api "repos/$GH_OWNER/$GH_REPO/git/ref/heads/$DEFAULT_BRANCH" --jq .object.sha)
gh api -X POST "repos/$GH_OWNER/$GH_REPO/git/refs" -f ref="refs/heads/$GH_BRANCH" -f sha="$BASE_SHA" --jq .ref
gh api -X PUT "repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH" \
-f message="Seed status file" -f branch="$GH_BRANCH" \
-f content="$(printf 'status: green\n' | base64)" --jq .content.sha
To start from a new repository instead, run gh repo create "$GH_OWNER/$GH_REPO" --private --add-readme before the other commands.
Now create a fine-grained personal access token in your GitHub settings. Scope it to this one repository. Give it Contents: read and write and Metadata: read. Nothing else.
read -rs QUICKSTART_GITHUB_TOKEN
export QUICKSTART_GITHUB_TOKEN
read -rs keeps the token out of your shell history.
Step 3: Install the SDK
Skip this step if you are using curl.
npm install @anlyonhq/sdkpnpm add @anlyonhq/sdkpip install anlyonStep 4: Store the token and define the action
Run this once, with the operator key. It is the only place your GitHub token appears.
The secret is bound to api.github.com, so Anlyon will not send it to any other host. The action is confined to one repository, one branch and the notes/ directory.
import { Client } from '@anlyonhq/sdk';
const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });
await ops.secrets.put('GITHUB_TOKEN', {
value: process.env.QUICKSTART_GITHUB_TOKEN!,
allowedHosts: ['api.github.com'],
});
await ops.actions.create({
name: 'update-status-file',
description: 'Update one file under notes/ on the agent-edits branch.',
requiresApproval: false,
adapter: {
type: 'github.file_update',
config: {
owner: process.env.GH_OWNER!,
repo: process.env.GH_REPO!,
branch: process.env.GH_BRANCH!,
pathPrefix: 'notes/',
},
credentialSecret: 'GITHUB_TOKEN',
},
});
console.log('Action defined.');import os
from anlyon import Client
ops = Client(api_key=os.environ["ANLYON_OPERATOR_KEY"])
ops.secrets.put(
"GITHUB_TOKEN",
value=os.environ["QUICKSTART_GITHUB_TOKEN"],
allowed_hosts=["api.github.com"],
)
ops.actions.create(
name="update-status-file",
description="Update one file under notes/ on the agent-edits branch.",
requires_approval=False,
adapter={
"type": "github.file_update",
"config": {
"owner": os.environ["GH_OWNER"],
"repo": os.environ["GH_REPO"],
"branch": os.environ["GH_BRANCH"],
"pathPrefix": "notes/",
},
"credentialSecret": "GITHUB_TOKEN",
},
)
ops.close()
print("Action defined.")curl --fail-with-body -X PUT https://api.anlyon.com/api/v2/secrets/GITHUB_TOKEN \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
-H "Content-Type: application/json" \
-d @- <<JSON
{ "value": "$QUICKSTART_GITHUB_TOKEN", "allowedHosts": ["api.github.com"] }
JSON
curl --fail-with-body -X POST https://api.anlyon.com/api/v2/actions \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"name": "update-status-file",
"description": "Update one file under notes/ on the agent-edits branch.",
"requiresApproval": false,
"adapter": {
"type": "github.file_update",
"config": { "owner": "$GH_OWNER", "repo": "$GH_REPO", "branch": "$GH_BRANCH", "pathPrefix": "notes/" },
"credentialSecret": "GITHUB_TOKEN"
}
}
JSONSave the TypeScript as setup.mts and run npx tsx setup.mts. Save the Python as setup.py and run python setup.py.
Step 5: Invoke the action as the agent
This is everything the agent holds: one Anlyon key, an action name, and the input. There is no GitHub token, no repository URL and no header.
import { Client } from '@anlyonhq/sdk';
const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });
const { data: invocation } = await anlyon.actions.invoke('update-status-file', {
path: 'notes/status.txt',
content: 'status: updated by an agent\n',
message: 'Update status',
});
console.log('Status:', invocation!.status);
console.log('Effect:', invocation!.effectId);import os
from anlyon import Client
anlyon = Client(api_key=os.environ["ANLYON_AGENT_KEY"])
invocation = anlyon.actions.invoke(
"update-status-file",
{
"path": "notes/status.txt",
"content": "status: updated by an agent\n",
"message": "Update status",
},
).data
print("Status:", invocation["status"])
print("Effect:", invocation["effectId"])curl --fail-with-body -X POST https://api.anlyon.com/api/v2/actions/update-status-file/invoke \
-H "Authorization: Bearer $ANLYON_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"path": "notes/status.txt",
"content": "status: updated by an agent\n",
"message": "Update status"
}
}'Save the TypeScript as agent.mts, or the Python as agent.py. Do not run it yet. Step 6 adds the receipt read to the same file.
The response names the effect this invocation created and carries its grade.
{
"status": "succeeded",
"effectId": "eff_01JABCDEF",
"grade": "confirmed"
}
Step 6: Read the receipt
An effect is the record of one governed action. Read it by the id from step 5.
const { data: receipt } = await anlyon.actions.effect(invocation!.effectId!);
console.log('Grade:', receipt!.grade);
console.log('Commit:', receipt!.providerReference);receipt = anlyon.actions.effect(invocation["effectId"]).data
print("Grade:", receipt["grade"])
print("Commit:", receipt["providerReference"])
anlyon.close()curl --fail-with-body "https://api.anlyon.com/api/v2/actions/effects/$EFFECT_ID" \
-H "Authorization: Bearer $ANLYON_AGENT_KEY"Add the TypeScript or Python lines to the end of the file from step 5. Then run npx tsx agent.mts or python agent.py. With curl, set EFFECT_ID to the effectId from step 5.
These are the fields that carry the receipt. The commit SHA is your own.
{
"stage": "settled",
"outcome": "succeeded",
"partialEffect": "none",
"grade": "confirmed",
"verification": "provider",
"providerReference": "5f1c9a3e7b2d4086a1c5e9f3b7d2064a8c1e5f39"
}
confirmed means GitHub’s own record shows the write. Here that record is the commit GitHub created, and providerReference is its SHA. Anlyon adds a line reading Anlyon-Effect: and the effect id to the commit message, so you can find the commit from the receipt.
gh api "repos/$GH_OWNER/$GH_REPO/commits?sha=$GH_BRANCH&path=$GH_PATH" --jq '.[0].commit.message'
A receipt is a database record with provider evidence. Read Receipts and grades for what each grade means.
What you just built
- The agent changed a file on GitHub. It never held the GitHub token.
- The token can only be sent to
api.github.com. - The action can only touch files under
notes/on one branch of one repository. - You hold a receipt that names the commit.
This holds for actions routed through Anlyon. A tool your own code calls directly, with a credential your own process holds, is not governed.
To run the agent again, change content. The action refuses a write that would leave the file unchanged.
Version pinning
Pins are opt-in on an agent version: each invocation reads the agent’s currently published version, including when a run is already in progress. Unpinned calls use the live action definition. An explicit pin that cannot be resolved returns 409 ACTION_VERSION_PIN_UNRESOLVED.
Promotion and rollback explains setup and limitations.
Clean up
Delete the action and the secret with the operator key. Then revoke the token on GitHub and delete the agent-edits branch.
Deleting an action archives it, and an archived action keeps its name. Creating update-status-file again in the same environment answers 409 CONFLICT. To run this quickstart again, give the action a new name in step 4 and step 5.
curl -X DELETE https://api.anlyon.com/api/v2/actions/update-status-file \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
curl -X DELETE https://api.anlyon.com/api/v2/secrets/GITHUB_TOKEN \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
gh api -X DELETE "repos/$GH_OWNER/$GH_REPO/git/refs/heads/$GH_BRANCH"
Next steps
Impact limits
Put one shared allowance on what every agent, session and key may change.
Bound approvals
Have a person approve the exact change before it runs.
Receipts and grades
What confirmed, acknowledged, unknown, failed, refused and denied mean.
Govern any HTTP API
Declare what an action changes on an HTTPS API of your own choosing.
Actions
Definitions, input schemas, versions and adapters in full.
Environments and the kill switch
Keep a staging key out of production data, and halt an environment.

