diff --git a/add-roles-permissions/CHANGELOG.md b/add-roles-permissions/CHANGELOG.md index ae8eb82..c8398f4 100644 --- a/add-roles-permissions/CHANGELOG.md +++ b/add-roles-permissions/CHANGELOG.md @@ -10,6 +10,32 @@ All notable changes to `deploy-security-protocol` are documented here. --- +## v3.3.0 — 2026-06-12 + +Remove the Neon dependency from Motion-web identity resolution. Installs no longer require — or ask for — any production database credential. + +### Why + +The June 2026 runtime change added a local daemon conversation store (`/daemon/conversation-store/conversations.db` plus a `$CONVERSATION_ID` env var), which makes Motion-web identity resolvable locally. The fleet's resolvers were updated to use it in the June 10–11 rollout, but this repo's install path was never updated to match — it still asked for a runtime database secret at install time. A fresh install should never prompt for a database credential. This release brings the repo in line with the fleet. + +Version is 3.3.0 (not 3.2.x) because PR #115 has v3.2.0 claimed for the attribution-layer removal. + +### Changed + +- **`motion-whoami.sh` resolves locally.** Reads `userEmail` from `/daemon/conversation-store/conversations.db` (via `sqlite3 -readonly -json`) using the runtime-injected `$CONVERSATION_ID`, then resolves against `organization-map.json`. No network, no secrets. The script body matches the resolver proven on ~10 customer VMs in the June 2026 fleet rollout (compatibility variant, SHA `946bfa2a043944075ba0ad415faa35a28d668cca07131fbeea551244e5f85d01`), adapted only to this package's naming: `organization-map.json` / `RUNNETH_ORG_MAP` and `/agent/brain/team/` home bases (the fleet uses `workspace-map.json` / `/agent/brain/members/`). +- **Fail-loud semantics kept** (decided 6/2): on resolver failure — `$CONVERSATION_ID` unset, daemon DB missing, no email for the conversation — the script exits non-zero with error JSON on stderr, and the permissions layer treats unknown identity as no-writes. +- **Legacy-map compatibility:** the resolver reads `.motionUserEmails` and falls back to legacy `.motionEmails`, and keeps both in sync on provision, so older installs keep resolving existing users after upgrade. +- **Phase 1 Look-around 6** no longer checks for a runtime secret and no longer hard-stops fresh installs. It now checks the two things the resolver needs — daemon DB present, `sqlite3` available — both on every current VM by default. If either is missing (outdated image), warn and proceed; Motion-web users resolve as unknown until the runtime is updated. +- **Prerequisites** (SKILL.md + README): `NEON_DATABASE_URL` dropped; `sqlite3` added. + +### Removed + +- **`motion-whoami-neon.py`** and its install step. Phase 5 Step 4 now tells the agent to delete a leftover copy if one exists from a prior install. + +Refs: PDEC-7817. + +--- + ## v3.1.5 — 2026-06-03 Slim pass. 73 lines trimmed from SKILL.md (1,137 → 1,064) without dropping a single rule, look-around, phase, or piece of educational content. Pure deduplication and prose tightening. diff --git a/add-roles-permissions/README.md b/add-roles-permissions/README.md index 2d7f5ef..f8239ef 100644 --- a/add-roles-permissions/README.md +++ b/add-roles-permissions/README.md @@ -15,7 +15,7 @@ The skill is built from six primitives. The install conversation discovers which 3. **Writers per space.** Each space has a writer rule: `everyone`, `specific`, or `admins_only`. Default is `everyone`. Anything in `everyone` is implicit and does not need to be listed. 4. **Attribution.** Every durable save under `/agent/brain/` carries `author: @`. Always on. 5. **Approval routing.** Optional Slack channel where blocked-edit requests get posted with the requester's handle, the space they tried to edit, and a short summary. Admins approve or decline in a follow-up message. -6. **Identity resolution.** Slack ID or Motion email → handle. Neon-only (queries `agent_conversation`). No SQLite fallback. +6. **Identity resolution.** Slack ID or Motion email → handle. Motion-web identity reads the local daemon conversation store (`/daemon/conversation-store/conversations.db` via `$CONVERSATION_ID`). No runtime secrets required. Fails loudly: unknown identity means no writes. The install flow shapes these into the org's setup. @@ -34,8 +34,7 @@ The install flow shapes these into the org's setup. │ ├── spaces.json ← protected areas + writer rules + approval channel │ ├── organization-map.json ← people registry │ ├── slack-whoami.sh ← Slack resolver + auto-provisioning - │ ├── motion-whoami.sh ← Motion web resolver (Neon-only) + auto-provisioning - │ └── motion-whoami-neon.py ← Neon agent_conversation query helper + │ └── motion-whoami.sh ← Motion web resolver (local daemon DB) + auto-provisioning ├── members/ ← per-person home bases (owner-write only) │ └── /{.md, brain/, conversations/} └── (any protected spaces the conversation produced — paths chosen with the admin) @@ -49,7 +48,7 @@ The install flow shapes these into the org's setup. Seven phases. The admin sees a conversation; the agent runs the rest behind the scenes. -1. **Look around and plan.** The agent silently inspects the VM: existing `permissions.md`, prior-version files, suspicious content in `user.md`, partial installs, Neon-secret availability. Holds the findings in memory. +1. **Look around and plan.** The agent silently inspects the VM: existing `permissions.md`, prior-version files, suspicious content in `user.md`, partial installs, daemon conversation-store availability. Holds the findings in memory. 2. **Framing the opening.** Composes the opening turn in its own voice, shaped by the admin's triggering message and what Phase 1 surfaced. No canned script. 3. **The conversation.** Flowing chat. The agent listens for: who's on the team, the work shape, content categories that come up, **areas where only certain people should be editing**, areas that should stay open, and who the first admin is (plus a backup). If protected areas come up, the agent also asks about an approval channel and a backup approver. Brain-organization questions (where to put notes, how to group playbooks) get punted: "I can think about that separately — it's not part of this setup." 4. **Read it back, then confirm.** Plain-language summary in the admin's words, with writers named per space so wrong attribution is easy to spot. Confirms before any writes. @@ -81,8 +80,8 @@ The conversational tone, the communication style (no code or JSON in chat unless - Admin running the skill (or the instance owner on a fresh sandbox). - `/agent/` writable. - `/agent/user.md` exists (may be blank). -- `jq` installed. -- `NEON_DATABASE_URL` runtime secret configured. Hard-stop on fresh installs without it: Motion-web users would resolve as unknown on every message and writes would be blocked. Existing installs get a soft warn so reconfigures can proceed offline. +- `jq` and `sqlite3` installed. +- `/daemon/conversation-store/conversations.db` present (the runtime maintains it on every current VM by default). Motion-web identity resolves locally from it — no runtime secrets required. If it's missing the image is outdated: Motion-web users resolve as unknown (no writes) until the runtime is updated. --- @@ -93,7 +92,7 @@ Safe to re-run. Specifically: - `organization-map.json` merges; identity entries are never deleted. - `spaces.json` archives the prior version to `/agent/brain/admin/.archive/` before any reconfigure write, then merges. Spaces not mentioned in a new conversation are preserved as-is; never silently deleted. - `permissions.md` is regenerated from `spaces.json`. The old one is archived before overwrite. -- `slack-whoami.sh` / `motion-whoami.sh` / `motion-whoami-neon.py` are pure scripts and safe to rewrite. +- `slack-whoami.sh` / `motion-whoami.sh` are pure scripts and safe to rewrite. - `user.md` only prepends the protocol pointer if absent; updates in place if the installed version differs (with explicit admin confirmation). - Member home bases are never deleted by reconfigure. @@ -111,7 +110,7 @@ Safe to re-run. Specifically: ## Source -- **Skill version:** 3.0.0 +- **Skill version:** 3.3.0 - **Source org:** Motion (Creative Analytics) - **Predecessor:** `deploy-admin-permissions@2.3.0` - **Refs:** PDEC-7817 diff --git a/add-roles-permissions/SKILL.md b/add-roles-permissions/SKILL.md index 7596e14..582e356 100644 --- a/add-roles-permissions/SKILL.md +++ b/add-roles-permissions/SKILL.md @@ -15,7 +15,7 @@ trigger_domains: - security-deploy - cross-org-deployment - bootstrap -version: "3.1.5" +version: "3.3.0" source_org: "Motion (Creative Analytics)" predecessor: "deploy-admin-permissions@3.0.0" --- @@ -84,7 +84,6 @@ You'll touch a small, specific set of files. Nothing else. - `/agent/brain/admin/spaces.json` — the list of protected areas and who can edit each. - `/agent/brain/admin/organization-map.json` — the people list. - `/agent/brain/admin/slack-whoami.sh` and `motion-whoami.sh` — small scripts that figure out who is talking when someone messages on Slack or Motion web. -- `/agent/brain/admin/motion-whoami-neon.py` — a helper the Motion web script uses to look up who is chatting from a Motion conversation. - `/agent/brain/team//` — a personal space for each teammate (created if it does not exist yet). - The saved-instructions file at the top of the agent's setup — prepends a short pointer to the rulebook so the agent runs the "who is talking" check on every message. @@ -95,8 +94,8 @@ Personal spaces under `/agent/brain/team//` can only be edited by the pe - You must be running as an admin (or as the instance owner on a fresh sandbox). - `/agent/` must be writable. - `/agent/user.md` must exist (may be blank). -- `jq` must be installed. -- `NEON_DATABASE_URL` runtime secret must be configured for Motion-web identity resolution. If it is not, install still works but Motion-web users resolve as unknown until the secret is added. +- `jq` and `sqlite3` must be installed. +- `/daemon/conversation-store/conversations.db` must exist (it is present on every current VM by default — the runtime maintains it). Motion-web identity resolution reads it locally; no runtime secrets are required. --- @@ -152,22 +151,23 @@ The `let's set up your roles and permissions` pattern catches the v2.0.1 `team-m ### Look-around 5 — Which permission files are missing? ```bash -for f in permissions.md organization-map.json spaces.json slack-whoami.sh motion-whoami.sh motion-whoami-neon.py; do +for f in permissions.md organization-map.json spaces.json slack-whoami.sh motion-whoami.sh; do [ -f "/agent/brain/admin/$f" ] && echo "PRESENT: $f" || echo "MISSING: $f" done ``` Report exact state. Phase 5 only writes what is missing or what the admin confirmed should be overwritten. -### Look-around 6 — Is the Neon secret available? +### Look-around 6 — Can Motion-web identity resolve locally? + +Identity for Motion-web messages resolves from the local daemon conversation store. No secrets, no network. Check the two things the resolver needs: ```bash -secret run --env DATABASE_URL=NEON_DATABASE_URL -- printenv DATABASE_URL >/dev/null 2>&1 && echo "OK" || echo "MISSING" +[ -f /daemon/conversation-store/conversations.db ] && echo "DB OK" || echo "DB MISSING" +command -v sqlite3 >/dev/null 2>&1 && echo "SQLITE3 OK" || echo "SQLITE3 MISSING" ``` -If MISSING on a fresh install (no existing `permissions.md`): hard stop. Tell the admin you can't set this up without Neon access and ask them to save `NEON_DATABASE_URL` as a runtime secret, then re-run. Do not proceed to Phase 2. - -If MISSING but `permissions.md` exists: warn and proceed. Reconfigures still work; Motion-web identity is just blocked until the secret returns. +Both are present on every current VM by default, so this almost always passes. If either is missing, the VM image is likely outdated: warn the admin in plain language that Motion-web identity won't resolve on this sandbox until the runtime is updated, and suggest they contact support. Slack-side identity and the rest of the setup still work, so proceed — but note that Motion-web users will be treated as unknown (no writes) until the check passes. ### Look-around 7 — What shape is this org based on its Motion workspaces? @@ -236,7 +236,7 @@ Use these in your own words. Pick one or two that fit the trigger; don't recite - **Reconfigure** (existing config detected): acknowledge what's set up, confirm whether they want a full re-walkthrough or a targeted change. Only ask the team question if full walkthrough was confirmed. - **Partial install**: tell them you noticed it, ask whether to finish or start fresh. - **TMM v2.0.1 leak detected**: mention casually as something you'll tidy up, do not headline. -- **Neon secret missing**: say Motion-web identity needs that secret first and offer to walk them through saving it. Do not start the team conversation if Phase 1 hard-stopped. +- **Daemon conversation store missing**: mention that identity for Motion-web messages won't work on this sandbox until the runtime is updated, and that the rest of the setup still goes through. Don't make it the headline. - **Org shape hint from workspaces**: lean the opener in a direction (agency, single brand, dept) without committing to a fully-formed proposal. If unclear, don't invoke it. It's a tilt, not a script. - **Session-open routines loading files**: don't mention in the opener. Carry the list into Phase 3 so you can name the specific files when you raise the behavior-shaping question. @@ -518,142 +518,69 @@ If any validation fails, do not write. Surface the failure to the admin in plain 2. Run the slug-pinning logic from Phase 3 against existing spaces before slugifying any new name. 3. Merge: spaces named in the new conversation overwrite the corresponding entries; spaces in the old file that were not mentioned this run are preserved as-is. Never delete a space silently — if the admin wants one removed, they say so explicitly. -### Step 4 — Write the Motion-side resolver and its helper - -Write the helper at `/agent/brain/admin/motion-whoami-neon.py` and the shell wrapper at `/agent/brain/admin/motion-whoami.sh`. The pair queries Neon's `agent_conversation` table to figure out who is messaging from Motion web. No SQLite fallback: on Neon failure the script exits non-zero and writes are refused. `chmod +x` both files. - -Helper file body, verbatim: - -```python -#!/usr/bin/env python3 -"""Resolve user_email from Neon agent_conversation. Read-only. - -Usage: secret run --env DATABASE_URL=NEON_DATABASE_URL -- python3 motion-whoami-neon.py -Exit: 0 ok, 1 missing args/env, 7 conv not found or empty email, 8 query failed. -""" -import os -import sys -import json +### Step 4 — Write the Motion-side resolver -sys.path.insert(0, "/daemon/cache/python/user-base/lib/python3.11/site-packages") -try: - import psycopg -except ImportError as e: - print(json.dumps({"error": f"psycopg not available: {e}"}), file=sys.stderr) - sys.exit(1) - -if len(sys.argv) < 2: - print(json.dumps({"error": "conversation_id required"}), file=sys.stderr) - sys.exit(1) -conv_id = sys.argv[1].strip() - -if not os.environ.get("DATABASE_URL"): - print( - json.dumps({ - "error": "DATABASE_URL not in env", - "hint": "invoke via: secret run --env DATABASE_URL=NEON_DATABASE_URL -- python3 motion-whoami-neon.py ", - }), - file=sys.stderr, - ) - sys.exit(1) - -try: - with psycopg.connect(os.environ["DATABASE_URL"], connect_timeout=3) as conn, conn.cursor() as cur: - cur.execute( - "SELECT user_email, workspace_id, organization_id, mondrian_user_id " - "FROM agent_conversation WHERE id = %s", - (conv_id,), - ) - row = cur.fetchone() -except Exception as e: - print(json.dumps({"error": f"Neon query failed: {e}"}), file=sys.stderr) - sys.exit(8) - -if not row: - sys.exit(7) - -user_email, ws_id, org_id, mondrian_user_id = row -if not user_email: - sys.exit(7) - -print(json.dumps({ - "user_email": user_email, - "workspace_id": ws_id, - "organization_id": org_id, - "mondrian_user_id": mondrian_user_id, -})) -``` +Write the resolver at `/agent/brain/admin/motion-whoami.sh`. It reads the messaging user's email from the local daemon conversation store (`/daemon/conversation-store/conversations.db`) using the `$CONVERSATION_ID` env var the runtime injects, then resolves against `organization-map.json`. Fully local: no network, no runtime secrets. It fails loudly — if `$CONVERSATION_ID` is unset, the daemon DB is missing, or no email comes back, the script exits non-zero and writes are refused (the permissions layer treats that as identity-unknown). `chmod +x` the file. -```bash -chmod +x /agent/brain/admin/motion-whoami-neon.py -``` +The script body matches the resolver proven on the customer fleet in the June 2026 rollout (compatibility variant, SHA `946bfa2a043944075ba0ad415faa35a28d668cca07131fbeea551244e5f85d01`), adapted only to this package's file names: it reads `organization-map.json` (env override `RUNNETH_ORG_MAP`) and scaffolds home bases under `/agent/brain/team/`. The legacy `.motionEmails` map is still read (and kept in sync on provision) so upgrades from older installs keep resolving existing users. -Then write `/agent/brain/admin/motion-whoami.sh` verbatim, and `chmod +x`: +Write verbatim, then `chmod +x`: ```bash #!/usr/bin/env bash -# motion-whoami.sh — Motion-side identity resolver. Neon-only. +# motion-whoami.sh — Motion-side identity resolver. Local daemon DB. # Resolves the messaging user's email against organization-map.json. # Returns JSON { scope, handle, home_base, status } where status is -# resolved | provisioned | collision. Non-zero exit on Neon failure; +# resolved | provisioned | collision. Non-zero exit on resolver failure; # the permissions layer treats that as identity-unknown. -# Accepts CONVERSATION_ID from env, falls back to cwd basename. +# Reads userEmail from /daemon/conversation-store/conversations.db +# using $CONVERSATION_ID (injected by the runtime). No network, no secrets. # Usage: motion-whoami.sh [] set -euo pipefail MAP_FILE="${RUNNETH_ORG_MAP:-/agent/brain/admin/organization-map.json}" -NEON_HELPER="${RUNNETH_MOTION_WHOAMI_NEON:-/agent/brain/admin/motion-whoami-neon.py}" -CONV_DIR="/agent/conversations" +CONV_DB="/daemon/conversation-store/conversations.db" DISPLAY_NAME="${1:-}" -# Resolve conversation_id: env first (runtime sets it), then cwd basename. -if [[ -n "${CONVERSATION_ID:-}" ]]; then - CONV_ID="$CONVERSATION_ID" -else - CWD="$(pwd -P)" - if [[ "$CWD" != "$CONV_DIR/"* ]]; then - echo '{"error":"no CONVERSATION_ID in env and not in a conversation directory","cwd":"'"$CWD"'"}' >&2 - exit 1 - fi - CONV_ID="$(basename "$CWD")" +if [[ -z "${CONVERSATION_ID:-}" ]]; then + echo '{"error":"CONVERSATION_ID env var is not set"}' >&2 + exit 1 fi -if [[ ! -f "$NEON_HELPER" ]]; then - echo '{"error":"motion-whoami-neon.py helper missing","path":"'"$NEON_HELPER"'","conversation_id":"'"$CONV_ID"'"}' >&2 - exit 2 +if [ ! -f "$CONV_DB" ]; then + echo '{"error":"conversations.db not found","path":"'"$CONV_DB"'"}' >&2 + exit 1 fi -NEON_OUT=$(timeout 6 secret run --env DATABASE_URL=NEON_DATABASE_URL -- \ - python3 "$NEON_HELPER" "$CONV_ID" 2>/dev/null) -NEON_RC=$? +RAW=$(sqlite3 -readonly -json "$CONV_DB" \ + -cmd ".timeout 3000" \ + "SELECT json_extract(conversation_json,'\$.userEmail') as userEmail FROM conversations WHERE conversation_id='$CONVERSATION_ID' LIMIT 1" \ + 2>/dev/null) -if [[ $NEON_RC -ne 0 || -z "$NEON_OUT" ]]; then - echo '{"error":"Neon agent_conversation lookup failed","helper_exit_code":'"$NEON_RC"',"conversation_id":"'"$CONV_ID"'"}' >&2 - exit 3 -fi +USER_EMAIL=$(echo "$RAW" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['userEmail'] if d and d[0]['userEmail'] else '')" 2>/dev/null) -USER_EMAIL=$(echo "$NEON_OUT" | jq -r '.user_email // empty' 2>/dev/null) if [[ -z "$USER_EMAIL" ]]; then - echo '{"error":"Neon returned no user_email for this conversation","conversation_id":"'"$CONV_ID"'"}' >&2 - exit 4 + echo '{"error":"userEmail not found for conversation","conversation_id":"'"$CONVERSATION_ID"'"}' >&2 + exit 1 fi if [ ! -f "$MAP_FILE" ]; then - echo '{"error":"organization-map.json missing","path":"'"$MAP_FILE"'"}' >&2 - exit 5 + echo '{"error":"organization-map.json not found","path":"'"$MAP_FILE"'"}' >&2 + exit 1 fi -REF=$(jq -r --arg email "$USER_EMAIL" '.motionUserEmails[$email] // empty' "$MAP_FILE") +REF=$(jq -r --arg email "$USER_EMAIL" '(.motionUserEmails[$email] // .motionEmails[$email] // empty)' "$MAP_FILE") if [ -n "$REF" ]; then HANDLE="${REF#member:}" jq -c --arg h "$HANDLE" ' - .members[$h] - | { scope: .scope, handle: .handle, - home_base: ("/agent/brain/team/" + .handle + "/"), - status: "resolved", - resolution: "neon" } + .members[$h] | { + scope: .scope, + handle: .handle, + home_base: ("/agent/brain/team/" + .handle + "/"), + status: "resolved" + } ' "$MAP_FILE" exit 0 fi @@ -665,31 +592,41 @@ HANDLE=$(echo "$NAME_FOR_HANDLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0 # Collision check COLLISION=$(jq -c --arg name "$DISPLAY_NAME" --arg email "$USER_EMAIL" ' - [ (.members | to_entries[] | select((.value.name != null and $name != "" and .value.name == $name) or .value.email == $email) - | { source: "members", handle: .key, entry: .value }) ] - | .[0] // empty + [ (.members | to_entries[] | + select((.value.name != null and $name != "" and .value.name == $name) or .value.email == $email) | + { source: "members", handle: .key, entry: .value }) ] | .[0] // empty ' "$MAP_FILE") if [ -n "$COLLISION" ] && [ "$COLLISION" != "null" ]; then - echo "{\"status\": \"collision\", \"candidate\": $COLLISION, \"proposed_handle\": \"$HANDLE\", \"display_name\": \"$DISPLAY_NAME\", \"email\": \"$USER_EMAIL\", \"resolution\": \"neon\"}" + echo "{\"status\": \"collision\", \"candidate\": $COLLISION, \"proposed_handle\": \"$HANDLE\", \"display_name\": \"$DISPLAY_NAME\", \"email\": \"$USER_EMAIL\"}" exit 0 fi # No collision. Provision new member entry. mkdir -p "/agent/brain/team/$HANDLE" + tmp=$(mktemp) jq --arg email "$USER_EMAIL" --arg h "$HANDLE" --arg name "${DISPLAY_NAME:-$EMAIL_LOCAL}" ' - .motionUserEmails[$email] = ("member:" + $h) - | .members[$h] = { "name": $name, "scope": "member", "handle": $h, "email": $email } + .motionUserEmails = (.motionUserEmails // {}) | + .motionUserEmails[$email] = ("member:" + $h) | + (if has("motionEmails") then .motionEmails[$email] = ("member:" + $h) else . end) | + .members[$h] = { + "name": $name, + "scope": "member", + "handle": $h, + "email": $email + } ' "$MAP_FILE" > "$tmp" && mv "$tmp" "$MAP_FILE" -echo "{\"scope\": \"member\", \"handle\": \"$HANDLE\", \"home_base\": \"/agent/brain/team/$HANDLE/\", \"status\": \"provisioned\", \"resolution\": \"neon\"}" +echo "{\"scope\": \"member\", \"handle\": \"$HANDLE\", \"home_base\": \"/agent/brain/team/$HANDLE/\", \"status\": \"provisioned\"}" ``` ```bash chmod +x /agent/brain/admin/motion-whoami.sh ``` +If a leftover `/agent/brain/admin/motion-whoami-neon.py` exists from a prior install, remove it — nothing references it anymore. + --- ### Step 5 — Write `/agent/brain/admin/slack-whoami.sh` @@ -978,7 +915,7 @@ Leave all other content untouched. ## PHASE 6 — POST-DEPLOYMENT VERIFICATION ```bash -for f in permissions.md organization-map.json spaces.json slack-whoami.sh motion-whoami.sh motion-whoami-neon.py; do +for f in permissions.md organization-map.json spaces.json slack-whoami.sh motion-whoami.sh; do [ -f "/agent/brain/admin/$f" ] && echo "OK admin/$f" || echo "MISSING: admin/$f" done diff --git a/add-roles-permissions/install-config.json b/add-roles-permissions/install-config.json index ee3b941..2c6884a 100644 --- a/add-roles-permissions/install-config.json +++ b/add-roles-permissions/install-config.json @@ -1,7 +1,7 @@ { "schema": "1.0", "id": "add-roles-permissions", - "version": "3.1.5", + "version": "3.3.0", "description": "Most teams don't realize this until something gets accidentally changed: out of the box, anyone the team trusts to chat with Runneth can also edit anything Runneth knows. That includes the context Runneth references in conversations and the rules for how it behaves for everyone on the team. Saved feedback like \"let's not use the word cheap anymore\" can quietly become the rule for everyone, not just the person who said it. This skill lets the admin choose which areas get locked down and who is allowed to change each. A short consultative conversation captures what to protect, then Runneth blocks unauthorized edits and routes change requests through an approval channel. Every save gets stamped with who wrote it. New teammates onboard automatically.", "installs": [ { diff --git a/team-member-memory/README.md b/team-member-memory/README.md index f1c80d3..b8763f0 100644 --- a/team-member-memory/README.md +++ b/team-member-memory/README.md @@ -21,8 +21,7 @@ Runneth gets to know each person it works with — their goals, preferences, and ├── admin/ │ ├── organization-map.json ← identity registry — auto-provisioned on first message │ ├── slack-whoami.sh ← Slack resolver + auto-scaffolding - │ ├── motion-whoami.sh ← Motion-web resolver + auto-scaffolding (Neon-first) - │ └── motion-whoami-neon.py ← Neon agent_conversation query helper + │ └── motion-whoami.sh ← Motion-web resolver + auto-scaffolding (local daemon DB) ├── identity/ │ └── people/ │ ├── TEMPLATE.md ← team-member template (configurable path) @@ -38,8 +37,8 @@ Runneth gets to know each person it works with — their goals, preferences, and ## How identity resolution works - **Slack:** `slack-whoami.sh []` reads `/agent/brain/admin/organization-map.json`. Known IDs resolve. Unknown IDs auto-provision a new entry and home base. -- **Motion web:** `motion-whoami.sh []` resolves the speaker email via the Neon `agent_conversation` table, invoked through `motion-whoami-neon.py` + `secret run --env DATABASE_URL=NEON_DATABASE_URL`. Authoritative, zero-lag, works for brand-new conversations. -- **No SQLite fallback.** If Neon fails, the resolver returns `status: "unresolved"`. The behavior-snippet handles unresolved sessions by continuing read-only and, when a durable write is about to happen, asking the user where it should land (org level vs. a specific person's folder) instead of guessing a handle. +- **Motion web:** `motion-whoami.sh []` reads the speaker email from the local daemon conversation store (`/daemon/conversation-store/conversations.db`) using the runtime-injected `$CONVERSATION_ID`. Fully local, zero-lag, works for brand-new conversations. No runtime secrets required. +- **Graceful when resolution fails.** If the lookup fails (`$CONVERSATION_ID` not set, daemon DB missing on an older image, no email on the row), the resolver returns `status: "unresolved"`. The behavior-snippet handles unresolved sessions by continuing read-only and, when a durable write is about to happen, asking the user where it should land (org level vs. a specific person's folder) instead of guessing a handle. Both resolvers return `{ handle, home_base, status }`. Status is `resolved` or `provisioned`. @@ -50,7 +49,7 @@ This package is fully standalone. If an admin later installs `add-roles-permissi ## Prerequisites - Any Runneth sandbox -- `jq` installed +- `jq` and `sqlite3` installed - A platform identifier (Slack ID or Motion `userEmail`) for the speaker — otherwise the resolvers fall back gracefully and the conversation proceeds without personalized context. ## Version history diff --git a/team-member-memory/behavior-snippet.md b/team-member-memory/behavior-snippet.md index d999c73..618fcda 100644 --- a/team-member-memory/behavior-snippet.md +++ b/team-member-memory/behavior-snippet.md @@ -1,4 +1,4 @@ - + ### On every new conversation — session open @@ -8,12 +8,12 @@ Run silently before answering the first message: - **Slack:** `bash /agent/brain/admin/slack-whoami.sh []` - **Motion web:** `bash /agent/brain/admin/motion-whoami.sh []` - Both resolve against `/agent/brain/admin/organization-map.json`. Motion web goes to Neon's `agent_conversation` table for the speaker email; there is no SQLite fallback. + Both resolve against `/agent/brain/admin/organization-map.json`. Motion web reads the speaker email from the local daemon conversation store (`/daemon/conversation-store/conversations.db`) using the `$CONVERSATION_ID` env var the runtime injects — no network, no secrets. Returned status values: - `resolved` — known person; `handle` and `home_base` returned. Continue normally. - `provisioned` — new person; a home base was just scaffolded at `` with a stub `.md` and a `brain/` subfolder. Continue normally — there is no prior context to load. - - `unresolved` — Neon could not return an email for this conversation (Neon down, brand-new row not yet replicated, helper missing, etc.). Continue the conversation, but switch to the **unresolved write rule** below for this session. + - `unresolved` — the resolver could not return an email for this conversation (`$CONVERSATION_ID` not set in this context, daemon DB missing on an older image, no `userEmail` on the row, etc.). Continue the conversation, but switch to the **unresolved write rule** below for this session. If `add-roles-permissions` is also installed, the resolver may additionally return a `scope` field and a `collision` status. Handle those per the permissions package — for memory purposes, `resolved` / `provisioned` / `unresolved` are the only statuses that gate the memory steps below. diff --git a/team-member-memory/install-config.json b/team-member-memory/install-config.json index 80c5b22..7cdb048 100644 --- a/team-member-memory/install-config.json +++ b/team-member-memory/install-config.json @@ -1,10 +1,10 @@ { "schema": "1.0", "id": "team-member-memory", - "version": "3.1.0", + "version": "3.2.0", "description": "Runneth gets to know each person it works with \u2014 their goals, preferences, and working style \u2014 and adapts to them over time. Standalone: resolves identity from Slack ID or Motion email and scaffolds each person's home base on first message. No permissions setup required.", "requires": { - "runtime": "any Runneth sandbox; jq must be installed", + "runtime": "any Runneth sandbox; jq and sqlite3 must be installed", "preinstalled": [], "recommended": [] }, @@ -44,12 +44,6 @@ "if-not-exists": true, "chmod": "0755" }, - { - "file": "motion-whoami-neon.py", - "dest": "/agent/brain/admin/motion-whoami-neon.py", - "if-not-exists": true, - "chmod": "0755" - }, { "file": "motion-whoami.sh", "dest": "/agent/brain/admin/motion-whoami.sh", @@ -70,6 +64,12 @@ } ], "changelog": [ + { + "version": "3.2.0", + "date": "2026-06-12", + "type": "minor", + "notes": "Motion-web identity resolves locally — no runtime secrets. motion-whoami.sh now reads the speaker email from the local daemon conversation store (/daemon/conversation-store/conversations.db, via sqlite3 -readonly -json) using the runtime-injected $CONVERSATION_ID, replacing the Neon agent_conversation query. motion-whoami-neon.py is removed from the package and the install list; NEON_DATABASE_URL is no longer needed. The resolution logic matches the resolver proven on the customer fleet in the June 2026 rollout (compatibility variant: reads .motionUserEmails with legacy .motionEmails fallback, keeps both in sync on provision), adapted to this package's organization-map.json naming and /agent/brain/identity/people/ home bases. Soft failure semantics unchanged: on any lookup failure ($CONVERSATION_ID unset, daemon DB missing on an older image, no userEmail on the row) the resolver returns status \"unresolved\" and the behavior-snippet asks the user at write time where the durable note should land instead of guessing a handle. slack-whoami.sh is unaffected — it never touched Neon. sqlite3 added to runtime requirements. Refs: PDEC-7817." + }, { "version": "3.1.0", "date": "2026-06-02", diff --git a/team-member-memory/motion-whoami-neon.py b/team-member-memory/motion-whoami-neon.py deleted file mode 100755 index c96de9f..0000000 --- a/team-member-memory/motion-whoami-neon.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -"""motion-whoami-neon.py — Resolve user_email from the Neon agent_conversation table. - -Usage: - secret run --env DATABASE_URL=NEON_DATABASE_URL -- \ - python3 motion-whoami-neon.py - -Output (stdout, success): - {"user_email": "...", "workspace_id": "...", "organization_id": "...", "mondrian_user_id": "..."} - -Exit codes: - 0 - success, prints JSON - 1 - missing args or DATABASE_URL not in env - 7 - conversation row not found or user_email is empty (recoverable miss) - 8 - Neon connection or query failed - -Read-only by intent. Same query shape as /agent/tools/admin/_neon_resolve_conv.py -but returns only the identity columns without doing the workspace-map join, so -the calling shell script can do its own light or strict resolution on top. -""" -import os -import sys -import json - -sys.path.insert(0, "/daemon/cache/python/user-base/lib/python3.11/site-packages") -try: - import psycopg -except ImportError as e: - print(json.dumps({"error": f"psycopg not available: {e}"}), file=sys.stderr) - sys.exit(1) - -if len(sys.argv) < 2: - print(json.dumps({"error": "conversation_id required"}), file=sys.stderr) - sys.exit(1) -conv_id = sys.argv[1].strip() - -if not os.environ.get("DATABASE_URL"): - print( - json.dumps({ - "error": "DATABASE_URL not in env", - "hint": "invoke via: secret run --env DATABASE_URL=NEON_DATABASE_URL -- python3 motion-whoami-neon.py ", - }), - file=sys.stderr, - ) - sys.exit(1) - -try: - with psycopg.connect(os.environ["DATABASE_URL"], connect_timeout=3) as conn, conn.cursor() as cur: - cur.execute( - "SELECT user_email, workspace_id, organization_id, mondrian_user_id " - "FROM agent_conversation WHERE id = %s", - (conv_id,), - ) - row = cur.fetchone() -except Exception as e: - print(json.dumps({"error": f"Neon query failed: {e}"}), file=sys.stderr) - sys.exit(8) - -if not row: - sys.exit(7) - -user_email, ws_id, org_id, mondrian_user_id = row -if not user_email: - sys.exit(7) - -print(json.dumps({ - "user_email": user_email, - "workspace_id": ws_id, - "organization_id": org_id, - "mondrian_user_id": mondrian_user_id, -})) diff --git a/team-member-memory/motion-whoami.sh b/team-member-memory/motion-whoami.sh index 0052be1..1cd7fe3 100755 --- a/team-member-memory/motion-whoami.sh +++ b/team-member-memory/motion-whoami.sh @@ -3,21 +3,19 @@ # # Lightweight identity → handle → home_base. No scope rules, no write blocking. # -# Resolution path: Neon agent_conversation table (authoritative, zero-lag) via -# the motion-whoami-neon.py helper, invoked through: -# secret run --env DATABASE_URL=NEON_DATABASE_URL -- \ -# python3 /agent/brain/admin/motion-whoami-neon.py +# Resolution path: the local daemon conversation store at +# /daemon/conversation-store/conversations.db +# read with `sqlite3 -readonly -json`, keyed by the $CONVERSATION_ID env var +# the runtime injects. Fully local: no network, no runtime secrets. # -# There is no SQLite fallback. The local conversations.db is unreliable for -# brand-new conversations (live DB is a 0-byte placeholder; backups lag 30 min), -# and the case where Neon is down AND the conversation is older than 30 min is -# too narrow to design around. On Neon failure this script returns a clean -# 'unresolved' JSON so the calling logic (behavior-snippet) can ask the user -# at write time where to save instead of guessing. +# On any resolution failure ($CONVERSATION_ID unset, daemon DB missing, no +# userEmail for the conversation) this script returns a clean 'unresolved' +# JSON so the calling logic (behavior-snippet) can ask the user at write time +# where to save instead of guessing a handle. # # Returns JSON, exit 0 in all non-fatal cases: -# { "handle", "home_base", "status": "resolved", "resolution": "neon" } -# { "handle", "home_base", "status": "provisioned","resolution": "neon" } +# { "handle", "home_base", "status": "resolved", "resolution": "daemon-db" } +# { "handle", "home_base", "status": "provisioned","resolution": "daemon-db" } # { "status": "unresolved", "reason": "", "conversation_id": "..." } # # Accepts CONVERSATION_ID from env (the runtime sets it); falls back to the @@ -29,7 +27,7 @@ set -euo pipefail MAP_FILE="${RUNNETH_ORG_MAP:-/agent/brain/admin/organization-map.json}" -NEON_HELPER="${RUNNETH_MOTION_WHOAMI_NEON:-/agent/brain/admin/motion-whoami-neon.py}" +CONV_DB="/daemon/conversation-store/conversations.db" CONV_DIR="/agent/conversations" DISPLAY_NAME="${1:-}" @@ -45,20 +43,23 @@ else CONV_ID="$(basename "$CWD")" fi -if [[ ! -f "$NEON_HELPER" ]]; then - echo '{"status":"unresolved","reason":"motion-whoami-neon.py helper missing","conversation_id":"'"$CONV_ID"'"}' +if [[ ! -f "$CONV_DB" ]]; then + echo '{"status":"unresolved","reason":"daemon conversations.db missing","path":"'"$CONV_DB"'","conversation_id":"'"$CONV_ID"'"}' exit 0 fi -NEON_OUT=$(timeout 6 secret run --env DATABASE_URL=NEON_DATABASE_URL -- \ - python3 "$NEON_HELPER" "$CONV_ID" 2>/dev/null || true) +RAW=$(sqlite3 -readonly -json "$CONV_DB" \ + -cmd ".timeout 3000" \ + "SELECT json_extract(conversation_json,'\$.userEmail') as userEmail FROM conversations WHERE conversation_id='$CONV_ID' LIMIT 1" \ + 2>/dev/null || true) + USER_EMAIL="" -if [[ -n "$NEON_OUT" ]]; then - USER_EMAIL=$(echo "$NEON_OUT" | jq -r '.user_email // empty' 2>/dev/null || true) +if [[ -n "$RAW" ]]; then + USER_EMAIL=$(echo "$RAW" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['userEmail'] if d and d[0]['userEmail'] else '')" 2>/dev/null || true) fi if [[ -z "$USER_EMAIL" ]]; then - echo '{"status":"unresolved","reason":"Neon agent_conversation lookup failed or returned no user_email","conversation_id":"'"$CONV_ID"'"}' + echo '{"status":"unresolved","reason":"daemon conversation-store lookup failed or returned no userEmail","conversation_id":"'"$CONV_ID"'"}' exit 0 fi @@ -67,7 +68,7 @@ if [ ! -f "$MAP_FILE" ]; then exit 0 fi -REF=$(jq -r --arg email "$USER_EMAIL" '.motionUserEmails[$email] // empty' "$MAP_FILE") +REF=$(jq -r --arg email "$USER_EMAIL" '(.motionUserEmails[$email] // .motionEmails[$email] // empty)' "$MAP_FILE") if [ -n "$REF" ]; then HANDLE="${REF#member:}" @@ -76,7 +77,7 @@ if [ -n "$REF" ]; then | { handle: .handle, home_base: ("/agent/brain/identity/people/" + .handle + "/"), status: "resolved", - resolution: "neon" } + resolution: "daemon-db" } ' "$MAP_FILE" exit 0 fi @@ -114,8 +115,10 @@ fi tmp=$(mktemp) jq --arg email "$USER_EMAIL" --arg h "$HANDLE" --arg name "$DISPLAY_FOR_STUB" ' - .motionUserEmails[$email] = ("member:" + $h) - | .members[$h] = { "name": $name, "handle": $h, "email": $email } + .motionUserEmails = (.motionUserEmails // {}) | + .motionUserEmails[$email] = ("member:" + $h) | + (if has("motionEmails") then .motionEmails[$email] = ("member:" + $h) else . end) | + .members[$h] = { "name": $name, "handle": $h, "email": $email } ' "$MAP_FILE" > "$tmp" && mv "$tmp" "$MAP_FILE" -echo "{\"handle\": \"$HANDLE\", \"home_base\": \"$HOME_BASE/\", \"status\": \"provisioned\", \"resolution\": \"neon\"}" +echo "{\"handle\": \"$HANDLE\", \"home_base\": \"$HOME_BASE/\", \"status\": \"provisioned\", \"resolution\": \"daemon-db\"}"