Status: Draft — Phase 0 helper shipped ( ools/oracle-discord/drive-oauth-setup.mjs) · Date: 2026-07-17 · Owner: Ryan
Goal: Use the dedicated Google account's 5TB Drive as the server's cloud
storage backend — per-user workspace files, message attachments, and per-user
memory .md files — with storage quotas enforced per paid tier.
---
The KAI server (command-center-server.mjs) gains a Drive storage module
that stores each user's files in their own folder inside the 5TB Drive, tracks
how many bytes each user has used, and refuses uploads that would exceed the
user's tier limit. Files are addressed by the real user id from the identity
isolation fix (v9.10 / 2026-07-17), so storage is correctly attributed per user.
This is additive. Nothing about the existing engine, Discord fleet, or dashboard
changes except new /api/storage/* endpoints and a new server module.
---
The 5TB lives on a dedicated account you created for this. That's the clean
case: the server can act fully *as* that account with no risk to a personal
identity.
Chosen path: OAuth2 with a stored refresh token, scope drive.file.
Ranked, with why:
1. OAuth2 refresh token, drive.file scope — RECOMMENDED (high confidence).
The server authenticates as the dedicated account and can create/read/manage
*only the files it creates* (drive.file = least privilege; it cannot see
anything else in that Drive). Files count against the account's 5TB. One-time
consent → long-lived refresh token stored in .env.
2. OAuth2 with full drive scope — works, not recommended. Broader access
than needed. Only use if we later need to read files the server did not create.
3. Service account + Shared Drive — only if this becomes a Workspace account.
Personal Gmail accounts cannot host Shared Drives, so this is off the table
for the current dedicated Gmail. If you later move to Google Workspace, this
becomes the cleanest option (no refresh-token maintenance).
4. Plain service account (no shared drive) — RULED OUT. A service account has
its own ~15GB quota and separate billing; files it creates do not draw
from the 5TB. This is the common trap.
5. GOOGLE_API_KEY / Vertex ADC (already in .env) — RULED OUT for storage.
Neither carries a Drive scope; they cannot access Drive user storage.
---
This is the only part I can't do for you: creating the credential and doing the
consent both require signing into the dedicated Google account. Steps:
1. Enable the API. Google Cloud Console → the existing project (the one in
ANTIGRAV_PROJECT) → *APIs & Services → Library* → search **Google Drive
API → Enable**. (Reusing your Antigravity project is fine.)
2. OAuth consent screen. *APIs & Services → OAuth consent screen* → set it to
External, add the dedicated account's email as a Test user (so it works
without Google verification), add scope .../auth/drive.file.
3. Create credentials. *Credentials → Create credentials → OAuth client ID →
Application type: Desktop app.* Download the JSON — it contains
client_id and client_secret.
4. Get a refresh token (one time). I'll ship a tiny helper script
tools/oracle-discord/drive-oauth-setup.mjs. You run it once:
approve.
code; the script exchanges it for arefresh token and prints it.
access_type=offline + prompt=consent are set so a refresh token isactually returned.
5. Paste 3 values into .env (see §4). Done — the server can now use the 5TB.
> Security: the refresh token is a long-lived secret. It lives only in
> tools/oracle-discord/.env (same as every other secret) — never printed,
> committed, or echoed.
---
.env keysGDRIVE_CLIENT_ID=<from the OAuth client JSON> GDRIVE_CLIENT_SECRET=<from the OAuth client JSON> GDRIVE_REFRESH_TOKEN=<from the one-time setup script> GDRIVE_ROOT_FOLDER_NAME=KAI-Server # optional; default "KAI-Server" GDRIVE_ENABLED=1 # kill-switch: 0 = disable cleanly
No new secrets are needed beyond these three real ones.
---
tools/oracle-discord/storage/drive.mjsUses the googleapis npm package (add to package.json). Singleton Drive client
built from the refresh token; token auto-refreshes in memory.
Drive layout:
KAI-Server/ (root app folder, id cached)
users/
<userId>/ (one folder per real user id)
workspace/ (Builder+ workspace files)
attachments/ (message/DM attachments)
memory/ (per-user .md memory files — task #20)
Functions (all async, all keyed by the caller's real userId):
ensureUserFolder(userId, sub) → folderId (creates lazily, caches id).uploadFile(userId, sub, name, buffer, mime) → {fileId, size, webViewLink}.downloadFile(fileId, userId) → stream (verifies the file is under the user'sfolder before serving — no cross-user reads).
listFiles(userId, sub) → [{id,name,size,modifiedTime}].deleteFile(fileId, userId) → moves to trash (never hard-delete).userUsageBytes(userId) → summed size of the user's subtree (cached ~60s).accountQuota() → about.get({fields:'storageQuota'}) → {limit,usage} forthe whole 5TB (ops dashboard).
Design notes:
drive.file scope means the module only ever sees its own files — safe. it's inside users/<callerId>/. This is the storage-side equivalent of the
identity isolation fix — a user can never fetch another user's file id.
---
Drive has no native per-subfolder quota, so the server enforces it: before an
upload, userUsageBytes(userId) + incoming.size must be ≤ the user's tier limit.
Usage is also cached in the user record for fast reads.
Proposed starting limits (adjust — see §10; total must fit inside 5TB):
Capacity math: 5TB = ~5,000 GB. Mixed tiers leave comfortable headroom for a
few hundred paid users; the ops dashboard shows total 5TB usage via
accountQuota() so you see the ceiling coming.
Enforcement returns HTTP 413 {ok:false,error:'quota_exceeded',limit,used} so
the UI can show "You're out of space — upgrade your tier."
Tier limits live in one server constant (TIER_STORAGE_BYTES) next to the
existing plan/credits config, so they're one edit to tune.
---
currentContentUser → real user only)
POST /api/storage/upload (multipart or base64) → {fileId,size,link}
GET /api/storage/list?sub= → user's files in a subfolder
GET /api/storage/file?id= → download (ownership-checked)
POST /api/storage/delete {fileId} → trash
GET /api/storage/usage → {used, limit, tier, pct}
GET /api/storage/account (owner/admin only) → whole-5TB usage
These reuse the same auth wall and per-user identity as the DM/receipt work.
---
1. Message / DM attachments — the "Photo" button and DM images upload to
attachments/, store the returned link in the post/message record.
2. Per-user memory .md (task #20) — write/read the user's memory file in
memory/, with the local device copy as the fast cache and Drive as backup.
3. Builder-tier workspace — the Antigravity workspace files for Builder users
live in workspace/ (ties into the "Builder tier → Antigravity session"
request).
---
.env values. ~15 min.drive.mjs module + npm i googleapis + a self-test that uploadsand deletes a 1KB probe file. Verifiable in isolation, touches nothing else.
/api/storage/* endpoints + TIER_STORAGE_BYTES + quota gate.GDRIVE_ENABLED=0.---
tokens can expire after 7 days. Fix: move the consent screen to
"In production" (no Google verification needed for drive.file with a single
known user) — then the refresh token is long-lived. I'll flag this in the setup
steps so you don't get surprised in a week.
of band. Mitigation: periodic reconcile against Drive's actual per-folder
listing; usage cache TTL ~60s.
and cache; don't poll usage on every request (cache it).
this).
is locked, storage is unavailable. Acceptable for now; note it for scale.
attachments; if you later want fast public media, add signed Drive links or a
real object store (R2/S3) — but 5TB Drive is a great start.
---
1. Tier storage sizes — accept the §6 table or give me your numbers.
2. drive.file vs full drive scope — recommend drive.file (least
privilege). Confirm.
3. Attachment size cap per upload (e.g., 25MB Free → 250MB Builder?).
4. When to move the OAuth consent screen to "production" (do it during setup
to avoid the 7-day token expiry — recommended).
Once you've done Phase 0 and answered §11, I'll build Phase 1 and give you the
probe-file test to confirm the 5TB is live before wiring anything else.