fix(fleet): enforce exact comms authority (#787)
This commit was merged in pull request #787.
This commit is contained in:
@@ -5,20 +5,20 @@ Tool suites live at `~/.config/mosaic/tools/<suite>/`. This is the index only.
|
||||
read it (or the relevant service guide) when your task actually touches that service.
|
||||
Project-specific tooling belongs in the project's `AGENTS.md`, not here.
|
||||
|
||||
## ⚡ Most-used fleet tools (reach for these FIRST — don't hand-roll)
|
||||
## Most-used fleet tools (reach for these first)
|
||||
|
||||
You are a Mosaic fleet agent. These cover the highest-frequency cross-agent and git-provider
|
||||
tasks — use them before improvising with raw `tmux send-keys`, raw `tea`/`gh`/`glab`, or `curl`.
|
||||
<!-- fleet-comms-contract: 1 -->
|
||||
|
||||
**1. Message another agent** → `tools/tmux/agent-send.sh` (NOT raw `tmux send-keys`):
|
||||
You are a Mosaic fleet agent. Use the runtime-composed **Fleet Comms — authoritative exact targets**
|
||||
section for inter-agent messaging. It renders your authoritative local host, exact agent/session, resolved
|
||||
tmux socket, installed helper path, generation, and one executable command per known peer.
|
||||
|
||||
```bash
|
||||
tools/tmux/agent-send.sh -s <target-session> -m "message" # or -f <file> to send a file's contents
|
||||
```
|
||||
Select only a peer row rendered for your exact roster identity. Never invent, substitute, or fuzzy-match
|
||||
a host, session, socket, SSH destination, or helper path. If a peer is absent, stop and run the exact
|
||||
self-scoped discovery command shown in that composed section; report the peer as unknown if it remains
|
||||
absent. Do not use raw `tmux send-keys` for fleet messaging.
|
||||
|
||||
The coordinator session is `mos-claude` — send status, findings, and questions there.
|
||||
|
||||
**2. Issues / PRs / milestones** → `tools/git/*.sh` wrappers (before raw `tea`/`gh`/`glab`):
|
||||
**Issues / PRs / milestones** → `tools/git/*.sh` wrappers (before raw `tea`/`gh`/`glab`):
|
||||
|
||||
```bash
|
||||
tools/git/pr-create.sh ... tools/git/issue-create.sh ... tools/git/pr-merge.sh ...
|
||||
|
||||
@@ -94,11 +94,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ssh": {
|
||||
"description": "SSH target (user@host) for a cross-host peer, so onboarding renders the `agent-send.sh -H <user@host>` form. Optional; only needed for agents on a different host than the fleet.",
|
||||
"description": "Explicit SSH target (normally user@host) for a cross-host inventory peer. Exact comms rendering requires this whenever the peer's resolved host differs from the current agent's host; the host value is never substituted as an SSH destination.",
|
||||
"type": "string"
|
||||
},
|
||||
"socket": {
|
||||
"description": "tmux socket the agent's session runs on. Onboarding renders `-L <socket>` when set; absent = the default socket (no `-L`). Must match the LIVE socket, not blindly inherit the roster's tmux.socket_name.",
|
||||
"description": "Optional compatibility declaration of the fleet-wide tmux socket. When present it must exactly equal tmux.socket_name; independent per-agent sockets are rejected because the local fleet runtime provisions every session on the fleet-wide socket.",
|
||||
"type": "string"
|
||||
},
|
||||
"working_directory": {
|
||||
@@ -150,29 +150,67 @@
|
||||
"description": "Orchestrator chat connector (F4). Optional — absent means tmux (back-compat). Secrets (access/bot tokens) come from the environment, never this file.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind"],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": ["tmux", "discord", "matrix"]
|
||||
},
|
||||
"matrix": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["homeserver_url", "user_id", "room_id"],
|
||||
"properties": {
|
||||
"homeserver_url": { "type": "string" },
|
||||
"user_id": { "type": "string" },
|
||||
"room_id": { "type": "string" }
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": { "kind": { "const": "tmux" } },
|
||||
"required": ["kind"],
|
||||
"not": {
|
||||
"anyOf": [{ "required": ["discord"] }, { "required": ["matrix"] }]
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["channel_id"],
|
||||
{
|
||||
"properties": {
|
||||
"channel_id": { "type": "string" }
|
||||
}
|
||||
"kind": { "const": "discord" },
|
||||
"discord": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["channel_id"],
|
||||
"properties": {
|
||||
"channel_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["kind", "discord"],
|
||||
"not": { "required": ["matrix"] }
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": { "const": "matrix" },
|
||||
"matrix": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["homeserver_url", "user_id", "room_id"],
|
||||
"properties": {
|
||||
"homeserver_url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["kind", "matrix"],
|
||||
"not": { "required": ["discord"] }
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"kind": { "enum": ["tmux", "discord", "matrix"] },
|
||||
"matrix": { "type": "object" },
|
||||
"discord": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
||||
# fleet/* — the framework SEEDS fleet/examples, fleet/roles, fleet/profiles, and
|
||||
# fleet/roster.schema.json (synced normally — every fleet/roles/*.md role contract
|
||||
# and fleet/profiles/*.yaml system-type profile lands automatically via this sync,
|
||||
# so no per-file entry is needed; the preserved "fleet/*.yaml" glob is anchored to
|
||||
# the top level only and does NOT shadow fleet/profiles/*.yaml). The user's
|
||||
# so no per-file entry is needed; exact preserved roster paths are anchored to
|
||||
# the top level only and do NOT shadow fleet/profiles/*.yaml). The user's
|
||||
# own fleet files MUST
|
||||
# survive `mosaic update` (which runs this sync automatically): the active
|
||||
# roster (`fleet/roster.yaml` + any other `fleet/*.yaml`), per-agent env
|
||||
# rosters (`fleet/roster.yaml` and `fleet/roster.json`), per-agent env
|
||||
# (`fleet/agents/`), heartbeat run dir (`fleet/run/`), and the Mosaic-native
|
||||
# backlog-of-record store (`fleet/backlog/` — embedded PGlite data dir; see
|
||||
# packages/mosaic/src/commands/fleet-backlog.ts). Without these, an update
|
||||
@@ -44,7 +44,7 @@ INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
||||
# and user-ADDED personas instead live in fleet/roles.local/ and MUST survive
|
||||
# `mosaic update` — they win over the baseline on merge (AC-NS-7; see
|
||||
# packages/mosaic/src/commands/fleet-personas.ts).
|
||||
PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials" "fleet/*.yaml" "fleet/agents" "fleet/run" "fleet/backlog" "fleet/roles.local")
|
||||
PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials" "fleet/roster.yaml" "fleet/roster.json" "fleet/agents" "fleet/run" "fleet/backlog" "fleet/roles.local")
|
||||
|
||||
# Framework-owned contract files: re-copied from defaults/ on every upgrade (the
|
||||
# user must not edit them; a divergent copy is backed up once before overwrite).
|
||||
@@ -200,8 +200,8 @@ sync_framework() {
|
||||
return
|
||||
fi
|
||||
|
||||
# Fallback: cp-based sync. Glob-aware so entries like "fleet/*.yaml" preserve
|
||||
# every matching user file (parity with the rsync --exclude path above).
|
||||
# Fallback: cp-based sync. Exact top-level preserved paths mirror the
|
||||
# root-anchored rsync excludes above.
|
||||
local preserve_tmp=""
|
||||
if [[ "$INSTALL_MODE" == "keep" ]]; then
|
||||
preserve_tmp="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-preserve-XXXXXX")"
|
||||
|
||||
@@ -61,25 +61,33 @@ MOSAIC_HOME="$T5" MOSAIC_INSTALL_MODE=bogus MOSAIC_SYNC_ONLY=1 bash "$INSTALL" >
|
||||
chk "F5 failure: invalid mode rejected (nonzero exit)" "[ $rc -ne 0 ]"
|
||||
chk "F5 failure: SOUL + credentials intact" "grep -q orig '$T5/SOUL.md' && grep -q keepme '$T5/credentials/c.json'"
|
||||
|
||||
# F6 — keep-mode re-seed (the `mosaic update` path) MUST NOT wipe user fleet data.
|
||||
# Regression for the roster-loss bug: fleet/ was not in PRESERVE_PATHS.
|
||||
# F6 — keep-mode re-seed (the `mosaic update` path) MUST preserve only the
|
||||
# exact user-owned roster paths while refreshing framework-owned schema/examples.
|
||||
T6=$(mktemp -d); mkdir -p "$T6/fleet/examples" "$T6/fleet/run" "$T6/fleet/agents"
|
||||
printf '# persona\n' > "$T6/SOUL.md" # makes it a recognized existing install (→ keep mode)
|
||||
printf 'version: 1\nagents:\n - name: coder0\n' > "$T6/fleet/roster.yaml"
|
||||
printf 'version: 1\nagents:\n - name: custom\n' > "$T6/fleet/my-fleet.yaml"
|
||||
printf '{"version":1,"agents":[{"name":"json-user"}]}\n' > "$T6/fleet/roster.json"
|
||||
printf 'version: 1\nagents:\n - name: not-active-roster\n' > "$T6/fleet/my-fleet.yaml"
|
||||
printf 'ts=x\n' > "$T6/fleet/run/coder0.hb"
|
||||
printf 'MOSAIC_AGENT_NAME=coder0\n' > "$T6/fleet/agents/coder0.env"
|
||||
printf '# stale preset\n' > "$T6/fleet/examples/general.yaml"
|
||||
printf '{"stale":true}\n' > "$T6/fleet/roster.schema.json"
|
||||
E6=$(mktemp -d)
|
||||
cp "$T6/fleet/roster.yaml" "$E6/roster-yaml.expected"
|
||||
cp "$T6/fleet/roster.json" "$E6/roster-json.expected"
|
||||
cp "$T6/fleet/run/coder0.hb" "$E6/run.expected"
|
||||
cp "$T6/fleet/agents/coder0.env" "$E6/agent.expected"
|
||||
echo 3 > "$T6/.framework-version"
|
||||
run "$T6" keep
|
||||
chk "F6 reseed: user roster.yaml SURVIVES keep-mode sync" "grep -q coder0 '$T6/fleet/roster.yaml'"
|
||||
chk "F6 reseed: other user fleet/*.yaml survives (glob)" "[ -f '$T6/fleet/my-fleet.yaml' ]"
|
||||
chk "F6 reseed: per-agent env (fleet/agents) survives" "[ -f '$T6/fleet/agents/coder0.env' ]"
|
||||
chk "F6 reseed: heartbeat run dir (fleet/run) survives" "[ -f '$T6/fleet/run/coder0.hb' ]"
|
||||
chk "F6 reseed: framework examples ARE refreshed (not preserved stale)" "grep -q orchestrator '$T6/fleet/examples/general.yaml'"
|
||||
chk "F6 reseed: framework roster.schema.json seeded" "[ -f '$T6/fleet/roster.schema.json' ]"
|
||||
chk "F6 reseed: exact roster.yaml bytes survive keep-mode sync" "cmp -s '$T6/fleet/roster.yaml' '$E6/roster-yaml.expected'"
|
||||
chk "F6 reseed: exact roster.json bytes survive keep-mode sync" "cmp -s '$T6/fleet/roster.json' '$E6/roster-json.expected'"
|
||||
chk "F6 reseed: unrelated fleet YAML is not preserved" "[ ! -f '$T6/fleet/my-fleet.yaml' ]"
|
||||
chk "F6 reseed: per-agent env bytes survive" "cmp -s '$T6/fleet/agents/coder0.env' '$E6/agent.expected'"
|
||||
chk "F6 reseed: heartbeat bytes survive" "cmp -s '$T6/fleet/run/coder0.hb' '$E6/run.expected'"
|
||||
chk "F6 reseed: framework examples are refreshed" "grep -q orchestrator '$T6/fleet/examples/general.yaml'"
|
||||
chk "F6 reseed: framework roster schema is refreshed" "cmp -s '$T6/fleet/roster.schema.json' '$FW/fleet/roster.schema.json'"
|
||||
|
||||
rm -rf "$T1" "$T2" "$T3" "$T4" "$T5" "$T6"
|
||||
rm -rf "$T1" "$T2" "$T3" "$T4" "$T5" "$T6" "$E6"
|
||||
echo
|
||||
echo "RESULT: $pass passed, $fail failed"
|
||||
[ "$fail" -eq 0 ]
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression checks for connector-kind-conditional fleet roster schema."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
schema_path = Path(__file__).resolve().parents[3] / "fleet" / "roster.schema.json"
|
||||
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||
validator = Draft202012Validator(schema)
|
||||
base = {
|
||||
"version": 1,
|
||||
"transport": "tmux",
|
||||
"agents": [{"name": "orchestrator", "runtime": "pi"}],
|
||||
}
|
||||
|
||||
valid = [
|
||||
{"kind": "tmux"},
|
||||
{"kind": "discord", "discord": {"channel_id": "123"}},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "https://matrix.example",
|
||||
"user_id": "@mosaic:example",
|
||||
"room_id": "!room:example",
|
||||
},
|
||||
},
|
||||
]
|
||||
invalid = [
|
||||
{"kind": "tmux", "discord": {"channel_id": "123"}},
|
||||
{"kind": "tmux", "matrix": {}},
|
||||
{"kind": "discord"},
|
||||
{"kind": "discord", "matrix": {}},
|
||||
{
|
||||
"kind": "discord",
|
||||
"discord": {"channel_id": "123"},
|
||||
"matrix": {
|
||||
"homeserver_url": "https://matrix.example",
|
||||
"user_id": "@mosaic:example",
|
||||
"room_id": "!room:example",
|
||||
},
|
||||
},
|
||||
{"kind": "matrix"},
|
||||
{"kind": "matrix", "discord": {"channel_id": "123"}},
|
||||
{"kind": "discord", "discord": {"channel_id": ""}},
|
||||
{"kind": "discord", "discord": {"channel_id": " "}},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "",
|
||||
"user_id": "@mosaic:example",
|
||||
"room_id": "!room:example",
|
||||
},
|
||||
},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "\t",
|
||||
"user_id": "@mosaic:example",
|
||||
"room_id": "!room:example",
|
||||
},
|
||||
},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "https://matrix.example",
|
||||
"user_id": "",
|
||||
"room_id": "!room:example",
|
||||
},
|
||||
},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "https://matrix.example",
|
||||
"user_id": " ",
|
||||
"room_id": "!room:example",
|
||||
},
|
||||
},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "https://matrix.example",
|
||||
"user_id": "@mosaic:example",
|
||||
"room_id": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"kind": "matrix",
|
||||
"matrix": {
|
||||
"homeserver_url": "https://matrix.example",
|
||||
"user_id": "@mosaic:example",
|
||||
"room_id": "\n",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for connector in valid:
|
||||
errors = list(validator.iter_errors({**base, "connector": connector}))
|
||||
if errors:
|
||||
print(f"expected valid connector {connector}: {errors}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
for connector in invalid:
|
||||
if not list(validator.iter_errors({**base, "connector": connector})):
|
||||
print(f"expected invalid connector: {connector}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
print("connector schema regression: PASS")
|
||||
Reference in New Issue
Block a user