garc-gws-agent-runtime/bin/garc
林 駿甫 (Shunsuke Hayashi) 7b5951a1d5 fix: resolve all 17 playbook findings (P0–P3)
P0 fixes:
- agent register: upsert by agent_id (no duplicate rows)
- daemon poll-once: extract _gmail_poll_cycle, run synchronously
- garc_core.py: suppress urllib3/googleapiclient DeprecationWarnings

P1 fixes:
- OAuth: detect RefreshError → delete stale token → re-auth flow
- OAuth: scope coverage check before returning valid creds
- ingress: add stale-reset subcommand (reset in_progress > N min)
- sheets: trim-sheet / clean-all — deleteDimension for empty rows
- approval gate: send Gmail notification to GARC_APPROVAL_EMAIL

P2 additions:
- Google Chat: garc-chat-helper.py + garc send chat subcommands
- Service Account: garc auth service-account verify + DWD support
- Audit log: Sheets audit tab + garc audit list + bin/garc async hook
- garc auth revoke: POST /revoke + delete token file
- kg: pagination fix, shell injection fix, garc-kg-query.py
- docs: _doc_insert_text / append_doc / garc drive append-doc

P3 additions:
- Multi-tenant: lib/profile.sh (list/use/add/show/remove/current)
  bin/garc: auto-load profile config.env and token.json
- Google Forms pipeline: garc-forms-helper.py + lib/forms.sh
  garc forms list/responses/watch
- systemd: _daemon_install_service OS-detect → launchd or systemd units
- Python version gate (>=3.10) in bin/garc + pyproject.toml
- garc doctor command for environment diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 09:55:33 +09:00

384 lines
14 KiB
Bash
Executable file

