k9o 1.1.1 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8266cd8220381fff27135c8ef7bdcdc6432bdaf90a98ce31ce710f1eb583882
4
- data.tar.gz: b5110855f5acd547d18180eab1b6d22a00ae25d7de1b441411ed4d2ee54cd2e2
3
+ metadata.gz: fddde5f475c121ba44d0e069107845e30e0104613b717edb809813d152e2882a
4
+ data.tar.gz: af7564a794aa6c40150a09bf38800dad60e092afb26dfdf8e918524124b4a68c
5
5
  SHA512:
6
- metadata.gz: d3d7559ed789b15ae5bf26aecf6abe623c52794cd48685d6471605c21acf73f21a73a233d80ec9cf0de6e5d6423a00f51bf1142e5e5a2045a3ba9025df08bcf7
7
- data.tar.gz: 3a14bb7e47092c45c34e350c1029598ae0ee056a58c06519c4270487ac88951a491fba6f8731d947233cc08ca166502d3f741f3faa3d0f241cd39979e3560c73
6
+ metadata.gz: c3735d8f97f9a0eacc9fa02dcf897ac0182056c606179b7cdcc104cc32ebba9ddb14b433c7522e30e4a10deadbd8b13022de1db262f4abccb2912b9edb05ac4a
7
+ data.tar.gz: f4f833edfa3f6fc6ea2d75efccfb85f09c902e0e2fb79cc43c8edad7db02bcd3817832087227005944fb4714f198ac4b0dd6f88134248698c9d2b4e3674aa1a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog — k9o (Ruby)
2
2
 
3
+ ## 1.3.0 — 2026-10-26
4
+
5
+ Every method now matches the gateway.
6
+
7
+ ### Changed
8
+
9
+ - `projects.create` takes `display_name:` (and optional `slug:`, `region:`, `custody_mode:`, ...);
10
+ `projects.get` and `projects.update` use the projects routes.
11
+ - `functions.deploy` takes `name:`, `runtime:`, `tier:` (`gateway` or `sandbox`), `artifact_key:`, `version:` and `description:`.
12
+ Inline code is gone.
13
+ - `functions.invoke` sends the payload as `{payload}`.
14
+ - `policies.create`, `update` and `validate` take a `policy:` document (plus optional `rules:`, `editor_mode:`);
15
+ `policies.list` takes only `status:`.
16
+ - `audit.query` filters are `event_type:`, `actor_id:`, `after:`, `before:`, `limit:` and `cursor:`.
17
+ - Org roles, identity providers and default model use the current routes; `smtp.set` uses `password_ref:` and `from_address:`;
18
+ `smtp.test` takes `to:` and `subject:`.
19
+ - `me.api_tokens.create` takes `label:` and `scope_manifest:` (and `ttl_seconds:`); `me.sessions.revoke` and `revoke_all` use new routes.
20
+ - `compute.llms.invoke` settings are sent flat, and the result has `content` and `usage`.
21
+ - `studio.workflows.trigger` sends the context as `triggerContext`.
22
+ - `gate.sign_out` requires the refresh token.
23
+ - The API host comes from the base URL: any `<org>.api.<apex>` host works and no apex is hardcoded.
24
+ `https://api.k9o.io` is only the default.
25
+
26
+ ### Removed
27
+
28
+ - `gate.verify_session`, `gate.hosted`, `realtime.replay` and `realtime.subscribe_poll`.
29
+ - `me.notifications`, `me.audit_log` and `me.data_export`.
30
+ - `org.groups.create`, `org.groups.delete`, `org.members.get` and `org.smtp.get`.
31
+
32
+ ### Added
33
+
34
+ - `functions.upload_artifact(bytes, kind)` uploads a build and returns the `artifactKey` for `deploy`.
35
+ - `UrlGrammar.parse_api_host` reads the org, region and apex from any gateway host.
36
+ - A contract test checks every method against the gateway's API description.
37
+
38
+ ## 1.2.0 — 2026-10-25
39
+
40
+ - The README now introduces the Kilimanjaro products (Keep, Pulse, Gate, Kibo). No change to the public API.
41
+
3
42
  ## 1.1.1 — 2026-10-25
4
43
 
5
44
  - Documentation links corrected: the documentation hub is https://docs.kilimanjaro.dev, with the website, platform API and console listed in a Links section. Package metadata points at the same places.
data/README.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  The Kilimanjaro platform SDK for Ruby.
4
4
 
5
+ Kilimanjaro is the AI-native foundation for building critical applications.
6
+ Its products are:
7
+
8
+ - Keep: AI Security and Governance
9
+ - Pulse: Data Intelligence
10
+ - Gate: Customer Identity and Access Management
11
+ - Kibo: Keys and Secrets Management
12
+
5
13
  ## Contents
