From 79d377a885078f340b38238689df64bdd92f4df5 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Fri, 9 Oct 2026 18:45:10 +0000
Subject: [PATCH 1/4] Document scoped API keys
---
info/api-keys.mdx | 126 ++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 123 insertions(+), 3 deletions(-)
diff --git a/info/api-keys.mdx b/info/api-keys.mdx
index 8d7ae74b..b1e1bff3 100644
--- a/info/api-keys.mdx
+++ b/info/api-keys.mdx
@@ -88,6 +88,123 @@ func main() {
```
+## Scope a key to specific resources
+
+By default, an API key can do anything in its organization or project. Add a `policy` when you create a key to limit it to specific actions on specific resources. A key with a policy is denied everything the policy doesn't grant.
+
+Use scoped keys when you hand a credential to code you trust less than your backend, such as an AI agent, a customer-facing worker, or a short-lived job. Keys created without `policy` work exactly as before.
+
+### Write a policy
+
+A policy is a list of allow statements. Each statement pairs one `action` with one `resource`:
+
+| Resource | Matches |
+| --- | --- |
+| `organizations/{org_id}/*` | Everything in the organization. |
+| `projects/{project_id}/*` | Everything in the project. |
+| `projects/{project_id}/{kind}/{id}` | One browser, profile, vault, or proxy, for example `projects/proj_staging_9f3k/profiles/prof_7h2kq9x4m1`. |
+| `{kind}/{id}` | One object in the key's project, or anywhere in the organization for an org-scoped key. |
+
+`kind` is `browsers`, `profiles`, `vaults`, or `proxies`. Kernel checks every exact ID when you create the key. IDs that don't exist return `404`, and resources outside the key's organization or project return `400`.
+
+| Action | Allows |
+| --- | --- |
+| `browsers:create` | Create browsers. Grant it on a project or organization, not an ID. |
+| `browsers:use` | Inspect, connect to, and control browsers. |
+| `browsers:write` | Update browser configuration. |
+| `browsers:delete` | Delete browsers. |
+| `profiles:create` / `profiles:read` | Create profiles / view profile metadata. |
+| `profiles:use` | Attach profiles to browsers and export them. |
+| `profiles:write` / `profiles:delete` | Rename profiles and save browser state into them / delete profiles. |
+| `vaults:create` / `vaults:read` | Create vaults / view vault and item metadata. |
+| `vaults:use` | Fill credentials and run vault operations. |
+| `vaults:write` / `vaults:delete` | Create, update, and delete vault items / delete vaults. |
+| `proxies:create` / `proxies:read` | Create proxies / view proxy configuration. |
+| `proxies:use` | Select saved proxies for browsers and run health checks. |
+| `proxies:write` / `proxies:delete` | Rename proxies / delete proxies. |
+| `projects:read` / `organizations:read` | View project or organization metadata and limits. |
+
+Resources a browser references need their own grants:
+
+- **Creating a browser** needs `browsers:create` and `browsers:use` on its project or organization, because the response includes connection URLs.
+- **Profiles:** attaching a profile needs `profiles:use` on it. Saving changes back to it also needs `profiles:write`.
+- **Vaults:** linking a vault needs `vaults:use` on every vault.
+- **Proxies:** selecting a saved proxy needs `proxies:use` on it.
+- **Existing browsers:** a key can only use a browser if it can also use that browser's attached profile and vaults.
+
+List endpoints return only the resources the key can access, so a key without a matching grant gets an empty list rather than an error. A profile appears in `GET /profiles` only with `profiles:read`, so grant it alongside `profiles:use` if your code looks profiles up by listing them.
+
+### Create a scoped key
+
+This key can create and control browsers in one project, only with profile `prof_7h2kq9x4m1` or no profile, and it expires in one hour:
+
+
+```typescript TypeScript
+import Kernel from '@onkernel/sdk';
+
+const kernel = new Kernel({
+ apiKey: process.env.KERNEL_API_KEY,
+});
+
+const project = 'projects/proj_staging_9f3k';
+
+const apiKey = await kernel.apiKeys.create({
+ name: 'checkout-agent-run-4812',
+ project_id: 'proj_staging_9f3k',
+ expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
+ policy: {
+ statements: [
+ { action: 'browsers:create', resource: `${project}/*` },
+ { action: 'browsers:use', resource: `${project}/*` },
+ { action: 'profiles:use', resource: `${project}/profiles/prof_7h2kq9x4m1` },
+ ],
+ },
+});
+
+console.log(apiKey.key); // Save this value now. Kernel won't show it again.
+console.log(apiKey.policy);
+```
+
+```python Python
+import os
+from datetime import datetime, timedelta, timezone
+
+from kernel import Kernel
+
+client = Kernel(api_key=os.environ["KERNEL_API_KEY"])
+
+project = "projects/proj_staging_9f3k"
+
+api_key = client.api_keys.create(
+ name="checkout-agent-run-4812",
+ project_id="proj_staging_9f3k",
+ expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
+ policy={
+ "statements": [
+ {"action": "browsers:create", "resource": f"{project}/*"},
+ {"action": "browsers:use", "resource": f"{project}/*"},
+ {"action": "profiles:use", "resource": f"{project}/profiles/prof_7h2kq9x4m1"},
+ ],
+ },
+)
+
+print(api_key.key) # Save this value now. Kernel won't show it again.
+print(api_key.policy)
+```
+
+
+To limit a key to vault `vlt_3m8q2r7k1x` instead, grant `vaults:use` on `projects/proj_staging_9f3k/vaults/vlt_3m8q2r7k1x`. Add `vaults:read` if the key also needs to read the vault's metadata.
+
+### Expiry and revocation
+
+- `expires_at` sets an exact expiry time, so a key can live for minutes rather than whole days. It can't be combined with `days_to_expire`.
+- A scoped key can't outlive the credential that creates it. If your key expires, set `expires_at` or `days_to_expire` no later than that.
+- An expired key is rejected immediately. A deleted key is rejected within about 10 seconds.
+- Neither closes browser connections that are already open. Delete those browsers to cut off access immediately.
+- Rotating a scoped key gives the replacement the same policy.
+
+Scoped keys can't create, list, or delete API keys, and they get `403` with `insufficient_scope` on any endpoint that doesn't support scoped access yet. Manage keys with an unscoped key.
+
## Deployment API keys
When you deploy an app, Kernel mints a **deployment-scoped API key** for that deployment and injects it into the deployment (and every invocation it runs) as the `KERNEL_API_KEY` environment variable. Because the SDKs read `KERNEL_API_KEY` from the environment by default, your app can call the Kernel API as itself without you managing a key.
@@ -102,7 +219,7 @@ If you need a credential whose lifetime you control (for CI, a persistent backen
## List and inspect API keys
-List keys to audit what exists. List and retrieve responses include `masked_key`, `project_id`, `project_name`, `created_by`, and expiry metadata, but they don't include the plaintext key.
+List keys to audit what exists. List and retrieve responses include `masked_key`, `project_id`, `project_name`, `created_by`, expiry metadata, and `policy` for scoped keys, but they don't include the plaintext key.
```typescript TypeScript
@@ -228,7 +345,7 @@ func main() {
## Rotate a key
-`rotate` issues a replacement key in a single call and keeps the old key working for a short grace period, so your workload can switch over without downtime. The new key copies the rotated key's name and project scope, and—like create—Kernel returns the plaintext `key` only once.
+`rotate` issues a replacement key in a single call and keeps the old key working for a short grace period, so your workload can switch over without downtime. The new key copies the rotated key's name, project scope, and policy, and—like create—Kernel returns the plaintext `key` only once.
Two optional parameters control the timing:
@@ -308,5 +425,8 @@ To cut over immediately instead of using a grace window, pass `expire_in_days: 0
| --- | --- | --- |
| `400 Bad Request` | The name is missing, `days_to_expire` is outside `1`-`3650`, `expire_in_days` is outside `0`-`3650`, or `project_id` is empty. | Send a name, choose a valid expiry, or omit `project_id` for an org-scoped key. |
| `400 Bad Request` (rotate) | `days_to_expire` is shorter than `expire_in_days`, so the new key would expire before the old key's grace window ends. | Raise `days_to_expire` or lower `expire_in_days`. |
+| `400 Bad Request` (policy) | A statement uses an unknown action, a malformed resource, or a resource outside the key's organization or project. | Check the action and resource against [Write a policy](#write-a-policy). A project-scoped key can't be granted `organizations/{org_id}/*`. |
+| `400 Bad Request` (expiry) | `expires_at` is in the past or combined with `days_to_expire`, or a scoped key would outlive the credential creating it. | Send one expiry field, no later than your own key's expiry. |
| `401 Unauthorized` | Kernel couldn't authenticate the request. | Set a valid `KERNEL_API_KEY`. |
-| `404 Not Found` | The project or API key doesn't exist, or the caller can't access it. | Check the ID. If you're using a project-scoped key, you can only rotate keys in that same project. |
+| `403 Forbidden` (`insufficient_scope`) | A scoped key is missing a permission or called an endpoint scoped keys can't use yet. | The message names the missing action and resource. Add it to the policy, or use an unscoped key. |
+| `404 Not Found` | The project, API key, or a resource named in the policy doesn't exist, or the caller can't access it. | Check the ID. If you're using a project-scoped key, you can only rotate keys in that same project. |
From aa0abe6b2dba6dc606c173e6b5c8ddb0f152ab4b Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Fri, 9 Oct 2026 19:49:33 +0000
Subject: [PATCH 2/4] Define scoped key actions once and tighten the section
---
info/api-keys.mdx | 88 ++++++++++++++++-------------------------------
1 file changed, 29 insertions(+), 59 deletions(-)
diff --git a/info/api-keys.mdx b/info/api-keys.mdx
index b1e1bff3..3f0eba08 100644
--- a/info/api-keys.mdx
+++ b/info/api-keys.mdx
@@ -90,53 +90,34 @@ func main() {
## Scope a key to specific resources
-By default, an API key can do anything in its organization or project. Add a `policy` when you create a key to limit it to specific actions on specific resources. A key with a policy is denied everything the policy doesn't grant.
+By default, an API key can do anything in its organization or project. Add a `policy` when you create a key to limit it to specific actions on specific resources. The key is denied everything the policy doesn't grant. Keys created without `policy` work exactly as before.
-Use scoped keys when you hand a credential to code you trust less than your backend, such as an AI agent, a customer-facing worker, or a short-lived job. Keys created without `policy` work exactly as before.
+Use scoped keys when you hand a credential to code you trust less than your backend, such as an AI agent or a short-lived job.
### Write a policy
-A policy is a list of allow statements. Each statement pairs one `action` with one `resource`:
+A policy is a list of statements, each pairing an `action` with a `resource`.
-| Resource | Matches |
-| --- | --- |
-| `organizations/{org_id}/*` | Everything in the organization. |
-| `projects/{project_id}/*` | Everything in the project. |
-| `projects/{project_id}/{kind}/{id}` | One browser, profile, vault, or proxy, for example `projects/proj_staging_9f3k/profiles/prof_7h2kq9x4m1`. |
-| `{kind}/{id}` | One object in the key's project, or anywhere in the organization for an org-scoped key. |
+A resource is one of:
+
+- `projects/{project_id}/*` or `organizations/{org_id}/*` for everything in a project or organization.
+- `projects/{project_id}/{kind}/{id}` for one object, where `kind` is `browsers`, `profiles`, `vaults`, or `proxies`.
-`kind` is `browsers`, `profiles`, `vaults`, or `proxies`. Kernel checks every exact ID when you create the key. IDs that don't exist return `404`, and resources outside the key's organization or project return `400`.
+An action is `{kind}:{verb}`, for example `browsers:use`:
-| Action | Allows |
+| Verb | Allows |
| --- | --- |
-| `browsers:create` | Create browsers. Grant it on a project or organization, not an ID. |
-| `browsers:use` | Inspect, connect to, and control browsers. |
-| `browsers:write` | Update browser configuration. |
-| `browsers:delete` | Delete browsers. |
-| `profiles:create` / `profiles:read` | Create profiles / view profile metadata. |
-| `profiles:use` | Attach profiles to browsers and export them. |
-| `profiles:write` / `profiles:delete` | Rename profiles and save browser state into them / delete profiles. |
-| `vaults:create` / `vaults:read` | Create vaults / view vault and item metadata. |
-| `vaults:use` | Fill credentials and run vault operations. |
-| `vaults:write` / `vaults:delete` | Create, update, and delete vault items / delete vaults. |
-| `proxies:create` / `proxies:read` | Create proxies / view proxy configuration. |
-| `proxies:use` | Select saved proxies for browsers and run health checks. |
-| `proxies:write` / `proxies:delete` | Rename proxies / delete proxies. |
-| `projects:read` / `organizations:read` | View project or organization metadata and limits. |
-
-Resources a browser references need their own grants:
-
-- **Creating a browser** needs `browsers:create` and `browsers:use` on its project or organization, because the response includes connection URLs.
-- **Profiles:** attaching a profile needs `profiles:use` on it. Saving changes back to it also needs `profiles:write`.
-- **Vaults:** linking a vault needs `vaults:use` on every vault.
-- **Proxies:** selecting a saved proxy needs `proxies:use` on it.
-- **Existing browsers:** a key can only use a browser if it can also use that browser's attached profile and vaults.
-
-List endpoints return only the resources the key can access, so a key without a matching grant gets an empty list rather than an error. A profile appears in `GET /profiles` only with `profiles:read`, so grant it alongside `profiles:use` if your code looks profiles up by listing them.
+| `create` | Create objects of that kind. Grant it on a project or organization, not an ID. Creating a browser also needs `browsers:use`, because the response includes connection URLs. |
+| `read` | View metadata. Lists only include objects the key can read, or for browsers, use. |
+| `use` | Control a browser, attach a profile, fill from a vault, or select a saved proxy. Using a browser also needs `use` on its attached profile and vaults. |
+| `write` | Update an object. For profiles, this includes saving browser state into them. For vaults, it covers creating, updating, and deleting items. |
+| `delete` | Delete an object. |
+
+Browsers support every verb except `read`. `projects:read` and `organizations:read` view project and organization metadata.
### Create a scoped key
-This key can create and control browsers in one project, only with profile `prof_7h2kq9x4m1` or no profile, and it expires in one hour:
+This key can create and control browsers in one project, only with profile `prof_7h2kq9x4m1` or no profile, and expires in one hour:
```typescript TypeScript
@@ -146,23 +127,20 @@ const kernel = new Kernel({
apiKey: process.env.KERNEL_API_KEY,
});
-const project = 'projects/proj_staging_9f3k';
-
const apiKey = await kernel.apiKeys.create({
name: 'checkout-agent-run-4812',
project_id: 'proj_staging_9f3k',
expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
policy: {
statements: [
- { action: 'browsers:create', resource: `${project}/*` },
- { action: 'browsers:use', resource: `${project}/*` },
- { action: 'profiles:use', resource: `${project}/profiles/prof_7h2kq9x4m1` },
+ { action: 'browsers:create', resource: 'projects/proj_staging_9f3k/*' },
+ { action: 'browsers:use', resource: 'projects/proj_staging_9f3k/*' },
+ { action: 'profiles:use', resource: 'projects/proj_staging_9f3k/profiles/prof_7h2kq9x4m1' },
],
},
});
console.log(apiKey.key); // Save this value now. Kernel won't show it again.
-console.log(apiKey.policy);
```
```python Python
@@ -173,37 +151,30 @@ from kernel import Kernel
client = Kernel(api_key=os.environ["KERNEL_API_KEY"])
-project = "projects/proj_staging_9f3k"
-
api_key = client.api_keys.create(
name="checkout-agent-run-4812",
project_id="proj_staging_9f3k",
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
policy={
"statements": [
- {"action": "browsers:create", "resource": f"{project}/*"},
- {"action": "browsers:use", "resource": f"{project}/*"},
- {"action": "profiles:use", "resource": f"{project}/profiles/prof_7h2kq9x4m1"},
+ {"action": "browsers:create", "resource": "projects/proj_staging_9f3k/*"},
+ {"action": "browsers:use", "resource": "projects/proj_staging_9f3k/*"},
+ {"action": "profiles:use", "resource": "projects/proj_staging_9f3k/profiles/prof_7h2kq9x4m1"},
],
},
)
print(api_key.key) # Save this value now. Kernel won't show it again.
-print(api_key.policy)
```
-To limit a key to vault `vlt_3m8q2r7k1x` instead, grant `vaults:use` on `projects/proj_staging_9f3k/vaults/vlt_3m8q2r7k1x`. Add `vaults:read` if the key also needs to read the vault's metadata.
-
### Expiry and revocation
-- `expires_at` sets an exact expiry time, so a key can live for minutes rather than whole days. It can't be combined with `days_to_expire`.
-- A scoped key can't outlive the credential that creates it. If your key expires, set `expires_at` or `days_to_expire` no later than that.
-- An expired key is rejected immediately. A deleted key is rejected within about 10 seconds.
-- Neither closes browser connections that are already open. Delete those browsers to cut off access immediately.
-- Rotating a scoped key gives the replacement the same policy.
+- Use `expires_at` for an exact expiry, such as minutes from now, instead of `days_to_expire`. A scoped key can't outlive the key that creates it.
+- An expired key is rejected immediately, and a deleted key within about 10 seconds. Neither closes browser connections that are already open, so delete those browsers to cut off access.
+- Rotating a scoped key keeps its policy.
-Scoped keys can't create, list, or delete API keys, and they get `403` with `insufficient_scope` on any endpoint that doesn't support scoped access yet. Manage keys with an unscoped key.
+Scoped keys can't manage API keys, and they get `403` on endpoints that don't support scoped access yet.
## Deployment API keys
@@ -425,8 +396,7 @@ To cut over immediately instead of using a grace window, pass `expire_in_days: 0
| --- | --- | --- |
| `400 Bad Request` | The name is missing, `days_to_expire` is outside `1`-`3650`, `expire_in_days` is outside `0`-`3650`, or `project_id` is empty. | Send a name, choose a valid expiry, or omit `project_id` for an org-scoped key. |
| `400 Bad Request` (rotate) | `days_to_expire` is shorter than `expire_in_days`, so the new key would expire before the old key's grace window ends. | Raise `days_to_expire` or lower `expire_in_days`. |
-| `400 Bad Request` (policy) | A statement uses an unknown action, a malformed resource, or a resource outside the key's organization or project. | Check the action and resource against [Write a policy](#write-a-policy). A project-scoped key can't be granted `organizations/{org_id}/*`. |
-| `400 Bad Request` (expiry) | `expires_at` is in the past or combined with `days_to_expire`, or a scoped key would outlive the credential creating it. | Send one expiry field, no later than your own key's expiry. |
+| `400 Bad Request` (scoped key) | The policy has an unknown action or a resource outside the key's organization or project, or the expiry is invalid or later than your own key's. | Check the policy against [Write a policy](#write-a-policy) and send one expiry field. |
| `401 Unauthorized` | Kernel couldn't authenticate the request. | Set a valid `KERNEL_API_KEY`. |
-| `403 Forbidden` (`insufficient_scope`) | A scoped key is missing a permission or called an endpoint scoped keys can't use yet. | The message names the missing action and resource. Add it to the policy, or use an unscoped key. |
+| `403 Forbidden` (`insufficient_scope`) | A scoped key is missing a permission. | The message names the missing action and resource. Add it to the policy. |
| `404 Not Found` | The project, API key, or a resource named in the policy doesn't exist, or the caller can't access it. | Check the ID. If you're using a project-scoped key, you can only rotate keys in that same project. |
From b45520225669216cb3a1fa2e9e4450b5aa09ac39 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Fri, 9 Oct 2026 19:56:01 +0000
Subject: [PATCH 3/4] Move scoped key permissions to a reference page
---
docs.json | 1 +
info/api-key-permissions.mdx | 85 ++++++++++++++++++++++++++++++++++++
info/api-keys.mdx | 26 +++--------
3 files changed, 93 insertions(+), 19 deletions(-)
create mode 100644 info/api-key-permissions.mdx
diff --git a/docs.json b/docs.json
index 567cde9e..3e33ce15 100644
--- a/docs.json
+++ b/docs.json
@@ -266,6 +266,7 @@
"introduction/manage",
"info/projects",
"info/api-keys",
+ "info/api-key-permissions",
"info/audit-logs",
"info/network-access"
]
diff --git a/info/api-key-permissions.mdx b/info/api-key-permissions.mdx
new file mode 100644
index 00000000..4333580f
--- /dev/null
+++ b/info/api-key-permissions.mdx
@@ -0,0 +1,85 @@
+---
+title: "API Key Permissions"
+description: "Actions and resources you can grant in a scoped API key policy"
+---
+
+This page lists everything you can grant in a scoped API key's `policy`. To create a scoped key, see [Scope a key to specific resources](/info/api-keys#scope-a-key-to-specific-resources).
+
+## Resources
+
+| Resource | Matches |
+| --- | --- |
+| `organizations/{org_id}/*` | Everything in the organization. |
+| `projects/{project_id}/*` | Everything in the project. |
+| `projects/{project_id}/{kind}/{id}` | One object. `kind` is `browsers`, `profiles`, `vaults`, or `proxies`. |
+
+Resources must be inside the new key's organization, or its project for a project-scoped key. Kernel checks that every object ID exists when you create the key.
+
+## Actions
+
+### Browsers
+
+| Action | Allows |
+| --- | --- |
+| `browsers:create` | Create browsers. Grant it on a project or organization. |
+| `browsers:use` | List, inspect, connect to, and control browsers. |
+| `browsers:write` | Update browser configuration. |
+| `browsers:delete` | Delete browsers. |
+
+### Profiles
+
+| Action | Allows |
+| --- | --- |
+| `profiles:create` | Create profiles. Grant it on a project or organization. |
+| `profiles:read` | List profiles and view their metadata. |
+| `profiles:use` | Attach profiles to browsers and download them. |
+| `profiles:write` | Rename profiles and save browser state into them. |
+| `profiles:delete` | Delete profiles. |
+
+### Vaults
+
+| Action | Allows |
+| --- | --- |
+| `vaults:create` | Create vaults. Grant it on a project or organization. |
+| `vaults:read` | List vaults and view vault and item metadata. |
+| `vaults:use` | Link vaults to browsers, fill credentials, and run vault item operations. |
+| `vaults:write` | Create, update, and delete vault items. |
+| `vaults:delete` | Delete vaults and their items. |
+
+### Proxies
+
+| Action | Allows |
+| --- | --- |
+| `proxies:create` | Create proxies. Grant it on a project or organization. |
+| `proxies:read` | List proxies and view their configuration, without credentials. |
+| `proxies:use` | Select saved proxies for browsers and run health checks. |
+| `proxies:write` | Rename proxies. |
+| `proxies:delete` | Delete proxies. |
+
+### Projects and organizations
+
+| Action | Allows |
+| --- | --- |
+| `projects:read` | View project metadata and limits. |
+| `organizations:read` | View organization entitlements and limits. |
+
+## Operations that need more than one action
+
+| Operation | Requires |
+| --- | --- |
+| Create a browser | `browsers:create` and `browsers:use` on the project or organization, because the response includes connection URLs. Add `profiles:use` on its profile, `profiles:write` if it saves profile changes, `vaults:use` on each linked vault, and `proxies:use` on a saved proxy. |
+| Any operation on an existing browser | The browser action, plus `profiles:use` on its attached profile and `vaults:use` on every linked vault. If the browser saves profile changes, `profiles:write` on that profile too. |
+| Update a browser | `browsers:write` and `browsers:use`, plus `use` on any profile, vault, or proxy the update adds. |
+| Fill from a vault | `vaults:use` on the vault and `browsers:use` on the browser. |
+| Run a proxy health check | `proxies:use` and `proxies:read`. |
+
+List endpoints return only the objects the key can access, so a key without a matching grant gets an empty list.
+
+## Not available to scoped keys
+
+Scoped keys get `403` with `insufficient_scope` on:
+
+- API key management, including creating, listing, rotating, and deleting keys.
+- Browser pools.
+- Creating browsers that use saved extensions, telemetry export, or app invocations.
+- Any other endpoint not covered by the actions above.
diff --git a/info/api-keys.mdx b/info/api-keys.mdx
index 3f0eba08..92f2daf2 100644
--- a/info/api-keys.mdx
+++ b/info/api-keys.mdx
@@ -90,30 +90,18 @@ func main() {
## Scope a key to specific resources
-By default, an API key can do anything in its organization or project. Add a `policy` when you create a key to limit it to specific actions on specific resources. The key is denied everything the policy doesn't grant. Keys created without `policy` work exactly as before.
+By default, an API key can do anything in its organization or project. Add a `policy` when you create a key to limit what it can do. Keys created without `policy` work exactly as before.
Use scoped keys when you hand a credential to code you trust less than your backend, such as an AI agent or a short-lived job.
-### Write a policy
+### How policies work
-A policy is a list of statements, each pairing an `action` with a `resource`.
+A policy is a list of **statements**. Each statement allows one **action** on one **resource**, and the key is denied everything no statement allows.
-A resource is one of:
+- A **resource** is either one object, `projects/{project_id}/{kind}/{id}`, or everything in a container, `projects/{project_id}/*` or `organizations/{org_id}/*`.
+- An **action** is `{kind}:{verb}`, such as `browsers:use` or `vaults:read`.
-- `projects/{project_id}/*` or `organizations/{org_id}/*` for everything in a project or organization.
-- `projects/{project_id}/{kind}/{id}` for one object, where `kind` is `browsers`, `profiles`, `vaults`, or `proxies`.
-
-An action is `{kind}:{verb}`, for example `browsers:use`:
-
-| Verb | Allows |
-| --- | --- |
-| `create` | Create objects of that kind. Grant it on a project or organization, not an ID. Creating a browser also needs `browsers:use`, because the response includes connection URLs. |
-| `read` | View metadata. Lists only include objects the key can read, or for browsers, use. |
-| `use` | Control a browser, attach a profile, fill from a vault, or select a saved proxy. Using a browser also needs `use` on its attached profile and vaults. |
-| `write` | Update an object. For profiles, this includes saving browser state into them. For vaults, it covers creating, updating, and deleting items. |
-| `delete` | Delete an object. |
-
-Browsers support every verb except `read`. `projects:read` and `organizations:read` view project and organization metadata.
+Some operations need more than one action. For example, creating a browser with a profile needs `browsers:create` and `browsers:use` on the project, plus `profiles:use` on that profile. See [API key permissions](/info/api-key-permissions) for every action and what each operation requires.
### Create a scoped key
@@ -396,7 +384,7 @@ To cut over immediately instead of using a grace window, pass `expire_in_days: 0
| --- | --- | --- |
| `400 Bad Request` | The name is missing, `days_to_expire` is outside `1`-`3650`, `expire_in_days` is outside `0`-`3650`, or `project_id` is empty. | Send a name, choose a valid expiry, or omit `project_id` for an org-scoped key. |
| `400 Bad Request` (rotate) | `days_to_expire` is shorter than `expire_in_days`, so the new key would expire before the old key's grace window ends. | Raise `days_to_expire` or lower `expire_in_days`. |
-| `400 Bad Request` (scoped key) | The policy has an unknown action or a resource outside the key's organization or project, or the expiry is invalid or later than your own key's. | Check the policy against [Write a policy](#write-a-policy) and send one expiry field. |
+| `400 Bad Request` (scoped key) | The policy has an unknown action or a resource outside the key's organization or project, or the expiry is invalid or later than your own key's. | Check the policy against [API key permissions](/info/api-key-permissions) and send one expiry field. |
| `401 Unauthorized` | Kernel couldn't authenticate the request. | Set a valid `KERNEL_API_KEY`. |
| `403 Forbidden` (`insufficient_scope`) | A scoped key is missing a permission. | The message names the missing action and resource. Add it to the policy. |
| `404 Not Found` | The project, API key, or a resource named in the policy doesn't exist, or the caller can't access it. | Check the ID. If you're using a project-scoped key, you can only rotate keys in that same project. |
From 9b2cf5f8e4da49e68df5d33cdd4baa82261895e8 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Wed, 14 Oct 2026 18:09:00 +0000
Subject: [PATCH 4/4] Drop unsupported project and org actions; clarify
connection URLs after revocation
---
info/api-key-permissions.mdx | 8 +-------
info/api-keys.mdx | 2 +-
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/info/api-key-permissions.mdx b/info/api-key-permissions.mdx
index 4333580f..0567f3b0 100644
--- a/info/api-key-permissions.mdx
+++ b/info/api-key-permissions.mdx
@@ -56,13 +56,6 @@ Resources must be inside the new key's organization, or its project for a projec
| `proxies:write` | Rename proxies. |
| `proxies:delete` | Delete proxies. |
-### Projects and organizations
-
-| Action | Allows |
-| --- | --- |
-| `projects:read` | View project metadata and limits. |
-| `organizations:read` | View organization entitlements and limits. |
-
## Operations that need more than one action
| Operation | Requires |
@@ -81,5 +74,6 @@ Scoped keys get `403` with `insufficient_scope` on:
- API key management, including creating, listing, rotating, and deleting keys.
- Browser pools.
+- Project and organization endpoints, such as listing projects or reading limits.
- Creating browsers that use saved extensions, telemetry export, or app invocations.
- Any other endpoint not covered by the actions above.
diff --git a/info/api-keys.mdx b/info/api-keys.mdx
index 92f2daf2..3f543546 100644
--- a/info/api-keys.mdx
+++ b/info/api-keys.mdx
@@ -159,7 +159,7 @@ print(api_key.key) # Save this value now. Kernel won't show it again.
### Expiry and revocation
- Use `expires_at` for an exact expiry, such as minutes from now, instead of `days_to_expire`. A scoped key can't outlive the key that creates it.
-- An expired key is rejected immediately, and a deleted key within about 10 seconds. Neither closes browser connections that are already open, so delete those browsers to cut off access.
+- An expired key is rejected immediately, and a deleted key within about 10 seconds. Neither revokes connection URLs (CDP, WebDriver, live view) the key already obtained. They keep working, including for new connections, until the browser is deleted, so delete the key's browsers to cut off access.
- Rotating a scoped key keeps its policy.
Scoped keys can't manage API keys, and they get `403` on endpoints that don't support scoped access yet.