Skip to content

Commit e5518ef

Browse files
RoyLinRoyLin
authored andcommitted
feat: v0.6.0 — embeddable judge + native in-process SDKs (PyO3 / napi)
Reworked the SDKs to a3s-code's architecture: native in-process bindings, not subprocess wrappers. Rust lib: - src/config.rs: unified ACL config (SdkConfig) — one sentry.acl with rules + llm{}/agent{} backends + deny{} sinks + fail mode → builds the Pipeline + Enforcer - src/sdk.rs: embeddable Sentry — create(acl) + evaluate(event)->Decision + evaluate_and_enforce - rules.rs: LiveRules::from_engine (build L1 from in-memory rules) - example policy/sentry.acl; 43 lib + 12 integration tests green, clippy clean Native SDKs (replace the subprocess wrappers): - sdk/python: PyO3 + maturin (abi3-py39), import a3s_sentry — Sentry.create/evaluate/ evaluate_and_enforce, event builders, Decision/EnforceAction. 8 tests (maturin develop verified) - sdk/typescript: napi-rs, @a3s-lab/sentry — same surface, generated .d.ts. 4 tests (napi build verified) - both judge in-process (no daemon); verified against the embedded engine (SSRF->block/DenyEgress, SDK-authored ACL rule fires at tier=Rules) - publish-python.yml -> maturin wheels; publish-ts.yml -> napi per-platform binaries
1 parent f2ae9db commit e5518ef

42 files changed

Lines changed: 3065 additions & 2036 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: publish-python
22

3-
# Publish the Python SDK to PyPI on a `python-vX.Y.Z` tag (independent of the daemon's `v*` tags).
4-
# Requires a `PYPI_API_TOKEN` repository secret.
3+
# Publish the native PyO3 Python SDK to PyPI on a `python-vX.Y.Z` tag (independent of the daemon's
4+
# `v*` tags). Builds an abi3 wheel per platform with maturin. Requires a `PYPI_API_TOKEN` secret.
55
on:
66
push:
77
tags: ["python-v*"]
@@ -10,24 +10,35 @@ permissions:
1010
contents: read
1111