6
14
 
7
15
  - [Links](#links)
@@ -95,27 +103,28 @@ The client needs a key and, for project calls, an org and project.
95
103
  | Option | What it does |
96
104
  |---|---|
97
105
  | `api_key:` | Bearer token sent with every request. |
98
- | `org_slug:` | Organization. On the default host the SDK uses `https://<org_slug>.api.k9o.io`. |
106
+ | `org_slug:` | Organization. The SDK builds `https://<org_slug>.<host>` from the platform host. |
99
107
  | `project_slug:` | Project. Required by every project-scoped namespace. |
100
108
 
101
109
  See [client options](#client-options) for every option.
102
110
 
103
111
  ### Choose a host
104
112
 
105
- Pick one of two forms.
113
+ The API host always comes from the base URL. Pick one of two forms.
106
114
 
107
- - Default platform host: pass the org slug, project slug and key.
108
- - Custom or self-hosted host: pass `base_url`, project slug and key, and no org slug.
115
+ - Default public host: pass the org slug, project slug and key.
116
+ - Any other platform, internal or air-gapped host: pass its `base_url` as well.
109
117
 
110
118
  ```ruby
111
119
  k9o = K9o.create_client(
112
- base_url: "https://k9o.internal.example.com",
120
+ base_url: "https://api.corp.internal",
121
+ org_slug: "acme",
113
122
  project_slug: "task-app",
114
123
  api_key: ENV.fetch("K9O_API_KEY")
115
124
  )
116
125
  ```
117
126
 
118
- The base URL rules are listed under [regional hosts](#regional-hosts).
127
+ This client talks to `https://acme.api.corp.internal`. See [regional hosts](#regional-hosts) for the rules.
119
128
 
120
129
  ## Running queries
121
130
 
@@ -177,9 +186,9 @@ Collection not found
177
186
  | Option | What it does |
178
187
  |---|---|
179
188
  | `api_key:` | Bearer token sent with every request. |
180
- | `org_slug:` | Organization. On the default host the SDK uses `https://<org_slug>.api.k9o.io`. |
189
+ | `org_slug:` | Organization. Prefixed to a platform host (`api.<apex>`) to form the org host. |
181
190
  | `project_slug:` | Project. Required by every project-scoped namespace. |
182
- | `base_url:` | Scheme and host only. Defaults to `https://api.k9o.io`. |
191
+ | `base_url:` | Scheme and host only. Defaults to the public host `https://api.k9o.io`. |
183
192
  | `default_headers:` | Extra headers added to every request. |
184
193
  | `http_client:` | A `Net::HTTP`-compatible object used instead of the default connection. |
185
194
 
@@ -234,9 +243,18 @@ Chain filters before `execute`. Values are matched against one column.
234
243
  | Method | What it does |
235
244
  |---|---|
236
245
  | `k9o.projects.list` | List projects you can access. |
237
- | `k9o.projects.create` | Create a project from `slug` and `name`. |
246
+ | `k9o.projects.create` | Create a project from `display_name:` (optional `slug:`, `region:`, `custody_mode:`, more). |
238
247
  | `k9o.projects.get` | Read a project by slug. |
239
- | `k9o.projects.update` | Change a project's `name`. |
248
+ | `k9o.projects.update` | Change a project by slug: `display_name:`, `slug:` (rename), `description:`, `region:`, more. |
249
+
250
+ ```ruby
251
+ project = k9o.projects.create(display_name: "Task app", slug: "task-app")
252
+ puts project.data["slug"]
253
+ ```
254
+
255
+ ```text
256
+ task-app
257
+ ```
240
258
 
241
259
  #### Gate (end-user sign-in)
242
260
 
@@ -246,11 +264,9 @@ Chain filters before `execute`. Values are matched against one column.
246
264
  |---|---|
247
265
  | `k9o.gate.sign_up` | Create an end user from email and password. |
248
266
  | `k9o.gate.sign_in` | Sign in with email and password. |
249
- | `k9o.gate.sign_out` | Revoke the session's refresh token. |
267
+ | `k9o.gate.sign_out` | Revoke a session. Requires `refresh_token:`. |
250
268
  | `k9o.gate.whoami` | Return the signed-in identity. |
251
269
  | `k9o.gate.refresh_token` | Exchange a refresh token for a new access token. |
252
- | `k9o.gate.verify_session` | Check that the current session is valid. |
253
- | `k9o.gate.hosted` | Return the hosted sign-in `signInUrl` and `rpId`. |
254
270
 
255
271
  ```ruby
256
272
  session = k9o.gate.sign_in(email: "ada@example.com", password: "correct-horse")
@@ -301,11 +317,32 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
301
317
 
302
318
  | Method | What it does |
303
319
  |---|---|
304
- | `k9o.functions.deploy` | Deploy a function from `name`, `runtime` and `code`. |
305
- | `k9o.functions.list` | List functions. |
320
+ | `k9o.functions.upload_artifact` | Upload the build (`bytes`, `kind` of `"wasm"` or `"tar"`). Returns `artifactKey`. |
321
+ | `k9o.functions.deploy` | Deploy from `name:`, `runtime:`, `tier:`, `artifact_key:`, `version:`, optional `description:`. |
322
+ | `k9o.functions.list` | List functions. Options: `status:`, `limit:`, `cursor:`. |
306
323
  | `k9o.functions.get` | Read one function by name. |
307
324
  | `k9o.functions.delete` | Remove a function. |
308
- | `k9o.functions.invoke` | Run a function with a payload. |
325
+ | `k9o.functions.invoke` | Run a function. The `payload` argument is sent as `{payload}`. |
326
+
327
+ `tier:` is `"gateway"` (WebAssembly, `"wasm"`) or `"sandbox"` (OCI image archive, `"tar"`).
328
+
329
+ ```ruby
330
+ artifact = k9o.functions.upload_artifact(File.binread("hello.wasm"), "wasm")
331
+
332
+ k9o.functions.deploy(
333
+ name: "hello",
334
+ runtime: "bun",
335
+ tier: "gateway",
336
+ artifact_key: artifact.data["artifactKey"],
337
+ version: "1.0.0"
338
+ )
339
+ result = k9o.functions.invoke("hello", { "name" => "Ada" })
340
+ puts result.data["greeting"]
341
+ ```
342
+
343
+ ```text
344
+ Hello, Ada
345
+ ```
309
346
 
310
347
  #### Migrations
311
348
 
@@ -329,11 +366,11 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
329
366
  | Method | What it does |
330
367
  |---|---|
331
368
  | `k9o.policies.current` | Read the published bundle. |
332
- | `k9o.policies.list` | List bundles. |
333
- | `k9o.policies.create` | Create a bundle from `name` and `rules`. |
334
- | `k9o.policies.validate` | Check rules without saving. |
369
+ | `k9o.policies.list` | List bundles. Only option: `status:`. |
370
+ | `k9o.policies.create` | Create a bundle from a `policy:` document (optional `rules:`, `editor_mode:`). |
371
+ | `k9o.policies.validate` | Check a `policy:` document (and optional `rules:`) without saving. |
335
372
  | `k9o.policies.get` | Read one bundle. |
336
- | `k9o.policies.update` | Change a bundle. |
373
+ | `k9o.policies.update` | Replace a bundle's `policy:` (optional `rules:`, `editor_mode:`). |
337
374
  | `k9o.policies.delete` | Remove a bundle. |
338
375
  | `k9o.policies.publish` | Make a bundle current. |
339
376
 
@@ -343,7 +380,9 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
343
380
 
344
381
  | Method | What it does |
345
382
  |---|---|
346
- | `k9o.audit.query` | Search events by time range, type and more. |
383
+ | `k9o.audit.query` | Filter by `event_type:`, `actor_id:`, `after:`, `before:`, `limit:`, `cursor:`. |
384
+
385
+ `after:` and `before:` are ISO 8601 timestamps.
347
386
 
348
387
  #### AI embeddings
349
388
 
@@ -362,7 +401,18 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
362
401
  | `k9o.compute.llms.list` | List available models. |
363
402
  | `k9o.compute.llms.read` | Read one model. |
364
403
  | `k9o.compute.llms.invoke` | Send a prompt and get the reply. |
365
- | `k9o.compute.llms.invoke_stream` | Stream the reply token by token (returns an `Enumerator`). |
404
+ | `k9o.compute.llms.invoke_stream` | Stream the reply chunk by chunk (returns an `Enumerator`). |
405
+
406
+ Pass settings in `params:` (`max_tokens`, `temperature`, `usage_type`). The result `data` has `content` and `usage`.
407
+
408
+ ```ruby
409
+ reply = k9o.compute.llms.invoke("llama-3-8b", prompt: "Say hi", params: { max_tokens: 20 })
410
+ puts "#{reply.data['content']} #{reply.data['usage']['totalTokens']}"
411
+ ```
412
+
413
+ ```text
414
+ Hi there! 14
415
+ ```
366
416
 
367
417
  #### Realtime
368
418
 
@@ -370,9 +420,7 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
370
420
 
371
421
  | Method | What it does |
372
422
  |---|---|
373
- | `k9o.realtime.subscribe` | Stream live events (returns an `Enumerator`). |
374
- | `k9o.realtime.replay` | Read past events from a sequence number. |
375
- | `k9o.realtime.subscribe_poll` | Poll for events from a sequence number. |
423
+ | `k9o.realtime.subscribe` | Stream live events over SSE (returns an `Enumerator`). |
376
424
 
377
425
  `K9o::Realtime::Topic` builds topic names, for example `Topic.collection("tasks")`.
378
426
 
@@ -389,7 +437,7 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
389
437
  | `k9o.studio.workflows.read` | Read one workflow. |
390
438
  | `k9o.studio.workflows.update` | Change a workflow. |
391
439
  | `k9o.studio.workflows.delete` | Delete a workflow. |
392
- | `k9o.studio.workflows.trigger` | Start a run. |
440
+ | `k9o.studio.workflows.trigger` | Start a run, with an optional context hash. |
393
441
  | `k9o.studio.workflows.runs.list` | List a workflow's runs. |
394
442
  | `k9o.studio.workflows.runs.read` | Read one run. |
395
443
  | `k9o.studio.workflows.runs.cancel` | Cancel a run. |
@@ -413,19 +461,13 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
413
461
  | Method | What it does |
414
462
  |---|---|
415
463
  | `k9o.me.profile.get` | Read the profile. |
416
- | `k9o.me.profile.update` | Change the display name. |
464
+ | `k9o.me.profile.update` | Change `display_name:`, `locale:` or `timezone:` (only those given). |
417
465
  | `k9o.me.sessions.list` | List active sessions. |
418
466
  | `k9o.me.sessions.revoke` | End one session. |
419
467
  | `k9o.me.sessions.revoke_all` | End all sessions. |
420
468
  | `k9o.me.api_tokens.list` | List API tokens. |
421
- | `k9o.me.api_tokens.create` | Create an API token. |
469
+ | `k9o.me.api_tokens.create` | Create a token from `label:` and `scope_manifest:` (optional `ttl_seconds:`). |
422
470
  | `k9o.me.api_tokens.delete` | Delete an API token. |
423
- | `k9o.me.notifications.list` | List notifications. |
424
- | `k9o.me.notifications.mark_read` | Mark one as read. |
425
- | `k9o.me.notifications.mark_all_read` | Mark all as read. |
426
- | `k9o.me.audit_log.query` | Search your own audit events. |
427
- | `k9o.me.data_export.request` | Request a data export. |
428
- | `k9o.me.data_export.status` | Check an export job. |
429
471
 
430
472
  #### Organization
431
473
 
@@ -445,30 +487,26 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
445
487
  | Method | What it does |
446
488
  |---|---|
447
489
  | `k9o.org.get` | Read the active organization. |
448
- | `k9o.org.members.list` | List members. |
490
+ | `k9o.org.members.list` | List members. Options: `user_id:`, `role:`, `acceptance_status:`, `status:`, `state:`. |
449
491
  | `k9o.org.members.invite` | Invite a member by email and role. |
450
- | `k9o.org.members.get` | Read one member. |
451
492
  | `k9o.org.members.remove` | Remove a member. |
452
- | `k9o.org.groups.list` | List groups. |
453
- | `k9o.org.groups.create` | Create a group. |
493
+ | `k9o.org.groups.list` | List groups, optionally for one `user_id:`. |
454
494
  | `k9o.org.groups.get` | Read one group. |
455
- | `k9o.org.groups.delete` | Delete a group. |
456
- | `k9o.org.audit_log.query` | Search organization audit events. |
495
+ | `k9o.org.audit_log.query` | Search organization audit events by `type:` and `limit:`. |
457
496
  | `k9o.org.gate.roles.list` | List role grants. |
458
- | `k9o.org.gate.roles.grant` | Grant a role to a user. |
497
+ | `k9o.org.gate.roles.grant` | Grant a role to a user (`user_id:`, `role:`, optional `decision_reason:`). |
459
498
  | `k9o.org.gate.roles.get` | Read one grant. |
460
499
  | `k9o.org.gate.roles.revoke` | Revoke a grant. |
461
500
  | `k9o.org.gate.idp_providers.list` | List identity providers. |
462
- | `k9o.org.gate.idp_providers.create` | Add an identity provider. |
501
+ | `k9o.org.gate.idp_providers.create` | Add a provider: `idp_provider:`, `display_name:`, `client_id:`, `client_secret_ref:`, `tier_gating:`. |
463
502
  | `k9o.org.gate.idp_providers.get` | Read one provider. |
464
503
  | `k9o.org.gate.idp_providers.configure` | Change a provider. |
465
504
  | `k9o.org.gate.idp_providers.delete` | Remove a provider. |
466
- | `k9o.org.smtp.get` | Read the mail server settings. |
467
- | `k9o.org.smtp.set` | Save the mail server settings. |
468
- | `k9o.org.smtp.test` | Send a test email. |
505
+ | `k9o.org.smtp.set` | Save the mail server: `host:`, `port:`, `username:`, `password_ref:`, `from_address:`. |
506
+ | `k9o.org.smtp.test` | Send a test email to `to:`, with an optional `subject:`. |
469
507
  | `k9o.org.smtp.unset` | Remove the mail server settings. |
470
508
  | `k9o.org.default_llm.get` | Read the default model. |
471
- | `k9o.org.default_llm.set` | Set the default model. |
509
+ | `k9o.org.default_llm.set` | Set the default model from a model id. |
472
510
 
473
511
  #### Vault secrets
474
512
 
@@ -477,7 +515,7 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
477
515
  | Method | What it does |
478
516
  |---|---|
479
517
  | `k9o.kibo.vault.secrets.list` | List secrets by prefix. |
480
- | `k9o.kibo.vault.secrets.get` | Read a secret's details. |
518
+ | `k9o.kibo.vault.secrets.get` | Read a secret by name (`region:` is required). |
481
519
  | `k9o.kibo.vault.secrets.create` | Store a new secret. |
482
520
  | `k9o.kibo.vault.secrets.reveal_challenge` | Get a one-time challenge for revealing. |
483
521
  | `k9o.kibo.vault.secrets.reveal` | Reveal the value using the challenge. |
@@ -532,10 +570,12 @@ The server may send codes not listed here, so keep a fallback branch.
532
570
 
533
571
  ### Regional hosts
534
572
 
535
- - Organization hosts such as `https://acme.api.k9o.io` are region-free.
536
- - The SDK sends one credential-free `HEAD` request to learn the region.
537
- - Later requests go to the regional host.
538
- - Any other base URL is used exactly as given.
573
+ - A platform host is `api.<apex>` or `api-staging.<apex>`, on any apex.
574
+ - With `org_slug`, the SDK builds the org host `<org_slug>.<host>`.
575
+ - A region-free org host gets one credential-free `HEAD` request to learn its region.
576
+ - Later requests go to the regional host, which must share the org, family and apex.
577
+ - Any other base URL (internal, air-gapped, localhost) is used exactly as given.
578
+ - `https://api.k9o.io` is only the default when `base_url:` is omitted.
539
579
 
540
580
  ### Related packages
541
581
 
data/lib/k9o/audit.rb CHANGED
@@ -2,27 +2,25 @@
2
2
 
3
3
  module K9o
4
4
  # Project-plane audit log namespace — k9o.audit.*.
5
- # Distinct from k9o.org.audit_log + k9o.me.audit_log.
5
+ # Distinct from k9o.org.audit_log.
6
6
  class Audit
7
7
  def initialize(config)
8
8
  @config = config
9
9
  end
10
10
 
11
- # Query the audit log (reverse-chronological; filters AND-compose).
12
- # Accepts keyword filters: since/until/type/actor_id/target_kind/
13
- # target_id/limit/cursor (wire camelCase mapping applied here).
14
- def query(since: nil, until_: nil, type: nil, actor_id: nil,
15
- target_kind: nil, target_id: nil, limit: nil, cursor: nil)
11
+ # GET /@<slug>/audit-log — reverse-chronological; filters AND-compose. Requires the AUDIT_READER role.
12
+ # event_type: audit-event name prefix (e.g. 'data.write.access') actor_id: filter by actor
13
+ # after / before: ISO 8601 bounds (events after / before this instant)
14
+ # limit: page size (bounded server-side) cursor: from the prior response's meta.cursor.next
15
+ def query(event_type: nil, actor_id: nil, after: nil, before: nil, limit: nil, cursor: nil)
16
16
  slug = Studio.require_project_slug(@config, 'audit')
17
17
  q = {}
18
- q['since'] = since if since
19
- q['until'] = until_ if until_
20
- q['type'] = type if type
21
- q['actorId'] = actor_id if actor_id
22
- q['targetKind'] = target_kind if target_kind
23
- q['targetId'] = target_id if target_id
24
- q['limit'] = limit if limit
25
- q['cursor'] = cursor if cursor
18
+ q['eventType'] = event_type unless event_type.nil?
19
+ q['actorId'] = actor_id unless actor_id.nil?
20
+ q['after'] = after unless after.nil?
21
+ q['before'] = before unless before.nil?
22
+ q['limit'] = limit unless limit.nil?
23
+ q['cursor'] = cursor unless cursor.nil?
26
24
  HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'audit-log'), query: q)
27
25
  end
28
26
  end
data/lib/k9o/client.rb CHANGED
@@ -36,17 +36,15 @@ module K9o
36
36
 
37
37
  # Factory — construct a K9oClient bound to the supplied options.
38
38
  #
39
- # Per spec sample:
40
39
  # k9o = K9o.create_client(
41
40
  # api_key: ENV["K9O_API_KEY"],
42
41
  # org_slug: "acme",
43
42
  # project_slug: "member-portal",
44
43
  # )
45
44
  #
46
- # base_url is OPTIONAL. When omitted it
47
- # defaults to the platform apex https://api.k9o.io; when the host is the
48
- # bare apex and org_slug is set, the canonical org-subdomain wire root
49
- # https://<org_slug>.api.k9o.io is composed. Any other host passes through.
45
+ # base_url is OPTIONAL. When omitted it defaults to the public-cloud platform host (a convenience only, never used
46
+ # to parse a host). When the host is a platform host (+api.<apex>+ or +api-staging.<apex>+, any apex) and org_slug
47
+ # is set, the org host +<org_slug>.<host>+ is composed. Any other host passes through unchanged.
50
48
  def self.create_client(base_url: nil, api_key: nil, org_slug: nil, project_slug: nil, default_headers: {}, http_client: nil)
51
49
  normalised = UrlGrammar.resolve_base_url(base_url, org_slug)
52
50
  config = HttpClientConfig.new(
data/lib/k9o/compute.rb CHANGED
@@ -4,30 +4,37 @@ module K9o
4
4
  # Compute namespace (Compute LLMs registry).
5
5
  # k9o.compute.llms.*
6
6
  class Compute
7
- # Curated 9-model closed enum (extensible v2.x).
7
+ # The curated model ids (closed enum).
8
8
  LLM_MODEL_IDS = %w[
9
9
  mistral-7b mistral-8x7b llama-3-8b llama-3-70b
10
10
  claude-3-5-sonnet gpt-4o gpt-4o-mini
11
11
  text-embedding-3-small text-embedding-3-large
12
12
  ].freeze
13
13
 
14
+ # Inference parameters the gateway reads (top-level body fields next to +prompt+). Snake_case spellings map to
15
+ # the gateway's.
16
+ PARAM_FIELDS = {
17
+ 'max_tokens' => 'maxTokens', 'maxTokens' => 'maxTokens',
18
+ 'temperature' => 'temperature',
19
+ 'usage_type' => 'usageType', 'usageType' => 'usageType'
20
+ }.freeze
21
+
14
22
  attr_reader :llms
15
23
 
16
24
  def initialize(config)
17
25
  @llms = Llms.new(config)
18
26
  end
19
27
 
20
- # k9o.compute.llms.*.
28
+ # k9o.compute.llms.*
21
29
  class Llms
22
30
  def initialize(config)
23
31
  @config = config
24
32
  end
25
33
 
26
- def list(modality: nil)
34
+ # GET /@<slug>/compute/llms
35
+ def list
27
36
  slug = Studio.require_project_slug(@config, 'compute.llms')
28
- q = modality ? { 'modality' => modality } : nil
29
- HttpTransport.request(config: @config, method: 'GET',
30
- path: UrlGrammar.project_path(slug, 'compute/llms'), query: q)
37
+ HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'compute/llms'))
31
38
  end
32
39
 
33
40
  def read(model_id)
@@ -36,29 +43,39 @@ module K9o
36
43
  path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}"))
