authdrift-harness 0.1.0__py3-none-any.whl
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.
- authdrift/__init__.py +6 -0
- authdrift/__main__.py +3 -0
- authdrift/cli/__init__.py +0 -0
- authdrift/cli/main.py +32 -0
- authdrift/core/__init__.py +0 -0
- authdrift/core/events.py +12 -0
- authdrift/core/injector.py +36 -0
- authdrift/core/result.py +8 -0
- authdrift/core/runner.py +125 -0
- authdrift/core/scenario.py +23 -0
- authdrift/hooks/__init__.py +0 -0
- authdrift/hooks/lifecycle.py +14 -0
- authdrift_harness-0.1.0.dist-info/METADATA +260 -0
- authdrift_harness-0.1.0.dist-info/RECORD +18 -0
- authdrift_harness-0.1.0.dist-info/WHEEL +5 -0
- authdrift_harness-0.1.0.dist-info/entry_points.txt +2 -0
- authdrift_harness-0.1.0.dist-info/licenses/LICENSE +21 -0
- authdrift_harness-0.1.0.dist-info/top_level.txt +1 -0
authdrift/__init__.py
ADDED
authdrift/__main__.py
ADDED
|
File without changes
|
authdrift/cli/main.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import runpy
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from authdrift import run
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv=None):
|
|
10
|
+
parser = argparse.ArgumentParser(prog="authdrift")
|
|
11
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
12
|
+
command = commands.add_parser("run", help="execute a trusted local scenario file")
|
|
13
|
+
command.add_argument("scenario", type=Path)
|
|
14
|
+
command.add_argument("--repeats", type=int, default=1)
|
|
15
|
+
command.add_argument("--json", type=Path, default=Path("authdrift-results.json"))
|
|
16
|
+
args = parser.parse_args(argv)
|
|
17
|
+
try:
|
|
18
|
+
namespace = runpy.run_path(str(args.scenario.resolve()))
|
|
19
|
+
source = namespace.get("build_scenario", namespace.get("scenario"))
|
|
20
|
+
if source is None:
|
|
21
|
+
raise ValueError("scenario file must export build_scenario() or scenario")
|
|
22
|
+
report = run(source, repeats=args.repeats, json_path=args.json)
|
|
23
|
+
print(f"Evidence: {args.json}")
|
|
24
|
+
return {"CLOSED": 0, "REVOCATION_ESCAPE": 1,
|
|
25
|
+
"BASELINE_FAILURE": 2, "INVALID_EXPERIMENT": 3}[report["result"]]
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
print(f"authdrift: {exc}", file=sys.stderr)
|
|
28
|
+
return 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
raise SystemExit(main())
|
|
File without changes
|
authdrift/core/events.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from time import monotonic_ns
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Events:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.start = monotonic_ns()
|
|
7
|
+
self.items = []
|
|
8
|
+
|
|
9
|
+
def add(self, event, **details):
|
|
10
|
+
self.items.append({"sequence": len(self.items), "event": event,
|
|
11
|
+
"t_ms": (monotonic_ns() - self.start) / 1_000_000,
|
|
12
|
+
**details})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from time import monotonic, sleep
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InvalidExperiment(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def invoke(callback):
|
|
10
|
+
value = callback()
|
|
11
|
+
if inspect.isawaitable(value):
|
|
12
|
+
if inspect.iscoroutine(value):
|
|
13
|
+
value.close()
|
|
14
|
+
raise InvalidExperiment("async callbacks are unsupported; supply a synchronous wrapper")
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def predicate(callback):
|
|
19
|
+
value = invoke(callback)
|
|
20
|
+
if type(value) is not bool:
|
|
21
|
+
raise InvalidExperiment("authoritative predicates must return bool")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def revoke_and_confirm(scenario, events):
|
|
26
|
+
events.add("revocation_requested")
|
|
27
|
+
invoke(scenario.revoke)
|
|
28
|
+
deadline = monotonic() + scenario.confirmation_timeout
|
|
29
|
+
while True:
|
|
30
|
+
confirmed = predicate(scenario.revoked)
|
|
31
|
+
if monotonic() > deadline:
|
|
32
|
+
raise InvalidExperiment("revocation confirmation timed out")
|
|
33
|
+
if confirmed:
|
|
34
|
+
events.add("revocation_confirmed")
|
|
35
|
+
return
|
|
36
|
+
sleep(min(scenario.poll_interval, max(0, deadline - monotonic())))
|
authdrift/core/result.py
ADDED
authdrift/core/runner.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from .events import Events
|
|
6
|
+
from .injector import InvalidExperiment, invoke, predicate, revoke_and_confirm
|
|
7
|
+
from .result import Outcome
|
|
8
|
+
from .scenario import Scenario
|
|
9
|
+
from ..hooks.lifecycle import _hook
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _experiment(source, mode):
|
|
13
|
+
events = Events()
|
|
14
|
+
evidence = {"run_id": str(uuid4()), "experiment": mode, "events": events.items}
|
|
15
|
+
token = None
|
|
16
|
+
reached = False
|
|
17
|
+
injection_error = None
|
|
18
|
+
try:
|
|
19
|
+
scenario = source if isinstance(source, Scenario) else invoke(source)
|
|
20
|
+
if not isinstance(scenario, Scenario):
|
|
21
|
+
raise InvalidExperiment("factory must return Scenario")
|
|
22
|
+
evidence["scenario"] = scenario.name
|
|
23
|
+
if scenario.reset is not None:
|
|
24
|
+
invoke(scenario.reset)
|
|
25
|
+
if isinstance(source, Scenario) and scenario.reset is None:
|
|
26
|
+
raise InvalidExperiment("a Scenario instance requires reset; alternatively pass a fresh factory")
|
|
27
|
+
if predicate(scenario.committed) or predicate(scenario.revoked):
|
|
28
|
+
raise InvalidExperiment("experiment must start with valid authority and an empty sink")
|
|
29
|
+
events.add("experiment_ready")
|
|
30
|
+
|
|
31
|
+
def hook(name):
|
|
32
|
+
nonlocal reached, injection_error
|
|
33
|
+
events.add("checkpoint", name=name)
|
|
34
|
+
if name != scenario.trigger or reached:
|
|
35
|
+
return
|
|
36
|
+
reached = True
|
|
37
|
+
if mode != "mid_flight":
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
if predicate(scenario.committed):
|
|
41
|
+
raise InvalidExperiment("effect already committed before injection")
|
|
42
|
+
revoke_and_confirm(scenario, events)
|
|
43
|
+
# A commit during revocation cannot be ordered after confirmation.
|
|
44
|
+
if predicate(scenario.committed):
|
|
45
|
+
raise InvalidExperiment("effect present at confirmation; commit ordering is ambiguous")
|
|
46
|
+
events.add("sink_absent_after_confirmation")
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
injection_error = exc
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
if mode == "negative":
|
|
52
|
+
revoke_and_confirm(scenario, events)
|
|
53
|
+
if predicate(scenario.committed):
|
|
54
|
+
raise InvalidExperiment("revocation itself changed the sink")
|
|
55
|
+
token = _hook.set(hook)
|
|
56
|
+
invoke(scenario.run)
|
|
57
|
+
events.add("workflow_completed")
|
|
58
|
+
if injection_error is not None:
|
|
59
|
+
raise injection_error
|
|
60
|
+
if mode == "mid_flight" and not reached:
|
|
61
|
+
raise InvalidExperiment("trigger checkpoint not reached")
|
|
62
|
+
if mode != "positive" and not predicate(scenario.revoked):
|
|
63
|
+
raise InvalidExperiment("authority was restored during the experiment")
|
|
64
|
+
committed = predicate(scenario.committed)
|
|
65
|
+
events.add("effect_observed" if committed else "effect_absent")
|
|
66
|
+
evidence["committed"] = committed
|
|
67
|
+
if mode == "positive":
|
|
68
|
+
result = "PASS" if committed else Outcome.INVALID_EXPERIMENT.value
|
|
69
|
+
elif mode == "negative":
|
|
70
|
+
result = Outcome.BASELINE_FAILURE.value if committed else "PASS"
|
|
71
|
+
else:
|
|
72
|
+
result = (Outcome.REVOCATION_ESCAPE if committed else Outcome.CLOSED).value
|
|
73
|
+
evidence["result"] = result
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
evidence["result"] = Outcome.INVALID_EXPERIMENT.value
|
|
76
|
+
evidence["reason"] = f"{type(exc).__name__}: {exc}"
|
|
77
|
+
events.add("experiment_invalid", reason=evidence["reason"])
|
|
78
|
+
finally:
|
|
79
|
+
if token is not None:
|
|
80
|
+
_hook.reset(token)
|
|
81
|
+
return evidence
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run(scenario, *, repeats=1, json_path=None, quiet=False):
|
|
85
|
+
"""Run isolated positive, negative, and mid-flight experiments per repetition.
|
|
86
|
+
|
|
87
|
+
Pass a fresh Scenario factory or a Scenario with a reset callback. Callbacks
|
|
88
|
+
must terminate, and run() must wait for all effects to become observable.
|
|
89
|
+
"""
|
|
90
|
+
if type(repeats) is not int or repeats < 1:
|
|
91
|
+
raise ValueError("repeats must be a positive integer")
|
|
92
|
+
if not isinstance(scenario, Scenario) and not callable(scenario):
|
|
93
|
+
raise TypeError("expected Scenario or factory")
|
|
94
|
+
if _hook.get() is not None:
|
|
95
|
+
raise ValueError("nested experiments are unsupported")
|
|
96
|
+
trials = []
|
|
97
|
+
for index in range(repeats):
|
|
98
|
+
experiments = []
|
|
99
|
+
for mode in ("positive", "negative", "mid_flight"):
|
|
100
|
+
result = _experiment(scenario, mode)
|
|
101
|
+
experiments.append(result)
|
|
102
|
+
if result["result"] != "PASS":
|
|
103
|
+
break
|
|
104
|
+
trials.append({"repetition": index + 1, "result": experiments[-1]["result"],
|
|
105
|
+
"experiments": experiments})
|
|
106
|
+
escapes = sum(t["result"] == Outcome.REVOCATION_ESCAPE for t in trials)
|
|
107
|
+
valid = sum(t["result"] in (Outcome.CLOSED, Outcome.REVOCATION_ESCAPE) for t in trials)
|
|
108
|
+
priority = (Outcome.INVALID_EXPERIMENT, Outcome.BASELINE_FAILURE,
|
|
109
|
+
Outcome.REVOCATION_ESCAPE, Outcome.CLOSED)
|
|
110
|
+
overall = next(o.value for o in priority if any(t["result"] == o for t in trials))
|
|
111
|
+
report = {"schema_version": "1.0", "result": overall, "repeats": repeats,
|
|
112
|
+
"valid_trials": valid, "escapes": escapes,
|
|
113
|
+
"escape_rate": escapes / valid if valid else None, "trials": trials}
|
|
114
|
+
if json_path is not None:
|
|
115
|
+
Path(json_path).write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
116
|
+
if not quiet:
|
|
117
|
+
for trial in trials:
|
|
118
|
+
print(f"Trial {trial['repetition']}: {trial['result']}")
|
|
119
|
+
for experiment in trial["experiments"]:
|
|
120
|
+
print(f" {experiment['experiment']}: {experiment['result']}")
|
|
121
|
+
if "reason" in experiment:
|
|
122
|
+
print(f" {experiment['reason']}")
|
|
123
|
+
rate = f"{report['escape_rate']:.1%}" if valid else "unavailable"
|
|
124
|
+
print(f"Escapes: {escapes}/{valid} valid trials ({rate}); total trials: {repeats}")
|
|
125
|
+
return report
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Scenario:
|
|
8
|
+
name: str
|
|
9
|
+
run: Callable[[], None]
|
|
10
|
+
revoke: Callable[[], None]
|
|
11
|
+
revoked: Callable[[], bool]
|
|
12
|
+
committed: Callable[[], bool]
|
|
13
|
+
trigger: str = "authority_observed"
|
|
14
|
+
reset: Callable[[], None] | None = None
|
|
15
|
+
confirmation_timeout: float = 1.0
|
|
16
|
+
poll_interval: float = 0.001
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
if not self.name or not self.trigger:
|
|
20
|
+
raise ValueError("name and trigger must be nonempty")
|
|
21
|
+
for value in (self.confirmation_timeout, self.poll_interval):
|
|
22
|
+
if not math.isfinite(value) or value <= 0:
|
|
23
|
+
raise ValueError("timeouts and polling intervals must be finite and positive")
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
|
|
3
|
+
_hook = ContextVar("authdrift_checkpoint", default=None)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def checkpoint(name: str):
|
|
7
|
+
"""Pause this synchronous workflow for the active experiment's injection.
|
|
8
|
+
|
|
9
|
+
Outside an experiment this is a no-op. Thread/process propagation is not
|
|
10
|
+
automatic; a workflow must join all consequential work before returning.
|
|
11
|
+
"""
|
|
12
|
+
hook = _hook.get()
|
|
13
|
+
if hook is not None:
|
|
14
|
+
hook(name)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authdrift-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A developer testing harness for confirmed authority changes in agent workflows
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/cheerstopriya/authdrift
|
|
7
|
+
Project-URL: Issues, https://github.com/cheerstopriya/authdrift/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/cheerstopriya/authdrift#readme
|
|
9
|
+
Project-URL: Security, https://github.com/cheerstopriya/authdrift/security/advisories/new
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# AuthDrift
|
|
24
|
+
|
|
25
|
+
**Can your AI agent still act after you revoke its authority?**
|
|
26
|
+
|
|
27
|
+
AuthDrift is a developer testing harness for injecting confirmed authority changes
|
|
28
|
+
into running agent trajectories and observing whether consequential effects still
|
|
29
|
+
commit. It reproduces the gap between an earlier approval and a later action.
|
|
30
|
+
It does not enforce policy or protect a running agent.
|
|
31
|
+
|
|
32
|
+
## A 30-second example
|
|
33
|
+
|
|
34
|
+
An approval is checked, then withdrawn while the workflow is paused. Does the
|
|
35
|
+
refund still happen?
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# Naive: reuse the earlier decision.
|
|
39
|
+
approved = approval.valid()
|
|
40
|
+
authdrift.checkpoint("authority_observed")
|
|
41
|
+
prepare_refund()
|
|
42
|
+
if approved:
|
|
43
|
+
issue_refund()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# Corrected for this synchronous fixture: consult current authority.
|
|
48
|
+
approved = approval.valid()
|
|
49
|
+
authdrift.checkpoint("authority_observed")
|
|
50
|
+
prepare_refund()
|
|
51
|
+
if approval.valid():
|
|
52
|
+
issue_refund()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
These illustrative snippets assume revocation invalidates outstanding effects.
|
|
56
|
+
The runnable fixtures below explicitly define that contract. In concurrent
|
|
57
|
+
systems the final check and mutation must be atomic with respect to revocation;
|
|
58
|
+
a separate recheck alone does not guarantee that.
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Python **3.10+**, **no runtime dependencies**. From a source checkout:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
git clone https://github.com/cheerstopriya/authdrift.git
|
|
66
|
+
cd authdrift
|
|
67
|
+
python -m venv .venv
|
|
68
|
+
# Windows PowerShell: .venv\Scripts\Activate.ps1
|
|
69
|
+
# macOS/Linux: source .venv/bin/activate
|
|
70
|
+
python -m pip install -e .
|
|
71
|
+
authdrift --help
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Version 0.1.0 is prepared for release. PyPI publication is pending;
|
|
75
|
+
use the source installation above until publication is confirmed.
|
|
76
|
+
The distribution name is `authdrift-harness`; the Python import and CLI remain
|
|
77
|
+
`authdrift`. The existing PyPI package named `authdrift` is a different project.
|
|
78
|
+
Source builds require setuptools >= 77, normally supplied by pip's isolated
|
|
79
|
+
build environment. Examples are included in the repository/source distribution.
|
|
80
|
+
The connected editable-install workflow passed on Windows and Ubuntu with Python
|
|
81
|
+
3.10, 3.12, and 3.13; see [GitHub validation](release/validation.md).
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
authdrift run examples/refund/scenario.py --repeats 20 --json vulnerable.json
|
|
87
|
+
authdrift run examples/refund/safe.py --repeats 20 --json safe.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The controlled vulnerable fixture reports `REVOCATION_ESCAPE` (exit **1**);
|
|
91
|
+
the corrected fixture reports `CLOSED` (exit **0**).
|
|
92
|
+
|
|
93
|
+
In AuthDrift's deliberately vulnerable controlled fixtures, all 20/20 runs
|
|
94
|
+
produced `REVOCATION_ESCAPE`; the corresponding corrected fixtures produced
|
|
95
|
+
20/20 `CLOSED`. These counts are fixture runs, not vulnerabilities discovered in
|
|
96
|
+
independent agents. The scenario's declared authority contract gives the security
|
|
97
|
+
classification its meaning; AuthDrift cannot infer that contract.
|
|
98
|
+
|
|
99
|
+
Measured final summary lines for the refund commands:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
vulnerable: Escapes: 20/20 valid trials (100.0%); total trials: 20
|
|
103
|
+
corrected: Escapes: 0/20 valid trials (0.0%); total trials: 20
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Run all three families and the tests:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
python benchmarks/run.py
|
|
110
|
+
python -m unittest discover -s tests -v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See [release validation](release/validation.md) for measured results and
|
|
114
|
+
exact commands. The fixtures use in-memory effect records, not real payments,
|
|
115
|
+
LLM calls, or durable external services.
|
|
116
|
+
|
|
117
|
+
## How AuthDrift works
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
Workflow observes authority
|
|
121
|
+
|
|
|
122
|
+
Checkpoint pauses continuation
|
|
123
|
+
|
|
|
124
|
+
Request authority change -> independently confirm state
|
|
125
|
+
|
|
|
126
|
+
Verify effect is still absent
|
|
127
|
+
|
|
|
128
|
+
Resume the same workflow -> inspect authoritative sink state
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each repetition uses fresh state for positive, pre-revoked negative, and
|
|
132
|
+
mid-flight controls. Failed controls prevent that mid-flight trial from running.
|
|
133
|
+
Confirmation reads the supplied authority predicate, not the revocation callback's
|
|
134
|
+
return value or model narration.
|
|
135
|
+
|
|
136
|
+
## Outcomes
|
|
137
|
+
|
|
138
|
+
**Scenario contract required:** confirmed revocation must invalidate outstanding,
|
|
139
|
+
uncommitted effects. A policy/configuration change alone does not establish this.
|
|
140
|
+
The v0.1 API assumes this contract; it cannot discover or verify it automatically.
|
|
141
|
+
Do not use its security classifications for unspecified upstream semantics.
|
|
142
|
+
|
|
143
|
+
| Outcome | Meaning under the scenario contract | CLI exit |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| `REVOCATION_ESCAPE` | Effect absent after confirmation, present after continuation | 1 |
|
|
146
|
+
| `CLOSED` | Authority remains revoked; no effect observed after completion, scoped to this trial | 0 |
|
|
147
|
+
| `BASELINE_FAILURE` | Pre-revoked control commits; temporal interpretation stops | 2 |
|
|
148
|
+
| `INVALID_EXPERIMENT` | Failed positive control, bad predicates, missing checkpoint, callback error, or ambiguous ordering | 3 |
|
|
149
|
+
|
|
150
|
+
JSON schema 1.0 contains controls, per-experiment IDs, ordered monotonic events,
|
|
151
|
+
reasons, counts, and escape rate over valid mid-flight trials only (null if none).
|
|
152
|
+
Mixed reports prioritize invalid experiments, baseline failures, escapes, then
|
|
153
|
+
closed outcomes. CLI errors also exit 3. JSON does not yet separate observation
|
|
154
|
+
from security interpretation; see [methodology](research/methodology.md).
|
|
155
|
+
|
|
156
|
+
## Included scenarios
|
|
157
|
+
|
|
158
|
+
| Family | Authority | Controlled effect | Implementations |
|
|
159
|
+
| --- | --- | --- | --- |
|
|
160
|
+
| Refund | Approval | Refund record | [vulnerable](examples/refund/scenario.py), [corrected](examples/refund/safe.py) |
|
|
161
|
+
| Delegation | Worker eligibility | Worker result | [vulnerable](examples/delegation/scenario.py), [corrected](examples/delegation/safe.py) |
|
|
162
|
+
| Session | Session authorization | Protected-action record | [vulnerable](examples/session/scenario.py), [corrected](examples/session/safe.py) |
|
|
163
|
+
|
|
164
|
+
Each directory documents its authority contract. Delegation is a synchronous
|
|
165
|
+
worker fixture, not a distributed-agent integration.
|
|
166
|
+
|
|
167
|
+
## Writing your own scenario
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import authdrift
|
|
171
|
+
|
|
172
|
+
def build_scenario():
|
|
173
|
+
# Contract: revoked approval invalidates every outstanding refund.
|
|
174
|
+
state = {"approved": True, "refunds": []}
|
|
175
|
+
|
|
176
|
+
def workflow():
|
|
177
|
+
approved = state["approved"]
|
|
178
|
+
authdrift.checkpoint("authority_observed")
|
|
179
|
+
if approved:
|
|
180
|
+
state["refunds"].append("refund")
|
|
181
|
+
|
|
182
|
+
return authdrift.Scenario(
|
|
183
|
+
name="refund-revocation",
|
|
184
|
+
run=workflow,
|
|
185
|
+
revoke=lambda: state.update(approved=False),
|
|
186
|
+
revoked=lambda: not state["approved"],
|
|
187
|
+
committed=lambda: bool(state["refunds"]),
|
|
188
|
+
trigger="authority_observed",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
authdrift.run(build_scenario)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
CLI files export `build_scenario()` or `scenario`. Prefer a factory creating
|
|
196
|
+
fresh authority and sink state for each experiment. A `Scenario` instance requires
|
|
197
|
+
`reset=` restoring authorization and clearing the sink. Callbacks are synchronous;
|
|
198
|
+
predicates return actual booleans. Optional `confirmation_timeout` and
|
|
199
|
+
`poll_interval` default to 1 second and 0.001 seconds.
|
|
200
|
+
|
|
201
|
+
## Methodology and validity
|
|
202
|
+
|
|
203
|
+
Place the checkpoint after authority observation and before any effect. With
|
|
204
|
+
truthful monotonic sink state, no independent writers, and synchronous continuation,
|
|
205
|
+
absence after confirmation followed by presence establishes causal ordering:
|
|
206
|
+
observation < confirmed revocation < commit. Exact commit time is not measured.
|
|
207
|
+
`effect_observed` is an observation timestamp; **Post-Revocation Commit Delay
|
|
208
|
+
(PRCD)** is not calculated from it.
|
|
209
|
+
|
|
210
|
+
The workflow must join all consequential work before returning. Revocation must
|
|
211
|
+
persist without regrant; effects must not be erased. Predicates independently
|
|
212
|
+
read actual authority and sink state without side effects. Factory isolation is
|
|
213
|
+
a caller obligation, not a sandbox. Read [methodology](research/methodology.md)
|
|
214
|
+
and [threat model](research/threat-model.md) before interpreting results.
|
|
215
|
+
|
|
216
|
+
## What AuthDrift does not do
|
|
217
|
+
|
|
218
|
+
No enforcement, automatic interception, framework adapters, universal compatibility,
|
|
219
|
+
async execution, or production validation. Threads/processes do not automatically
|
|
220
|
+
inherit the checkpoint hook. Confirmation polling has a timeout, but the harness
|
|
221
|
+
cannot safely preempt blocked callbacks. Scenario files execute Python: load only
|
|
222
|
+
trusted code. CLOSED is not proof of general revocation closure.
|
|
223
|
+
|
|
224
|
+
## External validation status
|
|
225
|
+
|
|
226
|
+
| Specimen | Execution status | Security outcome |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| LangGraph airline | `BLOCKED_NOT_EXECUTED` | None |
|
|
229
|
+
| Goose | `BLOCKED_NOT_EXECUTED`; `SEMANTICS_UNSPECIFIED` | None |
|
|
230
|
+
|
|
231
|
+
These are validation-attempt provenance, not successful external validation or
|
|
232
|
+
AuthDrift failures. No LangGraph or Goose vulnerability is claimed. Details and
|
|
233
|
+
the failed Ollama memory preflight: [external validation](research/external-validation.md).
|
|
234
|
+
|
|
235
|
+
## Related research
|
|
236
|
+
|
|
237
|
+
The intended contribution is developer-facing testing, injection, and reproduction
|
|
238
|
+
tooling. AuthDrift does not claim to invent stale authority, TOCTOU, commit-time
|
|
239
|
+
authorization, or revocation closure. See [related work](research/related-work.md).
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Independent specimens are welcome with
|
|
244
|
+
honest execution provenance and explicit authority contracts. For suspected
|
|
245
|
+
harness vulnerabilities, see [SECURITY.md](SECURITY.md).
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
[MIT](LICENSE). Retained upstream specimen material keeps its own license.
|
|
250
|
+
|
|
251
|
+
## Trusted code and report privacy
|
|
252
|
+
|
|
253
|
+
AuthDrift executes user-supplied Python scenarios. Running a scenario is
|
|
254
|
+
equivalent to executing trusted Python code. AuthDrift is not a sandbox.
|
|
255
|
+
Only execute scenarios you trust.
|
|
256
|
+
|
|
257
|
+
Exceptions and error messages may be included in generated result artifacts
|
|
258
|
+
and console output. Users should review and sanitize reports before sharing
|
|
259
|
+
or publishing them. AuthDrift does not automatically redact secrets or personal
|
|
260
|
+
data. Keep raw reports private by default.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
authdrift/__init__.py,sha256=-1hiQXsk0GNTRWTKVlxCy3PMT4tqOMlMVLpqbJQ5iT8,208
|
|
2
|
+
authdrift/__main__.py,sha256=v3re0grUuwzafBOzjQRJBSEvNO6-5A3Q9YULs8vGgF0,56
|
|
3
|
+
authdrift/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
authdrift/cli/main.py,sha256=2_9IRGFqPyQSfaaTa6N_MlluQQrO9-QcLtFNw-xS8dY,1252
|
|
5
|
+
authdrift/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
authdrift/core/events.py,sha256=G9dNZYzS4PC3WvuB2XHsb2cyhulcto2whj9WRvGRh0A,369
|
|
7
|
+
authdrift/core/injector.py,sha256=0nH9hB1xywJNn2KJXx3eMduix5W6-bjXOJVX6_aRd9c,1080
|
|
8
|
+
authdrift/core/result.py,sha256=su5GtX7nv1peAfjj0G_TcUPPx0o1biwytDgFu-7FX_8,212
|
|
9
|
+
authdrift/core/runner.py,sha256=ILCCT63Sa9-TOvhg95nyhM6JnY3aIBlkc0hn_F2mwMA,6000
|
|
10
|
+
authdrift/core/scenario.py,sha256=LqeKqlgYrbMcQT2BE1x7F-qKwzoTlnkkEt00bg7D9Yc,782
|
|
11
|
+
authdrift/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
authdrift/hooks/lifecycle.py,sha256=Lq6qjBAak76HMFPnZ4f_BRpvigetU1lPJXRn2z6RoEI,444
|
|
13
|
+
authdrift_harness-0.1.0.dist-info/licenses/LICENSE,sha256=ulEcXozLIQEujHbn7LsIeIPapYVtuPpFXaACbGdKP3U,1100
|
|
14
|
+
authdrift_harness-0.1.0.dist-info/METADATA,sha256=wCA0aO_t5MGg0blRGrH9-rYPFTwOguewqvOHNqOW0dw,10908
|
|
15
|
+
authdrift_harness-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
authdrift_harness-0.1.0.dist-info/entry_points.txt,sha256=KjBAaKB5kHet1FO5QdHxF7hsqR429C_sr6NRGT4F8mo,54
|
|
17
|
+
authdrift_harness-0.1.0.dist-info/top_level.txt,sha256=UQa1boU2TfoVHpzRj_kwHKsdInJc0WKjie44Jst4CnI,10
|
|
18
|
+
authdrift_harness-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuthDrift contributors
|
|
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 @@
|
|
|
1
|
+
authdrift
|