k9o 1.2.0-aarch64-linux → 1.5.2-aarch64-linux

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +123 -1
  3. data/README.md +1602 -180
  4. data/k9o.gemspec +2 -2
  5. data/lib/k9o/_ffi/kibo_kms_rest_ffi.rb +928 -25
  6. data/lib/k9o/_ffi/libkibo_kms_rest_ffi.so +0 -0
  7. data/lib/k9o/ai.rb +15 -0
  8. data/lib/k9o/analytics.rb +403 -0
  9. data/lib/k9o/approvals.rb +253 -0
  10. data/lib/k9o/audit.rb +162 -14
  11. data/lib/k9o/client.rb +28 -9
  12. data/lib/k9o/compute.rb +32 -15
  13. data/lib/k9o/crypto.rb +471 -0
  14. data/lib/k9o/custody.rb +28 -0
  15. data/lib/k9o/data.rb +125 -5
  16. data/lib/k9o/ed25519.rb +182 -0
  17. data/lib/k9o/end_users.rb +93 -0
  18. data/lib/k9o/envelope.rb +20 -5
  19. data/lib/k9o/functions.rb +127 -9
  20. data/lib/k9o/gate.rb +52 -22
  21. data/lib/k9o/http_transport.rb +78 -16
  22. data/lib/k9o/jcs.rb +117 -0
  23. data/lib/k9o/kerror.rb +57 -5
  24. data/lib/k9o/keys.rb +131 -0
  25. data/lib/k9o/kibo.rb +3 -3
  26. data/lib/k9o/kibo_client_core.rb +385 -0
  27. data/lib/k9o/kibo_ffi_core.rb +178 -0
  28. data/lib/k9o/locate.rb +118 -0
  29. data/lib/k9o/me.rb +27 -87
  30. data/lib/k9o/metadata.rb +342 -0
  31. data/lib/k9o/moat.rb +443 -0
  32. data/lib/k9o/org.rb +80 -68
  33. data/lib/k9o/orgs.rb +1 -0
  34. data/lib/k9o/policies.rb +25 -6
  35. data/lib/k9o/projects.rb +43 -8
  36. data/lib/k9o/query.rb +174 -0
  37. data/lib/k9o/rampart.rb +273 -0
  38. data/lib/k9o/realtime.rb +261 -32
  39. data/lib/k9o/region.rb +20 -18
  40. data/lib/k9o/schedules.rb +109 -0
  41. data/lib/k9o/schema.rb +35 -0
  42. data/lib/k9o/streams.rb +94 -0
  43. data/lib/k9o/studio.rb +21 -11
  44. data/lib/k9o/url_grammar.rb +46 -20
  45. data/lib/k9o/vault.rb +79 -84
  46. data/lib/k9o/vectors.rb +124 -0
  47. data/lib/k9o/version.rb +1 -1
  48. data/lib/k9o.rb +17 -0
  49. data/sig/k9o/ai.rbs +4 -0
  50. data/sig/k9o/approvals.rbs +71 -0
  51. data/sig/k9o/datasets.rbs +125 -0
  52. data/sig/k9o/gate.rbs +9 -6
  53. data/sig/k9o/kerror.rbs +24 -0
  54. data/sig/k9o/kibo_client_core.rbs +25 -0
  55. data/sig/k9o/metadata.rbs +54 -0
  56. data/sig/k9o/moat.rbs +29 -0
  57. data/sig/k9o/query.rbs +26 -0
  58. data/sig/k9o/rampart.rbs +59 -0
  59. data/sig/k9o/realtime.rbs +34 -0
  60. data/sig/k9o/region.rbs +14 -3
  61. data/sig/k9o/workload.rbs +64 -0
  62. metadata +33 -4
data/lib/k9o/vault.rb CHANGED
@@ -1,48 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
4
3
  require 'uri'
5
4
 
6
5
  module K9o
7
- # `k9o.kibo.vault.secrets.*` — the typed secrets surface
6
+ # `k9o.kibo.vault.secrets.*` - the typed secrets surface
8
7
  # (`services/gateway/src/routes/vault-secrets.ts`). Full family: list / get /
