3 Commits

Author SHA1 Message Date
11546d9bef feat(security): add GIT_AUTHOR_EMAIL per agent type
Some checks failed
CI / Check for .beads changes (pull_request) Successful in 6s
CI / Check embedded formulas (pull_request) Failing after 10s
CI / Test (pull_request) Failing after 1m18s
CI / Lint (pull_request) Failing after 14s
CI / Integration Tests (pull_request) Successful in 1m21s
Integration Tests / Integration Tests (pull_request) Successful in 1m20s
CI / Coverage Report (pull_request) Has been skipped
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 11s
CI / Test (push) Failing after 1m29s
CI / Lint (push) Failing after 15s
CI / Integration Tests (push) Successful in 1m19s
CI / Coverage Report (push) Has been skipped
Phase 1 of agent security model: Set distinct email addresses for each
agent type to improve audit trail clarity.

Email format:
- Town-level: {role}@gastown.local (mayor, deacon, boot)
- Rig-level: {rig}-{role}@gastown.local (witness, refinery)
- Named agents: {rig}-{role}-{name}@gastown.local (polecat, crew)

This makes git log filtering by agent type trivial and provides a
foundation for per-agent key separation in future phases.

Refs: hq-biot
2026-01-19 20:20:07 -08:00
d3bf408eba ci: disable block-internal-prs for fork workflow
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 12s
CI / Test (push) Failing after 1m18s
CI / Lint (push) Failing after 13s
CI / Integration Tests (push) Successful in 1m19s
CI / Coverage Report (push) Has been skipped
We use PRs for human review before merging in our fork.
2026-01-19 20:19:58 -08:00
34c77e883d feat(mayor): add escalation check to startup protocol
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 50s
CI / Test (push) Failing after 1m34s
CI / Lint (push) Failing after 52s
CI / Integration Tests (push) Successful in 1m55s
CI / Coverage Report (push) Has been skipped
Mayor now checks `gt escalate list` between hook and mail checks at startup.
This ensures pending escalations from other agents are handled promptly.

Other roles (witness, refinery, polecat, crew, deacon) are unaffected -
they create escalations but don't handle them at startup.
2026-01-18 23:07:02 -08:00
4 changed files with 25 additions and 55 deletions

View File

@@ -1,51 +0,0 @@
name: Block Internal PRs
on:
pull_request:
types: [opened, reopened]
jobs:
block-internal-prs:
name: Block Internal PRs
# Only run if PR is from the same repo (not a fork)
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Close PR and comment
uses: actions/github-script@v7
with:
script: |
const prNumber = context.issue.number;
const branch = context.payload.pull_request.head.ref;
const body = [
'**Internal PRs are not allowed.**',
'',
'Gas Town agents push directly to main. PRs are for external contributors only.',
'',
'To land your changes:',
'```bash',
'git checkout main',
'git merge ' + branch,
'git push origin main',
'git push origin --delete ' + branch,
'```',
'',
'See CLAUDE.md: "Crew workers push directly to main. No feature branches. NEVER create PRs."'
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed'
});
core.setFailed('Internal PR blocked. Push directly to main instead.');

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.