Metadata-Version: 2.5
Name: sentisec-sdk
Version: 1.0.0
Summary: Sentisec public SDK — cognitive integrity monitoring for autonomous agents (thin client).
Project-URL: Homepage, https://sentisec.ch
Project-URL: Documentation, https://docs.sentisec.ch
Project-URL: Support, https://sentisec.ch
Author-email: Sentisec <hello@sentisec.ch>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agent-monitoring,ai-safety,anthropic,llm,openai,sentisec
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: opentelemetry-proto>=1.25
Requires-Dist: pydantic>=2.9
Requires-Dist: rich>=13.7
Requires-Dist: starlette>=0.37
Requires-Dist: tomli-w>=1.0
Requires-Dist: typer<0.25,>=0.12
Requires-Dist: uvicorn>=0.30
Requires-Dist: watchdog>=4.0
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
Provides-Extra: build
Requires-Dist: shiv>=1.0; extra == 'build'
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: openai
Requires-Dist: openai>=1.54.0; extra == 'openai'
Description-Content-Type: text/markdown

# sentisec-sdk

Public Python SDK for [Sentisec](https://sentisec.ch).

`sentisec-sdk` is a thin client. You install it with `pip`, point it at
your workspace, and wrap your existing OpenAI / Anthropic client. Your
agent's tool calls then flow through Sentisec's hosted control plane,
where they are evaluated and either allowed, rewritten, or blocked
before they reach the world.

## Install

```sh
pip install sentisec-sdk
```

Python 3.11+ is required.

## Quick start

```python
from sentisec_sdk import Monitor

monitor = Monitor()  # picks up ~/.sentisec/credentials.toml after `sentisec login`
client = monitor.wrap_openai(...)
client = monitor.wrap_anthropic(...)
```

The `sentisec` CLI is shipped in the same wheel — see
[The `sentisec` CLI](#the-sentisec-cli) below for the full surface.

```sh
sentisec login
sentisec status
sentisec demo run
sentisec logout
```

`sentisec login` always prints its pairing URL to stderr before waiting.
If no browser opens, open that URL yourself. `sentisec login --print-url-only`
skips browser launch and waits up to 120 seconds on a real local callback
port; successful pairing saves credentials just like ordinary login.
The browser must reach this machine's localhost. To pair from a browser
on another machine, copy a pairing code from the dashboard and run
`sentisec login --code <code>` on the machine you are pairing.

> From `0.1.0a4` onwards `SENTISEC_WORKSPACE_ID` is OPTIONAL — the
> Sentisec proxy resolves the workspace from the API key prefix, so
> a one-env-var setup (`SENTISEC_API_KEY` only) is sufficient for
> the `wrap_anthropic` / `wrap_openai` path.
>
> Corrected 2026-08-18: this section used to warn that `Monitor()` /
> `Session()` "raises `NotImplementedError`" in the alpha. That has not
> been true since `0.1.0a0` — the wrapper path is fully implemented and
> tested. The note is removed rather than softened; it described a build
> state, not the shipped wheel.

## Protecting a Claude Code / Codex subscription (`sentisec edge`)

The same wheel ships the **edge daemon** — the way to put Sentisec in
front of an agent you drive on a Claude or ChatGPT *subscription*, with
no API key and no provider token ever leaving your machine.

```sh
sentisec edge            # guided setup: pair, install, start
sentisec edge status     # what is wired and whether it is healthy
sentisec edge off        # pause: calls pass through unchecked
sentisec edge on         # resume (also restarts the local service)
sentisec edge logs       # the local service log
sentisec edge uninstall  # remove the service, hooks and provider entry
```

Setup is re-runnable and refuses before it writes: it looks for Claude
Code first, then verifies your workspace key with Sentisec, then shows
every file it is about to change and asks once. For a headless install,
pass `--key` (or set `SENTISEC_WORKSPACE_KEY`) with `--yes`:

```sh
sentisec edge setup --yes --key "$SENTISEC_WORKSPACE_KEY"
```

`sentisec edge off` is real: while it is paused the local service answers
every Claude Code hook with an explicit "not checked" allow, relays Codex
turns untouched, and uploads nothing.

Setup starts a small local service (launchd on macOS, a systemd user unit
on Linux) listening on `127.0.0.1:45711`. It forwards Claude Code's
`PreToolUse` hooks to Sentisec's hosted control plane for a verdict and
uploads a token-scrubbed activity trail. When Codex is wired it also
listens on `45712` and relays each turn to `chatgpt.com` — your ChatGPT
credential goes there and nowhere else, and the copy Sentisec analyses is
redacted before it is sent. When Sentisec blocks a call in a Codex turn,
Codex receives the turn with that call replaced by a
`sentisec_tool_call_blocked` tool call and the other calls intact, in the
turn's own form (streamed or not); a block Sentisec cannot rewrite refuses
the turn.

**No detection logic runs on your machine.** The daemon forwards and
enforces; the analysis happens server-side.

### When the local service is stopped or hung

Claude Code's `PreToolUse` command hook asks the local service for a
decision with a 10-second deadline. If the connection is refused, the
service is hung, or its answer is unusable, the hook uses your explicit
posture setting, then the workspace's remembered mode:

| Remembered mode (no explicit override) | Claude Code hook answer |
|---|---|
| enforcing (`enforce`, `scoped_enforce`) | ask for approval |
| monitoring (`shadow`, `observe`) | allow, marked as not checked |
| unknown | ask for approval |

An explicit `open`, `ask` or `closed` posture overrides that mode choice.
A paused edge returns its explicit "not checked" allow. The hook has a
15-second host timeout; if its Python or `sentisec` executable cannot
start, a shell fallback answers `ask`.

Run `sentisec edge doctor` to see the installed hook transport, host
versions and a timed round trip through the hook. `sentisec edge on`
restarts the local service. After upgrading Sentisec, `sentisec edge on`
and `sentisec edge status` replace an existing legacy HTTP `PreToolUse`
hook with the command hook, preserve your other hooks, and print one
upgrade notice to stderr. Repeating either command leaves current hooks
untouched. `sentisec edge setup` also installs the current hook.

### When Sentisec cannot be reached

The daemon has to answer a tool call it is holding, even when Sentisec's
hosted plane is unreachable. What it answers is yours to choose:

| `SENTISEC_EDGE_FAIL_POSTURE` | Claude Code | Codex |
|---|---|---|
| `open` | the call is allowed | the turn is relayed |
| `ask` | Claude Code prompts you | the turn is **held** — Codex has no approval prompt, so it cannot be approved |
| `closed` | the call is denied | the turn is refused (HTTP 403) |
| *(unset — the default)* | monitoring-only workspaces allow; enforcing **and not-yet-known** workspaces ask | monitoring-only workspaces relay; enforcing **and not-yet-known** workspaces hold |

The same value can live in `~/.sentisec/edge.toml` as `fail_posture = "…"`;
the environment variable wins, so you can change posture during an
incident without editing a file the setup wizard also writes.
The command hook reads `SENTISEC_EDGE_FAIL_POSTURE` from Claude Code's
environment; the daemon reads it from its launchd or systemd service
environment. Configure each environment where you want the override to
apply; setting it in a terminal does not change an already-running
Claude Code process or service.

**Which workspace you belong to is remembered.** Setup asks Sentisec
(`/v1/sdk/whoami`), and Claude Code checks report it again. `sentisec status`,
`sentisec doctor` and `sentisec edge status` also refresh the record when
the edge workspace key receives a recognized mode from that live endpoint.
The answer is stored in `~/.sentisec/enforcement-mode.json` — the
mode word, your workspace id, when it was learned and which call taught
it, and nothing else. No key, no token, no request content ever goes in
that file, it is written `0600`, and `sentisec edge uninstall` removes
it. It is what lets a restart, a reboot or a Codex-only install still
know what you asked for. A record written for a different workspace is
ignored rather than inherited.

The diagnostic commands report whether the record was refreshed. An offline,
rejected, unavailable or invalid answer leaves it unchanged and says so;
`--offline` on status/doctor also leaves it unchanged. A local write failure
is reported separately. The CLI refresh takes effect at the daemon's next
start: an already-running daemon keeps its process-local mode until its own
authenticated check reports a mode. SDK login credentials can belong to a
different workspace and never refresh the edge record.

**A fresh boot with no usable mode record prompts on Claude Code when the
gate cannot answer.** Missing, unreadable, invalid or wrong-workspace records
leave the mode unknown, which now resolves to `ask` on both arms: Claude
prompts, while Codex continues to hold. Each unchecked Claude call may need
approval, so unattended work can wait during a partition, even for a workspace
that is actually monitoring-only but whose mode has not been learned. Setup,
`sentisec status` and `sentisec doctor` seed the record when Sentisec can be
reached; the daemon reads that record at its next start. An already-running
daemon learns from its own authenticated gate answers. Known monitoring modes
and explicit posture overrides are unchanged.

**No key, no allow.** A daemon with no workspace key — `~/.sentisec/edge.toml`
deleted, or setup never finished — sends nothing to Sentisec and answers every
tool call by the same rule: an explicit posture, else the remembered mode
(enforcing or unknown asks, monitoring allows); the Codex relay holds an
enforcing or unknown workspace's turn. The reason names the missing key and
`sentisec edge setup`. Deleting the file and restarting the daemon therefore
does not switch checking off. (Earlier versions allowed every call with no key.)

**On Codex, an unknown workspace is treated as an enforcing one.** Codex's
relay has no way to put an approval prompt in front of you mid-turn, so a
turn that could not be checked is answered with a readable refusal —
naming your workspace mode, what failed, and the one setting that
releases it — instead of being passed through as if it had been checked.
If you would rather have the turn than the check, set
`SENTISEC_EDGE_FAIL_POSTURE=open`.

Whatever it decides, it records: every unchecked call leaves a
`event=edge_fail_posture_applied` line in the daemon's log naming the
cause, the posture and the decision, every held Codex turn leaves an
`event=edge_codex_turn_held` line naming the workspace mode, and the
activity trail marks the call as unchecked once connectivity returns.

## The `sentisec` CLI

One command group, one exit-code contract, `--json` on everything that
has something to report.

| Command | What it does |
|---|---|
| `sentisec` | grouped help; names the two ways to get started |
| `sentisec login` / `logout` | pair (or unpair) this machine with a workspace |
| `sentisec status` | what this machine is connected to, and whether it works |
| `sentisec doctor` | fifteen local checks, each with the fix on the line |
| `sentisec demo run` | a scripted scenario against your workspace |
| `sentisec update` / `version` | what is installed, and what is available |
| `sentisec edge …` | cover a Claude Code / Codex subscription (above) |

**Exit codes.** Every command uses the same seven:

| Code | Meaning |
|---|---|
| 0 | success |
| 1 | failure, or a configured thing is broken |
| 2 | usage |
| 3 | not configured / not authorised |
| 4 | network or endpoint |
| 5 | the verdict diverged from what the scenario expected |
| 130 | interrupted |

**Machine-readable output.** `--json` prints exactly one JSON document on
stdout and nothing else; every document carries `schema_version` and
`exit_code`. Available on `status`, `doctor`, `demo run`, `update`,
`version`, `login`, `logout`, `edge status`, `edge setup --yes`,
`edge on`, `edge off` and `edge uninstall`. A workspace key never appears
in any of them — only its last four characters.

**Terminal behaviour.** Colour is off under `NO_COLOR`, `TERM=dumb`,
`--no-color`, `--plain`, and whenever stdout is not a terminal. `--plain`
prints the same lines without panels, spinners or colour, with ASCII
status markers. `-q` prints nothing and lets the exit code carry the
answer.

**Version check.** `status`, `doctor`, `version`, `update` and the bare
help may check for a newer release: one request, a one-second budget,
cached for a day under `~/.sentisec/update-check.json`, silent on
failure, and never before the command's own output. Set
`SENTISEC_NO_UPDATE_CHECK=1` to switch it off. `sentisec update` only
prints the upgrade command for however you installed the CLI; it never
changes the installation itself.

## Documentation

- Product overview: <https://sentisec.ch>
- Developer docs: <https://docs.sentisec.ch>
- Dashboard: <https://app.sentisec.ch>
- Support: <mailto:support@sentisec.ch>

## Building the standalone CLI

For shipping channels that cannot rely on a working `pip` on the user's
box (curl installer, npm postinstall, GitHub Releases tarball), the
`sentisec` console-script entry is bundled as a single
[`shiv`](https://github.com/linkedin/shiv) zipapp:

```sh
# one-time: install the build-only extra
uv pip install --system shiv          # or: pipx install shiv

# build
./scripts/build-cli.sh                # → dist/sentisec (executable)

# (optional) verify the build is reproducible
VERIFY_DETERMINISM=1 ./scripts/build-cli.sh
```

The build script:

- runs `shiv --console-script sentisec --output-file dist/sentisec
  --reproducible --compressed --python "/usr/bin/env python3" .`,
- smoke-tests `./dist/sentisec --version` and asserts the output equals
  `sentisec <version-from-pyproject>`,
- when `VERIFY_DETERMINISM=1`, rebuilds into a tempdir and `diff`s the
  two artifacts byte-for-byte (shiv's `--reproducible` flag pins all
  zip-entry timestamps to a fixed value).

**Platform caveat.** The resulting `dist/sentisec` shebangs to
`/usr/bin/env python3`, but its bundled wheels include the native
`pydantic-core` extension for the **builder's** `(os, arch,
python-minor)` tuple. The artifact therefore runs on hosts matching
that tuple only. The release pipeline (P-INST-02) matrix-builds one
artifact per supported tuple and uploads each as a separate GitHub
Release asset.

## License

Apache License 2.0. See [LICENSE](./LICENSE).
