Skip to content

Governed Agent Ledger POC — Drupal + Dolt + Beads

Status: POC playbook

Purpose: prove that Drupal can govern agent work stored in the existing Beads/Dolt ledger without creating a parallel work schema, a custom Drupal module, or a new orchestration platform.

This playbook defines the proof. Beads track the work required to execute it.


1. POC claim

The proof is intentionally small:

Agent writes work on a Dolt task branch
        ↓
Drupal surfaces the existing Bead record and branch diff
        ↓
Human reviewer approves or rejects
        ↓
Drupal emits a governed approval event
        ↓
Agent runtime performs the Dolt merge
        ↓
Drupal reflects the merged state

The second proof introduces a real conflict:

Agent A edits bead 1234 on branch A
Agent B edits bead 1234 on branch B
        ↓
merge A succeeds
merge B conflicts
        ↓
Drupal renders the conflict as a review decision

The POC is successful only if the same Beads records remain usable by agents operating inside and outside Drupal.


2. Authority boundary

Keep the systems separate.

Drupal owns governance

Drupal owns:

  • human and agent identity exposed to the product surface
  • permissions
  • reviewer authority
  • approval/rejection state exposed to humans
  • review UX
  • evidence visible to the reviewer
  • product workflow

Drupal does not own the engineering work ledger and does not directly perform Dolt merges.

Beads/Dolt owns durable work state and versioned substrate

Beads/Dolt owns:

  • the existing agent work records
  • task branches
  • diffs
  • merge history
  • conflicts
  • durable work state used by Gas City and agents outside Drupal

Do not create a replacement Drupal work schema.

Gas City / runtime owns execution

The runtime owns:

  • creating task branches
  • agent execution
  • writing Beads
  • validating approval events
  • executing the merge
  • handling merge failure/conflict
  • returning execution result

Use native Gas City primitives before Bluefly code: Beads, Orders, Formulas, hooks, Polecats, packs, external clients, MCP, and JSON command contracts where applicable.

GitLab remains source/CI authority

Any code or configuration change required to build the POC follows the normal GitLab path and shared CI authority. Do not use this POC as a reason to add project-local CI.


3. Negative-ownership gate

For every implementation decision use:

DELETE
→ REUSE EXISTING
→ GAS CITY NATIVE
→ DRUPAL CORE / CONTRIB
→ OTHER UPSTREAM
→ CONFIGURE
→ PATCH / OVERLAY
→ THIN ADAPTER
→ CUSTOM CODE only on proven gap

Target outcome:

CUSTOM_DRUPAL_MODULES=0
CUSTOM_PHP=0
CUSTOM_WORK_LEDGER=0
CUSTOM_AGENT_ORCHESTRATOR=0
CUSTOM_MODEL_ROUTER=0
CUSTOM_EVENT_PLATFORM=0

If one of these becomes non-zero, record the exact capability gap that forced it. An honest thin adapter is preferable to hiding custom ownership.


4. Proposed stack

Use the existing Beads schema as-is.

Required proof candidates

  • Drupal 11 core multiple database connections
  • Dolt using the MySQL wire protocol
  • existing Beads database/schema
  • External Entities v3
  • xnttsql SQL storage plugin
  • dbxschema and appropriate database driver support
  • ECA
  • Entity Webhook + Broadcast, only if Step 0 proves it fires for external entities
  • Key for webhook signing material if Entity Webhook is used

Conditional / fallback pieces

  • Http Client Manager only if Entity Webhook cannot handle the external-entity approval event; pin a version known to contain the relevant security fix before use
  • Views Database Connector only for a read-only simplification where entity permissions/events are not needed
  • ai_agents and Tool API only for the optional “agent inside Drupal” demonstration
  • Gas City external messaging (extmsg), MCP, or existing Tool API integration should be evaluated before a custom Drupal↔Gas City bridge

Do not install every candidate up front. Add only what the proof currently requires.


5. Decisions locked for the POC

Branch granularity: per task

Use one Dolt branch per task.

Why:

  • the branch has a natural beginning and end
  • approval corresponds to one bounded unit of work
  • a task branch can be merged, rejected, retried, or deleted
  • per-agent branches live too long
  • per-session branches create unnecessary merge noise