37
44
  end
38
45
 
39
- # Synchronous inference (PROJECT_AUTHOR/ADMIN).
46
+ # Synchronous inference: POST /@<slug>/compute/llms/<id>/invoke { prompt, maxTokens?, temperature?,
47
+ # usageType? }. +params+ takes max_tokens / temperature / usage_type (the gateway's spellings also work).
48
+ # The envelope's data is { 'modelId', 'content', 'finishReason', 'usage' => { 'promptTokens',
49
+ # 'completionTokens', 'totalTokens' }, 'usageType' }.
40
50
  def invoke(model_id, prompt:, params: nil)
41
51
  slug = Studio.require_project_slug(@config, 'compute.llms')
42
- body = { 'prompt' => prompt }
43
- body['params'] = params if params
44
52
  HttpTransport.request(config: @config, method: 'POST',
45
- path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke"), body: body)
53
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke"),
54
+ body: invoke_body(prompt, params))
46
55
  end
47
56
 
48
- # Streaming inference (SSE per-token). Returns an
49
- # Enumerator of chunk hashes (Ruby idiom). Consume with
57
+ # Streaming inference (SSE per-token). Returns an Enumerator of chunk hashes. Consume with
50
58
  # `.each { |chunk| ... }`.
51
59
  def invoke_stream(model_id, prompt:, params: nil)
