-
Notifications
You must be signed in to change notification settings - Fork 1
docs: make multi-worker consistency semantics executable #280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8f16f5b
test: make multi-worker consistency semantics executable
dgenio 8d97702
docs: publish current deployment consistency model
dgenio 79f3797
style: keep consistency test ruff-clean
dgenio 9fb4757
style: normalize consistency-test imports
dgenio 7966b20
style: sort consistency-test imports
dgenio 2ebfc76
test: prove multi-worker semantics across processes
dgenio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # Deployment consistency model | ||
|
|
||
| Weaver Kernel is an **in-process enforcement runtime**, not a distributed authorization service. Some state is deliberately local to a Kernel/process unless the deployment supplies a shared backend. | ||
|
|
||
| This matters because signed capability tokens are partly stateless while revocation, rate limiting, handles and other runtime state can be local. A multi-worker deployment can therefore have different semantics from a single process even when every worker uses the same signing secret. | ||
|
|
||
| The current behavior is pinned by [`tests/test_multi_worker_consistency.py`](../tests/test_multi_worker_consistency.py). | ||
|
|
||
| ## Current matrix | ||
|
|
||
| | Component | Default state | Two workers sharing the same secret | Consequence | | ||
| | --- | --- | --- | --- | | ||
| | capability-token signature verification | stateless HMAC | token issued by A verifies in B | expected and useful | | ||
| | token revocation | in-memory store unless replaced | revoking in A does not revoke in B | revocation is not globally consistent by default | | ||
| | rate-limit windows | in-memory | each worker has an independent window | an effective deployment-wide limit can scale with worker count | | ||
| | handles / expanded results | in-memory `HandleStore` | handle created in A is unknown in B | requests that move workers cannot expand that handle | | ||
| | in-memory traces | process-local | each worker sees its own trace store | audit history fragments unless a shared/durable store is used | | ||
| | budget/runtime counters | process-local where backed by in-memory state | counters can diverge | deployment-wide budgets require a shared coordination model | | ||
|
|
||
| ## Reproducible evidence | ||
|
|
||
| The test suite demonstrates four load-bearing facts through public/component APIs: | ||
|
|
||
| 1. two `HMACTokenProvider` instances with the same secret accept the same valid signed token; | ||
| 2. revoking that token in worker A does not alter worker B's independent in-memory revocation store; | ||
| 3. two `RateLimiter` instances have independent windows for the same logical principal/capability key; | ||
| 4. two `HandleStore` instances do not share handle payloads. | ||
|
|
||
| Run: | ||
|
|
||
| ```bash | ||
| pytest -q tests/test_multi_worker_consistency.py | ||
| ``` | ||
|
|
||
| These tests are intentionally documentation-as-code: if the implementation changes, the deployment claim must change with it. | ||
|
|
||
| ## Supported deployment guidance today | ||
|
|
||
| ### Single process | ||
|
|
||
| A single process gives the clearest semantics for the default in-memory stores. It is the easiest deployment profile to reason about when evaluating the library. | ||
|
|
||
| ### Multiple workers with only a shared signing secret | ||
|
|
||
| Do **not** interpret a shared `WEAVER_KERNEL_SECRET` as shared authorization state. It lets workers verify the same token signatures; it does not by itself synchronize revocation, limits, handles or traces. | ||
|
|
||
| If a security requirement depends on immediate global revocation, one deployment-wide rate limit, portable handles or one authoritative audit history, the default independent in-memory stores are insufficient. | ||
|
|
||
| ### Shared/durable stores | ||
|
|
||
| Use an available shared/durable backend where one exists and validate its consistency properties for the deployment. A durable backend solves only the state it actually owns; it should not be described as making every Kernel subsystem distributed automatically. | ||
|
|
||
| For example, sharing trace storage does not automatically share rate-limit windows or handles. | ||
|
|
||
| ## Architectural decision before a sidecar | ||
|
|
||
| The existence of process-local state does **not** by itself justify building a remote Kernel service. | ||
|
|
||
| The sequence should be: | ||
|
|
||
| 1. identify which guarantees real adopters need across workers; | ||
| 2. determine whether a small shared-store protocol is sufficient; | ||
| 3. measure the latency/failure/operational cost of shared state; | ||
| 4. use a sidecar/remote Kernel only if it materially simplifies the required consistency or trust boundary. | ||
|
|
||
| This is why the remote-mode proposal (#227) is intentionally lower priority than documenting and validating this consistency model. | ||
|
|
||
| ## Security claim language | ||
|
|
||
| Prefer: | ||
|
|
||
| > “With the default in-memory stores, revocation, rate limits and handles are process-local. Signed tokens can verify across workers that share the signing secret.” | ||
|
|
||
| Avoid: | ||
|
|
||
| > “Workers share Kernel authorization state because they use the same secret.” | ||
|
|
||
| Also avoid describing Kernel as a distributed policy service unless the deployed backends and topology actually establish those semantics. | ||
|
|
||
| ## Follow-up decisions | ||
|
|
||
| The evidence here should inform, rather than pre-decide: | ||
|
|
||
| - whether revocation needs a first-class shared-store recommendation; | ||
| - whether invocation limits need deployment-wide state after #170/PR #259 settles their semantics; | ||
| - whether handles should ever be portable across workers or should remain intentionally sticky/local; | ||
| - whether audit stores need a recommended production backend; | ||
| - whether #227 earns its complexity from actual adopter requirements. | ||
|
|
||
| See the [Security Contract](security-contract.md) and [Roadmap](../ROADMAP.md) for the broader product/security gates. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """Executable documentation for current process-local consistency semantics (#226).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import subprocess | ||
| import sys | ||
| from typing import Any | ||
|
|
||
| import pytest | ||
|
|
||
| from weaver_kernel import HandleStore, HMACTokenProvider, TokenRevoked | ||
| from weaver_kernel.rate_limit import RateLimiter | ||
|
|
||
| _SECRET = "multi-worker-consistency-test-secret" | ||
|
|
||
|
|
||
| def _run_fresh_python(source: str, payload: dict[str, Any]) -> str: | ||
| """Run a probe in a separate Python process and return its stdout.""" | ||
| completed = subprocess.run( | ||
| [sys.executable, "-c", source], | ||
| input=json.dumps(payload), | ||
| text=True, | ||
| capture_output=True, | ||
| check=True, | ||
| ) | ||
| return completed.stdout.strip() | ||
|
|
||
|
|
||
| def test_token_signature_verifies_across_processes_that_share_a_secret() -> None: | ||
| worker_a = HMACTokenProvider(secret=_SECRET) | ||
| token = worker_a.issue("tickets.read", "alice") | ||
|
|
||
| result = _run_fresh_python( | ||
| """ | ||
| import json | ||
| import sys | ||
| from weaver_kernel import CapabilityToken, HMACTokenProvider | ||
|
|
||
| payload = json.load(sys.stdin) | ||
| token = CapabilityToken.from_dict(payload["token"]) | ||
| provider = HMACTokenProvider(secret=payload["secret"]) | ||
| provider.verify( | ||
| token, | ||
| expected_principal_id="alice", | ||
| expected_capability_id="tickets.read", | ||
| ) | ||
| print("verified") | ||
| """, | ||
| {"secret": _SECRET, "token": token.to_dict()}, | ||
| ) | ||
|
|
||
| assert result == "verified" | ||
|
|
||
|
|
||
| def test_in_memory_revocation_does_not_propagate_to_fresh_process() -> None: | ||
| worker_a = HMACTokenProvider(secret=_SECRET) | ||
| token = worker_a.issue("tickets.read", "alice") | ||
| worker_a.revoke(token.token_id) | ||
|
|
||
| with pytest.raises(TokenRevoked): | ||
| worker_a.verify( | ||
| token, | ||
| expected_principal_id="alice", | ||
| expected_capability_id="tickets.read", | ||
| ) | ||
|
|
||
| result = _run_fresh_python( | ||
| """ | ||
| import json | ||
| import sys | ||
| from weaver_kernel import CapabilityToken, HMACTokenProvider | ||
|
|
||
| payload = json.load(sys.stdin) | ||
| token = CapabilityToken.from_dict(payload["token"]) | ||
| provider = HMACTokenProvider(secret=payload["secret"]) | ||
| provider.verify( | ||
| token, | ||
| expected_principal_id="alice", | ||
| expected_capability_id="tickets.read", | ||
| ) | ||
| print("verified") | ||
| """, | ||
| {"secret": _SECRET, "token": token.to_dict()}, | ||
| ) | ||
|
|
||
| assert result == "verified" | ||
|
|
||
|
|
||
| def test_rate_limit_windows_are_process_local() -> None: | ||
|
dgenio marked this conversation as resolved.
|
||
| def fixed_clock() -> float: | ||
| return 100.0 | ||
|
|
||
| worker_a = RateLimiter(clock=fixed_clock) | ||
| key = "alice:tickets.read" | ||
|
|
||
| assert worker_a.check(key, limit=1, window_seconds=60.0) | ||
| worker_a.record(key) | ||
| assert not worker_a.check(key, limit=1, window_seconds=60.0) | ||
|
|
||
| result = _run_fresh_python( | ||
| """ | ||
| import json | ||
| import sys | ||
| from weaver_kernel.rate_limit import RateLimiter | ||
|
|
||
| payload = json.load(sys.stdin) | ||
| limiter = RateLimiter(clock=lambda: 100.0) | ||
| print("allowed" if limiter.check(payload["key"], limit=1, window_seconds=60.0) else "blocked") | ||
| """, | ||
| {"key": key}, | ||
| ) | ||
|
|
||
| assert result == "allowed" | ||
|
|
||
|
|
||
| def test_default_handle_store_is_not_portable_to_fresh_process() -> None: | ||
| worker_a = HandleStore() | ||
| handle = worker_a.store( | ||
| "tickets.read", | ||
| [{"id": 1, "title": "Example"}], | ||
| principal_id="alice", | ||
| ) | ||
| assert worker_a.get(handle.handle_id) == [{"id": 1, "title": "Example"}] | ||
|
|
||
| result = _run_fresh_python( | ||
| """ | ||
| import json | ||
| import sys | ||
| from weaver_kernel import HandleNotFound, HandleStore | ||
|
|
||
| payload = json.load(sys.stdin) | ||
| try: | ||
| HandleStore().get(payload["handle_id"]) | ||
| except HandleNotFound: | ||
| print("missing") | ||
| else: | ||
| print("found") | ||
| """, | ||
| {"handle_id": handle.handle_id}, | ||
| ) | ||
|
|
||
| assert result == "missing" | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.