Enrol a machine principal
Do not put a person’s bearer token in a pipeline secret. Do this before you put anything in CI — retrofitting identity onto an audit trail that already blames a human is not fun.
| A human token | A machine credential |
|---|---|
| Audit says a person did it | Audit says which agent did it |
| Carries that person’s full scope and clearance | Carries only what this job needs |
| Rotating it breaks that person | Rotates independently |
| Revoking it locks out a human | Revoking stops one agent |
The scope point is the security one. An agent reads attacker-influenced text — an issue comment, a
dependency README — and may be persuaded to try things. The credential bounds the damage, not the
agent’s judgement. A principal that cannot see repo:infra cannot be talked into exfiltrating it.
The lifecycle
Section titled “The lifecycle”1. Enrol
Section titled “1. Enrol”Create the principal with the narrowest scope and clearance that lets the job work:
vec agent enroll \ --name "release-bot" \ --scope "repo:payments" \ --clearance "internal" \ --capabilities "memory.search,preflight.action,evidence.ingest"You get a client id and a client secret. The secret is shown once. Put it straight into your secret manager.
Ask for the least that works. A bot that reads memory and reports outcomes does not need
memory.remember, and a bot scoped to one repository cannot be walked into another.
2. Use it
Section titled “2. Use it”Every SDK exchanges the credential for a short-lived token and refreshes it transparently. You supply the id and secret; you never handle the token.
from vectadyne import AgentConfig, ClientCredentialsfrom vectadyne.auth import Secret
cfg = AgentConfig( mcp_url=os.environ["VECTADYNE_MCP_URL"], token_source=ClientCredentials( client_id=os.environ["VECTADYNE_CLIENT_ID"], client_secret=Secret(os.environ["VECTADYNE_CLIENT_SECRET"]), ),)cfg := vectadyne.Config{ App: "release-bot", ClientID: os.Getenv("VECTADYNE_CLIENT_ID"), ClientSecret: os.Getenv("VECTADYNE_CLIENT_SECRET"),}.FromEnv().WithDefaults()Secret (and its Go equivalent) makes redaction a property of the value: repr, asdict,
vars, %+v and f-strings all yield <redacted>, and reading the real value is an explicit,
greppable call. Hiding a field from one display path is not redaction — that was a real bug once,
where repr(cfg) was clean and asdict(cfg) returned the secret in plaintext.
The TypeScript SDK has no auth module; wire the exchange yourself. See SDK parity.
3. Rotate without downtime
Section titled “3. Rotate without downtime”vec agent rotate --client-id "…" # issue the new secret# deploy it; let both run brieflyvec agent revoke --client-id "…" --credential "…" # retire the old oneOverlap is the point: issue, deploy, then revoke. Revoking first is a self-inflicted outage.
Rotate on a schedule, and immediately if a secret may have been exposed.
4. Revoke
Section titled “4. Revoke”vec agent revoke --client-id "…"Immediate. The next token exchange fails and any live token stops being accepted.
env: VECTADYNE_MCP_URL: ${{ vars.VECTADYNE_MCP_URL }} VECTADYNE_CLIENT_ID: ${{ secrets.VECTADYNE_CLIENT_ID }} VECTADYNE_CLIENT_SECRET: ${{ secrets.VECTADYNE_CLIENT_SECRET }}The URL is not a secret. The client secret is. One principal per pipeline, not one shared across all of them — a shared credential makes the audit trail useless in exactly the situation you need it.
GitHub Actions OIDC is not accepted as a bearer. Exchange the client credential.
Rate limits on the token endpoint
Section titled “Rate limits on the token endpoint”Token acquisition is throttled per IP and answers a 429 with the OAuth error shape, not our
fault envelope. The SDKs normalise both into one typed RateLimitedError. If you build the exchange
yourself, handle both shapes.