#!/usr/bin/env bash
# GARC — Google Workspace Agent Runtime CLI
# Main entrypoint
set -euo pipefail
GARC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# ── Python version gate ────────────────────────────────────────────────────
if ! python3 -c "import sys; assert sys.version_info >= (3,10), f'Python 3.10+ required, got {sys.version}'" 2>/dev/null; then
_PY_VER=$(python3 --version 2>&1)
echo "❌ GARC requires Python 3.10 or higher." >&2
echo " Detected: ${_PY_VER}" >&2
echo " Install Python 3.10+: https://www.python.org/downloads/" >&2
exit 1
fi
GARC_LIB="${GARC_DIR}/lib"
GARC_CONFIG="${HOME}/.garc"
GARC_CONFIG_ENV="${GARC_CONFIG}/config.env"
# Load base config if present
if [[ -f "${GARC_CONFIG_ENV}" ]]; then
# shellcheck source=/dev/null
source "${GARC_CONFIG_ENV}"
fi
# Load profile-specific config (overrides base config)
GARC_PROFILE="${GARC_PROFILE:-}"
if [[ -n "${GARC_PROFILE}" ]]; then
_PROFILE_ENV="${GARC_CONFIG}/profiles/${GARC_PROFILE}/config.env"
if [[ -f "${_PROFILE_ENV}" ]]; then
# shellcheck source=/dev/null
source "${_PROFILE_ENV}"
fi
# Use profile-specific token if not already overridden
_PROFILE_TOKEN="${GARC_CONFIG}/profiles/${GARC_PROFILE}/token.json"
if [[ -f "${_PROFILE_TOKEN}" && -z "${GARC_TOKEN_FILE:-}" ]]; then
export GARC_TOKEN_FILE="${_PROFILE_TOKEN}"
fi
fi
# Defaults
GARC_CACHE_DIR="${GARC_CACHE_DIR:-${GARC_CONFIG}/cache}"
GARC_CACHE_TTL="${GARC_CACHE_TTL:-300}"
GARC_DEFAULT_AGENT="${GARC_DEFAULT_AGENT:-main}"
VERSION="0.1.0"
usage() {
cat <<EOF
GARC v${VERSION} — Google Workspace Agent Runtime CLI
Usage: garc <command> [subcommand] [options]
━━━ Core ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
init Initialize GARC workspace (config + dirs)
setup [all|check|sheets|drive] Provision GWS resources automatically
bootstrap [--agent <id>] Load disclosure chain from Google Drive
status Show config and connection health
━━━ Gmail ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
gmail send --to <email> --subject <text> --body <text> [--cc] [--html]
gmail reply --thread-id <id> --to <email> --body <text>
gmail search <query> [--max N] [--body]
gmail read <message_id>
gmail inbox [--max N] [--unread]
gmail draft --to <email> --subject <text> --body <text>
gmail labels
gmail profile
send "<msg>" --to <email> Shorthand for gmail send
━━━ Google Calendar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
calendar today Events for today
calendar week Events for this week
calendar list [--days N] [--query <text>]
calendar create --summary <text> --start <dt> --end <dt> [--attendees ...]
calendar update <event_id> [--summary ...] [--start ...] [--end ...]
calendar delete <event_id>
calendar get <event_id>
calendar freebusy --start <date> --end <date> --emails email1 [...]
calendar quick-add "<natural language>"
calendar calendars List all accessible calendars
━━━ Google Drive ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
drive list [--folder-id <id>] [--query <name>]
drive search <query> [--type doc|sheet|slide|folder|pdf]
drive info <file_id>
drive download --file-id <id> | --folder-id + --filename [--output <path>]
drive upload <local_path> [--folder-id <id>] [--convert]
drive create-folder <name> [--parent-id <id>]
drive create-doc <name> [--folder-id <id>] [--content <text>]
drive share <file_id> --email <email> [--role reader|writer]
drive move <file_id> --to <folder_id>
drive delete <file_id> [--permanent]
━━━ Google Sheets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
sheets info [--sheets-id <id>]
sheets read --range <A1:Z100> [--format table|json]
sheets write --range <A1> --values '[[...]]'
sheets append --sheet <name> --values '[...]'
sheets search --sheet <name> --query <text> [--format json]
sheets clear --range <range>
━━━ Memory ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
memory pull Sync Sheets memory → local cache
memory push "<entry>" Save entry to Sheets memory
memory search <query> Search memory entries
━━━ Tasks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
task list [--list <id>] [--completed] [--format json]
task show <task_id>
task create "<title>" [--due YYYY-MM-DD] [--notes <text>] [--list <id>]
task update <task_id> [--title] [--due] [--notes]
task done <task_id> Mark task complete
task delete <task_id>
task clear-completed Remove all completed tasks
task tasklists List all task lists
━━━ People & Contacts ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
people search <query> Search personal contacts
people directory <query> Search GWS org directory
people list [--max N]
people show <contact_id>
people create --name <name> [--email] [--phone] [--company] [--title]
people update <contact_id> [--name] [--email] ...
people delete <contact_id>
people lookup <name> Quick name → email lookup
━━━ Permission & Approval ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
auth suggest "<task>" Infer minimum OAuth scopes
auth check [--profile <p>] Verify current token scopes
auth login [--profile <p>] Launch OAuth2 flow
auth status Show token info
approve gate <task_type> Check execution gate
approve list List pending approvals
approve create "<task>" Create approval request
approve act <id> --action approve|reject
━━━ Agents & Queue ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
agent list List registered agents
agent register [--file] Register from agents.yaml
agent show <id> Show agent details
ingress enqueue --text "<msg>" [--source gmail|manual] [--sender <email>]
ingress list [--status pending|done|failed|all]
ingress next [--agent <id>]
ingress run-once [--agent <id>] [--dry-run] → outputs Claude prompt
ingress execute-stub --queue-id <id> → show execution plan
ingress context --queue-id <id> → full Claude-readable bundle
ingress delegate --queue-id <id> --to <agent>
ingress handoff --queue-id <id>
ingress approve/resume --queue-id <id>
ingress done/fail --queue-id <id> [--note <text>]
ingress verify --queue-id <id>
ingress stats
━━━ Google Forms (Response Pipeline) ━━━━━━━━━━━━━━━━━━━━━━━━━━
forms list List accessible Google Forms
forms responses <id> List form responses
forms watch <id> --agent Poll form and auto-enqueue new responses
━━━ Daemon (Gmail Poller) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
daemon start [--interval <sec>] [--agent <id>]
daemon stop
daemon status
daemon restart
daemon poll-once Single poll cycle (foreground)
daemon logs [--follow]
daemon install Install macOS launchd service
━━━ Knowledge Graph ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
kg build Build KG from Drive Docs
kg query "<concept>" Search knowledge graph
kg show <doc_id> Show doc + links
━━━ Profiles (Multi-tenant) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
profile list List all tenant profiles
profile use <name> Activate a profile (eval output)
profile add <name> Create a new profile
profile show [<name>] Show profile config
profile remove <name> Delete a profile
profile current Show active profile
━━━ System ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
heartbeat Log system state to Sheets
audit list [--agent <id>] [--since YYYY-MM-DD] View audit log
doctor Check Python version and dependencies
Options:
--help, -h Show this help
--version, -v Show version
--debug Enable debug output
--dry-run Preview without executing
--confirm Auto-confirm preview-gated operations
Config: ~/.garc/config.env | Cache: ~/.garc/cache/
Docs: docs/google-cloud-setup.md | Quickstart: docs/quickstart.md
EOF
}
# Parse global flags
DEBUG=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
--debug) DEBUG=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--help|-h) usage; exit 0 ;;
--version|-v) echo "garc ${VERSION}"; exit 0 ;;
*) break ;;
esac
done
export DEBUG DRY_RUN GARC_DIR GARC_LIB GARC_CONFIG GARC_CACHE_DIR GARC_CACHE_TTL
# ── Non-blocking audit log ─────────────────────────────────────────────────
# Fires in background so it never blocks or fails the main command.
_garc_audit_log() {
local cmd="$1"
local args_str="$2"
local result="${3:-ok}"
local sheets_id="${GARC_SHEETS_ID:-}"
local agent_id="${GARC_DEFAULT_AGENT:-}"
[[ -z "${sheets_id}" ]] && return 0
[[ "${cmd}" == "audit" ]] && return 0 # don't audit audit itself
(
python3 "${GARC_DIR}/scripts/garc-sheets-helper.py" audit-append \
--sheets-id "${sheets_id}" \
--agent-id "${agent_id}" \
--cmd "${cmd}" \
--args "${args_str}" \
--result "${result}" \
--user "${USER:-}" \
--timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
2>/dev/null
) &
}
COMMAND="${1:-help}"
shift || true
case "${COMMAND}" in
init)
source "${GARC_LIB}/bootstrap.sh"
garc_init "$@"
;;
setup)
python3 "${GARC_DIR}/scripts/garc-setup.py" "${1:-all}" "${@:2}"
;;
bootstrap)
source "${GARC_LIB}/bootstrap.sh"
garc_bootstrap "$@"
;;
status)
source "${GARC_LIB}/bootstrap.sh"
garc_status "$@"
;;
memory)
source "${GARC_LIB}/memory.sh"
garc_memory "$@"
;;
gmail)
source "${GARC_LIB}/gmail.sh"
garc_gmail "$@"
;;
send)
# Shorthand: garc send "<msg>" --to <email>
source "${GARC_LIB}/send.sh"
garc_send "$@"
;;
calendar|cal)
source "${GARC_LIB}/calendar.sh"
garc_calendar "$@"
;;
drive)
source "${GARC_LIB}/drive.sh"
garc_drive "$@"
;;
sheets)
source "${GARC_LIB}/sheets.sh"
garc_sheets "$@"
;;
task)
source "${GARC_LIB}/task.sh"
garc_task "$@"
;;
people|contacts)
source "${GARC_LIB}/people.sh"
garc_people "$@"
;;
approve)
source "${GARC_LIB}/approve.sh"
garc_approve "$@"
;;
agent)
source "${GARC_LIB}/agent.sh"
garc_agent "$@"
;;
auth)
source "${GARC_LIB}/auth.sh"
garc_auth "$@"
;;
heartbeat)
source "${GARC_LIB}/heartbeat.sh"
garc_heartbeat "$@"
;;
kg)
source "${GARC_LIB}/kg.sh"
garc_kg "$@"
;;
ingress)
source "${GARC_LIB}/ingress.sh"
garc_ingress "$@"
;;
daemon)
source "${GARC_LIB}/daemon.sh"
garc_daemon "$@"
;;
audit)
source "${GARC_LIB}/audit.sh"
_garc_audit_log "audit" "$*"
garc_audit "$@"
;;
profile)
source "${GARC_LIB}/profile.sh"
garc_profile "$@"
;;
forms)
source "${GARC_LIB}/forms.sh"
garc_forms "$@"
;;
doctor)
python3 - <<'PY'
import sys, importlib, subprocess
print("GARC Doctor — Environment Check")
print("─" * 40)
# Python version
pv = sys.version_info
status = "✅" if pv >= (3, 10) else "❌"
print(f"{status} Python {pv.major}.{pv.minor}.{pv.micro} (required: >=3.10,<3.13)")
# Required packages
required = [
("googleapiclient", "google-api-python-client"),
("google.auth", "google-auth"),
("google_auth_oauthlib", "google-auth-oauthlib"),
("httplib2", "google-auth-httplib2"),
("requests", "requests"),
("yaml", "pyyaml"),
("dateutil", "python-dateutil"),
("rich", "rich"),
]
print()
print("Dependencies:")
all_ok = True
for module, pkg in required:
try:
importlib.import_module(module)
print(f" ✅ {pkg}")
except ImportError:
print(f" ❌ {pkg} → pip install {pkg}")
all_ok = False
print()
if all_ok:
print("✅ All checks passed.")
else:
print("⚠️ Some packages missing. Run: pip install -r requirements.txt")
sys.exit(1)
PY
;;
help|--help|-h)
usage
;;
*)
echo "garc: unknown command '${COMMAND}'" >&2
echo "Run 'garc --help' for usage." >&2
exit 1
;;
esac