9
8
  # create / reveal_challenge / reveal / update / rotate / move / delete.
10
9
  #
11
- # Contract live-verified against the real staging gateway (not just the
12
- # route's OpenAPI schema, which is wrong/incomplete in several places noted
13
- # below) by the `@k9o/kibo` TS reference build — this wrapper mirrors that
14
- # verified behavior exactly:
15
- #
16
- # - Every operation on a NAMED secret (everything except `list`) REQUIRES
17
- # `region` on the query string. Omitting it fails with
18
- # K9O_INVALID_FIELD_VALUE ("this request requires an authoritative
19
- # region"). NOT documented in the route's OpenAPI schema. `list` is the
20
- # one exception (verified it does NOT need `region`).
21
- # - The wire `value` field is base64 in BOTH directions, contradicting the
22
- # OpenAPI's plain-language "the plaintext to seal" description (reads
23
- # like it should be raw). A raw non-base64 `value` on create/update is
24
- # rejected as unparseable. `create`/`update` take plaintext as a normal
25
- # Ruby String and base64-encode it here; `reveal` base64-decodes the
26
- # wire `value` back to plaintext before returning it — callers never
27
- # see base64 on either side.
28
- # - `update`/`rotate`/`move` responses are LIVE-VERIFIED NARROWER than (or
29
- # entirely undocumented vs.) the OpenAPI's claimed `SecretSummary` — see
30
- # the comment on each method below.
10
+ # - `region` is OPTIONAL on every named-secret operation: the gateway
11
+ # ignores any caller-supplied region and uses its own. It is sent on the
12
+ # query string only when the caller passes one (never by `get`, which
13
+ # reads the list route).
14
+ # - The wire `value` field is base64 in BOTH directions. `create`/`update`
15
+ # take plaintext as a normal Ruby String and base64-encode it here;
16
+ # `reveal` base64-decodes the wire `value` back to plaintext before
17
+ # returning it - callers never see base64 on either side.
18
+ # - `update`/`rotate`/`move` responses are narrower than a full summary -
19
+ # see the comment on each method below.
31
20
  #
32
21
  # `data` is returned as the raw wire Hash (camelCase string keys), matching
33
- # this package's existing convention (Projects/Policies/Studio/etc. all
34
- # return `envelope.data` untransformed — there is no generic
35
- # camelCase<->snake_case response transform anywhere in this gem); `reveal`
36
- # is the one exception, where the `value` key is base64-decoded in place
37
- # (mirroring the TS reference's own `value`-only transform).
22
+ # this package's existing convention; `reveal` is the one exception, where
23
+ # the `value` key is base64-decoded in place.
38
24
  class VaultSecrets
25
+ MAX_GET_PAGES = 20
26
+
39
27
  def initialize(config)
40
28
  @config = config
41
29
  end
42
30
 
43
31
  # List secrets (metadata only — never lists ciphertext).
44
32
  # `GET /@<project_slug>/vault/secrets`.
45
- # The one secrets operation that does NOT need `region` (live-verified).
46
33
  def list(prefix: nil, cursor: nil, limit: nil)
47
34
  slug = require_project_slug
48
35
  q = {}
@@ -56,146 +43,154 @@ module K9o
56
43
  )
57
44
  end
58
45
 
59
- # Read a secret's metadata (never its value — use `reveal_challenge` +
46
+ # Read a secret's metadata (never its value - use `reveal_challenge` +
60
47
  # `reveal` for that).
61
- # `GET /@<project_slug>/vault/secrets/<name>?region=<region>`.
62
48
  #
