k9o 1.2.0 → 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: 8d700cb2145cc18fc4ae7a341d859b2f6e20cb8599e00a1849296c1969453147
4
- data.tar.gz: e5d01af0e76082f72c8c96e980ccfa1f7e5f7404978dae3766cc6351e40b010d
3
+ metadata.gz: fddde5f475c121ba44d0e069107845e30e0104613b717edb809813d152e2882a
4
+ data.tar.gz: af7564a794aa6c40150a09bf38800dad60e092afb26dfdf8e918524124b4a68c
5
5
  SHA512:
6
- metadata.gz: f7d3bb66992ebfb359483234ed2b125b33a1c696a630074f0931b75ea95412cf19deaae566cf457126eb5b785f14bf30a560e000b0f3f55eed06c453b9247643
7
- data.tar.gz: f10f26cffc62c664e9c4cdf2a9ecf9345947163e84b306593d930da3f1213e14be08bcfb1cf687b14ce1a8ed9c61229f47005314ef0cbcb8728bef2fe55de24f
6
+ metadata.gz: c3735d8f97f9a0eacc9fa02dcf897ac0182056c606179b7cdcc104cc32ebba9ddb14b433c7522e30e4a10deadbd8b13022de1db262f4abccb2912b9edb05ac4a
7
+ data.tar.gz: f4f833edfa3f6fc6ea2d75efccfb85f09c902e0e2fb79cc43c8edad7db02bcd3817832087227005944fb4714f198ac4b0dd6f88134248698c9d2b4e3674aa1a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
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
+
3
38
  ## 1.2.0 — 2026-10-25
4
39
 
5
40
  - The README now introduces the Kilimanjaro products (Keep, Pulse, Gate, Kibo). No change to the public API.
data/README.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  The Kilimanjaro platform SDK for Ruby.
4
4
 
5
- Kilimanjaro is an AI-native platform on a Governed Database. Its products are Keep (AI Security and Governance), Pulse (Data Intelligence), Gate (Customer Identity and Access Management) and Kibo (Keys and Secrets Management).
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
6
12
 
7
13
  ## Contents
8
14
 
@@ -97,27 +103,28 @@ The client needs a key and, for project calls, an org and project.
97
103
  | Option | What it does |
98
104
  |---|---|
99
105
  | `api_key:` | Bearer token sent with every request. |
100
- | `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. |
101
107
  | `project_slug:` | Project. Required by every project-scoped namespace. |
102
108
 
103
109
  See [client options](#client-options) for every option.
104
110
 
105
111
  ### Choose a host
106
112
 
107
- Pick one of two forms.
113
+ The API host always comes from the base URL. Pick one of two forms.
108
114
 
109
- - Default platform host: pass the org slug, project slug and key.
110
- - 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.
111
117
 
112
118
  ```ruby
113
119
  k9o = K9o.create_client(
114
- base_url: "https://k9o.internal.example.com",
120
+ base_url: "https://api.corp.internal",
121
+ org_slug: "acme",
115
122
  project_slug: "task-app",
116
123
  api_key: ENV.fetch("K9O_API_KEY")
117
124
  )
118
125
  ```
119
126
 
120
- 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.
121
128
 
122
129
  ## Running queries
123
130
 
@@ -179,9 +186,9 @@ Collection not found
179
186
  | Option | What it does |
180
187
  |---|---|
181
188
  | `api_key:` | Bearer token sent with every request. |
182
- | `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. |
183
190
  | `project_slug:` | Project. Required by every project-scoped namespace. |
184
- | `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`. |
185
192
  | `default_headers:` | Extra headers added to every request. |
186
193
  | `http_client:` | A `Net::HTTP`-compatible object used instead of the default connection. |
187
194
 
@@ -236,9 +243,18 @@ Chain filters before `execute`. Values are matched against one column.
236
243
  | Method | What it does |
237
244
  |---|---|
238
245
  | `k9o.projects.list` | List projects you can access. |