52
60
  slug = Studio.require_project_slug(@config, 'compute.llms')
53
- body = { 'prompt' => prompt }
54
- body['params'] = params if params
55
61
  HttpTransport.stream_sse(config: @config, method: 'POST',
56
62
  path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke-stream"),
57
- body: body)
63
+ body: invoke_body(prompt, params))
58
64
  end
59
65
 
60
66
  private
61
67
 
68
+ def invoke_body(prompt, params)
69
+ body = { 'prompt' => prompt }
70
+ (params || {}).each do |key, value|
71
+ wire = PARAM_FIELDS[key.to_s]
72
+ raise ArgumentError, "unknown inference parameter: #{key}" if wire.nil?
73
+
74
+ body[wire] = value unless value.nil?
75
+ end
76
+ body
77
+ end
78
+
62
79
  def enc(s)
63
80
  ::URI.encode_www_form_component(s)
64
81
  end
data/lib/k9o/functions.rb CHANGED
@@ -2,22 +2,52 @@
2
2
 
3
3
  module K9o
4
4
  # Function lifecycle namespace — k9o.functions.*.
5
+ #
6
+ # Upload the function's bytes first (+upload_artifact+ returns the +artifactKey+), then +deploy+ references that
7
+ # artifact. Deploy requires PROJECT_ADMIN and returns with status +deploying+; the function is ready when status
8
+ # flips to +active+.
5
9
  class Functions