Naming should map cleanly to the Bead/task identifier.

Drupal reads committed/main ledger state, not active working branches

Drupal's normal ledger view reads the authoritative main branch.

Review views read the diff between a task branch and main.

Do not point normal Drupal product views at an agent's in-progress branch.

The agent resolves conflicts

Drupal decides approve/reject and displays conflict evidence.

The agent/runtime resolves the conflict after rejection or conflict detection.

Do not build a general conflict-resolution editor in Drupal for the POC.

Beads schema remains authoritative

Do not create a new “agent memory” or “agent task” schema in Drupal.

The entire point is to compose on the existing work ledger.


6. Step 0 — gating experiment

Do this before Dolt integration or UI work.

Question

Does Entity Webhook Broadcast fire when an External Entity is updated?

This is currently an assumption, not a proven fact in this playbook.

Minimal experiment

  1. Use a disposable Drupal 11 site.
  2. Configure any second SQL database/table. Dolt is not required yet.
  3. Install only the minimum External Entities/database storage pieces and Entity Webhook Broadcast required for the test.
  4. Surface one row as an external entity.
  5. Configure an outbound test endpoint.
  6. Edit the row through Drupal.
  7. Observe whether the POST fires.
  8. Verify payload, signature behavior if enabled, retry/log behavior, and whether the event can be conditioned on the changed approval/status value.

Gate

EXTERNAL_ENTITY_UPDATE=PASS
ENTITY_WEBHOOK_FIRED=YES
DELIVERY_LOG_CREATED=YES
CONDITION_FILTER_WORKS=YES

If all required behavior passes: use Entity Webhook.

If it does not: test the smallest ECA + outbound HTTP fallback. Do not create custom PHP until the contrib fallback is actually proven insufficient.

Nothing else in the POC should depend on this assumption before Step 0 closes.


7. Step 1 — prove Dolt mechanics independently

Before Drupal is involved, prove the version-control behavior at the SQL/runtime level against the existing governed Beads/Dolt topology.

Do not start another Dolt server merely for convenience if the authoritative environment already provides the required logical database.

Prove:

BEADS_DATABASE_REACHABLE=YES
TASK_BRANCH_CREATED=YES
BEAD_WRITTEN_ON_TASK_BRANCH=YES
MAIN_UNCHANGED_BEFORE_MERGE=YES
DIFF_QUERY_WORKS=YES
MERGE_WORKS=YES
MAIN_CONTAINS_CHANGE_AFTER_MERGE=YES

Also prove the negative path:

TWO_BRANCHES_EDIT_SAME_RECORD=YES
FIRST_MERGE=PASS
SECOND_MERGE=CONFLICT
CONFLICT_DATA_QUERYABLE=YES

Use native Dolt branch/diff/merge mechanisms. Capture exact commands and queries as execution evidence in the POC Bead, not as an alternate hand-maintained runbook.


8. Step 2 — connect Drupal to the ledger read surface

Use Drupal's second database target to reach the Dolt/Beads database through its MySQL-compatible endpoint.

Rules:

  • credentials come from the approved environment/1Password path; never literals in tracked settings
  • Drupal receives only the database privileges needed for the POC
  • the default read connection points at the authoritative/main ledger state
  • do not give Drupal a general-purpose merge credential

Proof:

DRUPAL_SECOND_CONNECTION=PASS
READ_FROM_BEADS_MAIN=PASS
NO_DRUPAL_MERGE_CAPABILITY=YES
CREDENTIAL_LITERAL_IN_REPO=NO

Treat branch-addressing through connection configuration as a hypothesis to verify against the deployed Dolt version/topology rather than assuming it from this document.


9. Step 3 — surface Beads without inventing a schema

Configure an External Entity type representing the existing Bead record.

Start read-only.

Map only the fields required for the demo:

  • Bead/task ID
  • title/summary
  • status
  • branch/task reference if present or derivable through existing data
  • timestamps / actor fields required for evidence

Only after read behavior is proven, allow the smallest update required for governance. Do not give Drupal generic write access to Bead content.

Preferred POC boundary:

Agents/runtime own work content.
Drupal reviewer owns approval decision.

