01
The agent proposes. A human approves.
Duct can prepare a change to your live systems: pause a campaign, add negative keywords, mark a GA4 key event. It shows you a preview and a guardrail check. Then it stops.
There is no agent-facing approve or apply tool. Not a disabled one, not one behind a flag. It does not exist in the tool registry the model sees, so the model cannot call it, cannot be talked into calling it, and cannot be prompt-injected into calling it. The destructive gate is absolute: no configuration lets a destructive operation auto-apply. Rollback is part of every executor's contract, not an afterthought.
Higher autonomy is a posture, not a wider allowlist. It changes how often the agent interrupts you. It never changes what may apply without a click.
backend/service/execution/policy.py
02
Duct rents an agent harness. It does not marry one.
Agent frameworks are the fastest-churning, least-differentiated layer in this stack. They are 0.x, they ship weekly, and every one of them has forced a breaking migration on somebody. Building your product on top of one is renting a foundation.
So domain code here imports no agent framework. Prompts, tools, schemas, goals, scoring and the artifact contract are plain Python. Framework imports live only in runners and binders, and a test enforces the boundary with an explicit allowlist. Adding a file to that allowlist is a deliberate act, not the way you fix a red test.
There is deliberately no AgentHarness interface, and there should never be one. Take the intersection and you lose subagents, filesystem, skills, HITL granularity and compaction, which is the entire reason to rent a harness. Take the union and you are writing a framework.
backend/agents/core/ports/__init__.py
03
Write the adapter on the second implementation, not the first.
A port with one implementation is a guess. A port with two is a fact. Abstractions built before the second case exists encode whatever the first case happened to need, and then everything after fights the shape.
Every port in the agent layer has two adapters, except one, and that one says so in its own docstring and names the implementation that would earn it. Naming the gap is what keeps it from being quietly filled with a guess.
STYLE.md
04
Authorization is membership, and an API key is not a boundary.
The app's API key ships to the browser. It proves “this request came from the Duct app.” It has never proved “this caller owns that row,” and treating it as if it did is how multi-tenant data leaks.
So every route touching a project-scoped row resolves a signed-in user and checks membership. A non-member gets 404, not 403, because 403 tells them the row exists. A test walks every route in the tree and fails the build if one is ungated without a written reason.
backend/tests/test_route_auth_boundaries.py
05
Your machine, your keys, your data.
Duct runs as a desktop app that bundles the backend as a local sidecar: SQLite on disk, loopback only, provider keys in the OS keychain. No account, no server, nothing leaving the machine. Or self-host the backend. Or use the hosted version at getduct.ai, which runs the same code you can read.
Bring your own model, too. Anthropic, OpenAI, Gemini, or anything on OpenRouter, assigned per job across three tiers so reasoning-heavy work and cheap summarization do not pay the same price. A tool that reads your whole business should not also decide who your vendor is.
desktop/README.md
06
Comments carry the why. The diff already shows the what.
A comment restating the line below it is worse than no comment, because it teaches readers to skip comments. The ones worth writing carry what the code cannot: the constraint, the contract, the failure that motivated the line, the alternative that was rejected and why.
This matters more, not less, in a codebase written partly by agents. A set of individually defensible changes that together dissolve an architecture is the specific way agent-accelerated codebases fail: every change argues for itself and nothing argues for the whole. Reasoning left in the file is what lets the next person, or the next model, tell the difference.
STYLE.md
07
Agent-written contributions are welcome. Reviewed harder, not less.
Plenty of projects are quietly banning AI-assisted pull requests. Duct asks the opposite: the PR template asks where an agent helped, so a reviewer knows where to look hardest. That is not a penalty box. It is the same reason you say which part of a change you were least sure about.
Duct is an agent product built with agents. Pretending otherwise in the contribution process would be dishonest, and it would throw away the most useful signal a reviewer can get.
CONTRIBUTING.md