10
+ ARTIFACT_CONTENT_TYPES = { 'wasm' => 'application/wasm', 'tar' => 'application/x-tar' }.freeze
11
+
6
12
  def initialize(config)
7
13
  @config = config
8
14
  end
9
15
 
10
- def deploy(name:, runtime:, code:, tier: nil, config: nil)
16
+ # POST /@<slug>/functions/artifacts with the raw bytes: a WebAssembly module for the gateway tier (up to 16 MiB,
17
+ # +kind+ 'wasm') or an OCI image archive for the sandbox tier (up to 256 MiB, +kind+ 'tar'). Uploading the same
18
+ # bytes again returns the same key. The envelope's data is { 'artifactKey' => String }.
19
+ def upload_artifact(bytes, kind)
20
+ content_type = ARTIFACT_CONTENT_TYPES[kind.to_s]
21
+ raise ArgumentError, "kind must be 'wasm' or 'tar'" if content_type.nil?
22
+
23
+ bytes = bytes.read if bytes.respond_to?(:read)
24
+ raise ArgumentError, 'bytes must be a String or an IO' unless bytes.is_a?(String)
25
+
26
+ slug = require_project_slug
27
+ HttpTransport.request(config: @config, method: 'POST',
28
+ path: UrlGrammar.project_path(slug, 'functions/artifacts'),
29
+ raw_body: bytes, headers: { 'content-type' => content_type })
30
+ end
31
+
32
+ # POST /@<slug>/functions — deploy or redeploy from an uploaded artifact.
33
+ # runtime: bun python go java kotlin dotnet swift php ruby rust tinygo zig assemblyscript
34
+ # tier: 'gateway' (WebAssembly) or 'sandbox' (OCI image archive)
35
+ # artifact_key: the key +upload_artifact+ returned; version: label for this deploy (1 to 64 characters)
36
+ def deploy(name:, runtime:, tier:, artifact_key:, version:, description: nil)
11
37
  slug = require_project_slug
