contextos-client 0.1.0a13__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.
contextos/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """ContextOS kernel for Python, in-process.
2
+
3
+ The kernel is pure and deterministic: no I/O, no clock, no randomness. Verdicts
4
+ (deny, rejected, insufficient evidence) are returned as results; ``ContextOSError``
5
+ is raised only for unusable input.
6
+
7
+ import contextos
8
+ compiled = contextos.compile(pack=pack, request=request, run_context=run_context)
9
+ decision = contextos.evaluate_authorization(authorization_input)
10
+ """
11
+
12
+ import json
13
+ from typing import Any, Optional
14
+
15
+ try:
16
+ from . import _native
17
+ except ImportError: # the HTTP client (contextos.client) works without the extension
18
+ _native = None
19
+
20
+ __version__ = getattr(_native, "__version__", "0.1.0-alpha.13")
21
+
22
+
23
+ class ContextOSError(Exception):
24
+ """Raised when the kernel cannot evaluate the input."""
25
+
26
+ def __init__(self, code: str, message: str, detail: Any = None):
27
+ super().__init__(f"{code}: {message}")
28
+ self.code = code
29
+ self.detail = detail
30
+
31
+
32
+ def call(operation: str, payload: Any) -> Any:
33
+ """Runs one kernel operation; see the embed facade for the operation list."""
34
+ if _native is None:
35
+ raise ContextOSError("native_unavailable", "the in-process kernel is not built: run `pip install ./bindings/python` (needs maturin and Rust); contextos.client works without it")
36
+ response = json.loads(_native.call(operation, json.dumps(payload, allow_nan=False)))
37
+ if "error" in response:
38
+ error = response["error"]
39
+ raise ContextOSError(error.get("code", "error"), error.get("message", ""), error.get("detail"))
40
+ return response["ok"]
41
+
42
+
43
+ def compile(pack: dict, request: dict, run_context: dict, deps: Optional[dict] = None) -> dict:
44
+ """Compiles a Context Pack into a CompiledContext."""
45
+ return call("compile", {"pack": pack, "request": request, "run_context": run_context, "deps": deps or {}})
46
+
47
+
48
+ def evaluate_authorization(authorization_input: dict) -> dict:
49
+ """Conjunctive authorization of one action binding."""
50
+ return call("evaluate_authorization", authorization_input)
51
+
52
+
53
+ def digest_action_binding(binding: dict) -> str:
54
+ """RFC 8785 digest of an ActionBinding."""
55
+ return call("digest_action_binding", binding)["action_digest"]
56
+
57
+
58
+ def negotiate(contract: dict, support: dict, features: dict) -> dict:
59
+ """Negotiates an execution contract against the standard profile registry."""
60
+ return call("negotiate", {"contract": contract, "support": support, "features": features})
61
+
62
+
63
+ def validate(schema: str, instance: Any, definition: Optional[str] = None) -> dict:
64
+ """Validates a payload against a published ContextOS schema."""
65
+ return call("validate", {"schema": schema, "def": definition, "instance": instance})
66
+
67
+
68
+ def canonicalize(value: Any) -> dict:
69
+ """RFC 8785 canonical JSON and its sha256 digest."""
70
+ return call("canonicalize", value)
71
+
72
+
73
+ __all__ = [
74
+ "ContextOSError", "call", "compile", "evaluate_authorization", "digest_action_binding",
75
+ "negotiate", "validate", "canonicalize", "__version__",
76
+ ]
contextos/aio.py ADDED
@@ -0,0 +1,119 @@
1
+ """The ``contextosd`` client for asyncio agents (ADK, the OpenAI Agents SDK, LlamaIndex,
2
+ Microsoft Agent Framework, Pydantic AI and other async frameworks).
3
+
4
+ ``AsyncClient`` and ``AsyncRun`` have the methods of ``Client`` and ``Run`` as
5
+ coroutines. The client stays standard-library only: each request runs the synchronous
6
+ client in a worker thread (``asyncio.to_thread``), so the event loop never blocks on
7
+ the network while urllib keeps handling TLS, proxies and retries. Outcomes raise the
8
+ same exceptions (``ToolPaused``, ``BudgetExhausted``, ...)::
9
+
10
+ from contextos.aio import AsyncClient
11
+ from contextos.client import governed_tool
12
+
13
+ client = AsyncClient("https://contextos.internal", token=os.environ["CONTEXTOS_TOKEN"])
14
+ run = await client.admit(admission_body)
15
+
16
+ @governed_tool(run, "adp_orders.lookup") # an async tool, since run is an AsyncRun
17
+ async def lookup_order(order_id: str) -> dict: ...
18
+
19
+ async with run.budgeted_model_call(tokens=2000):
20
+ response = await model.generate(...)
21
+
22
+ Concurrent calls share the default thread pool; pass ``executor`` to bound them
23
+ separately.
24
+ """
25
+ import asyncio
26
+ import contextlib
27
+ import functools
28
+ from concurrent.futures import Executor
29
+ from typing import Any, AsyncIterator, List, Optional
30
+
31
+ from .client import Client, Run
32
+
33
+ __all__ = ["AsyncClient", "AsyncRun"]
34
+
35
+
36
+ class _Async:
37
+ """Exposes a synchronous object's methods as coroutines run in a worker thread;
38
+ plain attributes read through. A returned ``Run`` comes back as an ``AsyncRun``."""
39
+
40
+ def __init__(self, target: Any, executor: Optional[Executor]):
41
+ self._target = target
42
+ self._executor = executor
43
+
44
+ async def _call(self, function, *args: Any, **kwargs: Any) -> Any:
45
+ args = tuple(a._target if isinstance(a, AsyncRun) else a for a in args)
46
+ loop = asyncio.get_running_loop()
47
+ result = await loop.run_in_executor(self._executor, functools.partial(function, *args, **kwargs))
48
+ if isinstance(result, Run):
49
+ return self if result is self._target else AsyncRun(result, self._executor)
50
+ return result
51
+
52
+ def __getattr__(self, name: str) -> Any:
53
+ attribute = getattr(self._target, name)
54
+ if not callable(attribute):
55
+ return attribute
56
+
57
+ @functools.wraps(attribute)
58
+ async def method(*args: Any, **kwargs: Any) -> Any:
59
+ return await self._call(attribute, *args, **kwargs)
60
+
61
+ return method
62
+
63
+
64
+ class AsyncClient(_Async):
65
+ """``Client`` for asyncio; see the module docstring."""
66
+
67
+ def __init__(self, base_url: str, token: str, timeout: float = 30.0, max_retries: int = 3,
68
+ executor: Optional[Executor] = None, *, allow_http: bool = False):
69
+ super().__init__(Client(base_url, token, timeout, max_retries, allow_http=allow_http), executor)
70
+
71
+ def __repr__(self) -> str:
72
+ return f"Async{self._target!r}"
73
+
74
+ async def admit(self, body: dict, principal_chain: Optional[List[dict]] = None) -> "AsyncRun":
75
+ return await self._call(self._target.admit, body, principal_chain)
76
+
77
+ async def attach(self, run_id: str, trace_id: str, principal_chain: List[dict]) -> "AsyncRun":
78
+ return await self._call(self._target.attach, run_id, trace_id, principal_chain)
79
+
80
+ async def audit_entries(self, start: int = 1, page: int = 500) -> AsyncIterator[dict]:
81
+ """Yields the tenant's signed audit entries in order, a page per request."""
82
+ cursor: Optional[int] = start
83
+ while cursor is not None:
84
+ batch = await self._call(self._target.request, "GET", "/v1/audit/entries",
85
+ query={"from": cursor, "limit": page})
86
+ for entry in batch["entries"]:
87
+ yield entry
88
+ cursor = batch.get("next")
89
+
90
+
91
+ class AsyncRun(_Async):
92
+ """``Run`` for asyncio; see the module docstring."""
93
+
94
+ def __init__(self, run: Run, executor: Optional[Executor] = None):
95
+ super().__init__(run, executor)
96
+
97
+ def __repr__(self) -> str:
98
+ return f"Async{self._target!r}"
99
+
100
+ async def call_tool(self, capability_id: str, args: dict, **options: Any) -> dict:
101
+ return await self._call(self._target.call_tool, capability_id, args, **options)
102
+
103
+ def metering_headers(self, reservation_id: str) -> dict:
104
+ return self._target.metering_headers(reservation_id)
105
+
106
+ @contextlib.asynccontextmanager
107
+ async def budgeted_model_call(self, *, tokens: int, cost_cents: int = 0,
108
+ metered: bool = False) -> AsyncIterator[str]:
109
+ """As ``Run.budgeted_model_call``: reserves before one model call and settles
110
+ after it; ``metered=True`` leaves settlement to a metering LLM gateway."""
111
+ reservation = await self._call(self._target.reserve_usage, "model_call", tokens=tokens, cost_cents=cost_cents)
112
+ settle = self._target._settle_open if metered else self._target.settle_usage
113
+ try:
114
+ yield reservation
115
+ except BaseException:
116
+ await self._call(settle, reservation)
117
+ raise
118
+ if not metered:
119
+ await self._call(settle, reservation)
contextos/client.py ADDED
@@ -0,0 +1,602 @@
1
+ """HTTP client for ``contextosd``: govern an agent's tool calls from Python.
2
+
3
+ Standard library only. Typical use::
4
+
5
+ from contextos.client import Client, governed_tool
6
+
7
+ client = Client("https://contextos.internal", token=os.environ["CONTEXTOS_TOKEN"])
8
+ run = client.admit(admission_body) # holds the executor generation
9
+ result = run.call_tool("adp_payments.issue_refund",
10
+ {"order_id": "ord_881", "amount_minor": 1200},
11
+ idempotency_key="refund:ord_881",
12
+ approval_mode="destructive",
13
+ evidence_refs=["kg:order:ord_881#snapshot"])
14
+
15
+ @governed_tool(run, "adp_orders.lookup", approval_mode="read_only")
16
+ def lookup_order(order_id: str): ... # body never runs; the gateway dispatches
17
+
18
+ Results that are not plain success raise typed exceptions, so agent code cannot
19
+ mistake a pause, a refusal or an unknown outcome for a result:
20
+
21
+ - ``ToolPaused``: approval pending, input required, or the outcome is unknown and
22
+ must be reconciled (never retry it blindly);
23
+ - ``ToolBlocked``: authority, policy, constraints or budget refused it;
24
+ - ``ToolRejected``: malformed, unsurfaced or incompatible reuse;
25
+ - ``ToolFailed``: the provider definitively did not apply the effect.
26
+
27
+ A model-call reservation the run cannot afford raises ``BudgetExhausted``. For asyncio
28
+ agents, ``contextos.aio`` has the same client as coroutines.
29
+
30
+ The client retries only requests the server did not process (``429``, ``503``),
31
+ honouring ``Retry-After``. It never retries a tool call after a lost response on
32
+ its own: the gateway's operation ledger makes a retry with the same idempotency key
33
+ safe, but deciding to retry is the caller's.
34
+ """
35
+
36
+ import contextlib
37
+ import functools
38
+ import inspect
39
+ import json
40
+ import time
41
+ import urllib.error
42
+ import urllib.parse
43
+ import urllib.request
44
+ import uuid
45
+ import warnings
46
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
47
+
48
+ from .errors import (_HTTP_ERRORS, _OUTCOMES, BudgetExhausted, ContextOSHTTPError, IncompatibleServer, ToolBlocked,
49
+ ToolFailed, ToolOutcome, ToolPaused, ToolRejected, VersionMismatchWarning, _pause_kind)
50
+
51
+ GENERATION_HEADER = "ContextOS-Executor-Generation"
52
+ # On a model request, these tell a metering LLM gateway which reservation to settle
53
+ # with the usage it measures (see examples/metering-proxy).
54
+ RUN_HEADER = "ContextOS-Run-Id"
55
+ RESERVATION_HEADER = "ContextOS-Usage-Reservation"
56
+ # The HTTP API generation this client speaks; a server reports its own in
57
+ # ``GET /v1/deployment``.
58
+ API_VERSION = "v1"
59
+ # This client's release (scripts/package_clients.py checks it against Cargo.toml).
60
+ CLIENT_VERSION = "0.1.0-alpha.13"
61
+
62
+
63
+ def _check_versions(deployment: Any, client_version: str) -> dict:
64
+ """Raises on a different API generation; warns on a different or unreported release."""
65
+ deployment = deployment if isinstance(deployment, dict) else {}
66
+ api, runtime = deployment.get("api_version"), deployment.get("runtime_version")
67
+ if api is not None and api != API_VERSION:
68
+ raise IncompatibleServer(f"contextosd speaks API {api}; this client speaks {API_VERSION}")
69
+ if runtime is None:
70
+ warnings.warn(f"contextosd does not report its release (older than this client, {client_version}); "
71
+ "use the same release on both sides", VersionMismatchWarning, stacklevel=4)
72
+ elif runtime != client_version:
73
+ warnings.warn(f"contextosd is release {runtime}; this client is {client_version}. "
74
+ "Use the same release on both sides", VersionMismatchWarning, stacklevel=4)
75
+ return {"api_version": api, "runtime_version": runtime, "client_version": client_version}
76
+
77
+
78
+ def task_scope(root_run_id: str, purpose_id: str, capability_id: str, *, resources: List[str] = (),
79
+ destinations: List[str] = (), classification: str = "CONFIDENTIAL",
80
+ amount: Optional[tuple] = None, argument_constraints: List[dict] = ()) -> dict:
81
+ """One `AuthorityGrant` scope for a task authorization: this capability, on these
82
+ resources, to these destinations, up to ``amount`` = ``(minor_units, currency)``."""
83
+ scope: Dict[str, Any] = {
84
+ "root_run_id": root_run_id, "purpose_ids": [purpose_id], "capability_ids": [capability_id],
85
+ "resource_ids": list(resources), "destination_ids": list(destinations),
86
+ "classification_labels": [classification], "argument_constraints": list(argument_constraints)}
87
+ if amount is not None:
88
+ scope["amount_ceiling"] = {"minor_units": amount[0], "currency": amount[1]}
89
+ return scope
90
+
91
+
92
+ def task_authorization(issued_by: dict, scopes: List[dict], *, out_of_scope: str = "deny",
93
+ read_only: str = "allow", confirmation_ref: Optional[dict] = None,
94
+ max_effects: Optional[int] = None, amount_total: Optional[Tuple[int, str]] = None) -> dict:
95
+ """What the principal authorized for a run, for ``admit(body)`` as
96
+ ``body["task_authorization"]``. Effects outside every scope are refused
97
+ (``out_of_scope="deny"``) or wait for an exact approval (``"ask"``, raised as
98
+ ``ToolPaused`` with ``kind == "approval"``). ``max_effects`` and ``amount_total``
99
+ (``(minor_units, currency)``) bound the task as a whole, across all its effects."""
100
+ task: Dict[str, Any] = {"issued_by": issued_by, "scopes": scopes, "out_of_scope": out_of_scope,
101
+ "read_only": read_only}
102
+ if confirmation_ref is not None:
103
+ task["confirmation_ref"] = confirmation_ref
104
+ limits: Dict[str, Any] = {}
105
+ if max_effects is not None:
106
+ limits["max_effects"] = max_effects
107
+ if amount_total is not None:
108
+ limits["amount_total"] = {"minor_units": amount_total[0], "currency": amount_total[1]}
109
+ if limits:
110
+ task["limits"] = limits
111
+ return task
112
+
113
+
114
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
115
+ """Refuses redirects: urllib would resend the bearer token to wherever one points."""
116
+
117
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
118
+ return None
119
+
120
+
121
+ _OPENER = urllib.request.build_opener(_NoRedirect())
122
+ _LOOPBACK = {"localhost", "127.0.0.1", "::1"}
123
+
124
+
125
+ class Client:
126
+ """A connection to one ``contextosd`` deployment, authenticated as one caller.
127
+
128
+ The token is sent only to ``base_url``: redirects are not followed (a ``3xx`` raises
129
+ ``ContextOSHTTPError``), and plain ``http`` is refused except to a loopback address
130
+ unless ``allow_http=True`` (for example inside a private network you trust).
131
+
132
+ Before its first admission the client reads ``GET /v1/deployment`` once: a server
133
+ with another API generation raises ``IncompatibleServer``, another release warns
134
+ with ``VersionMismatchWarning``. ``check_server=False`` skips it."""
135
+
136
+ def __init__(self, base_url: str, token: str, timeout: float = 30.0, max_retries: int = 3, *,
137
+ allow_http: bool = False, check_server: bool = True):
138
+ parsed = urllib.parse.urlsplit(base_url)
139
+ if parsed.scheme not in ("https", "http") or not parsed.hostname:
140
+ raise ValueError("base_url must be an http(s) URL")
141
+ if parsed.scheme == "http" and parsed.hostname not in _LOOPBACK and not allow_http:
142
+ raise ValueError("base_url is plain http to a remote host: the token would travel in the clear; "
143
+ "use https, or pass allow_http=True")
144
+ self.base_url = base_url.rstrip("/")
145
+ self._token = token
146
+ self.timeout = timeout
147
+ self.max_retries = max_retries
148
+ self._server_checked = not check_server
149
+
150
+ def __repr__(self) -> str: # never print the token
151
+ return f"Client({self.base_url!r})"
152
+
153
+ def request(self, method: str, path: str, body: Any = None, generation: Optional[int] = None,
154
+ query: Optional[Dict[str, Any]] = None) -> Any:
155
+ url = self.base_url + path
156
+ if query:
157
+ url += "?" + urllib.parse.urlencode(query)
158
+ data = None if body is None else json.dumps(body, allow_nan=False).encode()
159
+ headers = {"Authorization": f"Bearer {self._token}", "Accept": "application/json"}
160
+ if data is not None:
161
+ headers["Content-Type"] = "application/json"
162
+ if generation is not None:
163
+ headers[GENERATION_HEADER] = str(generation)
164
+ for attempt in range(self.max_retries + 1):
165
+ request = urllib.request.Request(url, data=data, method=method, headers=headers)
166
+ try:
167
+ with _OPENER.open(request, timeout=self.timeout) as response:
168
+ payload = response.read()
169
+ return json.loads(payload) if payload else None
170
+ except urllib.error.HTTPError as error:
171
+ payload = error.read()
172
+ try:
173
+ decoded = json.loads(payload) if payload else {}
174
+ except ValueError:
175
+ decoded = {}
176
+ # Not processed: safe to retry after the server's hint.
177
+ if error.code in (429, 503) and attempt < self.max_retries:
178
+ time.sleep(_retry_after(error.headers.get("Retry-After"), attempt))
179
+ continue
180
+ failure = decoded.get("error") if isinstance(decoded, dict) else None
181
+ if isinstance(failure, dict):
182
+ code = failure.get("code", "error")
183
+ raise _HTTP_ERRORS.get(code, ContextOSHTTPError)(error.code, code, failure.get("message", ""),
184
+ failure.get("detail")) from None
185
+ # Lifecycle refusals return their outcome body with 409.
186
+ raise ContextOSHTTPError(error.code, "refused", json.dumps(decoded), decoded) from None
187
+ raise AssertionError("unreachable")
188
+
189
+ def check_server(self) -> dict:
190
+ """Compares the server's API generation and release with this client's (see the
191
+ class docstring) and returns the three versions."""
192
+ versions = _check_versions(self.request("GET", "/v1/deployment"), CLIENT_VERSION)
193
+ self._server_checked = True
194
+ return versions
195
+
196
+ def admit(self, body: dict, principal_chain: Optional[List[dict]] = None) -> "Run":
197
+ """Admits a run (``POST /v1/runs``) and returns a handle that owns it."""
198
+ if not self._server_checked:
199
+ self.check_server()
200
+ admitted = self.request("POST", "/v1/runs", body)
201
+ context = body.get("run_context", {})
202
+ agent = (context.get("agent") or {}).get("agent_id")
203
+ chain = principal_chain or ([{"kind": "agent", "id": agent}] if agent else [])
204
+ return Run(self, admitted["run_id"], context.get("trace_id", ""), admitted["executor_generation"], chain,
205
+ admitted)
206
+
207
+ def attach(self, run_id: str, trace_id: str, principal_chain: List[dict]) -> "Run":
208
+ """A handle on an existing run, at its current generation (no takeover)."""
209
+ status = self.request("GET", f"/v1/runs/{_quote(run_id)}")
210
+ return Run(self, run_id, trace_id, status["executor_generation"], principal_chain, status)
211
+
212
+ def amend_task_authorization(self, run_id: str, expected_revision: int, task: dict) -> dict:
213
+ """Corrects what a run's principal authorized. Narrowing is open to the executor;
214
+ widening needs an ``authorize`` token that does not run tasks, the principal's
215
+ ``confirmation_ref`` and the same ``issued_by``. Returns the new revision."""
216
+ return self.request("PUT", f"/v1/runs/{_quote(run_id)}/task-authorization",
217
+ {"expected_revision": expected_revision, "task_authorization": task})
218
+
219
+ def reconcile(self, run_id: str, operation_id: str, *, outcome: Optional[str] = None,
220
+ evidence_ref: Optional[dict] = None) -> dict:
221
+ """Resolves another principal's unknown effect as an independent reconciler (a
222
+ token with ``reconcile`` that did not admit the run): by provider query, or by
223
+ asserting ``outcome`` with evidence. No executor generation is sent."""
224
+ body = {"source": "provider"} if outcome is None else \
225
+ {"source": "operator", "outcome": outcome, "evidence_ref": evidence_ref}
226
+ return self.request("POST", f"/v1/runs/{_quote(run_id)}/operations/{_quote(operation_id)}/reconcile", body)
227
+
228
+ def report_outcome(self, run_id: str, operation_id: str, attestation: str) -> dict:
229
+ """Relays an outcome witness's signed reading (a compact JWS) for an applied effect
230
+ whose capability declares an outcome contract. Needs the ``outcome:report`` scope;
231
+ the signature, not the caller, is what the runtime trusts."""
232
+ return self.request("POST", f"/v1/runs/{_quote(run_id)}/operations/{_quote(operation_id)}/outcome",
233
+ {"attestation": attestation})
234
+
235
+ def score(self, run_id: str, record_id: str, evaluator_ref: str, *, policy: float, utility: float,
236
+ safety: float) -> dict:
237
+ """Scores a recorded decision as an independent evaluator (a client whose token
238
+ has only the `evaluate` scope). The runtime adds the measured cost and latency."""
239
+ return self.request("POST", f"/v1/runs/{_quote(run_id)}/decisions/{_quote(record_id)}/scores",
240
+ {"evaluator_ref": evaluator_ref, "policy": policy, "utility": utility, "safety": safety})
241
+
242
+ def file_feedback(self, run_id: str, kind: str, target_surface: str, summary: str, *,
243
+ decision_record_id: Optional[str] = None, expected_outcome: Optional[str] = None) -> dict:
244
+ """Files an operator correction or override against a run as an independent
245
+ reviewer (`evaluate` scope); the run's replay case becomes its replay obligation."""
246
+ body: Dict[str, Any] = {"kind": kind, "target_surface": target_surface, "summary": summary}
247
+ if decision_record_id is not None:
248
+ body["decision_record_id"] = decision_record_id
249
+ if expected_outcome is not None:
250
+ body["expected_outcome"] = expected_outcome
251
+ return self.request("POST", f"/v1/runs/{_quote(run_id)}/feedback", body)
252
+
253
+ def open_escalations(self, limit: int = 500) -> dict:
254
+ """The tenant's open policy escalations, oldest first (`audit:read`)."""
255
+ return self.request("GET", "/v1/escalations", query={"limit": limit})
256
+
257
+ def resolve_escalation(self, escalation_id: str, resolution: str, reason: str) -> dict:
258
+ """Records how a human resolved an escalation, `handled` or `dismissed` (`control`).
259
+ Grants nothing: an action the human approves is retried through policy."""
260
+ return self.request("POST", f"/v1/escalations/{_quote(escalation_id)}/resolve",
261
+ {"resolution": resolution, "reason": reason})
262
+
263
+ def open_feedback(self, limit: int = 500) -> dict:
264
+ """The tenant's open feedback, oldest first: the replay corpus a change must pass."""
265
+ return self.request("GET", "/v1/feedback", query={"limit": limit})
266
+
267
+ def close_feedback(self, feedback_id: str, status: str, *, artifact_type: str, artifact_id: str,
268
+ sha256: str) -> dict:
269
+ """Closes feedback as fixed, still_failing, invalid or superseded, with a reference
270
+ to the evidence (`control` scope)."""
271
+ return self.request("POST", f"/v1/feedback/{_quote(feedback_id)}/close", {
272
+ "status": status,
273
+ "evidence_ref": {"artifact_type": artifact_type, "artifact_id": artifact_id, "sha256": sha256}})
274
+
275
+ def revoke(self, kind: str, target: str, reason: str) -> dict:
276
+ """Kill switch (a client whose token has the `control` scope): revokes a `tenant`,
277
+ `pack`, `agent`, `adapter` or `capability` from the next dispatch and admission."""
278
+ return self.request("POST", "/v1/revocations", {"kind": kind, "target": target, "reason": reason})
279
+
280
+ def restore(self, kind: str, target: str, reason: str) -> dict:
281
+ """Restores a revoked target."""
282
+ return self.request("POST", "/v1/revocations/restore", {"kind": kind, "target": target, "reason": reason})
283
+
284
+ def revocations(self) -> dict:
285
+ """The tenant's revoked targets."""
286
+ return self.request("GET", "/v1/revocations")
287
+
288
+ def audit_entries(self, start: int = 1, page: int = 500):
289
+ """Yields the tenant's signed audit entries in order."""
290
+ cursor: Optional[int] = start
291
+ while cursor is not None:
292
+ batch = self.request("GET", "/v1/audit/entries", query={"from": cursor, "limit": page})
293
+ yield from batch["entries"]
294
+ cursor = batch.get("next")
295
+
296
+
297
+ class Run:
298
+ """One admitted run, held by this executor at ``executor_generation``."""
299
+
300
+ def __init__(self, client: Client, run_id: str, trace_id: str, executor_generation: int,
301
+ principal_chain: List[dict], admitted: Optional[dict] = None):
302
+ self.client = client
303
+ self.run_id = run_id
304
+ self.trace_id = trace_id
305
+ self.executor_generation = executor_generation
306
+ self.principal_chain = principal_chain
307
+ self.admitted = admitted or {}
308
+ # Peer notices that came with the latest tool result (peer data, never instructions).
309
+ self.last_notices: Optional[dict] = None
310
+
311
+ def __repr__(self) -> str:
312
+ return f"Run({self.run_id!r}, generation={self.executor_generation})"
313
+
314
+ def _path(self, suffix: str = "") -> str:
315
+ return f"/v1/runs/{_quote(self.run_id)}{suffix}"
316
+
317
+ def _post(self, suffix: str, body: Any = None) -> Any:
318
+ return self.client.request("POST", self._path(suffix), body if body is not None else {},
319
+ generation=self.executor_generation)
320
+
321
+ def envelope(self, capability_id: str, args: dict, *, approval_mode: str, idempotency_key: Optional[str] = None,
322
+ evidence_refs: Optional[List[str]] = None, tool_call_id: Optional[str] = None) -> dict:
323
+ """The ``contextos.tool_call.v1`` envelope for a call."""
324
+ return {
325
+ "envelope_version": "contextos.tool_call.v1",
326
+ "tool_call_id": tool_call_id or f"tc_{uuid.uuid4().hex}",
327
+ "run_id": self.run_id, "trace_id": self.trace_id, "capability_id": capability_id, "args": args,
328
+ "principal_chain": self.principal_chain, "approval_mode_requested": approval_mode,
329
+ "approval_mode_effective": approval_mode, "approval_mode_highest": approval_mode,
330
+ "evidence_refs": evidence_refs or [], "idempotency_key": idempotency_key,
331
+ }
332
+
333
+ def call_tool(self, capability_id: str, args: dict, *, approval_mode: str = "read_only",
334
+ idempotency_key: Optional[str] = None, evidence_refs: Optional[List[str]] = None,
335
+ tool_call_id: Optional[str] = None) -> dict:
336
+ """Governs and dispatches one tool call; returns its output or raises a ``ToolOutcome``."""
337
+ envelope = self.envelope(capability_id, args, approval_mode=approval_mode, idempotency_key=idempotency_key,
338
+ evidence_refs=evidence_refs, tool_call_id=tool_call_id)
339
+ response = self._post("/tool-calls", envelope)
340
+ self.last_notices = (response.get("result", {}).get("metadata") or {}).get("notices")
341
+ return _outcome(response)
342
+
343
+ def provide_input(self, paused: ToolPaused, envelope: dict, responses: dict) -> dict:
344
+ """Answers a tool's input requests and continues the same operation."""
345
+ body = {"tool_call": envelope, "input_responses": responses}
346
+ return _outcome(self._post(f"/operations/{_quote(paused.operation_id or '')}/input", body))
347
+
348
+ def reconcile(self, operation_id: str, *, outcome: Optional[str] = None,
349
+ evidence_ref: Optional[dict] = None) -> dict:
350
+ """Resolves an unknown effect: by provider query, or by asserting ``outcome`` with evidence.
351
+ The run's own principal may only assert ``confirmed``; ``not_applied`` and ``partial``
352
+ need an independent reconciler (``Client.reconcile``)."""
353
+ body = {"source": "provider"} if outcome is None else \
354
+ {"source": "operator", "outcome": outcome, "evidence_ref": evidence_ref}
355
+ return self._post(f"/operations/{_quote(operation_id)}/reconcile", body)
356
+
357
+ def compensations(self) -> dict:
358
+ """The run's compensable effects and where each stands (``state``)."""
359
+ return self.client.request("GET", self._path("/compensations"))
360
+
361
+ def compensate(self, operation_id: str, *, tool_call_id: Optional[str] = None) -> dict:
362
+ """Compensates one applied effect with its declared compensating capability, governed
363
+ as a new operation; returns its output or raises a ``ToolOutcome``."""
364
+ body = {"tool_call_id": tool_call_id or f"tc_{uuid.uuid4().hex}"}
365
+ return _outcome(self._post(f"/operations/{_quote(operation_id)}/compensate", body))
366
+
367
+ def compensate_all(self) -> dict:
368
+ """Compensates the run's applied effects newest first, stopping at any in doubt.
369
+ Returns ``{"status": "compensated" | "stopped", "steps", "stopped_at"}``."""
370
+ return self._post("/compensate")
371
+
372
+ def record_decision(self, decision_key: str, status: str, *, decision_version: str = "1",
373
+ outcome: Optional[str] = None, tool_calls: Optional[List[dict]] = None, **fields: Any) -> dict:
374
+ """Records a DecisionRecord; returns ``{"record", "replay_packet"}``."""
375
+ body = {"decision_key": decision_key, "decision_version": decision_version, "status": status,
376
+ "tool_calls": tool_calls or [], **fields}
377
+ if outcome is not None:
378
+ body["outcome"] = outcome
379
+ return self._post("/decisions", body)
380
+
381
+ def delegate(self, child_admission: dict, envelope: dict) -> "Run":
382
+ """Admits a child run in a narrowed lane (``AgentDelegationEnvelope``). Its
383
+ budget is reserved from this run; this run must hold the current generation."""
384
+ body = dict(child_admission, delegation=envelope)
385
+ admitted = self.client.request("POST", "/v1/runs", body, generation=self.executor_generation)
386
+ context = child_admission.get("run_context", {})
387
+ return Run(self.client, admitted["run_id"], context.get("trace_id", self.trace_id), admitted["executor_generation"],
388
+ self.principal_chain, admitted)
389
+
390
+ def close_child(self, child: "Run", decision: str = "accept") -> dict:
391
+ """Closes a delegated child, settling its spend into this run."""
392
+ return self.client.request("POST", f"/v1/runs/{_quote(child.run_id)}/close", {"decision": decision},
393
+ generation=self.executor_generation)
394
+
395
+ def checkpoint(self, state: Any, expected_revision: int) -> dict:
396
+ """Saves the orchestrator's state as the next checkpoint revision."""
397
+ return self._post("/checkpoint", {"expected_revision": expected_revision, "state": state})
398
+
399
+ def latest_checkpoint(self) -> dict:
400
+ """The latest checkpoint (`state`, `revision`), e.g. after taking the run over."""
401
+ return self.client.request("GET", self._path("/checkpoint"), generation=self.executor_generation)
402
+
403
+ def publish_work(self, task_id: str, title: str, acceptance_criteria: str, *,
404
+ dependencies: Optional[List[str]] = None) -> dict:
405
+ """Publish immutable task metadata shared with this root's descendants.
406
+ Claims and worker-reported evidence confer no tool authority."""
407
+ return self._post("/work", {"task_id": task_id, "title": title,
408
+ "acceptance_criteria": acceptance_criteria, "dependencies": dependencies or []})
409
+
410
+ def work_tasks(self, *, after: Optional[str] = None, limit: int = 100) -> dict:
411
+ """A live page of tasks; restart from the beginning to see new earlier ids."""
412
+ query: Dict[str, Any] = {"limit": limit}
413
+ if after is not None:
414
+ query["after"] = after
415
+ return self.client.request("GET", self._path("/work"), query=query, generation=self.executor_generation)
416
+
417
+ def work_task(self, task_id: str) -> dict:
418
+ """Inspect current revision and lease, including predecessor provenance."""
419
+ return self.client.request("GET", self._path(f"/work/{_quote(task_id)}"), generation=self.executor_generation)
420
+
421
+ def update_work(self, task_id: str, action: str, expected_revision: int, *, lease_epoch: Optional[int] = None,
422
+ lease_seconds: Optional[int] = None, evidence_refs: Optional[List[dict]] = None) -> dict:
423
+ """Conditionally claim, renew, release or report completion. After a lost
424
+ response or conflict, inspect the task. Reclaiming a lease does not establish
425
+ that the previous worker's external effects are safe to repeat."""
426
+ body: Dict[str, Any] = {"action": action, "expected_revision": expected_revision}
427
+ if lease_epoch is not None:
428
+ body["lease_epoch"] = lease_epoch
429
+ if lease_seconds is not None:
430
+ body["lease_seconds"] = lease_seconds
431
+ if evidence_refs is not None:
432
+ body["evidence_refs"] = evidence_refs
433
+ return self._post(f"/work/{_quote(task_id)}", body)
434
+
435
+ def publish_finding(self, finding_id: str, statement: str, *, kind: str = "claim",
436
+ depends_on: Optional[List[str]] = None, basis_operation_id: Optional[str] = None,
437
+ basis_run_id: Optional[str] = None, refutes: Optional[str] = None,
438
+ urgent: bool = False) -> dict:
439
+ """Shares a finding with the team: a ``claim``, or an ``observed`` finding bound to a
440
+ recorded tool call (``basis_operation_id``, in ``basis_run_id`` or this run) whose
441
+ facts the runtime takes from its operation ledger; an observation that ``refutes``
442
+ a peer's finding can then invalidate it. ``urgent`` notifies the team."""
443
+ body: Dict[str, Any] = {"finding_id": finding_id, "kind": kind, "statement": statement,
444
+ "depends_on": depends_on or [], "urgent": urgent}
445
+ if basis_operation_id is not None:
446
+ body["kind"] = "observed"
447
+ body["basis"] = {"operation_id": basis_operation_id}
448
+ if basis_run_id is not None:
449
+ body["basis"]["run_id"] = basis_run_id
450
+ if refutes is not None:
451
+ body["refutes"] = refutes
452
+ return self._post("/findings", body)
453
+
454
+ def findings(self, *, after: Optional[str] = None, limit: int = 100) -> dict:
455
+ """A page of the team's findings with status (active, suspect, invalidated)."""
456
+ query: Dict[str, Any] = {"limit": limit}
457
+ if after is not None:
458
+ query["after"] = after
459
+ return self.client.request("GET", self._path("/findings"), query=query, generation=self.executor_generation)
460
+
461
+ def invalidate_finding(self, finding_id: str, reason: str, *, refuted_by: Optional[str] = None) -> dict:
462
+ """Falsifies a finding: withdraw your own, or cite an observed finding refuting a
463
+ peer's. Its dependents become suspect and the team is notified."""
464
+ body: Dict[str, Any] = {"reason": reason}
465
+ if refuted_by is not None:
466
+ body["refuted_by"] = refuted_by
467
+ return self._post(f"/findings/{_quote(finding_id)}/invalidate", body)
468
+
469
+ def notices(self, *, limit: int = 20) -> dict:
470
+ """This run's unacknowledged peer notices; reading does not acknowledge them."""
471
+ return self.client.request("GET", self._path("/notices"), query={"limit": limit},
472
+ generation=self.executor_generation)
473
+
474
+ def ack_notices(self, through: int) -> dict:
475
+ """Acknowledges notices through sequence ``through`` once the agent has seen them."""
476
+ return self._post("/notices/ack", {"through": through})
477
+
478
+ def reserve_usage(self, kind: str, *, tokens: int = 0, cost_cents: int = 0) -> str:
479
+ """Reserves budget for a `model_call` or a `replan`; returns the reservation id."""
480
+ return self._post("/usage/reserve", {"kind": kind, "tokens": tokens, "cost_cents": cost_cents})["reservation_id"]
481
+
482
+ def settle_usage(self, reservation_id: str) -> dict:
483
+ """Settles a reservation as the executor: the whole reservation is charged. Pass
484
+ the reservation id to your metering gateway instead to settle measured usage."""
485
+ return self._post("/usage/settle", {"reservation_id": reservation_id})
486
+
487
+ def metering_headers(self, reservation_id: str) -> Dict[str, str]:
488
+ """Headers for a model request that a metering LLM gateway settles with the
489
+ usage it measures (``budgeted_model_call(..., metered=True)``)."""
490
+ return {RUN_HEADER: self.run_id, RESERVATION_HEADER: reservation_id}
491
+
492
+ @contextlib.contextmanager
493
+ def budgeted_model_call(self, *, tokens: int, cost_cents: int = 0, metered: bool = False) -> Iterator[str]:
494
+ """Reserves budget for one model call and settles it afterwards, even if the call
495
+ raises. Raises ``BudgetExhausted`` before the call when the run cannot afford it.
496
+
497
+ ``metered=True`` leaves settlement to a metering LLM gateway (send
498
+ ``metering_headers(reservation)`` with the model request), which charges what it
499
+ measured. If the call raises, a reservation the gateway did not settle is settled
500
+ here, charging all of it: unknown usage is never free."""
501
+ reservation = self.reserve_usage("model_call", tokens=tokens, cost_cents=cost_cents)
502
+ try:
503
+ yield reservation
504
+ except BaseException:
505
+ if metered:
506
+ self._settle_open(reservation)
507
+ else:
508
+ self.settle_usage(reservation)
509
+ raise
510
+ if not metered:
511
+ self.settle_usage(reservation)
512
+
513
+ def _settle_open(self, reservation_id: str) -> None:
514
+ try:
515
+ self.settle_usage(reservation_id)
516
+ except ContextOSHTTPError as error:
517
+ if error.status != 404: # 404: already settled by the gateway
518
+ raise
519
+
520
+ def status(self) -> dict:
521
+ return self.client.request("GET", self._path())
522
+
523
+ def take_over(self) -> "Run":
524
+ """Takes the run over from whichever executor held it; the old one becomes stale."""
525
+ current = self.status()["executor_generation"]
526
+ taken = self.client.request("POST", self._path("/takeover"), {"expected_generation": current})
527
+ self.executor_generation = taken["executor_generation"]
528
+ return self
529
+
530
+ def complete(self) -> dict:
531
+ return self._post("/complete")
532
+
533
+ def fail(self) -> dict:
534
+ return self._post("/fail")
535
+
536
+ def cancel(self) -> dict:
537
+ return self.client.request("POST", self._path("/cancel"), {})
538
+
539
+ def evidence(self) -> dict:
540
+ return self.client.request("GET", self._path("/evidence"))
541
+
542
+
543
+ def governed_tool(run: Run, capability_id: str, *, approval_mode: str = "read_only",
544
+ idempotency_key: Optional[Callable[..., str]] = None,
545
+ evidence_refs: Optional[List[str]] = None,
546
+ tool_call_id: Optional[Callable[..., str]] = None) -> Callable[[Callable[..., Any]], Callable[..., dict]]:
547
+ """Turns a tool's signature into a governed call: arguments are bound by name and
548
+ sent through the gateway; the decorated body never runs locally.
549
+
550
+ ``idempotency_key`` derives the operation key from the call's arguments, so a
551
+ retried agent step maps to the same operation and cannot apply twice.
552
+ ``tool_call_id`` derives the call's id the same way, so a DecisionRecord can cite it.
553
+ With an ``AsyncRun`` (``contextos.aio``) the governed function is a coroutine.
554
+ """
555
+
556
+ def wrap(function: Callable[..., Any]) -> Callable[..., Any]:
557
+ signature = inspect.signature(function)
558
+
559
+ def dispatch(args: Any, kwargs: Any) -> Any:
560
+ bound = signature.bind(*args, **kwargs)
561
+ bound.apply_defaults()
562
+ arguments = dict(bound.arguments)
563
+ key = idempotency_key(**arguments) if idempotency_key else None
564
+ call_id = tool_call_id(**arguments) if tool_call_id else None
565
+ return run.call_tool(capability_id, arguments, approval_mode=approval_mode, idempotency_key=key,
566
+ evidence_refs=evidence_refs, tool_call_id=call_id)
567
+
568
+ if inspect.iscoroutinefunction(run.call_tool):
569
+ @functools.wraps(function)
570
+ async def call_async(*args: Any, **kwargs: Any) -> dict:
571
+ return await dispatch(args, kwargs)
572
+
573
+ return call_async
574
+
575
+ @functools.wraps(function)
576
+ def call(*args: Any, **kwargs: Any) -> dict:
577
+ return dispatch(args, kwargs)
578
+
579
+ return call
580
+
581
+ return wrap
582
+
583
+
584
+ def _outcome(response: dict) -> dict:
585
+ result, report = response["result"], response["report"]
586
+ status = result.get("status")
587
+ if status in ("ok", "completed"):
588
+ return result.get("output") or {}
589
+ raise _OUTCOMES.get(status, ToolOutcome)(result, report)
590
+
591
+
592
+ def _retry_after(header: Optional[str], attempt: int) -> float:
593
+ try:
594
+ return min(float(header), 30.0) if header else min(0.2 * 2 ** attempt, 5.0)
595
+ except ValueError:
596
+ return min(0.2 * 2 ** attempt, 5.0)
597
+
598
+
599
+ def _quote(segment: str) -> str:
600
+ if segment in ("", ".", ".."):
601
+ raise ValueError(f"invalid path segment {segment!r}")
602
+ return urllib.parse.quote(segment, safe="")
contextos/errors.py ADDED
@@ -0,0 +1,91 @@
1
+ """What the ``contextosd`` client raises: refused requests, governed tool outcomes that
2
+ are not plain success, and an incompatible server. Import them from
3
+ ``contextos.client``, which re-exports every name here."""
4
+ from typing import Any, List, Optional
5
+
6
+
7
+ class ContextOSHTTPError(Exception):
8
+ """The server refused the request (4xx/5xx other than a governed tool result)."""
9
+
10
+ def __init__(self, status: int, code: str, message: str, detail: Any = None):
11
+ super().__init__(f"{status} {code}: {message}")
12
+ self.status = status
13
+ self.code = code
14
+ self.detail = detail
15
+
16
+
17
+ class BudgetExhausted(ContextOSHTTPError):
18
+ """The run's budget cannot fit a model-call or replan reservation (``reasons``
19
+ says which limit). Stop the loop: retrying the same reservation cannot succeed."""
20
+
21
+ @property
22
+ def reasons(self) -> List[str]:
23
+ return self.detail if isinstance(self.detail, list) else []
24
+
25
+
26
+ _HTTP_ERRORS = {"budget_exhausted": BudgetExhausted}
27
+
28
+
29
+ class IncompatibleServer(Exception):
30
+ """The server speaks a different HTTP API generation than this client."""
31
+
32
+
33
+ class VersionMismatchWarning(UserWarning):
34
+ """The server is a different ContextOS release than this client. Releases before
35
+ 1.0 may change the API, so use the same release on both sides."""
36
+
37
+
38
+ class ToolOutcome(Exception):
39
+ """A governed tool call that did not simply succeed."""
40
+
41
+ def __init__(self, result: dict, report: dict):
42
+ self.result = result
43
+ self.report = report
44
+ self.status = result.get("status")
45
+ self.reason_codes: List[str] = report.get("reason_codes", [])
46
+ self.obligations: List[str] = report.get("obligations", [])
47
+ self.operation_id: Optional[str] = report.get("operation_id")
48
+ super().__init__(f"{self.status}: {', '.join(self.reason_codes) or result.get('error') or ''}")
49
+
50
+
51
+ class ToolPaused(ToolOutcome):
52
+ """Approval pending, input required, or an unknown outcome awaiting reconciliation."""
53
+
54
+ @property
55
+ def kind(self) -> str:
56
+ """What the pause waits on: ``approval`` (redeem by re-sending the same call once
57
+ approved), ``input`` (answer with ``provide_input``), ``unknown_outcome`` (the
58
+ effect may have happened: reconcile), ``duplicate`` (this operation was already
59
+ requested and is not repeated) or ``other``."""
60
+ return _pause_kind(self.obligations, self.reason_codes)
61
+
62
+ @property
63
+ def input_requests(self) -> Optional[dict]:
64
+ return (self.result.get("output") or {}).get("result", {}).get("inputRequests")
65
+
66
+
67
+ class ToolBlocked(ToolOutcome):
68
+ """Authority, policy, constraints or budget refused the call."""
69
+
70
+
71
+ class ToolRejected(ToolOutcome):
72
+ """The call was malformed, not surfaced to this run, or reused an operation key."""
73
+
74
+
75
+ class ToolFailed(ToolOutcome):
76
+ """The provider definitively did not apply the effect."""
77
+
78
+
79
+ def _pause_kind(obligations: List[str], reason_codes: List[str]) -> str:
80
+ if "await_approval" in obligations:
81
+ return "approval"
82
+ if "provide_requested_input" in obligations:
83
+ return "input"
84
+ if "reconcile_effect_before_retry" in obligations:
85
+ return "unknown_outcome"
86
+ if "retry_unsafe" in reason_codes or "reconcile_existing_operation" in obligations:
87
+ return "duplicate"
88
+ return "other"
89
+
90
+
91
+ _OUTCOMES = {"paused": ToolPaused, "blocked": ToolBlocked, "rejected": ToolRejected, "failed": ToolFailed}
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.1
2
+ Name: contextos-client
3
+ Version: 0.1.0a13
4
+ Summary: ContextOS kernel: governed context, authorization and durable effects for AI agents
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.9
7
+ Project-URL: Source, https://github.com/contextosai/ContextOS
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Description-Content-Type: text/markdown
11
+
12
+ # ContextOS for Python
13
+
14
+ Two modules, one package:
15
+
16
+ - `contextos.client` (and `contextos.aio` for asyncio) talks to `contextosd` over
17
+ HTTP. Standard library only: no compiled code, no dependencies.
18
+ - `contextos` itself is the kernel, in-process, through a compiled extension
19
+ (`contextos._native`). You need it only to embed the kernel without a server.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ pip install --pre contextos-client
25
+ ```
26
+
27
+ Published on PyPI as `contextos-client` from 0.1.0-alpha.13; the module is still
28
+ `import contextos`. Linux (x86_64 and aarch64, glibc 2.28 or newer) and macOS on
29
+ Apple silicon get a wheel with the compiled kernel. Every other platform gets
30
+ a pure-Python wheel with the HTTP client only; kernel calls there raise
31
+ `native_unavailable`. Use the client release that matches your `contextosd`: before its
32
+ first admission the client warns (`VersionMismatchWarning`) when the server is another
33
+ release, and refuses (`IncompatibleServer`) a server with another API generation.
34
+
35
+ ## The HTTP client (most agents)
36
+
37
+ From a checkout instead of the package index, put the package on your path or copy
38
+ `bindings/python/contextos/` into your project:
39
+
40
+ ```sh
41
+ export PYTHONPATH=/path/to/ContextOS/bindings/python
42
+ python3 -c "from contextos.client import Client; print('ok')"
43
+ ```
44
+
45
+ Try it against a local server: `./examples/dev-server.sh` prints `CONTEXTOS_URL` and
46
+ `CONTEXTOS_TOKEN`, and `examples/support-refund/requests/run.json` is a run you can
47
+ admit. `examples/frameworks/` shows the client inside eleven agent frameworks.
48
+
49
+ ## The embedded kernel
50
+
51
+ The platform wheels above include it. To build the extension yourself, use
52
+ [maturin](https://www.maturin.rs/) (needs the Rust toolchain):
53
+
54
+ ```sh
55
+ python3 -m pip install maturin
56
+ python3 -m pip install ./bindings/python # or: cd bindings/python && maturin develop
57
+ python3 -c "import contextos; print(contextos.__version__)"
58
+ ```
59
+
60
+ Without it, `import contextos` still works for the client; kernel calls raise an error
61
+ that says to build the extension.
@@ -0,0 +1,9 @@
1
+ contextos/__init__.py,sha256=pgUa4PhD2hR5b-lw8MZELmkxA4cqqVmvJq5NsP744Vo,2976
2
+ contextos/client.py,sha256=fl98lzwx8AxDQsm-eutKwqpzQ_Jl-UDo8C5m-Mb4XbE,33434
3
+ contextos/errors.py,sha256=IVU4Td2ihJxkASw2m_eQsHpuRnqd9ZxabStzUhc5DO4,3432
4
+ contextos/aio.py,sha256=6uUycooyBXtueitp82GopKw7hXC0MlKiBYrPUSfg_2o,5129
5
+ contextos_client-0.1.0a13.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
6
+ contextos_client-0.1.0a13.dist-info/licenses/NOTICE,sha256=lMzC_abhf9T5x-5zPUG6WWLFdhu1O493F4S0xljAOg4,894
7
+ contextos_client-0.1.0a13.dist-info/METADATA,sha256=4r6Dy--DAe4EsXRFVd1huYHsacorOBNBqK6OKCWQSes,2413
8
+ contextos_client-0.1.0a13.dist-info/WHEEL,sha256=j5NxlqV4xFdG6alboEDVE7NM2YEmUYICLhdZyMk-S7I,99
9
+ contextos_client-0.1.0a13.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: contextos package_clients.py
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,20 @@
1
+ ContextOS
2
+ Copyright 2026 The ContextOS Authors
3
+
4
+ This product includes software developed as part of the ContextOS project
5
+ (https://github.com/contextosai/ContextOS), licensed under the Apache License,
6
+ Version 2.0 (see LICENSE).
7
+
8
+ Third-party dependency licenses and attribution are in THIRD_PARTY_NOTICES.txt.
9
+
10
+ conformance/reference/ includes the unmodified deterministic TypeScript reference
11
+ and synthetic fixtures from contextosai/contextosai, revision
12
+ 7e2462fd931a79e3777ede81fb382e0cbf2ff46c.
13
+ Copyright (c) 2025-2026 ContextOS; licensed under Apache-2.0.
14
+
15
+ Documentation prose in docs/ and the Markdown guides is licensed under the
16
+ Creative Commons Attribution 4.0 International license (see LICENSE-docs);
17
+ code examples within it are licensed under Apache-2.0.
18
+
19
+ "ContextOS" and its logo are trademarks of the ContextOS project; neither
20
+ license grants rights to use them (see TRADEMARKS.md).