needlepath 0.2.2__tar.gz → 0.2.4__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.
- needlepath-0.2.4/CHANGELOG.md +139 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/PKG-INFO +70 -20
- {needlepath-0.2.2 → needlepath-0.2.4}/README.md +69 -19
- {needlepath-0.2.2 → needlepath-0.2.4}/pyproject.toml +1 -1
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/__init__.py +6 -4
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/client.py +209 -29
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/errors.py +13 -4
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/features.py +5 -3
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/integrations/system_one.py +13 -1
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/result.py +151 -11
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/version.py +1 -1
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_fail_open.py +151 -0
- needlepath-0.2.4/tests/test_guardrails.py +436 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_system_one_adapter.py +31 -6
- needlepath-0.2.2/CHANGELOG.md +0 -78
- {needlepath-0.2.2 → needlepath-0.2.4}/.gitignore +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/LICENSE +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/integrations/__init__.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/models.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/py.typed +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/testing.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/src/needlepath/transport.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_async_parity.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_config_and_guards.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_live_endpoint.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_no_secret_leak.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_r4_contract.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_retries.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_shadow_mode.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_simple_shape.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_urllib_transport.py +0 -0
- {needlepath-0.2.2 → needlepath-0.2.4}/tests/test_wire_compatibility.py +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `needlepath` Python package are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.4] - 2026-11-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`SelectionResult.protected` and `SelectionResult.applying()`.** The two
|
|
13
|
+
rules an adapter must follow once workspace protection is in force, decided
|
|
14
|
+
in the core so no adapter re-derives them. `protected` is `True` when the
|
|
15
|
+
response's `governance` forbids raw-context fallback (a blocked verdict
|
|
16
|
+
included): an adapter's own shadow flag must exempt exactly these results
|
|
17
|
+
(`shadow and not protected`), mirroring `_finish`, which keeps a protected
|
|
18
|
+
delivery applied in a shadow run. `applying()` is a context for the install
|
|
19
|
+
step: an unexpected failure inside it is re-raised as a sanitized
|
|
20
|
+
`NeedlepathGuardrailError` when the result is protected and passes through
|
|
21
|
+
to the adapter's ordinary fail-open handler otherwise.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A blocked result is canonicalized to an empty delivery.** `_finish` kept
|
|
26
|
+
the wire's `rendered_context` and `selected` on a `Reason.BLOCKED` result,
|
|
27
|
+
so a type-valid body that contradicted its own verdict (a render, retained
|
|
28
|
+
records with excerpts) could be forwarded by a caller reading those fields
|
|
29
|
+
or by the System One adapter. A blocked result now always exposes an empty
|
|
30
|
+
render and no selected records; the body is not rejected (the verdict is
|
|
31
|
+
the engine's word, the render is simply not delivered), and the receipt
|
|
32
|
+
keeps what the wire reported (`outcome`, `governance`, `usage`,
|
|
33
|
+
`security_action`, request id, token and record counts).
|
|
34
|
+
`to_system_one_state` mirrors the rule at the point of use: a blocked
|
|
35
|
+
result or response, however assembled, contributes no state.
|
|
36
|
+
|
|
37
|
+
## [0.2.3] - 2026-11-04
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`outcome: blocked` is applied, never passed through.** A governed
|
|
42
|
+
workspace answers a record its policy withholds with top-level
|
|
43
|
+
`outcome: "blocked"`, zero records, an empty render and a `governance`
|
|
44
|
+
block. By shape that is an empty selection, and `classify` returned
|
|
45
|
+
`EMPTY_SELECTION`, a passthrough, so every adapter delivered the withheld
|
|
46
|
+
record. `classify` now reads `blocked` first and returns `Reason.BLOCKED`,
|
|
47
|
+
an applied result with nothing to deliver: adapters install the verdict (a
|
|
48
|
+
notice, a placeholder, or no context) and never the original. Benign, not a
|
|
49
|
+
failure. A blocked verdict is enforced in shadow mode too: shadow never
|
|
50
|
+
restores a withheld record, so the result stays `reason: blocked`,
|
|
51
|
+
`applied: true` with `shadow: true`, `outcome` and `governance` on the
|
|
52
|
+
receipt.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **Workspace guardrails.** `NeedlepathGuardrailError`, raised when the
|
|
57
|
+
service answers `guardrails_unavailable`, when a `governance` block is
|
|
58
|
+
malformed, or when a protected delivery (workspace policies applied,
|
|
59
|
+
raw-context fallback forbidden) cannot be applied. `guardrails_required=True`
|
|
60
|
+
on either client additionally stops on transport errors, timeouts and
|
|
61
|
+
missing enforcement evidence; it is never sent on the wire. A protected
|
|
62
|
+
delivery is applied even in shadow mode and cannot be `declined()` back to
|
|
63
|
+
the original. A blocked verdict is the exception on purpose: it is applied
|
|
64
|
+
as an empty delivery and never raises.
|
|
65
|
+
- **`SelectionResult.metadata()` carries the whole receipt.** `outcome`,
|
|
66
|
+
`security_action`, `usage` and `governance` are now part of the
|
|
67
|
+
metadata-only receipt, verbatim and only when the response has them, so an
|
|
68
|
+
adapter that writes nothing but `metadata()` still shows what the engine
|
|
69
|
+
decided, what it billed and what its policies withheld.
|
|
70
|
+
|
|
71
|
+
## [0.2.2] - 2026-11-03
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- First published build carrying the `sanitized_fallback` and `blocked`
|
|
76
|
+
security-screening outcomes, `security_mode` on requests and the
|
|
77
|
+
`security_action` receipt field (see the API release notes). 0.2.1 predates
|
|
78
|
+
them and classifies a screened fallback as `selection_unsafe`.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- **System One state adapter**: `needlepath.integrations.system_one` reshapes a
|
|
83
|
+
selection into the `{state, questions}` request body that small classifier
|
|
84
|
+
models (TypeSafe's Jev, Laya) accept. `to_system_one_state()` carries every
|
|
85
|
+
record in `response.selected`, in the order returned, with the text
|
|
86
|
+
unmodified; `mode="rendered"` returns `state` as one string instead of an
|
|
87
|
+
array. `questions` travels in the shape it was passed, mapping or sequence,
|
|
88
|
+
so a `{id: question}` mapping never arrives as a list of its keys.
|
|
89
|
+
A `SelectionResult` is accepted only when it `applied`; a passthrough raises
|
|
90
|
+
`NeedlepathConfigError` naming the reason, since its response can carry a
|
|
91
|
+
selection the client declined to use. `selection_receipt()` returns
|
|
92
|
+
`request_id`, `selection_id`, `policy_version`, `tokens_saved` and `outcome`
|
|
93
|
+
as a sidecar dict, prefixed the way `SelectionResult.metadata()` prefixes its
|
|
94
|
+
keys.
|
|
95
|
+
|
|
96
|
+
## [0.2.1] - 2026-09-03
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- Docs, docstrings and examples pin np-2026-08-r4. No behaviour change.
|
|
101
|
+
|
|
102
|
+
## [0.2.0] - 2026-08-07
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **Simple text shape** (NEXPE-461): `select(text=...)` accepts a raw string as
|
|
107
|
+
the alternative to `records=`. Exactly one of the two may be passed; passing
|
|
108
|
+
both fails open client-side with a `NeedlepathConfigError` whose message
|
|
109
|
+
mirrors the server's `400 text_and_records_both_set`. The text goes on the
|
|
110
|
+
wire verbatim; the server chunks it, and each chunk comes back in
|
|
111
|
+
`selected[].record_id` as a content-hash id of the form `sha256:<16 hex>`.
|
|
112
|
+
`ContextRequest` gains an optional `text` field, omitted from the wire when
|
|
113
|
+
unset.
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
|
|
117
|
+
- **`request_id` is now optional** (NEXPE-461): it is a sent-side correlation
|
|
118
|
+
id. `ContextRequest.request_id` may be `None`, in which case the key is
|
|
119
|
+
omitted from the payload and the server assigns its own id (reported on
|
|
120
|
+
`SelectionResult.request_id`). In `select()` / `build_request()`, leaving the
|
|
121
|
+
argument off still auto-generates an id as before; pass `request_id=None`
|
|
122
|
+
explicitly to omit it. The response echo check is skipped when no id was
|
|
123
|
+
sent.
|
|
124
|
+
|
|
125
|
+
### Note
|
|
126
|
+
|
|
127
|
+
- No breaking changes for existing callers: records requests serialize
|
|
128
|
+
byte-identically to 0.1.x, omitting `request_id` still auto-generates one,
|
|
129
|
+
and `operating_point` remains the required, immutable-label constructor
|
|
130
|
+
argument.
|
|
131
|
+
|
|
132
|
+
## [0.1.1] - 2026-08-06
|
|
133
|
+
|
|
134
|
+
- Default base URL moved to `https://api.nextmoca.com`; no wire-shape changes.
|
|
135
|
+
|
|
136
|
+
## [0.1.0] - 2026-08-01
|
|
137
|
+
|
|
138
|
+
- Initial release: fail-open `select()`, pinned operating point, client-side
|
|
139
|
+
6 MB payload guard, shadow mode, sync and async clients.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: needlepath
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Framework-free client for the Needlepath hosted context-selection service.
|
|
5
5
|
Project-URL: Homepage, https://nextmoca.com
|
|
6
6
|
Author: Next Moca Global, Inc.
|
|
@@ -72,8 +72,51 @@ result = client.select(
|
|
|
72
72
|
context = result.rendered_context if result.applied else my_original_context
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
original context.
|
|
75
|
+
Ordinary unprotected failures return `result.applied=False`, allowing your
|
|
76
|
+
original context. A record your workspace policy withholds comes back as
|
|
77
|
+
`result.reason == "blocked"` with `result.applied=True` and an empty
|
|
78
|
+
`rendered_context`: the verdict is "deliver nothing from these records", and
|
|
79
|
+
that is what you install. Workspace guardrail failures (the service could not
|
|
80
|
+
enforce, or returned a protected response that cannot be applied) raise
|
|
81
|
+
`NeedlepathGuardrailError`; stop the model request instead of substituting the
|
|
82
|
+
original context.
|
|
83
|
+
|
|
84
|
+
### Workspace guardrails
|
|
85
|
+
|
|
86
|
+
The framework integrations (LangChain/LangGraph, Strands, LiteLLM and
|
|
87
|
+
LlamaIndex) install a blocked verdict as a withheld notice in place of the
|
|
88
|
+
record, or as no context, and let the agent continue; they propagate
|
|
89
|
+
`NeedlepathGuardrailError` rather than falling back to the original.
|
|
90
|
+
`guardrails_required` is a core-client option: pass a client you constructed
|
|
91
|
+
with it to the integration.
|
|
92
|
+
|
|
93
|
+
For a workspace with guardrails, update the SDK and construct either the sync
|
|
94
|
+
or async client with `guardrails_required=True`:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
client = NeedlepathClient(
|
|
98
|
+
operating_point="np-2026-08-r4",
|
|
99
|
+
guardrails_required=True,
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This local fallback setting is never sent to the server and cannot disable
|
|
104
|
+
workspace policies. It stops on transport errors, timeouts, invalid requests,
|
|
105
|
+
and missing or malformed enforcement evidence. An authoritative admin pause
|
|
106
|
+
(`enabled=False`) or active configuration with zero policies is honored.
|
|
107
|
+
|
|
108
|
+
Regardless of this setting, invalid governance metadata or
|
|
109
|
+
`guardrails_unavailable` raises a sanitized `NeedlepathGuardrailError`. A
|
|
110
|
+
blocked response never raises: it is applied as an empty delivery
|
|
111
|
+
(`reason: "blocked"`), in shadow mode too, and the receipt's `governance`
|
|
112
|
+
block says what was withheld and under which policy revision.
|
|
113
|
+
When the server forbids raw-context fallback, only its permitted selected
|
|
114
|
+
context is applied, including in shadow mode and after engine recovery.
|
|
115
|
+
`result.declined(...)` cannot restore the original context for a protected
|
|
116
|
+
response. Do not catch a guardrail exception or a response rewrite error and
|
|
117
|
+
continue with raw records. Existing clients
|
|
118
|
+
must update and enable this setting to stop safely when an outage prevents
|
|
119
|
+
the server from returning any enforcement evidence.
|
|
77
120
|
|
|
78
121
|
### Async
|
|
79
122
|
|
|
@@ -134,6 +177,7 @@ Do not send prompts, context, model output, answers, or free-form notes.
|
|
|
134
177
|
| `max_retries` | — | `2` |
|
|
135
178
|
| `max_request_bytes` | — | `6 * 1024 * 1024` |
|
|
136
179
|
| `shadow` | — | `False` |
|
|
180
|
+
| `guardrails_required` | — | `False` |
|
|
137
181
|
|
|
138
182
|
The base URL is a **documented default, not a hard-coded one**: a durable custom
|
|
139
183
|
domain replaces it, and swapping it is a config change rather than a release.
|
|
@@ -143,12 +187,13 @@ constraint; loopback is exempt so the localhost benchmark adapter still works.
|
|
|
143
187
|
|
|
144
188
|
## The four binding rules, and where they live
|
|
145
189
|
|
|
146
|
-
### 1. Fail open
|
|
190
|
+
### 1. Fail open for ordinary unprotected requests
|
|
147
191
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
192
|
+
With the default configuration and no workspace protection evidence, a timeout,
|
|
193
|
+
5xx, violated contract, empty selection, or stood-down gate returns a
|
|
194
|
+
`SelectionResult` with `applied=False`, the exception on `.error`, and a
|
|
195
|
+
machine-readable `.reason`. The workspace guardrail contract above takes
|
|
196
|
+
precedence over these optimization fallbacks.
|
|
152
197
|
|
|
153
198
|
```python
|
|
154
199
|
if not result.applied:
|
|
@@ -178,18 +223,19 @@ Treat an unrecognized reason as "do not apply". `result.failed` separates the
|
|
|
178
223
|
error paths from the benign declines, which is the distinction that decides
|
|
179
224
|
whether a passthrough deserves a `WARNING` or a `DEBUG`.
|
|
180
225
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
226
|
+
An ordinary empty selection degrades to passthrough. A blocked verdict is
|
|
227
|
+
applied as an empty delivery (`reason: "blocked"`, `applied=True`), never a
|
|
228
|
+
passthrough and never an exception. An empty selection under workspace
|
|
229
|
+
protection that is not a blocked verdict raises and must stop the model request.
|
|
184
230
|
|
|
185
|
-
**`select_or_raise()` exists** for
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
**`select_or_raise()` exists** for diagnostics and benchmarks that must not
|
|
232
|
+
silently substitute a passthrough for a measurement. Framework adapters use
|
|
233
|
+
`select()` with its explicit guardrail exception contract.
|
|
188
234
|
|
|
189
235
|
### 2. Pin the operating point
|
|
190
236
|
|
|
191
|
-
`operating_point` is required at construction
|
|
192
|
-
|
|
237
|
+
`operating_point` is required at construction; an invalid configuration raises.
|
|
238
|
+
Failing open on an ordinary unprotected bad request is degradation a
|
|
193
239
|
customer absorbs; failing open on "no operating point was pinned" accepts
|
|
194
240
|
exactly the behaviour drift the rule exists to prevent. It fails at wire-up,
|
|
195
241
|
never under load.
|
|
@@ -215,10 +261,14 @@ that passes at 5.99 MB can still be rejected upstream. Lower
|
|
|
215
261
|
client = NeedlepathClient(operating_point="np-2026-08-r4", shadow=True)
|
|
216
262
|
```
|
|
217
263
|
|
|
218
|
-
The call is made exactly as a live call would be
|
|
219
|
-
operating point
|
|
220
|
-
|
|
221
|
-
|
|
264
|
+
The call is made exactly as a live call would be, with the same body, billing,
|
|
265
|
+
and operating point. Ordinary unprotected selections are reported without
|
|
266
|
+
being applied (`reason: "shadow"`). A blocked verdict is enforced in shadow
|
|
267
|
+
mode too: shadow never restores a withheld record, so the result stays
|
|
268
|
+
`reason: "blocked"`, `applied=True`, with `shadow: true`, `outcome` and
|
|
269
|
+
`governance` on the receipt. A protected delivery likewise takes precedence:
|
|
270
|
+
only its permitted selected context may be used, or the request stops with
|
|
271
|
+
`NeedlepathGuardrailError`.
|
|
222
272
|
|
|
223
273
|
`budget.mode = "shadow"` is **not on the wire yet**. `BudgetSpec.mode` on the
|
|
224
274
|
published contract accepts `fixed` and `adaptive` only; adding `shadow` is a
|
|
@@ -43,8 +43,51 @@ result = client.select(
|
|
|
43
43
|
context = result.rendered_context if result.applied else my_original_context
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
original context.
|
|
46
|
+
Ordinary unprotected failures return `result.applied=False`, allowing your
|
|
47
|
+
original context. A record your workspace policy withholds comes back as
|
|
48
|
+
`result.reason == "blocked"` with `result.applied=True` and an empty
|
|
49
|
+
`rendered_context`: the verdict is "deliver nothing from these records", and
|
|
50
|
+
that is what you install. Workspace guardrail failures (the service could not
|
|
51
|
+
enforce, or returned a protected response that cannot be applied) raise
|
|
52
|
+
`NeedlepathGuardrailError`; stop the model request instead of substituting the
|
|
53
|
+
original context.
|
|
54
|
+
|
|
55
|
+
### Workspace guardrails
|
|
56
|
+
|
|
57
|
+
The framework integrations (LangChain/LangGraph, Strands, LiteLLM and
|
|
58
|
+
LlamaIndex) install a blocked verdict as a withheld notice in place of the
|
|
59
|
+
record, or as no context, and let the agent continue; they propagate
|
|
60
|
+
`NeedlepathGuardrailError` rather than falling back to the original.
|
|
61
|
+
`guardrails_required` is a core-client option: pass a client you constructed
|
|
62
|
+
with it to the integration.
|
|
63
|
+
|
|
64
|
+
For a workspace with guardrails, update the SDK and construct either the sync
|
|
65
|
+
or async client with `guardrails_required=True`:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
client = NeedlepathClient(
|
|
69
|
+
operating_point="np-2026-08-r4",
|
|
70
|
+
guardrails_required=True,
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This local fallback setting is never sent to the server and cannot disable
|
|
75
|
+
workspace policies. It stops on transport errors, timeouts, invalid requests,
|
|
76
|
+
and missing or malformed enforcement evidence. An authoritative admin pause
|
|
77
|
+
(`enabled=False`) or active configuration with zero policies is honored.
|
|
78
|
+
|
|
79
|
+
Regardless of this setting, invalid governance metadata or
|
|
80
|
+
`guardrails_unavailable` raises a sanitized `NeedlepathGuardrailError`. A
|
|
81
|
+
blocked response never raises: it is applied as an empty delivery
|
|
82
|
+
(`reason: "blocked"`), in shadow mode too, and the receipt's `governance`
|
|
83
|
+
block says what was withheld and under which policy revision.
|
|
84
|
+
When the server forbids raw-context fallback, only its permitted selected
|
|
85
|
+
context is applied, including in shadow mode and after engine recovery.
|
|
86
|
+
`result.declined(...)` cannot restore the original context for a protected
|
|
87
|
+
response. Do not catch a guardrail exception or a response rewrite error and
|
|
88
|
+
continue with raw records. Existing clients
|
|
89
|
+
must update and enable this setting to stop safely when an outage prevents
|
|
90
|
+
the server from returning any enforcement evidence.
|
|
48
91
|
|
|
49
92
|
### Async
|
|
50
93
|
|
|
@@ -105,6 +148,7 @@ Do not send prompts, context, model output, answers, or free-form notes.
|
|
|
105
148
|
| `max_retries` | — | `2` |
|
|
106
149
|
| `max_request_bytes` | — | `6 * 1024 * 1024` |
|
|
107
150
|
| `shadow` | — | `False` |
|
|
151
|
+
| `guardrails_required` | — | `False` |
|
|
108
152
|
|
|
109
153
|
The base URL is a **documented default, not a hard-coded one**: a durable custom
|
|
110
154
|
domain replaces it, and swapping it is a config change rather than a release.
|
|
@@ -114,12 +158,13 @@ constraint; loopback is exempt so the localhost benchmark adapter still works.
|
|
|
114
158
|
|
|
115
159
|
## The four binding rules, and where they live
|
|
116
160
|
|
|
117
|
-
### 1. Fail open
|
|
161
|
+
### 1. Fail open for ordinary unprotected requests
|
|
118
162
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
With the default configuration and no workspace protection evidence, a timeout,
|
|
164
|
+
5xx, violated contract, empty selection, or stood-down gate returns a
|
|
165
|
+
`SelectionResult` with `applied=False`, the exception on `.error`, and a
|
|
166
|
+
machine-readable `.reason`. The workspace guardrail contract above takes
|
|
167
|
+
precedence over these optimization fallbacks.
|
|
123
168
|
|
|
124
169
|
```python
|
|
125
170
|
if not result.applied:
|
|
@@ -149,18 +194,19 @@ Treat an unrecognized reason as "do not apply". `result.failed` separates the
|
|
|
149
194
|
error paths from the benign declines, which is the distinction that decides
|
|
150
195
|
whether a passthrough deserves a `WARNING` or a `DEBUG`.
|
|
151
196
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
197
|
+
An ordinary empty selection degrades to passthrough. A blocked verdict is
|
|
198
|
+
applied as an empty delivery (`reason: "blocked"`, `applied=True`), never a
|
|
199
|
+
passthrough and never an exception. An empty selection under workspace
|
|
200
|
+
protection that is not a blocked verdict raises and must stop the model request.
|
|
155
201
|
|
|
156
|
-
**`select_or_raise()` exists** for
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
**`select_or_raise()` exists** for diagnostics and benchmarks that must not
|
|
203
|
+
silently substitute a passthrough for a measurement. Framework adapters use
|
|
204
|
+
`select()` with its explicit guardrail exception contract.
|
|
159
205
|
|
|
160
206
|
### 2. Pin the operating point
|
|
161
207
|
|
|
162
|
-
`operating_point` is required at construction
|
|
163
|
-
|
|
208
|
+
`operating_point` is required at construction; an invalid configuration raises.
|
|
209
|
+
Failing open on an ordinary unprotected bad request is degradation a
|
|
164
210
|
customer absorbs; failing open on "no operating point was pinned" accepts
|
|
165
211
|
exactly the behaviour drift the rule exists to prevent. It fails at wire-up,
|
|
166
212
|
never under load.
|
|
@@ -186,10 +232,14 @@ that passes at 5.99 MB can still be rejected upstream. Lower
|
|
|
186
232
|
client = NeedlepathClient(operating_point="np-2026-08-r4", shadow=True)
|
|
187
233
|
```
|
|
188
234
|
|
|
189
|
-
The call is made exactly as a live call would be
|
|
190
|
-
operating point
|
|
191
|
-
|
|
192
|
-
|
|
235
|
+
The call is made exactly as a live call would be, with the same body, billing,
|
|
236
|
+
and operating point. Ordinary unprotected selections are reported without
|
|
237
|
+
being applied (`reason: "shadow"`). A blocked verdict is enforced in shadow
|
|
238
|
+
mode too: shadow never restores a withheld record, so the result stays
|
|
239
|
+
`reason: "blocked"`, `applied=True`, with `shadow: true`, `outcome` and
|
|
240
|
+
`governance` on the receipt. A protected delivery likewise takes precedence:
|
|
241
|
+
only its permitted selected context may be used, or the request stops with
|
|
242
|
+
`NeedlepathGuardrailError`.
|
|
193
243
|
|
|
194
244
|
`budget.mode = "shadow"` is **not on the wire yet**. `BudgetSpec.mode` on the
|
|
195
245
|
published contract accepts `fixed` and `adaptive` only; adding `shadow` is a
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
Three properties this package guarantees, because the framework adapters built
|
|
14
14
|
on it are only as safe as it is:
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
1. Ordinary unprotected failures return ``applied=False`` with recorded errors.
|
|
17
|
+
A blocked verdict is applied (``reason: blocked``, nothing delivered) and
|
|
18
|
+
never raises. Workspace protection or ``guardrails_required`` failures
|
|
19
|
+
instead raise ``NeedlepathGuardrailError``; raw-context fallback is forbidden.
|
|
20
20
|
2. **The operating point is pinned in the constructor**, or construction fails.
|
|
21
21
|
3. **The 6 MB payload cap is measured client-side**, before the request leaves.
|
|
22
22
|
|
|
@@ -50,6 +50,7 @@ from .errors import (
|
|
|
50
50
|
NeedlepathConfigError,
|
|
51
51
|
NeedlepathContractError,
|
|
52
52
|
NeedlepathError,
|
|
53
|
+
NeedlepathGuardrailError,
|
|
53
54
|
NeedlepathHTTPError,
|
|
54
55
|
NeedlepathPayloadTooLargeError,
|
|
55
56
|
NeedlepathTimeoutError,
|
|
@@ -87,6 +88,7 @@ from .transport import (
|
|
|
87
88
|
from .version import __version__
|
|
88
89
|
|
|
89
90
|
__all__ = [
|
|
91
|
+
"NeedlepathGuardrailError",
|
|
90
92
|
"__version__",
|
|
91
93
|
# clients
|
|
92
94
|
"NeedlepathClient",
|