If the existing Beads schema does not contain the exact approval/status representation needed, do not alter the ledger casually. First determine whether approval is better represented as existing Beads state, a linked governance record, or an event outside the work-row mutation. The POC must not corrupt native Beads semantics merely to make Drupal convenient.

Proof:

BEAD_ENTITY_LIST=PASS
BEAD_ENTITY_DETAIL=PASS
BEADS_SCHEMA_REUSED=YES
NEW_LEDGER_SCHEMA=NO

10. Step 4 — render the review diff

Create the smallest review surface that can answer:

What did this agent/task branch change compared with main?

Use Dolt's native diff/query surface and expose the structured result through Drupal.

The review screen should show, at minimum:

  • task / Bead ID
  • task branch
  • changed record/field
  • before value
  • proposed value
  • change type
  • actor/task evidence
  • approve
  • reject with reason

Do not build a generalized diff framework. One Beads table and one useful review screen are sufficient for the POC.

Proof:

TASK_BRANCH_DIFF_QUERY=PASS
DRUPAL_DIFF_VIEW=PASS
BEFORE_VALUE_VISIBLE=YES
AFTER_VALUE_VISIBLE=YES

11. Step 5 — governance and approval semantics

Do not claim Drupal Content Moderation is being used unless the entity type actually supports the required revision/moderation semantics. For this POC, assume it does not until proven otherwise.

Implement approval with existing Drupal permissions plus configuration/ECA where possible.

Minimum roles:

agent / machine principal
reviewer
administrator

Minimum properties:

  • unprivileged actor cannot approve
  • reviewer can approve/reject
  • identity of reviewer is recorded
  • rejection includes a reason visible to the runtime/agent
  • approval does not directly execute SQL merge from Drupal

Proof:

UNPRIVILEGED_APPROVAL=DENIED
REVIEWER_APPROVAL=PASS
APPROVER_ID_RECORDED=YES
REJECTION_REASON_SUPPORTED=YES
DRUPAL_DIRECT_MERGE=NO

Machine identities must be real least-privilege principals. Do not borrow Thomas's identity and do not invent secret references.


12. Step 6 — emit approval as an event

Preferred path, if Step 0 passes:

Drupal approval
   ↓
Entity Webhook Broadcast
   ↓
signed payload
   ↓
existing runtime / Gas City-facing receiver

Payload should contain identifiers, not broad authority:

bead_id
task_branch
approval_decision
approver_id
timestamp
correlation_id

The runtime must independently verify that the request is valid and authorized before merge.

If Entity Webhook fails the Step 0 gate, use the smallest proven ECA + HTTP path.

Do not create a new event bus.

Where Gas City can consume the event through an existing native mechanism (Order, hook, external client, existing API, MCP/tool boundary), use that before a Bluefly receiver service.

Proof:

APPROVAL_EVENT_FIRED=YES
SIGNED_OR_EQUIVALENT_AUTHENTICATED=YES
DELIVERY_EVIDENCE=YES
CORRELATION_TO_BEAD=YES
CUSTOM_EVENT_PLATFORM=NO

13. Step 7 — runtime merge

Drupal never performs DOLT_MERGE directly.

The runtime receives the approved task identifier, validates it, resolves the task branch, and performs the native merge through the authorized runtime identity.

Target flow:

approval event
   ↓
authorization validation
   ↓
Gas City Order / governed runtime action
   ↓
Dolt merge
   ↓
result recorded in Beads / runtime evidence
   ↓
Drupal reflects resulting main state

On conflict:

  • do not force merge
  • classify the task as requiring resolution
  • capture conflict evidence
  • return rejection/conflict state to the review surface
  • route a new/child Bead only if native Beads semantics require additional work tracking
  • let the agent resolve and resubmit

Proof:

HMAC_OR_EVENT_AUTH_VERIFIED=YES
AUTHORIZED_RUNTIME_IDENTITY=YES
MERGE_EXECUTED_BY_RUNTIME=YES
MAIN_UPDATED=YES
DRUPAL_REFRESH_SHOWS_RESULT=YES

14. Step 8 — two-agent conflict demo

