enact-sdk 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.
- enact_sdk-0.1.0/LICENSE +94 -0
- enact_sdk-0.1.0/PKG-INFO +261 -0
- enact_sdk-0.1.0/README.md +236 -0
- enact_sdk-0.1.0/enact/__init__.py +19 -0
- enact_sdk-0.1.0/enact/client.py +168 -0
- enact_sdk-0.1.0/enact/connectors/__init__.py +1 -0
- enact_sdk-0.1.0/enact/connectors/github.py +259 -0
- enact_sdk-0.1.0/enact/models.py +145 -0
- enact_sdk-0.1.0/enact/policies/__init__.py +1 -0
- enact_sdk-0.1.0/enact/policies/access.py +98 -0
- enact_sdk-0.1.0/enact/policies/crm.py +118 -0
- enact_sdk-0.1.0/enact/policies/git.py +137 -0
- enact_sdk-0.1.0/enact/policies/time.py +73 -0
- enact_sdk-0.1.0/enact/policy.py +85 -0
- enact_sdk-0.1.0/enact/receipt.py +163 -0
- enact_sdk-0.1.0/enact/workflows/__init__.py +1 -0
- enact_sdk-0.1.0/enact/workflows/agent_pr_workflow.py +71 -0
- enact_sdk-0.1.0/enact/workflows/db_safe_insert.py +81 -0
- enact_sdk-0.1.0/enact_sdk.egg-info/PKG-INFO +261 -0
- enact_sdk-0.1.0/enact_sdk.egg-info/SOURCES.txt +30 -0
- enact_sdk-0.1.0/enact_sdk.egg-info/dependency_links.txt +1 -0
- enact_sdk-0.1.0/enact_sdk.egg-info/requires.txt +21 -0
- enact_sdk-0.1.0/enact_sdk.egg-info/top_level.txt +1 -0
- enact_sdk-0.1.0/pyproject.toml +33 -0
- enact_sdk-0.1.0/setup.cfg +4 -0
- enact_sdk-0.1.0/tests/test_client.py +194 -0
- enact_sdk-0.1.0/tests/test_git_policies.py +196 -0
- enact_sdk-0.1.0/tests/test_github.py +159 -0
- enact_sdk-0.1.0/tests/test_policies.py +252 -0
- enact_sdk-0.1.0/tests/test_policy_engine.py +106 -0
- enact_sdk-0.1.0/tests/test_receipt.py +171 -0
- enact_sdk-0.1.0/tests/test_workflows.py +118 -0
enact_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Elastic License 2.0
|
|
2
|
+
|
|
3
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
+
available, and prepare derivative works of the software, in each case subject
|
|
14
|
+
to the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
## Limitations
|
|
17
|
+
|
|
18
|
+
You may not provide the software to third parties as a hosted or managed
|
|
19
|
+
service, where the service provides users with access to any substantial set
|
|
20
|
+
of the features or functionality of the software.
|
|
21
|
+
|
|
22
|
+
You may not sell, resell, sublicense, or otherwise commercially distribute the
|
|
23
|
+
software itself as a standalone product or as the primary component of a paid
|
|
24
|
+
offering. For purposes of this restriction, "Sell" means practicing any or all
|
|
25
|
+
of the rights granted to you under this license to provide to third parties,
|
|
26
|
+
for a fee or other consideration, a product or service whose value derives
|
|
27
|
+
entirely or substantially from the functionality of the software.
|
|
28
|
+
|
|
29
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
30
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
31
|
+
software that is protected by the license key.
|
|
32
|
+
|
|
33
|
+
You may not alter, remove, or obscure any licensing, copyright, or other
|
|
34
|
+
notices of the licensor in the software. Any use of the licensor's trademarks
|
|
35
|
+
is subject to applicable law.
|
|
36
|
+
|
|
37
|
+
## Patents
|
|
38
|
+
|
|
39
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
40
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
41
|
+
sale, import and otherwise transfer the software. This license does not cover
|
|
42
|
+
any patent claims that you cause to be infringed by modifications or additions
|
|
43
|
+
to the software. If you or your company make any written claim that the
|
|
44
|
+
software infringes or contributes to infringement of any patent, your patent
|
|
45
|
+
license for the software granted under these terms ends immediately.
|
|
46
|
+
|
|
47
|
+
## Notices
|
|
48
|
+
|
|
49
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
50
|
+
you also gets a copy of these terms.
|
|
51
|
+
|
|
52
|
+
If you modify the software, you must include in any modified copies of the
|
|
53
|
+
software prominent notices stating that you have modified the software.
|
|
54
|
+
|
|
55
|
+
## No Other Rights
|
|
56
|
+
|
|
57
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
58
|
+
these terms.
|
|
59
|
+
|
|
60
|
+
## Termination
|
|
61
|
+
|
|
62
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
63
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
64
|
+
with a notice of your violation, and you cease all violation of this license
|
|
65
|
+
no later than 30 days after you receive that notice, your licenses will be
|
|
66
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
67
|
+
reinstatement, any additional violation of these terms will cause your licenses
|
|
68
|
+
to terminate automatically and permanently.
|
|
69
|
+
|
|
70
|
+
## No Liability
|
|
71
|
+
|
|
72
|
+
As far as the law allows, the software comes as is, without any warranty or
|
|
73
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
74
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
75
|
+
legal claim.
|
|
76
|
+
|
|
77
|
+
## Definitions
|
|
78
|
+
|
|
79
|
+
The *licensor* is the entity offering these terms, and the *software* is the
|
|
80
|
+
software the licensor makes available under these terms, including any portion
|
|
81
|
+
of it.
|
|
82
|
+
|
|
83
|
+
*you* refers to the individual or entity agreeing to these terms.
|
|
84
|
+
|
|
85
|
+
*your company* is any legal entity, sole proprietorship, or other kind of
|
|
86
|
+
organization that you work for, plus all organizations that have control over,
|
|
87
|
+
are under the control of, or are under common control with that organization.
|
|
88
|
+
*control* means ownership of substantially all the assets of an entity, or the
|
|
89
|
+
power to direct its management and legal affairs.
|
|
90
|
+
|
|
91
|
+
*your licenses* are all the licenses granted to you for the software under
|
|
92
|
+
these terms.
|
|
93
|
+
|
|
94
|
+
*use* includes any use of the software, including use in modified form.
|
enact_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: enact-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An action firewall for AI agents
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: python-dotenv
|
|
10
|
+
Provides-Extra: postgres
|
|
11
|
+
Requires-Dist: psycopg2-binary; extra == "postgres"
|
|
12
|
+
Provides-Extra: github
|
|
13
|
+
Requires-Dist: PyGithub; extra == "github"
|
|
14
|
+
Provides-Extra: hubspot
|
|
15
|
+
Requires-Dist: hubspot-api-client; extra == "hubspot"
|
|
16
|
+
Provides-Extra: all
|
|
17
|
+
Requires-Dist: psycopg2-binary; extra == "all"
|
|
18
|
+
Requires-Dist: PyGithub; extra == "all"
|
|
19
|
+
Requires-Dist: hubspot-api-client; extra == "all"
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
23
|
+
Requires-Dist: responses; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Enact
|
|
27
|
+
|
|
28
|
+
**An action firewall for AI agents.**
|
|
29
|
+
|
|
30
|
+
Enact sits between your AI agent and the outside world. Every action goes through a policy gate first. If it passes, Enact executes it and returns a signed receipt. If it doesn't, nothing happens.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from enact import EnactClient
|
|
34
|
+
from enact.connectors.github import GitHubConnector
|
|
35
|
+
from enact.workflows.agent_pr_workflow import agent_pr_workflow
|
|
36
|
+
from enact.policies.git import no_push_to_main, require_branch_prefix
|
|
37
|
+
|
|
38
|
+
enact = EnactClient(
|
|
39
|
+
systems={"github": GitHubConnector(token="...")},
|
|
40
|
+
policies=[no_push_to_main, require_branch_prefix(prefix="agent/")],
|
|
41
|
+
workflows=[agent_pr_workflow],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
result, receipt = enact.run(
|
|
45
|
+
workflow="agent_pr_workflow",
|
|
46
|
+
actor_email="agent@company.com",
|
|
47
|
+
payload={"repo": "owner/repo", "branch": "agent/my-feature"},
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## How It Works
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
agent calls enact.run()
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
┌───────────────────┐
|
|
60
|
+
│ Policy Gate │ All policies run. Any failure = BLOCK.
|
|
61
|
+
│ (pure Python, │ No LLMs. Versioned in Git. Testable.
|
|
62
|
+
│ no LLMs) │
|
|
63
|
+
└────────┬──────────┘
|
|
64
|
+
PASS │ BLOCK
|
|
65
|
+
│ └──▶ Receipt (decision=BLOCK, actions_taken=[])
|
|
66
|
+
▼
|
|
67
|
+
┌───────────────────┐
|
|
68
|
+
│ Workflow runs │ Enact executes the workflow against real systems.
|
|
69
|
+
│ against real │ Each action produces an ActionResult.
|
|
70
|
+
│ systems │
|
|
71
|
+
└────────┬──────────┘
|
|
72
|
+
│
|
|
73
|
+
▼
|
|
74
|
+
┌───────────────────┐
|
|
75
|
+
│ Signed Receipt │ HMAC-SHA256 signed. Captures who/what/why/
|
|
76
|
+
│ │ pass-fail/what changed.
|
|
77
|
+
└────────┬──────────┘
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
RunResult returned to agent
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Three Things Enact Gives You
|
|
86
|
+
|
|
87
|
+
1. **Vetted action allowlist** — agents can only call workflows you explicitly register
|
|
88
|
+
2. **Deterministic policy engine** — plain Python functions, no LLMs, Git-versioned, fully testable
|
|
89
|
+
3. **Human-readable receipts** — every run records who, what, why, pass/fail, and what changed
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## What Enact Can Do Right Now
|
|
94
|
+
|
|
95
|
+
### Policy enforcement
|
|
96
|
+
- Block agents from pushing directly to `main` or `master`
|
|
97
|
+
- Require branch names to match a prefix (e.g. `agent/`)
|
|
98
|
+
- Cap how many files an agent can touch per commit
|
|
99
|
+
- Restrict actions to a UTC time window (e.g. 2am–6am maintenance window), including midnight-crossing windows like 22:00–06:00
|
|
100
|
+
- Block contractors from writing to PII fields
|
|
101
|
+
- Require the actor to hold a specific role (`admin`, `engineer`, etc.)
|
|
102
|
+
- Prevent duplicate contacts from being created in HubSpot (live lookup before the workflow runs)
|
|
103
|
+
- Rate-limit how many tasks an agent creates per contact within a rolling time window
|
|
104
|
+
|
|
105
|
+
### GitHub operations (via `GitHubConnector`)
|
|
106
|
+
- Create a branch
|
|
107
|
+
- Open a pull request
|
|
108
|
+
- Create an issue
|
|
109
|
+
- Delete a branch
|
|
110
|
+
- Merge a pull request
|
|
111
|
+
|
|
112
|
+
Every method is allowlisted at construction time — `GitHubConnector(token=..., allowlist=["create_branch", "create_pr"])` means the connector will refuse to call any method not on the list, even if the workflow tries.
|
|
113
|
+
|
|
114
|
+
### Built-in workflows
|
|
115
|
+
- **`agent_pr_workflow`** — creates a feature branch then opens a PR; aborts cleanly if branch creation fails so you never get a PR pointing at a non-existent branch
|
|
116
|
+
- **`db_safe_insert`** — checks for a duplicate row before inserting; returns an explanatory failure instead of letting the database raise a constraint violation
|
|
117
|
+
|
|
118
|
+
### Receipts
|
|
119
|
+
Every run — pass or block — produces an HMAC-SHA256 signed JSON receipt written to `receipts/`. It captures: who ran what, the full payload, every policy result with its reason, the final decision, and a timestamp. `verify_signature()` lets you prove a receipt hasn't been tampered with after the fact.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## File Structure
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
enact/
|
|
127
|
+
├── enact/ # pip-installable package
|
|
128
|
+
│ ├── __init__.py # exports: EnactClient, all models
|
|
129
|
+
│ ├── models.py # data shapes for every object in a run
|
|
130
|
+
│ ├── client.py # EnactClient — orchestrates the full run() loop
|
|
131
|
+
│ ├── policy.py # policy engine — runs all checks, returns PolicyResult list
|
|
132
|
+
│ ├── receipt.py # builds, HMAC-signs, verifies, and writes receipts
|
|
133
|
+
│ ├── connectors/
|
|
134
|
+
│ │ ├── github.py # GitHub: create_branch, create_pr, create_issue, delete_branch, merge_pr
|
|
135
|
+
│ │ └── postgres.py # Postgres: insert_row, update_row, select_rows, delete_row (planned v0.2)
|
|
136
|
+
│ ├── workflows/
|
|
137
|
+
│ │ ├── agent_pr_workflow.py # create branch → open PR (never to main)
|
|
138
|
+
│ │ └── db_safe_insert.py # check constraints → insert row
|
|
139
|
+
│ └── policies/
|
|
140
|
+
│ ├── git.py # no_push_to_main, max_files_per_commit, require_branch_prefix
|
|
141
|
+
│ ├── crm.py # no_duplicate_contacts, limit_tasks_per_contact
|
|
142
|
+
│ ├── access.py # contractor_cannot_write_pii, require_actor_role
|
|
143
|
+
│ └── time.py # within_maintenance_window
|
|
144
|
+
├── tests/
|
|
145
|
+
│ ├── test_policy_engine.py
|
|
146
|
+
│ ├── test_receipt.py
|
|
147
|
+
│ ├── test_client.py
|
|
148
|
+
│ ├── test_github.py
|
|
149
|
+
│ ├── test_git_policies.py
|
|
150
|
+
│ ├── test_policies.py
|
|
151
|
+
│ └── test_workflows.py
|
|
152
|
+
├── examples/
|
|
153
|
+
│ └── quickstart.py # runnable demo — runs PASS then BLOCK, prints receipt
|
|
154
|
+
├── receipts/ # auto-created at runtime, gitignored
|
|
155
|
+
└── pyproject.toml # PyPI config
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### What each file does
|
|
159
|
+
|
|
160
|
+
| File | Job |
|
|
161
|
+
|------|-----|
|
|
162
|
+
| `models.py` | Defines data shapes. `WorkflowContext` (inputs), `PolicyResult` (one policy check), `ActionResult` (one workflow action), `Receipt` (full signed run record), `RunResult` (what the agent gets back). |
|
|
163
|
+
| `client.py` | The main entry point. `EnactClient.run()` builds context, runs policies, executes the workflow if PASS, writes the receipt, returns `RunResult`. |
|
|
164
|
+
| `policy.py` | Runs every registered policy against `WorkflowContext`. Returns `list[PolicyResult]`. Never bails early — always runs all checks. |
|
|
165
|
+
| `receipt.py` | Takes policy results + action results, builds a `Receipt`, signs it with HMAC-SHA256, writes it to `receipts/`. |
|
|
166
|
+
| `connectors/` | Thin wrappers around vendor SDKs. Each connector exposes named actions (`create_branch`, `insert_row`, etc.) that workflows call. |
|
|
167
|
+
| `workflows/` | Python functions that orchestrate connector actions. Each workflow step produces an `ActionResult`. |
|
|
168
|
+
| `policies/` | Built-in reusable policy functions (ships with `pip install enact`). Each takes a `WorkflowContext` and returns a `PolicyResult`. |
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Data Flow (in code)
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
enact.run(workflow="agent_pr_workflow", actor_email="agent@co.com", payload={"repo": "owner/repo", "branch": "agent/fix"})
|
|
176
|
+
│
|
|
177
|
+
├─▶ WorkflowContext(workflow, actor_email, payload, systems)
|
|
178
|
+
│
|
|
179
|
+
├─▶ policy_results = [
|
|
180
|
+
│ PolicyResult(policy="no_push_to_main", passed=True, reason="Branch is not main/master"),
|
|
181
|
+
│ PolicyResult(policy="require_branch_prefix", passed=True, reason="Branch 'agent/fix' has required prefix"),
|
|
182
|
+
│ ]
|
|
183
|
+
│
|
|
184
|
+
├─▶ decision = PASS → execute workflow
|
|
185
|
+
│
|
|
186
|
+
├─▶ actions_taken = [
|
|
187
|
+
│ ActionResult(action="create_branch", system="github", success=True, output={"branch": "agent/fix"}),
|
|
188
|
+
│ ActionResult(action="create_pr", system="github", success=True, output={"pr_number": 42, "url": "..."}),
|
|
189
|
+
│ ]
|
|
190
|
+
│
|
|
191
|
+
├─▶ Receipt(run_id, workflow, actor_email, payload, policy_results,
|
|
192
|
+
│ decision="PASS", actions_taken, timestamp, signature)
|
|
193
|
+
│
|
|
194
|
+
└─▶ RunResult(success=True, workflow="agent_pr_workflow", output={...})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Connectors (v0.1)
|
|
200
|
+
|
|
201
|
+
| System | Actions | Status |
|
|
202
|
+
|--------|---------|--------|
|
|
203
|
+
| GitHub | `create_branch`, `create_pr`, `push_commit`, `delete_branch`, `create_issue`, `merge_pr` | ✅ v0.1 |
|
|
204
|
+
| Postgres | `insert_row`, `update_row`, `select_rows`, `delete_row` | 🔜 v0.2 |
|
|
205
|
+
| HubSpot | `create_contact`, `update_deal`, `create_task`, `get_contact` | 🔜 v0.2 |
|
|
206
|
+
|
|
207
|
+
GitHub connector works with any repo accessible via a personal access token or GitHub App.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Built-in Policies (v0.1)
|
|
212
|
+
|
|
213
|
+
| File | Policy | What it blocks |
|
|
214
|
+
|------|--------|----------------|
|
|
215
|
+
| `git.py` | `no_push_to_main` | Any direct push to main/master |
|
|
216
|
+
| `git.py` | `max_files_per_commit` | Commits touching too many files (blast radius) |
|
|
217
|
+
| `git.py` | `require_branch_prefix` | Agent branches not prefixed correctly |
|
|
218
|
+
| `crm.py` | `no_duplicate_contacts` | Creating a contact that already exists |
|
|
219
|
+
| `crm.py` | `limit_tasks_per_contact` | Too many tasks created in a time window |
|
|
220
|
+
| `access.py` | `contractor_cannot_write_pii` | Contractors writing PII fields |
|
|
221
|
+
| `access.py` | `require_actor_role` | Actors without an allowed role |
|
|
222
|
+
| `time.py` | `within_maintenance_window` | Actions outside allowed UTC time windows |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Quickstart
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
git clone https://github.com/russellmiller3/enact
|
|
230
|
+
cd enact
|
|
231
|
+
pip install -e ".[dev]"
|
|
232
|
+
python examples/quickstart.py
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Run Tests
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
pytest tests/ -v
|
|
241
|
+
# 96 tests, 0 failures
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Environment Variables
|
|
247
|
+
|
|
248
|
+
| Variable | Default | Purpose |
|
|
249
|
+
|----------|---------|---------|
|
|
250
|
+
| `ENACT_SECRET` | `enact-default-secret` | HMAC signing key for receipts. Override in production. |
|
|
251
|
+
| `GITHUB_TOKEN` | — | GitHub PAT for GitHubConnector |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
[Elastic License 2.0 (ELv2)](./LICENSE)
|
|
258
|
+
|
|
259
|
+
Free to use, modify, and redistribute. You may **not** offer Enact as a hosted
|
|
260
|
+
or managed service, nor sell or resell the software itself as a product.
|
|
261
|
+
See [LICENSE](./LICENSE) for full terms.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Enact
|
|
2
|
+
|
|
3
|
+
**An action firewall for AI agents.**
|
|
4
|
+
|
|
5
|
+
Enact sits between your AI agent and the outside world. Every action goes through a policy gate first. If it passes, Enact executes it and returns a signed receipt. If it doesn't, nothing happens.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from enact import EnactClient
|
|
9
|
+
from enact.connectors.github import GitHubConnector
|
|
10
|
+
from enact.workflows.agent_pr_workflow import agent_pr_workflow
|
|
11
|
+
from enact.policies.git import no_push_to_main, require_branch_prefix
|
|
12
|
+
|
|
13
|
+
enact = EnactClient(
|
|
14
|
+
systems={"github": GitHubConnector(token="...")},
|
|
15
|
+
policies=[no_push_to_main, require_branch_prefix(prefix="agent/")],
|
|
16
|
+
workflows=[agent_pr_workflow],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
result, receipt = enact.run(
|
|
20
|
+
workflow="agent_pr_workflow",
|
|
21
|
+
actor_email="agent@company.com",
|
|
22
|
+
payload={"repo": "owner/repo", "branch": "agent/my-feature"},
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
agent calls enact.run()
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
┌───────────────────┐
|
|
35
|
+
│ Policy Gate │ All policies run. Any failure = BLOCK.
|
|
36
|
+
│ (pure Python, │ No LLMs. Versioned in Git. Testable.
|
|
37
|
+
│ no LLMs) │
|
|
38
|
+
└────────┬──────────┘
|
|
39
|
+
PASS │ BLOCK
|
|
40
|
+
│ └──▶ Receipt (decision=BLOCK, actions_taken=[])
|
|
41
|
+
▼
|
|
42
|
+
┌───────────────────┐
|
|
43
|
+
│ Workflow runs │ Enact executes the workflow against real systems.
|
|
44
|
+
│ against real │ Each action produces an ActionResult.
|
|
45
|
+
│ systems │
|
|
46
|
+
└────────┬──────────┘
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
┌───────────────────┐
|
|
50
|
+
│ Signed Receipt │ HMAC-SHA256 signed. Captures who/what/why/
|
|
51
|
+
│ │ pass-fail/what changed.
|
|
52
|
+
└────────┬──────────┘
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
RunResult returned to agent
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Three Things Enact Gives You
|
|
61
|
+
|
|
62
|
+
1. **Vetted action allowlist** — agents can only call workflows you explicitly register
|
|
63
|
+
2. **Deterministic policy engine** — plain Python functions, no LLMs, Git-versioned, fully testable
|
|
64
|
+
3. **Human-readable receipts** — every run records who, what, why, pass/fail, and what changed
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## What Enact Can Do Right Now
|
|
69
|
+
|
|
70
|
+
### Policy enforcement
|
|
71
|
+
- Block agents from pushing directly to `main` or `master`
|
|
72
|
+
- Require branch names to match a prefix (e.g. `agent/`)
|
|
73
|
+
- Cap how many files an agent can touch per commit
|
|
74
|
+
- Restrict actions to a UTC time window (e.g. 2am–6am maintenance window), including midnight-crossing windows like 22:00–06:00
|
|
75
|
+
- Block contractors from writing to PII fields
|
|
76
|
+
- Require the actor to hold a specific role (`admin`, `engineer`, etc.)
|
|
77
|
+
- Prevent duplicate contacts from being created in HubSpot (live lookup before the workflow runs)
|
|
78
|
+
- Rate-limit how many tasks an agent creates per contact within a rolling time window
|
|
79
|
+
|
|
80
|
+
### GitHub operations (via `GitHubConnector`)
|
|
81
|
+
- Create a branch
|
|
82
|
+
- Open a pull request
|
|
83
|
+
- Create an issue
|
|
84
|
+
- Delete a branch
|
|
85
|
+
- Merge a pull request
|
|
86
|
+
|
|
87
|
+
Every method is allowlisted at construction time — `GitHubConnector(token=..., allowlist=["create_branch", "create_pr"])` means the connector will refuse to call any method not on the list, even if the workflow tries.
|
|
88
|
+
|
|
89
|
+
### Built-in workflows
|
|
90
|
+
- **`agent_pr_workflow`** — creates a feature branch then opens a PR; aborts cleanly if branch creation fails so you never get a PR pointing at a non-existent branch
|
|
91
|
+
- **`db_safe_insert`** — checks for a duplicate row before inserting; returns an explanatory failure instead of letting the database raise a constraint violation
|
|
92
|
+
|
|
93
|
+
### Receipts
|
|
94
|
+
Every run — pass or block — produces an HMAC-SHA256 signed JSON receipt written to `receipts/`. It captures: who ran what, the full payload, every policy result with its reason, the final decision, and a timestamp. `verify_signature()` lets you prove a receipt hasn't been tampered with after the fact.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## File Structure
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
enact/
|
|
102
|
+
├── enact/ # pip-installable package
|
|
103
|
+
│ ├── __init__.py # exports: EnactClient, all models
|
|
104
|
+
│ ├── models.py # data shapes for every object in a run
|
|
105
|
+
│ ├── client.py # EnactClient — orchestrates the full run() loop
|
|
106
|
+
│ ├── policy.py # policy engine — runs all checks, returns PolicyResult list
|
|
107
|
+
│ ├── receipt.py # builds, HMAC-signs, verifies, and writes receipts
|
|
108
|
+
│ ├── connectors/
|
|
109
|
+
│ │ ├── github.py # GitHub: create_branch, create_pr, create_issue, delete_branch, merge_pr
|
|
110
|
+
│ │ └── postgres.py # Postgres: insert_row, update_row, select_rows, delete_row (planned v0.2)
|
|
111
|
+
│ ├── workflows/
|
|
112
|
+
│ │ ├── agent_pr_workflow.py # create branch → open PR (never to main)
|
|
113
|
+
│ │ └── db_safe_insert.py # check constraints → insert row
|
|
114
|
+
│ └── policies/
|
|
115
|
+
│ ├── git.py # no_push_to_main, max_files_per_commit, require_branch_prefix
|
|
116
|
+
│ ├── crm.py # no_duplicate_contacts, limit_tasks_per_contact
|
|
117
|
+
│ ├── access.py # contractor_cannot_write_pii, require_actor_role
|
|
118
|
+
│ └── time.py # within_maintenance_window
|
|
119
|
+
├── tests/
|
|
120
|
+
│ ├── test_policy_engine.py
|
|
121
|
+
│ ├── test_receipt.py
|
|
122
|
+
│ ├── test_client.py
|
|
123
|
+
│ ├── test_github.py
|
|
124
|
+
│ ├── test_git_policies.py
|
|
125
|
+
│ ├── test_policies.py
|
|
126
|
+
│ └── test_workflows.py
|
|
127
|
+
├── examples/
|
|
128
|
+
│ └── quickstart.py # runnable demo — runs PASS then BLOCK, prints receipt
|
|
129
|
+
├── receipts/ # auto-created at runtime, gitignored
|
|
130
|
+
└── pyproject.toml # PyPI config
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### What each file does
|
|
134
|
+
|
|
135
|
+
| File | Job |
|
|
136
|
+
|------|-----|
|
|
137
|
+
| `models.py` | Defines data shapes. `WorkflowContext` (inputs), `PolicyResult` (one policy check), `ActionResult` (one workflow action), `Receipt` (full signed run record), `RunResult` (what the agent gets back). |
|
|
138
|
+
| `client.py` | The main entry point. `EnactClient.run()` builds context, runs policies, executes the workflow if PASS, writes the receipt, returns `RunResult`. |
|
|
139
|
+
| `policy.py` | Runs every registered policy against `WorkflowContext`. Returns `list[PolicyResult]`. Never bails early — always runs all checks. |
|
|
140
|
+
| `receipt.py` | Takes policy results + action results, builds a `Receipt`, signs it with HMAC-SHA256, writes it to `receipts/`. |
|
|
141
|
+
| `connectors/` | Thin wrappers around vendor SDKs. Each connector exposes named actions (`create_branch`, `insert_row`, etc.) that workflows call. |
|
|
142
|
+
| `workflows/` | Python functions that orchestrate connector actions. Each workflow step produces an `ActionResult`. |
|
|
143
|
+
| `policies/` | Built-in reusable policy functions (ships with `pip install enact`). Each takes a `WorkflowContext` and returns a `PolicyResult`. |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Data Flow (in code)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
enact.run(workflow="agent_pr_workflow", actor_email="agent@co.com", payload={"repo": "owner/repo", "branch": "agent/fix"})
|
|
151
|
+
│
|
|
152
|
+
├─▶ WorkflowContext(workflow, actor_email, payload, systems)
|
|
153
|
+
│
|
|
154
|
+
├─▶ policy_results = [
|
|
155
|
+
│ PolicyResult(policy="no_push_to_main", passed=True, reason="Branch is not main/master"),
|
|
156
|
+
│ PolicyResult(policy="require_branch_prefix", passed=True, reason="Branch 'agent/fix' has required prefix"),
|
|
157
|
+
│ ]
|
|
158
|
+
│
|
|
159
|
+
├─▶ decision = PASS → execute workflow
|
|
160
|
+
│
|
|
161
|
+
├─▶ actions_taken = [
|
|
162
|
+
│ ActionResult(action="create_branch", system="github", success=True, output={"branch": "agent/fix"}),
|
|
163
|
+
│ ActionResult(action="create_pr", system="github", success=True, output={"pr_number": 42, "url": "..."}),
|
|
164
|
+
│ ]
|
|
165
|
+
│
|
|
166
|
+
├─▶ Receipt(run_id, workflow, actor_email, payload, policy_results,
|
|
167
|
+
│ decision="PASS", actions_taken, timestamp, signature)
|
|
168
|
+
│
|
|
169
|
+
└─▶ RunResult(success=True, workflow="agent_pr_workflow", output={...})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Connectors (v0.1)
|
|
175
|
+
|
|
176
|
+
| System | Actions | Status |
|
|
177
|
+
|--------|---------|--------|
|
|
178
|
+
| GitHub | `create_branch`, `create_pr`, `push_commit`, `delete_branch`, `create_issue`, `merge_pr` | ✅ v0.1 |
|
|
179
|
+
| Postgres | `insert_row`, `update_row`, `select_rows`, `delete_row` | 🔜 v0.2 |
|
|
180
|
+
| HubSpot | `create_contact`, `update_deal`, `create_task`, `get_contact` | 🔜 v0.2 |
|
|
181
|
+
|
|
182
|
+
GitHub connector works with any repo accessible via a personal access token or GitHub App.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Built-in Policies (v0.1)
|
|
187
|
+
|
|
188
|
+
| File | Policy | What it blocks |
|
|
189
|
+
|------|--------|----------------|
|
|
190
|
+
| `git.py` | `no_push_to_main` | Any direct push to main/master |
|
|
191
|
+
| `git.py` | `max_files_per_commit` | Commits touching too many files (blast radius) |
|
|
192
|
+
| `git.py` | `require_branch_prefix` | Agent branches not prefixed correctly |
|
|
193
|
+
| `crm.py` | `no_duplicate_contacts` | Creating a contact that already exists |
|
|
194
|
+
| `crm.py` | `limit_tasks_per_contact` | Too many tasks created in a time window |
|
|
195
|
+
| `access.py` | `contractor_cannot_write_pii` | Contractors writing PII fields |
|
|
196
|
+
| `access.py` | `require_actor_role` | Actors without an allowed role |
|
|
197
|
+
| `time.py` | `within_maintenance_window` | Actions outside allowed UTC time windows |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Quickstart
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git clone https://github.com/russellmiller3/enact
|
|
205
|
+
cd enact
|
|
206
|
+
pip install -e ".[dev]"
|
|
207
|
+
python examples/quickstart.py
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Run Tests
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pytest tests/ -v
|
|
216
|
+
# 96 tests, 0 failures
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Environment Variables
|
|
222
|
+
|
|
223
|
+
| Variable | Default | Purpose |
|
|
224
|
+
|----------|---------|---------|
|
|
225
|
+
| `ENACT_SECRET` | `enact-default-secret` | HMAC signing key for receipts. Override in production. |
|
|
226
|
+
| `GITHUB_TOKEN` | — | GitHub PAT for GitHubConnector |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
[Elastic License 2.0 (ELv2)](./LICENSE)
|
|
233
|
+
|
|
234
|
+
Free to use, modify, and redistribute. You may **not** offer Enact as a hosted
|
|
235
|
+
or managed service, nor sell or resell the software itself as a product.
|
|
236
|
+
See [LICENSE](./LICENSE) for full terms.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Enact — an action firewall for AI agents."""
|
|
2
|
+
|
|
3
|
+
from enact.client import EnactClient
|
|
4
|
+
from enact.models import (
|
|
5
|
+
WorkflowContext,
|
|
6
|
+
PolicyResult,
|
|
7
|
+
ActionResult,
|
|
8
|
+
Receipt,
|
|
9
|
+
RunResult,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"EnactClient",
|
|
14
|
+
"WorkflowContext",
|
|
15
|
+
"PolicyResult",
|
|
16
|
+
"ActionResult",
|
|
17
|
+
"Receipt",
|
|
18
|
+
"RunResult",
|
|
19
|
+
]
|