Skip to content

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 tokenA machine credential
Audit says a person did itAudit says which agent did it
Carries that person’s full scope and clearanceCarries only what this job needs
Rotating it breaks that personRotates independently
Revoking it locks out a humanRevoking 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.

enrolid + secret, shown onceuseexchange for a short-livedtokenrotatenew credential, overlap,revoke oldrevokeimmediate

Create the principal with the narrowest scope and clearance that lets the job work:

Terminal window
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.

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, ClientCredentials
from 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.

Terminal window
vec agent rotate --client-id "" # issue the new secret
# deploy it; let both run briefly
vec agent revoke --client-id "" --credential "" # retire the old one

Overlap 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.

Terminal window
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.

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.