apl-sidecar 0.1.0__tar.gz
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.
- apl_sidecar-0.1.0/LICENSE +21 -0
- apl_sidecar-0.1.0/PKG-INFO +271 -0
- apl_sidecar-0.1.0/README.md +255 -0
- apl_sidecar-0.1.0/adapters/__init__.py +0 -0
- apl_sidecar-0.1.0/adapters/base.py +37 -0
- apl_sidecar-0.1.0/adapters/byok/__init__.py +0 -0
- apl_sidecar-0.1.0/adapters/byok/_http.py +66 -0
- apl_sidecar-0.1.0/adapters/byok/anthropic_provider.py +104 -0
- apl_sidecar-0.1.0/adapters/byok/openai_provider.py +116 -0
- apl_sidecar-0.1.0/adapters/local_stub/__init__.py +0 -0
- apl_sidecar-0.1.0/adapters/local_stub/provider.py +16 -0
- apl_sidecar-0.1.0/adapters/mock.py +30 -0
- apl_sidecar-0.1.0/adapters/mock_provider_a/__init__.py +0 -0
- apl_sidecar-0.1.0/adapters/mock_provider_a/provider.py +11 -0
- apl_sidecar-0.1.0/adapters/mock_provider_b/__init__.py +0 -0
- apl_sidecar-0.1.0/adapters/mock_provider_b/provider.py +11 -0
- apl_sidecar-0.1.0/adapters/registry.py +27 -0
- apl_sidecar-0.1.0/apl_sidecar.egg-info/PKG-INFO +271 -0
- apl_sidecar-0.1.0/apl_sidecar.egg-info/SOURCES.txt +100 -0
- apl_sidecar-0.1.0/apl_sidecar.egg-info/dependency_links.txt +1 -0
- apl_sidecar-0.1.0/apl_sidecar.egg-info/entry_points.txt +2 -0
- apl_sidecar-0.1.0/apl_sidecar.egg-info/requires.txt +7 -0
- apl_sidecar-0.1.0/apl_sidecar.egg-info/top_level.txt +7 -0
- apl_sidecar-0.1.0/app/__init__.py +0 -0
- apl_sidecar-0.1.0/app/local_playground/README.md +14 -0
- apl_sidecar-0.1.0/app/local_playground/app.js +352 -0
- apl_sidecar-0.1.0/app/local_playground/index.html +142 -0
- apl_sidecar-0.1.0/app/local_playground/style.css +95 -0
- apl_sidecar-0.1.0/cli/__init__.py +0 -0
- apl_sidecar-0.1.0/cli/apl.py +179 -0
- apl_sidecar-0.1.0/cli/commands/__init__.py +0 -0
- apl_sidecar-0.1.0/cli/commands/_common.py +267 -0
- apl_sidecar-0.1.0/cli/commands/_resources.py +78 -0
- apl_sidecar-0.1.0/cli/commands/_signing.py +52 -0
- apl_sidecar-0.1.0/cli/commands/break_receipt.py +31 -0
- apl_sidecar-0.1.0/cli/commands/demo.py +145 -0
- apl_sidecar-0.1.0/cli/commands/inspect.py +59 -0
- apl_sidecar-0.1.0/cli/commands/mask.py +37 -0
- apl_sidecar-0.1.0/cli/commands/preview.py +35 -0
- apl_sidecar-0.1.0/cli/commands/rehydrate.py +30 -0
- apl_sidecar-0.1.0/cli/commands/run_live.py +413 -0
- apl_sidecar-0.1.0/cli/commands/run_mock.py +95 -0
- apl_sidecar-0.1.0/cli/commands/verify.py +17 -0
- apl_sidecar-0.1.0/examples/00_private_idea/README.md +56 -0
- apl_sidecar-0.1.0/examples/00_private_idea/final_rehydrated_answer.txt +34 -0
- apl_sidecar-0.1.0/examples/00_private_idea/input.original.example.txt +35 -0
- apl_sidecar-0.1.0/examples/00_private_idea/inspect.expected.txt +35 -0
- apl_sidecar-0.1.0/examples/00_private_idea/local_only.json +9 -0
- apl_sidecar-0.1.0/examples/00_private_idea/masking_plan.yaml +24 -0
- apl_sidecar-0.1.0/examples/00_private_idea/mock_answer_a.txt +27 -0
- apl_sidecar-0.1.0/examples/00_private_idea/mock_answer_b.txt +26 -0
- apl_sidecar-0.1.0/examples/00_private_idea/provider_a_payload.txt +10 -0
- apl_sidecar-0.1.0/examples/00_private_idea/provider_b_payload.txt +9 -0
- apl_sidecar-0.1.0/examples/00_private_idea/receipt.json +78 -0
- apl_sidecar-0.1.0/examples/00_private_idea/tampered_receipt.example.json +78 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/README.md +51 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/final_rehydrated_answer.txt +26 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/input.original.example.txt +44 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/inspect.expected.txt +35 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/local_only.json +9 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/masking_plan.yaml +21 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/mock_answer_a.txt +30 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/mock_answer_b.txt +24 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/provider_a_payload.txt +16 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/provider_b_payload.txt +9 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/receipt.json +78 -0
- apl_sidecar-0.1.0/examples/01_private_code_context/tampered_receipt.example.json +78 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/README.md +46 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/final_rehydrated_answer.txt +29 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/input.original.example.txt +30 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/local_only.json +7 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/masking_plan.yaml +31 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/mock_answer_a.txt +20 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/mock_answer_b.txt +17 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/mock_answer_c.txt +20 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/provider_a_payload.txt +8 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/provider_b_payload.txt +8 -0
- apl_sidecar-0.1.0/examples/02_market_entry_three_way/provider_c_payload.txt +8 -0
- apl_sidecar-0.1.0/examples/__init__.py +0 -0
- apl_sidecar-0.1.0/pyproject.toml +34 -0
- apl_sidecar-0.1.0/relay/__init__.py +2 -0
- apl_sidecar-0.1.0/relay/openai_proxy.py +104 -0
- apl_sidecar-0.1.0/relay/relay_server.py +161 -0
- apl_sidecar-0.1.0/setup.cfg +4 -0
- apl_sidecar-0.1.0/spec/__init__.py +0 -0
- apl_sidecar-0.1.0/spec/apl-oss-demo-key.pem +3 -0
- apl_sidecar-0.1.0/spec/demo_policy_manifest.json +9 -0
- apl_sidecar-0.1.0/tests/test_adapters.py +31 -0
- apl_sidecar-0.1.0/tests/test_byok_adapters.py +134 -0
- apl_sidecar-0.1.0/tests/test_demo.py +78 -0
- apl_sidecar-0.1.0/tests/test_host_normalization.py +52 -0
- apl_sidecar-0.1.0/tests/test_no_deprecated_terms.py +95 -0
- apl_sidecar-0.1.0/tests/test_nway.py +311 -0
- apl_sidecar-0.1.0/tests/test_openai_proxy.py +57 -0
- apl_sidecar-0.1.0/tests/test_packaged_resources.py +93 -0
- apl_sidecar-0.1.0/tests/test_policy_schema.py +59 -0
- apl_sidecar-0.1.0/tests/test_public_claims.py +37 -0
- apl_sidecar-0.1.0/tests/test_receipt_verify.py +56 -0
- apl_sidecar-0.1.0/tests/test_relay.py +85 -0
- apl_sidecar-0.1.0/tests/test_run_live.py +294 -0
- apl_sidecar-0.1.0/tests/test_tamper_vectors.py +59 -0
- apl_sidecar-0.1.0/verifier/apl_verify.py +368 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OIA Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apl-sidecar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local exposure control and signed receipts for sensitive AI tasks
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: cryptography
|
|
10
|
+
Requires-Dist: PyYAML
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: jsonschema; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# APL Sidecar
|
|
18
|
+
|
|
19
|
+
> Private Mode for AI
|
|
20
|
+
|
|
21
|
+
  
