diff --git a/CONTRACT.md b/CONTRACT.md index 35567b8..d420599 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -1,10 +1,15 @@ --- -tier: S +name: steev-contract +tier: T1 status: active owner: jp source: hand +last_reviewed: 2026-05-23 review_by: 2026-08-21 -note: Steev profile contract — this file wins for the Steev profile +description: steev profile behavior contract — what Steev does, doesn't do, edge cases. Tier T1 — this file wins for the steev profile. +depends_on: + - profile-distribution-protocol +note: legacy tier S remapped to T1 per FRONTMATTER-SPEC 2026-05-23. Required fields filled (name, last_reviewed, description) per §7 audit. --- # Steev — Source of Truth diff --git a/credbridge.sh b/credbridge.sh new file mode 100755 index 0000000..763ebb3 --- /dev/null +++ b/credbridge.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# credbridge.sh — resolve credctl credentials into env vars for steev's +# personal-flow tools, then exec the CLI. Secrets NEVER echoed, logged, or +# written to disk. +# +# Usage: credbridge.sh [args...] +# tools: google-workspace | proton-bridge | perplexity +# +# Per PROFILE-DISTRIBUTION-PROTOCOL §3 (shared core, "credbridge" row) and §6 +# (Conventions → Secrets), every profile distribution exposes credentials via +# this verb chain: credctl set → credenv → exec, with secrets per-call only. +# +# This is the personal-assistant variant of the credbridge pattern. Steev's +# cred surface is narrow by design: +# +# - google-workspace: Gmail + Calendar + Contacts (OAuth blob from credctl) +# - proton-bridge: IMAP/SMTP password for the local Proton Bridge T6 +# sidecar — gives Steev access to JP's Proton mail via +# himalaya (cleartext on 127.0.0.1 only) +# - perplexity: Perplexity API key for WebSearch toolset (lightweight +# — most Steev work uses the perplexity MCP instead) +# +# Plan B marketing platforms (WooCommerce, Mailchimp, Meta, GA4, etc.) are OUT +# OF SCOPE here — that's cmo-planb's credbridge. Steev MUST NEVER resolve a +# marketing platform credential. The CLAUDE.md "no access to Plan B marketing +# platform credentials" rule is enforced by omission from this case statement. +# +# Design notes (same as cmo/credbridge.sh — shared core): +# - credctl values read into local vars, exported straight to the child env +# - JSON-valued creds (google-workspace OAuth) parsed via `node -e` reading +# from stdin so the value never lands on argv / process list +# - No `echo $secret`. set +x stays off. + +set -euo pipefail + +# Paths env-overridable for portability; defaults match the JP install. +CREDCTL="${CREDCTL:-/home/svrnty/workspaces/cortex/L6-svrnty.core-credentials/credctl}" +STEEV_LIB="${STEEV_LIB:-/home/svrnty/.hermes/steev}" + +die() { printf '{"error":"%s"}\n' "$1" >&2; exit 1; } + +[ $# -ge 1 ] || die "usage: credbridge.sh [args...]" +TOOL="$1"; shift + +[ -x "$CREDCTL" ] || die "credctl not found/executable at $CREDCTL" + +# cred_raw — print unmasked credential value to stdout. Caller captures +# into a var; value never displayed. Strips the "Value:" header from credctl. +cred_raw() { + "$CREDCTL" get "$1" --unmask 2>/dev/null \ + | sed -n '/^Value:/,$p' | sed '1s/^Value:[[:space:]]*//' +} + +# json_field — extract a string field via node; value never on argv. +json_field() { + printf '%s' "$1" | node -e ' + let s="";process.stdin.on("data",d=>s+=d); + process.stdin.on("end",()=>{try{const o=JSON.parse(s); + const v=o[process.argv[1]];process.stdout.write(v==null?"":String(v)); + }catch(e){process.stdout.write("");}});' "$2" +} + +case "$TOOL" in + google-workspace) + # Gmail Data API + Calendar API + People API all expect a bearer token + # minted from this service-account / OAuth blob. The blob is JSON; we + # export the whole document so the child CLI can introspect scope. + GW_JSON="$(cred_raw google-workspace)" + [ -n "$GW_JSON" ] || die "credctl: google-workspace not set" + export GOOGLE_WORKSPACE_CREDENTIALS_JSON="$GW_JSON" + exec "$@" + ;; + proton-bridge) + # Steev reads JP's Proton inbox via the local Proton Bridge IMAP daemon + # (T6 sidecar — see PROFILE-DISTRIBUTION-PROTOCOL §4.T6). credctl stores + # the bridge password (rotates when JP rotates the bridge). + PB_PASS="$(cred_raw proton-bridge-imap)" + [ -n "$PB_PASS" ] || die "credctl: proton-bridge-imap not set" + export PROTON_BRIDGE_IMAP_PASSWORD="$PB_PASS" + exec "$@" + ;; + perplexity) + # Lightweight WebSearch path. Most Steev research goes through the + # perplexity MCP server (which holds its own key); this credbridge entry + # exists for scripts that need a raw key (rare). + PPL_KEY="$(cred_raw perplexity-api)" + [ -n "$PPL_KEY" ] || die "credctl: perplexity-api not set" + export PERPLEXITY_API_KEY="$PPL_KEY" + exec "$@" + ;; + *) + die "unknown tool: $TOOL (allowed: google-workspace|proton-bridge|perplexity)" + ;; +esac diff --git a/cron/steev-daily-briefing.json.template b/cron/steev-daily-briefing.json.template new file mode 100644 index 0000000..96bea81 --- /dev/null +++ b/cron/steev-daily-briefing.json.template @@ -0,0 +1,40 @@ +{ + "id": "steev-daily-briefing", + "name": "steev-daily-briefing", + "prompt": "Steev daily briefing. DB=${HERMES_HOME}/steev/steev.db, helper=${HERMES_HOME}/steev/steev_db.py.\n1. runtime-get: if agent_runtime.status='halted' → STOP, output \"halted, skipping\", do nothing else.\n2. Aggregate today's context:\n - calendar events from google-workspace (today + tomorrow morning)\n - flagged + unread emails from proton-bridge (himalaya) + google-workspace (gmail)\n - due tasks from apple-reminders + obsidian vault (~/vaults/steev/Tasks/)\n - carried items from steev.db (briefings.carried_over)\n - brief news scan via perplexity (1-2 items only)\n3. Compose a single digest in JP's voice (FR/EN per JP's language preference for that day). Surface only what needs JP's attention.\n4. Persist digest to steev.db (briefings table) as status='draft'.\n5. Output a 5W summary of items surfaced.\n6. NEVER auto-send. NEVER touch business comms surface (that's CEO → CMO).", + "skills": [ + "steev-agent" + ], + "skill": "steev-agent", + "model": null, + "provider": null, + "base_url": null, + "script": null, + "no_agent": false, + "context_from": null, + "schedule": { + "kind": "cron", + "expr": "30 6 * * *", + "display": "06:30 daily" + }, + "schedule_display": "06:30 daily", + "repeat": { + "times": null, + "completed": 0 + }, + "enabled": false, + "state": "paused", + "paused_at": null, + "paused_reason": "installed paused — enable via: hermes cron resume steev-daily-briefing", + "created_at": null, + "next_run_at": null, + "last_run_at": null, + "last_status": null, + "last_error": null, + "last_delivery_error": null, + "deliver": "local", + "origin": null, + "enabled_toolsets": null, + "workdir": "${HERMES_HOME}/steev", + "profile": "steev" +} diff --git a/distribution.yaml b/distribution.yaml new file mode 100644 index 0000000..97462d4 --- /dev/null +++ b/distribution.yaml @@ -0,0 +1,29 @@ +# Hermes profile distribution manifest — native Hermes install contract. +# Used by `hermes profile install`. Distinct from manifest.yaml (our workspace +# convention layered on top — see ../sot/03-PROTOCOLS/PROFILE-DISTRIBUTION-PROTOCOL.md). +name: steev +version: 1.0.0 +description: "Steev — JP's personal AI chief of staff. Daily briefing (calendar + flagged emails + due tasks + carried items + brief news scan), inbox triage, comms drafting in JP's voice (French/English bilingual), and business-task delegation to ceo-planb. Personal-flow manager — no Plan B marketing surface. Sovereign on qwen3.6-35b-a3b." +hermes_requires: ">=0.14.0" +author: "Svrnty / JP " +license: "proprietary" + +# Steev is personal/agnostic per CORTEX-OS-FRAMEWORK §6.1 — no org suffix. +# Profile name = `steev` (not `steev-`) because there is exactly one +# principal: JP. Cloning steev for another principal = rename in +# distribution.yaml only; no other code changes. + +env_requires: [] # credentials provisioned via credctl at install/runtime, never in env + +distribution_owned: + - AGENT.md + - CONTRACT.md + - CLAUDE.md + - README.md + - manifest.yaml + - schema.sql + - install.sh + - credbridge.sh + - validate_access.sh + - skills/ + - cron/ diff --git a/manifest.yaml b/manifest.yaml index cc73e5f..329c0ad 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -1,19 +1,73 @@ # Steev profile distribution manifest — machine-readable identity + install contract. -# Read by install.sh. Convention shared by all profile distributions (see PROFILE-DISTRIBUTION-SPEC.md). -profile: steev -kind: profile-distribution -role: steev +# Read by install.sh. Convention shared by all Hermes profile distributions +# (see ../sot/03-PROTOCOLS/PROFILE-DISTRIBUTION-PROTOCOL.md — the canonical protocol). +profile: steev # Hermes profile name (personal — no org suffix per FRAMEWORK §6.1) +kind: profile-distribution # family marker; steev = personal-assistant reference impl +role: personal-assistant # function — Chief of Staff for one principal (JP) +# org: ~ # intentionally omitted — steev is personal/agnostic version: 1.0.0 -identity: AGENT.md -reference: docs/STEEV-MASTER.md +identity: AGENT.md # WHO (role, mission, boundaries) +contract: CONTRACT.md # behavior contract — tier T1 (this file wins) +reference: docs/STEEV-MASTER.md # full operating source of truth -skills: - - skills/steev-agent # personal assistant orchestrator +skills: # exposed to Hermes via skills.external_dirs (→ /skills) + - skills/steev-agent # orchestrator — daily briefing, inbox triage, comms drafting, + # business delegation to ceo-planb + - skills/proton-tools # Proton Calendar + Email + Contacts (24-tool reference) — + # uses the 3 cortex MCP servers (proton-calendar/-email/-contacts) + +# Role tools = scripts at repo root (the "lib"), reached through credbridge. +# Personal-flow surface only; Plan B marketing CLIs out of scope (cmo-planb owns those). +lib: + - credbridge.sh # credctl → env → google-workspace / proton-bridge / perplexity + - validate_access.sh # PASS / BLOCKED / FAIL per credential per §7 + +# Hermes built-in / external skills Steev reuses but does NOT vendor (per CLAUDE.md +# "reuse existing core skills"). Informational — these come from Hermes' global skills +# tree (~/.hermes/skills/) or external skill libraries the principal already installed. +expected_external_skills: + - google-workspace # Gmail + Calendar + Contacts + - apple-notes # macOS-local via osascript + - apple-reminders # macOS-local via osascript + - obsidian # ~/vaults/steev PKM + - himalaya # IMAP/SMTP via proton-bridge sidecar + - imessage # macOS-local + - perplexity # WebSearch toolset (lightweight; MCP preferred) + +# MCP servers Steev consumes. Names match runtime-prefixed form (mcp__). +optional_tools: + - mcp_proton_calendar # 8-tool Proton Calendar facade + - mcp_proton_email # 10-tool Proton Email facade + - mcp_proton_contacts # 6-tool Proton Contacts facade + - mcp_perplexity # research / WebSearch (key held by MCP server, not credbridge) requires_tools: [terminal, memory_tool] -db: - file: steev.db # runtime state; created from schema.sql; never committed - schema: schema.sql +credentials: # validated by validate_access.sh + - name: google-workspace + purpose: Gmail + Calendar + Contacts read/write for daily briefing + inbox triage + resolved_via: credbridge.sh + - name: proton-bridge-imap + purpose: local Proton Bridge IMAP/SMTP password (himalaya path) + resolved_via: credbridge.sh + - name: perplexity-api + purpose: Perplexity API key for raw WebSearch (MCP path preferred) + resolved_via: credbridge.sh -cron: [] # daily-briefing cron registered disabled at launch +db: + file: steev.db # runtime state; created from schema.sql; never committed + schema: schema.sql # briefings + inbox_items + invocations + agent_runtime + +cron: + - id: steev-daily-briefing + schedule: "30 6 * * *" # 06:30 local — well before JP's start of day + skill: steev-agent + input: { mode: daily-briefing } + disabled_on_install: true # ships disabled per profile protocol §6 (Safety) + template: cron/steev-daily-briefing.json.template + +sovereignty: + llm_model: qwen-local/qwen3.6-35b-a3b + host: dgx-spark + external_api_dependencies: + - perplexity # WebSearch only; build-time research path. Daily briefing scan uses 1-2 items. diff --git a/validate_access.sh b/validate_access.sh new file mode 100755 index 0000000..630b480 --- /dev/null +++ b/validate_access.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# validate_access.sh — report PASS / BLOCKED / FAIL per credential, per +# PROFILE-DISTRIBUTION-PROTOCOL §7 (readiness checklist, "credbridge resolves +# every credential the manifest lists; validate_access reports PASS/BLOCKED/ +# FAIL"). Sourceable from install.sh and standalone. +# +# Usage: validate_access.sh +# Exit code: always 0. Emits one JSON line per credential, suitable for jq / +# log aggregation. +# +# Statuses: +# PASS credctl key set + non-empty +# BLOCKED key absent or empty — actionable: run `credctl set ` +# FAIL credctl itself missing or broken — environmental issue + +set -uo pipefail + +CREDCTL="${CREDCTL:-/home/svrnty/workspaces/cortex/L6-svrnty.core-credentials/credctl}" + +CREDENTIALS=( + google-workspace + proton-bridge-imap + perplexity-api +) + +check() { + local name="$1" status reason + if [ ! -x "$CREDCTL" ]; then + status="FAIL"; reason="credctl not found at $CREDCTL" + elif ! "$CREDCTL" list 2>/dev/null | grep -q "^${name}[[:space:]]"; then + status="BLOCKED"; reason="credctl key not set — run: credctl set ${name}" + elif [ -z "$("$CREDCTL" get "$name" --unmask 2>/dev/null | sed -n '/^Value:/,$p' | sed '1s/^Value:[[:space:]]*//')" ]; then + status="BLOCKED"; reason="key exists but value empty" + else + status="PASS"; reason="present" + fi + printf '{"credential":"%s","status":"%s","reason":"%s"}\n' "$name" "$status" "$reason" +} + +for cred in "${CREDENTIALS[@]}"; do + check "$cred" +done