Deploy
kobilctl, using access your KOBIL contact provides. To run an agent on your own Kubernetes,
talk to your KOBIL contact.
Prepare
Install the CLI, connect to the cluster, and let kobilctl tell you the URL your agent will
answer on.
Install
What: the CLI, connected to the cluster.
How: one credential, plus standard machine setup.
The credential — your registry token, from a platform admin.
It is the only one you are given: login exchanges it for cluster access itself, so you never
request or handle a cluster credential.
The machine setup — one-time, and nothing to rotate:
- Node 20 or newer, with
kubectl,helmandgiton your PATH - SSH access to the cluster host: your admin authorises your key, and gives you the alias for
~/.ssh/config.kobilctlopens and repairs the tunnel itself. - Docker is not required — the cluster builds your image
cat >> ~/.npmrc <<'EOF' @development:registry=https://gitlab.kobil.com/api/v4/packages/npm/ //gitlab.kobil.com/api/v4/packages/npm/:_authToken=YOUR_DEPLOY_TOKEN EOF chmod 600 ~/.npmrc npm install -g @development/kobilctl
This is a different registry — and a different token — from the @kobil one
in the Integration Guide. The SDK token does not install kobilctl: GitLab reports the mismatch as
404 Project not found, not as an auth error. Ask your platform admin for the kobilctl install token.
kobilctl login alice
login writes ~/.kobil/kubeconfig.yaml at mode 0600.
Access lasts 90 days and renews itself, so there is nothing to request later. Pass --token or
--kubeconfig if you were handed either; a kubeconfig you supply is never touched.
kobilctl doctor reports no failures.
Describe the app
What: a Dockerfile, a kobil.json, and your URL.
How: run init in the agent's directory.
cd my-agent && kobilctl init # → https://alice-my-agent.shift-go.uk
Run this before you open the Dashboard, so you know the host before anything references it (registration takes no description field — the URL is for your own records). Registration itself does not ask for the URL.
Commit kobil.json. Its app field fixes your namespace and URL, so
every redeploy lands on the same host.
"oidc": false tells deploy the Dashboard owns
the login. init can only add it once agent credentials exist, so set it by hand after Step 3.
Leave it out if the app has its own user login.
On a Next.js project, init also adds output: "standalone" to your
Next config if it is missing, and tells you it did. The generated image ships
.next/standalone, which Next only produces when asked; without it the container starts and exits
immediately.
kobil.json has your app name (plus "oidc": false only if the Dashboard owns the login), and init printed your URL.
Identity
The Dashboard issues the agent's identity. Bring it back to disk.
Register in the Dashboard
What: credentials, an mTLS certificate, and the policy that governs the agent.
Where: the Agent Trust Dashboard, under Agents → Register agent. Sign in with your KOBIL account.
How: fill in the registration form.
- Name — required, and it becomes the Client ID unless you set one. The Client ID is immutable afterwards, and it prefixes the certificate files you download.
- Owner Email — required. This is the agent's default approver: when an approver routing rule matches it wins, otherwise the request falls back to this address, and it fails only when there is neither. Set it even if your code picks approvers itself — it is the fallback that keeps approval working.
- Deployment: External — see the warning below. The field defaults to In-cluster, so you have to change it.
- Scopes and Tool permissions — both come pre-selected for the agent type. The tool list is a fixed catalogue, not free text: you set each one to allow, ciba or deny, and a tool your code calls that is not in the catalogue has no policy and is refused.
Registration never asks for the URL your agent runs on. The fields under Advanced are optional and play no part in deployment — leave them empty.
Choose Deployment: External. In-cluster writes the mTLS identity straight
into the platform's own cluster, where your agent is not running, and hands you nothing to download.
External returns the certificate and key once, which is what Step 3 puts in
certs/.
Then: the Dashboard shows the credentials once. Take all four now:
agent idandclient id— the agent id is a UUID that addresses the agent over the API. The client id is what goes inAITRUST_AGENT_IDandAITRUST_CLIENT_IDin Step 3: audit and authorization are written againstagent:<client-id>, never the UUID.client secret— into.env.localin Step 3<client-id>-tls.crtand<client-id>-tls.key, from the download buttons — intocerts/in Step 3
Neither is shown again. The secret is displayed once and the private key is never stored by the platform. Lose either and the only way forward is to register the agent again for a fresh pair.
Declare what the agent may do, as you register it. Send one entry per tool your
code declares — allow, ciba or deny — and your own scopes.
The platform turns them into the authorization rules it enforces.
curl -X POST "$AITRUST_MANAGEMENT_URL/api/agents/register" \
-H 'Content-Type: application/json' -d '{
"name": "my-agent",
"ownerEmail": "[email protected]",
"deploymentTarget": "external",
"scopes": ["my-agent-read", "my-agent-write"],
"toolPermissions": { "search_listings": "allow", "grant_report_access": "deny" }
}'
The registration form offers only the built-in insurance agent's tools and scopes,
with no field for your own. Until that changes, register through the API above if your agent is anything else.
Scope names are free strings, but the ones you send here and the ones your tools declare in code must match
exactly — nothing reconciles them for you. The response echoes them under unboundScopes,
meaning they exist as authorization rules but are not yet SHIFT IDP client scopes; tool rules still apply.
Afterwards the Dashboard shows the rules under Authorization → Rules, but its editor only
offers tools it already knows, so change your own through the same API:
# allow can_execute · approval needs_approval · deny denied_agent
# POST ADDS a rule. It never replaces one, and deny outranks allow — so to change a
# rule, DELETE the old one first. Adding allow on top of a deny leaves it denied.
curl -X DELETE "$AITRUST_MANAGEMENT_URL/api/authorization/tuples" \
-H 'Content-Type: application/json' \
-d '{"user":"agent:my-agent","relation":"denied_agent","object":"tool:search_listings"}'
curl -X POST "$AITRUST_MANAGEMENT_URL/api/authorization/tuples" \
-H 'Content-Type: application/json' \
-d '{"user":"agent:my-agent","relation":"can_execute","object":"tool:search_listings"}'
Deleting a rule revokes it, since a tool with no rule is denied. The change is live on the next call
— provided the agent does not cache the decision (Integration Step 4). Check the decision, not
the response. POST answers {"ok":true} for a rule it added even when that rule
changes nothing, because the deny you left in place still wins. Confirm with
POST /api/authorization/check before believing a change took effect.
A tool with no rule is denied. Register with an empty
toolPermissions and the agent deploys, reports healthy, and refuses every call — which reads
like a broken integration rather than an empty policy.
The Dashboard is also where you read the audit trail.
The development Dashboard serves a staging certificate, so your browser warns that the connection
is not private the first time you open it. That is expected in this environment. Verify the host reads
….gondor.dev.kobil.com before you continue.
The agent is listed under Agents, you have both PEM downloads on disk, and Authorization → Rules shows a rule for every tool your agent declares.
Credentials on disk
What: the Dashboard's output, where deploy reads it.
How: credentials into .env.local, and the two PEMs into
certs/ — renamed. The Dashboard downloads them prefixed with the client id;
deploy looks for tls.crt and tls.key.
mkdir -p certs mv ~/Downloads/<client-id>-tls.crt certs/tls.crt mv ~/Downloads/<client-id>-tls.key certs/tls.key chmod 600 certs/tls.key
Registered through the API instead? There are no downloads — the same two PEMs come back in the
response as mtlsCertificate and mtlsPrivateKey. Write them out yourself, and
do not save the response to a file: it holds the private key and the client secret, the generated
.dockerignore excludes certs/ but knows nothing about a file you invent, and every
Dockerfile here does COPY . . — so a saved response is baked into an image layer and pushed to
the registry. Pipe it:
mkdir -p certs
# pipe the response straight in — do not save it to a file first
curl -s -X POST "$AITRUST_MANAGEMENT_URL/api/agents/register" \
-H 'Content-Type: application/json' -d @register.json \
| python3 -c 'import json,sys; d=json.load(sys.stdin)
open("certs/tls.crt","w").write(d["mtlsCertificate"])
open("certs/tls.key","w").write(d["mtlsPrivateKey"])
print("client secret:", d["clientSecret"])'
chmod 600 certs/tls.key
my-agent/
├── kobil.json "oidc": false (see Step 1)
├── .env.local the settings below
└── certs/
├── tls.crt
└── tls.key
# Issued by the Dashboard in Step 2 AITRUST_CLIENT_ID=my-agent AITRUST_CLIENT_SECRET=... AITRUST_AGENT_ID=my-agent # the client id, not the UUID # The environment: ask your KOBIL contact for these AITRUST_PLATFORM_URL=https://<platform-host>/dashboard AITRUST_MANAGEMENT_URL=https://<platform-host>/dashboard AITRUST_AUDIT_URL=https://<platform-host>/dashboard AITRUST_PROXY_URL=https://<mtls-host> # NOT the platform host — ask for it AITRUST_IDP_URL=https://<idp-host>/auth AITRUST_REALM=agenttrust
AITRUST_AGENT_ID is easy to miss and the failure is opaque: without it the app
starts, serves traffic, and refuses the first tool call with
MISSING_CONFIG: GuardConfig.agentId is required.
The platform URLs belong to the environment, not to your agent, so registration does not
return them. AITRUST_PROXY_URL is the one people miss, twice over. Without it the first tool call
fails with proxyUrl is required for clientCredentials — and it is a separate host
from the platform URLs above, because it is the only endpoint that terminates mutual TLS. Point it at the platform
host and every tool call fails with Client certificate not provided, because that host never asks for
your certificate. Ask your KOBIL contact for the mTLS host explicitly; it is not derivable from the others.
Set OPENFGA_STORE_ID only if your own code queries OpenFGA directly. The SDK
does not read it — it asks the platform, which resolves the store itself.
certs/ is mounted into the pod at /certs, and
AITRUST_PROXY_CERT_PATH and AITRUST_PROXY_KEY_PATH are set for you. Keep the PEMs as
files: flattening one into .env fails at the TLS handshake.
How env files merge: .env.production.local → .env.local →
.env.production → .env, first seen wins — and the merged result ships to the pod.
AITRUST_PROXY_CERT_PATH/_KEY_PATH are derived by kobilctl as defaults, so any
value in your env files overrides them; a relative local path there breaks the pod (kobilctl warns when it sees one).
deploy refuses to run if git is tracking anything in
certs/. init adds it to .gitignore and .dockerignore, so this
only fires if it was committed before.
The development platform serves a staging certificate. Node refuses it with
UNABLE_TO_GET_ISSUER_CERT, so the agent starts but cannot reach its control plane. Ship the chain
and point NODE_EXTRA_CA_CERTS at it. Do not disable verification.
HOST=<platform-host>
{ openssl s_client -connect $HOST:443 -servername $HOST -showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/' | awk 'BEGIN{n=0} /BEGIN CERT/{n++} n>1'
curl -s https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x1.pem
} > certs/platform-ca.pem
# then in .env.local
NODE_EXTRA_CA_CERTS=/certs/platform-ca.pem
It goes in certs/ because everything there is mounted at /certs
for you — no Dockerfile change. The server sends its leaf and intermediates but not the root, and Node
needs a chain ending in one, hence the second command. /certs is the container path — this same .env.local is what next dev reads on your machine, where it does not exist, so to run locally override all three pod paths (NODE_EXTRA_CA_CERTS, AITRUST_PROXY_CERT_PATH, AITRUST_PROXY_KEY_PATH) to your own certs/ directory.
Both PEMs are in place, and certs/ is not tracked. If the project is a git repo,
git ls-files certs/ returns nothing; if it is not one, there is nothing to check — nothing here
requires git.
Ship
Publish, then confirm the agent works rather than merely runs.
Deploy
What: the agent live at your URL.
How: one command. It sends your source to the cluster, which builds the image and pushes it, then applies the chart and waits for pods to become ready.
Building in the cluster — the default — needs the SDK vendored:
build pods cannot reach gitlab.kobil.com, so npm ci fails on getaddrinfo.
Install once locally, copy into vendor/@kobil/, depend on them by path:
mkdir -p vendor/@kobil cp -R node_modules/@kobil/* vendor/@kobil/ ls vendor/@kobil # must list every package, not just aitrust # in package.json — every @kobil entry, not only the meta-package "@kobil/aitrust": "file:vendor/@kobil/aitrust", "@kobil/kobil-agent-sdk": "file:vendor/@kobil/kobil-agent-sdk", "@kobil/kobil-agent-audit": "file:vendor/@kobil/kobil-agent-audit" rm -f .npmrc rm -rf node_modules package-lock.json && npm install # lockfile must reference no remote @kobil
Do run that ls. Without --legacy-peer-deps npm nests
kobil-agent-sdk inside aitrust/node_modules, the copy misses it, and the app builds
fine then dies at runtime on Cannot find module.
In your source tree the SDK is no longer a dependency to a bundler: it walks the package and fails on the LangChain, OpenAI and Vercel adapters. Mark the ones you do not use as external.
webpack: (config) => {
config.externals = [...(Array.isArray(config.externals) ? config.externals : []),
"@langchain/core", "@langchain/core/tools", "openai", "ai"];
return config;
},
// The SDK reaches the audit package through a dynamic import, which Next's standalone
// tracer cannot see — so it is left out of the image and you lose the audit trail with
// no build error. Name it explicitly.
outputFileTracingIncludes: {
"/**": ["./node_modules/@kobil/**"],
},
ls vendor/@kobil passing is not enough — it checks your source tree, not the image.
Verify after deploying by writing an audit event and reading it back (Integration Step 6), not by reading
health(): that line says "not installed" under Next.js whether or not the package shipped, so it cannot
tell you either way.
kobilctl deploy
Three to five minutes on a first deploy, mostly the build, whose log streams back. If the
cluster is too busy to start it, kobilctl says so in the cluster's own words.
Redeploying is idempotent: same host, no leftovers. To roll back, pass a tag you already
built — kobilctl deploy --tag <tag> --skip-build; every deploy prints its tag.
--local-build uses your own Docker and its layer cache when iterating.
The run ends with pods ready and prints your URL.
Verify
What: evidence the agent works, not just that the command exited zero.
How: check health, then make it do something.
kobilctl status # health, URL, deployed image tag kobilctl logs -f # stream container logs
A green pod is not the finish line. The failures worth catching here only surface on a tool call, so make one.
A tool call completes and appears in the audit trail in the Dashboard.
When something goes wrong
Start here: kobilctl doctor checks every prerequisite in the order
they depend on each other, so fix the first failure and re-run rather than working through the list.
doctor --fix repairs what it safely can.
kobilctl doctor --fix
| What you see | What it means | What to do |
|---|---|---|
| The cluster rejected your credential | Your cluster access is not valid and could not be renewed automatically, usually a missing registry
token in ~/.npmrc. |
Re-run kobilctl login <your-name>. |
| Cannot reach the cluster | The SSH tunnel could not be opened. The network is the problem, not your credential. | Check you are on the internal network and that your SSH alias resolves. |
| waiting for the cluster… | The cluster has no room to start your build yet. The message carries the cluster's own reason. | Retry shortly. If it persists, send that line to your KOBIL contact. |
| The build failed in the cluster | Your image did not build. The reason is quoted from the build log. | Fix the Dockerfile or sources and deploy again. |
| Pod runs, URL does not answer | Routing, not the app. | kobilctl status for the pod and image, then kobilctl logs -f. |
| Build cannot resolve the registry | A getaddrinfo failure on gitlab.kobil.com: build pods cannot reach it — for the
SDK download, or for the FROM base image. |
For the SDK: vendor it into your source, as Step 3 describes. For the base image: common bases are
mirrored on registry.shift-go.uk; a MANIFEST_UNKNOWN means yours is not — ask an
admin to mirror it (crane copy), or build with --local-build meanwhile. |
| Certificate not trusted | UNABLE_TO_GET_ISSUER_CERT from the development platform's staging certificate. The agent
cannot reach its own control plane. |
Ship the chain, point NODE_EXTRA_CA_CERTS at it. Never disable verification. |
| Certificate not provided | The SDK takes tokens only over mutual TLS. Either AITRUST_PROXY_URL points at the platform
host instead of the mTLS host, or TLS is terminating before your certificate arrives. |
Check the host first — it is the common cause and it is yours to fix. If it is already the mTLS host, the rest is platform-side: send the message to your KOBIL contact. |
| Every tool call is refused | The agent has no rules of its own, so the platform denies each tool by default. | Declare your tools in toolPermissions at registration, as Step 2 describes. |
| Login lands on an unreachable host | The app built its redirect from the inbound request URL, which behind the gateway is the container's own listener. | Redirect against your public base URL. Application code, not the platform. |
If your agent redirects to a login page instead of serving, that is its own authentication working, not a deployment failure. Follow the redirect before concluding anything is broken.