239
- | `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). |
240
247
  | `k9o.projects.get` | Read a project by slug. |
241
- | `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
+ ```
242
258
 
243
259
  #### Gate (end-user sign-in)
244
260
 
@@ -248,11 +264,9 @@ Chain filters before `execute`. Values are matched against one column.
248
264
  |---|---|
249
265
  | `k9o.gate.sign_up` | Create an end user from email and password. |
250
266
  | `k9o.gate.sign_in` | Sign in with email and password. |
251
- | `k9o.gate.sign_out` | Revoke the session's refresh token. |
267
+ | `k9o.gate.sign_out` | Revoke a session. Requires `refresh_token:`. |
252
268
  | `k9o.gate.whoami` | Return the signed-in identity. |
253
269
  | `k9o.gate.refresh_token` | Exchange a refresh token for a new access token. |
254
- | `k9o.gate.verify_session` | Check that the current session is valid. |
255
- | `k9o.gate.hosted` | Return the hosted sign-in `signInUrl` and `rpId`. |
256
270
 
257
271
  ```ruby
258
272
  session = k9o.gate.sign_in(email: "ada@example.com", password: "correct-horse")
@@ -303,11 +317,32 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
303
317
 
304
318
  | Method | What it does |
305
319
  |---|---|
306
- | `k9o.functions.deploy` | Deploy a function from `name`, `runtime` and `code`. |
307
- | `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:`. |
308
323
  | `k9o.functions.get` | Read one function by name. |
309
324
  | `k9o.functions.delete` | Remove a function. |
310
- | `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
+ ```
311
346
 
312
347
  #### Migrations
313
348
 
@@ -331,11 +366,11 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
331
366
  | Method | What it does |
332
367
  |---|---|
333
368
  | `k9o.policies.current` | Read the published bundle. |
334
- | `k9o.policies.list` | List bundles. |
335
- | `k9o.policies.create` | Create a bundle from `name` and `rules`. |
336
- | `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. |
337
372
  | `k9o.policies.get` | Read one bundle. |
338
- | `k9o.policies.update` | Change a bundle. |
373
+ | `k9o.policies.update` | Replace a bundle's `policy:` (optional `rules:`, `editor_mode:`). |
339
374
  | `k9o.policies.delete` | Remove a bundle. |
340
375
  | `k9o.policies.publish` | Make a bundle current. |
341
376
 
@@ -345,7 +380,9 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
345
380
 
346
381
  | Method | What it does |
347
382
  |---|---|
348
- | `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.
349
386
 
350
387
  #### AI embeddings
351
388
 
@@ -364,7 +401,18 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
364
401
  | `k9o.compute.llms.list` | List available models. |
365
402
  | `k9o.compute.llms.read` | Read one model. |
366
403
  | `k9o.compute.llms.invoke` | Send a prompt and get the reply. |
367
- | `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
+ ```
368
416
 
369
417
  #### Realtime
370
418
 
@@ -372,9 +420,7 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
372
420
 
373
421
  | Method | What it does |
374
422
  |---|---|
375
- | `k9o.realtime.subscribe` | Stream live events (returns an `Enumerator`). |
376
- | `k9o.realtime.replay` | Read past events from a sequence number. |
377
- | `k9o.realtime.subscribe_poll` | Poll for events from a sequence number. |
423
+ | `k9o.realtime.subscribe` | Stream live events over SSE (returns an `Enumerator`). |
378
424
 
379
425
  `K9o::Realtime::Topic` builds topic names, for example `Topic.collection("tasks")`.
380
426
 
@@ -391,7 +437,7 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
391
437
  | `k9o.studio.workflows.read` | Read one workflow. |
392
438
  | `k9o.studio.workflows.update` | Change a workflow. |
393
439
  | `k9o.studio.workflows.delete` | Delete a workflow. |
394
- | `k9o.studio.workflows.trigger` | Start a run. |
440
+ | `k9o.studio.workflows.trigger` | Start a run, with an optional context hash. |
395
441
  | `k9o.studio.workflows.runs.list` | List a workflow's runs. |
396
442
  | `k9o.studio.workflows.runs.read` | Read one run. |
397
443
  | `k9o.studio.workflows.runs.cancel` | Cancel a run. |
@@ -415,19 +461,13 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
415
461
  | Method | What it does |
416
462
  |---|---|
417
463
  | `k9o.me.profile.get` | Read the profile. |
418
- | `k9o.me.profile.update` | Change the display name. |
464
+ | `k9o.me.profile.update` | Change `display_name:`, `locale:` or `timezone:` (only those given). |
419
465
  | `k9o.me.sessions.list` | List active sessions. |
