Skip to main content

FASE 28 — the user's folder

This document is the phase's scope: what it pursues, what it refuses, and in what order. The decisions live in ADR 0146 and ADR 0147 — whatever is here and not there is intention, not decision.

The problem​

The product has had three execution modes since ADR 0104, and none of them delivers the simplest thing a user expects: open the editor and see the work happening in a folder of their own.

modeuser's folderwhat it costs
containerdoes not existthe code lives in a volume they never open
mountedyes, under BRABO_PROJECTS_BASEinvisible in practice: without the variable the mode isn't even offered, and nothing in the product asks for it to be configured
runneryes, real folderbinary, manual chmod +x, pairing, and Docker as a hard prerequisite (RN-507/508)

mounted is the most instructive case. ADR 0141 built the single base and ADR 0142 made it possible to suggest a path that doesn't exist yet — but neither of the two things reached the screen. GET /workspaces/:workspaceId/projects-base exists, is consumed by nobody in the web, and so the mode that was already ready remains out of reach for whoever installs the product.

The design, in three layers​

Each layer with a single mechanism, and none inventing new authority:

layerwheremechanism
consentpnpm bootstrap, once, on the hostpicks the base, writes BRABO_PROJECTS_BASE, proves Docker Desktop file sharing
live folderinside the basebind by identity (ADR 0141) — zero synchronization
destination outside the baseoptional, per projectthe local agent's espelho capability: one direction, never deletes

The third layer exists for a physical reason: a bind mount does not cross the network nor reach a path outside what was mounted. To write to an arbitrary folder on the host there are only three ways out — run api/engine natively, have a live process on the host, or have the browser write through the File System Access API. The second is the only continuous one, and brabo-runner already is that process.

Configuration consent ≠ action approval​

The point both ADRs need to declare, and which decides the mirror's design:

The mirror's write does not go through proposed_action, because it isn't an agent asking to act — it is the system writing to a path the user themselves declared when creating the project. Syncing through a terminal command would be the opposite: it would fall under the path scope of ADR 0055, become a queue of routine approvals, and erode precisely the ceiling that gives the click its meaning.

Numbering — counted, not inherited​

The repository's prose counts are in known drift. CONTRIBUTING.md says "68 ADRs"; the frontmatter of docs/adr/index.md says "108". No session of this phase trusts a number written in prose.

The method, which every session of this phase repeats before reserving any number:

# ADRs — arquivos, não prosa
ls docs/adr/ | grep -cE '^[0-9]{4}-.*\.md$'

# RNs — cabeçalhos, nos três arquivos que compartilham a numeração
git grep -h -E '^### RN-' -- 'docs/business-rules.md' 'docs/business-rules/*.md' \
| grep -oE 'RN-[0-9]+' | sort -u | wc -l

Measured at the start of session 1, against dev: 144 ADRs (highest 0145, with a gap at 0062) and 371 RNs (highest 509, zero duplicate anchors). Hence 0146 and 0147.

The range reserved for the phase is RN-511 to RN-520, and the jump over RN-510 is deliberate. Counting only dev would return 510 — but RN-510 was already allocated in a work branch that hadn't merged yet. Reserving it here would recreate exactly the collision that happened with RN-505, when two PRs allocated the same number two minutes apart and the {#rn-505} anchor stayed duplicated for weeks.

It's worth recording why that collision survived so long: docs:build doesn't catch a duplicate anchor. onBrokenAnchors: 'throw' fails a link to an anchor that doesn't exist; with two identical anchors Docusaurus resolves the first and the second becomes unreachable, silently. Counting only what is in dev is a necessary condition and not a sufficient one — you also have to count what is in flight.

The sessions​

One deliverable each, in order. The full prompt for each session is written at the end of the previous one, with whatever it discovered.

