k9o 1.0.0__py3-none-any.whl
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.
- k9o/__init__.py +143 -0
- k9o/ai.py +45 -0
- k9o/audit.py +48 -0
- k9o/barbican/__init__.py +20 -0
- k9o/barbican/_ffi/__init__.py +5 -0
- k9o/barbican/barbican.py +218 -0
- k9o/client.py +132 -0
- k9o/compute.py +94 -0
- k9o/data.py +246 -0
- k9o/envelope.py +144 -0
- k9o/errors.py +87 -0
- k9o/functions.py +58 -0
- k9o/gate.py +100 -0
- k9o/http.py +378 -0
- k9o/kibo/__init__.py +53 -0
- k9o/kibo/_ffi/__init__.py +3 -0
- k9o/kibo/crypto.py +62 -0
- k9o/kibo_vault.py +285 -0
- k9o/me.py +109 -0
- k9o/migrations.py +355 -0
- k9o/org.py +180 -0
- k9o/orgs.py +35 -0
- k9o/policies.py +70 -0
- k9o/projects.py +67 -0
- k9o/realtime.py +150 -0
- k9o/region.py +110 -0
- k9o/storage.py +334 -0
- k9o/studio.py +197 -0
- k9o/url_grammar.py +119 -0
- k9o-1.0.0.dist-info/METADATA +96 -0
- k9o-1.0.0.dist-info/RECORD +35 -0
- k9o-1.0.0.dist-info/WHEEL +5 -0
- k9o-1.0.0.dist-info/licenses/LICENSE +201 -0
- k9o-1.0.0.dist-info/licenses/NOTICE +4 -0
- k9o-1.0.0.dist-info/top_level.txt +1 -0
k9o/__init__.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
k9o — Kilimanjaro platform SDK (Python; idiomatic).
|
|
3
|
+
|
|
4
|
+
Canonical usage:
|
|
5
|
+
|
|
6
|
+
from k9o import create_client, KError
|
|
7
|
+
|
|
8
|
+
k9o = create_client(
|
|
9
|
+
base_url="https://acme.api.k9o.io",
|
|
10
|
+
api_key=os.environ["K9O_API_KEY"],
|
|
11
|
+
org_slug="acme",
|
|
12
|
+
project_slug="member-portal",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
response = k9o.from_("posts").select().limit(10).execute()
|
|
17
|
+
for row in response.data:
|
|
18
|
+
print(row)
|
|
19
|
+
session = k9o.gate.sign_in(email="user@example.com", password="…")
|
|
20
|
+
print("signed in:", session.data["user"]["id"])
|
|
21
|
+
except KError as e:
|
|
22
|
+
print(f"[{e.code}] {e.message}: {e.recommendation}")
|
|
23
|
+
|
|
24
|
+
Architectural posture: hand-authored idiomatic SDK against the platform API specification
|
|
25
|
+
+ services/gateway/openapi.json contract source; replaces the prior
|
|
26
|
+
OpenAPI-generator output (`k9o_gate` raw client) with ch13 idiomatic
|
|
27
|
+
shape (snake_case methods + raise KError + envelope dataclass with
|
|
28
|
+
typed meta accessor sugar + data DSL).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from .audit import AuditNamespace
|
|
34
|
+
from .client import K9oClient, create_client
|
|
35
|
+
from .compute import ComputeNamespace, LlmsNamespace, LLM_MODEL_IDS, LLM_ERROR_CODES
|
|
36
|
+
from .data import FromBuilder, SelectBuilder, UpdateBuilder, DeleteBuilder
|
|
37
|
+
from .functions import FunctionsNamespace
|
|
38
|
+
from .kibo_vault import KiboNamespace, SecretsNamespace, VaultNamespace
|
|
39
|
+
from .me import MeNamespace
|
|
40
|
+
from .migrations import MigrationsNamespace
|
|
41
|
+
from .org import OrgNamespace
|
|
42
|
+
from .orgs import OrgsNamespace
|
|
43
|
+
from .policies import PoliciesNamespace
|
|
44
|
+
from .realtime import RealtimeNamespace, realtime_topic, REALTIME_TOPIC_FAMILIES
|
|
45
|
+
from .studio import (
|
|
46
|
+
StudioNamespace,
|
|
47
|
+
WorkflowsNamespace,
|
|
48
|
+
DashboardsNamespace,
|
|
49
|
+
WORKFLOW_ERROR_CODES,
|
|
50
|
+
DASHBOARD_ERROR_CODES,
|
|
51
|
+
WORKFLOW_TRIGGER_TYPES,
|
|
52
|
+
WORKFLOW_ACTION_TYPES,
|
|
53
|
+
WIDGET_TYPES,
|
|
54
|
+
)
|
|
55
|
+
from .envelope import (
|
|
56
|
+
Envelope,
|
|
57
|
+
Meta,
|
|
58
|
+
MetaContext,
|
|
59
|
+
MetaCursor,
|
|
60
|
+
MetaIdempotency,
|
|
61
|
+
MetaSubscription,
|
|
62
|
+
MetaTiming,
|
|
63
|
+
MetaWarning,
|
|
64
|
+
)
|
|
65
|
+
from .errors import KError, KErrorWire, kerror_from_wire
|
|
66
|
+
from .gate import GateNamespace
|
|
67
|
+
from .http import HttpClientConfig, RetryConfig
|
|
68
|
+
from .projects import Project, ProjectsNamespace
|
|
69
|
+
from .url_grammar import (
|
|
70
|
+
org_path,
|
|
71
|
+
platform_path,
|
|
72
|
+
project_path,
|
|
73
|
+
resolve_base_url,
|
|
74
|
+
validate_base_url,
|
|
75
|
+
validate_slug,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__version__ = "0.3.0"
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"__version__",
|
|
82
|
+
# Factory + client
|
|
83
|
+
"create_client",
|
|
84
|
+
"K9oClient",
|
|
85
|
+
# Error
|
|
86
|
+
"KError",
|
|
87
|
+
"KErrorWire",
|
|
88
|
+
"kerror_from_wire",
|
|
89
|
+
# Envelope + meta
|
|
90
|
+
"Envelope",
|
|
91
|
+
"Meta",
|
|
92
|
+
"MetaContext",
|
|
93
|
+
"MetaCursor",
|
|
94
|
+
"MetaIdempotency",
|
|
95
|
+
"MetaSubscription",
|
|
96
|
+
"MetaTiming",
|
|
97
|
+
"MetaWarning",
|
|
98
|
+
# Data DSL
|
|
99
|
+
"FromBuilder",
|
|
100
|
+
"SelectBuilder",
|
|
101
|
+
"UpdateBuilder",
|
|
102
|
+
"DeleteBuilder",
|
|
103
|
+
# Namespaces
|
|
104
|
+
"ProjectsNamespace",
|
|
105
|
+
"Project",
|
|
106
|
+
"GateNamespace",
|
|
107
|
+
"StudioNamespace",
|
|
108
|
+
"WorkflowsNamespace",
|
|
109
|
+
"DashboardsNamespace",
|
|
110
|
+
"ComputeNamespace",
|
|
111
|
+
"LlmsNamespace",
|
|
112
|
+
"RealtimeNamespace",
|
|
113
|
+
"realtime_topic",
|
|
114
|
+
"OrgNamespace",
|
|
115
|
+
"OrgsNamespace",
|
|
116
|
+
"MigrationsNamespace",
|
|
117
|
+
"FunctionsNamespace",
|
|
118
|
+
"PoliciesNamespace",
|
|
119
|
+
"AuditNamespace",
|
|
120
|
+
"MeNamespace",
|
|
121
|
+
"KiboNamespace",
|
|
122
|
+
"VaultNamespace",
|
|
123
|
+
"SecretsNamespace",
|
|
124
|
+
# P6 closed enums + error-code maps
|
|
125
|
+
"WORKFLOW_ERROR_CODES",
|
|
126
|
+
"DASHBOARD_ERROR_CODES",
|
|
127
|
+
"LLM_ERROR_CODES",
|
|
128
|
+
"WORKFLOW_TRIGGER_TYPES",
|
|
129
|
+
"WORKFLOW_ACTION_TYPES",
|
|
130
|
+
"WIDGET_TYPES",
|
|
131
|
+
"LLM_MODEL_IDS",
|
|
132
|
+
"REALTIME_TOPIC_FAMILIES",
|
|
133
|
+
# URL grammar
|
|
134
|
+
"validate_base_url",
|
|
135
|
+
"resolve_base_url",
|
|
136
|
+
"validate_slug",
|
|
137
|
+
"project_path",
|
|
138
|
+
"org_path",
|
|
139
|
+
"platform_path",
|
|
140
|
+
# HTTP config (advanced use)
|
|
141
|
+
"HttpClientConfig",
|
|
142
|
+
"RetryConfig",
|
|
143
|
+
]
|
k9o/ai.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI namespace — `client.ai.*` (platform AI primitives).
|
|
3
|
+
|
|
4
|
+
Wire mapping (project-plane):
|
|
5
|
+
- embed(input, model=None) -> POST /@<slug>/ai/embeddings
|
|
6
|
+
body {model?, input: str | list[str]}
|
|
7
|
+
-> Envelope(data={"embeddings": [[float, ...], ...], "model": str, "dimensions": int})
|
|
8
|
+
|
|
9
|
+
Bounds (non-empty inputs, count, size, concurrency) are enforced by the
|
|
10
|
+
gateway, not here. The SDK sends `input` exactly as given and never puts it
|
|
11
|
+
in an error it constructs.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Sequence
|
|
17
|
+
|
|
18
|
+
from .envelope import Envelope
|
|
19
|
+
from .http import HttpClientConfig, request
|
|
20
|
+
from .url_grammar import project_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _require_project_slug(config: HttpClientConfig) -> str:
|
|
24
|
+
if not config.project_slug:
|
|
25
|
+
raise ValueError("ai operations require project_slug bound on the client")
|
|
26
|
+
return config.project_slug
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AiNamespace:
|
|
30
|
+
"""`client.ai.*`."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: HttpClientConfig) -> None:
|
|
33
|
+
self._config = config
|
|
34
|
+
|
|
35
|
+
def embed(self, input: str | Sequence[str], *, model: str | None = None) -> Envelope:
|
|
36
|
+
"""POST /@<slug>/ai/embeddings — embed one string or a list of strings.
|
|
37
|
+
|
|
38
|
+
`model` is omitted from the body when None (server default). A single
|
|
39
|
+
string stays a string; a sequence is sent as a list, in order.
|
|
40
|
+
"""
|
|
41
|
+
slug = _require_project_slug(self._config)
|
|
42
|
+
body: dict = {"input": input if isinstance(input, str) else list(input)}
|
|
43
|
+
if model is not None:
|
|
44
|
+
body["model"] = model
|
|
45
|
+
return request(self._config, method="POST", path=project_path(slug, "ai/embeddings"), body=body)
|
k9o/audit.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project-plane audit log namespace — `client.audit.*`.
|
|
3
|
+
|
|
4
|
+
Distinct from `client.org.audit_log` (org-plane) and
|
|
5
|
+
`client.me.audit_log` (operator-self). Python idiomatic per ch13 §2.1.1.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
from .envelope import Envelope
|
|
15
|
+
from .http import HttpClientConfig, request
|
|
16
|
+
from .url_grammar import project_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _require_project_slug(config: HttpClientConfig) -> str:
|
|
20
|
+
if not config.project_slug:
|
|
21
|
+
raise ValueError("audit operations require project_slug bound on the client")
|
|
22
|
+
return config.project_slug
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _clean(d: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
return {k: v for k, v in d.items() if v is not None}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuditNamespace:
|
|
30
|
+
def __init__(self, config: HttpClientConfig) -> None:
|
|
31
|
+
self._config = config
|
|
32
|
+
|
|
33
|
+
def query(self, *, since: str | None = None, until: str | None = None, type: str | None = None,
|
|
34
|
+
actor_id: str | None = None, target_kind: str | None = None, target_id: str | None = None,
|
|
35
|
+
limit: int | None = None, cursor: str | None = None) -> Envelope:
|
|
36
|
+
"""Query the audit log (reverse-chronological; filters AND-compose)."""
|
|
37
|
+
slug = _require_project_slug(self._config)
|
|
38
|
+
return request(self._config, method="GET", path=project_path(slug, "audit-log"),
|
|
39
|
+
query=_clean({"since": since, "until": until, "type": type, "actorId": actor_id,
|
|
40
|
+
"targetKind": target_kind, "targetId": target_id,
|
|
41
|
+
"limit": limit, "cursor": cursor}))
|
|
42
|
+
|
|
43
|
+
def tail(self, *, since_minutes: int = 1, limit: int = 100) -> Envelope:
|
|
44
|
+
"""Convenience: recent events within a since-window."""
|
|
45
|
+
slug = _require_project_slug(self._config)
|
|
46
|
+
since = (datetime.now(timezone.utc) - timedelta(minutes=since_minutes)).isoformat()
|
|
47
|
+
return request(self._config, method="GET", path=project_path(slug, "audit-log"),
|
|
48
|
+
query={"since": since, "limit": limit})
|
k9o/barbican/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""``k9o.barbican`` — free, embeddable L2 Keep guardrail (Python).
|
|
2
|
+
|
|
3
|
+
Unicode-normalize -> Aho-Corasick deny over a signed ruleset -> buffer-and-
|
|
4
|
+
scan output stream-filter. A thin wrapper over the audited ``k9o-keep-core``
|
|
5
|
+
engine, compiled to a native lib via uniffi (mirrors the ``k9o.kibo`` crypto
|
|
6
|
+
wrapper's FFI shape). ZERO security logic here: every normalization pass,
|
|
7
|
+
deny match, verdict, and output redaction is the core's; this layer only
|
|
8
|
+
forwards typed calls to the generated binding and maps its error -> ``KError``.
|
|
9
|
+
|
|
10
|
+
CLAIM BOUNDARY (security-claims-non-negotiable): Barbican is advisory /
|
|
11
|
+
defense-in-depth (Edge Filtration). "Un-bypassable" attaches ONLY to the
|
|
12
|
+
Enforced Boundary (Warden as the sole sandbox egress) -- never this SDK.
|
|
13
|
+
|
|
14
|
+
The native lib + generated uniffi binding under ``_ffi/`` are build artifacts
|
|
15
|
+
(generated per platform; gitignored) -- same convention as ``k9o.kibo``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .barbican import Barbican, OutputFilter
|
|
19
|
+
|
|
20
|
+
__all__ = ["Barbican", "OutputFilter"]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Generated uniffi binding + native lib live here (build artifacts, gitignored).
|
|
2
|
+
# Regenerate: cargo build -p k9o-keep-core-ffi-uniffi --release --features cli
|
|
3
|
+
# + cargo run --bin uniffi-bindgen --features cli -- generate \
|
|
4
|
+
# --library target/release/libk9o_keep_core_ffi_uniffi.dylib \
|
|
5
|
+
# --language python --out-dir .
|
k9o/barbican/barbican.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Thin wrapper over the bound ``k9o-keep-core`` engine (native, via uniffi).
|
|
2
|
+
|
|
3
|
+
Sync throughout -- the core has no async fn (unlike ``k9o.kibo``'s crypto,
|
|
4
|
+
which calls a network KMS), and the rest of the ``k9o`` Python SDK is sync
|
|
5
|
+
(``requests``/``httpx``-style), so there is no async-for-future-proofing need
|
|
6
|
+
here the way there is in the TS SDK (JS network calls force async; Python
|
|
7
|
+
doesn't have that constraint -- a future hosted-tier escalation can stay sync).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ..errors import KError
|
|
16
|
+
from ._ffi import k9o_keep_core_ffi_uniffi as _ffi
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _map_error(e: Exception, stage: str = "ruleset") -> KError:
|
|
20
|
+
"""Map the bound core's ``KeepError`` -> ``KError``. ``KeepError.RulesetInvalid``
|
|
21
|
+
is the only variant today (a refused ruleset, manifest or key manifest); the
|
|
22
|
+
core's closed reason token (``unknown_kid`` / ``signature_invalid`` /
|
|
23
|
+
``signature_non_canonical`` / ``version_stale`` / ``malformed`` / ... or an
|
|
24
|
+
unknown-severity message) rides in ``context.reason``, and ``context.stage``
|
|
25
|
+
says whether it came from the ruleset or the key manifest.
|
|
26
|
+
"""
|
|
27
|
+
reason = getattr(e, "reason", None)
|
|
28
|
+
if reason is not None:
|
|
29
|
+
return KError(
|
|
30
|
+
code="K9O_BARBICAN_RULESET_INVALID",
|
|
31
|
+
message=str(e),
|
|
32
|
+
context={"reason": reason, "stage": stage},
|
|
33
|
+
)
|
|
34
|
+
return KError(code="K9O_BARBICAN_WASM_ERROR", message=str(e))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OutputFilter:
|
|
38
|
+
"""A streaming output redactor. Buffer-and-scan-before-flush: never emits a
|
|
39
|
+
byte the core has not cleared (no RST-after-leak)."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, handle: Any) -> None:
|
|
42
|
+
self._handle = handle
|
|
43
|
+
|
|
44
|
+
def push(self, chunk: str) -> str:
|
|
45
|
+
"""Feed a chunk; returns the cleared (redacted) prefix safe to emit now."""
|
|
46
|
+
return self._handle.push(chunk)
|
|
47
|
+
|
|
48
|
+
def finish(self) -> str:
|
|
49
|
+
"""Flush the residual buffer (also scanned) at end of stream."""
|
|
50
|
+
return self._handle.finish()
|
|
51
|
+
|
|
52
|
+
def report(self) -> Any:
|
|
53
|
+
"""Leak report (counts only -- never the leaked values): an object with
|
|
54
|
+
``emails``, ``secrets``, ``markers``, ``total``."""
|
|
55
|
+
return self._handle.report()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Barbican:
|
|
59
|
+
"""The free, embeddable L2 guardrail. See the package docstring for the
|
|
60
|
+
claim boundary."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
fail_open: bool = False,
|
|
66
|
+
leet: bool = True,
|
|
67
|
+
decode_depth: int = 0,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""``decode_depth`` of 0 uses the core default (3) -- a bounded base64/
|
|
70
|
+
leetspeak de-obfuscation depth (decode-bomb guard)."""
|
|
71
|
+
self._core = _ffi.KeepCore(
|
|
72
|
+
_ffi.KeepConfig(fail_open=fail_open, leet=leet, decode_depth=decode_depth)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def load_ruleset(self, ruleset_version: int, rules: list[dict[str, str]]) -> int:
|
|
76
|
+
"""Load a static ruleset bundled with your package (Mode-A path: provenance
|
|
77
|
+
is vouched for by your package, so signature verification is skipped --
|
|
78
|
+
the core still enforces anti-rollback, never loading a version <= the
|
|
79
|
+
current one). ``rules`` = ``[{"id", "pattern", "severity"}]``
|
|
80
|
+
(severity one of low|medium|high|critical). Returns the accepted version.
|
|
81
|
+
"""
|
|
82
|
+
blob = _ffi.KeepRulesetBlob(
|
|
83
|
+
ruleset_version=ruleset_version,
|
|
84
|
+
rules=[
|
|
85
|
+
_ffi.KeepRule(id=r["id"], pattern=r["pattern"], severity=r["severity"])
|
|
86
|
+
for r in rules
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
return self._core.load_ruleset_unsigned(blob)
|
|
91
|
+
except _ffi.KeepError as e: # type: ignore[attr-defined]
|
|
92
|
+
raise _map_error(e) from e
|
|
93
|
+
|
|
94
|
+
def load_key_manifest(self, jws: str, now_secs: int | None = None) -> int:
|
|
95
|
+
"""Load a Barbican key manifest (compact JWS, ``typ k9o.barbican.keys.v1``).
|
|
96
|
+
The core checks it against the root key compiled into it and, only if it
|
|
97
|
+
verifies, REPLACES the trusted ruleset-signing keys with the ones it lists.
|
|
98
|
+
A refusal (wrong signer, rollback, a key changing its public key, a
|
|
99
|
+
malformed entry) raises ``K9O_BARBICAN_RULESET_INVALID`` with
|
|
100
|
+
``context.stage == "key_manifest"`` and leaves the trusted keys unchanged.
|
|
101
|
+
Returns the manifest ``sequence``."""
|
|
102
|
+
try:
|
|
103
|
+
return self._core.load_key_manifest(
|
|
104
|
+
jws, int(time.time()) if now_secs is None else now_secs
|
|
105
|
+
)
|
|
106
|
+
except _ffi.KeepError as e: # type: ignore[attr-defined]
|
|
107
|
+
raise _map_error(e, "key_manifest") from e
|
|
108
|
+
|
|
109
|
+
def verify_manifest(self, jws: str) -> Any:
|
|
110
|
+
"""Verify a ruleset manifest (compact JWS) under a trusted key and return
|
|
111
|
+
its authenticated fields (``ruleset_version``, ``signing_key_kid``,
|
|
112
|
+
``artifact_hash``, ``artifact_url``, ``valid_until``)."""
|
|
113
|
+
try:
|
|
114
|
+
return self._core.verify_manifest(jws)
|
|
115
|
+
except _ffi.KeepError as e: # type: ignore[attr-defined]
|
|
116
|
+
raise _map_error(e) from e
|
|
117
|
+
|
|
118
|
+
def load_ruleset_signed(
|
|
119
|
+
self,
|
|
120
|
+
ruleset_version: int,
|
|
121
|
+
rules: list[dict[str, str]],
|
|
122
|
+
signing_key_kid: str,
|
|
123
|
+
signature: bytes,
|
|
124
|
+
created_at: int = 0,
|
|
125
|
+
) -> int:
|
|
126
|
+
"""Load a signed ruleset. The core verifies the Ed25519 signature ITSELF,
|
|
127
|
+
over its own canonical bytes, under a key from the loaded key manifest:
|
|
128
|
+
there is no key or verifier to pass. Any failure (no key manifest loaded,
|
|
129
|
+
unknown key, bad or non-canonical signature, stale version, malformed
|
|
130
|
+
rules) raises ``K9O_BARBICAN_RULESET_INVALID`` with ``context.reason`` and
|
|
131
|
+
leaves the current ruleset running. Returns the accepted version."""
|
|
132
|
+
try:
|
|
133
|
+
return self._core.load_ruleset_signed(self._signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at))
|
|
134
|
+
except _ffi.KeepError as e: # type: ignore[attr-defined]
|
|
135
|
+
raise _map_error(e) from e
|
|
136
|
+
|
|
137
|
+
def ruleset_digest(
|
|
138
|
+
self,
|
|
139
|
+
ruleset_version: int,
|
|
140
|
+
rules: list[dict[str, str]],
|
|
141
|
+
signing_key_kid: str,
|
|
142
|
+
signature: bytes,
|
|
143
|
+
created_at: int = 0,
|
|
144
|
+
) -> str:
|
|
145
|
+
"""Lowercase hex SHA-256 of a ruleset's canonical signing bytes -- the value
|
|
146
|
+
a manifest's ``artifact_hash`` names. A digest only; it decides no trust."""
|
|
147
|
+
try:
|
|
148
|
+
return self._core.ruleset_digest(self._signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at))
|
|
149
|
+
except _ffi.KeepError as e: # type: ignore[attr-defined]
|
|
150
|
+
raise _map_error(e) from e
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at) -> Any:
|
|
154
|
+
return _ffi.KeepSignedRulesetBlob(
|
|
155
|
+
ruleset_version=ruleset_version,
|
|
156
|
+
rules=[
|
|
157
|
+
_ffi.KeepRule(id=r["id"], pattern=r["pattern"], severity=r["severity"])
|
|
158
|
+
for r in rules
|
|
159
|
+
],
|
|
160
|
+
signing_key_kid=signing_key_kid,
|
|
161
|
+
signature=bytes(signature),
|
|
162
|
+
created_at=created_at,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def key_manifest_sequence(self) -> int:
|
|
167
|
+
"""The ``sequence`` of the key manifest in force (0 before one is loaded)."""
|
|
168
|
+
return self._core.key_manifest_sequence()
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def signed_floor(self) -> int:
|
|
172
|
+
"""The highest VERIFIED ruleset version loaded (0 = none): the anti-rollback floor for signed
|
|
173
|
+
updates. A refresh compares a published version against this, not the version in force."""
|
|
174
|
+
return self._core.signed_floor()
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def active_ruleset_is_signed(self) -> bool:
|
|
178
|
+
"""Whether the ruleset in force came from a verified load. False when none is loaded, or when
|
|
179
|
+
``load_ruleset`` (unsigned, host-vouched) replaced a verified one."""
|
|
180
|
+
return self._core.active_ruleset_is_signed()
|
|
181
|
+
|
|
182
|
+
def trusted_kids(self) -> list[str]:
|
|
183
|
+
"""The signing ``kid`` s the loaded key manifest currently trusts, sorted."""
|
|
184
|
+
return self._core.trusted_kids()
|
|
185
|
+
|
|
186
|
+
def deprecated_kids(self) -> list[str]:
|
|
187
|
+
"""The trusted ``kid`` s the key manifest flags ``deprecated``, sorted."""
|
|
188
|
+
return self._core.deprecated_kids()
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def uses_test_trust_root() -> bool:
|
|
192
|
+
"""``False`` on every shipped build. ``True`` means this binding trusts the
|
|
193
|
+
PUBLISHED TEST root and must never be used outside tests."""
|
|
194
|
+
return _ffi.uses_test_trust_root()
|
|
195
|
+
|
|
196
|
+
def inspect(self, text: str, *, waypoint_class: str | None = None) -> Any:
|
|
197
|
+
"""Inspect an input (normalize -> deny -> local verdict). Obfuscation is
|
|
198
|
+
peeled BEFORE matching, so static deny-lists aren't defeated by trivial
|
|
199
|
+
tricks. ``waypoint_class`` is the manifest's meter hint -- it NEVER gates
|
|
200
|
+
the decision. Returns the verdict object (``decision``, ``reason``,
|
|
201
|
+
``matched``, ``schema_valid``, ``waypoint_class_hint``, ``fail_mode``,
|
|
202
|
+
``ruleset_version``, ``core_version``)."""
|
|
203
|
+
return self._core.inspect_input(text, waypoint_class)
|
|
204
|
+
|
|
205
|
+
def classify(self, text: str) -> str:
|
|
206
|
+
"""Local L3 stand-in classification (``"clear"|"suspicious"|"malicious"``).
|
|
207
|
+
No receipt -- a classification is not an enforcement decision."""
|
|
208
|
+
return self._core.classify_local(text)
|
|
209
|
+
|
|
210
|
+
def create_output_filter(self) -> OutputFilter:
|
|
211
|
+
"""Open an output stream-filter bound to the active ruleset's exfil
|
|
212
|
+
markers."""
|
|
213
|
+
return OutputFilter(self._core.output_filter())
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def core_version(self) -> str:
|
|
217
|
+
"""The bound core's semver (audit anchor; rides every verdict too)."""
|
|
218
|
+
return self._core.core_version()
|
k9o/client.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`create_client(...)` factory + `K9oClient`.
|
|
3
|
+
|
|
4
|
+
Canonical usage:
|
|
5
|
+
|
|
6
|
+
from k9o import create_client, KError
|
|
7
|
+
|
|
8
|
+
k9o = create_client(
|
|
9
|
+
base_url="https://acme.api.k9o.io",
|
|
10
|
+
api_key=os.environ["K9O_API_KEY"],
|
|
11
|
+
org_slug="acme",
|
|
12
|
+
project_slug="member-portal",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
response = k9o.from_("posts").select().limit(10).execute()
|
|
17
|
+
for row in response.data:
|
|
18
|
+
print(row)
|
|
19
|
+
if response.meta.cursor:
|
|
20
|
+
print("next cursor:", response.meta.cursor.next)
|
|
21
|
+
except KError as e:
|
|
22
|
+
print(f"[{e.code}] {e.message}: {e.recommendation}")
|
|
23
|
+
raise
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from .ai import AiNamespace
|
|
29
|
+
from .audit import AuditNamespace
|
|
30
|
+
from .compute import ComputeNamespace
|
|
31
|
+
from .data import FromBuilder
|
|
32
|
+
from .functions import FunctionsNamespace
|
|
33
|
+
from .gate import GateNamespace
|
|
34
|
+
from .http import HttpClientConfig, RetryConfig
|
|
35
|
+
from .kibo_vault import KiboNamespace
|
|
36
|
+
from .me import MeNamespace
|
|
37
|
+
from .migrations import MigrationsNamespace
|
|
38
|
+
from .org import OrgNamespace
|
|
39
|
+
from .orgs import OrgsNamespace
|
|
40
|
+
from .policies import PoliciesNamespace
|
|
41
|
+
from .projects import ProjectsNamespace
|
|
42
|
+
from .realtime import RealtimeNamespace
|
|
43
|
+
from .storage import StorageNamespace
|
|
44
|
+
from .studio import StudioNamespace
|
|
45
|
+
from .url_grammar import resolve_base_url
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class K9oClient:
|
|
49
|
+
"""The client returned by `create_client(...)`.
|
|
50
|
+
|
|
51
|
+
Namespaces hang off the instance — composed from per-resource
|
|
52
|
+
factories sharing the bound HTTP config.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
projects — `client.projects.list()` / `.create(...)` / `.get(slug)` / `.update(slug, ...)`
|
|
56
|
+
gate — `client.gate.sign_in(...)` / `.sign_up(...)` / `.whoami()` / etc. (requires project_slug)
|
|
57
|
+
kibo — `client.kibo.vault.secrets.{list,get,create,reveal_challenge,reveal,update,rotate,move,delete}`
|
|
58
|
+
(ZK vault-secrets surface, ch08 §14; requires project_slug)
|
|
59
|
+
storage — `client.storage.{create_bucket,get_bucket,list_buckets,update_bucket,delete_bucket}` and
|
|
60
|
+
`client.storage.from_(bucket).{upload,create_signed_url,list,remove}` (each with an `_async`
|
|
61
|
+
twin; requires project_slug)
|
|
62
|
+
|
|
63
|
+
Methods:
|
|
64
|
+
from_(collection) — data-plane DSL; returns a FromBuilder
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, config: HttpClientConfig) -> None:
|
|
68
|
+
self._config = config
|
|
69
|
+
self.projects = ProjectsNamespace(config)
|
|
70
|
+
self.gate = GateNamespace(config)
|
|
71
|
+
self.migrations = MigrationsNamespace(config)
|
|
72
|
+
self.functions = FunctionsNamespace(config)
|
|
73
|
+
self.policies = PoliciesNamespace(config)
|
|
74
|
+
self.audit = AuditNamespace(config)
|
|
75
|
+
self.orgs = OrgsNamespace(config)
|
|
76
|
+
self.me = MeNamespace(config)
|
|
77
|
+
self.studio = StudioNamespace(config)
|
|
78
|
+
self.compute = ComputeNamespace(config)
|
|
79
|
+
self.ai = AiNamespace(config)
|
|
80
|
+
self.realtime = RealtimeNamespace(config)
|
|
81
|
+
self.org = OrgNamespace(config)
|
|
82
|
+
self.kibo = KiboNamespace(config)
|
|
83
|
+
self.storage = StorageNamespace(config)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def config(self) -> HttpClientConfig:
|
|
87
|
+
"""Bound config (read-only access for advanced use)."""
|
|
88
|
+
return self._config
|
|
89
|
+
|
|
90
|
+
def from_(self, collection: str) -> FromBuilder:
|
|
91
|
+
"""Data-plane DSL entry point. `from` is a Python keyword so we use `from_`."""
|
|
92
|
+
return FromBuilder(self._config, collection)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_client(
|
|
96
|
+
*,
|
|
97
|
+
base_url: str | None = None,
|
|
98
|
+
api_key: str | None = None,
|
|
99
|
+
org_slug: str | None = None,
|
|
100
|
+
project_slug: str | None = None,
|
|
101
|
+
default_headers: dict[str, str] | None = None,
|
|
102
|
+
retry_config: RetryConfig | None = None,
|
|
103
|
+
) -> K9oClient:
|
|
104
|
+
"""Construct a K9oClient bound to the supplied gateway baseUrl + auth + context.
|
|
105
|
+
|
|
106
|
+
base_url is OPTIONAL (ch13 §7.1): when omitted it defaults to the platform
|
|
107
|
+
apex 'https://api.k9o.io'; when the host is the bare apex and org_slug is
|
|
108
|
+
set, the SDK derives the canonical org-subdomain root
|
|
109
|
+
'https://<orgSlug>.api.k9o.io'. Any other host (explicit subdomain,
|
|
110
|
+
self-hosted, dev) is used unchanged. Validates baseUrl at construct time
|
|
111
|
+
per ch13 §7.2 — path-bearing URLs raise KError(K9O_INVALID_BASE_URL).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
base_url: optional scheme + host (e.g. 'https://acme.api.k9o.io' or
|
|
115
|
+
'https://api.k9o.io'); defaults to 'https://api.k9o.io'
|
|
116
|
+
api_key: bearer token for default auth (per-call override is supported)
|
|
117
|
+
org_slug: active org context; sent as 'x-k9o-org-slug' header
|
|
118
|
+
project_slug: active project context; sent as 'x-k9o-project-slug' header
|
|
119
|
+
default_headers: headers merged into every request
|
|
120
|
+
retry_config: retry/backoff policy (ch13 §7.1); None ⇒ defaults
|
|
121
|
+
(3 retries, exponential) on idempotent methods + transient failures
|
|
122
|
+
"""
|
|
123
|
+
resolved = resolve_base_url(base_url, org_slug)
|
|
124
|
+
config = HttpClientConfig(
|
|
125
|
+
base_url=resolved,
|
|
126
|
+
api_key=api_key,
|
|
127
|
+
org_slug=org_slug,
|
|
128
|
+
project_slug=project_slug,
|
|
129
|
+
default_headers=dict(default_headers or {}),
|
|
130
|
+
retry_config=retry_config,
|
|
131
|
+
)
|
|
132
|
+
return K9oClient(config)
|