12
- body = { 'name' => name, 'runtime' => runtime, 'code' => code }
13
- body['tier'] = tier if tier
14
- body['config'] = config if config
38
+ body = { 'name' => name, 'runtime' => runtime, 'tier' => tier, 'artifactKey' => artifact_key,
39
+ 'version' => version }
40
+ body['description'] = description unless description.nil?
15
41
  HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.project_path(slug, 'functions'), body: body)
16
42
  end
17
43
 
18
- def list(status: nil)
44
+ # GET /@<slug>/functions (cursor pagination).
45
+ def list(status: nil, limit: nil, cursor: nil)
19
46
  slug = require_project_slug
20
- q = status ? { 'status' => status } : nil
47
+ q = {}
48
+ q['status'] = status unless status.nil?
49
+ q['limit'] = limit unless limit.nil?
50
+ q['cursor'] = cursor unless cursor.nil?
21
51
  HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'functions'), query: q)
22
52
  end
23
53
 
@@ -26,16 +56,25 @@ module K9o
26
56
  HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, "functions/#{enc(name)}"))
27
57
  end
28
58
 
59
+ # Retire a function; its name enters a 24h cooldown.
29
60
  def delete(name)
30
61
  slug = require_project_slug
31
62
  HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.project_path(slug, "functions/#{enc(name)}"))