|
|
22
|
+
|
|
23
|
+
## Your prompt has a blast radius.
|
|
24
|
+
|
|
25
|
+
Every request to an external AI is a disclosure.
|
|
26
|
+
|
|
27
|
+
APL shows what leaves your machine, splits sensitive tasks across providers, stitches the result locally, and gives you a receipt you can verify offline.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/OIA-LAB/apl-sidecar.git
|
|
31
|
+
cd apl-sidecar
|
|
32
|
+
python -m pip install -e .
|
|
33
|
+
apl demo
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**SEE** what each provider receives
|
|
37
|
+
**KEEP** final assembly local
|
|
38
|
+
**VERIFY** the disclosure trail offline
|
|
39
|
+
**BREAK** the receipt and watch verification fail
|
|
40
|
+
|
|
41
|
+
Don't trust the receipt? Good. Break it yourself:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
apl break-receipt apl-out/receipt.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
APL controls and records task exposure. It does not guarantee anonymity, zero trace, or resistance to intent reconstruction.
|
|
48
|
+
|
|
49
|
+
We publish [what we refuse to claim](docs/claims-and-limits.md).
|
|
50
|
+
|
|
51
|
+
## Demo GIF
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
The demo is deterministic, uses bundled fictional data and offline mock providers, and requires no account, API key, or external network call.
|
|
56
|
+
|
|
57
|
+
## 60-second quickstart
|
|
58
|
+
|
|
59
|
+
Requires Python 3.11 or newer.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m pip install -e .
|
|
63
|
+
apl demo
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
APL writes exactly three primary artifacts to `apl-out/`:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
apl-out/
|
|
70
|
+
├── exposure.html
|
|
71
|
+
├── receipt.json
|
|
72
|
+
└── assessment.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then inspect or verify them:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
apl inspect apl-out
|
|
79
|
+
apl verify apl-out/receipt.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`pipx install apl-sidecar` is the intended published-package experience. Until a release is explicitly published, install from this repository as shown above.
|
|
83
|
+
|
|
84
|
+
## What each provider saw
|
|
85
|
+
|
|
86
|
+
Open `apl-out/exposure.html` and switch between:
|
|
87
|
+
|
|
88
|
+
- **Full Local View** — original task, disclosure plan, local-only context, both fragments and outputs, local stitch, and receipt.
|
|
89
|
+
- **Provider A View** — only Provider A input, metadata, and output.
|
|
90
|
+
- **Provider B View** — only Provider B input, metadata, and output.
|
|
91
|
+
|
|
92
|
+
Each view labels information as `ORIGINAL`, `DERIVED`, `REDACTED`, `SENT`, `RECEIVED`, `LOCAL`, `INFERRED`, or `VERIFIED`.
|
|
93
|
+
|
|
94
|
+
## Break the receipt
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
apl break-receipt apl-out/receipt.json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This preserves the original receipt, creates `receipt.tampered.json`, changes one meaningful field, and runs the offline verifier. Output excerpt:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Changed field: provider_events[0].payload_sha256
|
|
104
|
+
Verification failed: receipt was modified or signature is invalid.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The original still succeeds:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
apl verify apl-out/receipt.json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`apl break-receipt` returns `0` when the generated tampered copy is rejected as expected. A non-zero code means the demonstration itself failed—for example, the source could not be read, could not be tampered with, or the tampered receipt unexpectedly verified. Use `apl verify` to validate a normal receipt.
|
|
114
|
+
|
|
115
|
+
## What just happened?
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
PASTE / bundled fictional task
|
|
119
|
+
↓
|
|
120
|
+
SPLIT / curated controlled disclosures
|
|
121
|
+
↓
|
|
122
|
+
SEE / exact Provider A and Provider B views
|
|
123
|
+
↓
|
|
124
|
+
STITCH / deterministic fixture output reassembled locally
|
|
125
|
+
↓
|
|
126
|
+
RECEIPT / signed Ed25519 disclosure record
|
|
127
|
+
↓
|
|
128
|
+
VERIFY / offline hash and signature verification
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The v0.1 split is user-guided and fixture-backed. Automatic semantic decomposition is not claimed.
|
|
132
|
+
|
|
133
|
+
## Three generated artifacts
|
|
134
|
+
|
|
135
|
+
- `exposure.html` is a self-contained visual record with three perspectives, disclosure flow, local stitch, receipt status, and residual-risk summary.
|
|
136
|
+
- `receipt.json` is the signed receipt produced by the APL signing path.
|
|
137
|
+
- `assessment.md` is a deterministic exploratory reconstruction assessment that states recovered signals, missing context, confidence, residual risk, and method.
|
|
138
|
+
|
|
139
|
+
## Exposure metrics
|
|
140
|
+
|
|
141
|
+
The signed receipt uses normalized character-count ratios defined in [the exposure model](docs/exposure_model.md). The viewer also presents transparent `ceil(characters / 4)` token estimates for developer orientation.
|
|
142
|
+
|
|
143
|
+
For the bundled scenario, Provider A and Provider B receive different partial inputs. Disclosure volume is shown separately from reconstruction risk. APL never transforms a disclosure ratio into “percent safer” or a privacy guarantee.
|
|
144
|
+
|
|
145
|
+
## OpenAI-compatible example
|
|
146
|
+
|
|
147
|
+
Route a tested OpenAI-compatible request through the local APL Sidecar after starting the offline proxy:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
apl proxy --port 8793
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from openai import OpenAI
|
|
155
|
+
|
|
156
|
+
client = OpenAI(base_url="http://127.0.0.1:8793/v1", api_key="local")
|
|
157
|
+
response = client.chat.completions.create(
|
|
158
|
+
model="apl-mock-a",
|
|
159
|
+
messages=[{"role": "user", "content": "Analyze this confidential task..."}],
|
|
160
|
+
)
|
|
161
|
+
print(response.choices[0].message.content)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The current integration surface implements `GET /v1/models` and non-streaming `POST /v1/chat/completions` for two offline mock models. It does not claim universal OpenAI-client compatibility.
|
|
165
|
+
|
|
166
|
+
## How it works
|
|
167
|
+
|
|
168
|
+
The bundled scenario supplies an original fictional task, a masking plan, declared local-only fields, two distinct provider payloads, deterministic mock answers, and a local final artifact. `apl demo` routes both payloads through the `ProviderRegistry`, rejects network-enabled adapters, reintroduces local context only on the machine, signs the receipt, verifies it, and renders the three artifacts.
|
|
169
|
+
|
|
170
|
+
## What the receipt proves
|
|
171
|
+
|
|
172
|
+
APL demonstrates:
|
|
173
|
+
|
|
174
|
+
- what payload the runtime prepared for each provider;
|
|
175
|
+
- what payload the configured adapter recorded as sent;
|
|
176
|
+
- which response hashes entered the run;
|
|
177
|
+
- which local-only field fingerprints were recorded;
|
|
178
|
+
- measured character disclosure volume under this run;
|
|
179
|
+
- whether the receipt remains internally consistent and its Ed25519 signature verifies.
|
|
180
|
+
|
|
181
|
+
See [the receipt standard](RECEIPT_STANDARD.md).
|
|
182
|
+
|
|
183
|
+
## What the receipt does not prove
|
|
184
|
+
|
|
185
|
+
APL does not prove:
|
|
186
|
+
|
|
187
|
+
- that a provider deleted data or did not log it elsewhere;
|
|
188
|
+
- that task intent cannot be reconstructed;
|
|
189
|
+
- that providers do not collude;
|
|
190
|
+
- that all semantic information was removed;
|
|
191
|
+
- cryptographic secrecy of task intent;
|
|
192
|
+
- regulatory compliance, legal non-liability, or universal provider compatibility.
|
|
193
|
+
|
|
194
|
+
See [claims and limits](docs/claims-and-limits.md).
|
|
195
|
+
|
|
196
|
+
## Reconstruction assessment
|
|
197
|
+
|
|
198
|
+
`assessment.md` is deliberately exploratory. It reports recovered entities, relationships and objectives; missing context; a qualitative reconstruction confidence; residual risk; and the exact offline method. It never emits a standalone `SAFE`, `PRIVATE`, or `SECURE` verdict.
|
|
199
|
+
|
|
200
|
+
Disclosure volume and reconstruction risk are different measurements.
|
|
201
|
+
|
|
202
|
+
## Architecture
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
existing scenario fixtures
|
|
206
|
+
↓
|
|
207
|
+
ProviderRegistry → offline mock adapters
|
|
208
|
+
↓
|
|
209
|
+
receipt builder → Ed25519 signer → reference verifier
|
|
210
|
+
↓
|
|
211
|
+
local fixture-backed stitch
|
|
212
|
+
↓
|
|
213
|
+
exposure.html + receipt.json + assessment.md
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The local OpenAI-compatible proxy is an integration surface, not the product itself.
|
|
217
|
+
|
|
218
|
+
## Provider adapters
|
|
219
|
+
|
|
220
|
+
The protocol is defined in `adapters/base.py`, registration is explicit in `adapters/registry.py`, and bundled offline adapters live in `adapters/mock.py`. Unknown plugins are never auto-loaded.
|
|
221
|
+
|
|
222
|
+
> Add a provider adapter without changing the APL runtime.
|
|
223
|
+
|
|
224
|
+
Read [the provider adapter guide](docs/provider-adapters.md).
|
|
225
|
+
|
|
226
|
+
## BYOK reference: run-live
|
|
227
|
+
|
|
228
|
+
`apl run-live` is the reference implementation of the real-provider path the
|
|
229
|
+
security model requires: an explicit opt-in command (never a default), a leak
|
|
230
|
+
gate shared with `apl mask`, a pre-flight disclosure preview with typed
|
|
231
|
+
consent, environment-only keys scrubbed from every error path, and a signed
|
|
232
|
+
`live_response` receipt that is verified before the command reports success —
|
|
233
|
+
and written even when a provider call fails. There is no silent fallback from
|
|
234
|
+
mock to live.
|
|
235
|
+
|
|
236
|
+
Read [the BYOK reference guide](docs/byok_reference.md).
|
|
237
|
+
|
|
238
|
+
## Scenario packs
|
|
239
|
+
|
|
240
|
+
The existing `examples/<scenario>/` layout is the scenario-pack boundary. A scenario can be contributed without modifying the core runtime when it supplies the documented original, plan, local-only fields, provider payloads, fixture responses, local result, and receipt fixtures.
|
|
241
|
+
|
|
242
|
+
Read [the scenario-pack guide](docs/scenario-packs.md).
|
|
243
|
+
|
|
244
|
+
## Security model
|
|
245
|
+
|
|
246
|
+
Default demo behavior is network disabled, offline mock providers only, local output only, no telemetry, no account, and no API key. Any future real-provider path must require an explicit network opt-in and a preview warning; there is no silent fallback to live providers.
|
|
247
|
+
|
|
248
|
+
Private Mode is a product mental model, not a zero-trace promise. APL cannot hide account, IP, or network metadata or control provider retention. See [SECURITY.md](SECURITY.md) and [claims and limits](docs/claims-and-limits.md).
|
|
249
|
+
|
|
250
|
+
## N-way fragmentation (2-5 seats)
|
|
251
|
+
|
|
252
|
+
Tasks that separate into independent workstreams can split across up to
|
|
253
|
+
five seats (`examples/02_market_entry_three_way` ships a three-way plan;
|
|
254
|
+
run-live names every seat: `--seat pricing=anthropic --seat channel=openai
|
|
255
|
+
--seat risk=openai`). Splitting lowers the measurable per-seat disclosure
|
|
256
|
+
share, signed into the receipt; it does not by itself guarantee lower
|
|
257
|
+
reconstruction risk, and seats that resolve to the same trust domain have
|
|
258
|
+
their exposure aggregated -- the receipt records that too. Numbers, hard
|
|
259
|
+
limits, and the trust-domain rule: [docs/fragmentation.md](docs/fragmentation.md).
|
|
260
|
+
|
|
261
|
+
## Enterprise Gateway
|
|
262
|
+
|
|
263
|
+
The open-source Sidecar demonstrates the runtime mechanism. Enterprise deployment, organization-wide policy, audit workflows, and provider governance belong to the [APL Enterprise Gateway](docs/enterprise_gateway.md).
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
Start with [CONTRIBUTING.md](CONTRIBUTING.md). Useful contributions include provider adapters, fictional scenario packs, verifier conformance vectors, accessibility improvements, and tests that sharpen product boundaries.
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT — see [LICENSE](LICENSE). All bundled scenario content is fictional and synthetic.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# APL Sidecar
|
|
2
|
+
|
|
3
|
+
> Private Mode for AI
|
|
4
|
+
|
|
5
|
+
  
