Skip to content

Commit fae0ad3

Browse files
feat(server): pluggable request-auth framework (management + runtime) (#204)
## Summary Pluggable request-auth framework that handles both auth flows the system needs: - **Management.** Online check on every request. The default authorizer authenticates the credential and authorizes the operation; in production this is `HttpUpstreamAuthProvider` forwarding to a configurable upstream service. - **Runtime.** Two-phase exchange-then-verify. A target-bearing call presents a long-lived credential plus `(target_type, target_id)` to a token exchange endpoint; the server mints a short-lived HS256 JWT bound to that target. Subsequent runtime calls verify the JWT locally, with no upstream round-trip on the hot path. Both flows route through the same primitives (`Operation` vocabulary on endpoints, `Principal` returned, `RequestAuthorizer` Protocol installed); a per-operation registry lets a deployment point management ops at one provider and runtime ops at another. Migrates the `/control-bindings` endpoint family onto the framework and ships the runtime token exchange endpoint. The runtime resolution path itself (`/evaluation` etc.) is wired in a follow-up; its provider override (`LocalJwtVerifyProvider`) is already in place when the runtime secret is configured. ## Module layout ``` server/src/agent_control_server/auth_framework/ __init__.py # public API core.py # Operation, Principal, RequestAuthorizer, require_operation, registry config.py # configure_auth_from_env, RuntimeAuthConfig, set_runtime_auth_config runtime_token.py # HS256 mint / verify helpers, UpstreamGrantExpiredError providers/ __init__.py header.py # HeaderAuthProvider + DEFAULT_OPERATION_ACCESS http_upstream.py # HttpUpstreamAuthProvider (forward + parse grant) local_jwt.py # LocalJwtVerifyProvider (hot-path JWT verify) server/src/agent_control_server/endpoints/ auth.py # POST /api/v1/auth/runtime-token-exchange ``` `auth.py` (legacy local credential check) is unchanged; `HeaderAuthProvider` re-uses `_validate_api_key` from it. Non-binding routes still go through the legacy router-level gate; their migration happens in follow-up PRs. ## Operation vocabulary ```python class Operation(StrEnum): # Wired on endpoints in this PR. CONTROL_BINDINGS_READ = "control_bindings.read" CONTROL_BINDINGS_WRITE = "control_bindings.write" RUNTIME_TOKEN_EXCHANGE = "runtime.token_exchange" # Reserved; not yet wired on endpoints. CONTROLS_READ = "controls.read" CONTROLS_CREATE = "controls.create" CONTROLS_UPDATE = "controls.update" CONTROLS_DELETE = "controls.delete" RUNTIME_USE = "runtime.use" ``` ## Per-operation authorizer registry `set_authorizer(authorizer, operation=...)` overrides the default for one operation. Without `operation=`, it becomes the default for every operation that does not have a specific binding. Used to route management ops through one provider and `Operation.RUNTIME_USE` through `LocalJwtVerifyProvider`: ```python set_authorizer(HttpUpstreamAuthProvider(...)) # default set_authorizer(LocalJwtVerifyProvider(secret=...), # override operation=Operation.RUNTIME_USE) ``` `require_operation(op)` consults the override first, falls back to the default. The local-credential path (no override installed) routes everything to `HeaderAuthProvider`; the no-auth flow (`api_key_enabled=False`) is preserved end-to-end. `require_operation` accepts an optional `context_builder` so the endpoint can surface request-shaped context (path / query / body fields) to the authorizer. The body-bearing binding endpoints, the target-filtered list endpoint, and the runtime token exchange endpoint all forward `(target_type, target_id)` so an upstream that resolves the target's owning project has the identifiers it needs to make a project-level decision. ## Providers (three ship in-tree) **`HeaderAuthProvider`**: local-credential path, single namespace. - Maps each `Operation` to one of three access levels (`PUBLIC`, `AUTHENTICATED`, `ADMIN`); single source of truth in `DEFAULT_OPERATION_ACCESS`. - Reuses the existing local API-key + session-cookie credential check from `auth.py`, so behavior matches the previous `require_admin_key` path verbatim. - Returns a normalized `runtime.use` scope only for `Operation.RUNTIME_TOKEN_EXCHANGE`, so the exchange endpoint can uniformly require `runtime.use` in `principal.scopes` across every provider; there is no implicit fallback that could escalate an upstream-supplied empty scope grant. - The no-auth flow (`api_key_enabled=False`) is preserved: every operation succeeds with a non-admin `Principal`. Pinned by a regression test. - Always returns `DEFAULT_NAMESPACE_KEY`. The namespace header lookup branch is preserved but inert until non-binding write endpoints are threaded. **`HttpUpstreamAuthProvider`**: generic upstream-delegating provider. - Forwards caller credentials (`X-API-Key`, `Authorization`, `Cookie`) on a POST to a configurable URL with `{operation, context?}`. - Optional service-to-service token header for upstream trust. - Parses the upstream response into a `Principal`: `namespace_key`, `is_admin`, `caller_id`, plus optional grant fields (`target_type`, `target_id`, `scopes`, `expires_at`) so the runtime token exchange can mint from the same response. - Maps `200` to `Principal`; `401` / `403` / `404` to matching error; `5xx`, network errors, malformed payloads, naive (`tzinfo`-less) `expires_at`, and partial target grants (only one of `target_type` / `target_id`) all fail closed (502/503). **`LocalJwtVerifyProvider`**: hot-path runtime verifier. - Reads a Bearer token from `Authorization`, verifies signature against the runtime secret, checks `domain == "runtime"`, the issuer, expiry, and that the token's scope covers the requested `Operation`. - Returns a `Principal` with the bound `(namespace_key, target_type, target_id)` so runtime endpoints inherit the namespace and target binding without re-deriving them. - When the dependency surfaces `target_type` / `target_id` via `context_builder`, the provider also enforces that they match the token's binding; runtime endpoints get the request-target check for free. ## Runtime token shape HS256, dedicated secret (`AGENT_CONTROL_RUNTIME_TOKEN_SECRET`), issuer `agent-control/server`. Claims: | Claim | Purpose | |---|---| | `domain` | Pinned to `runtime`; tokens minted here MUST not be accepted on management endpoints. | | `namespace_key` | The namespace the token authorizes within. Required for mint and verify; preserved end-to-end so a token minted for one namespace cannot be used to resolve controls in another. | | `actor_id` | Caller identity surfaced from the upstream grant. | | `scopes` | Granted runtime capabilities (e.g., `["runtime.use"]`). The exchange endpoint refuses to mint when `principal.scopes` does not contain `runtime.use`, including the case where the upstream's grant explicitly lists an empty scope set. | | `target_type` / `target_id` | Bind the token to one target. | | `iat` / `exp` | Bounded lifetime. The local TTL is capped by the upstream grant's `expires_at` so the local token can never outlive its grant. | | `jti` | Random identifier; reserved for future revocation. | `mint_runtime_token` rejects an `upstream_expires_at` whose `tzinfo is None` or whose `utcoffset()` is `None` with `RuntimeTokenError` so a custom authorizer that supplies a naive datetime surfaces as a typed auth error rather than a raw `TypeError` deeper in the comparison. ## Runtime token exchange endpoint ``` POST /api/v1/auth/runtime-token-exchange { "target_type": "...", "target_id": "..." } ``` - Authenticated and authorized via `Operation.RUNTIME_TOKEN_EXCHANGE` through the default authorizer (typically `HttpUpstreamAuthProvider` in production). The authorizer's `context_builder` forwards the requested target to the upstream so it can authorize against the right resource. - Refuses with 503 when `AGENT_CONTROL_RUNTIME_TOKEN_SECRET` is not configured. - Mints a local token from `Principal.scopes` / `Principal.grant_expires_at`, capped by the configured TTL (default 300s). - When the provider's `Principal` carries a target binding, the endpoint verifies it matches the requested target before minting. - An upstream grant whose `expires_at` is already in the past surfaces as 502 (`UpstreamGrantExpiredError`), distinct from the 503 misconfigured-server path so the public status reflects which side the operator should investigate. Response: `{ token, expires_at, target_type, target_id, scopes }`. ## Storage namespace under the framework The migrated binding endpoints take the storage `namespace_key` from `get_namespace_key` (the same resolver the rest of the server uses), not from `principal.namespace_key`. The auth chain still runs through `require_operation` for authentication and authorization, but the row's namespace is sourced from the resolver so binding writes and runtime reads stay in lockstep until auth-derived namespace resolution lands across `/controls`, `/policies`, `/agents`, and `/evaluation` together. The principal's namespace is observed (and used by `LocalJwtVerifyProvider` for its own contract) but is not used to pick the row's storage namespace at this stage. ## Migrated endpoints All seven `/api/v1/control-bindings*` endpoints now use `Depends(require_operation(...))`: | Method | Path | Operation | Context forwarded | |---|---|---|---| | PUT | `/control-bindings` | `control_bindings.write` | body: `target_type`, `target_id` | | GET | `/control-bindings` | `control_bindings.read` | query: `target_type`, `target_id` (when present) | | GET | `/control-bindings/{binding_id}` | `control_bindings.read` | N/A (namespace-wide) | | PATCH | `/control-bindings/{binding_id}` | `control_bindings.write` | N/A (namespace-wide) | | DELETE | `/control-bindings/{binding_id}` | `control_bindings.write` | N/A (namespace-wide) | | PUT | `/control-bindings/by-key` | `control_bindings.write` | body: `target_type`, `target_id` | | POST | `/control-bindings/by-key:delete` | `control_bindings.write` | body: `target_type`, `target_id` | The four binding-id-based routes are documented as namespace-wide: their target identifiers are not available before the binding row is loaded, and `require_operation` is single-pass. Clients whose authorization model requires per-target permissions are steered to the natural-key endpoints and the target-filtered list, all of which forward the target to the authorizer. Two-phase auth on the by-id routes is a follow-up. New: `POST /api/v1/auth/runtime-token-exchange` (operation `runtime.token_exchange`). The framework-protected routers (`/control-bindings`, `/auth`) are mounted with the existing non-validating `get_api_key_from_header` Security extractor as a router-level dependency. `require_operation` still owns runtime authentication and authorization; the Security dependency exists purely so the generated OpenAPI spec advertises `X-API-Key` on these routes for downstream SDK generation. ## Generated client The TypeScript wrapper exposes both `auth` and `controlBindings` getters alongside the existing surface, so consumers using the public client can call `runtimeTokenExchange` and the binding API without reaching into the generated internals. ## Env vars | Var | Default | Purpose | |---|---|---| | `AGENT_CONTROL_AUTH_MODE` | `header` | Default authorizer: `header` or `http_upstream`. | | `AGENT_CONTROL_AUTH_UPSTREAM_URL` | none | Required when mode is `http_upstream`. | | `AGENT_CONTROL_AUTH_UPSTREAM_TIMEOUT_SECONDS` | `5.0` | Per-request timeout. | | `AGENT_CONTROL_AUTH_UPSTREAM_SERVICE_TOKEN` | none | Optional upstream service token. | | `AGENT_CONTROL_AUTH_UPSTREAM_SERVICE_TOKEN_HEADER` | `X-Agent-Control-Service-Token` | Header name for the service token. | | `AGENT_CONTROL_RUNTIME_TOKEN_SECRET` | none | Required to enable runtime auth + the exchange endpoint. Validated at startup; rejected if shorter than 32 bytes. | | `AGENT_CONTROL_RUNTIME_TOKEN_TTL_SECONDS` | `300` | Local token TTL ceiling (capped further by the upstream grant). Validated at startup. | `configure_auth_from_env` parses both runtime fields once at startup into a frozen `RuntimeAuthConfig`. The exchange endpoint and `LocalJwtVerifyProvider` read the same object, so the mint and verify sides cannot drift apart on a process. When the runtime secret is absent, `RUNTIME_USE` falls through to the default authorizer; this is logged at WARNING so an operator can immediately see what trust model is in effect. `RUNTIME_USE` is reserved and not wired to `/evaluation` in this PR, so this fallback does not affect the runtime hot path yet. The follow-up that wires runtime endpoints should explicitly choose legacy fallback or fail-closed JWT-only behavior. ## Out of scope (follow-ups) - Migrate `/controls` CRUD onto `require_operation` using the reserved `CONTROLS_*` operations. - Wire `Operation.RUNTIME_USE` on the runtime resolution path (`/evaluation`, etc.) and the SDK side of the runtime exchange. The provider override is already in place when the runtime secret is configured. - Migrate `/agents/initAgent` onto `require_operation`. The `HttpUpstreamAuthProvider`'s `context_builder` should forward the request's `target_type` / `target_id` to the upstream so the upstream can authorize against the requested resource. - Auth-derived `get_namespace_key` so the binding endpoints can use the principal's namespace for storage along with the rest of the server. - Two-phase auth for the four binding-id-based routes (GET/PATCH/DELETE `/control-bindings/{binding_id}`) so they can forward target context to the upstream. - Drop `auth.py`'s `require_admin_key` once every management endpoint is migrated. ## Stacking Stacked on **PR #203** (`abhi/data-model-v1`); rebased onto its current head `8adc328` so the merged effective-controls contract, namespace-threaded agent endpoints, and savepoint-protected binding writes are the base this PR builds on. Will rebase onto `main` once #203 merges. ## Test plan - [x] 55 framework + endpoint tests covering: - Default coverage: every `Operation` member has a default access mapping (regression guard). - `HeaderAuthProvider`: PUBLIC bypass, AUTHENTICATED + ADMIN paths route to the legacy validator with the right `require_admin` flag, no-auth mode passes admin operations, namespace-header lookup currently inert, unknown operation raises, normalized `runtime.use` scope returned for `RUNTIME_TOKEN_EXCHANGE`. - `HttpUpstreamAuthProvider`: 200 happy path with realistic JSON wire shapes (ISO datetime + JSON array scopes round-trip), service token forwarding, 401/403/404 mapping, 5xx fail-closed, network-error fail-closed, strict-grant rejection on wrong-typed `is_admin` / malformed `scopes` / bad `expires_at` / non-string target fields, partial target grant rejected, naive `expires_at` rejected. - `require_operation` factory: routes through the installed authorizer, per-operation overrides take precedence, clearing an override falls back to the default, `get_authorizer` raises when nothing is set. - Lifecycle: reconfiguring without the runtime secret drops the previous `LocalJwtVerifyProvider` override; teardown clears every authorizer; secret shorter than 32 bytes raises at startup; invalid TTL raises at startup. - Runtime token mint / verify: round-trip, wrong-secret rejection, expiry rejection, TTL capped by upstream grant, management-domain token refused on runtime verify, missing-namespace rejection, already-expired upstream grant raises `UpstreamGrantExpiredError`, naive `upstream_expires_at` raises `RuntimeTokenError`. - `LocalJwtVerifyProvider`: target-bound `Principal`, namespace carried from token, missing token returns 401, wrong scope returns 403, non-Bearer header returns 401, target-context match enforcement (mismatch on type or id returns 403). - Exchange endpoint: 503 without secret, mint when configured, target mismatch rejected (400), missing target rejected (422), grant-without-runtime-use rejected (no privilege escalation), explicit empty-scope grant rejected (no fallback escalation), target context forwarded to authorizer, non-default namespace propagates into the token, full exchange-then-verify round trip, already-expired upstream grant surfaces as 502 distinct from the 503 misconfigured-server path. - [x] Full server suite: 676 passed. - [x] `make lint` clean. - [x] `make typecheck` clean. - [x] `make sdk-ts-generate-check` clean. - [x] TypeScript SDK regenerated alongside the new endpoint (`auth-runtime-token-exchange`, request/response models, `Auth` and `ControlBindings` groups exposed via the public client).
1 parent 15ed4fd commit fae0ad3

28 files changed

Lines changed: 3266 additions & 42 deletions

‎sdks/typescript/overlays/method-names.overlay.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ actions:
100100
x-speakeasy-group: agents
101101
x-speakeasy-name-override: updatePolicy
102102

103+
- target: $["paths"]["/api/v1/auth/runtime-token-exchange"]["post"]
104+
update:
105+
x-speakeasy-group: auth
106+
x-speakeasy-name-override: runtimeTokenExchange
107+
103108
- target: $["paths"]["/api/v1/control-bindings"]["get"]
104109
update:
105110
x-speakeasy-group: controlBindings

‎sdks/typescript/src/client.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface AgentControlInitOptions {
1818
}
1919

2020
export type AgentsApi = AgentControlSDK["agents"];
21+
export type AuthApi = AgentControlSDK["auth"];
2122
export type ControlBindingsApi = AgentControlSDK["controlBindings"];
2223
export type ControlsApi = AgentControlSDK["controls"];
2324
export type EvaluationApi = AgentControlSDK["evaluation"];
@@ -52,6 +53,10 @@ export class AgentControlClient {
5253
return this.requireSDK().agents;
5354
}
5455

56+
get auth(): AuthApi {
57+
return this.requireSDK().auth;
58+
}
59+
5560
get controlBindings(): ControlBindingsApi {
5661
return this.requireSDK().controlBindings;
5762
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
3+
*/
4+
5+
import * as z from "zod/v4-mini";
6+
import { AgentControlSDKCore } from "../core.js";
7+
import { encodeJSON } from "../lib/encodings.js";
8+
import * as M from "../lib/matchers.js";
9+
import { compactMap } from "../lib/primitives.js";
10+
import { safeParse } from "../lib/schemas.js";
11+
import { RequestOptions } from "../lib/sdks.js";
12+
import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js";
13+
import { pathToFunc } from "../lib/url.js";
14+
import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js";
15+
import {
16+
ConnectionError,
17+
InvalidRequestError,
18+
RequestAbortedError,
19+
RequestTimeoutError,
20+
UnexpectedClientError,
21+
} from "../models/errors/http-client-errors.js";
22+
import * as errors from "../models/errors/index.js";
23+
import { ResponseValidationError } from "../models/errors/response-validation-error.js";
24+
import { SDKValidationError } from "../models/errors/sdk-validation-error.js";
25+
import * as models from "../models/index.js";
26+
import { APICall, APIPromise } from "../types/async.js";
27+
import { Result } from "../types/fp.js";
28+
29+
/**
30+
* Exchange a credential for a runtime token bound to a target
31+
*
32+
* @remarks
33+
* Mint a short-lived runtime token for the requested target.
34+
*
35+
* The caller's credential is authenticated and authorized by the
36+
* installed default authorizer; the resulting :class:`Principal`
37+
* supplies the actor identity and (when the upstream surfaces it)
38+
* the grant scopes and expiry. This endpoint then mints a local HS256
39+
* token whose lifetime cannot outlive the upstream grant.
40+
*
41+
* Runtime auth must be enabled via
42+
* ``AGENT_CONTROL_RUNTIME_TOKEN_SECRET``; otherwise the endpoint
43+
* returns 503.
44+
*/
45+
export function authRuntimeTokenExchange(
46+
client: AgentControlSDKCore,
47+
request: models.RuntimeTokenExchangeRequest,
48+
options?: RequestOptions,
49+
): APIPromise<
50+
Result<
51+
models.RuntimeTokenExchangeResponse,
52+
| errors.HTTPValidationError
53+
| AgentControlSDKError
54+
| ResponseValidationError
55+
| ConnectionError
56+
| RequestAbortedError
57+
| RequestTimeoutError
58+
| InvalidRequestError
59+
| UnexpectedClientError
60+
| SDKValidationError
61+
>
62+
> {
63+
return new APIPromise($do(
64+
client,
65+
request,
66+
options,
67+
));
68+
}
69+
70+
async function $do(
71+
client: AgentControlSDKCore,
72+
request: models.RuntimeTokenExchangeRequest,
73+
options?: RequestOptions,
74+
): Promise<
75+
[
76+
Result<
77+
models.RuntimeTokenExchangeResponse,
78+
| errors.HTTPValidationError
79+
| AgentControlSDKError
80+
| ResponseValidationError
81+
| ConnectionError
82+
| RequestAbortedError
83+
| RequestTimeoutError
84+
| InvalidRequestError
85+
| UnexpectedClientError
86+
| SDKValidationError
87+
>,
88+
APICall,
89+
]
90+
> {
91+
const parsed = safeParse(
92+
request,
93+
(value) =>
94+
z.parse(models.RuntimeTokenExchangeRequest$outboundSchema, value),
95+
"Input validation failed",
96+
);
97+
if (!parsed.ok) {
98+
return [parsed, { status: "invalid" }];
99+
}
100+
const payload = parsed.value;
101+
const body = encodeJSON("body", payload, { explode: true });
102+
103+
const path = pathToFunc("/api/v1/auth/runtime-token-exchange")();
104+
105+
const headers = new Headers(compactMap({
106+
"Content-Type": "application/json",
107+
Accept: "application/json",
108+
}));
109+
110+
const secConfig = await extractSecurity(client._options.apiKeyHeader);
111+
const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig };
112+
const requestSecurity = resolveGlobalSecurity(securityInput);
113+
114+
const context = {
115+
options: client._options,
116+
baseURL: options?.serverURL ?? client._baseURL ?? "",
117+
operationID:
118+
"runtime_token_exchange_api_v1_auth_runtime_token_exchange_post",
119+
oAuth2Scopes: null,
120+
121+
resolvedSecurity: requestSecurity,
122+
123+
securitySource: client._options.apiKeyHeader,
124+
retryConfig: options?.retries
125+
|| client._options.retryConfig
126+
|| { strategy: "none" },
127+
retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"],
128+
};
129+
130+
const requestRes = client._createRequest(context, {
131+
security: requestSecurity,
132+
method: "POST",
133+
baseURL: options?.serverURL,
134+
path: path,
135+
headers: headers,
136+
body: body,
137+
userAgent: client._options.userAgent,
138+
timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1,
139+
}, options);
140+
if (!requestRes.ok) {
141+
return [requestRes, { status: "invalid" }];
142+
}
143+
const req = requestRes.value;
144+
145+
const doResult = await client._do(req, {
146+
context,
147+
errorCodes: ["422", "4XX", "5XX"],
148+
retryConfig: context.retryConfig,
149+
retryCodes: context.retryCodes,
150+
});
151+
if (!doResult.ok) {
152+
return [doResult, { status: "request-error", request: req }];
153+
}
154+
const response = doResult.value;
155+
156+
const responseFields = {
157+
HttpMeta: { Response: response, Request: req },
158+
};
159+
160+
const [result] = await M.match<
161+
models.RuntimeTokenExchangeResponse,
162+
| errors.HTTPValidationError
163+
| AgentControlSDKError
164+
| ResponseValidationError
165+
| ConnectionError
166+
| RequestAbortedError
167+
| RequestTimeoutError
168+
| InvalidRequestError
169+
| UnexpectedClientError
170+
| SDKValidationError
171+
>(
172+
M.json(200, models.RuntimeTokenExchangeResponse$inboundSchema),
173+
M.jsonErr(422, errors.HTTPValidationError$inboundSchema),
174+
M.fail("4XX"),
175+
M.fail("5XX"),
176+
)(response, req, { extraFields: responseFields });
177+
if (!result.ok) {
178+
return [result, { status: "complete", request: req, response }];
179+
}
180+
181+
return [result, { status: "complete", request: req, response }];
182+
}

‎sdks/typescript/src/generated/funcs/control-bindings-create.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ import { Result } from "../types/fp.js";
3232
* @remarks
3333
* Attach a control to an opaque external target.
3434
*
35-
* Each binding row is an attachment scoped to the request's namespace; the
36-
* ``enabled`` flag is a soft toggle. Per-agent overrides and exemptions are
37-
* intentionally out of scope; see ``ControlBinding`` for the forward path.
35+
* Each binding row is scoped to the request namespace as resolved by
36+
* ``get_namespace_key``. The auth chain still runs via
37+
* ``require_operation`` for authentication and authorization, but the
38+
* storage namespace is taken from the same resolver the rest of the
39+
* server uses so binding writes and runtime reads stay in lockstep
40+
* until auth-derived namespace resolution lands across every endpoint.
3841
*/
3942
export function controlBindingsCreate(
4043
client: AgentControlSDKCore,

‎sdks/typescript/src/generated/funcs/control-bindings-delete.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ import { APICall, APIPromise } from "../types/async.js";
2828
import { Result } from "../types/fp.js";
2929

3030
/**
31-
* Delete a control binding
31+
* Delete a control binding (namespace-wide)
32+
*
33+
* @remarks
34+
* Delete a control binding by surrogate ID.
35+
*
36+
* See the GET-by-id docstring for the authorization scope: this route
37+
* is namespace-wide because the target identifiers are not available
38+
* before the binding is loaded. Use ``POST /by-key:delete`` for
39+
* target-scoped detach that forwards the target to the authorizer.
3240
*/
3341
export function controlBindingsDelete(
3442
client: AgentControlSDKCore,

‎sdks/typescript/src/generated/funcs/control-bindings-get.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,18 @@ import { APICall, APIPromise } from "../types/async.js";
2828
import { Result } from "../types/fp.js";
2929

3030
/**
31-
* Get a control binding
31+
* Get a control binding (namespace-wide)
32+
*
33+
* @remarks
34+
* Read a single control binding by surrogate ID.
35+
*
36+
* Authorization is namespace-wide: the binding's target identifiers
37+
* are not forwarded to the upstream because they are only discoverable
38+
* after the row is loaded, and ``require_operation`` is single-pass.
39+
* Callers whose authorization model requires per-target permissions
40+
* should use the natural-key endpoints (``PUT /by-key``,
41+
* ``POST /by-key:delete``) and the target-filtered list endpoint, all
42+
* of which forward ``(target_type, target_id)`` to the authorizer.
3243
*/
3344
export function controlBindingsGet(
3445
client: AgentControlSDKCore,

‎sdks/typescript/src/generated/funcs/control-bindings-list.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ import { Result } from "../types/fp.js";
3131
* List control bindings
3232
*
3333
* @remarks
34-
* Return bindings in the current namespace with optional filters and
35-
* cursor-based pagination. Bindings are ordered by ID descending (newest
36-
* first). The cursor is opaque to clients: pass back the
37-
* ``next_cursor`` value verbatim to fetch the following page.
34+
* Return bindings in the request namespace with optional filters and
35+
* cursor-based pagination. Bindings are ordered by ID descending
36+
* (newest first). The cursor is opaque to clients: pass back the
37+
* ``next_cursor`` value verbatim to fetch the following page. The
38+
* storage namespace is resolved by ``get_namespace_key`` so this
39+
* listing stays in lockstep with the rest of the server's reads.
3840
*/
3941
export function controlBindingsList(
4042
client: AgentControlSDKCore,

‎sdks/typescript/src/generated/funcs/control-bindings-update.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ import { APICall, APIPromise } from "../types/async.js";
2828
import { Result } from "../types/fp.js";
2929

3030
/**
31-
* Update a control binding
31+
* Update a control binding (namespace-wide)
3232
*
3333
* @remarks
3434
* Update the ``enabled`` flag on a control binding.
35+
*
36+
* See the GET-by-id docstring for the authorization scope: this route
37+
* is namespace-wide because the target identifiers are not available
38+
* before the binding is loaded. Use ``PUT /by-key`` for target-scoped
39+
* upserts that forward the target to the authorizer.
3540
*/
3641
export function controlBindingsUpdate(
3742
client: AgentControlSDKCore,

‎sdks/typescript/src/generated/models/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export * from "./regex-template-parameter.js";
8585
export * from "./remove-agent-control-response.js";
8686
export * from "./render-control-template-request.js";
8787
export * from "./render-control-template-response.js";
88+
export * from "./runtime-token-exchange-request.js";
89+
export * from "./runtime-token-exchange-response.js";
8890
export * from "./security.js";
8991
export * from "./set-control-data-request.js";
9092
export * from "./set-control-data-response.js";
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
3+
*/
4+
5+
import * as z from "zod/v4-mini";
6+
import { remap as remap$ } from "../lib/primitives.js";
7+
8+
/**
9+
* Body for the runtime token exchange endpoint.
10+
*/
11+
export type RuntimeTokenExchangeRequest = {
12+
/**
13+
* Opaque target identifier.
14+
*/
15+
targetId: string;
16+
/**
17+
* Opaque target kind (e.g., ``log_stream``).
18+
*/
19+
targetType: string;
20+
};
21+
22+
/** @internal */
23+
export type RuntimeTokenExchangeRequest$Outbound = {
24+
target_id: string;
25+
target_type: string;
26+
};
27+
28+
/** @internal */
29+
export const RuntimeTokenExchangeRequest$outboundSchema: z.ZodMiniType<
30+
RuntimeTokenExchangeRequest$Outbound,
31+
RuntimeTokenExchangeRequest
32+
> = z.pipe(
33+
z.object({
34+
targetId: z.string(),
35+
targetType: z.string(),
36+
}),
37+
z.transform((v) => {
38+
return remap$(v, {
39+
targetId: "target_id",
40+
targetType: "target_type",
41+
});
42+
}),
43+
);
44+
45+
export function runtimeTokenExchangeRequestToJSON(
46+
runtimeTokenExchangeRequest: RuntimeTokenExchangeRequest,
47+
): string {
48+
return JSON.stringify(
49+
RuntimeTokenExchangeRequest$outboundSchema.parse(
50+
runtimeTokenExchangeRequest,
51+
),
52+
);
53+
}

0 commit comments

Comments
 (0)