32
63
  end
33
64
 
34
- # Invoke a function; payload passed to the runtime verbatim.
35
- def invoke(name, payload)
65
+ # POST /@<slug>/functions/<name>/invoke. +payload+ is sent as the +payload+ field of the request body and passed
66
+ # to the function runtime verbatim. Optional controls: +background+ (run asynchronously), +delegation+
67
+ # ({ 'token' => ..., 'invokeSessionId' => ... }), +on_behalf_of+, +confined_to+.
68
+ def invoke(name, payload = nil, background: nil, delegation: nil, on_behalf_of: nil, confined_to: nil)
36
69
  slug = require_project_slug
70
+ body = {}
71
+ body['payload'] = payload unless payload.nil?
72
+ body['background'] = background unless background.nil?
73
+ body['delegation'] = delegation unless delegation.nil?
74
+ body['onBehalfOf'] = on_behalf_of unless on_behalf_of.nil?
75
+ body['confinedTo'] = confined_to unless confined_to.nil?
37
76
  HttpTransport.request(config: @config, method: 'POST',
38
- path: UrlGrammar.project_path(slug, "functions/#{enc(name)}/invoke"), body: payload)
77
+ path: UrlGrammar.project_path(slug, "functions/#{enc(name)}/invoke"), body: body)
39
78
  end