|
|
6
|
+
|
|
7
|
+
## Your prompt has a blast radius.
|
|
8
|
+
|
|
9
|
+
Every request to an external AI is a disclosure.
|
|
10
|
+
|
|
11
|
+
APL shows what leaves your machine, splits sensitive tasks across providers, stitches the result locally, and gives you a receipt you can verify offline.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/OIA-LAB/apl-sidecar.git
|
|
15
|
+
cd apl-sidecar
|
|
16
|
+
python -m pip install -e .
|
|
17
|
+
apl demo
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**SEE** what each provider receives
|
|
21
|
+
**KEEP** final assembly local
|
|
22
|
+
**VERIFY** the disclosure trail offline
|
|
23
|
+
**BREAK** the receipt and watch verification fail
|
|
24
|
+
|
|
25
|
+
Don't trust the receipt? Good. Break it yourself:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
apl break-receipt apl-out/receipt.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
APL controls and records task exposure. It does not guarantee anonymity, zero trace, or resistance to intent reconstruction.
|
|
32
|
+
|
|
33
|
+
We publish [what we refuse to claim](docs/claims-and-limits.md).
|
|
34
|
+
|
|
35
|
+
## Demo GIF
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
The demo is deterministic, uses bundled fictional data and offline mock providers, and requires no account, API key, or external network call.
|
|
40
|
+
|
|
41
|
+
## 60-second quickstart
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11 or newer.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install -e .
|
|
47
|
+
apl demo
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
APL writes exactly three primary artifacts to `apl-out/`:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
apl-out/
|
|
54
|
+
├── exposure.html
|
|
55
|
+
├── receipt.json
|
|
56
|
+
└── assessment.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then inspect or verify them:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
apl inspect apl-out
|
|
63
|
+
apl verify apl-out/receipt.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`pipx install apl-sidecar` is the intended published-package experience. Until a release is explicitly published, install from this repository as shown above.
|
|
67
|
+
|
|
68
|
+
## What each provider saw
|
|
69
|
+
|
|
70
|
+
Open `apl-out/exposure.html` and switch between:
|
|
71
|
+
|
|
72
|
+
- **Full Local View** — original task, disclosure plan, local-only context, both fragments and outputs, local stitch, and receipt.
|
|
73
|
+
- **Provider A View** — only Provider A input, metadata, and output.
|
|
74
|
+
- **Provider B View** — only Provider B input, metadata, and output.
|
|
75
|
+
|
|
76
|
+
Each view labels information as `ORIGINAL`, `DERIVED`, `REDACTED`, `SENT`, `RECEIVED`, `LOCAL`, `INFERRED`, or `VERIFIED`.
|
|
77
|
+
|
|
78
|
+
## Break the receipt
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
apl break-receipt apl-out/receipt.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This preserves the original receipt, creates `receipt.tampered.json`, changes one meaningful field, and runs the offline verifier. Output excerpt:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Changed field: provider_events[0].payload_sha256
|
|
88
|
+
Verification failed: receipt was modified or signature is invalid.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The original still succeeds:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
apl verify apl-out/receipt.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`apl break-receipt` returns `0` when the generated tampered copy is rejected as expected. A non-zero code means the demonstration itself failed—for example, the source could not be read, could not be tampered with, or the tampered receipt unexpectedly verified. Use `apl verify` to validate a normal receipt.
|
|
98
|
+
|
|
99
|
+
## What just happened?
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
PASTE / bundled fictional task
|
|
103
|
+
↓
|
|
104
|
+
SPLIT / curated controlled disclosures
|
|
105
|
+
↓
|
|
106
|
+
SEE / exact Provider A and Provider B views
|
|
107
|
+
↓
|
|
108
|
+
STITCH / deterministic fixture output reassembled locally
|
|
109
|
+
↓
|
|
110
|
+
RECEIPT / signed Ed25519 disclosure record
|
|
111
|
+
↓
|
|
112
|
+
VERIFY / offline hash and signature verification
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The v0.1 split is user-guided and fixture-backed. Automatic semantic decomposition is not claimed.
|
|
116
|
+
|
|
117
|
+
## Three generated artifacts
|
|
118
|
+
|
|
119
|
+
- `exposure.html` is a self-contained visual record with three perspectives, disclosure flow, local stitch, receipt status, and residual-risk summary.
|
|
120
|
+
- `receipt.json` is the signed receipt produced by the APL signing path.
|
|
121
|
+
- `assessment.md` is a deterministic exploratory reconstruction assessment that states recovered signals, missing context, confidence, residual risk, and method.
|
|
122
|
+
|
|
123
|
+
## Exposure metrics
|
|
124
|
+
|
|
125
|
+
The signed receipt uses normalized character-count ratios defined in [the exposure model](docs/exposure_model.md). The viewer also presents transparent `ceil(characters / 4)` token estimates for developer orientation.
|
|
126
|
+
|
|
127
|
+
For the bundled scenario, Provider A and Provider B receive different partial inputs. Disclosure volume is shown separately from reconstruction risk. APL never transforms a disclosure ratio into “percent safer” or a privacy guarantee.
|
|
128
|
+
|
|
129
|
+
## OpenAI-compatible example
|
|
130
|
+
|
|
131
|
+
Route a tested OpenAI-compatible request through the local APL Sidecar after starting the offline proxy:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
apl proxy --port 8793
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from openai import OpenAI
|
|
139
|
+
|
|
140
|
+
client = OpenAI(base_url="http://127.0.0.1:8793/v1", api_key="local")
|
|
141
|
+
response = client.chat.completions.create(
|
|
142
|
+
model="apl-mock-a",
|
|
143
|
+
messages=[{"role": "user", "content": "Analyze this confidential task..."}],
|
|
144
|
+
)
|
|
145
|
+
print(response.choices[0].message.content)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The current integration surface implements `GET /v1/models` and non-streaming `POST /v1/chat/completions` for two offline mock models. It does not claim universal OpenAI-client compatibility.
|
|
149
|
+
|
|
150
|
+
## How it works
|
|
151
|
+
|
|
152
|
+
The bundled scenario supplies an original fictional task, a masking plan, declared local-only fields, two distinct provider payloads, deterministic mock answers, and a local final artifact. `apl demo` routes both payloads through the `ProviderRegistry`, rejects network-enabled adapters, reintroduces local context only on the machine, signs the receipt, verifies it, and renders the three artifacts.
|
|
153
|
+
|
|
154
|
+
## What the receipt proves
|
|
155
|
+
|
|
156
|
+
APL demonstrates:
|
|
157
|
+
|
|
158
|
+
- what payload the runtime prepared for each provider;
|
|
159
|
+
- what payload the configured adapter recorded as sent;
|
|
160
|
+
- which response hashes entered the run;
|
|
161
|
+
- which local-only field fingerprints were recorded;
|
|
162
|
+
- measured character disclosure volume under this run;
|
|
163
|
+
- whether the receipt remains internally consistent and its Ed25519 signature verifies.
|
|
164
|
+
|
|
165
|
+
See [the receipt standard](RECEIPT_STANDARD.md).
|
|
166
|
+
|
|
167
|
+
## What the receipt does not prove
|
|
168
|
+
|
|
169
|
+
APL does not prove:
|
|
170
|
+
|
|
171
|
+
- that a provider deleted data or did not log it elsewhere;
|
|
172
|
+
- that task intent cannot be reconstructed;
|
|
173
|
+
- that providers do not collude;
|
|
174
|
+
- that all semantic information was removed;
|
|
175
|
+
- cryptographic secrecy of task intent;
|
|
176
|
+
- regulatory compliance, legal non-liability, or universal provider compatibility.
|
|
177
|
+
|
|
178
|
+
See [claims and limits](docs/claims-and-limits.md).
|
|
179
|
+
|
|
180
|
+
## Reconstruction assessment
|
|
181
|
+
|
|
182
|
+
`assessment.md` is deliberately exploratory. It reports recovered entities, relationships and objectives; missing context; a qualitative reconstruction confidence; residual risk; and the exact offline method. It never emits a standalone `SAFE`, `PRIVATE`, or `SECURE` verdict.
|
|
183
|
+
|
|
184
|
+
Disclosure volume and reconstruction risk are different measurements.
|
|
185
|
+
|
|
186
|
+
## Architecture
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
existing scenario fixtures
|
|
190
|
+
↓
|
|
191
|
+
ProviderRegistry → offline mock adapters
|
|
192
|
+
↓
|
|
193
|
+
receipt builder → Ed25519 signer → reference verifier
|
|
194
|
+
↓
|
|
195
|
+
local fixture-backed stitch
|
|
196
|
+
↓
|
|
197
|
+
exposure.html + receipt.json + assessment.md
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The local OpenAI-compatible proxy is an integration surface, not the product itself.
|
|
201
|
+
|
|
202
|
+
## Provider adapters
|
|
203
|
+
|
|
204
|
+
The protocol is defined in `adapters/base.py`, registration is explicit in `adapters/registry.py`, and bundled offline adapters live in `adapters/mock.py`. Unknown plugins are never auto-loaded.
|
|
205
|
+
|
|
206
|
+
> Add a provider adapter without changing the APL runtime.
|
|
207
|
+
|
|
208
|
+
Read [the provider adapter guide](docs/provider-adapters.md).
|
|
209
|
+
|
|
210
|
+
## BYOK reference: run-live
|
|
211
|
+
|
|
212
|
+
`apl run-live` is the reference implementation of the real-provider path the
|
|
213
|
+
security model requires: an explicit opt-in command (never a default), a leak
|
|
214
|
+
gate shared with `apl mask`, a pre-flight disclosure preview with typed
|
|
215
|
+
consent, environment-only keys scrubbed from every error path, and a signed
|
|
216
|
+
`live_response` receipt that is verified before the command reports success —
|
|
217
|
+
and written even when a provider call fails. There is no silent fallback from
|
|
218
|
+
mock to live.
|
|
219
|
+
|
|
220
|
+
Read [the BYOK reference guide](docs/byok_reference.md).
|
|
221
|
+
|
|
222
|
+
## Scenario packs
|
|
223
|
+
|
|
224
|
+
The existing `examples/<scenario>/` layout is the scenario-pack boundary. A scenario can be contributed without modifying the core runtime when it supplies the documented original, plan, local-only fields, provider payloads, fixture responses, local result, and receipt fixtures.
|
|
225
|
+
|
|
226
|
+
Read [the scenario-pack guide](docs/scenario-packs.md).
|
|
227
|
+
|
|
228
|
+
## Security model
|
|
229
|
+
|
|
230
|
+
Default demo behavior is network disabled, offline mock providers only, local output only, no telemetry, no account, and no API key. Any future real-provider path must require an explicit network opt-in and a preview warning; there is no silent fallback to live providers.
|
|
231
|
+
|
|
232
|
+
Private Mode is a product mental model, not a zero-trace promise. APL cannot hide account, IP, or network metadata or control provider retention. See [SECURITY.md](SECURITY.md) and [claims and limits](docs/claims-and-limits.md).
|
|
233
|
+
|
|
234
|
+
## N-way fragmentation (2-5 seats)
|
|
235
|
+
|
|
236
|
+
Tasks that separate into independent workstreams can split across up to
|
|
237
|
+
five seats (`examples/02_market_entry_three_way` ships a three-way plan;
|
|
238
|
+
run-live names every seat: `--seat pricing=anthropic --seat channel=openai
|
|
239
|
+
--seat risk=openai`). Splitting lowers the measurable per-seat disclosure
|
|
240
|
+
share, signed into the receipt; it does not by itself guarantee lower
|
|
241
|
+
reconstruction risk, and seats that resolve to the same trust domain have
|
|
242
|
+
their exposure aggregated -- the receipt records that too. Numbers, hard
|
|
243
|
+
limits, and the trust-domain rule: [docs/fragmentation.md](docs/fragmentation.md).
|
|
244
|
+
|
|
245
|
+
## Enterprise Gateway
|
|
246
|
+
|
|
247
|
+
The open-source Sidecar demonstrates the runtime mechanism. Enterprise deployment, organization-wide policy, audit workflows, and provider governance belong to the [APL Enterprise Gateway](docs/enterprise_gateway.md).
|
|
248
|
+
|
|
249
|
+
## Contributing
|
|
250
|
+
|
|
251
|
+
Start with [CONTRIBUTING.md](CONTRIBUTING.md). Useful contributions include provider adapters, fictional scenario packs, verifier conformance vectors, accessibility improvements, and tests that sharpen product boundaries.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT — see [LICENSE](LICENSE). All bundled scenario content is fictional and synthetic.
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Provider adapter contract shared by offline and future network providers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ProviderCapabilities:
|
|
11
|
+
network: bool
|
|
12
|
+
streaming: bool = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ProviderRequest:
|
|
17
|
+
prompt: str
|
|
18
|
+
model: str
|
|
19
|
+
fixture_dir: Path | None = None
|
|
20
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ProviderResponse:
|
|
25
|
+
text: str
|
|
26
|
+
provider_id: str
|
|
27
|
+
model: str
|
|
28
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class ProviderAdapter(Protocol):
|
|
33
|
+
provider_id: str
|
|
34
|
+
capabilities: ProviderCapabilities
|
|
35
|
+
|
|
36
|
+
def complete(self, request: ProviderRequest) -> ProviderResponse: ...
|
|
37
|
+
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Minimal JSON POST transport for BYOK adapters. Stdlib only, fail-close.
|
|
2
|
+
|
|
3
|
+
Design rules (mirrors the receipt philosophy: evidence over trust):
|
|
4
|
+
- hard timeout and response size cap — a hung or oversized response is a failure;
|
|
5
|
+
- secrets passed by the caller are scrubbed from every raised message, so an
|
|
6
|
+
API key can never leak through a traceback, log line, or receipt;
|
|
7
|
+
- no retries — a live call either happened once or it did not, which keeps
|
|
8
|
+
provider_events honest.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
DEFAULT_TIMEOUT_S = 60.0
|
|
18
|
+
MAX_RESPONSE_BYTES = 2 * 1024 * 1024 # 2 MiB
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ByokConfigError(ValueError):
|
|
22
|
+
"""Adapter is not configured correctly (missing key, model, or URL)."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TransportError(RuntimeError):
|
|
26
|
+
"""Network call failed. Message is always secret-scrubbed."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _scrub(message: str, secrets: tuple[str, ...]) -> str:
|
|
30
|
+
for secret in secrets:
|
|
31
|
+
if secret:
|
|
32
|
+
message = message.replace(secret, "[redacted]")
|
|
33
|
+
return message
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def post_json(url: str, headers: dict[str, str], body: dict[str, Any],
|
|
37
|
+
timeout: float = DEFAULT_TIMEOUT_S,
|
|
38
|
+
max_bytes: int = MAX_RESPONSE_BYTES,
|
|
39
|
+
secrets: tuple[str, ...] = ()) -> dict[str, Any]:
|
|
40
|
+
"""POST a JSON body, return the parsed JSON response. Raises TransportError."""
|
|
41
|
+
data = json.dumps(body).encode("utf-8")
|
|
42
|
+
request = urllib.request.Request(
|
|
43
|
+
url, data=data, method="POST",
|
|
44
|
+
headers={**headers, "Content-Type": "application/json"})
|
|
45
|
+
try:
|
|
46
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
47
|
+
raw = response.read(max_bytes + 1)
|
|
48
|
+
except urllib.error.HTTPError as exc:
|
|
49
|
+
detail = ""
|
|
50
|
+
try:
|
|
51
|
+
detail = exc.read(500).decode("utf-8", errors="replace")
|
|
52
|
+
except Exception: # noqa: BLE001 — never let error reporting raise
|
|
53
|
+
pass
|
|
54
|
+
raise TransportError(_scrub(
|
|
55
|
+
f"provider returned HTTP {exc.code}: {detail}".strip(), secrets)) from None
|
|
56
|
+
except Exception as exc: # URLError, timeout, ConnectionError, ...
|
|
57
|
+
raise TransportError(_scrub(f"request failed: {exc}", secrets)) from None
|
|
58
|
+
if len(raw) > max_bytes:
|
|
59
|
+
raise TransportError(f"response exceeded {max_bytes} bytes")
|
|
60
|
+
try:
|
|
61
|
+
parsed = json.loads(raw.decode("utf-8"))
|
|
62
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
63
|
+
raise TransportError(_scrub(f"response is not valid JSON: {exc}", secrets)) from None
|
|
64
|
+
if not isinstance(parsed, dict):
|
|
65
|
+
raise TransportError("response JSON is not an object")
|
|
66
|
+
return parsed
|