#deliverabledepends on
1this document + ADR 0146 + ADR 0147 + reserved RN range—
2consented base in bootstrap.sh/preflight.mjs, .env.example, file-sharing proof1
3wizard offers mounted by default when the base exists2
4broker in the local compose per the verdict of ADR 01462
5capabilities declared in the local agent's protocol (no mirror yet)1
6espelho capability: own scope, one direction, never deletes5
7install as a user service, stop/remove, server-side revocation6
8mirror state on the web (last sync, count, error)6
9E2E, docs:check, docmap and CHANGELOG closed8

What the phase delivered, and where it deviated from the plan​

The table above is the plan. What follows is the record of what happened — the differences are here because a plan that rewrites itself to match the result stops teaching anything.

#RN/ADRhow it came out
1ADR 0146, ADR 0147as planned; range RN-511..520 reserved, with the jump over RN-510 (already allocated in an unmerged branch)
2ADR 0146 points 1–2as planned — default base $HOME/projetos-brabo, written by a Node script because a bootstrap.sh menu item runs with stdin on /dev/null and by construction cannot ask
3RN-513both halves were already rules (RN-500/RN-501) and had never reached the screen; the only NEW thing was the pre-selection
4RN-512, ADR 0146 point 3the broker leaves profiles in the LOCAL compose and only there — the two composes diverge on purpose
5RN-514, ADR 0147 point 1as planned; the refusal for a missing capability was born implemented and tested without firing — only RN-516 gave it its first real firing
6RN-515/RN-516, ADR 0147 points 2/3/4/8split into two halves (destination × copy), a decision made during the session
7RN-518, RN-519/520, ADR 0147 points 5–6delivered after session 8 — both depended only on 6, and nothing ordered them relative to each other
8RN-517, ADR 0147 point 7as planned
9—closing: the missing E2E, the docmap rule for apps/runner/** and this record

Three things came out differently from what the plan assumed, and all three are declared where it matters:

  • Session 7 ran after session 8. Both depended on 6 and nothing else, and neither depended on the other — the table order was reading order, not precedence.
  • BRB-031 did not die. ADR 0147 expected the install-as-a-service to kill the manual chmod +x of ADR 0118. It doesn't: whoever gets to run brabo-runner service install already had to make the binary executable to reach the command. The side effect depended on the installation coming from the versioned bootstrap of ADR 0146, which this phase did not wire up.
  • Revocation reaches {project, user}, never {key}. Point 6 of the ADR didn't promise per-credential precision, but that was its natural reading. It doesn't exist: runner_socket_tickets stores project_id/user_id/kind, and the credential's identity dies in PatAuthGuard. The cost is written in RN-520 and in security-surface.md instead of disguised.

What this phase does NOT touch​

Declared in both ADRs, and repeated here because it is what gives meaning to the rest:

  • decide.ts and the absolute ceilings — no ceiling gets an exception, not even a configurable one
  • proposed_action as the origin of every external effect of an agent
  • the immutability of the event log
  • the image gate in the three modes (ADR 0135)
  • the five operations and the computed spec of the broker (ADR 0128/0130)
  • the project_execution_mode enum — no new mode is born in this phase

Gaps the phase touches and doesn't solve​

  • git credential discarded on the docker exec path in runner mode — apps/runner/src/index.ts:318-320; packages/docker-port has no env field, by decision of ADR 0130. Declared in ADRs 0130/0145, not fixed here.
  • a symlink inside the project pointing outside is not detected by decide(), which is pure and does no I/O — ADR 0055. apps/runner/src/guard.ts:9-31 declares itself best-effort for the same reason.
  • BRB-005 — the published artifacts (GHCR images, runner binaries) are not signed. A one-line installer without a signature is worse than the current download, and that is why session 7 doesn't get a one-line installer.
  • BRB-031 — manual chmod +x in the browser configuration. It goes away as a side effect if installation starts coming from the versioned bootstrap.
  • {project_id, machine_id} exclusivity postponed until there is a second real simultaneous dev — ADR 0137.