Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,19 @@ the spawn wrapper's own `--preamble`, so there is one copy of the answer). A
renamed or deleted profile is reported and ignored — a delivery is never
dropped over it.

That setting is box-wide. A single watch names its own worker, which beats it:

```bash
agent-box-webhook subscribe OWNER/REPO --deliver-to subagent \
--when '{"any":[{"path":"action","in":["opened"]}]}' \
--profile cheap-triage --note "standing watch: new issues"
```

So one repo can triage new issues cheaply and put something stronger on a red
build. The choice is stored on the subscription itself (`spawnConfig.profile`,
local-webhook 0.25.0) and shown by `agent-box-webhook ls`; `--profile ''`
clears it.

**New user or new session?** A user is the trust boundary; a session is a
unit of work, and sessions of one user are *not* isolated from each other.
The decision rule, the measurements behind it, and what "1 user = 1
Expand Down
8 changes: 8 additions & 0 deletions bin/agentbox
Original file line number Diff line number Diff line change
Expand Up @@ -2666,6 +2666,14 @@ class Renderer:
# the session CLI) has no flock on it.
f"export AGENT_BOX_FLOCK_BIN="
f"{shlex.quote(f'{self.bin}/flock')}\n"
# webhook-spawn.sh reads the dispatch file with jq to find
# the watch's own agent profile (issue #321), and its
# --preamble caller is the settings daemon, whose PATH does
# not carry jq. Every jq use in that script is guarded, so
# an unfound binary reports the wrong worker instead of
# failing. Pinned here for the same reason the module pins
# it into webhookSpawn's own derivation.
f"export AGENT_BOX_JQ_BIN={shlex.quote(f'{self.bin}/jq')}\n"
# Which agent a match starts, for --preamble's report:
# the spawn calls `agent-box-session add` with no
# --agent, so it is the box default. Unset, the report
Expand Down
239 changes: 201 additions & 38 deletions modules/agent-box.nix

Large diffs are not rendered by default.

23 changes: 21 additions & 2 deletions modules/agent-box.nix.in
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,12 @@ let
# (util-linux) is not on it, and the hook-session cap check has to hold
# the registry lock through the add it decides on.
export AGENT_BOX_FLOCK_BIN=${pkgs.util-linux}/bin/flock
# --preamble is run by the SETTINGS DAEMON, whose unit forces a PATH
# without jq (the receiver unit's PATH has it, but that is the other
# caller). Every jq use in this script is guarded, so an unfound binary
# would report the wrong worker rather than fail — pin it like the two
# above instead.
export AGENT_BOX_JQ_BIN=${pkgs.jq}/bin/jq
# Which agent a match really starts. The spawn calls `agent-box-session
# add` with no --agent, so it is the box default — and --preamble has to
# NAME it, because that is the half of "what does this watch launch" the
Expand Down Expand Up @@ -1940,9 +1946,22 @@ in
the marketplace itself and tracks its default branch — this pin only
governs the copy the box runs.

