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.
Files changed (102) hide show
  1. apl_sidecar-0.1.0/LICENSE +21 -0
  2. apl_sidecar-0.1.0/PKG-INFO +271 -0
  3. apl_sidecar-0.1.0/README.md +255 -0
  4. apl_sidecar-0.1.0/adapters/__init__.py +0 -0
  5. apl_sidecar-0.1.0/adapters/base.py +37 -0
  6. apl_sidecar-0.1.0/adapters/byok/__init__.py +0 -0
  7. apl_sidecar-0.1.0/adapters/byok/_http.py +66 -0
  8. apl_sidecar-0.1.0/adapters/byok/anthropic_provider.py +104 -0
  9. apl_sidecar-0.1.0/adapters/byok/openai_provider.py +116 -0
  10. apl_sidecar-0.1.0/adapters/local_stub/__init__.py +0 -0
  11. apl_sidecar-0.1.0/adapters/local_stub/provider.py +16 -0
  12. apl_sidecar-0.1.0/adapters/mock.py +30 -0
  13. apl_sidecar-0.1.0/adapters/mock_provider_a/__init__.py +0 -0
  14. apl_sidecar-0.1.0/adapters/mock_provider_a/provider.py +11 -0
  15. apl_sidecar-0.1.0/adapters/mock_provider_b/__init__.py +0 -0
  16. apl_sidecar-0.1.0/adapters/mock_provider_b/provider.py +11 -0
  17. apl_sidecar-0.1.0/adapters/registry.py +27 -0
  18. apl_sidecar-0.1.0/apl_sidecar.egg-info/PKG-INFO +271 -0
  19. apl_sidecar-0.1.0/apl_sidecar.egg-info/SOURCES.txt +100 -0
  20. apl_sidecar-0.1.0/apl_sidecar.egg-info/dependency_links.txt +1 -0
  21. apl_sidecar-0.1.0/apl_sidecar.egg-info/entry_points.txt +2 -0
  22. apl_sidecar-0.1.0/apl_sidecar.egg-info/requires.txt +7 -0
  23. apl_sidecar-0.1.0/apl_sidecar.egg-info/top_level.txt +7 -0
  24. apl_sidecar-0.1.0/app/__init__.py +0 -0
  25. apl_sidecar-0.1.0/app/local_playground/README.md +14 -0
  26. apl_sidecar-0.1.0/app/local_playground/app.js +352 -0
  27. apl_sidecar-0.1.0/app/local_playground/index.html +142 -0
  28. apl_sidecar-0.1.0/app/local_playground/style.css +95 -0
  29. apl_sidecar-0.1.0/cli/__init__.py +0 -0
  30. apl_sidecar-0.1.0/cli/apl.py +179 -0
  31. apl_sidecar-0.1.0/cli/commands/__init__.py +0 -0
  32. apl_sidecar-0.1.0/cli/commands/_common.py +267 -0
  33. apl_sidecar-0.1.0/cli/commands/_resources.py +78 -0
  34. apl_sidecar-0.1.0/cli/commands/_signing.py +52 -0
  35. apl_sidecar-0.1.0/cli/commands/break_receipt.py +31 -0
  36. apl_sidecar-0.1.0/cli/commands/demo.py +145 -0
  37. apl_sidecar-0.1.0/cli/commands/inspect.py +59 -0
  38. apl_sidecar-0.1.0/cli/commands/mask.py +37 -0
  39. apl_sidecar-0.1.0/cli/commands/preview.py +35 -0
  40. apl_sidecar-0.1.0/cli/commands/rehydrate.py +30 -0
  41. apl_sidecar-0.1.0/cli/commands/run_live.py +413 -0
  42. apl_sidecar-0.1.0/cli/commands/run_mock.py +95 -0
  43. apl_sidecar-0.1.0/cli/commands/verify.py +17 -0
  44. apl_sidecar-0.1.0/examples/00_private_idea/README.md +56 -0
  45. apl_sidecar-0.1.0/examples/00_private_idea/final_rehydrated_answer.txt +34 -0
  46. apl_sidecar-0.1.0/examples/00_private_idea/input.original.example.txt +35 -0
  47. apl_sidecar-0.1.0/examples/00_private_idea/inspect.expected.txt +35 -0
  48. apl_sidecar-0.1.0/examples/00_private_idea/local_only.json +9 -0
  49. apl_sidecar-0.1.0/examples/00_private_idea/masking_plan.yaml +24 -0
  50. apl_sidecar-0.1.0/examples/00_private_idea/mock_answer_a.txt +27 -0
  51. apl_sidecar-0.1.0/examples/00_private_idea/mock_answer_b.txt +26 -0
  52. apl_sidecar-0.1.0/examples/00_private_idea/provider_a_payload.txt +10 -0
  53. apl_sidecar-0.1.0/examples/00_private_idea/provider_b_payload.txt +9 -0
  54. apl_sidecar-0.1.0/examples/00_private_idea/receipt.json +78 -0
  55. apl_sidecar-0.1.0/examples/00_private_idea/tampered_receipt.example.json +78 -0
  56. apl_sidecar-0.1.0/examples/01_private_code_context/README.md +51 -0
  57. apl_sidecar-0.1.0/examples/01_private_code_context/final_rehydrated_answer.txt +26 -0
  58. apl_sidecar-0.1.0/examples/01_private_code_context/input.original.example.txt +44 -0
  59. apl_sidecar-0.1.0/examples/01_private_code_context/inspect.expected.txt +35 -0
  60. apl_sidecar-0.1.0/examples/01_private_code_context/local_only.json +9 -0
  61. apl_sidecar-0.1.0/examples/01_private_code_context/masking_plan.yaml +21 -0
  62. apl_sidecar-0.1.0/examples/01_private_code_context/mock_answer_a.txt +30 -0
  63. apl_sidecar-0.1.0/examples/01_private_code_context/mock_answer_b.txt +24 -0
  64. apl_sidecar-0.1.0/examples/01_private_code_context/provider_a_payload.txt +16 -0
  65. apl_sidecar-0.1.0/examples/01_private_code_context/provider_b_payload.txt +9 -0
  66. apl_sidecar-0.1.0/examples/01_private_code_context/receipt.json +78 -0
  67. apl_sidecar-0.1.0/examples/01_private_code_context/tampered_receipt.example.json +78 -0
  68. apl_sidecar-0.1.0/examples/02_market_entry_three_way/README.md +46 -0
  69. apl_sidecar-0.1.0/examples/02_market_entry_three_way/final_rehydrated_answer.txt +29 -0
  70. apl_sidecar-0.1.0/examples/02_market_entry_three_way/input.original.example.txt +30 -0
  71. apl_sidecar-0.1.0/examples/02_market_entry_three_way/local_only.json +7 -0
  72. apl_sidecar-0.1.0/examples/02_market_entry_three_way/masking_plan.yaml +31 -0
  73. apl_sidecar-0.1.0/examples/02_market_entry_three_way/mock_answer_a.txt +20 -0
  74. apl_sidecar-0.1.0/examples/02_market_entry_three_way/mock_answer_b.txt +17 -0
  75. apl_sidecar-0.1.0/examples/02_market_entry_three_way/mock_answer_c.txt +20 -0
  76. apl_sidecar-0.1.0/examples/02_market_entry_three_way/provider_a_payload.txt +8 -0
  77. apl_sidecar-0.1.0/examples/02_market_entry_three_way/provider_b_payload.txt +8 -0
  78. apl_sidecar-0.1.0/examples/02_market_entry_three_way/provider_c_payload.txt +8 -0
  79. apl_sidecar-0.1.0/examples/__init__.py +0 -0
  80. apl_sidecar-0.1.0/pyproject.toml +34 -0
  81. apl_sidecar-0.1.0/relay/__init__.py +2 -0
  82. apl_sidecar-0.1.0/relay/openai_proxy.py +104 -0
  83. apl_sidecar-0.1.0/relay/relay_server.py +161 -0
  84. apl_sidecar-0.1.0/setup.cfg +4 -0
  85. apl_sidecar-0.1.0/spec/__init__.py +0 -0
  86. apl_sidecar-0.1.0/spec/apl-oss-demo-key.pem +3 -0
  87. apl_sidecar-0.1.0/spec/demo_policy_manifest.json +9 -0
  88. apl_sidecar-0.1.0/tests/test_adapters.py +31 -0
  89. apl_sidecar-0.1.0/tests/test_byok_adapters.py +134 -0
  90. apl_sidecar-0.1.0/tests/test_demo.py +78 -0
  91. apl_sidecar-0.1.0/tests/test_host_normalization.py +52 -0
  92. apl_sidecar-0.1.0/tests/test_no_deprecated_terms.py +95 -0
  93. apl_sidecar-0.1.0/tests/test_nway.py +311 -0
  94. apl_sidecar-0.1.0/tests/test_openai_proxy.py +57 -0
  95. apl_sidecar-0.1.0/tests/test_packaged_resources.py +93 -0
  96. apl_sidecar-0.1.0/tests/test_policy_schema.py +59 -0
  97. apl_sidecar-0.1.0/tests/test_public_claims.py +37 -0
  98. apl_sidecar-0.1.0/tests/test_receipt_verify.py +56 -0
  99. apl_sidecar-0.1.0/tests/test_relay.py +85 -0
  100. apl_sidecar-0.1.0/tests/test_run_live.py +294 -0
  101. apl_sidecar-0.1.0/tests/test_tamper_vectors.py +59 -0
  102. 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
+ ![status: experimental](https://img.shields.io/badge/status-experimental-orange) ![demo: offline](https://img.shields.io/badge/demo-offline-black) ![license: MIT](https://img.shields.io/badge/license-MIT-green)
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
+ ![APL Sidecar: run, inspect, tamper, and verify](assets/apl-demo.gif)
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
+ ![status: experimental](https://img.shields.io/badge/status-experimental-orange) ![demo: offline](https://img.shields.io/badge/demo-offline-black) ![license: MIT](https://img.shields.io/badge/license-MIT-green)
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
+ ![APL Sidecar: run, inspect, tamper, and verify](assets/apl-demo.gif)
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