k9o 1.2.0-aarch64-linux → 1.5.0-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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -1
- data/README.md +1575 -180
- data/k9o.gemspec +2 -2
- data/lib/k9o/_ffi/kibo_kms_rest_ffi.rb +928 -25
- data/lib/k9o/_ffi/libkibo_kms_rest_ffi.so +0 -0
- data/lib/k9o/ai.rb +15 -0
- data/lib/k9o/analytics.rb +403 -0
- data/lib/k9o/approvals.rb +253 -0
- data/lib/k9o/audit.rb +162 -14
- data/lib/k9o/client.rb +28 -9
- data/lib/k9o/compute.rb +32 -15
- data/lib/k9o/crypto.rb +471 -0
- data/lib/k9o/custody.rb +28 -0
- data/lib/k9o/data.rb +125 -5
- data/lib/k9o/ed25519.rb +182 -0
- data/lib/k9o/end_users.rb +93 -0
- data/lib/k9o/envelope.rb +20 -5
- data/lib/k9o/functions.rb +127 -9
- data/lib/k9o/gate.rb +52 -22
- data/lib/k9o/http_transport.rb +78 -16
- data/lib/k9o/jcs.rb +117 -0
- data/lib/k9o/kerror.rb +57 -5
- data/lib/k9o/keys.rb +131 -0
- data/lib/k9o/kibo.rb +3 -3
- data/lib/k9o/kibo_client_core.rb +385 -0
- data/lib/k9o/kibo_ffi_core.rb +178 -0
- data/lib/k9o/locate.rb +118 -0
- data/lib/k9o/me.rb +27 -87
- data/lib/k9o/metadata.rb +342 -0
- data/lib/k9o/moat.rb +443 -0
- data/lib/k9o/org.rb +81 -68
- data/lib/k9o/orgs.rb +1 -0
- data/lib/k9o/policies.rb +25 -6
- data/lib/k9o/projects.rb +43 -8
- data/lib/k9o/query.rb +174 -0
- data/lib/k9o/rampart.rb +273 -0
- data/lib/k9o/realtime.rb +261 -32
- data/lib/k9o/region.rb +20 -18
- data/lib/k9o/schedules.rb +109 -0
- data/lib/k9o/schema.rb +35 -0
- data/lib/k9o/streams.rb +94 -0
- data/lib/k9o/studio.rb +21 -11
- data/lib/k9o/url_grammar.rb +46 -20
- data/lib/k9o/vault.rb +79 -84
- data/lib/k9o/vectors.rb +124 -0
- data/lib/k9o/version.rb +1 -1
- data/lib/k9o.rb +17 -0
- data/sig/k9o/ai.rbs +4 -0
- data/sig/k9o/approvals.rbs +71 -0
- data/sig/k9o/datasets.rbs +125 -0
- data/sig/k9o/gate.rbs +9 -6
- data/sig/k9o/kerror.rbs +24 -0
- data/sig/k9o/kibo_client_core.rbs +25 -0
- data/sig/k9o/metadata.rbs +54 -0
- data/sig/k9o/moat.rbs +29 -0
- data/sig/k9o/query.rbs +26 -0
- data/sig/k9o/rampart.rbs +59 -0
- data/sig/k9o/realtime.rbs +34 -0
- data/sig/k9o/region.rbs +14 -3
- data/sig/k9o/workload.rbs +64 -0
- 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.*`
|
|
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
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# `
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
|
34
|
-
#
|
|
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
|
|
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
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
91
|
-
|
|
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' =>
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
107
|
+
# `reveal_challenge`. `GET .../<name>` with
|
|
119
108
|
# `X-K9O-Reveal-Challenge`. Base64-decodes the wire's `value` back to
|
|
120
|
-
# plaintext (
|
|
121
|
-
#
|
|
122
|
-
def reveal(name,
|
|
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:
|
|
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' =>
|
|
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
|
|
127
|
+
# `PUT .../<name>`.
|
|
139
128
|
#
|
|
140
|
-
#
|
|
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,
|
|
132
|
+
def update(name, value:, region: nil, metadata: nil)
|
|
144
133
|
slug = require_project_slug
|
|
145
|
-
body = { '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:
|
|
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
|
|
145
|
+
# unchanged) — owner-only. `POST .../<name>?op=rotate`.
|
|
157
146
|
#
|
|
158
|
-
#
|
|
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:
|
|
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
|
|
160
|
+
# `POST .../<name>?op=move` with `{newPath}`.
|
|
172
161
|
#
|
|
173
|
-
#
|
|
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,
|
|
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:
|
|
170
|
+
query: region_query(region, 'op' => 'move'),
|
|
182
171
|
body: { 'newPath' => new_path }
|
|
183
172
|
)
|
|
184
173
|
end
|
|
185
174
|
|
|
186
|
-
# Retire a secret
|
|
187
|
-
#
|
|
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:
|
|
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
|
|
210
|
-
# (
|
|
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,
|
|
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
|
data/lib/k9o/vectors.rb
ADDED
|
@@ -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
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
|