40
79
 
41
80
  private
data/lib/k9o/gate.rb CHANGED
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K9o
4
- # Gate (Identity-Aware Kernel) namespace. Project-scoped.
4
+ # Gate (Identity-Aware Kernel) namespace. Project-scoped; the gateway closes each body to the fields shown.
5
5
  class Gate
6
6
  def initialize(config)
7
7
  @config = config
8
8
  end
9
9
 
10
+ # POST /@<slug>/gate/signup { email, password, displayName? }
10
11
  def sign_up(email:, password:, display_name: nil)
11
12
  body = { 'email' => email, 'password' => password }
12
13
  body['displayName'] = display_name if display_name
13
14
  HttpTransport.request(config: @config, method: 'POST', path: gate_path('signup'), body: body)
14
15
  end
15
16
 
17
+ # POST /@<slug>/gate/signin { email, password }
16
18
  def sign_in(email:, password:)
17
19
  HttpTransport.request(
18
20
  config: @config, method: 'POST', path: gate_path('signin'),
@@ -20,15 +22,18 @@ module K9o
20
22
  )
21
23
  end
22
24
 
23
- def sign_out(refresh_token: nil)
24
- body = refresh_token ? { 'refreshToken' => refresh_token } : {}
25
- HttpTransport.request(config: @config, method: 'POST', path: gate_path('signout'), body: body)
25
+ # POST /@<slug>/gate/signout { refreshToken } — the refresh token is required (the gateway answers 400 without it).
26
+ def sign_out(refresh_token:)
27
+ HttpTransport.request(config: @config, method: 'POST', path: gate_path('signout'),
28
+ body: { 'refreshToken' => refresh_token })
26
29
  end
27
30
 
31
+ # GET /@<slug>/gate/whoami
28
32
  def whoami
29
33
  HttpTransport.request(config: @config, method: 'GET', path: gate_path('whoami'))
30
34
  end
31
35
 
36
+ # POST /@<slug>/gate/refresh { refreshToken }
32
37
  def refresh_token(refresh_token:)
33
38
  HttpTransport.request(
34
39
  config: @config, method: 'POST', path: gate_path('refresh'),
@@ -36,20 +41,6 @@ module K9o
36
41
  )
37
42
  end
38
43
 
39
- def verify_session
40
- HttpTransport.request(config: @config, method: 'GET', path: gate_path('verify'))
41
- end
42
-
43
- # The project's hosted sign-in URL and WebAuthn rpId:
44
- # GET /@<slug>/gate/hosted. Unauthenticated and cacheable; no credential is
45
- # sent even when an api_key is configured. Returns an Envelope whose `data`
46
- # is { 'signInUrl' => String, 'rpId' => String }. Take both values as given:
47
- # an unknown org, a deleted project and a non-Gate project all answer the
48
- # same 404 K9O_PROJECT_NOT_FOUND.
49
- def hosted
50
- HttpTransport.request(config: @config, method: 'GET', path: gate_path('hosted'), authenticated: false)
51
- end
52
-
53
44
  private
54
45
 
55
46
  def gate_path(action)