merge: feat/wf5-securestorage into the 0.0.50 integration line

Brings the per-agent harness-home work (FLEET_SEAT) onto the release line.
Clean merge, no conflicts.
This commit is contained in:
Jason Woltje
2026-08-15 10:28:32 -05:00
32 changed files with 6682 additions and 129 deletions
@@ -0,0 +1,89 @@
{
"hooks": {
"PreCompact": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py\" --runtime claude --reason pre-compact"
}
]
}
],
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py\" --runtime claude --reason session-start-compact"
}
]
},
{
"matcher": "resume|clear",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py\" --runtime claude --reason session-start-rollover --bump-generation"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "^/mosaic-promote$",
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/promote-begin.py",
"timeout": 15
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "~/.config/mosaic/tools/qa/prevent-memory-write.sh",
"timeout": 10
}
]
},
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude --recovery-command ~/.config/mosaic/tools/lease-broker/recover-context.py",
"timeout": 3
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.config/mosaic/tools/qa/reflect-stop-hook.sh",
"timeout": 15
}
]
},
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude --latest-entry; observer_status=$?; python3 ~/.config/mosaic/tools/lease-broker/promote-complete.py; exit $observer_status",
"timeout": 15
}
]
}
]
}
}
@@ -1,60 +1,7 @@
{
"model": "opus",
"hooks": {
"PreCompact": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py\" --runtime claude --reason pre-compact"
}
]
}
],
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py\" --runtime claude --reason session-start-compact"
}
]
},
{
"matcher": "resume|clear",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py\" --runtime claude --reason session-start-rollover --bump-generation"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "^/mosaic-promote$",
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/promote-begin.py",
"timeout": 15
}
]
}
],
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude --recovery-command ~/.config/mosaic/tools/lease-broker/recover-context.py",
"timeout": 3
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
@@ -101,11 +48,6 @@
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude --latest-entry; observer_status=$?; python3 ~/.config/mosaic/tools/lease-broker/promote-complete.py; exit $observer_status",
"timeout": 15
},
{
"type": "command",
"command": "~/.config/mosaic/tools/qa/reflect-stop-hook.sh",
@@ -325,5 +267,11 @@
"cpan",
"nohup"
],
"enableAllMcpTools": true
"enableAllMcpTools": true,
"mcpServers": {
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
}
}
}
@@ -5,6 +5,7 @@ MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
MODE="apply"
RUNTIME="all"
STRICT_CHECK=0
CLAUDE_CONFIG_DIR=""
PKG="@modelcontextprotocol/server-sequential-thinking"
@@ -29,6 +30,14 @@ while [[ $# -gt 0 ]]; do
STRICT_CHECK=1
shift
;;
--claude-config-dir)
if [[ $# -lt 2 ]]; then
err "--claude-config-dir requires an absolute seat config directory"
exit 2
fi
CLAUDE_CONFIG_DIR="$2"
shift 2
;;
*)
err "Unknown argument: $1"
exit 2
@@ -67,11 +76,19 @@ warm_package() {
}
check_claude_config() {
python3 - <<'PY'
CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" python3 - <<'PY'
import json
import os
from pathlib import Path
p = Path.home() / ".claude" / "settings.json"
if not p.exists():
# Claude reads MCP definitions from .claude.json, not settings.json. The
# settings.json fallback preserves legacy operator flows until their config is migrated.
config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
p = Path(config_dir) / ".claude.json" if config_dir else Path.home() / ".claude.json"
if not p.exists() and not config_dir:
p = Path.home() / ".claude" / "settings.json"
# Only explicit fleet seats require a private, non-symlink config. Operator
# config remains compatible with pre-existing permission conventions.
if not p.exists() or p.is_symlink() or (config_dir and (p.stat().st_mode & 0o077) != 0):
raise SystemExit(1)
try:
data = json.loads(p.read_text(encoding="utf-8"))
@@ -92,10 +109,15 @@ PY
}
apply_claude_config() {
python3 - <<'PY'
CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" python3 - <<'PY'
import json
import os
from pathlib import Path
p = Path.home() / ".claude" / "settings.json"
# Claude reads MCP definitions from .claude.json for both operator and
# explicitly isolated fleet config dirs. The checker retains a settings.json
# fallback only to avoid breaking legacy operator configurations.
config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
p = Path(config_dir) / ".claude.json" if config_dir else Path.home() / ".claude.json"
p.parent.mkdir(parents=True, exist_ok=True)
if p.exists():
try:
@@ -117,7 +139,7 @@ PY
}
check_codex_config() {
local cfg="$HOME/.codex/config.toml"
local cfg="${CODEX_HOME:-$HOME/.codex}/config.toml"
[[ -f "$cfg" ]] || return 1
grep -Eq '^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]' "$cfg" && \
grep -q '^command = "npx"' "$cfg" && \
@@ -125,7 +147,7 @@ check_codex_config() {
}
apply_codex_config() {
local cfg="$HOME/.codex/config.toml"
local cfg="${CODEX_HOME:-$HOME/.codex}/config.toml"
mkdir -p "$(dirname "$cfg")"
[[ -f "$cfg" ]] || touch "$cfg"
@@ -148,10 +170,11 @@ apply_codex_config() {
}
check_opencode_config() {
python3 - <<'PY'
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-}" python3 - <<'PY'
import json
import os
from pathlib import Path
p = Path.home() / ".config" / "opencode" / "config.json"
p = Path(os.environ["XDG_CONFIG_HOME"]) / "opencode" / "config.json" if os.environ.get("XDG_CONFIG_HOME") else Path.home() / ".config" / "opencode" / "config.json"
if not p.exists():
raise SystemExit(1)
try:
@@ -174,10 +197,11 @@ PY
}
apply_opencode_config() {
python3 - <<'PY'
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-}" python3 - <<'PY'
import json
import os
from pathlib import Path
p = Path.home() / ".config" / "opencode" / "config.json"
p = Path(os.environ["XDG_CONFIG_HOME"]) / "opencode" / "config.json" if os.environ.get("XDG_CONFIG_HOME") else Path.home() / ".config" / "opencode" / "config.json"
p.parent.mkdir(parents=True, exist_ok=True)
if p.exists():
try:
@@ -290,12 +290,24 @@ _build_runtime_bin_prefix() {
MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
PANE_PATH=${MOSAIC_RUNTIME_BIN_PREFIX:+${MOSAIC_RUNTIME_BIN_PREFIX}:}/usr/local/bin:/usr/bin:/bin
# A seat scaffolded by `mosaic fleet agent new` owns its harness home, settings
# overlay and auth bundle; launching it through `mosaic fleet launch` is what makes
# ~/.mosaic real for a roster-started pane instead of a directory nothing reads.
# Detection uses $PANE_HOME/.mosaic because the pane environment is cleared below,
# so `mosaic fleet launch` resolves the same root from HOME and the two agree.
FLEET_SEAT_DIR="$PANE_HOME/.mosaic/fleet/agents/$AGENT_NAME"
FLEET_SEAT=0
[ -f "$FLEET_SEAT_DIR/profile.json" ] && FLEET_SEAT=1
_ensure_claude_workdir_trusted() {
local workdir="$1"
local claude_json="$2"
local resolved
resolved=$(cd "$workdir" 2>/dev/null && pwd -P) || resolved="$workdir"
local claude_json="${MOSAIC_CLAUDE_JSON:-${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}}"
claude_json="${claude_json:-$HOME/.claude.json}"
if [ -z "$claude_json" ]; then
claude_json="${MOSAIC_CLAUDE_JSON:-${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}}"
claude_json="${claude_json:-$HOME/.claude.json}"
fi
command -v python3 >/dev/null 2>&1 || return 1
MOSAIC_CJ="$claude_json" MOSAIC_TRUST_DIR="$resolved" python3 - <<'PY'
import json, os, sys, tempfile
@@ -329,11 +341,23 @@ PY
}
if [ "$MOSAIC_AGENT_RUNTIME" = claude ]; then
_ensure_claude_workdir_trusted "$MOSAIC_AGENT_WORKDIR" || \
# Trust belongs to the home the seat will actually run in. Writing it to the
# operator's ~/.claude.json would leave the seat prompting on its first turn.
SEAT_CLAUDE_JSON=""
if [ "$FLEET_SEAT" = 1 ] && [ -d "$FLEET_SEAT_DIR/.claude" ]; then
SEAT_CLAUDE_JSON="$FLEET_SEAT_DIR/.claude/.claude.json"
fi
_ensure_claude_workdir_trusted "$MOSAIC_AGENT_WORKDIR" "$SEAT_CLAUDE_JSON" || \
echo "WARNING: could not pre-trust workdir for claude agent $AGENT_NAME" >&2
fi
LAUNCH_COMMAND=(mosaic yolo "$MOSAIC_AGENT_RUNTIME")
if [ "$FLEET_SEAT" = 1 ]; then
# --dangerous keeps the seat on the same permissions footing `mosaic yolo` gave it;
# the composition, not the roster, decides harness home, bundle and settings.
LAUNCH_COMMAND=(mosaic fleet launch "$AGENT_NAME" --dangerous)
else
LAUNCH_COMMAND=(mosaic yolo "$MOSAIC_AGENT_RUNTIME")
fi
if [ -n "$MOSAIC_AGENT_MODEL" ]; then LAUNCH_COMMAND+=(--model "$MOSAIC_AGENT_MODEL"); fi
if [ -n "$MOSAIC_AGENT_REASONING" ]; then LAUNCH_COMMAND+=(--thinking "$MOSAIC_AGENT_REASONING"); fi
@@ -551,4 +551,23 @@ if contains_literal "$stop_args" ambient-socket; then
fail "exact stop trusted an ambient socket"
fi
# A seat scaffolded under ~/.mosaic owns its harness home, so the pane launches
# through the composition instead of the operator's own home. --dangerous keeps the
# seat on the permissions footing `mosaic yolo` gave it.
: > "$TMUX_CALLS"
HOME_SEAT="$ROOT/seat"
write_generated "$HOME_SEAT" "coder-seat"
mkdir -p "$HOME_SEAT/.mosaic/fleet/agents/coder-seat"
printf '{"schema":1,"harness":"pi","bundle":"primary"}\n' \
> "$HOME_SEAT/.mosaic/fleet/agents/coder-seat/profile.json"
run_start "$HOME_SEAT" "coder-seat"
seat_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$seat_args" | grep -qxF 'fleet' || fail "scaffolded seat did not launch through fleet launch"
echo "$seat_args" | grep -qxF 'launch' || fail "scaffolded seat did not launch through fleet launch"
echo "$seat_args" | grep -qxF 'coder-seat' || fail "fleet launch did not name the seat"
echo "$seat_args" | grep -qxF -- '--dangerous' || fail "scaffolded seat lost dangerous permissions"
if echo "$seat_args" | grep -qxF 'yolo'; then
fail "scaffolded seat still launched through mosaic yolo"
fi
echo 'ok - start-agent-session generated environment boundary'