1212
jobs:
13+
wheels:
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os: [ubuntu-latest, macos-latest, windows-latest]
18+
runs-on: ${{ matrix.os }}
19+
steps:
20+
- uses: actions/checkout@v4
21+
with: { submodules: true }
22+
- uses: PyO3/maturin-action@v1
23+
with:
24+
command: build
25+
args: --release --out dist --manifest-path sdk/python/Cargo.toml
26+
- uses: actions/upload-artifact@v4
27+
with:
28+
name: wheels-${{ matrix.os }}
29+
path: dist
30+
1331
publish:
32+
needs: wheels
1433
runs-on: ubuntu-latest
15-
defaults:
16-
run:
17-
working-directory: sdk/python
1834
steps:
19-
- uses: actions/checkout@v4
20-
- uses: actions/setup-python@v5
35+
- uses: actions/download-artifact@v4
36+
with:
37+
path: dist
38+
merge-multiple: true
39+
- uses: PyO3/maturin-action@v1
2140
with:
22-
python-version: "3.12"
23-
- name: test
24-
run: python -m unittest discover -s tests -t . # integration tests auto-skip (no binary)
25-
- name: build
26-
run: |
27-
pip install --upgrade build twine
28-
python -m build
29-
- name: publish
30-
run: twine upload dist/*
41+
command: upload
42+
args: --skip-existing dist/*
3143
env:
32-
TWINE_USERNAME: __token__
33-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
44+
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/publish-ts.yml

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,60 @@
11
name: publish-ts
22

3-
# Publish the TypeScript SDK (@a3s-lab/sentry) to npm on a `ts-vX.Y.Z` tag (independent of the
4-
# daemon's `v*` tags). Requires an `NPM_TOKEN` repository secret.
3+
# Publish the native napi SDK (@a3s-lab/sentry) to npm on a `ts-vX.Y.Z` tag. Builds the native addon
4+
# per platform, then publishes the main package + per-platform binaries (the napi-rs convention).
5+
# Requires an `NPM_TOKEN` secret. (The per-platform sub-packages live under the @a3s-lab npm scope.)
56
on:
67
push:
78
tags: ["ts-v*"]
89

910
permissions:
1011
contents: read
1112

13+
defaults:
14+
run:
15+
working-directory: sdk/typescript
16+
1217
jobs:
18+
build:
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
settings:
23+
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu }
24+
- { host: macos-latest, target: aarch64-apple-darwin }
25+
- { host: windows-latest, target: x86_64-pc-windows-msvc }
26+
runs-on: ${{ matrix.settings.host }}
27+
steps:
28+
- uses: actions/checkout@v4
29+
with: { submodules: true }
30+
- uses: actions/setup-node@v4
31+
with: { node-version: "20" }
32+
- uses: dtolnay/rust-toolchain@stable
33+
with: { targets: ${{ matrix.settings.target }} }
34+
- run: npm install
35+
- run: npm run build -- --target ${{ matrix.settings.target }}
36+
- run: npm test
37+
- uses: actions/upload-artifact@v4
38+
with:
39+
name: bindings-${{ matrix.settings.target }}
40+
path: sdk/typescript/*.node
41+
1342
publish:
43+
needs: build
1444
runs-on: ubuntu-latest
15-
defaults:
16-
run:
17-
working-directory: sdk/typescript
1845
steps:
1946
- uses: actions/checkout@v4
47+
with: { submodules: true }
2048
- uses: actions/setup-node@v4
2149
with:
2250
node-version: "20"
2351
registry-url: "https://registry.npmjs.org"
24-
- run: npm ci
25-
- name: build + test
26-
run: npm test # tsc + node --test; the integration test auto-skips (no binary)
27-
- name: publish
28-
run: npm publish --access public
52+
- run: npm install
53+
- uses: actions/download-artifact@v4
54+
with:
55+
path: sdk/typescript/artifacts
56+
- run: npx napi artifacts --dir artifacts
57+
- run: npx napi prepublish -t npm
58+
- run: npm publish --access public
2959
env:
3060
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## [0.6.0] — embeddable judge + native in-process SDKs
4+
5+
The SDKs are now **native in-process bindings** (PyO3 / napi-rs), not subprocess wrappers — the Rust
6+
L1/L2/L3 judge embedded in your process, matching [`@a3s-lab/code`](https://github.com/A3S-Lab/Code)'s
7+
architecture. Build the judge from one ACL config and judge events in-process; no daemon, no subprocess.
8+
9+
### Added
10+
- **Embeddable `Sentry`** (`src/sdk.rs`, `src/config.rs`) — `Sentry::create(acl)` builds the pipeline
11+
from one ACL config; `evaluate(event) -> Option<Decision>` and `evaluate_and_enforce` judge in
12+
process. The unified `sentry.acl` carries everything — rules + optional `llm {}` (L2) / `agent {}`
13+
(L3) backends + `deny {}` sinks + fail mode (example: [`policy/sentry.acl`](policy/sentry.acl)).
14+
Plus `LiveRules::from_engine` to build L1 from in-memory rules.
15+
- **Native SDKs** (replacing the subprocess wrappers):
16+
- **Python** ([`sdk/python`](sdk/python), `a3s-sentry`) — PyO3 + maturin, abi3-py39 wheels.
17+
`Sentry.create` / `evaluate` / `evaluate_and_enforce`, event builders, `Decision`/`EnforceAction`.
18+
8 tests.
19+
- **TypeScript** ([`sdk/typescript`](sdk/typescript), `@a3s-lab/sentry`) — napi-rs, generated
20+
`.d.ts`, prebuilt per-platform binaries. Same surface. 4 tests.
21+
- Both judge in-process and are verified against the embedded engine (a cloud-metadata SSRF →
22+
`block`/`DenyEgress`; an SDK-authored ACL rule firing at `tier=Rules`). The PyPI/npm publish
23+
workflows are rebuilt for native artifacts (maturin wheels / napi binaries).
24+
325
## [0.5.2] — Python + TypeScript SDKs; the policy language is ACL
426

527
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-sentry"
3-
version = "0.5.2"
3+
version = "0.6.0"
44
edition = "2021"
55
license = "MIT"
66
description = "Tiered (L1 rules / L2 LLM / L3 agent) runtime security control for AI agents, built on a3s-observer."

README.md

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -143,40 +143,38 @@ in-house ruleset) and keep the escalation machinery.
143143

144144
## SDKs (Python · TypeScript)
145145

146-
Drive sentry from your own code — author ACL policy, run the judge, submit events, stream typed
147-
decisions, read metrics — instead of wiring stdin/stdout by hand. Both are dependency-free and mirror
148-
the same model; each is contract-tested against the real binary (an SSRF event round-trips to a
149-
`block`, and an SDK-authored ACL rule fires through the daemon's own parser).
146+
**Native, in-process** SDKs — the Rust L1/L2/L3 judge embedded via PyO3 (Python) and napi-rs
147+
(TypeScript), the same model as [`@a3s-lab/code`](https://github.com/A3S-Lab/Code). Build the judge
148+
from one ACL config (the daemon's whole config in a single file — rules + L2/L3 backends + sinks) and
149+
evaluate observer events in-process; no daemon, no subprocess. Each is verified by judging real events
150+
through the embedded engine (a cloud-metadata SSRF → `block`/`DenyEgress`; an SDK-authored ACL rule
151+
firing at `tier=Rules`).
150152

151-
- **Python**[`sdk/python`](sdk/python) (`pip install a3s-sentry`), async:
153+
- **Python**[`sdk/python`](sdk/python) (`pip install a3s-sentry`):
152154

153155
```python
154-
from a3s_sentry import Sentry, SentryConfig, Event, Policy, Rule, Verdict, Severity, Action
156+
from a3s_sentry import Sentry, egress, tool_exec
155157

156-
Policy([Rule("no-netcat", "ToolExec", r"(?i)\b(ncat|netcat)\b",
157-
Verdict.BLOCK, Severity.MEDIUM, "netcat", Action.DENY_EXEC)]).write("rules.acl")
158-
159-
async with Sentry(SentryConfig(policy="rules.acl", egress_deny="egress.txt")) as s:
160-
await s.submit(Event.egress(1, "169.254.169.254", 80)) # cloud-metadata SSRF
161-
async for audit in s.decisions():
162-
print(audit.decision.verdict, audit.subject); break
158+
sentry = Sentry.create("sentry.acl") # ACL file path or content
159+
d = sentry.evaluate(egress(1, "169.254.169.254", 80)) # cloud-metadata SSRF
160+
print(d.verdict, d.action.kind, d.action.target) # block DenyEgress 169.254.169.254
161+
d2, enforced = sentry.evaluate_and_enforce(tool_exec(2, ["/usr/bin/ncat", "h", "4444"]))
163162
```
164163

165-
- **TypeScript**[`sdk/typescript`](sdk/typescript) (`@a3s-lab/sentry`), Node 18+:
164+
- **TypeScript**[`sdk/typescript`](sdk/typescript) (`@a3s-lab/sentry`, Node ≥12):
166165

167166
```ts
168-
import { Sentry, Policy, Event } from "@a3s-lab/sentry";
169-
170-
new Policy([{ name: "no-netcat", on: "ToolExec", match: "(?i)\\b(ncat|netcat)\\b",
171-
verdict: "block", severity: "medium", reason: "netcat", action: "deny-exec" }])
172-
.write("rules.acl");
167+
import { Sentry, egress } from "@a3s-lab/sentry";
173168

174-
const s = new Sentry({ policy: "rules.acl", egressDeny: "egress.txt" });
175-
await s.start();
176-
await s.submit(Event.egress(1, "169.254.169.254", 80));
177-
for await (const audit of s.decisions()) { console.log(audit.decision.verdict, audit.subject); break; }
169+
const sentry = Sentry.create("sentry.acl");
170+
const d = sentry.evaluate(egress(1, "169.254.169.254", 80));
171+
if (d?.verdict === "block") console.log(d.reason, d.action); // { kind: "DenyEgress", target: "…" }
178172
```
179173

174+
The `sentry.acl` config — rules, optional `llm {}` (L2) / `agent {}` (L3) backends, and `deny {}`
175+
sinks — is shown in each SDK's README. Event builders (`egress`, `toolExec`, `dns`, `fileAccess`,
176+
`sslContent`, `securityAction`) construct the event JSON `evaluate` takes.
177+
180178
## Speculative parallelism
181179

182180
By default the tiers run serially (L2, then L3 only if L2 escalates). Set `A3S_SENTRY_SPECULATE=high`

policy/sentry.acl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# a3s-sentry — unified ACL config for the embeddable SDK (`Sentry.create("sentry.acl")`).
2+
#
3+
# Where the daemon takes env vars + a rules-only policy, the in-process SDK reads ONE ACL file with
4+
# everything: the fail mode, the L2/L3 backends, the deny-file sinks, and the L1 rules. Every block /
5+
# field is optional except a rule's required fields; the built-in default rules always apply.
6+
7+
fail_closed = false
8+
# speculate = "high" # uncomment to run L2 + L3 in parallel at/above this severity
9+
10+
# L2 — a fast LLM classifier (OpenAI-compatible). Omit for rules-only / rules+L3.
11+
# llm { url = "http://llm:18051/v1", model = "glm", key = "...", timeout_s = 30 }
12+
13+
# L3 — a deep a3s-code agent investigation. Omit if you don't run L3.
14+
# agent { bin = "a3s-code", skills = "./skills", timeout_s = 120 }
15+
16+
# Deny-file sinks the kernel guards read. Omit to judge without enforcing.
17+
deny {
18+
egress = "egress-deny.txt"
19+
exec = "exec-deny.txt"
20+
}
21+
22+
# L1 site rules — evaluated before the built-in defaults; first match wins. Separate inline-object
23+
# fields with commas (or put one per line).
24+
rules = [
25+
{ name = "no-netcat", on = "ToolExec", match = "(?i)\\b(ncat|netcat)\\b",
26+
verdict = "block", severity = "medium", reason = "netcat invocation", action = "deny-exec" },
27+
{ name = "admin-egress", on = "Egress", match = "^10\\.0\\.99\\.",
28+
verdict = "escalate", severity = "medium", reason = "connection into the admin subnet" },
29+
]

sdk/python/.gitignore

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
target/
12
__pycache__/
2-
*.pyc
3-
*.egg-info/
3+
*.so
44
dist/
5-
build/
6-
.pytest_cache/
5+
.venv/

0 commit comments

Comments
 (0)