knoxcall 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +439 -2
- data/exe/knoxcall +8 -0
- data/lib/knoxcall/bootstrap.rb +70 -0
- data/lib/knoxcall/bound_route.rb +47 -0
- data/lib/knoxcall/cli/ai.rb +79 -0
- data/lib/knoxcall/cli/ai_control.rb +275 -0
- data/lib/knoxcall/cli/common.rb +96 -0
- data/lib/knoxcall/cli/init.rb +94 -0
- data/lib/knoxcall/cli/login.rb +306 -0
- data/lib/knoxcall/cli/logout.rb +41 -0
- data/lib/knoxcall/cli/whoami.rb +29 -0
- data/lib/knoxcall/cli.rb +377 -0
- data/lib/knoxcall/client.rb +1025 -0
- data/lib/knoxcall/credentials_file.rb +442 -0
- data/lib/knoxcall/dpop.rb +79 -0
- data/lib/knoxcall/egress_observations.rb +372 -0
- data/lib/knoxcall/errors.rb +304 -0
- data/lib/knoxcall/intercept_patch.rb +181 -0
- data/lib/knoxcall/intercept_pipeline.rb +455 -0
- data/lib/knoxcall/intercept_resolver.rb +140 -0
- data/lib/knoxcall/intercept_store.rb +203 -0
- data/lib/knoxcall/login.rb +144 -0
- data/lib/knoxcall/resources/account.rb +12 -0
- data/lib/knoxcall/resources/agents.rb +25 -0
- data/lib/knoxcall/resources/ai_gateway.rb +417 -0
- data/lib/knoxcall/resources/api_keys.rb +35 -0
- data/lib/knoxcall/resources/audit_logs.rb +45 -0
- data/lib/knoxcall/resources/clients.rb +39 -0
- data/lib/knoxcall/resources/crypto.rb +122 -0
- data/lib/knoxcall/resources/dynamic_db.rb +68 -0
- data/lib/knoxcall/resources/environments.rb +16 -0
- data/lib/knoxcall/resources/logs.rb +51 -0
- data/lib/knoxcall/resources/oauth_clients.rb +34 -0
- data/lib/knoxcall/resources/opportunities.rb +61 -0
- data/lib/knoxcall/resources/pki.rb +41 -0
- data/lib/knoxcall/resources/roles.rb +27 -0
- data/lib/knoxcall/resources/routes.rb +53 -0
- data/lib/knoxcall/resources/secrets.rb +98 -0
- data/lib/knoxcall/resources/unwraps_envelope.rb +90 -0
- data/lib/knoxcall/resources/vaults.rb +77 -0
- data/lib/knoxcall/resources/webhooks.rb +48 -0
- data/lib/knoxcall/resources/workflows.rb +86 -0
- data/lib/knoxcall/resources/wrap.rb +352 -0
- data/lib/knoxcall/route_refusal.rb +67 -0
- data/lib/knoxcall/signup.rb +122 -0
- data/lib/knoxcall/token_exchange.rb +169 -0
- data/lib/knoxcall/ulid.rb +19 -0
- data/lib/knoxcall/warnings.rb +63 -0
- data/lib/knoxcall/workload_provider.rb +192 -0
- data/lib/knoxcall/wrap_faraday_adapter.rb +119 -0
- data/lib/knoxcall/wrap_faraday_middleware.rb +67 -0
- data/lib/knoxcall/wrap_transport.rb +139 -0
- data/lib/knoxcall.rb +45 -1
- metadata +70 -9
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
module KnoxCall
|
|
2
|
+
module Resources
|
|
3
|
+
# AI Gateway control plane (server: src/client-api/ai-gateway.ts).
|
|
4
|
+
#
|
|
5
|
+
# A gateway holds one or more agents; each agent mints capability tokens
|
|
6
|
+
# used by the AI egress data plane. Following this SDK's convention for a
|
|
7
|
+
# resource with sub-collections (see routes/vaults), the sub-collections
|
|
8
|
+
# are FLAT methods on one resource, grouped by prefix:
|
|
9
|
+
# +*_gateway+, +*_agent+, +*_token+, plus {#usage}.
|
|
10
|
+
#
|
|
11
|
+
# client.ai_gateway.list_gateways
|
|
12
|
+
# gw = client.ai_gateway.create_gateway(name: "Prod", slug: "prod")
|
|
13
|
+
# ag = client.ai_gateway.create_agent(gw["id"], name: "Support", slug: "support")
|
|
14
|
+
# tok = client.ai_gateway.mint_token(ag["id"], kind: "agent")
|
|
15
|
+
# tok["token"] # plaintext — shown ONCE, store it now
|
|
16
|
+
#
|
|
17
|
+
# The sandbox/test client (KnoxCall::Client.new(sandbox: true)) mints
|
|
18
|
+
# test-env tokens server-side; no env parameter is threaded through these
|
|
19
|
+
# methods.
|
|
20
|
+
class AiGateway
|
|
21
|
+
include UnwrapsEnvelope
|
|
22
|
+
|
|
23
|
+
def initialize(client) = @client = client
|
|
24
|
+
|
|
25
|
+
# -- Gateways ----------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
# Paginated. Params: page (default 1), per_page (default 20, max 100).
|
|
28
|
+
# Returns the {data, meta} envelope.
|
|
29
|
+
def list_gateways(**params) = @client.request("GET", "/v1/ai-gateway/gateways", query: params.empty? ? nil : params)
|
|
30
|
+
|
|
31
|
+
# Yield every gateway, walking pages transparently. Returns a lazy
|
|
32
|
+
# Enumerator when no block is given.
|
|
33
|
+
def each_gateway(**params, &block)
|
|
34
|
+
enum = paginate(params) { |p| list_gateways(**p) }
|
|
35
|
+
block ? enum.each(&block) : enum
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# body: name:, slug:, description?, budget_daily_usd?, budget_monthly_usd?,
|
|
39
|
+
# budget_overage_action? ('block' | 'warn', AIGW-150 — what happens
|
|
40
|
+
# when a cap is spent; refuses on BOTH data planes under 'block').
|
|
41
|
+
def create_gateway(**input) = unwrap(@client.request("POST", "/v1/ai-gateway/gateways", body: input))
|
|
42
|
+
def get_gateway(gateway_id) = unwrap(@client.request("GET", "/v1/ai-gateway/gateways/#{encode(gateway_id)}"))
|
|
43
|
+
# patch: name?, description?, budget_daily_usd?, budget_monthly_usd?,
|
|
44
|
+
# budget_overage_action? ('block' | 'warn', AIGW-150).
|
|
45
|
+
def update_gateway(gateway_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/gateways/#{encode(gateway_id)}", body: patch))
|
|
46
|
+
# Returns {"id", "status"}.
|
|
47
|
+
def delete_gateway(gateway_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/gateways/#{encode(gateway_id)}"))
|
|
48
|
+
|
|
49
|
+
# -- Agents ------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# Paginated. Params: page (default 1), per_page (default 20, max 100).
|
|
52
|
+
# Returns the {data, meta} envelope.
|
|
53
|
+
def list_agents(gateway_id, **params) = @client.request("GET", "/v1/ai-gateway/gateways/#{encode(gateway_id)}/agents", query: params.empty? ? nil : params)
|
|
54
|
+
|
|
55
|
+
# Yield every agent under a gateway, walking pages transparently. Returns
|
|
56
|
+
# a lazy Enumerator when no block is given.
|
|
57
|
+
def each_agent(gateway_id, **params, &block)
|
|
58
|
+
enum = paginate(params) { |p| list_agents(gateway_id, **p) }
|
|
59
|
+
block ? enum.each(&block) : enum
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# body: name:, slug:, description?, primary_route_id?, provider?,
|
|
63
|
+
# upstream_secret_id?, upstream?, default_model?, model_allowlist?,
|
|
64
|
+
# model_denylist?, budget_daily_usd?, budget_monthly_usd?,
|
|
65
|
+
# streaming_enabled?, firewall_policy_id?, pii_redact_policy_id?,
|
|
66
|
+
# pii_request_mode? (off|tokenize), pii_response_mode? (redact|detokenize).
|
|
67
|
+
#
|
|
68
|
+
# AIGW-100: pii_request_mode: decides what happens to the PROMPT before it
|
|
69
|
+
# leaves KnoxCall (default 'tokenize'); pii_response_mode: decides what
|
|
70
|
+
# happens to the answer (default 'detokenize'). 'off' on the request side
|
|
71
|
+
# is the only configuration on which the provider receives the real
|
|
72
|
+
# value.
|
|
73
|
+
#
|
|
74
|
+
# provider: composes the upstream route for you, INSTEAD of primary_route_id:.
|
|
75
|
+
# It is a plain string and the catalog is SERVER-side (fourteen ids at the
|
|
76
|
+
# time of writing, from anthropic and openai through groq, bedrock and
|
|
77
|
+
# openai-compatible); a bad value returns a 400 naming the valid set, so do
|
|
78
|
+
# not mirror the list here. KnoxCall creates
|
|
79
|
+
# an ai-gateway-<slug> route pointing at the provider, injecting
|
|
80
|
+
# upstream_secret_id: through the envelope store, and sets default_model
|
|
81
|
+
# from its pricebook default.
|
|
82
|
+
#
|
|
83
|
+
# upstream: is required for the four providers whose endpoint is yours
|
|
84
|
+
# rather than the vendor's: azure-openai, ollama, bedrock and
|
|
85
|
+
# openai-compatible. It is not defaulted: a bedrock or openai-compatible
|
|
86
|
+
# agent created without upstream: is refused with a 400 at create time.
|
|
87
|
+
#
|
|
88
|
+
# Supply provider: or primary_route_id:, NEVER BOTH (400). Supplying
|
|
89
|
+
# neither creates an agent with no upstream and no credential template,
|
|
90
|
+
# whose first data-plane call 502s.
|
|
91
|
+
#
|
|
92
|
+
# Every agent projection -- create, get, update and the list rows -- carries
|
|
93
|
+
# "agent_url": the data-plane base URL, https://{tenant}.knoxcall.com/v1/ai/{slug}.
|
|
94
|
+
# Point an AI SDK's base_url there with a capability token as the API key. It
|
|
95
|
+
# is server-computed rather than stored, so it MOVES when the slug changes,
|
|
96
|
+
# and is "" when the tenant slug cannot be resolved -- treat empty as "not
|
|
97
|
+
# available", never as a URL.
|
|
98
|
+
def create_agent(gateway_id, **input) = unwrap(@client.request("POST", "/v1/ai-gateway/gateways/#{encode(gateway_id)}/agents", body: input))
|
|
99
|
+
# Agents are addressed by their own id once created (not nested under the gateway).
|
|
100
|
+
def get_agent(agent_id) = unwrap(@client.request("GET", "/v1/ai-gateway/agents/#{encode(agent_id)}"))
|
|
101
|
+
def update_agent(agent_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/agents/#{encode(agent_id)}", body: patch))
|
|
102
|
+
# Returns {"id", "status"}.
|
|
103
|
+
def delete_agent(agent_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/agents/#{encode(agent_id)}"))
|
|
104
|
+
|
|
105
|
+
# -- MCP servers -------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
# Paginated. Params: page (default 1), per_page (default 20, max 100).
|
|
108
|
+
# Returns the {data, meta} envelope.
|
|
109
|
+
# AIGW-151/150: an MCP server also carries `pii_redact_policy_id` (the
|
|
110
|
+
# tenant policy whose recognizers apply to its tool arguments and results)
|
|
111
|
+
# and the five `guardrail_webhook_*` fields (the tenant's own scanner,
|
|
112
|
+
# offered BOTH directions — there is no streaming carve-out on this plane).
|
|
113
|
+
# A policy or secret id this tenant does not own is refused 422.
|
|
114
|
+
#
|
|
115
|
+
# AIGW-152: an MCP server is a Live or a Test object. The row carries the
|
|
116
|
+
# mode of the API key that created it, every read and write below is
|
|
117
|
+
# confined to that key's own space (a Live key does not see a Test server
|
|
118
|
+
# at all), and `sandbox` on the response is READ-ONLY — passing it to
|
|
119
|
+
# create/update is ignored, never honoured.
|
|
120
|
+
def list_mcp_servers(gateway_id, **params) = @client.request("GET", "/v1/ai-gateway/gateways/#{encode(gateway_id)}/mcp-servers", query: params.empty? ? nil : params)
|
|
121
|
+
|
|
122
|
+
# Yield every MCP server under a gateway, walking pages transparently.
|
|
123
|
+
# Returns a lazy Enumerator when no block is given.
|
|
124
|
+
def each_mcp_server(gateway_id, **params, &block)
|
|
125
|
+
enum = paginate(params) { |p| list_mcp_servers(gateway_id, **p) }
|
|
126
|
+
block ? enum.each(&block) : enum
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Register an upstream MCP server. KnoxCall proxies it at the returned
|
|
130
|
+
# "connect_url" and governs every tool call before it reaches the upstream.
|
|
131
|
+
#
|
|
132
|
+
# body: name:, slug:, upstream_url:, description?, transport?,
|
|
133
|
+
# allowed_tools?, pii_inspection?, auth?.
|
|
134
|
+
#
|
|
135
|
+
# upstream_url must be a public https:// address — private, loopback,
|
|
136
|
+
# link-local and cloud-metadata destinations are refused, because the
|
|
137
|
+
# request carries your decrypted upstream credential. Every value in
|
|
138
|
+
# auth[:headers] must reference a KnoxCall secret, e.g.
|
|
139
|
+
# {Authorization: "Bearer {{secret_id:<uuid>}}"}; a literal credential is
|
|
140
|
+
# refused with 422. allowed_tools EMPTY means the server advertises
|
|
141
|
+
# nothing. server_type "collection" is not accepted — the data plane does
|
|
142
|
+
# not serve it yet.
|
|
143
|
+
#
|
|
144
|
+
# The result carries BOTH "connect_url" (where an MCP client points) and
|
|
145
|
+
# "resource" (the RFC 8707 value a token for it must be bound to).
|
|
146
|
+
def create_mcp_server(gateway_id, **input) = unwrap(@client.request("POST", "/v1/ai-gateway/gateways/#{encode(gateway_id)}/mcp-servers", body: input))
|
|
147
|
+
# MCP servers are addressed by their own id once created.
|
|
148
|
+
def get_mcp_server(server_id) = unwrap(@client.request("GET", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}"))
|
|
149
|
+
# patch: name?, description?, upstream_url?, transport?, allowed_tools?,
|
|
150
|
+
# pii_inspection?, auth?, status? ("active"|"paused"; DELETE archives).
|
|
151
|
+
def update_mcp_server(server_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}", body: patch))
|
|
152
|
+
# Returns {"id", "status"}.
|
|
153
|
+
def delete_mcp_server(server_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}"))
|
|
154
|
+
|
|
155
|
+
# -- MCP tools ---------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
# Paginated tool metadata rows. What a client can actually call is the
|
|
158
|
+
# intersection of the server's allowed_tools, the upstream's real tools
|
|
159
|
+
# and the token's own tool scope.
|
|
160
|
+
def list_mcp_tools(server_id, **params) = @client.request("GET", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/tools", query: params.empty? ? nil : params)
|
|
161
|
+
|
|
162
|
+
# Yield every tool row for a server. Lazy Enumerator when no block given.
|
|
163
|
+
def each_mcp_tool(server_id, **params, &block)
|
|
164
|
+
enum = paginate(params) { |p| list_mcp_tools(server_id, **p) }
|
|
165
|
+
block ? enum.each(&block) : enum
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Insert or update a tool row by tool_name.
|
|
169
|
+
# body: tool_name:, description?, input_schema?, enabled?.
|
|
170
|
+
def upsert_mcp_tool(server_id, **input) = unwrap(@client.request("POST", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/tools", body: input))
|
|
171
|
+
# patch: description?, input_schema?, enabled?.
|
|
172
|
+
def update_mcp_tool(server_id, tool_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/tools/#{encode(tool_id)}", body: patch))
|
|
173
|
+
# Returns {"id", "deleted"}.
|
|
174
|
+
def delete_mcp_tool(server_id, tool_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/tools/#{encode(tool_id)}"))
|
|
175
|
+
|
|
176
|
+
# -- Delegated-OAuth connections (AIGW-190) ------------------------------
|
|
177
|
+
#
|
|
178
|
+
# A connection holds ONE person's upstream refresh token, envelope-encrypted
|
|
179
|
+
# under the tenant key. Nothing here returns it, redacted or otherwise.
|
|
180
|
+
#
|
|
181
|
+
# There is deliberately no #connect_mcp_server: consent has to be given by
|
|
182
|
+
# the person whose credential it is, so the flow starts from a signed-in
|
|
183
|
+
# KnoxCall session in the admin console. An API key is not a person.
|
|
184
|
+
|
|
185
|
+
# One page of the people who have connected their upstream account to this
|
|
186
|
+
# MCP server. Params: page, per_page. Returns the {data, meta} envelope.
|
|
187
|
+
def list_mcp_grants(server_id, **params) = @client.request("GET", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/grants", query: params.empty? ? nil : params)
|
|
188
|
+
|
|
189
|
+
# Yield every connection on this server, walking pages transparently.
|
|
190
|
+
# Returns a lazy Enumerator when no block is given.
|
|
191
|
+
def each_mcp_grant(server_id, **params, &block)
|
|
192
|
+
enum = paginate(params) { |p| list_mcp_grants(server_id, **p) }
|
|
193
|
+
block ? enum.each(&block) : enum
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Revoke ONE person's connection. The stored tokens are destroyed.
|
|
197
|
+
def revoke_mcp_grant(server_id, grant_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/grants/#{encode(grant_id)}"))
|
|
198
|
+
|
|
199
|
+
# Revoke EVERY connection on this server (offboarding in one call).
|
|
200
|
+
def revoke_all_mcp_grants(server_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/mcp-servers/#{encode(server_id)}/grants"))
|
|
201
|
+
|
|
202
|
+
# -- Tokens ------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
# Paginated; never returns plaintext (the plaintext is only in
|
|
205
|
+
# {#mint_token}'s response). Params: page, per_page. Returns the
|
|
206
|
+
# {data, meta} envelope.
|
|
207
|
+
def list_tokens(agent_id, **params) = @client.request("GET", "/v1/ai-gateway/agents/#{encode(agent_id)}/tokens", query: params.empty? ? nil : params)
|
|
208
|
+
|
|
209
|
+
# Yield every token row for an agent, walking pages transparently.
|
|
210
|
+
# Returns a lazy Enumerator when no block is given.
|
|
211
|
+
def each_token(agent_id, **params, &block)
|
|
212
|
+
enum = paginate(params) { |p| list_tokens(agent_id, **p) }
|
|
213
|
+
block ? enum.each(&block) : enum
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Mint a capability token. body: name?, kind? (agent|read|tool|oneshot),
|
|
217
|
+
# dpop_required?, dpop_jkt?, expires_in_seconds?.
|
|
218
|
+
# Defaults to 30 days when omitted; clamped to [60s, 90d]. A non-expiring token cannot be minted.
|
|
219
|
+
# Returns
|
|
220
|
+
# {"id", "name"?, "kind", "prefix", "token", "dpop_required", "expires_at"}
|
|
221
|
+
# — "token" is the plaintext credential, shown ONCE; store it now.
|
|
222
|
+
def mint_token(agent_id, **input) = unwrap(@client.request("POST", "/v1/ai-gateway/agents/#{encode(agent_id)}/tokens", body: input))
|
|
223
|
+
# Returns {"id", "revoked" => true}.
|
|
224
|
+
def revoke_token(agent_id, token_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/agents/#{encode(agent_id)}/tokens/#{encode(token_id)}"))
|
|
225
|
+
|
|
226
|
+
# -- Firewall policies --------------------------------------------------
|
|
227
|
+
#
|
|
228
|
+
# Prompt-firewall policies are TENANT-scoped and shared across gateways;
|
|
229
|
+
# attach one to an agent with firewall_policy_id. An agent with no policy
|
|
230
|
+
# still runs the built-in prompt-injection patterns but can never exceed
|
|
231
|
+
# "warn" — attach a policy with action: "block" to have matching requests
|
|
232
|
+
# refused with HTTP 400 firewall_block on the data plane.
|
|
233
|
+
|
|
234
|
+
# Paginated. Params: page, per_page. Returns the {data, meta} envelope.
|
|
235
|
+
def list_firewall_policies(**params) = @client.request("GET", "/v1/ai-gateway/firewall-policies", query: params.empty? ? nil : params)
|
|
236
|
+
|
|
237
|
+
# Yield every firewall policy, walking pages transparently.
|
|
238
|
+
def each_firewall_policy(**params, &block)
|
|
239
|
+
enum = paginate(params) { |p| list_firewall_policies(**p) }
|
|
240
|
+
block ? enum.each(&block) : enum
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# body: name:, heuristics? ([{name:, kind: "regex"|"keyword", pattern:,
|
|
244
|
+
# flags?}]), canary_enabled?, action? ("block"|"warn"|"tag", default
|
|
245
|
+
# "warn"). Re-using an existing name creates version N+1.
|
|
246
|
+
#
|
|
247
|
+
# Every regex rule is compiled server-side with the same linear-time
|
|
248
|
+
# engine the data plane runs, so lookahead/lookbehind/backreferences are a
|
|
249
|
+
# 400 here rather than a rule that silently matches nothing at scan time.
|
|
250
|
+
def create_firewall_policy(**input) = unwrap(@client.request("POST", "/v1/ai-gateway/firewall-policies", body: input))
|
|
251
|
+
|
|
252
|
+
def get_firewall_policy(policy_id) = unwrap(@client.request("GET", "/v1/ai-gateway/firewall-policies/#{encode(policy_id)}"))
|
|
253
|
+
|
|
254
|
+
# Updates in place — the version is NOT bumped. Rules are re-validated.
|
|
255
|
+
def update_firewall_policy(policy_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/firewall-policies/#{encode(policy_id)}", body: patch))
|
|
256
|
+
|
|
257
|
+
# Refused with 409 policy_in_use while any agent or MCP server is still
|
|
258
|
+
# attached. Returns {"id", "deleted" => true}.
|
|
259
|
+
def delete_firewall_policy(policy_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/firewall-policies/#{encode(policy_id)}"))
|
|
260
|
+
|
|
261
|
+
# Dry-run rules against sample text; saves nothing. Rules are compiled
|
|
262
|
+
# first, so this refuses exactly what create/update refuse. Returns
|
|
263
|
+
# {"matched", "matches" => [{"rule", "span", "matched"}], "skipped" => []}.
|
|
264
|
+
def test_firewall_rules(text:, heuristics: nil)
|
|
265
|
+
body = { text: text }
|
|
266
|
+
body[:heuristics] = heuristics unless heuristics.nil?
|
|
267
|
+
unwrap(@client.request("POST", "/v1/ai-gateway/firewall-policies/test", body: body))
|
|
268
|
+
end
|
|
269
|
+
# Every token under a gateway, INCLUDING gateway-level tokens with no
|
|
270
|
+
# agent (the shape POST /v1/oauth/token mints for MCP). {#list_tokens}
|
|
271
|
+
# filters on the agent and cannot see them. Plaintext is never returned.
|
|
272
|
+
def list_gateway_tokens(gateway_id, **params) = @client.request("GET", "/v1/ai-gateway/gateways/#{encode(gateway_id)}/tokens", query: params.empty? ? nil : params)
|
|
273
|
+
|
|
274
|
+
# Yield every token under a gateway. Lazy Enumerator when no block given.
|
|
275
|
+
def each_gateway_token(gateway_id, **params, &block)
|
|
276
|
+
enum = paginate(params) { |p| list_gateway_tokens(gateway_id, **p) }
|
|
277
|
+
block ? enum.each(&block) : enum
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Revoke any token under a gateway, including a gateway-level one. Use
|
|
281
|
+
# this rather than {#revoke_token} for a token minted by
|
|
282
|
+
# POST /v1/oauth/token: that token has no agent, so the per-agent revoke
|
|
283
|
+
# can never match it. Returns {"id", "revoked"}.
|
|
284
|
+
def revoke_gateway_token(gateway_id, token_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/gateways/#{encode(gateway_id)}/tokens/#{encode(token_id)}"))
|
|
285
|
+
|
|
286
|
+
# -- PII policies -------------------------------------------------------
|
|
287
|
+
#
|
|
288
|
+
# PII redaction policies are TENANT-scoped and shared across gateways,
|
|
289
|
+
# exactly like firewall policies; attach one to an agent with
|
|
290
|
+
# pii_redact_policy_id. Until AIGW-160 they lived only on the admin plane,
|
|
291
|
+
# so {#create_agent} accepted a pii_redact_policy_id that no /v1 call
|
|
292
|
+
# could produce.
|
|
293
|
+
#
|
|
294
|
+
# A policy row is {"id", "tenant_id", "name", "version",
|
|
295
|
+
# "recognizer_ids" => [uuid], "default_action" ("redact"|"tokenize"|
|
|
296
|
+
# "whitelist"|"warn"), "description" (may be nil), "created_at"}.
|
|
297
|
+
|
|
298
|
+
# Paginated. Params: page, per_page. Returns the {data, meta} envelope.
|
|
299
|
+
def list_pii_policies(**params) = @client.request("GET", "/v1/ai-gateway/pii-policies", query: params.empty? ? nil : params)
|
|
300
|
+
|
|
301
|
+
# Yield every PII policy, walking pages transparently. Returns a lazy
|
|
302
|
+
# Enumerator when no block is given.
|
|
303
|
+
def each_pii_policy(**params, &block)
|
|
304
|
+
enum = paginate(params) { |p| list_pii_policies(**p) }
|
|
305
|
+
block ? enum.each(&block) : enum
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# body: name:, recognizer_ids? ([uuid]), default_action? (default
|
|
309
|
+
# "redact"), description?.
|
|
310
|
+
#
|
|
311
|
+
# An EMPTY recognizer_ids means "every enabled recognizer this tenant
|
|
312
|
+
# owns", NOT "none" — omitting the field gives you the WIDEST policy, not
|
|
313
|
+
# an inert one. Every id you do list must be a recognizer this tenant
|
|
314
|
+
# owns: a foreign or unknown id is a 400 recognizer_not_found at write
|
|
315
|
+
# time, rather than a stored value that resolves to nothing at scan time
|
|
316
|
+
# and quietly runs fewer detectors than the policy names.
|
|
317
|
+
def create_pii_policy(**input) = unwrap(@client.request("POST", "/v1/ai-gateway/pii-policies", body: input))
|
|
318
|
+
|
|
319
|
+
def get_pii_policy(policy_id) = unwrap(@client.request("GET", "/v1/ai-gateway/pii-policies/#{encode(policy_id)}"))
|
|
320
|
+
|
|
321
|
+
# Updates in place — the version is NOT bumped. patch: recognizer_ids?,
|
|
322
|
+
# default_action?, description?. recognizer_ids is re-validated the same
|
|
323
|
+
# way, and an empty array still means "every enabled recognizer".
|
|
324
|
+
def update_pii_policy(policy_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/pii-policies/#{encode(policy_id)}", body: patch))
|
|
325
|
+
|
|
326
|
+
# Refused with 409 policy_in_use while any agent still references it. The
|
|
327
|
+
# foreign key is ON DELETE SET NULL, so an unchecked delete would detach
|
|
328
|
+
# every bound agent and turn redaction OFF for each of them with no error
|
|
329
|
+
# anywhere. Detach the agents first. Returns {"id", "deleted" => true}.
|
|
330
|
+
def delete_pii_policy(policy_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/pii-policies/#{encode(policy_id)}"))
|
|
331
|
+
|
|
332
|
+
# -- PII recognizers ----------------------------------------------------
|
|
333
|
+
#
|
|
334
|
+
# A recognizer is one tenant-defined detector: {"id", "tenant_id", "name",
|
|
335
|
+
# "kind" ("regex"|"aho_corasick"|"presidio_pattern"|"presidio_ner"|
|
|
336
|
+
# "presidio_custom"), "pattern", "context_words" => [String] (words that
|
|
337
|
+
# must appear nearby for a match to count), "confidence" (Float),
|
|
338
|
+
# "action", "format" (token format for "tokenize", nil otherwise),
|
|
339
|
+
# "enabled", "created_at"}. enabled: false mutes a recognizer without
|
|
340
|
+
# losing its definition.
|
|
341
|
+
|
|
342
|
+
# Paginated. Params: page, per_page. Returns the {data, meta} envelope.
|
|
343
|
+
def list_pii_recognizers(**params) = @client.request("GET", "/v1/ai-gateway/pii-recognizers", query: params.empty? ? nil : params)
|
|
344
|
+
|
|
345
|
+
# Yield every PII recognizer, walking pages transparently. Returns a lazy
|
|
346
|
+
# Enumerator when no block is given.
|
|
347
|
+
def each_pii_recognizer(**params, &block)
|
|
348
|
+
enum = paginate(params) { |p| list_pii_recognizers(**p) }
|
|
349
|
+
block ? enum.each(&block) : enum
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# body: name:, kind:, pattern:, context_words?, confidence? (0-1, default
|
|
353
|
+
# 0.85), action?, format?, enabled?.
|
|
354
|
+
#
|
|
355
|
+
# A "regex" pattern is compiled server-side with the same linear-time
|
|
356
|
+
# engine the data plane runs, so lookahead, lookbehind and backreferences
|
|
357
|
+
# are a 400 here rather than a recognizer that is silently skipped at scan
|
|
358
|
+
# time (fail open). action "whitelist" exempts the matched shape from
|
|
359
|
+
# every OTHER detector, so a whitelist pattern that matches arbitrary text
|
|
360
|
+
# is a kill switch for the built-in tier and the server refuses it.
|
|
361
|
+
def create_pii_recognizer(**input) = unwrap(@client.request("POST", "/v1/ai-gateway/pii-recognizers", body: input))
|
|
362
|
+
|
|
363
|
+
# Dry-run a candidate pattern against sample text; saves nothing.
|
|
364
|
+
# +pattern+ and +text+ are required; kind:, action:, context_words: and
|
|
365
|
+
# name: are optional.
|
|
366
|
+
#
|
|
367
|
+
# It compiles with the SAME engine the data plane runs, so a pattern that
|
|
368
|
+
# passes here is one that will actually execute. Do NOT preview with a
|
|
369
|
+
# local Regexp: Ruby accepts lookahead, lookbehind and backreferences the
|
|
370
|
+
# server refuses, so a local preview shows matches for a recognizer that
|
|
371
|
+
# can never run and then 400s on save.
|
|
372
|
+
#
|
|
373
|
+
# Returns {"matched", "matches" => [{"span" => [start, stop], "matched",
|
|
374
|
+
# "replacement", "entity_type"}]}.
|
|
375
|
+
def test_pii_recognizer(pattern:, text:, kind: nil, action: nil, context_words: nil, name: nil)
|
|
376
|
+
body = { pattern: pattern, text: text }
|
|
377
|
+
body[:kind] = kind unless kind.nil?
|
|
378
|
+
body[:action] = action unless action.nil?
|
|
379
|
+
body[:context_words] = context_words unless context_words.nil?
|
|
380
|
+
body[:name] = name unless name.nil?
|
|
381
|
+
unwrap(@client.request("POST", "/v1/ai-gateway/pii-recognizers/test", body: body))
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def get_pii_recognizer(recognizer_id) = unwrap(@client.request("GET", "/v1/ai-gateway/pii-recognizers/#{encode(recognizer_id)}"))
|
|
385
|
+
|
|
386
|
+
# The server validates the MERGED state, not the patch, so
|
|
387
|
+
# action: "whitelist" on its own is still checked against the STORED
|
|
388
|
+
# pattern. patch: any of the create fields.
|
|
389
|
+
def update_pii_recognizer(recognizer_id, **patch) = unwrap(@client.request("PATCH", "/v1/ai-gateway/pii-recognizers/#{encode(recognizer_id)}", body: patch))
|
|
390
|
+
|
|
391
|
+
# Refused with 409 recognizer_in_use while any PII policy still lists it:
|
|
392
|
+
# an empty recognizer_ids means "every enabled recognizer", so dropping
|
|
393
|
+
# the id would WIDEN each listing policy rather than shrink it. Remove it
|
|
394
|
+
# from every policy first. Returns {"id", "deleted" => true}.
|
|
395
|
+
def delete_pii_recognizer(recognizer_id) = unwrap(@client.request("DELETE", "/v1/ai-gateway/pii-recognizers/#{encode(recognizer_id)}"))
|
|
396
|
+
|
|
397
|
+
# -- Usage -------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
# Cost/token usage rollup. Params: period ("7d"|"30d"|"90d"), agent_id?.
|
|
400
|
+
# Returns {"period_days", "by_model" => [{"provider", "model", "requests",
|
|
401
|
+
# "input_tokens", "output_tokens", "cost_usd", "unpriced_requests"}],
|
|
402
|
+
# "totals" => {...}}.
|
|
403
|
+
def usage(**params) = unwrap(@client.request("GET", "/v1/ai-gateway/usage", query: params.empty? ? nil : params))
|
|
404
|
+
|
|
405
|
+
# FinOps export: aggregated spend grouped by user|team|agent|model|
|
|
406
|
+
# provider|"tag:<key>", over a period. +group_by+ is required; +period+
|
|
407
|
+
# ("7d"|"30d"|"90d") and +agent_id+ are optional. The SDK always sends
|
|
408
|
+
# format=json (nil optionals are dropped by the client's query compaction).
|
|
409
|
+
# Returns {"group_by", "period_days", "rows" => [{"group", "requests",
|
|
410
|
+
# "input_tokens", "output_tokens", "cost_usd", "unpriced_requests"}]}.
|
|
411
|
+
def export_usage(group_by:, period: nil, agent_id: nil)
|
|
412
|
+
query = { group_by: group_by, period: period, agent_id: agent_id, format: "json" }
|
|
413
|
+
unwrap(@client.request("GET", "/v1/ai-gateway/usage/export", query: query))
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module KnoxCall
|
|
2
|
+
module Resources
|
|
3
|
+
class ApiKeys
|
|
4
|
+
include UnwrapsEnvelope
|
|
5
|
+
|
|
6
|
+
def initialize(client) = @client = client
|
|
7
|
+
|
|
8
|
+
# Paginated. Params: page (default 1), per_page (default 20, max 100).
|
|
9
|
+
# Returns the {data, meta} envelope.
|
|
10
|
+
def list(**params) = @client.request("GET", "/v1/api-keys", query: params.empty? ? nil : params)
|
|
11
|
+
|
|
12
|
+
# Yield every API key, walking pages transparently. Returns a lazy
|
|
13
|
+
# Enumerator when no block is given.
|
|
14
|
+
def each(**params, &block)
|
|
15
|
+
enum = paginate(params) { |p| list(**p) }
|
|
16
|
+
block ? enum.each(&block) : enum
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The response's "api_key" plaintext is shown exactly once — store it now.
|
|
20
|
+
#
|
|
21
|
+
# `role_ids:` (array of role UUIDs) attaches permission roles in the SAME
|
|
22
|
+
# transaction as the key. Discover them with
|
|
23
|
+
# `client.roles.list(subject_kind: "api_key")`. A key created with no role
|
|
24
|
+
# is default-denied on every policy-gated endpoint.
|
|
25
|
+
#
|
|
26
|
+
# A key can never mint a key more privileged than itself: if a requested
|
|
27
|
+
# role grants something this credential does not hold, the server answers
|
|
28
|
+
# 403 privilege_escalation (a PermissionDeniedError) naming the offending
|
|
29
|
+
# grant verbatim.
|
|
30
|
+
def create(**input) = unwrap(@client.request("POST", "/v1/api-keys", body: input))
|
|
31
|
+
# Returns {"revoked" => true}.
|
|
32
|
+
def revoke(key_id) = unwrap(@client.request("DELETE", "/v1/api-keys/#{encode(key_id)}"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module KnoxCall
|
|
2
|
+
module Resources
|
|
3
|
+
class AuditLogs
|
|
4
|
+
include UnwrapsEnvelope
|
|
5
|
+
|
|
6
|
+
def initialize(client) = @client = client
|
|
7
|
+
|
|
8
|
+
# Paginated. Params: page (default 1), per_page (default 20, max 100),
|
|
9
|
+
# plus endpoint filters. Returns the {data, meta} envelope.
|
|
10
|
+
def list(**params) = @client.request("GET", "/v1/audit-logs", query: params.empty? ? nil : params)
|
|
11
|
+
|
|
12
|
+
# Yield every audit-log row, walking pages transparently. Returns a
|
|
13
|
+
# lazy Enumerator when no block is given.
|
|
14
|
+
def each(**params, &block)
|
|
15
|
+
enum = paginate(params) { |p| list(**p) }
|
|
16
|
+
block ? enum.each(&block) : enum
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# One page of the keyset audit event feed — the endpoint a SIEM shipper
|
|
20
|
+
# should use. Params: +cursor+, +limit+, +action+, +action_prefix+,
|
|
21
|
+
# +resource_type+.
|
|
22
|
+
#
|
|
23
|
+
# {#list} is offset-paginated over +created_at DESC+, which is right for
|
|
24
|
+
# a console and wrong for a feed: rows written while you page shift the
|
|
25
|
+
# offsets underneath you, so events are skipped or repeated with no way
|
|
26
|
+
# to tell which. This is ordered by a monotonic sequence and resumes from
|
|
27
|
+
# an opaque cursor.
|
|
28
|
+
#
|
|
29
|
+
# +action+ is exact-match; +action_prefix+ subscribes to a whole SURFACE
|
|
30
|
+
# — <tt>"ai_gateway."</tt> covers every AI-gateway action INCLUDING names
|
|
31
|
+
# added after your integration was built, which exact-match cannot.
|
|
32
|
+
#
|
|
33
|
+
# Delivery is AT LEAST ONCE — dedupe on +id+. +meta.next_cursor+ is
|
|
34
|
+
# OPAQUE; pass it back verbatim.
|
|
35
|
+
def events(**params) = @client.request("GET", "/v1/audit-logs/events", query: params.empty? ? nil : params)
|
|
36
|
+
|
|
37
|
+
# Yield every audit event, walking the cursor until the feed is drained
|
|
38
|
+
# to the watermark. Returns a lazy Enumerator when no block is given.
|
|
39
|
+
def each_event(**params, &block)
|
|
40
|
+
enum = paginate_cursor(params) { |p| events(**p) }
|
|
41
|
+
block ? enum.each(&block) : enum
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module KnoxCall
|
|
2
|
+
module Resources
|
|
3
|
+
class Clients
|
|
4
|
+
include UnwrapsEnvelope
|
|
5
|
+
|
|
6
|
+
def initialize(client) = @client = client
|
|
7
|
+
|
|
8
|
+
# Paginated. Params: page (default 1), per_page (default 20, max 100).
|
|
9
|
+
# Returns the {data, meta} envelope.
|
|
10
|
+
def list(**params) = @client.request("GET", "/v1/clients", query: params.empty? ? nil : params)
|
|
11
|
+
|
|
12
|
+
# Yield every calling client, walking pages transparently. Returns a
|
|
13
|
+
# lazy Enumerator when no block is given.
|
|
14
|
+
def each(**params, &block)
|
|
15
|
+
enum = paginate(params) { |p| list(**p) }
|
|
16
|
+
block ? enum.each(&block) : enum
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(client_id) = unwrap(@client.request("GET", "/v1/clients/#{encode(client_id)}"))
|
|
20
|
+
def create(**input) = unwrap(@client.request("POST", "/v1/clients", body: input))
|
|
21
|
+
def update(client_id, **input) = unwrap(@client.request("PATCH", "/v1/clients/#{encode(client_id)}", body: input))
|
|
22
|
+
# Returns {"deleted" => true}.
|
|
23
|
+
def delete(client_id) = unwrap(@client.request("DELETE", "/v1/clients/#{encode(client_id)}"))
|
|
24
|
+
|
|
25
|
+
# Bare array — no pagination. Secret material is redacted server-side.
|
|
26
|
+
def list_credentials(client_id) = unwrap(@client.request("GET", "/v1/clients/#{encode(client_id)}/credentials"))
|
|
27
|
+
# For mtls "issue" mode the response carries a ONE-SHOT "reveal"
|
|
28
|
+
# ({certificate_pem, private_key_pem, ca_chain_pem}) — store it now.
|
|
29
|
+
def create_credential(client_id, kind:, label:, data: nil)
|
|
30
|
+
body = { kind: kind, label: label }
|
|
31
|
+
body[:data] = data if data
|
|
32
|
+
unwrap(@client.request("POST", "/v1/clients/#{encode(client_id)}/credentials", body: body))
|
|
33
|
+
end
|
|
34
|
+
def update_credential(client_id, credential_id, **input) = unwrap(@client.request("PATCH", "/v1/clients/#{encode(client_id)}/credentials/#{encode(credential_id)}", body: input))
|
|
35
|
+
# Returns {"deleted" => true}.
|
|
36
|
+
def delete_credential(client_id, credential_id) = unwrap(@client.request("DELETE", "/v1/clients/#{encode(client_id)}/credentials/#{encode(credential_id)}"))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|