#!/usr/bin/env python3 """ GARC Setup — Interactive workspace provisioner - Creates Google Drive folder structure - Provisions Google Sheets with all required tabs and headers - Uploads initial disclosure chain templates to Drive - Validates all APIs are accessible """ import argparse import json import os import sys from pathlib import Path # Add scripts dir to path for garc-core sys.path.insert(0, str(Path(__file__).parent)) from garc_core import build_service, load_config, utc_now, GARC_CONFIG_DIR # Sheet definitions: tab name → headers SHEET_TABS = { "memory": ["agent_id", "timestamp", "entry", "source", "tags"], "agents": ["id", "model", "scopes", "description", "profile", "status", "drive_folder", "registered_at"], "queue": ["queue_id", "agent_id", "message", "status", "gate", "source", "created_at", "updated_at", "assigned_to"], "heartbeat": ["agent_id", "timestamp", "status", "notes", "platform", "context_file"], "approval": ["approval_id", "agent_id", "task", "status", "created_at", "resolved_at", "resolver", "notes"], "tasks_log": ["task_id", "agent_id", "title", "google_task_id", "status", "created_at", "completed_at"], "email_log": ["msg_id", "agent_id", "to", "subject", "sent_at", "thread_id"], "calendar_log": ["event_id", "agent_id", "title", "start", "end", "created_at"], } # Disclosure chain templates DISCLOSURE_TEMPLATES = { "SOUL.md": """# SOUL — Agent Identity agent_id: main platform: Google Workspace runtime: GARC v0.1.0 created: {timestamp} ## Core Principles 1. **Permission-first**: Always run `garc auth suggest` before a new task category. 2. **Minimum viable scopes**: Request only what is needed. 3. **Gate compliance**: Respect none / preview / approval execution gates. 4. **Transparency**: Explain actions before executing them. 5. **Reversibility preference**: Prefer reversible operations; flag irreversible ones. ## Identity This agent operates within Google Workspace on behalf of the registered user. It has access to Drive, Sheets, Gmail, Calendar, and Tasks as configured. ## Persona Helpful, precise, audit-minded. Acts as a trusted digital colleague. """, "USER.md": """# USER — User Profile ## Identity name: (your name) email: (your Gmail) timezone: Asia/Tokyo language: Japanese / English ## Work Context role: (your role) organization: (your organization) primary_tools: [Gmail, Google Drive, Google Sheets, Google Calendar] ## Preferences - Communication style: direct, structured - Approval threshold: always confirm before sending external emails - Memory: persist important decisions and context - Calendar: treat work hours as 09:00-18:00 JST ## Created {timestamp} — edit this file in Google Drive to customize. """, "MEMORY.md": """# MEMORY — Long-term Memory Index Last sync: {timestamp} Backend: Google Sheets (configured in ~/.garc/config.env) ## How to use - Pull latest: `garc memory pull` - Add entry: `garc memory push "key decision: ..."` - Search: `garc memory search "keyword"` - View raw: Google Sheets → memory tab ## Recent context (populated by `garc memory pull`) """, "RULES.md": """# RULES — Operating Rules ## Execution Rules 1. Always check execution gate before any write operation - `garc approve gate ` 2. For `preview` gate: show preview, ask for confirmation 3. For `approval` gate: create approval request, wait for human 4. Never send email without explicit confirmation unless gate is `none` 5. Never delete files or calendar events without `approval` gate clearance ## Memory Rules 1. After any significant decision, push to memory - `garc memory push "decided: ..."` 2. Pull memory at session start for context - `garc memory pull` 3. Heartbeat at session end - `garc heartbeat` ## Communication Rules 1. Default reply-to: use GARC_GMAIL_DEFAULT_TO for notifications 2. Subject prefix for agent emails: `[GARC]` 3. CC user on all outbound approvals ## Safety Rules 1. Max 50 emails/hour limit (self-imposed) 2. Max 100 Drive file operations/hour 3. Never modify shared Drives without explicit instruction 4. Always confirm before recurring calendar events """, "HEARTBEAT.md": """# HEARTBEAT — System State agent_id: main last_bootstrap: {timestamp} status: initialized platform: Google Workspace ## Latest State (updated by `garc heartbeat`) """, } def check_api_access(config: dict) -> dict: """Verify all required APIs are accessible.""" results = {} print("\n🔍 Checking API access...") # Drive try: svc = build_service("drive", "v3") svc.about().get(fields="user").execute() results["Drive API"] = "✅" except Exception as e: results["Drive API"] = f"❌ {str(e)[:60]}" # Sheets try: svc = build_service("sheets", "v4") results["Sheets API"] = "✅" except Exception as e: results["Sheets API"] = f"❌ {str(e)[:60]}" # Gmail try: svc = build_service("gmail", "v1") svc.users().getProfile(userId="me").execute() results["Gmail API"] = "✅" except Exception as e: results["Gmail API"] = f"❌ {str(e)[:60]}" # Calendar try: svc = build_service("calendar", "v3") svc.calendarList().list(maxResults=1).execute() results["Calendar API"] = "✅" except Exception as e: results["Calendar API"] = f"❌ {str(e)[:60]}" # Tasks try: svc = build_service("tasks", "v1") svc.tasklists().list(maxResults=1).execute() results["Tasks API"] = "✅" except Exception as e: results["Tasks API"] = f"❌ {str(e)[:60]}" # Docs try: svc = build_service("docs", "v1") results["Docs API"] = "✅" except Exception as e: results["Docs API"] = f"❌ {str(e)[:60]}" # People try: svc = build_service("people", "v1", scopes=["https://www.googleapis.com/auth/contacts.readonly"]) svc.people().connections().list( resourceName="people/me", personFields="names", pageSize=1, ).execute() results["People API"] = "✅" except Exception as e: results["People API"] = f"❌ {str(e)[:60]}" for api, status in results.items(): print(f" {status} {api}") return results def provision_sheets(config: dict) -> str: """Create or update Google Sheets with all required tabs.""" sheets_id = config.get("GARC_SHEETS_ID", "") svc = build_service("sheets", "v4") if sheets_id: print(f"\n📊 Using existing Sheets: {sheets_id}") # Get existing sheet info try: meta = svc.spreadsheets().get(spreadsheetId=sheets_id).execute() existing_tabs = {s["properties"]["title"] for s in meta.get("sheets", [])} print(f" Existing tabs: {', '.join(existing_tabs)}") except Exception as e: print(f"❌ Cannot access Sheets {sheets_id}: {e}") sheets_id = "" if not sheets_id: print("\n📊 Creating new Google Sheets for GARC...") result = svc.spreadsheets().create(body={ "properties": {"title": "GARC Workspace Data"}, "sheets": [{"properties": {"title": tab}} for tab in SHEET_TABS.keys()] }).execute() sheets_id = result["spreadsheetId"] existing_tabs = set(SHEET_TABS.keys()) print(f" ✅ Created: https://docs.google.com/spreadsheets/d/{sheets_id}") else: existing_tabs = existing_tabs # from above # Add missing tabs meta = svc.spreadsheets().get(spreadsheetId=sheets_id).execute() existing_tab_names = {s["properties"]["title"] for s in meta.get("sheets", [])} add_requests = [] for tab_name in SHEET_TABS: if tab_name not in existing_tab_names: add_requests.append({ "addSheet": {"properties": {"title": tab_name}} }) if add_requests: svc.spreadsheets().batchUpdate( spreadsheetId=sheets_id, body={"requests": add_requests} ).execute() print(f" ✅ Added tabs: {[r['addSheet']['properties']['title'] for r in add_requests]}") # Write headers to each tab batch_data = [] for tab_name, headers in SHEET_TABS.items(): batch_data.append({ "range": f"{tab_name}!A1:{chr(65 + len(headers) - 1)}1", "values": [headers] }) svc.spreadsheets().values().batchUpdate( spreadsheetId=sheets_id, body={ "valueInputOption": "RAW", "data": batch_data } ).execute() print(f" ✅ Headers written to all {len(SHEET_TABS)} tabs") # Bold the header row in each sheet (formatting) meta = svc.spreadsheets().get(spreadsheetId=sheets_id).execute() sheet_id_map = {s["properties"]["title"]: s["properties"]["sheetId"] for s in meta.get("sheets", [])} format_requests = [] for tab_name in SHEET_TABS: sid = sheet_id_map.get(tab_name) if sid is not None: format_requests.append({ "repeatCell": { "range": {"sheetId": sid, "startRowIndex": 0, "endRowIndex": 1}, "cell": {"userEnteredFormat": {"textFormat": {"bold": True}, "backgroundColor": {"red": 0.9, "green": 0.9, "blue": 0.9}}}, "fields": "userEnteredFormat(textFormat,backgroundColor)" } }) # Freeze header row format_requests.append({ "updateSheetProperties": { "properties": {"sheetId": sid, "gridProperties": {"frozenRowCount": 1}}, "fields": "gridProperties.frozenRowCount" } }) if format_requests: svc.spreadsheets().batchUpdate( spreadsheetId=sheets_id, body={"requests": format_requests} ).execute() print(" ✅ Headers formatted (bold + freeze)") return sheets_id def provision_drive(config: dict) -> str: """Create Drive folder structure for agent workspace.""" folder_id = config.get("GARC_DRIVE_FOLDER_ID", "") svc = build_service("drive", "v3") if folder_id: print(f"\n📁 Using existing Drive folder: {folder_id}") try: meta = svc.files().get(fileId=folder_id, fields="id,name").execute() print(f" Folder: {meta['name']}") except Exception: print(f"⚠️ Folder not accessible, creating new one...") folder_id = "" if not folder_id: print("\n📁 Creating GARC Drive folder...") result = svc.files().create(body={ "name": "GARC Workspace", "mimeType": "application/vnd.google-apps.folder" }, fields="id,name").execute() folder_id = result["id"] print(f" ✅ Created folder: {result['name']} ({folder_id})") # Create memory subfolder memory_query = f"'{folder_id}' in parents and name='memory' and mimeType='application/vnd.google-apps.folder'" existing = svc.files().list(q=memory_query, fields="files(id,name)").execute() if not existing.get("files"): svc.files().create(body={ "name": "memory", "mimeType": "application/vnd.google-apps.folder", "parents": [folder_id] }).execute() print(" ✅ Created memory/ subfolder") return folder_id def upload_disclosure_chain(folder_id: str): """Upload disclosure chain template files to Google Drive.""" svc = build_service("drive", "v3") ts = utc_now() print("\n📝 Uploading disclosure chain templates...") for filename, template in DISCLOSURE_TEMPLATES.items(): content = template.replace("{timestamp}", ts).encode("utf-8") # Check if file exists query = f"'{folder_id}' in parents and name='{filename}' and trashed=false" existing = svc.files().list(q=query, fields="files(id,name)").execute() if existing.get("files"): # Skip if already exists (don't overwrite user's customized files) print(f" ⏭️ {filename} (already exists, skipping)") continue from googleapiclient.http import MediaIoBaseUpload import io media = MediaIoBaseUpload(io.BytesIO(content), mimetype="text/plain") svc.files().create( body={"name": filename, "parents": [folder_id]}, media_body=media, fields="id,name" ).execute() print(f" ✅ {filename}") def save_config(config_updates: dict): """Save updated config values to ~/.garc/config.env.""" config_file = GARC_CONFIG_DIR / "config.env" GARC_CONFIG_DIR.mkdir(parents=True, exist_ok=True) # Read existing existing = {} if config_file.exists(): with open(config_file) as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: k, _, v = line.partition("=") existing[k.strip()] = v.strip() existing.update(config_updates) # Write back template_file = Path(__file__).parent.parent / "config" / "config.env.example" if template_file.exists(): with open(template_file) as f: template_lines = f.readlines() out_lines = [] written_keys = set() for line in template_lines: stripped = line.strip() if stripped and not stripped.startswith("#") and "=" in stripped: key = stripped.split("=")[0].strip() if key in existing: out_lines.append(f"{key}={existing[key]}\n") written_keys.add(key) continue out_lines.append(line) # Add any extra keys not in template for key, val in existing.items(): if key not in written_keys: out_lines.append(f"{key}={val}\n") with open(config_file, "w") as f: f.writelines(out_lines) else: with open(config_file, "w") as f: for key, val in existing.items(): f.write(f"{key}={val}\n") config_file.chmod(0o600) print(f"\n✅ Config saved: {config_file}") def main(): parser = argparse.ArgumentParser(description="GARC Setup Wizard") subparsers = parser.add_subparsers(dest="command") # Full setup setup_p = subparsers.add_parser("all", help="Run full setup wizard") setup_p.add_argument("--skip-upload", action="store_true", help="Skip disclosure chain upload") # Check only subparsers.add_parser("check", help="Check API access only") # Provision sheets only subparsers.add_parser("sheets", help="Provision Sheets tabs only") # Provision drive only subparsers.add_parser("drive", help="Provision Drive folder only") args = parser.parse_args() config = load_config() if args.command == "check": check_api_access(config) return if args.command == "sheets": sheets_id = provision_sheets(config) save_config({"GARC_SHEETS_ID": sheets_id}) return if args.command == "drive": folder_id = provision_drive(config) save_config({"GARC_DRIVE_FOLDER_ID": folder_id}) return # Full setup print("=" * 60) print("GARC Workspace Setup") print("=" * 60) # 1. Check APIs api_results = check_api_access(config) failed_apis = [k for k, v in api_results.items() if v.startswith("❌")] if failed_apis: print(f"\n⚠️ {len(failed_apis)} APIs not accessible: {', '.join(failed_apis)}") print(" See docs/google-cloud-setup.md for setup instructions") if len(failed_apis) > 3: print(" Too many failures. Please enable APIs first.") return # 2. Provision Drive folder_id = provision_drive(config) # 3. Provision Sheets sheets_id = provision_sheets(config) # 4. Upload disclosure chain if not getattr(args, "skip_upload", False): upload_disclosure_chain(folder_id) # 5. Save config save_config({ "GARC_DRIVE_FOLDER_ID": folder_id, "GARC_SHEETS_ID": sheets_id, }) # 6. Summary print("\n" + "=" * 60) print("✅ Setup complete!") print("=" * 60) print(f" Drive folder: https://drive.google.com/drive/folders/{folder_id}") print(f" Sheets: https://docs.google.com/spreadsheets/d/{sheets_id}") print() print("Next steps:") print(" garc bootstrap --agent main") print(" garc status") print(" garc memory pull") if __name__ == "__main__": main()