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:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user