feat(security): add GIT_AUTHOR_EMAIL per agent type #1

Closed
johno wants to merge 2 commits from feat/agent-email-identity into main
3 changed files with 25 additions and 4 deletions

View File

@@ -49,36 +49,43 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
case "mayor": case "mayor":
env["BD_ACTOR"] = "mayor" env["BD_ACTOR"] = "mayor"
env["GIT_AUTHOR_NAME"] = "mayor" env["GIT_AUTHOR_NAME"] = "mayor"
env["GIT_AUTHOR_EMAIL"] = "mayor@gastown.local"
case "deacon": case "deacon":
env["BD_ACTOR"] = "deacon" env["BD_ACTOR"] = "deacon"
env["GIT_AUTHOR_NAME"] = "deacon" env["GIT_AUTHOR_NAME"] = "deacon"
env["GIT_AUTHOR_EMAIL"] = "deacon@gastown.local"
case "boot": case "boot":
env["BD_ACTOR"] = "deacon-boot" env["BD_ACTOR"] = "deacon-boot"
env["GIT_AUTHOR_NAME"] = "boot" env["GIT_AUTHOR_NAME"] = "boot"
env["GIT_AUTHOR_EMAIL"] = "boot@gastown.local"
case "witness": case "witness":
env["GT_RIG"] = cfg.Rig env["GT_RIG"] = cfg.Rig
env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig) env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig)
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", cfg.Rig) env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", cfg.Rig)
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-witness@gastown.local", cfg.Rig)
case "refinery": case "refinery":
env["GT_RIG"] = cfg.Rig env["GT_RIG"] = cfg.Rig
env["BD_ACTOR"] = fmt.Sprintf("%s/refinery", cfg.Rig) env["BD_ACTOR"] = fmt.Sprintf("%s/refinery", cfg.Rig)
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", cfg.Rig) env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", cfg.Rig)
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-refinery@gastown.local", cfg.Rig)
case "polecat": case "polecat":
env["GT_RIG"] = cfg.Rig env["GT_RIG"] = cfg.Rig
env["GT_POLECAT"] = cfg.AgentName env["GT_POLECAT"] = cfg.AgentName
env["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", cfg.Rig, cfg.AgentName) env["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", cfg.Rig, cfg.AgentName)
env["GIT_AUTHOR_NAME"] = cfg.AgentName env["GIT_AUTHOR_NAME"] = cfg.AgentName
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-polecat-%s@gastown.local", cfg.Rig, cfg.AgentName)
case "crew": case "crew":
env["GT_RIG"] = cfg.Rig env["GT_RIG"] = cfg.Rig
env["GT_CREW"] = cfg.AgentName env["GT_CREW"] = cfg.AgentName
env["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", cfg.Rig, cfg.AgentName) env["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", cfg.Rig, cfg.AgentName)
env["GIT_AUTHOR_NAME"] = cfg.AgentName env["GIT_AUTHOR_NAME"] = cfg.AgentName
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-crew-%s@gastown.local", cfg.Rig, cfg.AgentName)
} }
// Only set GT_ROOT if provided // Only set GT_ROOT if provided

View File