420
466
  | `k9o.me.sessions.revoke` | End one session. |
421
467
  | `k9o.me.sessions.revoke_all` | End all sessions. |
422
468
  | `k9o.me.api_tokens.list` | List API tokens. |
423
- | `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:`). |
424
470
  | `k9o.me.api_tokens.delete` | Delete an API token. |
425
- | `k9o.me.notifications.list` | List notifications. |
426
- | `k9o.me.notifications.mark_read` | Mark one as read. |
427
- | `k9o.me.notifications.mark_all_read` | Mark all as read. |
428
- | `k9o.me.audit_log.query` | Search your own audit events. |
429
- | `k9o.me.data_export.request` | Request a data export. |
430
- | `k9o.me.data_export.status` | Check an export job. |
431
471
 
432
472
  #### Organization
433
473
 
@@ -447,30 +487,26 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
447
487
  | Method | What it does |
448
488
  |---|---|
449
489
  | `k9o.org.get` | Read the active organization. |
450
- | `k9o.org.members.list` | List members. |
490
+ | `k9o.org.members.list` | List members. Options: `user_id:`, `role:`, `acceptance_status:`, `status:`, `state:`. |
451
491
  | `k9o.org.members.invite` | Invite a member by email and role. |
452
- | `k9o.org.members.get` | Read one member. |
453
492
  | `k9o.org.members.remove` | Remove a member. |
454
- | `k9o.org.groups.list` | List groups. |
455
- | `k9o.org.groups.create` | Create a group. |
493
+ | `k9o.org.groups.list` | List groups, optionally for one `user_id:`. |
456
494
  | `k9o.org.groups.get` | Read one group. |
457
- | `k9o.org.groups.delete` | Delete a group. |
458
- | `k9o.org.audit_log.query` | Search organization audit events. |
495
+ | `k9o.org.audit_log.query` | Search organization audit events by `type:` and `limit:`. |
459
496
  | `k9o.org.gate.roles.list` | List role grants. |
460
- | `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:`). |
461
498
  | `k9o.org.gate.roles.get` | Read one grant. |
462
499
  | `k9o.org.gate.roles.revoke` | Revoke a grant. |
463
500
  | `k9o.org.gate.idp_providers.list` | List identity providers. |
464
- | `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:`. |
465
502
  | `k9o.org.gate.idp_providers.get` | Read one provider. |
466
503
  | `k9o.org.gate.idp_providers.configure` | Change a provider. |
467
504
  | `k9o.org.gate.idp_providers.delete` | Remove a provider. |
468
- | `k9o.org.smtp.get` | Read the mail server settings. |
469
- | `k9o.org.smtp.set` | Save the mail server settings. |
470
- | `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:`. |
471
507
  | `k9o.org.smtp.unset` | Remove the mail server settings. |
472
508
  | `k9o.org.default_llm.get` | Read the default model. |
473
- | `k9o.org.default_llm.set` | Set the default model. |
509
+ | `k9o.org.default_llm.set` | Set the default model from a model id. |
474
510
 
475
511
  #### Vault secrets
476
512
 
@@ -479,7 +515,7 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
479
515
  | Method | What it does |
480
516
  |---|---|
481
517
  | `k9o.kibo.vault.secrets.list` | List secrets by prefix. |
482
- | `k9o.kibo.vault.secrets.get` | Read a secret's details. |
518
+ | `k9o.kibo.vault.secrets.get` | Read a secret by name (`region:` is required). |
483
519
  | `k9o.kibo.vault.secrets.create` | Store a new secret. |
484
520
  | `k9o.kibo.vault.secrets.reveal_challenge` | Get a one-time challenge for revealing. |
485
521
  | `k9o.kibo.vault.secrets.reveal` | Reveal the value using the challenge. |
@@ -534,10 +570,12 @@ The server may send codes not listed here, so keep a fallback branch.
534
570
 
535
571
  ### Regional hosts
536
572
 
537
- - Organization hosts such as `https://acme.api.k9o.io` are region-free.
538
- - The SDK sends one credential-free `HEAD` request to learn the region.
539
- - Later requests go to the regional host.
540
- - 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.
541
579
 
542
580
  ### Related packages
543
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)