63
- # KNOWN LIVE BUG (staging, 2026-09-05): this call currently fails with
64
- # K9O_REVEAL_CHALLENGE_REQUIRED on every attempt — the gateway does not
65
- # honor its own documented "no X-K9O-Reveal-Challenge header => return
66
- # metadata" behavior; it demands a challenge unconditionally. Implemented
67
- # per the documented contract anyway (this is a backend defect to flag,
68
- # not something to design around) — `list(prefix: exact_name)` is the
69
- # current working alternative for a single secret's metadata until the
70
- # gateway side is fixed.
71
- def get(name, region:)
72
- slug = require_project_slug
73
- HttpTransport.request(
74
- config: @config, method: 'GET',
75
- path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
76
- query: { 'region' => region }
49
+ # `GET /@<project>/vault/secrets/<name>` is a reveal and always demands an
50
+ # `X-K9O-Reveal-Challenge` header, so metadata is read from the list route:
51
+ # list with the exact name as `prefix` (limit 100, following `nextCursor`
52
+ # for up to MAX_GET_PAGES pages) and return the secret whose `name` equals
53
+ # the argument. Raises `KError` `K9O_NOT_FOUND` (status 404) when none
54
+ # matches. `region` is accepted for symmetry and not sent.
55
+ def get(name, region: nil) # rubocop:disable Lint/UnusedMethodArgument
56
+ cursor = nil
57
+ MAX_GET_PAGES.times do
58
+ env = list(prefix: name, cursor: cursor, limit: 100)
59
+ data = env.data || {}
60
+ secret = (data['secrets'] || []).find { |s| s['name'] == name }
61
+ return Envelope.new(data: secret, meta: env.meta) if secret
62
+
63
+ cursor = data['nextCursor']
64
+ break if cursor.nil? || cursor.to_s.empty?
65
+ end
66
+ raise KError.new(
67
+ code: 'K9O_NOT_FOUND',
68
+ message: "No secret named '#{name}' in this project.",
69
+ context: { 'name' => name },
70
+ status: 404
77
71
  )
78
72
  end
79
73
 
80
74
  # Create a secret under the caller's own auth.
81
- # `POST /@<project_slug>/vault/secrets/<name>?region=<region>` → the full
82
- # `sec_<base62>` summary.
83
- #
84
- # `region` scopes the vault WRITE itself (residency guard) and is
85
- # REQUIRED — live-verified against staging, not documented in the route's
86
- # OpenAPI schema.
75
+ # `POST /@<project_slug>/vault/secrets/<name>` (+ `region` when given) -> the
76
+ # full `sec_<base62>` summary.
87
77
  #
88
78
  # `visibility` (`{'kind' => 'private'}` or
89
79
  # `{'kind' => 'shared', 'principals' => [...], 'roles' => [...]}`) and
90
- # `metadata` (`Hash[String, String]`) are passed through verbatim — their
91
- # own field names are already single words, so no wire-casing applies.
92
- def create(name:, value:, region:, visibility: nil, metadata: nil)
80
+ # `metadata` (`Hash[String, String]`) are passed through verbatim.
81
+ def create(name:, value:, region: nil, visibility: nil, metadata: nil)
93
82
  slug = require_project_slug
94
- body = { 'value' => Base64.strict_encode64(value) }
83
+ body = { 'value' => [value].pack('m0') }
95
84
  body['visibility'] = visibility if visibility
96
85
  body['metadata'] = metadata if metadata
97
86
  HttpTransport.request(
98
87
  config: @config, method: 'POST',
99
88
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
100
- query: { 'region' => region },
89
+ query: region_query(region),
101
90
  body: body
102
91
  )
103
92
  end
104
93
 
105
94
  # Mint a single-use reveal challenge — present it as
106
95
  # `X-K9O-Reveal-Challenge` on the immediately-following `reveal` call.
107
- # `POST .../<name>?op=reveal-challenge&region=<region>`.
108
- def reveal_challenge(name, region:)
96
+ # `POST .../<name>?op=reveal-challenge`.
97
+ def reveal_challenge(name, region: nil)
109
98
  slug = require_project_slug
110
99
  HttpTransport.request(
111
100
  config: @config, method: 'POST',
112
101
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
113
- query: { 'op' => 'reveal-challenge', 'region' => region }
102
+ query: region_query(region, 'op' => 'reveal-challenge')
114
103
  )
115
104
  end
116
105
 
117
106
  # Reveal a secret's plaintext value using a single-use challenge from
118
- # `reveal_challenge`. `GET .../<name>?region=<region>` with
107
+ # `reveal_challenge`. `GET .../<name>` with
119
108
  # `X-K9O-Reveal-Challenge`. Base64-decodes the wire's `value` back to
120
- # plaintext (live-verified: the reveal response's `value` is base64,
121
- # symmetric with `create`'s encode) before returning it to the caller.
122
- def reveal(name, region:, challenge:)
109
+ # plaintext (the reveal response's `value` is base64, symmetric with
110
+ # `create`'s encode) before returning it to the caller.
111
+ def reveal(name, challenge:, region: nil)
123
112
  slug = require_project_slug
124
113
  envelope = HttpTransport.request(
125
114
  config: @config, method: 'GET',
126
115
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
127
- query: { 'region' => region },
116
+ query: region_query(region),
128
117
  headers: { 'X-K9O-Reveal-Challenge' => challenge }
129
118
  )
130
119
  data = envelope.data
131
120
  if data.is_a?(Hash) && data.key?('value')
132
- data = data.merge('value' => Base64.decode64(data['value']))
121
+ data = data.merge('value' => data['value'].unpack1('m'))
133
122
  end
134
123
  Envelope.new(data: data, meta: envelope.meta)
135
124
  end
136
125
 
137
126
  # Update a secret's value (bumps version) — owner-only.
138
- # `PUT .../<name>?region=<region>`.
127
+ # `PUT .../<name>`.
139
128
  #
140
- # LIVE-VERIFIED NARROWER than documented: the gateway returns only
129
+ # Narrower than a full summary: the gateway returns only
141
130
  # `{id, name, version}` on a real PUT, not the full summary the OpenAPI
142
131
  # schema claims — call `get` afterward if the full summary is needed.
143
- def update(name, region:, value:, metadata: nil)
132
+ def update(name, value:, region: nil, metadata: nil)
144
133
  slug = require_project_slug
145
- body = { 'value' => Base64.strict_encode64(value) }
134
+ body = { 'value' => [value].pack('m0') }
146
135
  body['metadata'] = metadata if metadata
147
136
  HttpTransport.request(
148
137
  config: @config, method: 'PUT',
149
138
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
150
- query: { 'region' => region },
139
+ query: region_query(region),
151
140
  body: body
152
141
  )
153
142
  end
154
143
 
155
144
  # Rotate a secret's wrapping key (re-wraps under a fresh `kid`; content
156
- # unchanged) — owner-only. `POST .../<name>?op=rotate&region=<region>`.
145
+ # unchanged) — owner-only. `POST .../<name>?op=rotate`.
157
146
  #
158
- # LIVE-VERIFIED undocumented shape: `{id, name, version, rotatedAt,
147
+ # Response shape: `{id, name, version, rotatedAt,
159
148
  # ledgerSequenceNumber}` — the route's OpenAPI schema doesn't document
160
149
  # this response at all.
161
- def rotate(name, region:)
150
+ def rotate(name, region: nil)
162
151
  slug = require_project_slug
163
152
  HttpTransport.request(
164
153
  config: @config, method: 'POST',
165
154
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
166
- query: { 'op' => 'rotate', 'region' => region }
155
+ query: region_query(region, 'op' => 'rotate')
167
156
  )
168
157
  end
169
158
 
170
159
  # Move (rename) a secret's hierarchical path — owner-only.
171
- # `POST .../<name>?op=move&region=<region>` with `{newPath}`.
160
+ # `POST .../<name>?op=move` with `{newPath}`.
172
161
  #
173
- # LIVE-VERIFIED undocumented shape: `{id, name, event}` (observed
162
+ # Response shape: `{id, name, event}` (observed
174
163
  # `event: "SECRET_PATH_CHANGED"`; treat as a plain String, not a closed
175
164
  # enum — only one sample was observed).
176
- def move(name, region:, new_path:)
165
+ def move(name, new_path:, region: nil)
177
166
  slug = require_project_slug
178
167
  HttpTransport.request(
179
168
  config: @config, method: 'POST',
180
169
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
181
- query: { 'op' => 'move', 'region' => region },
170
+ query: region_query(region, 'op' => 'move'),
182
171
  body: { 'newPath' => new_path }
183
172
  )
184
173
  end
185
174
 
186
- # Retire a secret — owner-only. `DELETE .../<name>?region=<region>` →
187
- # `204 No Content` (HttpTransport already maps that to `data: nil`).
188
- def delete(name, region:)
175
+ # Retire a secret - owner-only. `DELETE .../<name>`; any 2xx is success
176
+ # (204 No Content maps to `data: nil`).
177
+ def delete(name, region: nil)
189
178
  slug = require_project_slug
190
179
  HttpTransport.request(
191
180
  config: @config, method: 'DELETE',
192
181
  path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
193
- query: { 'region' => region }
182
+ query: region_query(region)
194
183
  )
195
184
  end
196
185
 
197
186
  private
198
187
 
188
+ def region_query(region, extra = {})
189
+ q = extra.dup
190
+ q['region'] = region if region
191
+ q
192
+ end
193
+
199
194
  def require_project_slug
200
195
  Studio.require_project_slug(@config, 'kibo.vault.secrets')
201
196
  end
@@ -206,8 +201,8 @@ module K9o
206
201
  end
207
202
 
208
203
  # `k9o.kibo.vault.*` — the Kibo vault surface (member-plane, project-scoped).
209
- # Currently just `secrets`; a `zk_deks` accessor
210
- # (F.5, `POST vault/zk-deks` / `GET vault/zk-deks/<id>`) would
204
+ # Currently just `secrets` ; a `zk_deks` accessor
205
+ # (`POST vault/zk-deks` / `GET vault/zk-deks/<id>`) would
211
206
  # nest here alongside it if/when that surface is built for Ruby — out of
212
207
  # scope for this wrapper, which only covers the vault-secrets audit gap.
213
208
  class KiboVault
@@ -220,7 +215,7 @@ module K9o
220
215
 
221
216
  # `k9o.kibo.*` — per-client namespace root for the Kibo vault REST surface.
222
217
  # Distinct from the module-level `K9o::Kibo` (native ZK `encrypt`/`decrypt`
223
- # FFI bindings, /, called directly as
218
+ # FFI bindings, called directly as
224
219
  # `K9o::Kibo.encrypt`/`.decrypt` since that surface needs no per-client HTTP
225
220
  # config): this class exists so `client.kibo.vault.secrets.*` is reachable
226
221
  # per client instance, mirroring the TS reference's
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Vector collections: k9o.vectors.* store embeddings you computed yourself, next to the text and metadata they came
5
+ # from, and find the nearest ones.
6
+ #
7
+ # nearest(collection, query_vector, top_k:, min_score:, metric:, filters:) POST /@<slug>/vectors/<collection>/search
8
+ # upsert(collection, rows) POST /@<slug>/vectors/<collection>/upsert
9
+ #
10
+ # Both send the vector in the request body, so they take real embeddings of any common size (up to 16000 numbers).
11
+ # The collection name must start with +_+. The first upsert creates the collection and fixes its dimension; every
12
+ # later vector must have the same length. Searching a collection that was never upserted answers 404
13
+ # K9O_COLLECTION_NOT_FOUND.
14
+ class Vectors
15
+ METRICS = %w[cosine euclidean dot].freeze
16
+ MAX_VECTOR = 16_000
17
+ MAX_ROWS = 1000
18
+ # Snake_case spellings accepted for the camelCase wire keys of a row and of its +_source+.
19
+ ROW_ALIASES = { 'content_hash' => 'contentHash', 'subject_ids' => 'subjectIds' }.freeze
20
+ SOURCE_ALIASES = { 'chunk_index' => 'chunkIndex' }.freeze
21
+
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ # The rows nearest to +query_vector+ (1 to 16000 numbers), closest first. +top_k+ is 1 to 1000 (the gateway
27
+ # defaults to 10); +min_score+ drops results scoring below it; +metric+ is cosine (default), euclidean or dot;
28
+ # +filters+ is a Hash of equality checks on +id+, +contentHash+, +source.<key>+ and +metadata.<key>+.
29
+ # +data+ is { 'results' => [...], 'collection' => name }.
30
+ def nearest(collection, query_vector, top_k: nil, min_score: nil, metric: nil, filters: nil)
31
+ route = path(collection, 'search')
32
+ check_vector(query_vector, 'query_vector')
33
+ body = { 'queryVector' => query_vector.to_a }
34
+ unless top_k.nil?
35
+ unless top_k.is_a?(Integer) && top_k >= 1 && top_k <= 1000
36
+ raise invalid('top_k must be a whole number from 1 to 1000', top_k: top_k)
37
+ end
38
+
39
+ body['topK'] = top_k
40
+ end
41
+ unless min_score.nil?
42
+ raise invalid('min_score must be a number', min_score: min_score) unless finite?(min_score)
43
+
44
+ body['minScore'] = min_score
45
+ end
46
+ body['metric'] = ApprovalChecks.enum!(metric, METRICS, 'metric') unless metric.nil?
47
+ unless filters.nil?
48
+ raise invalid('filters must be a Hash of path to value') unless filters.is_a?(Hash)
49
+
50
+ body['filters'] = filters.transform_keys(&:to_s)
51
+ end
52
+ call(route, body)
53
+ end
54
+
55
+ # Insert or overwrite 1 to 1000 rows in one request; the first upsert creates the collection. A row is a Hash
56
+ # with +vector+ and +_source+ ({ handle:, chunkIndex: }), and optionally +id+, +contentHash+, +text+, +metadata+
57
+ # and +subjectIds+ (one end-user id, ["eu_..."]; required unless the collection was declared shared). Keys may
58
+ # be strings or symbols, and +content_hash+, +subject_ids+ and +chunk_index+ are accepted for the camelCase
59
+ # names. A single row may be passed on its own, with or without braces. +data+ is { 'upserted' => n,
60
+ # 'collection' => name }.
61
+ def upsert(collection, rows = nil, **row)
62
+ route = path(collection, 'upsert')
63
+ rows = [row] if rows.nil? && !row.empty?
64
+ rows = [rows] if rows.is_a?(Hash)
65
+ unless rows.is_a?(Array) && rows.size >= 1 && rows.size <= MAX_ROWS
66
+ raise invalid("rows must be an array of 1 to #{MAX_ROWS} rows")
67
+ end
68
+
69
+ wire = rows.each_with_index.map do |entry, index|
70
+ raise invalid("rows[#{index}] must be a Hash") unless entry.is_a?(Hash)
71
+
72
+ normalize_row(entry, index)
73
+ end
74
+ call(route, 'rows' => wire)
75
+ end
76
+
77
+ private
78
+
79
+ def normalize_row(entry, index)
80
+ row = entry.each_with_object({}) do |(key, value), out|
81
+ name = key.to_s
82
+ out[ROW_ALIASES.fetch(name, name)] = value
83
+ end
84
+ check_vector(row['vector'], "rows[#{index}].vector")
85
+ source = row['_source']
86
+ if source.is_a?(Hash)
87
+ row['_source'] = source.each_with_object({}) do |(key, value), out|
88
+ name = key.to_s
89
+ out[SOURCE_ALIASES.fetch(name, name)] = value
90
+ end
91
+ end
92
+ row['vector'] = row['vector'].to_a
93
+ row
94
+ end
95
+
96
+ def check_vector(vector, name)
97
+ unless vector.is_a?(Array) && vector.size >= 1 && vector.size <= MAX_VECTOR
98
+ raise invalid("#{name} must be an array of 1 to #{MAX_VECTOR} numbers")
99
+ end
100
+ return if vector.all? { |n| finite?(n) }
101
+
102
+ raise invalid("#{name} must contain only finite numbers")
103
+ end
104
+
105
+ def finite?(value)
106
+ value.is_a?(Numeric) && !value.is_a?(Complex) && (!value.respond_to?(:finite?) || value.finite?)
107
+ end
108
+
109
+ def invalid(message, context = {})
110
+ ApprovalChecks.invalid(message, context)
111
+ end
112
+
113
+ def call(route, body)
114
+ HttpTransport.request(config: @config, method: 'POST', path: route, body: body)
115
+ end
116
+
117
+ def path(collection, tail)
118
+ raise invalid('a vector collection name must start with _', collection: collection) unless collection.is_a?(String) && collection.start_with?('_')
119
+
120
+ slug = Studio.require_project_slug(@config, 'vectors')
121
+ UrlGrammar.project_path(slug, "vectors/#{::URI.encode_www_form_component(collection).gsub('+', '%20')}/#{tail}")
122
+ end
123
+ end
124
+ end
data/lib/k9o/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K9o
4
- VERSION = '1.2.0'
4
+ VERSION = '1.5.2'
5
5
  end
data/lib/k9o.rb CHANGED
@@ -29,15 +29,30 @@ require_relative 'k9o/url_grammar'
29
29
  require_relative 'k9o/region'
30
30
  require_relative 'k9o/http_transport'
31
31
  require_relative 'k9o/data'
32
+ require_relative 'k9o/query'
32
33
  require_relative 'k9o/projects'
34
+ require_relative 'k9o/locate'
33
35
  require_relative 'k9o/gate'
34
36
  require_relative 'k9o/studio'
35
37
  require_relative 'k9o/compute'
36
38
  require_relative 'k9o/ai'
37
39
  require_relative 'k9o/storage'
38
40
  require_relative 'k9o/realtime'
41
+ require_relative 'k9o/streams'
42
+ require_relative 'k9o/approvals'
43
+ require_relative 'k9o/schema'
39
44
  require_relative 'k9o/migrations'
40
45
  require_relative 'k9o/functions'
46
+ require_relative 'k9o/schedules'
47
+ require_relative 'k9o/vectors'
48
+ require_relative 'k9o/metadata'
49
+ require_relative 'k9o/rampart'
50
+ require_relative 'k9o/moat'
51
+ require_relative 'k9o/custody'
52
+ require_relative 'k9o/analytics'
53
+ require_relative 'k9o/keys'
54
+ require_relative 'k9o/crypto'
55
+ require_relative 'k9o/end_users'
41
56
  require_relative 'k9o/policies'
42
57
  require_relative 'k9o/audit'
43
58
  require_relative 'k9o/orgs'
@@ -46,6 +61,8 @@ require_relative 'k9o/org'
46
61
  # kibo ZK BYOK/HYOK crypto — self-guards its native binding (LoadError-safe), so
47
62
  # requiring k9o never breaks on a platform without the kibo native lib.
48
63
  require_relative 'k9o/kibo'
64
+ require_relative 'k9o/kibo_client_core'
65
+ require_relative 'k9o/kibo_ffi_core'
49
66
  # kibo vault REST surface (client.kibo.vault.secrets) —.
50
67
  require_relative 'k9o/vault'
51
68
  # Barbican guardrail SDK — self-guards its native binding (LoadError-safe), so
data/sig/k9o/ai.rbs CHANGED
@@ -8,6 +8,10 @@ module K9o
8
8
  # { "embeddings" => Array[Array[Float]], "model" => String, "dimensions" => Integer }.
9
9
  def initialize: (untyped config) -> void
10
10
  def embed: (input: (String | Array[String]), ?model: String?) -> Envelope
11
+
12
+ # Only the fields given are sent. Returns an Envelope whose `data` is
13
+ # { "modelId" => String, "content" => String, "finishReason" => String, "usage" => Hash, "usageType" => String }.
14
+ def generate: (String prompt, ?model: String?, ?max_tokens: Integer?, ?temperature: Numeric?, ?usage_type: String?) -> Envelope
11
15
  end
12
16
 
13
17
  class K9oClient
@@ -0,0 +1,71 @@
1
+ # RBS signatures for `client.approvals`, `client.approval_policies` and `client.schema`.
2
+
3
+ module K9o
4
+ module ApprovalChecks
5
+ def self.invalid: (String message, ?Hash[Symbol, untyped] context) -> KError
6
+ def self.check_limit: (untyped limit) -> void
7
+ def self.id_segment: (untyped value, String what) -> String
8
+ def self.reason!: (untyped reason) -> String
9
+ def self.predicate!: (untyped value, String field) -> Hash[untyped, untyped]
10
+ def self.enum!: (untyped value, Array[String] allowed, String field) -> String
11
+ end
12
+
13
+ class Approvals
14
+ attr_reader comments: ApprovalComments
15
+ def initialize: (untyped config) -> void
16
+ # One page, newest first. `limit` is 1 to 200 (default 50); pass `meta.cursor.next` back as `cursor`
17
+ # (nil on the last page).
18
+ def list: (?status: (String | Symbol)?, ?limit: Integer?, ?cursor: String?) -> Envelope
19
+ def get: (String id) -> Envelope
20
+ # `status` on the answer stays "pending_approval" while more approvers are needed.
21
+ def approve: (String id, ?reason: String?) -> Envelope
22
+ def reject: (String id, reason: String) -> Envelope
23
+ # HTTP 200 even when rows fail: `data["failed"]` is an Array of { "id", "code", "reason" } (empty when none).
24
+ # Compare `affectedIds` with what you intended.
25
+ def bulk: (action: (String | Symbol), criteria: Hash[(String | Symbol), untyped], ?reason: String?) -> Envelope
26
+ end
27
+
28
+ # `approvals.comments`. Posting needs a session (a personal access token alone is refused, K9O_RBAC_DENY).
29
+ class ApprovalComments
30
+ VISIBILITIES: Array[String]
31
+ BODY_MAX: Integer
32
+
33
+ # An Envelope whose data is the Array of comments, oldest first; `next_cursor` is nil on the last page.
34
+ # A comment is { "id", "caseId", "authorId", "visibility", "body", "createdAt" }.
35
+ class Page < Envelope
36
+ attr_reader next_cursor: String?
37
+ def initialize: (data: untyped, meta: Meta, ?next_cursor: String?) -> void
38
+ end
39
+
40
+ def initialize: (untyped config) -> void
41
+ # `body` is 1 to 4000 characters; `visibility` is "everyone" or "approvers_only", fixed once posted. HTTP 201.
42
+ def add: (String pending_id, body: String, visibility: (String | Symbol)) -> Envelope
43
+ # `limit` is 1 to 200 (default 50); pass `next_cursor` back as `cursor`.
44
+ def list: (String pending_id, ?limit: Integer?, ?cursor: String?) -> Page
45
+ end
46
+
47
+ class ApprovalPolicies
48
+ COLLECTION_RE: Regexp
49
+
50
+ def initialize: (untyped config) -> void
51
+ def list: () -> Envelope
52
+ def create: (collection: String, operation: (String | Symbol), action: (String | Symbol), predicate: Hash[(String | Symbol), untyped]) -> Envelope
53
+ def delete: (String id) -> Envelope
54
+ end
55
+
56
+ class SchemaCollections
57
+ def initialize: (untyped config) -> void
58
+ def list: (?limit: Integer?, ?cursor: String?) -> Envelope
59
+ end
60
+
61
+ class Schema
62
+ attr_reader collections: SchemaCollections
63
+ def initialize: (untyped config) -> void
64
+ end
65
+
66
+ class K9oClient
67
+ attr_reader approvals: Approvals
68
+ attr_reader approval_policies: ApprovalPolicies
69
+ attr_reader schema: Schema
70
+ end
71
+ end