@@ -14,6 +14,7 @@ func TestAgentEnv_Mayor(t *testing.T) {
assertEnv(t, env, "GT_ROLE", "mayor") assertEnv(t, env, "GT_ROLE", "mayor")
assertEnv(t, env, "BD_ACTOR", "mayor") assertEnv(t, env, "BD_ACTOR", "mayor")
assertEnv(t, env, "GIT_AUTHOR_NAME", "mayor") assertEnv(t, env, "GIT_AUTHOR_NAME", "mayor")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "mayor@gastown.local")
assertEnv(t, env, "GT_ROOT", "/town") assertEnv(t, env, "GT_ROOT", "/town")
assertNotSet(t, env, "GT_RIG") assertNotSet(t, env, "GT_RIG")
assertNotSet(t, env, "BEADS_NO_DAEMON") assertNotSet(t, env, "BEADS_NO_DAEMON")
@@ -31,6 +32,7 @@ func TestAgentEnv_Witness(t *testing.T) {
assertEnv(t, env, "GT_RIG", "myrig") assertEnv(t, env, "GT_RIG", "myrig")
assertEnv(t, env, "BD_ACTOR", "myrig/witness") assertEnv(t, env, "BD_ACTOR", "myrig/witness")
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/witness") assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/witness")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-witness@gastown.local")
assertEnv(t, env, "GT_ROOT", "/town") assertEnv(t, env, "GT_ROOT", "/town")
} }
@@ -49,6 +51,7 @@ func TestAgentEnv_Polecat(t *testing.T) {
assertEnv(t, env, "GT_POLECAT", "Toast") assertEnv(t, env, "GT_POLECAT", "Toast")
assertEnv(t, env, "BD_ACTOR", "myrig/polecats/Toast") assertEnv(t, env, "BD_ACTOR", "myrig/polecats/Toast")
assertEnv(t, env, "GIT_AUTHOR_NAME", "Toast") assertEnv(t, env, "GIT_AUTHOR_NAME", "Toast")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-polecat-Toast@gastown.local")
assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/Toast") assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/Toast")
assertEnv(t, env, "BEADS_NO_DAEMON", "1") assertEnv(t, env, "BEADS_NO_DAEMON", "1")
} }
@@ -68,6 +71,7 @@ func TestAgentEnv_Crew(t *testing.T) {
assertEnv(t, env, "GT_CREW", "emma") assertEnv(t, env, "GT_CREW", "emma")
assertEnv(t, env, "BD_ACTOR", "myrig/crew/emma") assertEnv(t, env, "BD_ACTOR", "myrig/crew/emma")
assertEnv(t, env, "GIT_AUTHOR_NAME", "emma") assertEnv(t, env, "GIT_AUTHOR_NAME", "emma")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-crew-emma@gastown.local")
assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/emma") assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/emma")
assertEnv(t, env, "BEADS_NO_DAEMON", "1") assertEnv(t, env, "BEADS_NO_DAEMON", "1")
} }
@@ -85,6 +89,7 @@ func TestAgentEnv_Refinery(t *testing.T) {
assertEnv(t, env, "GT_RIG", "myrig") assertEnv(t, env, "GT_RIG", "myrig")
assertEnv(t, env, "BD_ACTOR", "myrig/refinery") assertEnv(t, env, "BD_ACTOR", "myrig/refinery")
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/refinery") assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/refinery")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-refinery@gastown.local")
assertEnv(t, env, "BEADS_NO_DAEMON", "1") assertEnv(t, env, "BEADS_NO_DAEMON", "1")
} }
@@ -98,6 +103,7 @@ func TestAgentEnv_Deacon(t *testing.T) {
assertEnv(t, env, "GT_ROLE", "deacon") assertEnv(t, env, "GT_ROLE", "deacon")
assertEnv(t, env, "BD_ACTOR", "deacon") assertEnv(t, env, "BD_ACTOR", "deacon")
assertEnv(t, env, "GIT_AUTHOR_NAME", "deacon") assertEnv(t, env, "GIT_AUTHOR_NAME", "deacon")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "deacon@gastown.local")
assertEnv(t, env, "GT_ROOT", "/town") assertEnv(t, env, "GT_ROOT", "/town")
assertNotSet(t, env, "GT_RIG") assertNotSet(t, env, "GT_RIG")
assertNotSet(t, env, "BEADS_NO_DAEMON") assertNotSet(t, env, "BEADS_NO_DAEMON")
@@ -113,6 +119,7 @@ func TestAgentEnv_Boot(t *testing.T) {
assertEnv(t, env, "GT_ROLE", "boot") assertEnv(t, env, "GT_ROLE", "boot")
assertEnv(t, env, "BD_ACTOR", "deacon-boot") assertEnv(t, env, "BD_ACTOR", "deacon-boot")
assertEnv(t, env, "GIT_AUTHOR_NAME", "boot") assertEnv(t, env, "GIT_AUTHOR_NAME", "boot")
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "boot@gastown.local")
assertEnv(t, env, "GT_ROOT", "/town") assertEnv(t, env, "GT_ROOT", "/town")
assertNotSet(t, env, "GT_RIG") assertNotSet(t, env, "GT_RIG")
assertNotSet(t, env, "BEADS_NO_DAEMON") assertNotSet(t, env, "BEADS_NO_DAEMON")

View File

@@ -35,7 +35,9 @@ drive shaft - if you stall, the whole town stalls.
**Your startup behavior:** **Your startup behavior:**
1. Check hook (`gt hook`) 1. Check hook (`gt hook`)
2. If work is hooked → EXECUTE (no announcement beyond one line, no waiting) 2. If work is hooked → EXECUTE (no announcement beyond one line, no waiting)
3. If hook empty → Check mail, then wait for user instructions 3. If hook empty → Check escalations (`gt escalate list`)
4. Handle any pending escalations (these are urgent items from other agents)
5. Check mail, then wait for user instructions
**Note:** "Hooked" means work assigned to you. This triggers autonomous mode even **Note:** "Hooked" means work assigned to you. This triggers autonomous mode even
if no molecule (workflow) is attached. Don't confuse with "pinned" which is for if no molecule (workflow) is attached. Don't confuse with "pinned" which is for
@@ -241,16 +243,21 @@ Like crew, you're human-managed. But the hook protocol still applies:
gt hook # Shows hooked work (if any) gt hook # Shows hooked work (if any)
# Step 2: Work hooked? → RUN IT # Step 2: Work hooked? → RUN IT
# Hook empty? → Check mail for attached work
# Step 3: Hook empty? → Check escalations (mayor-specific)
gt escalate list # Shows pending escalations from other agents
# Handle any pending escalations - these are urgent items requiring your attention
# Step 4: Check mail for attached work
gt mail inbox gt mail inbox
# If mail contains attached work, hook it: # If mail contains attached work, hook it:
gt mol attach-from-mail <mail-id> gt mol attach-from-mail <mail-id>
# Step 3: Still nothing? Wait for user instructions # Step 5: Still nothing? Wait for user instructions
# You're the Mayor - the human directs your work # You're the Mayor - the human directs your work
``` ```
**Work hooked → Run it. Hook empty → Check mail. Nothing anywhere → Wait for user.** **Work hooked → Run it. Hook empty → Check escalations → Check mail. Nothing anywhere → Wait for user.**
Your hooked work persists across sessions. Handoff mail (🤝 HANDOFF subject) provides context notes. Your hooked work persists across sessions. Handoff mail (🤝 HANDOFF subject) provides context notes.