Metadata-Version: 2.5
Name: zetify-app
Version: 0.1.0
Summary: Python SDK for Zetify apps — the governed client a customer app uses to reach the Zetify platform.
Project-URL: Homepage, https://zetify.ai
Author: Zetify
Keywords: agents,apps,sdk,zetify
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.13
Description-Content-Type: text/markdown

# app-sdk (`zetify_app`)

The Python app SDK — the one client a customer app imports to reach the platform
from inside a daemon-hosted sandbox.

## The governed verbs

`open_app()` returns the handle you hold for the whole invocation. Call the
three governed verbs on it as many times as you need:

```python
import zetify_app

with zetify_app.open_app() as app:
    rows = app.query("customers.list", {"limit": 10})  # the READ leg
    created = app.action("orders.create", {"sku": "widget"})  # the WRITE leg
    digest = app.synthesize("reports.summarize", {"doc_id": "d1"})
```

Each verb returns the decoded `summary` from the broker's Response. `params` is
placed on the wire as a JSON **object** — `brokeruds.Request.ParamsJSON` is a
`json.RawMessage` that the daemon forwards verbatim as the upstream request
body, so it must never be pre-serialised into a JSON string.

`app.action` is the only leg that may carry `out_path`, naming a local file
inside the invocation's file-scope root whose bytes the daemon merges into the
upstream call — that is how a result larger than the 1 MiB wire frame reaches
the platform without crossing the socket.

**Refusals are typed, never wire-shaped.** A platform refusal raises
`LimitExceeded` / `PolicyDenied` / `PlatformRefusal` / `AppUsageError` (all
under `TilionAppError`), and an outcome token this SDK version does not yet
recognise still surfaces as a catchable `PlatformRefusal` with the raw token on
`.outcome` — never dropped.

**Ceilings, never hand-copied.** `app.limits` is a read-only mapping of the
*effective* per-invocation ceilings the enforcing daemon resolved for this run
(`memory`, `cpu`, `processes`, `open_files`, `bytes`, `result_bytes` — only the
dimensions actually delivered appear as keys):

```python
budget = app.limits["result_bytes"]
```

## One Request → one Response per connection

Both real brokers — `internal/apprund/udsproxy.go`'s `handleConn` and
`internal/agentd/dataplane.go`'s `serveConn` — service **exactly one**
Request/Response exchange over a connection and then close it. So:

- `Connection` (from `connect()`) is a **single-exchange** object. Its socket is
  opened on `request()` and closed as soon as the Response is read; a second
  `request()` raises `TransportClosedError` rather than the bare
  `BrokenPipeError` the kernel would give you.
- `App` is the **reusable** handle. It opens a fresh `Connection` per governed
  verb, which is why repeated `query`/`action`/`synthesize` calls just work.

Apps do not need to manage any of this — use `open_app()`. The lower-level
`connect()` seam stays supported for a single raw exchange (e.g.
`packages/app-uv-fixture/app_main.py`):

```python
with zetify_app.connect() as conn:
    response = conn.request({"kind": "action", "verb": "query", "action": "some.verb"})
```

## No fallback socket path

`connect()` — and therefore `open_app()` — raises
`zetify_app.TransportUnavailableError` immediately if `TILION_AGENTD_UDS` is
unset. A process not spawned inside a daemon-hosted sandbox has nothing to
connect to, and this package never falls back to a default socket path: it
fails loudly, before any app logic runs, rather than silently talking to the
wrong peer.

The socket path and the per-invocation peer-auth token
(`TILION_AGENTD_UDS_TOKEN`, attached to every request frame by construction)
are read from the child env by the SDK. An app never handles either.

## Wire format

A 4-byte big-endian `uint32` length prefix followed by that many bytes of JSON
body, capped at `MAX_FRAME_BYTES` (1 MiB) in both directions — must match
`internal/brokeruds/proto.go` exactly.

**Dependencies**: Python 3 standard library only — `dependencies = []`.

## Naming

The PyPI distribution is `zetify-app`; the importable module is `zetify_app`;
the base of the exception tree is `ZetifyAppError`. There is no alias, shim, or
transition window for the pre-rename spellings — every app in `apps/` and
`packages/` was migrated in the same change (ENG-3966), and nothing outside
this monorepo has ever been able to install this package. Zetify is the only
name this SDK has ever presented on a public index.

## Tests

```
uv run --package zetify-app pytest packages/app-sdk/tests
```

Those run against a fake broker and are the fast guard. The **evidence** that
the governed verbs work is Go-side, because both halves of the contract this
SDK has to satisfy live in Go: `internal/apprund/appsdk_governed_verbs_test.go`
drives the real `udsproxy` with a real spawned `python3` running this package,
and asserts the frames it emits survive the real Go decoders. A fake broker
that is more permissive than the wire it stands in for pins whatever the client
happens to do — which is how ENG-3649 and ENG-3650 shipped green.

```
go test ./internal/apprund/ -run TestAppSDK_GovernedVerbs
```
