DOC5A_WILL_ACCOUNT_BUILD.md
Current Specs/DOC5/DOC5A_WILL_ACCOUNT_BUILD.md
ELNOR REPO READER TEXT MIRROR
Original path: Current Specs/DOC5/DOC5A_WILL_ACCOUNT_BUILD.md
Source repo: /Users/OpenClaw1/Elnor/Elnor Specs
Git branch: main
Git commit: dbaa25962edc11ab30e8d4ca1715f9ae5bf77331
Generated: 2026-06-09T01:23:58.539Z
---
# DOC5A — File Sync Infrastructure (will account)
**Date:** 2026-02-25
**Status:** Implementation guide — for Codex / Claude Code on the `will` user account.
**Source:** DOC5 File Sync & Security Architecture v1.11.7 (§1–§6, §8–§10)
**Companion:** DOC5B — Elnor Skill Build (openclaw1 account)
---
## What This Document Covers
Everything that runs under or is owned by the `will` macOS user account:
- Folder structure creation (both accounts — `will` has admin rights)
- Permissions setup (cross-account ownership and chmod)
- Sync scripts (rsync mirror pull + outbox push)
- launchd plists (scheduled sync jobs)
- Cleanup/purge scripts and their plists
- The web dashboard (Express server, auth, API, frontend)
- config.json and include files
**What this does NOT cover:** The OpenClaw file-sync skill that Elnor uses. That's in DOC5B, built from the `openclaw1` account.
---
## Prerequisites
- Logged into the `will` macOS user account
- `will` has admin privileges (can `sudo`, can create folders under `/Users/openclaw1/`)
- Node.js installed (for the dashboard)
- OneDrive configured and syncing on the `will` account:
- Firm: `/Users/will/Library/CloudStorage/OneDrive-SharedLibraries-schallfirm.com/`
- Personal: `/Users/will/Library/CloudStorage/OneDrive-Personal/`
- The `openclaw1` user account exists
---
## Phase 1 — Folder Structure & Permissions
### Step 1: Create All Folders
Run as `will` (with sudo for openclaw1-owned paths):
```bash
#!/bin/bash
# setup-folders.sh — Run once from will account
# Creates the complete folder structure for the file sync system
set -e
OC_HOME="/Users/openclaw1"
echo "Creating folder structure..."
# Mirror (will-owned, openclaw1 reads)
sudo mkdir -p "$OC_HOME/onedrive-mirror/firm"
sudo mkdir -p "$OC_HOME/onedrive-mirror/personal"
# Outbox (openclaw1-owned, both read/write)
sudo mkdir -p "$OC_HOME/onedrive-outbox/firm"
sudo mkdir -p "$OC_HOME/onedrive-outbox/personal"
# Working (openclaw1-owned, private scratch)
sudo mkdir -p "$OC_HOME/working"
# Snapshots (openclaw1-owned)
sudo mkdir -p "$OC_HOME/snapshots/firm"
sudo mkdir -p "$OC_HOME/snapshots/personal"
# Mirror versions (will-owned)
sudo mkdir -p "$OC_HOME/onedrive-mirror-versions"
# Sync config (will-owned except requests.txt)
sudo mkdir -p "$OC_HOME/sync-config/scripts"
sudo mkdir -p "$OC_HOME/sync-config/logs"
echo "Folders created."
```
### Step 2: Set Ownership & Permissions
```bash
#!/bin/bash
# setup-permissions.sh — Run once from will account
# Sets ownership and permissions per §2.2 of DOC5
set -e
OC_HOME="/Users/openclaw1"
WILL_USER="will"
OC_USER="openclaw1"
echo "Setting ownership..."
# will-owned (Elnor reads only)
sudo chown -R "$WILL_USER" "$OC_HOME/onedrive-mirror"
sudo chown -R "$WILL_USER" "$OC_HOME/onedrive-mirror-versions"
sudo chown -R "$WILL_USER" "$OC_HOME/sync-config"
# openclaw1-owned (Elnor reads and writes)
sudo chown -R "$OC_USER" "$OC_HOME/onedrive-outbox"
sudo chown -R "$OC_USER" "$OC_HOME/working"
sudo chown -R "$OC_USER" "$OC_HOME/snapshots"
echo "Setting permissions..."
# Mirror: openclaw1 can read, not write
sudo chmod -R 755 "$OC_HOME/onedrive-mirror"
sudo chmod -R 755 "$OC_HOME/onedrive-mirror-versions"
# Outbox: both can read/write
sudo chmod -R 777 "$OC_HOME/onedrive-outbox"
# Working: openclaw1 only
sudo chmod -R 700 "$OC_HOME/working"
sudo chown -R "$OC_USER" "$OC_HOME/working"
# Snapshots: openclaw1 writes, will can read via dashboard
sudo chmod -R 755 "$OC_HOME/snapshots"
# Sync config: will owns, openclaw1 reads
sudo chmod -R 755 "$OC_HOME/sync-config"
sudo chmod -R 700 "$OC_HOME/sync-config/scripts" # scripts: will only
# Logs: will writes, openclaw1 reads
sudo chmod -R 755 "$OC_HOME/sync-config/logs"
# requests.txt: openclaw1 writes, will reads
sudo touch "$OC_HOME/sync-config/requests.txt"
sudo chown "$OC_USER" "$OC_HOME/sync-config/requests.txt"
sudo chmod 644 "$OC_HOME/sync-config/requests.txt"
echo "Permissions set."
```
### Step 3: Verify Permissions
After running both scripts, verify:
```bash
# From will account:
ls -la /Users/openclaw1/onedrive-mirror/ # owner: will
ls -la /Users/openclaw1/onedrive-outbox/ # owner: openclaw1
ls -la /Users/openclaw1/sync-config/ # owner: will
ls -la /Users/openclaw1/sync-config/scripts/ # owner: will, 700
ls -la /Users/openclaw1/sync-config/requests.txt # owner: openclaw1
# Test from openclaw1 (switch user or ssh):
# Should succeed:
# cat /Users/openclaw1/onedrive-mirror/firm/somefile.docx
# touch /Users/openclaw1/onedrive-outbox/firm/test.txt
# echo "test" >> /Users/openclaw1/sync-config/requests.txt
# Should fail:
# touch /Users/openclaw1/onedrive-mirror/firm/test.txt → Permission denied
# touch /Users/openclaw1/sync-config/firm-folders.txt → Permission denied
# cat /Users/openclaw1/sync-config/scripts/firm-mirror.sh → Permission denied
```
---
## Phase 2 — Sync Scripts
### Step 4: config.json
Create at `/Users/openclaw1/sync-config/config.json`:
```json
{
"accounts": {
"firm": {
"label": "Firm OneDrive",
"onedrive_path": "/Users/will/Library/CloudStorage/OneDrive-SharedLibraries-schallfirm.com/",
"mirror_path": "/Users/openclaw1/onedrive-mirror/firm/",
"outbox_path": "/Users/openclaw1/onedrive-outbox/firm/",
"mirror_interval_seconds": 180,
"outbox_interval_seconds": 180,
"mirror_active": true,
"outbox_active": true,
"auto_push": true
},
"personal": {
"label": "Personal OneDrive",
"onedrive_path": "/Users/will/Library/CloudStorage/OneDrive-Personal/",
"mirror_path": "/Users/openclaw1/onedrive-mirror/personal/",
"outbox_path": "/Users/openclaw1/onedrive-outbox/personal/",
"mirror_interval_seconds": 900,
"outbox_interval_seconds": 900,
"mirror_active": true,
"outbox_active": true,
"auto_push": true
}
},
"paths": {
"working": "/Users/openclaw1/working/",
"snapshots": "/Users/openclaw1/snapshots/",
"mirror_versions": "/Users/openclaw1/onedrive-mirror-versions/"
},
"naming_rule_default": "never_overwrite",
"naming_overrides": {},
"retention": {
"outbox_days": 7,
"mirror_versions_days": 30,
"snapshots_days": 60
},
"dashboard": {
"port": 8417
}
}
```
### Step 5: Include Files
Create `/Users/openclaw1/sync-config/firm-folders.txt`:
```
# Folders synced from firm OneDrive to Elnor's mirror
# Managed by the dashboard — do not edit manually
# First match wins. + includes, - excludes.
- *
```
Create `/Users/openclaw1/sync-config/personal-folders.txt`:
```
# Folders synced from personal OneDrive to Elnor's mirror
# Managed by the dashboard — do not edit manually
- *
```
Both start empty (exclude all). Folders are added via the dashboard.
### Step 6: Mirror Pull Scripts
Create `/Users/openclaw1/sync-config/scripts/firm-mirror.sh`:
```bash
#!/bin/bash
# Firm OneDrive → Mirror pull
# Runs as 'will' user via launchd
# SAFETY: rsync --delete only affects DESTINATION (the mirror), never SOURCE (OneDrive)
CONFIG_DIR="/Users/openclaw1/sync-config"
LOG="$CONFIG_DIR/logs/firm-mirror.log"
INCLUDE_FILE="$CONFIG_DIR/firm-folders.txt"
SOURCE="/Users/will/Library/CloudStorage/OneDrive-SharedLibraries-schallfirm.com/"
DEST="/Users/openclaw1/onedrive-mirror/firm/"
VERSIONS="/Users/openclaw1/onedrive-mirror-versions/$(date +%Y%m%d)"
# Check if paused
ACTIVE=$(python3 -c "
import json
config = json.load(open('$CONFIG_DIR/config.json'))
print(config['accounts']['firm']['mirror_active'])
")
if [ "$ACTIVE" = "False" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S'): Firm mirror PAUSED — skipping" >> "$LOG"
exit 0
fi
mkdir -p "$DEST" "$VERSIONS"
echo "$(date '+%Y-%m-%d %H:%M:%S'): Starting firm mirror sync" >> "$LOG"
rsync -av --delete \
--backup --backup-dir="$VERSIONS" \
--include-from="$INCLUDE_FILE" \
"$SOURCE" \
"$DEST" \
>> "$LOG" 2>&1
EXIT_CODE=$?
echo "$(date '+%Y-%m-%d %H:%M:%S'): Firm mirror sync complete (exit: $EXIT_CODE)" >> "$LOG"
```
Create `/Users/openclaw1/sync-config/scripts/personal-mirror.sh` — identical structure, swap `firm` → `personal` and use the Personal OneDrive path.
### Step 7: Outbox Push Scripts
Create `/Users/openclaw1/sync-config/scripts/firm-outbox.sh`:
```bash
#!/bin/bash
# Outbox → Firm OneDrive push
# Runs as 'will' user via launchd
# SAFETY: No --delete flag. Can only add/update files, never delete from OneDrive.
# SAFETY: Naming rules enforced before copy.
# SAFETY: Path traversal blocked.
CONFIG_DIR="/Users/openclaw1/sync-config"
CONFIG_FILE="$CONFIG_DIR/config.json"
LOG="$CONFIG_DIR/logs/firm-outbox.log"
SOURCE="/Users/openclaw1/onedrive-outbox/firm/"
DEST="/Users/will/Library/CloudStorage/OneDrive-SharedLibraries-schallfirm.com/"
# Check if paused
ACTIVE=$(python3 -c "
import json
config = json.load(open('$CONFIG_FILE'))
print(config['accounts']['firm']['outbox_active'])
")
if [ "$ACTIVE" = "False" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S'): Firm outbox push PAUSED — skipping" >> "$LOG"
exit 0
fi
echo "$(date '+%Y-%m-%d %H:%M:%S'): Starting firm outbox push" >> "$LOG"
# Read global naming rule
NAMING_RULE=$(python3 -c "
import json
config = json.load(open('$CONFIG_FILE'))
print(config.get('naming_rule_default', 'never_overwrite'))
")
# Process each file in the outbox (skip .pushed markers)
find "$SOURCE" -type f -not -name "*.pushed" | while read -r OUTBOX_FILE; do
REL_PATH="${OUTBOX_FILE#$SOURCE}"
# Path traversal check
if [[ "$REL_PATH" == *".."* ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S'): BLOCKED $REL_PATH — path traversal detected" >> "$LOG"
continue
fi
# Skip already-pushed files
if [ -f "${OUTBOX_FILE}.pushed" ]; then
continue
fi
DEST_FILE="$DEST$REL_PATH"
DEST_DIR=$(dirname "$DEST_FILE")
# Check for folder-specific naming override
FOLDER_RULE=$(python3 -c "
import json, sys
config = json.load(open('$CONFIG_FILE'))
overrides = config.get('naming_overrides', {})
rel = '$REL_PATH'
for folder, rule in overrides.items():
if rel.startswith(folder):
print(rule)
sys.exit(0)
print('$NAMING_RULE')
")
# Apply naming rule
case "$FOLDER_RULE" in
"elnor_decides")
;;
"never_overwrite")
if [ -f "$DEST_FILE" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S'): BLOCKED $REL_PATH — file exists (never_overwrite)" >> "$LOG"
continue
fi
;;
"auto_version")
if [ -f "$DEST_FILE" ]; then
BASENAME="${DEST_FILE%.*}"
EXT="${DEST_FILE##*.}"
VERSION=2
while [ -f "${BASENAME}_v${VERSION}.${EXT}" ]; do
VERSION=$((VERSION + 1))
done
DEST_FILE="${BASENAME}_v${VERSION}.${EXT}"
echo "$(date '+%Y-%m-%d %H:%M:%S'): Auto-versioned $REL_PATH → $(basename "$DEST_FILE")" >> "$LOG"
fi
;;
"timestamp")
BASENAME="${DEST_FILE%.*}"
EXT="${DEST_FILE##*.}"
TS=$(date +%Y%m%d-%H%M%S)
DEST_FILE="${BASENAME}_${TS}.${EXT}"
;;
esac
mkdir -p "$DEST_DIR"
cp -p "$OUTBOX_FILE" "$DEST_FILE"
if [ $? -eq 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S'): PUSHED $REL_PATH → $(basename "$DEST_FILE")" >> "$LOG"
touch "${OUTBOX_FILE}.pushed"
else
echo "$(date '+%Y-%m-%d %H:%M:%S'): FAILED $REL_PATH" >> "$LOG"
fi
done
echo "$(date '+%Y-%m-%d %H:%M:%S'): Firm outbox push complete" >> "$LOG"
```
Create `personal-outbox.sh` — same structure, swap `firm` → `personal`, use Personal OneDrive path.
### Step 8: Make Scripts Executable (will-only)
```bash
sudo chmod 700 /Users/openclaw1/sync-config/scripts/*.sh
sudo chown will /Users/openclaw1/sync-config/scripts/*.sh
```
---
## Phase 3 — launchd Jobs
### Step 9: Sync Plists
All plists go in `~/Library/LaunchAgents/` on the `will` account (i.e., `/Users/will/Library/LaunchAgents/`).
**com.elnor.sync-mirror-firm.plist:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.elnor.sync-mirror-firm</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/openclaw1/sync-config/scripts/firm-mirror.sh</string>
</array>
<key>StartInterval</key>
<integer>180</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/openclaw1/sync-config/logs/firm-mirror-stderr.log</string>
</dict>
</plist>
```
**Create four plists total:**
| Plist filename | Script | Default interval |
|---|---|---|
| `com.elnor.sync-mirror-firm.plist` | `firm-mirror.sh` | 180s (3 min) |
| `com.elnor.sync-mirror-personal.plist` | `personal-mirror.sh` | 900s (15 min) |
| `com.elnor.sync-outbox-firm.plist` | `firm-outbox.sh` | 180s (3 min) |
| `com.elnor.sync-outbox-personal.plist` | `personal-outbox.sh` | 900s (15 min) |
### Step 10: Cleanup Plists
**Purge scripts** (in `sync-config/scripts/`):
`purge-versions.sh`:
```bash
#!/bin/bash
VERSIONS_DIR="/Users/openclaw1/onedrive-mirror-versions"
RETENTION_DAYS=$(python3 -c "
import json
config = json.load(open('/Users/openclaw1/sync-config/config.json'))
print(config['retention']['mirror_versions_days'])
")
find "$VERSIONS_DIR" -maxdepth 1 -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \;
```
`purge-snapshots.sh`:
```bash
#!/bin/bash
SNAPSHOTS_DIR="/Users/openclaw1/snapshots"
RETENTION_DAYS=$(python3 -c "
import json
config = json.load(open('/Users/openclaw1/sync-config/config.json'))
print(config['retention']['snapshots_days'])
")
find "$SNAPSHOTS_DIR" -type f -mtime +${RETENTION_DAYS} -delete
```
`purge-outbox.sh`:
```bash
#!/bin/bash
OUTBOX_DIR="/Users/openclaw1/onedrive-outbox"
RETENTION_DAYS=$(python3 -c "
import json
config = json.load(open('/Users/openclaw1/sync-config/config.json'))
print(config['retention']['outbox_days'])
")
find "$OUTBOX_DIR" -name "*.pushed" -mtime +${RETENTION_DAYS} | while read -r MARKER; do
ORIGINAL="${MARKER%.pushed}"
rm -f "$MARKER" "$ORIGINAL"
done
```
**One plist for all cleanup** (`com.elnor.sync-purge.plist`):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.elnor.sync-purge</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>
/Users/openclaw1/sync-config/scripts/purge-versions.sh;
/Users/openclaw1/sync-config/scripts/purge-snapshots.sh;
/Users/openclaw1/sync-config/scripts/purge-outbox.sh
</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
```
### Step 11: Load All Jobs
```bash
launchctl load ~/Library/LaunchAgents/com.elnor.sync-mirror-firm.plist
launchctl load ~/Library/LaunchAgents/com.elnor.sync-mirror-personal.plist
launchctl load ~/Library/LaunchAgents/com.elnor.sync-outbox-firm.plist
launchctl load ~/Library/LaunchAgents/com.elnor.sync-outbox-personal.plist
launchctl load ~/Library/LaunchAgents/com.elnor.sync-purge.plist
# Verify:
launchctl list | grep elnor
```
---
## Phase 4 — Web Dashboard
### Step 12: Scaffold
Create the Express app at `~/.elnor-sync-dashboard/` on the `will` account.
```
~/.elnor-sync-dashboard/
├── package.json
├── server.js ← main Express server
├── auth.json ← bcrypt password hash (created on first run)
├── routes/
│ ├── auth.js
│ ├── status.js
│ ├── folders.js
│ ├── sync.js
│ ├── outbox.js
│ ├── requests.js
│ ├── snapshots.js
│ └── settings.js
└── public/
├── index.html ← single-page frontend
├── style.css
└── app.js
```
**Technology:**
- Node.js + Express
- bcryptjs for password hashing
- express-session for session cookies
- No database — reads/writes JSON config files and runs shell commands
### Step 13: Authentication
- Password set on first run (prompt or setup endpoint)
- Stored as bcrypt hash in `auth.json`
- Session-based: login once, cookie lasts 24h
- 5 failed attempts → 15 min lockout
- Bind to `127.0.0.1` only (never `0.0.0.0`)
- Every endpoint requires authentication — no exceptions
### Step 14: API Endpoints
Full endpoint list from DOC5 §6.3:
| Method | Endpoint | Purpose |
|---|---|---|
| POST | `/api/auth/login` | Login |
| POST | `/api/auth/logout` | Logout |
| GET | `/api/status` | Overall sync status |
| GET | `/api/accounts` | OneDrive account info |
| POST | `/api/accounts/detect` | Re-detect OneDrive folders |
| GET | `/api/folders/{account}` | Synced folders |
| POST | `/api/folders/{account}/add` | Add folder to sync |
| DELETE | `/api/folders/{account}/{folder}` | Remove folder from sync |
| GET | `/api/folders/{account}/available` | Browse OneDrive tree |
| POST | `/api/sync/{account}/mirror/pause` | Pause mirror |
| POST | `/api/sync/{account}/mirror/resume` | Resume mirror |
| POST | `/api/sync/{account}/mirror/now` | Trigger immediate mirror |
| POST | `/api/sync/{account}/outbox/pause` | Pause outbox push |
| POST | `/api/sync/{account}/outbox/resume` | Resume outbox push |
| POST | `/api/sync/{account}/outbox/now` | Trigger immediate push |
| POST | `/api/sync/emergency-stop` | Stop all jobs |
| GET | `/api/sync/{account}/timing` | Get interval |
| PUT | `/api/sync/{account}/timing` | Set interval (updates plist, reloads job) |
| GET | `/api/outbox/{account}` | List outbox files |
| POST | `/api/outbox/{account}/push/{file}` | Push specific file |
| POST | `/api/outbox/{account}/rename/{file}` | Rename outbox file |
| DELETE | `/api/outbox/{account}/{file}` | Delete from outbox |
| POST | `/api/outbox/{account}/overwrite/{file}` | Force-push blocked file |
| GET | `/api/requests` | Elnor's folder requests |
| POST | `/api/requests/{id}/approve` | Approve request (adds folder to include list) |
| POST | `/api/requests/{id}/deny` | Deny request |
| GET | `/api/snapshots/{account}` | List snapshots |
| POST | `/api/snapshots/{account}/restore/{file}` | Copy snapshot to outbox |
| DELETE | `/api/snapshots/{account}/{file}` | Delete snapshot |
| GET | `/api/versions/{account}` | Mirror version history |
| GET | `/api/logs/{job}` | Recent log lines |
| GET | `/api/settings` | All settings |
| PUT | `/api/settings` | Update settings |
**Key implementation details for sync control endpoints:**
Pause/resume: Update `mirror_active` or `outbox_active` in config.json. The sync scripts already check this flag at the start and skip if paused.
Timing changes: Update `mirror_interval_seconds` / `outbox_interval_seconds` in config.json, then:
```bash
launchctl unload ~/Library/LaunchAgents/com.elnor.sync-mirror-firm.plist
# Update <integer> in the plist file
launchctl load ~/Library/LaunchAgents/com.elnor.sync-mirror-firm.plist
```
Emergency stop: Unload all four sync plists.
Trigger now: Run the script directly via `execSync`.
### Step 15: Frontend
Single-page HTML/CSS/JS matching the wireframes in DOC5 §6.2.
Four views:
1. **Main page** — Sync status for both accounts, synced folders, outbox summary, Elnor's requests
2. **Outbox view** — Pending, blocked, recently pushed files with actions
3. **Snapshots view** — Browse and restore pre-edit backups
4. **Settings page** — Paths, naming rules, timing, retention, password
See DOC5 §6.2 for complete wireframe layouts.
### Step 16: Dashboard launchd Plist
`com.elnor.sync-dashboard.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.elnor.sync-dashboard</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/will/.elnor-sync-dashboard/server.js</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/will/.elnor-sync-dashboard/dashboard-error.log</string>
<key>StandardOutPath</key>
<string>/Users/will/.elnor-sync-dashboard/dashboard.log</string>
</dict>
</plist>
```
Load: `launchctl load ~/Library/LaunchAgents/com.elnor.sync-dashboard.plist`
---
## Phase 5 — Testing
### Infrastructure Tests (after Phase 1–3)
| # | Test | How | Expected |
|---|---|---|---|
| 1 | Folders exist | `ls -la /Users/openclaw1/onedrive-mirror/` etc. | All folders present |
| 2 | Permissions correct | See Step 3 verification commands | Pass all checks |
| 3 | Mirror pull works | Add a folder to firm-folders.txt, run `bash firm-mirror.sh` | Files appear in mirror |
| 4 | Mirror versioning | Edit a file in OneDrive, re-run mirror | Old version saved to versions dir |
| 5 | Outbox push works | Put a test file in outbox, run `bash firm-outbox.sh` | File appears in OneDrive |
| 6 | never_overwrite blocks | Put a file with existing name in outbox, push | Blocked, logged |
| 7 | auto_version works | Set naming rule, push conflicting file | File saved as `_v2` |
| 8 | Path traversal blocked | Put file with `..` in path, push | Blocked, logged |
| 9 | launchd jobs running | `launchctl list \| grep elnor` | All 5 jobs listed |
| 10 | Purge works | Create old test files, run purge script | Old files deleted |
### Dashboard Tests (after Phase 4)
| # | Test | How | Expected |
|---|---|---|---|
| 11 | Server starts | `node server.js`, visit localhost:8417 | Login page |
| 12 | Auth works | Login with password | Session created |
| 13 | Unauth rejected | Hit API without cookie | 401 |
| 14 | Status API | GET /api/status | Correct sync state |
| 15 | Add folder | Use folder picker, add a folder | Appears in include list, mirrors on next sync |
| 16 | Remove folder | Remove folder via dashboard | Removed from include list |
| 17 | Pause/resume | Pause mirror, verify skip, resume | Script skips when paused |
| 18 | Outbox management | View, push, rename, delete via dashboard | All operations work |
| 19 | Approve request | Write a request to requests.txt, approve in dashboard | Folder added |
| 20 | Settings persist | Change settings, restart dashboard | Settings retained |
---
## OneDrive Files-On-Demand Note
If OneDrive is set to "Files On-Demand," cloud-only stub files won't sync correctly. Best solution: right-click each synced folder in OneDrive → "Always Keep on This Device." Do this once per folder.
Fallback (add to mirror scripts before rsync):
```bash
# Force-download cloud-only stubs
find "$SOURCE" -name "*.docx" -o -name "*.pdf" -o -name "*.xlsx" | while read -r F; do
if xattr -p com.microsoft.OneDrive.DownloadState "$F" 2>/dev/null | grep -q "stub"; then
cat "$F" > /dev/null 2>&1
fi
done
sleep 5
```
---
## Files Created by This Build
| File | Location | Owner |
|---|---|---|
| `setup-folders.sh` | One-time run script | will |
| `setup-permissions.sh` | One-time run script | will |
| `config.json` | `/Users/openclaw1/sync-config/` | will |
| `firm-folders.txt` | `/Users/openclaw1/sync-config/` | will |
| `personal-folders.txt` | `/Users/openclaw1/sync-config/` | will |
| `requests.txt` | `/Users/openclaw1/sync-config/` | openclaw1 |
| `firm-mirror.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| `personal-mirror.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| `firm-outbox.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| `personal-outbox.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| `purge-versions.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| `purge-snapshots.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| `purge-outbox.sh` | `/Users/openclaw1/sync-config/scripts/` | will |
| 4× sync plists | `/Users/will/Library/LaunchAgents/` | will |
| 1× purge plist | `/Users/will/Library/LaunchAgents/` | will |
| 1× dashboard plist | `/Users/will/Library/LaunchAgents/` | will |
| Dashboard app | `/Users/will/.elnor-sync-dashboard/` | will |
---
## What Happens Next
After this build is complete and tested, switch to the `openclaw1` account and run DOC5B to build the file-sync skill that lets Elnor interact with this infrastructure.
---
**End of DOC5A.**