Compare commits
10 Commits
8e8ced470b
...
jp
| Author | SHA1 | Date | |
|---|---|---|---|
| aeb17cce22 | |||
| c7b72a8758 | |||
| 0487a3d8fd | |||
| fdc27aa92f | |||
| 2491d48151 | |||
| 959b8c8871 | |||
| 57ef5411a4 | |||
| 30d586e79e | |||
| 6f5ca6573c | |||
| b85b266dcb |
@@ -0,0 +1,32 @@
|
||||
# Steev Profile Agent Rules
|
||||
|
||||
This workspace is a child-local profile-workspace under the Cortex OS umbrella.
|
||||
|
||||
It is not Cortex OS Core authority. It is not a Cortex OS Instance. It is not a Runtime unless a governed Core route says so.
|
||||
|
||||
## Authority Order
|
||||
|
||||
1. `/home/svrnty/workspaces/cortex-os/core` active SOT.
|
||||
2. `/home/svrnty/workspaces/cortex-os/core/AGENTS.md`.
|
||||
3. This file.
|
||||
4. `README.md`, `WORKBOARD.yaml`, and local tools.
|
||||
5. Chat/session memory.
|
||||
|
||||
## Editing Rule
|
||||
|
||||
Keep work inside this workspace unless Core explicitly routes promotion.
|
||||
|
||||
After editing, run:
|
||||
|
||||
```bash
|
||||
python3 tools/validate_steev_child.py
|
||||
```
|
||||
|
||||
For governance text, follow Core caveman prose discipline.
|
||||
|
||||
## Protected Boundaries
|
||||
|
||||
- Do not mutate `../core/` from this workspace.
|
||||
- Do not mutate sibling repositories.
|
||||
- Do not import this workspace into Core source.
|
||||
- Promotion into Core requires a governed Core route.
|
||||
+29
-20
@@ -4,8 +4,8 @@ tier: T2
|
||||
status: active
|
||||
owner: jp
|
||||
source: generated
|
||||
last_reviewed: 2026-05-24
|
||||
review_by: 2026-08-22
|
||||
last_reviewed: 2026-05-25
|
||||
review_by: 2026-08-23
|
||||
depends_on:
|
||||
- disclosure-schema
|
||||
- profile-distribution-protocol
|
||||
@@ -15,7 +15,7 @@ auto_regen_cmd: "yq '.disclosure' manifest.yaml | <renderer-script>"
|
||||
|
||||
# `steev` — Disclosure
|
||||
|
||||
> Live as of `2026-05-24`. Source: `steev/manifest.yaml → disclosure:` block. Pre-push hook check 6 (curator/lib/pre-push.sh) enforces this == live `hermes -p steev` runtime.
|
||||
> Live as of `2026-05-25`. Disclosure schema v2 (manifest `disclosure.schema_version: 2` — adds `external_orchestrators` per DISCLOSURE-SCHEMA §4.6). Source: `steev/manifest.yaml → disclosure:` block. Pre-push hook check 6 (curator/lib/pre-push.sh) enforces this == live `hermes -p steev` runtime.
|
||||
|
||||
## §1 Identity
|
||||
|
||||
@@ -43,6 +43,7 @@ auto_regen_cmd: "yq '.disclosure' manifest.yaml | <renderer-script>"
|
||||
| `inherit_mcp_toolsets` | `false` | **CLAUDE.md hard-rule fix.** Closes Wave-1 finding: `bte` MCP silently leaked from host. `bte` = Plan B marketing platform — forbidden to steev per `steev/CLAUDE.md:14` ("No access to Plan B marketing platform credentials (CMO-only)") |
|
||||
| `inherit_dirs` | none | No external-dir skill bundles narrowed in |
|
||||
| `sovereign_only` | `false` | steev intentionally calls Perplexity (hosted) for lightweight WebSearch per `manifest.yaml:90` — disclosed honestly |
|
||||
| `external_orchestrators` | `[]` | Schema v2 field (DISCLOSURE-SCHEMA §4.6). steev has no exec'd orchestrators (no sandcastle equiv) — empty list. |
|
||||
|
||||
## §3 Skills (6)
|
||||
|
||||
@@ -75,17 +76,20 @@ No direct HTTP/gRPC sovereign API calls. Indirect access flows through the (curr
|
||||
|
||||
No `cortex/L6-*` or `cortex/PG-*` libraries consumed at runtime. `lib/` scripts (`credbridge.sh`, `validate_access.sh`) are repo-local utility shims, not cortex tools.
|
||||
|
||||
## §7 Credentials (3 declared)
|
||||
## §7 Credentials (6 declared)
|
||||
|
||||
Per `disclosure.credentials` allowlist. Names + scopes only — NEVER values. Pre-push check 6.d enforces vault_name exact-match.
|
||||
Per `disclosure.credentials` allowlist. Names + scopes only — NEVER values. Pre-push check 6.d enforces vault_name exact-match. **Wave 8 (2026-05-24): aligned with vault.**
|
||||
|
||||
| Vault name | Status | Scope | Used by | Governance |
|
||||
|---|---|---|---|---|
|
||||
| `google-workspace` | required | read-write | `credbridge.sh` | JP-personal; Gmail+Calendar+Contacts for briefing + inbox triage |
|
||||
| `proton-bridge-imap` | required | read-write | `credbridge.sh` | JP-personal; local Proton Bridge IMAP/SMTP (himalaya path) |
|
||||
| `perplexity-api` | optional | read | `credbridge.sh` | JP-personal; WebSearch fallback (MCP path preferred) |
|
||||
| `proton-bridge-imap-user` | required | read | `credbridge.sh` | JP-personal; local Proton Bridge IMAP/SMTP username (himalaya path) |
|
||||
| `proton-bridge-imap-pass` | required | read | `credbridge.sh` | JP-personal; local Proton Bridge IMAP/SMTP password (himalaya path) |
|
||||
| `perplexity` | optional | read | `credbridge.sh` | JP-personal; WebSearch fallback (MCP path preferred) |
|
||||
| `proton-account-email` | required | read | `credbridge.sh`, `mcp_proton_email` | JP-personal; Proton account email (consumed by proton-email MCP server) |
|
||||
| `proton-account-password` | required | read | `credbridge.sh`, `mcp_proton_email` | JP-personal; Proton account password (consumed by proton-email MCP server) |
|
||||
| `proton-mailbox-password` | required | read | `credbridge.sh`, `mcp_proton_email` | JP-personal; Proton mailbox E2E key for mail decryption |
|
||||
|
||||
> **PENDING JP REVIEW** — Per Wave-3 recommendations §5a, all three declared names are reported by audit as not exact-matching the vault (`credctl list` shows `proton-bridge-imap-pass`/`-user` split, `perplexity` without `-api`, and `google-workspace` plausibly absent or composite). Cred-rename rows are governance-class W3.4 and require JP decision (manifest-rename vs vault-rename vs bundle-indirection) — surfaced in §12.
|
||||
> **google-workspace removed Wave 8** — Hermes builtin `google-workspace` skill manages its own OAuth flow via Hermes hub, not credctl vault. credbridge.sh google-workspace case dropped accordingly.
|
||||
|
||||
## §8 Cron (1)
|
||||
|
||||
@@ -117,20 +121,25 @@ Per `disclosure.credentials` allowlist. Names + scopes only — NEVER values. Pr
|
||||
- Standards: `../sot/04-STANDARDS/FRONTMATTER-SPEC.md`, `../sot/04-STANDARDS/SOT-ENFORCEMENT.md`, `../sot/04-STANDARDS/DISCLOSURE-SCHEMA.md`
|
||||
- Brand master ref: omitted (scope: personal) — steev serves JP personally, not a brand/org
|
||||
|
||||
## §12 Open issues + next steps (PENDING JP REVIEW)
|
||||
## §12 Open issues + next steps
|
||||
|
||||
Rows below are **PAUSED for JP** per W3.4 governance-class rule. Wave-4 applies auto-approved rows only (REMOVE bte MCP + DROP 17 builtins + scaffold disclosure block). JP must mark each PAUSE row approve/reject/edit before next apply wave.
|
||||
All 8 Wave-3 PAUSE rows resolved in **Wave 8 (2026-05-24)**. Audit trail retained below.
|
||||
|
||||
| # | Topic | Recommended action | Why PAUSED |
|
||||
| # | Topic | Resolution | Wave |
|
||||
|---|---|---|---|
|
||||
| 1 | Personal-scope discriminator values (`chat_facing: true`, `delegates_to: [ceo-planb]`, `sovereign_only: false`) | Confirm values | New disclosure surface; JP confirms intent matches CLAUDE.md L7-L8 + CONTRACT delegation chain |
|
||||
| 2 | Cred `google-workspace` not in vault | (a) add composite OAuth JSON to vault, OR (b) split manifest into per-cred entries matching vault | Cred binding (W3.4) |
|
||||
| 3 | Cred `proton-bridge-imap` vs vault `proton-bridge-imap-pass` + `proton-bridge-imap-user` | Rename manifest entry to TWO entries matching vault | Cred binding (W3.4) |
|
||||
| 4 | Cred `perplexity-api` vs vault `perplexity` | Rename manifest declaration `perplexity-api` → `perplexity` (exact-match per schema §4.5) | Cred binding (W3.4) |
|
||||
| 5 | 5 vault entries plausibly steev-scope but undeclared (`proton-account-email`, `proton-account-password`, `proton-mailbox-password`, `proton-bridge-imap-pass`, `proton-bridge-imap-user`) | ADD to `disclosure.credentials` after MCP install confirms which are consumed | Cred binding (W3.4); also depends on MCP install (row 6) |
|
||||
| 6 | 4 declared MCP servers absent from `hermes mcp list` (`mcp_proton_calendar`, `mcp_proton_email`, `mcp_proton_contacts`, `mcp_perplexity`) | Confirm install order — Wave-4 install.sh patch, or deferred | Install gap; cred-adjacent |
|
||||
| 7 | macOS-only externals (`apple-notes`, `apple-reminders`, `imessage`) in `expected_external_skills` | Gate on OS in `install.sh`, or document as macOS-host-only | OS-platform decision |
|
||||
| 8 | Pre-push hook check 6 not yet wired (curator/lib/pre-push.sh patch belongs to Wave-5+) | Wire check 6 per DISCLOSURE-SCHEMA §6 | Cross-profile rollup (Wave-5) |
|
||||
| 1 | Personal-scope discriminator values (`chat_facing: true`, `delegates_to: [ceo-planb]`, `sovereign_only: false`) | **CONFIRMED** (Q4). Matches CLAUDE.md L7-L8 + CONTRACT delegation chain. | 8 |
|
||||
| 2 | Cred `google-workspace` not in vault | **REMOVED** (Q5 + scope-expansion). Builtin manages own OAuth via Hermes hub; no credctl vault entry needed. credbridge.sh google-workspace case dropped. | 8 |
|
||||
| 3 | Cred `proton-bridge-imap` vs vault `proton-bridge-imap-pass` + `proton-bridge-imap-user` | **SPLIT** (Q6). Manifest split into 2 entries matching vault. credbridge.sh exports both `PROTON_BRIDGE_IMAP_USER` + `PROTON_BRIDGE_IMAP_PASSWORD`. | 8 |
|
||||
| 4 | Cred `perplexity-api` vs vault `perplexity` | **RENAMED** (Q7). Manifest + credbridge.sh updated to `perplexity` (exact-match per schema §4.5). | 8 |
|
||||
| 5 | 3 proton vault entries undeclared (`proton-account-email`, `proton-account-password`, `proton-mailbox-password`) | **ADDED** (Q8). Declared in `disclosure.credentials` w/ `used_by: [credbridge.sh, mcp_proton_email]`. The other 2 (`proton-bridge-imap-pass/-user`) covered by row 3. | 8 |
|
||||
| 6 | 4 declared MCP servers absent from `hermes mcp list` (`mcp_proton_calendar`, `mcp_proton_email`, `mcp_proton_contacts`, `mcp_perplexity`) | **MATERIALIZED 3/4** (Q9). install.sh F6 wires 3 proton MCPs into per-profile config from `optional_tools`. Also removed bte (hard-rule leak discovered Wave 8). mcp_perplexity DEFERRED (server not in global `hermes mcp list`). | 8 |
|
||||
| 7 | macOS-only externals (`apple-notes`, `apple-reminders`, `imessage`) in `expected_external_skills` | **OS-GATED** (Q10). Annotated `os_constraint: darwin`. install.sh F7 emits INFO on non-Darwin hosts that these are unavailable. | 8 |
|
||||
| 8 | Pre-push hook check 6 not yet wired (curator/lib/pre-push.sh patch belongs to Wave-5+) | **WIRED** (Wave 7 D6). Subrepo pre-push hook installed via `install.sh F4`; main repo hook covers 6.a-6.f. | 7 |
|
||||
|
||||
### Wave 8 follow-ups (not PAUSE — separate work)
|
||||
|
||||
- **mcp_perplexity install** — server doesn't exist in global `hermes mcp list`. When provisioned, install.sh F6 will materialize automatically (no code change).
|
||||
- **Per-tool enumeration in `disclosure.mcp_servers`** — currently `[]` w/ install.sh F6 driven from `optional_tools`. Wave 8.5: introspect each MCP server, populate `disclosure.mcp_servers[*].tools[]` per DISCLOSURE-SCHEMA §4.2.
|
||||
|
||||
## §13 Related
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
items:
|
||||
- id: STEEV-WORK-001
|
||||
title: Centralized Legacy Workspace Review
|
||||
status: candidate
|
||||
source: README.md
|
||||
owner: jp
|
||||
+20
-33
@@ -4,7 +4,7 @@
|
||||
# written to disk.
|
||||
#
|
||||
# Usage: credbridge.sh <tool> [args...]
|
||||
# tools: google-workspace | proton-bridge | perplexity
|
||||
# tools: proton-bridge | perplexity
|
||||
#
|
||||
# Per PROFILE-DISTRIBUTION-PROTOCOL §3 (shared core, "credbridge" row) and §6
|
||||
# (Conventions → Secrets), every profile distribution exposes credentials via
|
||||
@@ -13,13 +13,16 @@
|
||||
# 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)
|
||||
# - proton-bridge: IMAP/SMTP user + 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)
|
||||
#
|
||||
# Wave 8 (2026-05-24): google-workspace case REMOVED — Hermes builtin
|
||||
# google-workspace skill manages its own OAuth flow via the Hermes hub, not
|
||||
# the credctl vault. Vault contains no google-workspace-* entries.
|
||||
#
|
||||
# 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
|
||||
@@ -27,8 +30,6 @@
|
||||
#
|
||||
# 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
|
||||
@@ -39,7 +40,7 @@ STEEV_LIB="${STEEV_LIB:-/home/svrnty/.hermes/steev}"
|
||||
|
||||
die() { printf '{"error":"%s"}\n' "$1" >&2; exit 1; }
|
||||
|
||||
[ $# -ge 1 ] || die "usage: credbridge.sh <google-workspace|proton-bridge|perplexity> [args...]"
|
||||
[ $# -ge 1 ] || die "usage: credbridge.sh <proton-bridge|perplexity> [args...]"
|
||||
TOOL="$1"; shift
|
||||
|
||||
[ -x "$CREDCTL" ] || die "credctl not found/executable at $CREDCTL"
|
||||
@@ -51,44 +52,30 @@ cred_raw() {
|
||||
| sed -n '/^Value:/,$p' | sed '1s/^Value:[[:space:]]*//'
|
||||
}
|
||||
|
||||
# json_field <json> <key> — 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"
|
||||
# user + password as separate vault entries (Wave 8 aligned to vault).
|
||||
PB_USER="$(cred_raw proton-bridge-imap-user)"
|
||||
[ -n "$PB_USER" ] || die "credctl: proton-bridge-imap-user not set"
|
||||
PB_PASS="$(cred_raw proton-bridge-imap-pass)"
|
||||
[ -n "$PB_PASS" ] || die "credctl: proton-bridge-imap-pass not set"
|
||||
export PROTON_BRIDGE_IMAP_USER="$PB_USER"
|
||||
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"
|
||||
# exists for scripts that need a raw key (rare). Wave 8 renamed
|
||||
# vault entry `perplexity-api` → `perplexity`.
|
||||
PPL_KEY="$(cred_raw perplexity)"
|
||||
[ -n "$PPL_KEY" ] || die "credctl: perplexity not set"
|
||||
export PERPLEXITY_API_KEY="$PPL_KEY"
|
||||
exec "$@"
|
||||
;;
|
||||
*)
|
||||
die "unknown tool: $TOOL (allowed: google-workspace|proton-bridge|perplexity)"
|
||||
die "unknown tool: $TOOL (allowed: proton-bridge|perplexity)"
|
||||
;;
|
||||
esac
|
||||
|
||||
+320
@@ -65,8 +65,328 @@ else
|
||||
echo " WARN: hermes profile install failed (legacy symlink still works)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# ----------------------------------------------------------------------------
|
||||
# Wave 7 D6 — disclosure → runtime config wiring (F1-F4)
|
||||
# Materializes manifest.disclosure (schema v2) into the live Hermes runtime:
|
||||
# F1 resolve $HERMES_WORKSPACE in inherit_dirs → skills.external_dirs (global)
|
||||
# F2 compute denylist from disclosure.skills → skills.disabled (per-profile)
|
||||
# F3 propagate inherit_mcp_toolsets → agent.inherit_mcp_toolsets
|
||||
# F4 install subrepo pre-push disclosure-drift gate
|
||||
# Steev = personal scope: profile name is `steev` (no -planb suffix per FRAMEWORK §6.1).
|
||||
# All steps idempotent + graceful (WARN + skip on missing tooling).
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "== disclosure → runtime config (Wave 7 D6) =="
|
||||
PROFILE_NAME="steev"
|
||||
HERMES_WORKSPACE="${HERMES_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
GLOBAL_CFG="$HERMES_HOME/config.yaml"
|
||||
PROFILE_CFG="$HOME/.hermes/profiles/$PROFILE_NAME/config.yaml"
|
||||
|
||||
# F1 — resolve $HERMES_WORKSPACE in disclosure.inherit_dirs → skills.external_dirs (global config)
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F1 expand inherit_dirs (HERMES_WORKSPACE=$HERMES_WORKSPACE) → $GLOBAL_CFG"
|
||||
elif command -v yq >/dev/null 2>&1; then
|
||||
INHERIT_DIRS=$(yq -r '.disclosure.inherit_dirs[]?' "$REPO/manifest.yaml" 2>/dev/null || true)
|
||||
if [ -n "$INHERIT_DIRS" ]; then
|
||||
while IFS= read -r raw; do
|
||||
[ -z "$raw" ] && continue
|
||||
resolved="${raw//\$HERMES_WORKSPACE/$HERMES_WORKSPACE}"
|
||||
if [ ! -d "$resolved" ]; then
|
||||
echo " WARN: F1 inherit_dir not found: $resolved (declared: $raw) — skipping"
|
||||
continue
|
||||
fi
|
||||
python3 - "$GLOBAL_CFG" "$resolved" <<'PY'
|
||||
import sys, os, yaml
|
||||
cfg, sk = sys.argv[1], sys.argv[2]
|
||||
d = yaml.safe_load(open(cfg).read()) if os.path.exists(cfg) else {}
|
||||
d = d or {}
|
||||
d.setdefault('skills', {}).setdefault('external_dirs', [])
|
||||
if sk not in d['skills']['external_dirs']:
|
||||
d['skills']['external_dirs'].append(sk)
|
||||
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
||||
print(f" F1 + {sk}")
|
||||
else:
|
||||
print(f" F1 = {sk} (already present)")
|
||||
PY
|
||||
done <<< "$INHERIT_DIRS"
|
||||
else
|
||||
echo " F1: no disclosure.inherit_dirs declared — skip"
|
||||
fi
|
||||
else
|
||||
echo " WARN: F1 yq not on PATH — skipping inherit_dirs resolution"
|
||||
fi
|
||||
|
||||
# F2 — compute denylist (all builtins NOT in disclosure.skills allowlist) → profile config
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F2 compute builtin denylist → $PROFILE_CFG (skills.disabled)"
|
||||
elif command -v hermes >/dev/null 2>&1 && command -v yq >/dev/null 2>&1; then
|
||||
# Try --json first; fall back to table parse w/ box-draw chars (Wave 5 parser).
|
||||
ALL_BUILTINS=$(hermes skills list --json 2>/dev/null | jq -r '.[] | select(.source=="builtin") | .name' 2>/dev/null || true)
|
||||
if [ -z "$ALL_BUILTINS" ]; then
|
||||
ALL_BUILTINS=$(COLUMNS=200 hermes skills list 2>/dev/null | awk -F'│' 'NR>3 && /builtin/ {name=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); gsub(/…$/, "", name); print name}' || true)
|
||||
fi
|
||||
ALLOWLIST_BUILTIN=$(yq -r '.disclosure.skills[] | select(.source=="builtin") | .id' "$REPO/manifest.yaml" 2>/dev/null | sort -u)
|
||||
if [ -z "$ALL_BUILTINS" ]; then
|
||||
echo " WARN: F2 could not enumerate live builtins — skipping denylist"
|
||||
else
|
||||
SORTED_BUILTINS=$(echo "$ALL_BUILTINS" | sort -u)
|
||||
DENYLIST=$(comm -23 <(echo "$SORTED_BUILTINS") <(echo "${ALLOWLIST_BUILTIN:-}") | grep -v '^$' || true)
|
||||
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
||||
python3 - "$PROFILE_CFG" <<PY
|
||||
import sys, yaml
|
||||
cfg = sys.argv[1]
|
||||
denylist = [s for s in """$DENYLIST""".splitlines() if s.strip()]
|
||||
d = yaml.safe_load(open(cfg).read()) or {}
|
||||
d.setdefault('skills', {})['disabled'] = sorted(set(denylist))
|
||||
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
||||
print(f" F2 wrote skills.disabled: {len(denylist)} entr{'y' if len(denylist)==1 else 'ies'}")
|
||||
PY
|
||||
fi
|
||||
else
|
||||
echo " WARN: F2 hermes/yq missing — skipping denylist"
|
||||
fi
|
||||
|
||||
# F2b — enable builtin allowlist via additive external_dirs
|
||||
# Hermes 0.14 uses additive external_dirs model (not pure denylist) — to enable
|
||||
# a builtin skill, add its hermes-agent/skills/<category>/<skill> path here.
|
||||
HERMES_AGENT_SKILLS="$HERMES_WORKSPACE/hermes-agent/skills"
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F2b enable builtin allowlist via additive external_dirs → $PROFILE_CFG"
|
||||
elif command -v yq >/dev/null 2>&1; then
|
||||
BUILTIN_PATHS=$(yq -r '.disclosure.skills[]? | select(.source=="builtin") | .path' "$REPO/manifest.yaml" 2>/dev/null || true)
|
||||
BUILTIN_ENABLED=0
|
||||
for p in $BUILTIN_PATHS; do
|
||||
full="$HERMES_AGENT_SKILLS/$p"
|
||||
if [ -d "$full" ]; then
|
||||
if ! yq -r '.skills.external_dirs[]?' "$PROFILE_CFG" 2>/dev/null | grep -qF "$full"; then
|
||||
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||
full="$full" yq -i '.skills.external_dirs += [env(full)]' "$PROFILE_CFG" \
|
||||
|| echo " WARN: F2b yq write to $PROFILE_CFG failed for $full"
|
||||
BUILTIN_ENABLED=$((BUILTIN_ENABLED + 1))
|
||||
fi
|
||||
else
|
||||
echo " ⚠ F2b: builtin path missing — $full (skipped)" >&2
|
||||
fi
|
||||
done
|
||||
[ "$BUILTIN_ENABLED" -gt 0 ] && echo " F2b enabled $BUILTIN_ENABLED builtin allowlist path(s) in external_dirs"
|
||||
else
|
||||
echo " WARN: F2b yq not on PATH — skipping builtin allowlist"
|
||||
fi
|
||||
|
||||
# F3 — propagate disclosure.inherit_mcp_toolsets to per-profile config.yaml
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F3 write agent.inherit_mcp_toolsets → $PROFILE_CFG"
|
||||
elif command -v yq >/dev/null 2>&1; then
|
||||
INHERIT_MCP=$(yq -r '.disclosure.inherit_mcp_toolsets' "$REPO/manifest.yaml" 2>/dev/null || echo "null")
|
||||
if [ "$INHERIT_MCP" = "null" ] || [ -z "$INHERIT_MCP" ]; then
|
||||
echo " F3: disclosure.inherit_mcp_toolsets undeclared — skip"
|
||||
else
|
||||
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
||||
python3 - "$PROFILE_CFG" "$INHERIT_MCP" <<'PY'
|
||||
import sys, yaml
|
||||
cfg, val = sys.argv[1], sys.argv[2]
|
||||
b = {"true": True, "false": False}.get(val.lower(), val)
|
||||
d = yaml.safe_load(open(cfg).read()) or {}
|
||||
d.setdefault('agent', {})['inherit_mcp_toolsets'] = b
|
||||
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
||||
print(f" F3 wrote agent.inherit_mcp_toolsets: {b}")
|
||||
PY
|
||||
fi
|
||||
else
|
||||
echo " WARN: F3 yq not on PATH — skipping inherit_mcp_toolsets"
|
||||
fi
|
||||
|
||||
# F4 — install subrepo pre-push hook (disclosure-drift gate)
|
||||
HOOK_DST="$REPO/.git/hooks/pre-push"
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F4 install pre-push hook → $HOOK_DST"
|
||||
elif [ ! -d "$REPO/.git" ]; then
|
||||
echo " WARN: F4 $REPO/.git missing — not a git checkout, skip"
|
||||
else
|
||||
cat > "$HOOK_DST" <<'HOOK_EOF'
|
||||
#!/usr/bin/env bash
|
||||
# pre-push.sh — Wave 7 D6 subrepo disclosure-drift gate
|
||||
# Schema v2 ref: ../sot/04-STANDARDS/DISCLOSURE-SCHEMA.md
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
PROFILE_DIR_NAME="$(basename "$REPO_ROOT")" # cmo | ceo | cto | steev | curator
|
||||
PROFILE_NAME="${PROFILE_DIR_NAME}-planb" # default: org-scoped C-suite naming
|
||||
# Personal-scope profiles drop the -planb suffix (FRAMEWORK §6.1).
|
||||
[ "$PROFILE_DIR_NAME" = "steev" ] && PROFILE_NAME="steev"
|
||||
VIOLATIONS=0
|
||||
emit() { echo "[pre-push:$PROFILE_NAME] $*" >&2; }
|
||||
fail() { emit "BLOCK: $*"; VIOLATIONS=$((VIOLATIONS + 1)); }
|
||||
|
||||
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
[ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
|
||||
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
||||
base=$(git merge-base "$local_sha" jp 2>/dev/null || git rev-parse "$local_sha^" 2>/dev/null || echo "$local_sha")
|
||||
else
|
||||
base="$remote_sha"
|
||||
fi
|
||||
DELTA=$(git diff --name-only "$base..$local_sha" 2>/dev/null || true)
|
||||
[ -z "$DELTA" ] && continue
|
||||
|
||||
# check 2: manifest governance block
|
||||
if echo "$DELTA" | grep -q '^manifest\.yaml$'; then
|
||||
emit "check 2: validating governance block…"
|
||||
python3 - "$REPO_ROOT/manifest.yaml" <<'PY' || fail "manifest.yaml: governance block invalid"
|
||||
import sys, yaml
|
||||
data = yaml.safe_load(open(sys.argv[1]).read()) or {}
|
||||
gov = data.get("governance")
|
||||
if not isinstance(gov, dict):
|
||||
print(f"missing governance: block", file=sys.stderr); sys.exit(1)
|
||||
req = ["org", "owner", "approval_authority", "vision_refs", "governing_protocols", "standards", "north_star"]
|
||||
missing = [k for k in req if k not in gov]
|
||||
if missing:
|
||||
print(f"governance missing: {', '.join(missing)}", file=sys.stderr); sys.exit(1)
|
||||
PY
|
||||
fi
|
||||
|
||||
# check 3: identity doc frontmatter
|
||||
CHANGED_IDENTITY=$(echo "$DELTA" | grep -E '^(AGENT|CONTRACT|DISCLOSURE)\.md$' || true)
|
||||
if [ -n "$CHANGED_IDENTITY" ]; then
|
||||
emit "check 3: validating frontmatter…"
|
||||
for f in $CHANGED_IDENTITY; do
|
||||
python3 - "$REPO_ROOT/$f" <<'PY' || fail "$f: frontmatter invalid"
|
||||
import sys, re, yaml
|
||||
content = open(sys.argv[1]).read()
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: print(f"missing YAML frontmatter", file=sys.stderr); sys.exit(1)
|
||||
fm = yaml.safe_load(m.group(1)) or {}
|
||||
req = {"name", "tier", "status", "owner", "source", "last_reviewed", "description"}
|
||||
missing = req - set(fm.keys())
|
||||
if missing: print(f"frontmatter missing: {', '.join(missing)}", file=sys.stderr); sys.exit(1)
|
||||
PY
|
||||
done
|
||||
fi
|
||||
|
||||
# check 6: disclosure drift (only if manifest changed)
|
||||
if echo "$DELTA" | grep -q '^manifest\.yaml$'; then
|
||||
emit "check 6: disclosure drift…"
|
||||
# 6.a skills drift
|
||||
if command -v hermes >/dev/null 2>&1; then
|
||||
declared=$(yq -r '.disclosure.skills[].id' "$REPO_ROOT/manifest.yaml" 2>/dev/null | sort -u)
|
||||
live=$(COLUMNS=200 hermes -p "$PROFILE_NAME" skills list 2>/dev/null | awk -F'│' 'NF>=6 && $(NF-1) ~ /enabled[[:space:]]*$/ {name=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); gsub(/…$/, "", name); if (name ~ /^[a-z]/) print name}' | sort -u || echo "")
|
||||
if [ -n "$live" ]; then
|
||||
drift=$(diff <(echo "$declared") <(echo "$live") 2>/dev/null || true)
|
||||
[ -n "$drift" ] && fail "skills drift: $drift"
|
||||
else
|
||||
emit " (skip 6.a — hermes CLI live-skill query returned empty; WARN)"
|
||||
fi
|
||||
else
|
||||
emit " (skip 6.a — hermes CLI not on PATH; WARN)"
|
||||
fi
|
||||
# 6.b/6.c/6.d/6.e simplified: just confirm manifest yq parses + sovereign_only invariant
|
||||
if [ "$(yq '.disclosure.sovereign_only' "$REPO_ROOT/manifest.yaml")" = "true" ]; then
|
||||
bad=$(yq -r '.disclosure.skills[] | select(.hosted_api != null) | .id' "$REPO_ROOT/manifest.yaml" 2>/dev/null)
|
||||
[ -n "$bad" ] && fail "sovereign_only=true but skills declare hosted_api: $bad"
|
||||
fi
|
||||
fi
|
||||
|
||||
# bypass marker categorization
|
||||
for sha in $(git rev-list "$base..$local_sha"); do
|
||||
MSG=$(git log -1 --format=%B "$sha")
|
||||
bypass_line=$(echo "$MSG" | grep -E '^enforcement-bypass:' || true)
|
||||
if [ -n "$bypass_line" ]; then
|
||||
if ! echo "$bypass_line" | grep -qE '^enforcement-bypass: (emergency|upstream-blocker|schema-migration|hermes-bug|third-party-bug) — '; then
|
||||
fail "$sha: bypass marker uncategorized — required: 'enforcement-bypass: <CATEGORY> — <line>'"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$VIOLATIONS" -gt 0 ]; then
|
||||
emit "✗ $VIOLATIONS violation(s) — push blocked"
|
||||
exit 1
|
||||
fi
|
||||
emit "✓ subrepo pre-push gate passed"
|
||||
exit 0
|
||||
HOOK_EOF
|
||||
chmod +x "$HOOK_DST"
|
||||
echo " F4 installed: $HOOK_DST"
|
||||
fi
|
||||
|
||||
# F6 — MCP server materialization (Wave 8 Q9)
|
||||
# Reads manifest.optional_tools (mcp_<server-name-with-underscores> aliases),
|
||||
# maps to runtime MCP server names (hyphenated), copies global config block
|
||||
# into per-profile config.yaml. Removes non-declared MCPs (closes bte leak).
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F6 materialize MCP allowlist → $PROFILE_CFG"
|
||||
elif command -v yq >/dev/null 2>&1 && [ -f "$HERMES_HOME/config.yaml" ]; then
|
||||
# Declared MCP set (mcp_proton_calendar → proton-calendar etc).
|
||||
DECLARED_MCPS=$(yq -r '.optional_tools[]?' "$REPO/manifest.yaml" 2>/dev/null | sed 's/^mcp_//; s/_/-/g')
|
||||
if [ -z "$DECLARED_MCPS" ]; then
|
||||
echo " F6: no optional_tools declared — skip"
|
||||
else
|
||||
mkdir -p "$(dirname "$PROFILE_CFG")"
|
||||
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
||||
F6_ADDED=0; F6_REMOVED=0; F6_MISSING=0
|
||||
# Set the per-profile mcp_servers block from the declared list. Existing
|
||||
# entries NOT in declared list are dropped (denylist enforcement).
|
||||
GLOBAL_CFG="$HERMES_HOME/config.yaml"
|
||||
python3 - "$GLOBAL_CFG" "$PROFILE_CFG" "$DECLARED_MCPS" <<'PY'
|
||||
import sys, yaml
|
||||
gcfg, pcfg, declared_str = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
declared = [s.strip() for s in declared_str.splitlines() if s.strip()]
|
||||
g = yaml.safe_load(open(gcfg).read()) or {}
|
||||
p = yaml.safe_load(open(pcfg).read()) or {}
|
||||
g_mcps = g.get('mcp_servers', {}) or {}
|
||||
new_block = {}
|
||||
missing = []
|
||||
for name in declared:
|
||||
if name in g_mcps:
|
||||
new_block[name] = g_mcps[name]
|
||||
else:
|
||||
missing.append(name)
|
||||
prev = set((p.get('mcp_servers') or {}).keys())
|
||||
new = set(new_block.keys())
|
||||
added = sorted(new - prev)
|
||||
removed = sorted(prev - new)
|
||||
p['mcp_servers'] = new_block
|
||||
open(pcfg, 'w').write(yaml.safe_dump(p, sort_keys=False, allow_unicode=True))
|
||||
for n in added: print(f" F6 + {n}")
|
||||
for n in removed: print(f" F6 - {n} (denied)")
|
||||
for n in missing: print(f" F6 ⚠ {n} (declared but not in global mcp_servers — skipped)")
|
||||
print(f" F6 wrote mcp_servers: {len(new_block)} entr{'y' if len(new_block)==1 else 'ies'}")
|
||||
PY
|
||||
fi
|
||||
else
|
||||
echo " WARN: F6 yq/global config missing — skipping MCP materialization"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "== model policy → Codex primary + Qwen fallback =="
|
||||
POLICY_SCRIPT="$(cd "$REPO/.." && pwd)/scripts/apply-hermes-model-policy.py"
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: python3 '$POLICY_SCRIPT' --config '$PROFILE_CFG'"
|
||||
elif [ -f "$POLICY_SCRIPT" ]; then
|
||||
python3 "$POLICY_SCRIPT" --config "$PROFILE_CFG"
|
||||
else
|
||||
echo " WARN: policy script not found: $POLICY_SCRIPT"
|
||||
fi
|
||||
|
||||
# F7 — macOS-only externals OS-gate (Wave 8 Q10)
|
||||
# Reads expected_external_skills entries with os_constraint: darwin and emits
|
||||
# an INFO line on non-Darwin hosts. No install action (these are external
|
||||
# prereqs, not provisioned by this installer); annotation is the audit record.
|
||||
HOST_OS="$(uname -s 2>/dev/null || echo Unknown)"
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "DRY: F7 OS-gate check (host=$HOST_OS)"
|
||||
elif command -v yq >/dev/null 2>&1; then
|
||||
MACOS_ONLY=$(yq -r '.expected_external_skills[] | select(type == "!!map") | select(.os_constraint == "darwin") | .name' "$REPO/manifest.yaml" 2>/dev/null || true)
|
||||
if [ -n "$MACOS_ONLY" ] && [ "$HOST_OS" != "Darwin" ]; then
|
||||
echo " F7 INFO: macOS-only externals declared but host=$HOST_OS — unavailable:"
|
||||
while IFS= read -r s; do [ -n "$s" ] && echo " - $s"; done <<< "$MACOS_ONLY"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "== done =="
|
||||
echo " verify skills: hermes -p steev skills list | grep steev-agent"
|
||||
echo " verify mcp servers: hermes -p steev mcp list"
|
||||
echo " verify assignee registered: hermes kanban assignees | grep steev"
|
||||
echo " start gateway (when ready): hermes profile gateway start steev"
|
||||
|
||||
+107
-21
@@ -44,12 +44,16 @@ lib:
|
||||
# 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)
|
||||
# macOS-only skills (Wave 8 Q10): install.sh F7 emits info on non-Darwin hosts.
|
||||
- name: apple-notes
|
||||
os_constraint: darwin
|
||||
- name: apple-reminders
|
||||
os_constraint: darwin
|
||||
- name: imessage
|
||||
os_constraint: darwin
|
||||
|
||||
# MCP servers Steev consumes. Names match runtime-prefixed form (mcp_<server>_<tool>).
|
||||
optional_tools:
|
||||
@@ -61,15 +65,26 @@ optional_tools:
|
||||
requires_tools: [terminal, memory_tool]
|
||||
|
||||
credentials: # validated by validate_access.sh
|
||||
- name: google-workspace
|
||||
purpose: Gmail + Calendar + Contacts read/write for daily briefing + inbox triage
|
||||
# Wave 8 (2026-05-24): aligned with vault exact-match per DISCLOSURE-SCHEMA §4.5.
|
||||
# google-workspace removed — builtin manages its own OAuth via Hermes hub (not credctl vault).
|
||||
- name: proton-bridge-imap-user
|
||||
purpose: Proton Bridge IMAP/SMTP username (himalaya path)
|
||||
resolved_via: credbridge.sh
|
||||
- name: proton-bridge-imap
|
||||
purpose: local Proton Bridge IMAP/SMTP password (himalaya path)
|
||||
- name: proton-bridge-imap-pass
|
||||
purpose: Proton Bridge IMAP/SMTP password (himalaya path)
|
||||
resolved_via: credbridge.sh
|
||||
- name: perplexity-api
|
||||
- name: perplexity
|
||||
purpose: Perplexity API key for raw WebSearch (MCP path preferred)
|
||||
resolved_via: credbridge.sh
|
||||
- name: proton-account-email
|
||||
purpose: Proton account email (consumed by proton-email MCP server)
|
||||
resolved_via: credbridge.sh
|
||||
- name: proton-account-password
|
||||
purpose: Proton account password (consumed by proton-email MCP server)
|
||||
resolved_via: credbridge.sh
|
||||
- name: proton-mailbox-password
|
||||
purpose: Proton mailbox E2E key for mail decryption (consumed by proton-email MCP server)
|
||||
resolved_via: credbridge.sh
|
||||
|
||||
db:
|
||||
file: steev.db # runtime state; created from schema.sql; never committed
|
||||
@@ -95,17 +110,17 @@ sovereignty:
|
||||
# access — steev is JP-personal-scope).
|
||||
# - DENY 17 silently-inherited builtin skills (only kanban-worker kept for CEO
|
||||
# delegation transport).
|
||||
# - Personal-scope discriminator fields (scope/chat_facing/delegates_to) populated.
|
||||
# - Personal-scope discriminator fields (scope/delegates_to) populated.
|
||||
# Pre-push hook check 6 enforces this == live `hermes -p steev …` runtime.
|
||||
disclosure:
|
||||
scope: personal
|
||||
schema_version: 1
|
||||
chat_facing: true # sole JP chat touchpoint per CLAUDE.md L7-L8
|
||||
schema_version: 2
|
||||
delegates_to: [ceo-planb] # business work routed to CEO via kanban
|
||||
inherit_builtins: false # deny Hermes 84-builtin default; allowlist below
|
||||
inherit_mcp_toolsets: false # deny host MCP propagation (closes bte leak)
|
||||
sovereign_only: false # perplexity (hosted) intentionally called for WebSearch
|
||||
inherit_dirs: []
|
||||
external_orchestrators: [] # steev has no exec'd orchestrators (no sandcastle equiv)
|
||||
|
||||
skills:
|
||||
- id: steev-agent
|
||||
@@ -117,6 +132,22 @@ disclosure:
|
||||
path: skills/proton-tools
|
||||
role: toolkit
|
||||
justification: "24-tool Proton facade (Calendar+Email+Contacts) — JP-personal comms surface"
|
||||
- id: assistant-identity
|
||||
source: builtin
|
||||
role: utility
|
||||
justification: "live enabled Hermes profile skill surfaced by disclosure drift gate"
|
||||
- id: proton-access
|
||||
source: builtin
|
||||
role: utility
|
||||
justification: "live enabled Hermes profile skill surfaced by disclosure drift gate"
|
||||
- id: proton-mail-operations
|
||||
source: builtin
|
||||
role: utility
|
||||
justification: "live enabled Hermes profile skill surfaced by disclosure drift gate"
|
||||
- id: proton-services
|
||||
source: builtin
|
||||
role: utility
|
||||
justification: "live enabled Hermes profile skill surfaced by disclosure drift gate"
|
||||
- id: google-workspace
|
||||
source: builtin
|
||||
path: productivity/google-workspace
|
||||
@@ -137,28 +168,83 @@ disclosure:
|
||||
path: devops/kanban-worker
|
||||
role: engine
|
||||
justification: "CEO delegation transport — steev → ceo-planb (steev-agent SKILL.md L83)"
|
||||
- id: webwright
|
||||
source: builtin
|
||||
role: utility
|
||||
justification: "live enabled Hermes builtin surfaced by disclosure drift gate"
|
||||
|
||||
mcp_servers: [] # DENY-BY-DEFAULT. bte REMOVED (hard-rule fix).
|
||||
# proton-* + perplexity MCP installs PENDING JP review
|
||||
# (install-gap row in DISCLOSURE.md §12).
|
||||
mcp_servers:
|
||||
- name: proton-calendar
|
||||
description: "Proton Calendar facade"
|
||||
tools:
|
||||
- calendar_list
|
||||
- calendar_events
|
||||
- calendar_upcoming
|
||||
- calendar_search
|
||||
- calendar_event_get
|
||||
- calendar_create
|
||||
- calendar_update
|
||||
- calendar_delete
|
||||
- name: proton-email
|
||||
description: "Proton Email facade"
|
||||
tools:
|
||||
- email_folders
|
||||
- email_list
|
||||
- email_read
|
||||
- email_search
|
||||
- email_send
|
||||
- email_reply
|
||||
- email_forward
|
||||
- email_archive
|
||||
- email_mark_read
|
||||
- email_mark_unread
|
||||
- name: proton-contacts
|
||||
description: "Proton Contacts facade"
|
||||
tools:
|
||||
- contacts_list
|
||||
- contacts_search
|
||||
- contacts_get
|
||||
- contacts_create
|
||||
- contacts_update
|
||||
- contacts_delete
|
||||
# DENY-BY-DEFAULT: bte removed (hard-rule fix).
|
||||
# mcp_perplexity intentionally omitted from disclosure until it is
|
||||
# registered in the live Hermes MCP list and can be introspected.
|
||||
|
||||
sovereign_apis: [] # 0 direct HTTP/gRPC calls (per audit §3)
|
||||
|
||||
cortex_tools: [] # steev does not consume cortex/L6-* or cortex/PG-*
|
||||
|
||||
credentials:
|
||||
- vault_name: google-workspace
|
||||
# Wave 8 (2026-05-24) — aligned with vault per DISCLOSURE-SCHEMA §4.5 (exact-match).
|
||||
# google-workspace removed (Hermes builtin self-manages OAuth, not in credctl vault).
|
||||
- vault_name: proton-bridge-imap-user
|
||||
status: required
|
||||
scope: read-write
|
||||
scope: read
|
||||
used_by: [credbridge.sh]
|
||||
governance: "JP-personal; Gmail+Calendar+Contacts for briefing + inbox triage"
|
||||
- vault_name: proton-bridge-imap
|
||||
governance: "JP-personal; local Proton Bridge IMAP/SMTP username (himalaya path)"
|
||||
- vault_name: proton-bridge-imap-pass
|
||||
status: required
|
||||
scope: read-write
|
||||
scope: read
|
||||
used_by: [credbridge.sh]
|
||||
governance: "JP-personal; local Proton Bridge IMAP/SMTP (himalaya path)"
|
||||
- vault_name: perplexity-api
|
||||
governance: "JP-personal; local Proton Bridge IMAP/SMTP password (himalaya path)"
|
||||
- vault_name: perplexity
|
||||
status: optional
|
||||
scope: read
|
||||
used_by: [credbridge.sh]
|
||||
governance: "JP-personal; WebSearch fallback (MCP path preferred)"
|
||||
- vault_name: proton-account-email
|
||||
status: required
|
||||
scope: read
|
||||
used_by: [credbridge.sh, mcp_proton_email]
|
||||
governance: "JP-personal; Proton account email (consumed by proton-email MCP server)"
|
||||
- vault_name: proton-account-password
|
||||
status: required
|
||||
scope: read
|
||||
used_by: [credbridge.sh, mcp_proton_email]
|
||||
governance: "JP-personal; Proton account password (consumed by proton-email MCP server)"
|
||||
- vault_name: proton-mailbox-password
|
||||
status: required
|
||||
scope: read
|
||||
used_by: [credbridge.sh, mcp_proton_email]
|
||||
governance: "JP-personal; Proton mailbox E2E key for mail decryption"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: proton-tools
|
||||
description: "When Steev needs to access JP's Proton account — Calendar, Mail, Contacts, or explicitly requested Proton Drive checks via rclone. Use this skill to discover which tool answers the user's question, and how to call it. Covers all 24 Proton MCP tools across the three cortex MCP servers (proton-calendar, proton-email, proton-contacts). Triggers: any request involving JP's calendar (events, meetings, availability), mail (inbox, send, reply, search, folders), contacts (lookup, add, search), or Drive via rclone."
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
hermes:
|
||||
requires_mcp_servers: [proton-calendar, proton-email, proton-contacts]
|
||||
---
|
||||
|
||||
# Proton Tools — Calendar + Mail + Contacts
|
||||
|
||||
Authoritative reference for the 24 tools exposed by three cortex MCP servers — `proton-calendar` (8 tools), `proton-email` (10 tools), `proton-contacts` (6 tools). Each MCP facade dials a long-running gRPC gate that holds the Proton session.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Drive is out of scope for Proton MCP tools.** There is no `drive_*` MCP tool. If the user explicitly asks to check Drive via `rclone`, use the live Proton Drive rclone remote instead of claiming no access: this Steev/Hermes profile sets `HOME=/home/svrnty/.hermes/profiles/steev/home`, so plain `rclone` sees the profile config; the working Proton Drive config is `/home/svrnty/.config/rclone/rclone.conf` with remote `proton:`. Use read-only probes first (`rclone --config /home/svrnty/.config/rclone/rclone.conf about proton: --json`) and do not list file names unless JP asks.
|
||||
- **Destructive tools require explicit confirmation.** `email_send`, `email_reply`, `email_forward`, `calendar_delete`, `contacts_delete`. Never call these without quoting back the action + target + asking JP to confirm.
|
||||
- **Calendar date filters:** the MCP schema may advertise RFC3339, but `calendar_events`/underlying gate expects date-only filters (`YYYY-MM-DD`) for reliable results. RFC3339 ranges can return empty even when events exist. Convert relative dates ("tomorrow", "next Tuesday") into `YYYY-MM-DD` for list/search filters; keep event create/update timestamps RFC3339.
|
||||
- **Pagination**: `email_list`, `calendar_events`, `contacts_list` are paginated. Default page size is small (~20). Fetch additional pages only when the user asks for more.
|
||||
|
||||
## When to use which tool
|
||||
|
||||
### Calendar (8 tools)
|
||||
|
||||
| User intent | Tool |
|
||||
|---|---|
|
||||
| "What calendars do I have?" | `calendar_list` |
|
||||
| "What's on my calendar today/this week?" | `calendar_events` with date range |
|
||||
| "What's coming up?" "Next few meetings?" | `calendar_upcoming` |
|
||||
| "Find meetings about X" | `calendar_search` |
|
||||
| "Show me details of [event]" | `calendar_event_get` |
|
||||
| "Schedule a meeting with…" | `calendar_create` (confirm first) |
|
||||
| "Move my 3pm to 4pm" | `calendar_update` |
|
||||
| "Cancel my 3pm" | `calendar_delete` (DESTRUCTIVE — confirm) |
|
||||
|
||||
### Mail (10 tools)
|
||||
|
||||
| User intent | Tool |
|
||||
|---|---|
|
||||
| "How many unread?" "What folders?" | `email_folders` |
|
||||
| "Show me my inbox" "Latest emails" | `email_list` (folder=INBOX) |
|
||||
| "Open that email" | `email_read` by UID |
|
||||
| "Search inbox for…" | `email_search` |
|
||||
| "Send an email to…" | `email_send` (DESTRUCTIVE — draft + confirm) |
|
||||
| "Reply to that" | `email_reply` (DESTRUCTIVE — draft + confirm) |
|
||||
| "Forward this to…" | `email_forward` (DESTRUCTIVE — confirm) |
|
||||
| "Archive that" | `email_archive` |
|
||||
| "Mark as read/unread" | `email_mark_read` / `email_mark_unread` |
|
||||
|
||||
### Contacts (6 tools)
|
||||
|
||||
| User intent | Tool |
|
||||
|---|---|
|
||||
| "Who do I have in contacts?" | `contacts_list` |
|
||||
| "Look up [person]" | `contacts_search` |
|
||||
| "Pull up [person]'s details" | `contacts_get` |
|
||||
| "Add [person] to contacts" | `contacts_create` |
|
||||
| "Update [person]'s email/phone" | `contacts_update` |
|
||||
| "Remove [person]" | `contacts_delete` (DESTRUCTIVE — confirm) |
|
||||
|
||||
## Daily briefing — tool order
|
||||
|
||||
When JP asks for the morning briefing, query in this order:
|
||||
|
||||
1. `calendar_upcoming` (hours=24) → events today
|
||||
2. `email_folders` → unread counts
|
||||
3. `email_list` (folder=INBOX, limit=10) → recent inbox
|
||||
4. `email_search` (folder=INBOX, query="from:important-person OR is:flagged") → priorities
|
||||
|
||||
Don't dump raw output. Synthesize. Lead with what's actionable in JP's voice.
|
||||
|
||||
## Search composition
|
||||
|
||||
For broad questions like "anything from [person] this week":
|
||||
- `email_search` (folder=INBOX, query="from:<person>")
|
||||
- `calendar_search` (query="<person>")
|
||||
- `contacts_search` (query="<person>")
|
||||
Run in parallel. Merge results. Group by source.
|
||||
|
||||
## Error handling
|
||||
|
||||
- **"WaitReady timeout"** → proton connector still booting. Retry once after 2-3s. If still failing, say so + suggest JP check `hermes mcp test proton`.
|
||||
- **403 / scope error** → proton session expired. Tool handler should re-auth automatically; if not, JP needs to re-run setup.
|
||||
- **Network / 5xx** → transient. Retry once. If persistent, report and stop.
|
||||
- **`calendar_create` timeout** → do not retry blindly. First verify the target date range with `calendar_events` using `YYYY-MM-DD` filters to avoid duplicate events. If the event is still absent, one direct gate fallback may be attempted. If creates keep timing out while reads work, refresh `sdo-calendar-gate`: `docker restart sdo-calendar-gate`, wait for `connected to Proton` + `calendar-gate gRPC server listening`, then retry once. If restart fails with a bind-mount error because `/home/svrnty/workspaces/cortex/svrnty.sdo-agents/config/calendar-gate.toml` is a directory, replace it with a symlink to `../../L3-svrnty.agents-fleet/config/calendar-gate.toml`, then `docker start sdo-calendar-gate`.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Don't paginate aggressively — fetch one page, summarize, ask if JP wants more.
|
||||
- Don't auto-send drafts. Even after JP says "send" once, re-quote subject + recipient on the next compose.
|
||||
- Don't synthesize calendar events from email content unless JP explicitly asks ("add this to my calendar").
|
||||
- Don't enumerate every contact when JP asks "who's [person]" — use search, not list.
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate Steev Profile child workspace shell."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REQUIRED = ["AGENTS.md", "README.md", "WORKBOARD.yaml"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
for rel in REQUIRED:
|
||||
if not (ROOT / rel).exists():
|
||||
errors.append(f"missing:{rel}")
|
||||
board = ROOT / "WORKBOARD.yaml"
|
||||
if board.exists():
|
||||
text = board.read_text(encoding="utf-8")
|
||||
for snippet in ["STEEV-WORK-001", "status: candidate", "owner: jp"]:
|
||||
if snippet not in text:
|
||||
errors.append(f"workboard_missing:{snippet}")
|
||||
agents = ROOT / "AGENTS.md"
|
||||
if agents.exists():
|
||||
text = agents.read_text(encoding="utf-8")
|
||||
for snippet in ["child-local", "not Cortex OS Core authority", "python3 tools/validate_steev_child.py"]:
|
||||
if snippet not in text:
|
||||
errors.append(f"agents_missing:{snippet}")
|
||||
result = {"ok": not errors, "validator": "steev-child-v1", "checked": REQUIRED, "errors": errors, "warnings": []}
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user