druks 0.0.1__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.
- druks-0.0.1/.gitignore +22 -0
- druks-0.0.1/LICENSE +21 -0
- druks-0.0.1/PKG-INFO +142 -0
- druks-0.0.1/README.md +103 -0
- druks-0.0.1/backend/druks/__init__.py +3 -0
- druks-0.0.1/backend/druks/agents.py +230 -0
- druks-0.0.1/backend/druks/alembic_support.py +62 -0
- druks-0.0.1/backend/druks/api/__init__.py +0 -0
- druks-0.0.1/backend/druks/api/app.py +250 -0
- druks-0.0.1/backend/druks/api/artifacts.py +21 -0
- druks-0.0.1/backend/druks/api/dependencies.py +18 -0
- druks-0.0.1/backend/druks/api/health_status.py +38 -0
- druks-0.0.1/backend/druks/api/routes.py +26 -0
- druks-0.0.1/backend/druks/api/runs.py +25 -0
- druks-0.0.1/backend/druks/api/schemas.py +43 -0
- druks-0.0.1/backend/druks/build/__init__.py +0 -0
- druks-0.0.1/backend/druks/build/contracts.py +307 -0
- druks-0.0.1/backend/druks/build/enums.py +36 -0
- druks-0.0.1/backend/druks/build/extension.py +233 -0
- druks-0.0.1/backend/druks/build/models.py +440 -0
- druks-0.0.1/backend/druks/build/policy.py +72 -0
- druks-0.0.1/backend/druks/build/routes.py +264 -0
- druks-0.0.1/backend/druks/build/schemas.py +191 -0
- druks-0.0.1/backend/druks/build/scoping/__init__.py +0 -0
- druks-0.0.1/backend/druks/build/scoping/contracts.py +52 -0
- druks-0.0.1/backend/druks/build/scoping/exceptions.py +2 -0
- druks-0.0.1/backend/druks/build/scoping/workflows.py +96 -0
- druks-0.0.1/backend/druks/build/subscribers.py +283 -0
- druks-0.0.1/backend/druks/build/workflows.py +624 -0
- druks-0.0.1/backend/druks/build/workspace.py +22 -0
- druks-0.0.1/backend/druks/cli.py +145 -0
- druks-0.0.1/backend/druks/core/__init__.py +0 -0
- druks-0.0.1/backend/druks/core/apis/__init__.py +0 -0
- druks-0.0.1/backend/druks/core/apis/exceptions.py +30 -0
- druks-0.0.1/backend/druks/core/apis/github.py +458 -0
- druks-0.0.1/backend/druks/core/apis/jira.py +100 -0
- druks-0.0.1/backend/druks/core/apis/linear.py +164 -0
- druks-0.0.1/backend/druks/core/extension.py +12 -0
- druks-0.0.1/backend/druks/core/models.py +11 -0
- druks-0.0.1/backend/druks/core/utils/__init__.py +0 -0
- druks-0.0.1/backend/druks/core/utils/time.py +15 -0
- druks-0.0.1/backend/druks/core/webhooks/__init__.py +3 -0
- druks-0.0.1/backend/druks/core/webhooks/github.py +96 -0
- druks-0.0.1/backend/druks/core/webhooks/jira.py +78 -0
- druks-0.0.1/backend/druks/core/webhooks/linear.py +80 -0
- druks-0.0.1/backend/druks/core/webhooks/slack.py +118 -0
- druks-0.0.1/backend/druks/core/workflows.py +54 -0
- druks-0.0.1/backend/druks/database.py +148 -0
- druks-0.0.1/backend/druks/db.py +6 -0
- druks-0.0.1/backend/druks/doctor.py +475 -0
- druks-0.0.1/backend/druks/durable/__init__.py +23 -0
- druks-0.0.1/backend/druks/durable/activity.py +11 -0
- druks-0.0.1/backend/druks/durable/dbos_state.py +78 -0
- druks-0.0.1/backend/druks/durable/engine.py +140 -0
- druks-0.0.1/backend/druks/durable/enums.py +24 -0
- druks-0.0.1/backend/druks/durable/exceptions.py +33 -0
- druks-0.0.1/backend/druks/durable/live.py +56 -0
- druks-0.0.1/backend/druks/durable/models.py +447 -0
- druks-0.0.1/backend/druks/durable/reads.py +216 -0
- druks-0.0.1/backend/druks/durable/schemas.py +213 -0
- druks-0.0.1/backend/druks/events/__init__.py +4 -0
- druks-0.0.1/backend/druks/events/builder.py +62 -0
- druks-0.0.1/backend/druks/events/feed.py +45 -0
- druks-0.0.1/backend/druks/events/models.py +52 -0
- druks-0.0.1/backend/druks/events/routes.py +82 -0
- druks-0.0.1/backend/druks/extensions/__init__.py +3 -0
- druks-0.0.1/backend/druks/extensions/base.py +462 -0
- druks-0.0.1/backend/druks/extensions/config.py +23 -0
- druks-0.0.1/backend/druks/extensions/exceptions.py +34 -0
- druks-0.0.1/backend/druks/extensions/fetcher.py +29 -0
- druks-0.0.1/backend/druks/extensions/loader.py +177 -0
- druks-0.0.1/backend/druks/extensions/registry.py +70 -0
- druks-0.0.1/backend/druks/extensions/settings.py +135 -0
- druks-0.0.1/backend/druks/harnesses/__init__.py +1 -0
- druks-0.0.1/backend/druks/harnesses/artifacts.py +126 -0
- druks-0.0.1/backend/druks/harnesses/base.py +562 -0
- druks-0.0.1/backend/druks/harnesses/claude.py +443 -0
- druks-0.0.1/backend/druks/harnesses/codex.py +632 -0
- druks-0.0.1/backend/druks/harnesses/datastructures.py +97 -0
- druks-0.0.1/backend/druks/harnesses/exceptions.py +62 -0
- druks-0.0.1/backend/druks/harnesses/models.py +68 -0
- druks-0.0.1/backend/druks/harnesses/registry.py +23 -0
- druks-0.0.1/backend/druks/harnesses/subprocess.py +35 -0
- druks-0.0.1/backend/druks/mcp/__init__.py +0 -0
- druks-0.0.1/backend/druks/mcp/catalog.json +9 -0
- druks-0.0.1/backend/druks/mcp/catalog.py +62 -0
- druks-0.0.1/backend/druks/mcp/constants.py +50 -0
- druks-0.0.1/backend/druks/mcp/exceptions.py +81 -0
- druks-0.0.1/backend/druks/mcp/models.py +192 -0
- druks-0.0.1/backend/druks/mcp/oauth.py +328 -0
- druks-0.0.1/backend/druks/mcp/routes.py +138 -0
- druks-0.0.1/backend/druks/mcp/schemas.py +84 -0
- druks-0.0.1/backend/druks/models.py +27 -0
- druks-0.0.1/backend/druks/notifications/__init__.py +3 -0
- druks-0.0.1/backend/druks/notifications/buttons.py +36 -0
- druks-0.0.1/backend/druks/notifications/datastructures.py +8 -0
- druks-0.0.1/backend/druks/notifications/delivery.py +73 -0
- druks-0.0.1/backend/druks/notifications/exceptions.py +57 -0
- druks-0.0.1/backend/druks/notifications/models.py +183 -0
- druks-0.0.1/backend/druks/notifications/outbox.py +71 -0
- druks-0.0.1/backend/druks/notifications/routes.py +97 -0
- druks-0.0.1/backend/druks/notifications/schemas.py +64 -0
- druks-0.0.1/backend/druks/notifications/services.py +77 -0
- druks-0.0.1/backend/druks/prompts/__init__.py +3 -0
- druks-0.0.1/backend/druks/prompts/resolver.py +98 -0
- druks-0.0.1/backend/druks/redis.py +19 -0
- druks-0.0.1/backend/druks/sandbox/__init__.py +17 -0
- druks-0.0.1/backend/druks/sandbox/client.py +229 -0
- druks-0.0.1/backend/druks/sandbox/constants.py +8 -0
- druks-0.0.1/backend/druks/sandbox/credentials.py +71 -0
- druks-0.0.1/backend/druks/sandbox/datastructures.py +235 -0
- druks-0.0.1/backend/druks/sandbox/druks-sandbox.sh +168 -0
- druks-0.0.1/backend/druks/sandbox/exceptions.py +34 -0
- druks-0.0.1/backend/druks/sandbox/gate.py +49 -0
- druks-0.0.1/backend/druks/sandbox/host.py +752 -0
- druks-0.0.1/backend/druks/sandbox/layout.py +60 -0
- druks-0.0.1/backend/druks/sandbox/repo.py +109 -0
- druks-0.0.1/backend/druks/sandbox/runner.py +329 -0
- druks-0.0.1/backend/druks/scaffolding/__init__.py +65 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/__init__.py-tpl +0 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/contracts.py-tpl +5 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/dist/index.html-tpl +12 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/extension.py-tpl +12 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/migrations/versions/.gitkeep +0 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/models.py-tpl +6 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/routes.py-tpl +10 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/schemas.py-tpl +5 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/subscribers.py-tpl +8 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/package/workflows.py-tpl +12 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/pyproject.toml-tpl +29 -0
- druks-0.0.1/backend/druks/scaffolding/extension_template/tests/test_extension.py-tpl +6 -0
- druks-0.0.1/backend/druks/schemas.py +12 -0
- druks-0.0.1/backend/druks/secrets/__init__.py +0 -0
- druks-0.0.1/backend/druks/secrets/exceptions.py +11 -0
- druks-0.0.1/backend/druks/secrets/fields.py +146 -0
- druks-0.0.1/backend/druks/secrets/utils.py +64 -0
- druks-0.0.1/backend/druks/settings.py +234 -0
- druks-0.0.1/backend/druks/setup_env.py +396 -0
- druks-0.0.1/backend/druks/signals.py +43 -0
- druks-0.0.1/backend/druks/skills/__init__.py +0 -0
- druks-0.0.1/backend/druks/skills/datastructures.py +13 -0
- druks-0.0.1/backend/druks/skills/install.py +134 -0
- druks-0.0.1/backend/druks/skills/models.py +97 -0
- druks-0.0.1/backend/druks/skills/routes.py +60 -0
- druks-0.0.1/backend/druks/skills/schemas.py +20 -0
- druks-0.0.1/backend/druks/ticketing/__init__.py +0 -0
- druks-0.0.1/backend/druks/ticketing/base.py +40 -0
- druks-0.0.1/backend/druks/ticketing/datastructures.py +41 -0
- druks-0.0.1/backend/druks/ticketing/enums.py +18 -0
- druks-0.0.1/backend/druks/ticketing/exceptions.py +8 -0
- druks-0.0.1/backend/druks/ticketing/helpers.py +34 -0
- druks-0.0.1/backend/druks/ticketing/jira.py +112 -0
- druks-0.0.1/backend/druks/ticketing/linear.py +97 -0
- druks-0.0.1/backend/druks/usage/__init__.py +0 -0
- druks-0.0.1/backend/druks/usage/extension.py +7 -0
- druks-0.0.1/backend/druks/usage/models.py +74 -0
- druks-0.0.1/backend/druks/usage/routes.py +223 -0
- druks-0.0.1/backend/druks/usage/schemas.py +89 -0
- druks-0.0.1/backend/druks/usage/workflows.py +9 -0
- druks-0.0.1/backend/druks/user_settings/__init__.py +0 -0
- druks-0.0.1/backend/druks/user_settings/datastructures.py +22 -0
- druks-0.0.1/backend/druks/user_settings/models.py +215 -0
- druks-0.0.1/backend/druks/user_settings/reads.py +117 -0
- druks-0.0.1/backend/druks/user_settings/routes.py +209 -0
- druks-0.0.1/backend/druks/user_settings/schemas.py +173 -0
- druks-0.0.1/backend/druks/webhooks/__init__.py +11 -0
- druks-0.0.1/backend/druks/webhooks/base.py +178 -0
- druks-0.0.1/backend/druks/webhooks/deliveries.py +32 -0
- druks-0.0.1/backend/druks/webhooks/exceptions.py +2 -0
- druks-0.0.1/backend/druks/webhooks/router.py +37 -0
- druks-0.0.1/backend/druks/webhooks/signatures.py +35 -0
- druks-0.0.1/backend/druks/workflows.py +721 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/_github_review.md +20 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/_header.md +94 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/_related_repos.md +21 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/_skills.md +1 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/evaluate_implementation.md +82 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/generate_plan.md +65 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/implement.md +78 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/review_code.md +73 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/review_plan.md +65 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/revise_contract.md +36 -0
- druks-0.0.1/backend/templates/prompts/build/build_workflow/triage_human_feedback.md +41 -0
- druks-0.0.1/backend/templates/prompts/build/profile/repo_profiler.md +41 -0
- druks-0.0.1/backend/templates/prompts/build/scope/scope_brief.md +168 -0
- druks-0.0.1/backend/templates/prompts/build/verification_block.md +18 -0
- druks-0.0.1/pyproject.toml +120 -0
druks-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.claude/
|
|
3
|
+
.DS_Store
|
|
4
|
+
.venv/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.idea/
|
|
8
|
+
.tmp/
|
|
9
|
+
/tmp/
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[cod]
|
|
12
|
+
/.coverage
|
|
13
|
+
/htmlcov/
|
|
14
|
+
/data/
|
|
15
|
+
/frontend/data/
|
|
16
|
+
secrets/
|
|
17
|
+
# druks/secrets is source (the encrypted-column fields), not stored secrets.
|
|
18
|
+
!backend/druks/secrets/
|
|
19
|
+
# Frontend build output — produced by `npm --prefix frontend run build`.
|
|
20
|
+
/dist/
|
|
21
|
+
# ADRs are private, kept out of the repo.
|
|
22
|
+
/docs/adr/
|
druks-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paulo Alvarado Garcia
|
|
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.
|
druks-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: druks
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Platform for durable agent orchestration.
|
|
5
|
+
Project-URL: Documentation, https://github.com/clawhaven/druks/tree/main/docs
|
|
6
|
+
Project-URL: Issues, https://github.com/clawhaven/druks/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/clawhaven/druks
|
|
8
|
+
Author: Paulo Alvarado Garcia
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,dbos,durable-execution,orchestration,workflows
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: alembic>=1.13
|
|
19
|
+
Requires-Dist: apprise>=1.9
|
|
20
|
+
Requires-Dist: asyncssh>=2.22
|
|
21
|
+
Requires-Dist: blinker>=1.9
|
|
22
|
+
Requires-Dist: croniter>=6.2.2
|
|
23
|
+
Requires-Dist: cryptography>=48.0.1
|
|
24
|
+
Requires-Dist: dbos>=2.26
|
|
25
|
+
Requires-Dist: drukbox-python-sdk>=0.0.6
|
|
26
|
+
Requires-Dist: fastapi>=0.138.0
|
|
27
|
+
Requires-Dist: githubkit[auth-app]>=0.15.5
|
|
28
|
+
Requires-Dist: httpx>=0.28.0
|
|
29
|
+
Requires-Dist: jinja2>=3.1
|
|
30
|
+
Requires-Dist: psycopg[binary]>=3.2
|
|
31
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
32
|
+
Requires-Dist: pydantic>=2.12.0
|
|
33
|
+
Requires-Dist: pyyaml>=6.0
|
|
34
|
+
Requires-Dist: redis>=8
|
|
35
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
36
|
+
Requires-Dist: uuid-utils>=0.16.0
|
|
37
|
+
Requires-Dist: uvicorn>=0.38.0
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<picture>
|
|
42
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo/web/DruksLogo_White.svg" />
|
|
43
|
+
<img src="docs/assets/logo/web/DruksLogo_Black.svg" alt="Druks" width="140" />
|
|
44
|
+
</picture>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
# Druks
|
|
48
|
+
|
|
49
|
+
> [!WARNING]
|
|
50
|
+
> Druks is under active development. Expect breaking changes and rough edges
|
|
51
|
+
> before 1.0; `main` and `latest` are edge builds, not stable releases.
|
|
52
|
+
|
|
53
|
+
Druks is a platform for **durable agent orchestration**. It gives
|
|
54
|
+
long-running agent applications a durable workflow engine, human gates,
|
|
55
|
+
sandboxed execution, events, webhooks, settings, and an operator dashboard.
|
|
56
|
+
The application supplies the domain logic as an independently packaged
|
|
57
|
+
extension.
|
|
58
|
+
|
|
59
|
+
An ordinary agent script loses its place when the process dies. A Druks
|
|
60
|
+
workflow records the result of each completed durable operation in Postgres.
|
|
61
|
+
After a restart or deploy, Druks replays the workflow and reuses those recorded
|
|
62
|
+
results instead of repeating completed work. If the process was interrupted
|
|
63
|
+
*inside* an operation, that operation may run again, so side effects still need
|
|
64
|
+
idempotency. [Durability and recovery](docs/concepts.md#durability-and-recovery)
|
|
65
|
+
explains the exact boundary.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
The installer supports three sandbox profiles backed by
|
|
70
|
+
[Drukbox](https://github.com/clawhaven/drukbox):
|
|
71
|
+
|
|
72
|
+
- `exe` (default) and `aws`: remote sandbox VMs, with Druks and Drukbox in Compose
|
|
73
|
+
- `docker`: local sandbox containers, with Drukbox running on the host
|
|
74
|
+
|
|
75
|
+
For a remote install:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/clawhaven/druks/main/scripts/install.sh)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That command follows the edge channel while Druks has no stable release. Once
|
|
82
|
+
versioned releases exist, install the script and image from the same tag as
|
|
83
|
+
described in [the release process](docs/releasing.md#install-an-immutable-version).
|
|
84
|
+
|
|
85
|
+
The first run creates `~/druks/.env`, generates secrets, and prints any values
|
|
86
|
+
still required. Re-run the same command after filling them; it pulls images,
|
|
87
|
+
runs migrations, and starts the stack. Re-running is also the upgrade path.
|
|
88
|
+
See the [deployment runbook](deploy/README.md) for prerequisites, access
|
|
89
|
+
control, verification, and rollback.
|
|
90
|
+
|
|
91
|
+
For a laptop-only stack:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
DRUKS_PROVIDER=docker bash <(curl -fsSL https://raw.githubusercontent.com/clawhaven/druks/main/scripts/install.sh)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then follow [full local setup](docs/full-local.md) to start Drukbox and connect
|
|
98
|
+
the agent harnesses. A complete installation needs GitHub Apps because the
|
|
99
|
+
bundled `build` extension is installed; a standalone extension may have
|
|
100
|
+
different integration requirements.
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
trigger ──> extension workflow ──> durable step ──> agent ──> sandbox
|
|
104
|
+
│ │ │
|
|
105
|
+
│ │ └─ Claude or Codex harness
|
|
106
|
+
│ └─ result checkpointed in Postgres
|
|
107
|
+
├─ event ──> feed / extension reaction
|
|
108
|
+
└─ gate ──> wait for human or external system ──> resume
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Platform and applications stay separate**
|
|
112
|
+
|
|
113
|
+
Druks owns the execution and operating substrate:
|
|
114
|
+
|
|
115
|
+
- DBOS workflows and queues backed by Postgres
|
|
116
|
+
- typed human gates, cancellation, schedules, and observable run state
|
|
117
|
+
- Claude and Codex harness dispatch through isolated Drukbox sandboxes
|
|
118
|
+
- append-only events, live feeds, webhooks, notifications, MCP servers, and skills
|
|
119
|
+
- validated operator settings, encrypted MCP/OAuth secrets, and the dashboard shell
|
|
120
|
+
- extension discovery, API namespaces, and independent migration histories
|
|
121
|
+
|
|
122
|
+
An **extension** owns the application: its workflows, agents, domain models,
|
|
123
|
+
routes, events, provider reactions, and optional dashboard pages. It is a normal
|
|
124
|
+
Python distribution registered through the `druks.extensions` entry-point
|
|
125
|
+
group. Installing the distribution registers it; Druks does not need an
|
|
126
|
+
extension-specific plugin list.
|
|
127
|
+
|
|
128
|
+
The bundled `build` extension is a concrete example. It coordinates coding
|
|
129
|
+
agents through tickets and GitHub pull requests, but GitHub PR orchestration is
|
|
130
|
+
`build` behavior—not the definition of Druks.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- **Evaluating Druks:** [Concepts and guarantees](docs/concepts.md)
|
|
135
|
+
- **Installing locally:** [Full local setup](docs/full-local.md)
|
|
136
|
+
- **Operating a remote stack:** [Deployment runbook](deploy/README.md)
|
|
137
|
+
- **Configuring integrations and secrets:** [Configuration](docs/configuration.md)
|
|
138
|
+
- **Building an application:** [Writing an extension](docs/writing-an-extension.md)
|
|
139
|
+
- **Diagnosing a run or service:** [Troubleshooting](docs/troubleshooting.md)
|
|
140
|
+
- **Contributing to Druks:** [Contribution guide](CONTRIBUTING.md)
|
|
141
|
+
- **Reporting a vulnerability:** [Security policy](SECURITY.md)
|
|
142
|
+
- **All documentation:** [Documentation index](docs/index.md)
|
druks-0.0.1/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo/web/DruksLogo_White.svg" />
|
|
4
|
+
<img src="docs/assets/logo/web/DruksLogo_Black.svg" alt="Druks" width="140" />
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
# Druks
|
|
9
|
+
|
|
10
|
+
> [!WARNING]
|
|
11
|
+
> Druks is under active development. Expect breaking changes and rough edges
|
|
12
|
+
> before 1.0; `main` and `latest` are edge builds, not stable releases.
|
|
13
|
+
|
|
14
|
+
Druks is a platform for **durable agent orchestration**. It gives
|
|
15
|
+
long-running agent applications a durable workflow engine, human gates,
|
|
16
|
+
sandboxed execution, events, webhooks, settings, and an operator dashboard.
|
|
17
|
+
The application supplies the domain logic as an independently packaged
|
|
18
|
+
extension.
|
|
19
|
+
|
|
20
|
+
An ordinary agent script loses its place when the process dies. A Druks
|
|
21
|
+
workflow records the result of each completed durable operation in Postgres.
|
|
22
|
+
After a restart or deploy, Druks replays the workflow and reuses those recorded
|
|
23
|
+
results instead of repeating completed work. If the process was interrupted
|
|
24
|
+
*inside* an operation, that operation may run again, so side effects still need
|
|
25
|
+
idempotency. [Durability and recovery](docs/concepts.md#durability-and-recovery)
|
|
26
|
+
explains the exact boundary.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
The installer supports three sandbox profiles backed by
|
|
31
|
+
[Drukbox](https://github.com/clawhaven/drukbox):
|
|
32
|
+
|
|
33
|
+
- `exe` (default) and `aws`: remote sandbox VMs, with Druks and Drukbox in Compose
|
|
34
|
+
- `docker`: local sandbox containers, with Drukbox running on the host
|
|
35
|
+
|
|
36
|
+
For a remote install:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/clawhaven/druks/main/scripts/install.sh)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That command follows the edge channel while Druks has no stable release. Once
|
|
43
|
+
versioned releases exist, install the script and image from the same tag as
|
|
44
|
+
described in [the release process](docs/releasing.md#install-an-immutable-version).
|
|
45
|
+
|
|
46
|
+
The first run creates `~/druks/.env`, generates secrets, and prints any values
|
|
47
|
+
still required. Re-run the same command after filling them; it pulls images,
|
|
48
|
+
runs migrations, and starts the stack. Re-running is also the upgrade path.
|
|
49
|
+
See the [deployment runbook](deploy/README.md) for prerequisites, access
|
|
50
|
+
control, verification, and rollback.
|
|
51
|
+
|
|
52
|
+
For a laptop-only stack:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
DRUKS_PROVIDER=docker bash <(curl -fsSL https://raw.githubusercontent.com/clawhaven/druks/main/scripts/install.sh)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then follow [full local setup](docs/full-local.md) to start Drukbox and connect
|
|
59
|
+
the agent harnesses. A complete installation needs GitHub Apps because the
|
|
60
|
+
bundled `build` extension is installed; a standalone extension may have
|
|
61
|
+
different integration requirements.
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
trigger ──> extension workflow ──> durable step ──> agent ──> sandbox
|
|
65
|
+
│ │ │
|
|
66
|
+
│ │ └─ Claude or Codex harness
|
|
67
|
+
│ └─ result checkpointed in Postgres
|
|
68
|
+
├─ event ──> feed / extension reaction
|
|
69
|
+
└─ gate ──> wait for human or external system ──> resume
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Platform and applications stay separate**
|
|
73
|
+
|
|
74
|
+
Druks owns the execution and operating substrate:
|
|
75
|
+
|
|
76
|
+
- DBOS workflows and queues backed by Postgres
|
|
77
|
+
- typed human gates, cancellation, schedules, and observable run state
|
|
78
|
+
- Claude and Codex harness dispatch through isolated Drukbox sandboxes
|
|
79
|
+
- append-only events, live feeds, webhooks, notifications, MCP servers, and skills
|
|
80
|
+
- validated operator settings, encrypted MCP/OAuth secrets, and the dashboard shell
|
|
81
|
+
- extension discovery, API namespaces, and independent migration histories
|
|
82
|
+
|
|
83
|
+
An **extension** owns the application: its workflows, agents, domain models,
|
|
84
|
+
routes, events, provider reactions, and optional dashboard pages. It is a normal
|
|
85
|
+
Python distribution registered through the `druks.extensions` entry-point
|
|
86
|
+
group. Installing the distribution registers it; Druks does not need an
|
|
87
|
+
extension-specific plugin list.
|
|
88
|
+
|
|
89
|
+
The bundled `build` extension is a concrete example. It coordinates coding
|
|
90
|
+
agents through tickets and GitHub pull requests, but GitHub PR orchestration is
|
|
91
|
+
`build` behavior—not the definition of Druks.
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
- **Evaluating Druks:** [Concepts and guarantees](docs/concepts.md)
|
|
96
|
+
- **Installing locally:** [Full local setup](docs/full-local.md)
|
|
97
|
+
- **Operating a remote stack:** [Deployment runbook](deploy/README.md)
|
|
98
|
+
- **Configuring integrations and secrets:** [Configuration](docs/configuration.md)
|
|
99
|
+
- **Building an application:** [Writing an extension](docs/writing-an-extension.md)
|
|
100
|
+
- **Diagnosing a run or service:** [Troubleshooting](docs/troubleshooting.md)
|
|
101
|
+
- **Contributing to Druks:** [Contribution guide](CONTRIBUTING.md)
|
|
102
|
+
- **Reporting a vulnerability:** [Security policy](SECURITY.md)
|
|
103
|
+
- **All documentation:** [Documentation index](docs/index.md)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from dbos import DBOS, StepOptions
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
|
+
|
|
10
|
+
from druks.database import db_session
|
|
11
|
+
from druks.durable.activity import set_run_phase
|
|
12
|
+
from druks.durable.engine import _step_engine, step_session
|
|
13
|
+
from druks.durable.enums import AgentCallStatus
|
|
14
|
+
from druks.durable.exceptions import WorkflowError
|
|
15
|
+
from druks.durable.models import AgentCall, Artifact
|
|
16
|
+
from druks.extensions.registry import agents
|
|
17
|
+
from druks.harnesses.registry import get_harness_for_model
|
|
18
|
+
from druks.prompts import render_prompt
|
|
19
|
+
from druks.sandbox.client import sandbox_client
|
|
20
|
+
from druks.sandbox.constants import MAX_AGENT_TIMEOUT_SECONDS
|
|
21
|
+
from druks.settings import load_settings
|
|
22
|
+
from druks.user_settings.models import SettingsOverride
|
|
23
|
+
from druks.workflows import _in_step, current_workflow
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from druks.sandbox.datastructures import AgentResult, Workspace
|
|
27
|
+
from druks.workflows import Workflow
|
|
28
|
+
|
|
29
|
+
__all__ = ["Agent", "AgentOutput"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextlib.asynccontextmanager
|
|
33
|
+
async def _runner(
|
|
34
|
+
workflow: "Workflow", host_id: str | None, workflow_id: str, step: str | None
|
|
35
|
+
) -> AsyncIterator["Workspace"]:
|
|
36
|
+
# The agent always runs in a Workspace. A warm run attaches the run's held VM; the
|
|
37
|
+
# rest get a fresh ephemeral VM. Either way workflow.get_workspace() turns the VM into
|
|
38
|
+
# the runner — fresh per call, so nothing (connection or credential) is held across steps.
|
|
39
|
+
if host_id:
|
|
40
|
+
vm = sandbox_client.attach(host_id=host_id)
|
|
41
|
+
else:
|
|
42
|
+
vm = sandbox_client.ephemeral(idempotency_key=f"{workflow_id}:{step}")
|
|
43
|
+
async with vm as box:
|
|
44
|
+
yield await workflow.get_workspace(box)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentOutput(BaseModel):
|
|
48
|
+
"""Base for an agent's structured output contract — the model an ``Agent``
|
|
49
|
+
declares as its ``contract``. The harness sends the model's schema to OpenAI's
|
|
50
|
+
strict structured-output validator on a Codex model, which requires every
|
|
51
|
+
object node to set ``additionalProperties: false`` and list every property in
|
|
52
|
+
``required``: ``extra="forbid"`` gives the former, and declaring every field
|
|
53
|
+
without a default (optionals as required-but-nullable ``X | None``) gives the
|
|
54
|
+
latter — a field with a default 400s at runtime."""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(extra="forbid")
|
|
57
|
+
|
|
58
|
+
def to_result(self) -> Any:
|
|
59
|
+
# What the caller gets back from an agent call: the validated output itself
|
|
60
|
+
# by default. Override to map the strict agent output onto a looser domain
|
|
61
|
+
# type — the seam that keeps the agent contract out of durable records,
|
|
62
|
+
# applied by the call so no caller ever invokes it.
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def get_artifact(self) -> dict[str, str]:
|
|
66
|
+
# The call's renderable output as {kind, title, content} — the platform persists
|
|
67
|
+
# it after the call. Empty unless the contract produces a reviewable document.
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Agent:
|
|
73
|
+
contract: type[AgentOutput]
|
|
74
|
+
# Operator-tunable declared default: the model the agent runs unless it is
|
|
75
|
+
# overridden (per agent or globally) in settings. A family token
|
|
76
|
+
# (codex/claude) resolves to that family's operator-tunable model.
|
|
77
|
+
model: str
|
|
78
|
+
# Display label for the settings UI; ``id`` is shown when it's None.
|
|
79
|
+
name: str | None = None
|
|
80
|
+
# Short human-friendly blurb of what the agent does, shown in the settings UI.
|
|
81
|
+
description: str = ""
|
|
82
|
+
# The prompt template ``run`` renders. None for agents that build their
|
|
83
|
+
# prompt inline and drive the harness themselves (planning) instead of
|
|
84
|
+
# going through ``run``.
|
|
85
|
+
prompt: str | None = None
|
|
86
|
+
# Operator-tunable declared defaults, overridable per agent or globally.
|
|
87
|
+
# None inherits the global default (effort; timeout in seconds).
|
|
88
|
+
effort: str | None = None
|
|
89
|
+
timeout: int | None = None
|
|
90
|
+
# ``include_plugins=False`` skips the operator's plugin state for prompts
|
|
91
|
+
# that hit no MCP server.
|
|
92
|
+
include_plugins: bool = True
|
|
93
|
+
# ``id`` is the agent's durable key (settings, timeline, registry): the attribute
|
|
94
|
+
# name it's declared as, or an explicit ``id=`` for a standalone agent (a test, a
|
|
95
|
+
# one-off). ``extension`` is the owning Extension's name, read from the class in
|
|
96
|
+
# __set_name__ to group the settings UI — blank for a standalone agent (no owner).
|
|
97
|
+
id: str = field(default="", compare=False)
|
|
98
|
+
extension: str = field(init=False, compare=False, default="")
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
if self.id: # an explicit id means a standalone agent — it registers itself now
|
|
102
|
+
agents.register(self)
|
|
103
|
+
|
|
104
|
+
def __set_name__(self, owner: type, attr: str) -> None:
|
|
105
|
+
if self.id: # explicit id: already registered in __post_init__
|
|
106
|
+
return
|
|
107
|
+
object.__setattr__(self, "id", attr)
|
|
108
|
+
object.__setattr__(self, "extension", owner.name)
|
|
109
|
+
agents.register(self)
|
|
110
|
+
|
|
111
|
+
# The effective settings, resolved through the override store: per-agent
|
|
112
|
+
# override → the agent's declared value → the operator's global default.
|
|
113
|
+
# ``run`` uses these; callers that drive the harness themselves call them
|
|
114
|
+
# directly.
|
|
115
|
+
def get_model_name(self) -> str:
|
|
116
|
+
return SettingsOverride.agent_model(self.id, self.model).value
|
|
117
|
+
|
|
118
|
+
def get_effort(self) -> str:
|
|
119
|
+
harness = get_harness_for_model(self.get_model_name()).name
|
|
120
|
+
return SettingsOverride.agent_effort(self.id, self.effort, harness).value
|
|
121
|
+
|
|
122
|
+
def get_timeout(self) -> int:
|
|
123
|
+
harness = get_harness_for_model(self.get_model_name()).name
|
|
124
|
+
resolved = SettingsOverride.agent_timeout(self.id, self.timeout, harness).value
|
|
125
|
+
# Capped so a single call always fits inside a fresh sandbox lease.
|
|
126
|
+
return min(resolved, MAX_AGENT_TIMEOUT_SECONDS)
|
|
127
|
+
|
|
128
|
+
async def __call__(self, **context: object) -> Any:
|
|
129
|
+
"""Run the agent — ``await Build.implement(...)`` — as a durable step in the
|
|
130
|
+
current workflow and return its parsed output. An agent run is always memoized —
|
|
131
|
+
this picks which step does it: its own, or the @step it's already inside.
|
|
132
|
+
workflow_id comes from the workflow context, not the caller; everything
|
|
133
|
+
else (repo, …) is prompt context."""
|
|
134
|
+
workflow = current_workflow.get(None)
|
|
135
|
+
if not workflow:
|
|
136
|
+
raise WorkflowError(
|
|
137
|
+
f"agent {self.id!r} can only run inside a workflow; standalone agent "
|
|
138
|
+
"runs aren't supported yet"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def _invoke() -> Any:
|
|
142
|
+
return await self._run(workflow_id=workflow.workflow_id, **context)
|
|
143
|
+
|
|
144
|
+
if _in_step.get():
|
|
145
|
+
return await _invoke() # the enclosing @step owns the session + memoizes it
|
|
146
|
+
|
|
147
|
+
async def _do() -> Any: # a standalone run is its own memoized step + session
|
|
148
|
+
async with step_session():
|
|
149
|
+
return await _invoke()
|
|
150
|
+
|
|
151
|
+
return await DBOS.run_step_async(StepOptions(name=f"{workflow.kind}.agent.{self.id}"), _do)
|
|
152
|
+
|
|
153
|
+
async def _run(self, *, workflow_id: str, **context: Any) -> Any:
|
|
154
|
+
"""The raw execution: provision or attach a host, record the AgentCall, run
|
|
155
|
+
the harness. ``run_agent`` handles the durable wrapping + nesting."""
|
|
156
|
+
if not self.prompt:
|
|
157
|
+
raise WorkflowError(f"agent {self.id!r} has no prompt template to render")
|
|
158
|
+
model = self.get_model_name()
|
|
159
|
+
harness = get_harness_for_model(model)
|
|
160
|
+
# A run needs a connected harness — refusing here, with the fix in the
|
|
161
|
+
# message, beats provisioning a VM and 401ing mid-run.
|
|
162
|
+
harness.get_credentials()
|
|
163
|
+
workflow = current_workflow.get()
|
|
164
|
+
# An agent call is a durability boundary — its effects don't roll back —
|
|
165
|
+
# so commit here rather than hold the step's connection idle through the
|
|
166
|
+
# minutes of provisioning and the run.
|
|
167
|
+
db_session().commit()
|
|
168
|
+
host_id = await workflow._ensure_host()
|
|
169
|
+
settings = load_settings()
|
|
170
|
+
artifact_dir = settings.artifacts_dir / f"run-{workflow_id}"
|
|
171
|
+
|
|
172
|
+
engine = _step_engine()
|
|
173
|
+
call_id = harness.mint_run_id(None)
|
|
174
|
+
await set_run_phase("provisioning_vm")
|
|
175
|
+
|
|
176
|
+
# Record the call RUNNING once it has a host to run on (its id names the
|
|
177
|
+
# on-disk transcript dir) so the live step shows while the agent works,
|
|
178
|
+
# then finish it — or fail it if the run raised after starting. A
|
|
179
|
+
# provisioning failure happens before this and records no call.
|
|
180
|
+
async with _runner(workflow, host_id, workflow_id, self.id) as runner:
|
|
181
|
+
# Templates read the live workflow + the workspace the agent runs in, alongside
|
|
182
|
+
# whatever the workflow's get_prompt_context composes.
|
|
183
|
+
prompt_context = await workflow.get_prompt_context(**context)
|
|
184
|
+
prompt_context.setdefault("workflow", workflow)
|
|
185
|
+
prompt_context.setdefault("workspace", runner)
|
|
186
|
+
prompt = await render_prompt(self.prompt, **prompt_context)
|
|
187
|
+
await set_run_phase("agent_running")
|
|
188
|
+
AgentCall.start(
|
|
189
|
+
engine,
|
|
190
|
+
call_id=call_id,
|
|
191
|
+
run_id=workflow_id,
|
|
192
|
+
model=model,
|
|
193
|
+
agent=self.id,
|
|
194
|
+
host_id=runner.host_id,
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
result = await self._execute(runner, model, prompt, artifact_dir, call_id)
|
|
198
|
+
except BaseException as error:
|
|
199
|
+
AgentCall.fail(engine, call_id=call_id, error=str(error))
|
|
200
|
+
raise
|
|
201
|
+
AgentCall.finish(engine, call_id=call_id, result=result)
|
|
202
|
+
|
|
203
|
+
if result.status is AgentCallStatus.FAILED:
|
|
204
|
+
raise WorkflowError(result.last_error or f"agent {self.id!r} failed")
|
|
205
|
+
|
|
206
|
+
output = self.contract.model_validate(result.output)
|
|
207
|
+
if spec := output.get_artifact():
|
|
208
|
+
Artifact.record(call_dir=artifact_dir / call_id, call_id=call_id, **spec)
|
|
209
|
+
return output.to_result()
|
|
210
|
+
|
|
211
|
+
async def _execute(
|
|
212
|
+
self,
|
|
213
|
+
runner: "Workspace",
|
|
214
|
+
model: str,
|
|
215
|
+
prompt: str,
|
|
216
|
+
artifact_dir: Path,
|
|
217
|
+
call_id: str,
|
|
218
|
+
) -> "AgentResult":
|
|
219
|
+
schema = self.contract.model_json_schema()
|
|
220
|
+
return await runner.run_agent(
|
|
221
|
+
model=model,
|
|
222
|
+
prompt=prompt,
|
|
223
|
+
schema=schema,
|
|
224
|
+
agent=self.id,
|
|
225
|
+
effort=self.get_effort(),
|
|
226
|
+
timeout=self.get_timeout(),
|
|
227
|
+
artifact_dir=artifact_dir,
|
|
228
|
+
call_id=call_id,
|
|
229
|
+
include_plugins=self.include_plugins,
|
|
230
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from alembic import context
|
|
2
|
+
from sqlalchemy import engine_from_config, pool
|
|
3
|
+
|
|
4
|
+
from druks.models import Base
|
|
5
|
+
from druks.settings import load_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _render_item(type_, obj, autogen_context):
|
|
9
|
+
# The _UtcDateTime decorator only changes read-side tz coercion; its DDL is
|
|
10
|
+
# plain TIMESTAMPTZ. Render it as such so migrations never import extension code.
|
|
11
|
+
if type_ == "type" and obj.__class__.__name__ == "_UtcDateTime":
|
|
12
|
+
return "sa.DateTime(timezone=True)"
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_alembic_env(target_metadata=None) -> None:
|
|
17
|
+
"""Run the platform Alembic env against the target metadata. Core and every
|
|
18
|
+
installed extension share one ``env.py``; the runner selects the scope by setting
|
|
19
|
+
``target_metadata`` and ``version_table`` in ``config.attributes``. Metadata
|
|
20
|
+
falls back to the full ``Base.metadata`` (core's own scope, e.g. core's raw
|
|
21
|
+
autogenerate). ``include_object`` keeps autogenerate within the scope: a table
|
|
22
|
+
reflected from the DB but absent from the metadata belongs to another package,
|
|
23
|
+
so it's never proposed for a drop."""
|
|
24
|
+
config = context.config
|
|
25
|
+
if target_metadata is None:
|
|
26
|
+
target_metadata = config.attributes.get("target_metadata", Base.metadata)
|
|
27
|
+
if not config.get_main_option("sqlalchemy.url"):
|
|
28
|
+
config.set_main_option("sqlalchemy.url", load_settings().database_url)
|
|
29
|
+
|
|
30
|
+
def include_object(obj, name, type_, reflected, compare_to):
|
|
31
|
+
if type_ == "table" and reflected and compare_to is None:
|
|
32
|
+
return name in target_metadata.tables
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
options = {
|
|
36
|
+
"target_metadata": target_metadata,
|
|
37
|
+
"render_item": _render_item,
|
|
38
|
+
"include_object": include_object,
|
|
39
|
+
# Each migration history tracks its head in its own version table, so an
|
|
40
|
+
# installed extension's independent history never reads (and chokes on) core's
|
|
41
|
+
# revision in the shared default ``alembic_version``.
|
|
42
|
+
"version_table": config.attributes.get("version_table", "alembic_version"),
|
|
43
|
+
}
|
|
44
|
+
if context.is_offline_mode():
|
|
45
|
+
context.configure(
|
|
46
|
+
url=config.get_main_option("sqlalchemy.url"),
|
|
47
|
+
literal_binds=True,
|
|
48
|
+
dialect_opts={"paramstyle": "named"},
|
|
49
|
+
**options,
|
|
50
|
+
)
|
|
51
|
+
with context.begin_transaction():
|
|
52
|
+
context.run_migrations()
|
|
53
|
+
return
|
|
54
|
+
connectable = engine_from_config(
|
|
55
|
+
config.get_section(config.config_ini_section, {}),
|
|
56
|
+
prefix="sqlalchemy.",
|
|
57
|
+
poolclass=pool.NullPool,
|
|
58
|
+
)
|
|
59
|
+
with connectable.connect() as connection:
|
|
60
|
+
context.configure(connection=connection, **options)
|
|
61
|
+
with context.begin_transaction():
|
|
62
|
+
context.run_migrations()
|
|
File without changes
|