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.
Files changed (187) hide show
  1. druks-0.0.1/.gitignore +22 -0
  2. druks-0.0.1/LICENSE +21 -0
  3. druks-0.0.1/PKG-INFO +142 -0
  4. druks-0.0.1/README.md +103 -0
  5. druks-0.0.1/backend/druks/__init__.py +3 -0
  6. druks-0.0.1/backend/druks/agents.py +230 -0
  7. druks-0.0.1/backend/druks/alembic_support.py +62 -0
  8. druks-0.0.1/backend/druks/api/__init__.py +0 -0
  9. druks-0.0.1/backend/druks/api/app.py +250 -0
  10. druks-0.0.1/backend/druks/api/artifacts.py +21 -0
  11. druks-0.0.1/backend/druks/api/dependencies.py +18 -0
  12. druks-0.0.1/backend/druks/api/health_status.py +38 -0
  13. druks-0.0.1/backend/druks/api/routes.py +26 -0
  14. druks-0.0.1/backend/druks/api/runs.py +25 -0
  15. druks-0.0.1/backend/druks/api/schemas.py +43 -0
  16. druks-0.0.1/backend/druks/build/__init__.py +0 -0
  17. druks-0.0.1/backend/druks/build/contracts.py +307 -0
  18. druks-0.0.1/backend/druks/build/enums.py +36 -0
  19. druks-0.0.1/backend/druks/build/extension.py +233 -0
  20. druks-0.0.1/backend/druks/build/models.py +440 -0
  21. druks-0.0.1/backend/druks/build/policy.py +72 -0
  22. druks-0.0.1/backend/druks/build/routes.py +264 -0
  23. druks-0.0.1/backend/druks/build/schemas.py +191 -0
  24. druks-0.0.1/backend/druks/build/scoping/__init__.py +0 -0
  25. druks-0.0.1/backend/druks/build/scoping/contracts.py +52 -0
  26. druks-0.0.1/backend/druks/build/scoping/exceptions.py +2 -0
  27. druks-0.0.1/backend/druks/build/scoping/workflows.py +96 -0
  28. druks-0.0.1/backend/druks/build/subscribers.py +283 -0
  29. druks-0.0.1/backend/druks/build/workflows.py +624 -0
  30. druks-0.0.1/backend/druks/build/workspace.py +22 -0
  31. druks-0.0.1/backend/druks/cli.py +145 -0
  32. druks-0.0.1/backend/druks/core/__init__.py +0 -0
  33. druks-0.0.1/backend/druks/core/apis/__init__.py +0 -0
  34. druks-0.0.1/backend/druks/core/apis/exceptions.py +30 -0
  35. druks-0.0.1/backend/druks/core/apis/github.py +458 -0
  36. druks-0.0.1/backend/druks/core/apis/jira.py +100 -0
  37. druks-0.0.1/backend/druks/core/apis/linear.py +164 -0
  38. druks-0.0.1/backend/druks/core/extension.py +12 -0
  39. druks-0.0.1/backend/druks/core/models.py +11 -0
  40. druks-0.0.1/backend/druks/core/utils/__init__.py +0 -0
  41. druks-0.0.1/backend/druks/core/utils/time.py +15 -0
  42. druks-0.0.1/backend/druks/core/webhooks/__init__.py +3 -0
  43. druks-0.0.1/backend/druks/core/webhooks/github.py +96 -0
  44. druks-0.0.1/backend/druks/core/webhooks/jira.py +78 -0
  45. druks-0.0.1/backend/druks/core/webhooks/linear.py +80 -0
  46. druks-0.0.1/backend/druks/core/webhooks/slack.py +118 -0
  47. druks-0.0.1/backend/druks/core/workflows.py +54 -0
  48. druks-0.0.1/backend/druks/database.py +148 -0
  49. druks-0.0.1/backend/druks/db.py +6 -0
  50. druks-0.0.1/backend/druks/doctor.py +475 -0
  51. druks-0.0.1/backend/druks/durable/__init__.py +23 -0
  52. druks-0.0.1/backend/druks/durable/activity.py +11 -0
  53. druks-0.0.1/backend/druks/durable/dbos_state.py +78 -0
  54. druks-0.0.1/backend/druks/durable/engine.py +140 -0
  55. druks-0.0.1/backend/druks/durable/enums.py +24 -0
  56. druks-0.0.1/backend/druks/durable/exceptions.py +33 -0
  57. druks-0.0.1/backend/druks/durable/live.py +56 -0
  58. druks-0.0.1/backend/druks/durable/models.py +447 -0
  59. druks-0.0.1/backend/druks/durable/reads.py +216 -0
  60. druks-0.0.1/backend/druks/durable/schemas.py +213 -0
  61. druks-0.0.1/backend/druks/events/__init__.py +4 -0
  62. druks-0.0.1/backend/druks/events/builder.py +62 -0
  63. druks-0.0.1/backend/druks/events/feed.py +45 -0
  64. druks-0.0.1/backend/druks/events/models.py +52 -0
  65. druks-0.0.1/backend/druks/events/routes.py +82 -0
  66. druks-0.0.1/backend/druks/extensions/__init__.py +3 -0
  67. druks-0.0.1/backend/druks/extensions/base.py +462 -0
  68. druks-0.0.1/backend/druks/extensions/config.py +23 -0
  69. druks-0.0.1/backend/druks/extensions/exceptions.py +34 -0
  70. druks-0.0.1/backend/druks/extensions/fetcher.py +29 -0
  71. druks-0.0.1/backend/druks/extensions/loader.py +177 -0
  72. druks-0.0.1/backend/druks/extensions/registry.py +70 -0
  73. druks-0.0.1/backend/druks/extensions/settings.py +135 -0
  74. druks-0.0.1/backend/druks/harnesses/__init__.py +1 -0
  75. druks-0.0.1/backend/druks/harnesses/artifacts.py +126 -0
  76. druks-0.0.1/backend/druks/harnesses/base.py +562 -0
  77. druks-0.0.1/backend/druks/harnesses/claude.py +443 -0
  78. druks-0.0.1/backend/druks/harnesses/codex.py +632 -0
  79. druks-0.0.1/backend/druks/harnesses/datastructures.py +97 -0
  80. druks-0.0.1/backend/druks/harnesses/exceptions.py +62 -0
  81. druks-0.0.1/backend/druks/harnesses/models.py +68 -0
  82. druks-0.0.1/backend/druks/harnesses/registry.py +23 -0
  83. druks-0.0.1/backend/druks/harnesses/subprocess.py +35 -0
  84. druks-0.0.1/backend/druks/mcp/__init__.py +0 -0
  85. druks-0.0.1/backend/druks/mcp/catalog.json +9 -0
  86. druks-0.0.1/backend/druks/mcp/catalog.py +62 -0
  87. druks-0.0.1/backend/druks/mcp/constants.py +50 -0
  88. druks-0.0.1/backend/druks/mcp/exceptions.py +81 -0
  89. druks-0.0.1/backend/druks/mcp/models.py +192 -0
  90. druks-0.0.1/backend/druks/mcp/oauth.py +328 -0
  91. druks-0.0.1/backend/druks/mcp/routes.py +138 -0
  92. druks-0.0.1/backend/druks/mcp/schemas.py +84 -0
  93. druks-0.0.1/backend/druks/models.py +27 -0
  94. druks-0.0.1/backend/druks/notifications/__init__.py +3 -0
  95. druks-0.0.1/backend/druks/notifications/buttons.py +36 -0
  96. druks-0.0.1/backend/druks/notifications/datastructures.py +8 -0
  97. druks-0.0.1/backend/druks/notifications/delivery.py +73 -0
  98. druks-0.0.1/backend/druks/notifications/exceptions.py +57 -0
  99. druks-0.0.1/backend/druks/notifications/models.py +183 -0
  100. druks-0.0.1/backend/druks/notifications/outbox.py +71 -0
  101. druks-0.0.1/backend/druks/notifications/routes.py +97 -0
  102. druks-0.0.1/backend/druks/notifications/schemas.py +64 -0
  103. druks-0.0.1/backend/druks/notifications/services.py +77 -0
  104. druks-0.0.1/backend/druks/prompts/__init__.py +3 -0
  105. druks-0.0.1/backend/druks/prompts/resolver.py +98 -0
  106. druks-0.0.1/backend/druks/redis.py +19 -0
  107. druks-0.0.1/backend/druks/sandbox/__init__.py +17 -0
  108. druks-0.0.1/backend/druks/sandbox/client.py +229 -0
  109. druks-0.0.1/backend/druks/sandbox/constants.py +8 -0
  110. druks-0.0.1/backend/druks/sandbox/credentials.py +71 -0
  111. druks-0.0.1/backend/druks/sandbox/datastructures.py +235 -0
  112. druks-0.0.1/backend/druks/sandbox/druks-sandbox.sh +168 -0
  113. druks-0.0.1/backend/druks/sandbox/exceptions.py +34 -0
  114. druks-0.0.1/backend/druks/sandbox/gate.py +49 -0
  115. druks-0.0.1/backend/druks/sandbox/host.py +752 -0
  116. druks-0.0.1/backend/druks/sandbox/layout.py +60 -0
  117. druks-0.0.1/backend/druks/sandbox/repo.py +109 -0
  118. druks-0.0.1/backend/druks/sandbox/runner.py +329 -0
  119. druks-0.0.1/backend/druks/scaffolding/__init__.py +65 -0
  120. druks-0.0.1/backend/druks/scaffolding/extension_template/package/__init__.py-tpl +0 -0
  121. druks-0.0.1/backend/druks/scaffolding/extension_template/package/contracts.py-tpl +5 -0
  122. druks-0.0.1/backend/druks/scaffolding/extension_template/package/dist/index.html-tpl +12 -0
  123. druks-0.0.1/backend/druks/scaffolding/extension_template/package/extension.py-tpl +12 -0
  124. druks-0.0.1/backend/druks/scaffolding/extension_template/package/migrations/versions/.gitkeep +0 -0
  125. druks-0.0.1/backend/druks/scaffolding/extension_template/package/models.py-tpl +6 -0
  126. druks-0.0.1/backend/druks/scaffolding/extension_template/package/routes.py-tpl +10 -0
  127. druks-0.0.1/backend/druks/scaffolding/extension_template/package/schemas.py-tpl +5 -0
  128. druks-0.0.1/backend/druks/scaffolding/extension_template/package/subscribers.py-tpl +8 -0
  129. druks-0.0.1/backend/druks/scaffolding/extension_template/package/workflows.py-tpl +12 -0
  130. druks-0.0.1/backend/druks/scaffolding/extension_template/pyproject.toml-tpl +29 -0
  131. druks-0.0.1/backend/druks/scaffolding/extension_template/tests/test_extension.py-tpl +6 -0
  132. druks-0.0.1/backend/druks/schemas.py +12 -0
  133. druks-0.0.1/backend/druks/secrets/__init__.py +0 -0
  134. druks-0.0.1/backend/druks/secrets/exceptions.py +11 -0
  135. druks-0.0.1/backend/druks/secrets/fields.py +146 -0
  136. druks-0.0.1/backend/druks/secrets/utils.py +64 -0
  137. druks-0.0.1/backend/druks/settings.py +234 -0
  138. druks-0.0.1/backend/druks/setup_env.py +396 -0
  139. druks-0.0.1/backend/druks/signals.py +43 -0
  140. druks-0.0.1/backend/druks/skills/__init__.py +0 -0
  141. druks-0.0.1/backend/druks/skills/datastructures.py +13 -0
  142. druks-0.0.1/backend/druks/skills/install.py +134 -0
  143. druks-0.0.1/backend/druks/skills/models.py +97 -0
  144. druks-0.0.1/backend/druks/skills/routes.py +60 -0
  145. druks-0.0.1/backend/druks/skills/schemas.py +20 -0
  146. druks-0.0.1/backend/druks/ticketing/__init__.py +0 -0
  147. druks-0.0.1/backend/druks/ticketing/base.py +40 -0
  148. druks-0.0.1/backend/druks/ticketing/datastructures.py +41 -0
  149. druks-0.0.1/backend/druks/ticketing/enums.py +18 -0
  150. druks-0.0.1/backend/druks/ticketing/exceptions.py +8 -0
  151. druks-0.0.1/backend/druks/ticketing/helpers.py +34 -0
  152. druks-0.0.1/backend/druks/ticketing/jira.py +112 -0
  153. druks-0.0.1/backend/druks/ticketing/linear.py +97 -0
  154. druks-0.0.1/backend/druks/usage/__init__.py +0 -0
  155. druks-0.0.1/backend/druks/usage/extension.py +7 -0
  156. druks-0.0.1/backend/druks/usage/models.py +74 -0
  157. druks-0.0.1/backend/druks/usage/routes.py +223 -0
  158. druks-0.0.1/backend/druks/usage/schemas.py +89 -0
  159. druks-0.0.1/backend/druks/usage/workflows.py +9 -0
  160. druks-0.0.1/backend/druks/user_settings/__init__.py +0 -0
  161. druks-0.0.1/backend/druks/user_settings/datastructures.py +22 -0
  162. druks-0.0.1/backend/druks/user_settings/models.py +215 -0
  163. druks-0.0.1/backend/druks/user_settings/reads.py +117 -0
  164. druks-0.0.1/backend/druks/user_settings/routes.py +209 -0
  165. druks-0.0.1/backend/druks/user_settings/schemas.py +173 -0
  166. druks-0.0.1/backend/druks/webhooks/__init__.py +11 -0
  167. druks-0.0.1/backend/druks/webhooks/base.py +178 -0
  168. druks-0.0.1/backend/druks/webhooks/deliveries.py +32 -0
  169. druks-0.0.1/backend/druks/webhooks/exceptions.py +2 -0
  170. druks-0.0.1/backend/druks/webhooks/router.py +37 -0
  171. druks-0.0.1/backend/druks/webhooks/signatures.py +35 -0
  172. druks-0.0.1/backend/druks/workflows.py +721 -0
  173. druks-0.0.1/backend/templates/prompts/build/build_workflow/_github_review.md +20 -0
  174. druks-0.0.1/backend/templates/prompts/build/build_workflow/_header.md +94 -0
  175. druks-0.0.1/backend/templates/prompts/build/build_workflow/_related_repos.md +21 -0
  176. druks-0.0.1/backend/templates/prompts/build/build_workflow/_skills.md +1 -0
  177. druks-0.0.1/backend/templates/prompts/build/build_workflow/evaluate_implementation.md +82 -0
  178. druks-0.0.1/backend/templates/prompts/build/build_workflow/generate_plan.md +65 -0
  179. druks-0.0.1/backend/templates/prompts/build/build_workflow/implement.md +78 -0
  180. druks-0.0.1/backend/templates/prompts/build/build_workflow/review_code.md +73 -0
  181. druks-0.0.1/backend/templates/prompts/build/build_workflow/review_plan.md +65 -0
  182. druks-0.0.1/backend/templates/prompts/build/build_workflow/revise_contract.md +36 -0
  183. druks-0.0.1/backend/templates/prompts/build/build_workflow/triage_human_feedback.md +41 -0
  184. druks-0.0.1/backend/templates/prompts/build/profile/repo_profiler.md +41 -0
  185. druks-0.0.1/backend/templates/prompts/build/scope/scope_brief.md +168 -0
  186. druks-0.0.1/backend/templates/prompts/build/verification_block.md +18 -0
  187. 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,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -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