This is the most important demonstration after the happy path.

  1. Start from the same main state.
  2. Create task branch A and task branch B.
  3. Have two disposable agents/Polecats change the same Bead field differently.
  4. Present both as independent reviewable work.
  5. Approve A and merge it.
  6. Approve or attempt B.
  7. Observe native Dolt conflict rather than silently overwriting A.
  8. Surface both conflicting values and the conflict state in Drupal.
  9. Reject/return B to the agent for resolution.
  10. Agent resolves on its branch and resubmits.

Success is not “Drupal automatically resolves the conflict.”

Success is:

a concurrent agent write becomes explicit governed work instead of a silent race condition.

Proof:

TWO_AGENT_BRANCHES=YES
SAME_RECORD_CHANGED=YES
FIRST_MERGE=PASS
SECOND_MERGE=CONFLICT
SILENT_OVERWRITE=NO
CONFLICT_VISIBLE_IN_DRUPAL=YES
HUMAN_DECISION_POSSIBLE=YES
AGENT_CAN_RESUBMIT=YES

15. Step 9 — optional inside-Drupal agent path

Do this only after the outside-agent path is complete.

Outside-agent path is the canonical first proof:

Gas City/Polecat/agent
→ Dolt task branch
→ Beads
→ Drupal review
→ approval event
→ runtime merge

Then, if useful for the Drupal demonstration, prove a Drupal-hosted agent using existing ai_agents + Tool API capabilities.

Do not let this optional path change the ledger architecture. The same Beads records and task-branch semantics must remain valid for both execution locations.


16. Gas City native-first integration

Before creating any adapter, check native Gas City capabilities.

The POC should prefer:

  • existing agent definitions and harness configuration
  • existing upstream/provider abstraction for model choice
  • Polecats for disposable work
  • Orders for when/where work fires
  • Formulas only where a reusable multi-step method is actually proven
  • Beads for durable work
  • hooks/events instead of polling
  • packs/patches instead of copied agent definitions
  • gc ... --json / JSON-schema contracts for machine consumption rather than parsing human terminal output
  • external messaging, MCP, or existing Tool API endpoints before a custom Drupal↔Gas City bridge

Do not build the factory beside Gas City. Run this real POC through it.


17. Bead decomposition for the POC

Create/reconcile these as the minimum seed graph. Existing matching Beads win; do not duplicate them.

EPIC: GOVERNED_AGENT_LEDGER_POC
│
├── 0. VERIFY_EXTERNAL_ENTITY_WEBHOOK_GATE
│
├── 1. PROVE_DOLT_BEADS_BRANCH_DIFF_MERGE
│
├── 2. CONNECT_DRUPAL_TO_BEADS_READ_SURFACE
│
├── 3. SURFACE_BEADS_AS_EXTERNAL_ENTITIES
│
├── 4. BUILD_STRUCTURED_DIFF_REVIEW_VIEW
│
├── 5. PROVE_REVIEWER_PERMISSION_AND_DECISION
│
├── 6. EMIT_GOVERNED_APPROVAL_EVENT
│
├── 7. MERGE_THROUGH_AUTHORIZED_RUNTIME
│
├── 8. PROVE_TWO_AGENT_CONFLICT_FLOW
│
└── 9. OPTIONAL_DRUPAL_HOSTED_AGENT_PATH

Do not fully pre-plan every implementation task. Let each proof bead create linked child work only when execution discovers a real gap.


18. Execution order and stop conditions

Gate A — webhook/event feasibility

Do not proceed to full UI/configuration until Step 0 answers the external-entity event question.

Gate B — native Dolt mechanics

Do not involve Drupal until task branch, diff, merge, and conflict are proven directly.

Gate C — read model

Do not add approval mutation until Drupal can reliably render the existing Beads records and task diff.

Gate D — governance

Do not connect approval to merge until permission boundaries and actor identity are proven.

Gate E — happy path

Do not build the conflict demonstration until one complete approve→event→runtime merge round trip passes.

Gate F — ownership review

At the end, evaluate every added dependency/configuration/adapter:

KEEP
REPLACE_WITH_EXISTING
UPSTREAM
DELETE

19. Required end-to-end receipt

Do not claim the POC works until a fresh run proves:

AUTHORITATIVE_BEADS_LEDGER_REUSED=YES
NEW_WORK_SCHEMA=NO

TASK_BRANCH_CREATED=YES
AGENT_WROTE_BEAD=YES
MAIN_UNCHANGED_PRE_APPROVAL=YES

DRUPAL_SECOND_DB_CONNECTION=PASS
DRUPAL_LISTS_BEADS=YES
DRUPAL_DIFF_VIEW=PASS

UNPRIVILEGED_USER_CANNOT_APPROVE=YES
REVIEWER_APPROVED=YES
APPROVER_ID_RECORDED=YES

APPROVAL_EVENT_FIRED=YES
EVENT_AUTH_VERIFIED=YES
DELIVERY_EVIDENCE=YES

RUNTIME_PERFORMED_MERGE=YES
DRUPAL_PERFORMED_MERGE=NO
MAIN_CONTAINS_APPROVED_CHANGE=YES
DRUPAL_SHOWS_MERGED_STATE=YES

TWO_AGENT_CONFLICT_CREATED=YES
SECOND_MERGE_CONFLICTED=YES
SILENT_OVERWRITE=NO
CONFLICT_VISIBLE_TO_REVIEWER=YES
AGENT_RESOLUTION_AND_RESUBMIT=PASS

CUSTOM_DRUPAL_MODULES_WRITTEN=0
CUSTOM_PHP_WRITTEN=0
CUSTOM_LEDGER_SCHEMA=0
CUSTOM_EVENT_PLATFORM=0
CUSTOM_AGENT_ORCHESTRATOR=0

THOMAS_USED_AS_ROUTER=NO
THOMAS_CREDENTIAL_BORROWED=NO
WITNESS_VERIFICATION=PASS

If a zero becomes one, name the precise gap and decide whether the adapter is worth owning.


20. POC demo script

Demo 1 — approval

  1. Show main Bead state in Drupal.
  2. Launch/identify a real task branch.
  3. Agent changes one Bead on that branch.
  4. Refresh Drupal review queue.
  5. Show before/after values.
  6. Human clicks Approve.
  7. Show delivery/event evidence.
  8. Show runtime merge.
  9. Refresh Drupal main view and show the approved value.

Demo 2 — conflict

  1. Reset to known state.
  2. Agent A and Agent B edit the same Bead on separate task branches.
  3. Approve/merge A.
  4. Attempt B.
  5. Show native conflict.
  6. Show both values in Drupal.
  7. Reject/return B with reason.
  8. Agent resolves and resubmits.

The story is not “Drupal stores agent memory.”

The story is:

Drupal governs shared agent work without owning the ledger, and Dolt turns concurrency into a reviewable merge decision instead of a silent race condition.


21. Source design notes and unverified assumptions

This playbook incorporates the design from the Google Doc 09 BUILD SPEC — governed agent ledger, contrib only and the Bluefly native-first Gas City operating model.

Important assumptions that must be proven rather than repeated as facts:

  • Entity Webhook Broadcast behavior on External Entities
  • exact Drupal 11 compatibility/current release status of every selected contrib project
  • exact xnttsql write semantics required by the approval design
  • exact Dolt branch-addressing syntax in the deployed version/topology
  • whether approval should mutate an existing Beads status field or be represented through another native governance/event mechanism
  • which native Gas City event/API primitive is the thinnest receiver for the approval event

These are POC gates, not reasons to invent custom infrastructure in advance.


22. Completion definition

The POC is complete when one real agent task can be created on a Dolt task branch, reviewed through Drupal, approved by an authorized human, merged by the governed runtime, reflected back in Drupal, and then repeated with a deliberate two-agent conflict that becomes visible reviewable state rather than silent corruption.

The strongest completion state is:

REAL_BEADS_REUSED=YES
DRUPAL_GOVERNS=YES
DOLT_VERSIONS_AND_MERGES=YES
GAS_CITY_EXECUTES=YES
HUMAN_APPROVAL_VISIBLE=YES
CONFLICT_BECOMES_DECISION=YES
CUSTOM_MODULES=0

If those are true, stop. Do not generalize the POC into a platform until a second real use proves what deserves reuse.