0.23.0 (this pin, issue #380) retired the built-in failures-only CI
0.25.0 (issue #321) lets a dispatch entry carry
`spawnConfig`, a flat map of strings the receiver hands to the spawn
command in `LOCAL_WEBHOOK_SPAWN_CONFIG`. Every other
`LOCAL_WEBHOOK_SPAWN_*` variable describes the EVENT, so two watches
on one repo used to be indistinguishable to webhook-spawn.sh; this
one describes the WATCH, which is what `agent-box-webhook subscribe
--profile NAME` writes and what lets a watch name the agent profile
its own sessions start on. AGENT_BOX_HOOK_PROFILE stays the box-wide
fallback. The same release queues dispatch batches per (key,
spawnConfig) rather than per key, so two watches on one repo cannot
coalesce into a single spawn that would hand one watch's events to
the other's worker.

0.23.0 (issue #380) retired the built-in failures-only CI
brake for rule-less dispatch entries: a `--deliver-to subagent`
watch now MUST carry its own `when`/`drop` rules or webhook_subscribe
watch MUST carry its own `when`/`drop` rules or webhook_subscribe
refuses to create it. `ignoreSenders` also became a pure sender mute
with no CI-outcome carve-out. webhook-cli.sh (see subscribeCmd
below) fills in a default `when` for a rule-less GitHub subagent
Expand Down
6 changes: 5 additions & 1 deletion modules/src/default-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ plainly rather than handing it back.
profile. Profile env is convenience, not isolation: every session of this
user can read it out of /proc. A standing webhook watch hands its work to
a profile through `agent-box-session env set AGENT_BOX_HOOK_PROFILE NAME`,
which is the only way to pick the harness a dispatched hook-* session runs.
which is how the harness a dispatched hook-* session runs gets picked at
all. That setting is box-wide; one watch picks its OWN worker with
`agent-box-webhook subscribe TOPIC --deliver-to subagent --profile NAME`,
which beats it. So cheap triage can take new issues while a red build
starts something that can fix it.

## Slash commands: type them into your own pane

Expand Down
14 changes: 12 additions & 2 deletions modules/src/settings-daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1325,7 +1325,8 @@ def webhook_unsubscribe(key, topic, dispatch):


def hook_args_stamp():
"""(mtime_ns, size) of the env file and of the profiles directory.
"""(mtime_ns, size) of the env file, the profiles directory and the
dispatch filter file.

Part of hook_preamble's cache key. The dispatch script reports the
hook-session arguments that file can override (#292), so its output is
Expand All @@ -1339,9 +1340,14 @@ def hook_args_stamp():
profile still exists decides whether the watch uses it at all. Creating
or removing one does not touch the env file, and every write goes through
a rename inside this directory, so its mtime moves on both.

The dispatch filter file joins them because a watch can now name its own
profile (spawnConfig.profile, issue #321): the report reads that file, so
re-subscribing a watch with a different --profile has to re-render. Every
write to it is an atomic rename, so the mtime moves.
"""
stamps = []
for path in (ENV_FILE, PROFILES_DIR):
for path in (ENV_FILE, PROFILES_DIR, os.path.join(webhook_state_dir(), "filter.dispatch.json")):
try:
info = os.stat(path)
except OSError:
Expand Down Expand Up @@ -1373,6 +1379,10 @@ def hook_preamble(topic, note, stamp):
try:
proc = subprocess.run(
[HOOK_SPAWN_CMD, "--preamble", topic, note],
# The script reads the dispatch file to find THIS watch's own
# profile (#321); under socket activation nothing else puts the
# state dir in this daemon's environment.
env=dict(os.environ, LOCAL_WEBHOOK_STATE_DIR=webhook_state_dir()),
check=False,
capture_output=True,
text=True,
Expand Down
57 changes: 55 additions & 2 deletions modules/src/webhook-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ usage: agent-box-webhook subscribe TOPIC [--note TEXT] [--ttl HOURS]
[--deliver-to session|subagent]
[--renew-on-event] [--ignore-sender LOGIN]...
[--when JSON] [--drop JSON]
[--claim SPEC]...
[--claim SPEC]... [--profile NAME]
agent-box-webhook unsubscribe TOPIC [--deliver-to session|subagent]
agent-box-webhook ls
agent-box-webhook status
Expand Down Expand Up @@ -129,6 +129,22 @@ reports through commit statuses, you stay exposed on that one shape.
--claim and --include are mutually exclusive; write --include yourself
only for rules --claim cannot express.

--profile NAME (subagent watches only) names the agent profile the sessions
THIS watch spawns start on — a harness, a model, an effort level, an appended
system prompt (agent-box-profile ls). It beats the box-wide
AGENT_BOX_HOOK_PROFILE, so one repo can triage new issues cheaply and put
something stronger on a red build:

agent-box-webhook subscribe OWNER/REPO --deliver-to subagent \
--when '{"any":[{"path":"action","in":["opened"]}]}' \
--profile cheap-triage --note "standing watch: new issues"

It is stored on the subscription as spawnConfig.profile (webhook.py's
--spawn-config), so `agent-box-webhook ls` shows it. --profile '' clears it.
A profile that does not exist when an event arrives is reported and ignored,
and the session starts on the box default — a delivery is never dropped for
a renamed profile.

--when / --drop attach payload rules to the subscription: deliver (or
spawn) ONLY events matching --when, never those matching --drop. Rules are
JSON — {"any"/"all": [...]} over {"path": "a.b.c", "in"/"notIn": [values]}
Expand Down Expand Up @@ -477,7 +493,7 @@ case "$cmd" in
ensure_state
if [ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ]; then
deliver_to=session; have_when=0; have_drop=0; topic=""; want=""
have_include=0; claims=""
have_include=0; claims=""; profile=""; have_profile=0
# Filter --claim out of the argument list as we scan it: webhook.py has
# never heard of that flag, so it is translated to --include below and
# must not survive into the exec. Rotate idiom — take from the front,
Expand All @@ -502,6 +518,7 @@ case "$cmd" in
exit 2
fi
claims="$claims $a"; want=""; continue ;;
(profile) profile="$a"; have_profile=1; want=""; continue ;;
esac
want=""
set -- "$@" "$a"; continue
Expand All @@ -519,6 +536,13 @@ case "$cmd" in
exit 2
fi
claims="$claims ${a#--claim=}"; continue ;;
# Filtered out like --claim, and for the same reason: webhook.py
# spells this --spawn-config profile=NAME, and the agent-box word for
# the thing is "profile" (issue #321). Empty is legal — it clears the
# profile on a re-subscribe — so, unlike --claim, only the missing
# VALUE is an error.
--profile) want=profile; continue ;;
--profile=*) profile="${a#--profile=}"; have_profile=1; continue ;;
--*) ;;
*) [ -n "$topic" ] || topic="$a" ;;
esac
Expand All @@ -528,6 +552,35 @@ case "$cmd" in
echo "agent-box-webhook: --$want needs a value" >&2
exit 2
fi
if [ "$have_profile" = 1 ]; then
# A session subscription spawns nothing, so a profile on one would be a
# setting no code path reads. webhook.py refuses it too; saying it here
# names the flag the caller actually typed.
if [ "$deliver_to" != subagent ]; then
echo "agent-box-webhook: --profile only applies to a standing watch —" \
"it names the worker a spawned session starts on, and a session" \
"subscription spawns nothing; add --deliver-to subagent" >&2
exit 2
fi
case "$profile" in
("") set -- "$@" --no-spawn-config ;;
(*[!A-Za-z0-9_-]*)
echo "agent-box-webhook: --profile '$profile' is not a valid profile" \
"name (A-Za-z0-9_-)" >&2
exit 2 ;;
(*)
# A warning, never a refusal: profiles are runtime data, and
# subscribing a watch before creating its profile is a legitimate
# order to do things in. The spawn falls back to the box default
# and says so if the profile is still missing when an event lands.
if [ ! -r "$HOME/.config/agent-box/profiles/$profile.env" ]; then
echo "agent-box-webhook: no profile '$profile' on this box yet" \
"(agent-box-profile ls) — subscribing anyway; a match starts" \
"the box default until you create it" >&2
fi
set -- "$@" --spawn-config "profile=$profile" ;;
esac
fi
if [ -n "$claims" ]; then
if [ "$have_include" = 1 ]; then
echo "agent-box-webhook: pass --claim OR --include, not both —" \
Expand Down
11 changes: 9 additions & 2 deletions modules/src/webhook-policy-apply.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ fi
# For every entry whose topic is declared: the declaration REPLACES the
# managed fields (the payload predicates, ignoreSenders, note) wholesale —
# partial merges would let config and state drift apart. Runtime fields
# (ttlHours, renewOnEvent, timestamps) stay the entry's own. Bare-string
# topics normalize to the object form webhook.py itself writes.
# (ttlHours, renewOnEvent, spawnConfig, timestamps) stay the entry's own.
# Bare-string topics normalize to the object form webhook.py itself writes.
#
# spawnConfig is on that list deliberately (issue #321): it names an agent
# PROFILE, and a profile is runtime data a user creates with
# agent-box-profile — a declared value could name one that does not exist on
# the box, which is the same reason AGENT_BOX_HOOK_PROFILE has no NixOS option
# beside it. So `agent-box-webhook subscribe --profile` survives a receiver
# restart on a governed watch, rather than being reverted by a rebuild.
#
# local-webhook 0.19.0 renamed the two predicates when -> include and
# drop -> exclude (local-channels#294). It still ACCEPTS the old names on
Expand Down
Loading