bounded-agent 0.1.2__tar.gz → 0.1.3__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.
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/PKG-INFO +48 -9
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/README.md +47 -8
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/pyproject.toml +1 -1
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/actions.py +9 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_actions.py +15 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/uv.lock +1 -1
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/.github/workflows/ci.yml +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/.gitignore +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/LICENSE +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/examples/sentinel/contracts.yaml +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/examples/sentinel/sentinel.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/__init__.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/contracts.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/envelope.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/leaf.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/ledgers.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/__init__.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_contracts.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_envelope.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_leaf.py +0 -0
- {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_ledgers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bounded-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Contract-bounded autonomy for LLM agents: five small primitives that make an agent safe to leave alone.
|
|
5
5
|
Project-URL: Repository, https://github.com/scholih/bounded-agent
|
|
6
6
|
Project-URL: Manifesto, https://medium.com/@scholih/the-edge-is-the-harness-not-the-model-a543abade391
|
|
@@ -55,6 +55,16 @@ contracts:
|
|
|
55
55
|
scope: checkout-stack
|
|
56
56
|
tier: act # observe | propose | act
|
|
57
57
|
allows: [restart_worker]
|
|
58
|
+
- name: cache
|
|
59
|
+
status: active
|
|
60
|
+
scope: checkout-stack
|
|
61
|
+
tier: propose # may draft fixes; a human executes them
|
|
62
|
+
allows: [clear_cache]
|
|
63
|
+
- name: payments-db
|
|
64
|
+
status: proposed # not signed off — grants NOTHING until active
|
|
65
|
+
scope: checkout-stack
|
|
66
|
+
tier: act
|
|
67
|
+
allows: [failover_replica]
|
|
58
68
|
```
|
|
59
69
|
|
|
60
70
|
```python
|
|
@@ -63,8 +73,15 @@ from pathlib import Path
|
|
|
63
73
|
|
|
64
74
|
from bounded_agent import ActionSet, ContractBook, Envelope, EvidenceLedger, Gate
|
|
65
75
|
|
|
76
|
+
print("1. the book — who may do what, declared by a human:")
|
|
66
77
|
book = ContractBook.load("contracts.yaml") # raises loudly if missing/malformed
|
|
67
|
-
|
|
78
|
+
for c in book.contracts:
|
|
79
|
+
print(f" {c.name}: status={c.status} scope={c.scope} tier={c.tier} allows={list(c.allows)}")
|
|
80
|
+
|
|
81
|
+
gate = Gate(required_scope="checkout-stack", required_tier="act")
|
|
82
|
+
granted = book.granted(gate)
|
|
83
|
+
print(f"\n2. the gate — scope={gate.required_scope!r} at tier '{gate.required_tier}' grants:")
|
|
84
|
+
print(f" {granted or 'nothing — no active, in-scope contract at this tier'}")
|
|
68
85
|
|
|
69
86
|
actions = ActionSet() # the implemented closed set:
|
|
70
87
|
actions.register("restart_worker", lambda: (True, "worker restarted"))
|
|
@@ -72,20 +89,42 @@ actions.register("restart_worker", lambda: (True, "worker restarted"))
|
|
|
72
89
|
env = Envelope(armed=True, # ships disarmed; arming is explicit
|
|
73
90
|
kill_switch_path=Path("STOP"), # `touch STOP` halts everything
|
|
74
91
|
max_actions_per_run=2,
|
|
75
|
-
notify=
|
|
92
|
+
notify=lambda msg: print(f" [report] {msg}"))
|
|
93
|
+
ks = "PRESENT" if env.kill_switch_path.exists() else "absent"
|
|
94
|
+
print(f"\n3. the envelope — armed={env.armed}, kill-switch {ks}, breaker {env.max_actions_per_run}/run")
|
|
76
95
|
|
|
96
|
+
print("\n4. executing (double-closed: contracted AND registered; every outcome reported):")
|
|
77
97
|
outcome = actions.execute(list(granted.items()), env,
|
|
78
98
|
ledger=EvidenceLedger(Path("ledger")),
|
|
79
99
|
period=dt.date.today())
|
|
80
|
-
|
|
100
|
+
|
|
101
|
+
print(f"\n5. outcome — {outcome.summary()}")
|
|
102
|
+
print(f" ledger: ledger/{dt.date.today().isoformat()}.jsonl (append-only; every run adds lines)")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run it and the output narrates the authorization chain — note the gate silently denying
|
|
106
|
+
`cache` (propose tier) and `payments-db` (not signed off):
|
|
107
|
+
|
|
81
108
|
```
|
|
109
|
+
1. the book — who may do what, declared by a human:
|
|
110
|
+
queue-worker: status=active scope=checkout-stack tier=act allows=['restart_worker']
|
|
111
|
+
cache: status=active scope=checkout-stack tier=propose allows=['clear_cache']
|
|
112
|
+
payments-db: status=proposed scope=checkout-stack tier=act allows=['failover_replica']
|
|
113
|
+
|
|
114
|
+
2. the gate — scope='checkout-stack' at tier 'act' grants:
|
|
115
|
+
{'queue-worker': ('restart_worker',)}
|
|
82
116
|
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
3. the envelope — armed=True, kill-switch absent, breaker 2/run
|
|
118
|
+
|
|
119
|
+
4. executing (double-closed: contracted AND registered; every outcome reported):
|
|
120
|
+
[report] executed-ok: restart_worker on queue-worker (worker restarted)
|
|
121
|
+
|
|
122
|
+
5. outcome — 1 executed | queue-worker:restart_worker=executed-ok
|
|
123
|
+
ledger: ledger/2026-08-01.jsonl (append-only; every run adds lines)
|
|
124
|
+
```
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
budget remaining. Everything that happens (and everything refused) is one JSONL line.
|
|
126
|
+
Then `touch STOP` and run again to watch the kill-switch refuse everything — and land in
|
|
127
|
+
the ledger anyway (the halt is on the record too; silence is impossible).
|
|
89
128
|
|
|
90
129
|
For the LLM side, see `examples/sentinel/` — a complete service-health watchdog: deterministic
|
|
91
130
|
probes → an escalate-only LLM verdict → a typed diagnosis → contract-bounded remediation.
|
|
@@ -41,6 +41,16 @@ contracts:
|
|
|
41
41
|
scope: checkout-stack
|
|
42
42
|
tier: act # observe | propose | act
|
|
43
43
|
allows: [restart_worker]
|
|
44
|
+
- name: cache
|
|
45
|
+
status: active
|
|
46
|
+
scope: checkout-stack
|
|
47
|
+
tier: propose # may draft fixes; a human executes them
|
|
48
|
+
allows: [clear_cache]
|
|
49
|
+
- name: payments-db
|
|
50
|
+
status: proposed # not signed off — grants NOTHING until active
|
|
51
|
+
scope: checkout-stack
|
|
52
|
+
tier: act
|
|
53
|
+
allows: [failover_replica]
|
|
44
54
|
```
|
|
45
55
|
|
|
46
56
|
```python
|
|
@@ -49,8 +59,15 @@ from pathlib import Path
|
|
|
49
59
|
|
|
50
60
|
from bounded_agent import ActionSet, ContractBook, Envelope, EvidenceLedger, Gate
|
|
51
61
|
|
|
62
|
+
print("1. the book — who may do what, declared by a human:")
|
|
52
63
|
book = ContractBook.load("contracts.yaml") # raises loudly if missing/malformed
|
|
53
|
-
|
|
64
|
+
for c in book.contracts:
|
|
65
|
+
print(f" {c.name}: status={c.status} scope={c.scope} tier={c.tier} allows={list(c.allows)}")
|
|
66
|
+
|
|
67
|
+
gate = Gate(required_scope="checkout-stack", required_tier="act")
|
|
68
|
+
granted = book.granted(gate)
|
|
69
|
+
print(f"\n2. the gate — scope={gate.required_scope!r} at tier '{gate.required_tier}' grants:")
|
|
70
|
+
print(f" {granted or 'nothing — no active, in-scope contract at this tier'}")
|
|
54
71
|
|
|
55
72
|
actions = ActionSet() # the implemented closed set:
|
|
56
73
|
actions.register("restart_worker", lambda: (True, "worker restarted"))
|
|
@@ -58,20 +75,42 @@ actions.register("restart_worker", lambda: (True, "worker restarted"))
|
|
|
58
75
|
env = Envelope(armed=True, # ships disarmed; arming is explicit
|
|
59
76
|
kill_switch_path=Path("STOP"), # `touch STOP` halts everything
|
|
60
77
|
max_actions_per_run=2,
|
|
61
|
-
notify=
|
|
78
|
+
notify=lambda msg: print(f" [report] {msg}"))
|
|
79
|
+
ks = "PRESENT" if env.kill_switch_path.exists() else "absent"
|
|
80
|
+
print(f"\n3. the envelope — armed={env.armed}, kill-switch {ks}, breaker {env.max_actions_per_run}/run")
|
|
62
81
|
|
|
82
|
+
print("\n4. executing (double-closed: contracted AND registered; every outcome reported):")
|
|
63
83
|
outcome = actions.execute(list(granted.items()), env,
|
|
64
84
|
ledger=EvidenceLedger(Path("ledger")),
|
|
65
85
|
period=dt.date.today())
|
|
66
|
-
|
|
86
|
+
|
|
87
|
+
print(f"\n5. outcome — {outcome.summary()}")
|
|
88
|
+
print(f" ledger: ledger/{dt.date.today().isoformat()}.jsonl (append-only; every run adds lines)")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run it and the output narrates the authorization chain — note the gate silently denying
|
|
92
|
+
`cache` (propose tier) and `payments-db` (not signed off):
|
|
93
|
+
|
|
67
94
|
```
|
|
95
|
+
1. the book — who may do what, declared by a human:
|
|
96
|
+
queue-worker: status=active scope=checkout-stack tier=act allows=['restart_worker']
|
|
97
|
+
cache: status=active scope=checkout-stack tier=propose allows=['clear_cache']
|
|
98
|
+
payments-db: status=proposed scope=checkout-stack tier=act allows=['failover_replica']
|
|
99
|
+
|
|
100
|
+
2. the gate — scope='checkout-stack' at tier 'act' grants:
|
|
101
|
+
{'queue-worker': ('restart_worker',)}
|
|
68
102
|
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
3. the envelope — armed=True, kill-switch absent, breaker 2/run
|
|
104
|
+
|
|
105
|
+
4. executing (double-closed: contracted AND registered; every outcome reported):
|
|
106
|
+
[report] executed-ok: restart_worker on queue-worker (worker restarted)
|
|
107
|
+
|
|
108
|
+
5. outcome — 1 executed | queue-worker:restart_worker=executed-ok
|
|
109
|
+
ledger: ledger/2026-08-01.jsonl (append-only; every run adds lines)
|
|
110
|
+
```
|
|
71
111
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
budget remaining. Everything that happens (and everything refused) is one JSONL line.
|
|
112
|
+
Then `touch STOP` and run again to watch the kill-switch refuse everything — and land in
|
|
113
|
+
the ledger anyway (the halt is on the record too; silence is impossible).
|
|
75
114
|
|
|
76
115
|
For the LLM side, see `examples/sentinel/` — a complete service-health watchdog: deterministic
|
|
77
116
|
probes → an escalate-only LLM verdict → a typed diagnosis → contract-bounded remediation.
|
|
@@ -57,6 +57,15 @@ class ExecOutcome(BaseModel):
|
|
|
57
57
|
records: tuple[ActionRecord, ...] = ()
|
|
58
58
|
executed: int = 0 # attempts, successful or not
|
|
59
59
|
|
|
60
|
+
def summary(self) -> str:
|
|
61
|
+
"""One human line for logs/pagers: what ran, what was refused, what halted."""
|
|
62
|
+
if self.blocked_reason is not None:
|
|
63
|
+
return f"blocked: {self.blocked_reason}"
|
|
64
|
+
if not self.records:
|
|
65
|
+
return "0 executed (nothing actionable)"
|
|
66
|
+
parts = [f"{r.contract}:{r.action}={r.status}" for r in self.records]
|
|
67
|
+
return f"{self.executed} executed | " + "; ".join(parts)
|
|
68
|
+
|
|
60
69
|
|
|
61
70
|
class ActionSet:
|
|
62
71
|
"""The registered half of the double-closed check, plus the execution loop."""
|
|
@@ -107,3 +107,18 @@ def test_every_record_is_notified_and_ledgered(tmp_path):
|
|
|
107
107
|
ledger=ledger, period=DAY)
|
|
108
108
|
lines = (tmp_path / "ev" / f"{DAY.isoformat()}.jsonl").read_text().splitlines()
|
|
109
109
|
assert len(lines) == 2 and len(seen) == 2 # the refusal is recorded too
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_outcome_summary_reads_like_a_report():
|
|
113
|
+
calls: list[str] = []
|
|
114
|
+
out = _actions(calls).execute([("svc", ("restart_worker", "drop_database"))],
|
|
115
|
+
Envelope(armed=True, max_actions_per_run=5))
|
|
116
|
+
s = out.summary()
|
|
117
|
+
assert "1 executed" in s
|
|
118
|
+
assert "svc:restart_worker=executed-ok" in s
|
|
119
|
+
assert "svc:drop_database=refused" in s
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_outcome_summary_when_blocked():
|
|
123
|
+
out = _actions([]).execute([("svc", ("restart_worker",))], Envelope())
|
|
124
|
+
assert out.summary() == "blocked: disarmed"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|