ADR-0003: The content of a Drive file goes through the broker's deposit directory¶
Status: Accepted Date: 2026-07-26 Decision makers: Thomas (PO)
TL;DR — So that an agent can drop an already-written document (converted to a Google Doc) into the right account's Drive, rather than an empty file, the text is written to a temporary file in a dedicated, locked-down folder (
.uploads) from which the command-line tool runs, then erased right away — this detour is forced because that tool only accepts a file path (no in-memory content) and refuses any path outside its working directory.
Context¶
drive_create could only set metadata (name, mimeType,
parents): it created empty files (backlog item 0024). Yet the real use
is "drop a written deliverable into the right account's Drive" — that is the
project's raison d'être, since the other Drive connectors write content but
to the wrong account.
The Drive API expects a multipart upload (metadata + media in the same
request); this is also what triggers the conversion of text/markdown or
text/plain → application/vnd.google-apps.document. On the CLI side, this is written:
Two constraints frame the solution:
- gws does not accept in-memory content — only
--upload <path>. The text therefore has to be materialized in a file. - gws refuses any path resolved outside its current directory:
--upload '/tmp/x.md' resolves to '/private/tmp/x.md' which is outside the current directory. A classictempfilein/tmpis therefore unusable: gws's cwd is that of the broker, which depends on how it was started (REPO_DIRon automatic start, arbitrary when started by hand).
On top of this comes the project doctrine (ADR-0002): the broker is the only process that runs gws; the gateway launches no gws subprocess.
Decision¶
Create a dedicated deposit directory, <GWSA_ROOT>/.uploads (0700), and have
the broker run gws with this directory as its current directory.
- The gateway writes the content there to a temporary file (0600, created by
mkstemp), passes its path as--upload, and always erases it (finally), on success and on failure. - The broker sets
cwd=<deposit>for all its gws calls, not just uploads: gws's file sandbox becomes the deposit directory instead of the git repository. - The path is derived from
gwsa_root()on both sides — the gateway and broker already shareGWSA_ROOT(token, pidfile,usage.jsonl), and a lane (stable / dev, backlog item 0023) has its own deposit. parentsstays in--json: it is the only place thatpolicy-check.pyreads to check the write zone. Drive zones therefore apply to the content deposit exactly as they do to the creation of an empty file.- The directory starts with a dot: the three profile enumerators
(
gateway/profiles.py,bin/mag,admin/server.js) filter onALIAS_RE, so it never shows up as an account.
Options considered¶
Option A — Temporary file in /tmp, absolute path¶
| Dimension | Assessment |
|---|---|
| Simplicity | Maximal (about ten lines, zero coordination) |
| Works | No — gws refuses the path outside cwd |
Rejected: it does not work. Constraint no. 2 closed this door.
Option B — Shared deposit directory + broker cwd (chosen)¶
| Dimension | Assessment |
|---|---|
| Broker protocol | Unchanged (still "a list of arguments") |
| ADR-0002 doctrine | Respected — the gateway writes a file, it still does not run gws |
| Security | gws's sandbox narrowed: from the git repository to the deposit directory alone |
| Failure on a stale broker | Explicit (outside the current directory), never silent |
Pros: no protocol change, hence no version negotiation
between a new gateway and an already-running broker; and a gws cwd narrower
than before for all commands. Cons: responsibility is
shared (the gateway writes the file, the broker sets the cwd) — two pieces
of code must agree on a path. The coupling already exists (GWSA_ROOT)
and a hermetic test locks it down.
Option C — Extend the broker protocol (media field)¶
The content would travel in the RPC request; the broker would write the file,
add --upload, then clean up.
| Dimension | Assessment |
|---|---|
| Consistency | The best: the broker owns everything gws touches |
| Protocol | Changes — a broker already running (old code) ignores media… |
| Failure on a stale broker | …and creates an empty document without reporting anything |
Pros: single ownership of the media file. Cons: the broker is a
daemon that outlives code updates (deploy-local.sh recycles it
precisely for that): a silently ignored field produces silent
data loss. A capability negotiation (or a separate command) would be needed to make the failure explicit — extra machinery for
zero gain in trust here (gateway and broker are the same user, the same
deployed code, and the broker already trusts the gateway's arguments
subject to policy). Rejected for now; to reconsider if the
broker ever had to serve clients less trustworthy than the local gateway.
Option D — Create empty, then write via the Docs API¶
drive files create (empty) then docs documents batchUpdate /
gws docs +write.
| Dimension | Assessment |
|---|---|
| Policy | Blocked in practice: undeclared docs = refused (default-deny) |
| Atomicity | Two calls — "empty document" intermediate state on failure |
| Rendering | Plain text only: no headings, lists, or bold |
Rejected.
Consequences¶
- Becomes possible: dropping a written deliverable into the right account, in one request, converted to a Google Doc (markdown by default when the target is a Google type — that is the format in which agents write).
- Becomes safer: gws now runs from an empty deposit directory
rather than from the git repository; a crafted
--uploadcan no longer point at a project file. - Becomes more demanding: a broker launched before this version keeps
serving the old code and will refuse the upload (
outside the current directory). Remedy:mag broker stop(it restarts by itself on the next call) — this is already whatscripts/deploy-local.shdoes. - Content written by the LLM touches the disk, in clear, for the duration of one call
(0600, in a 0700 directory, erased in
finally). This is the same level of exposure asusage.jsonl, which already logs command arguments, and far lower than the credentials in the same directory.