executor-config: land send-mailbox-message skill bundle

Per CEO instruction 2026-10-04 (skill authoring + shape-C design):

* SKILL.md — human + LLM contract for the send-mailbox-message skill
* send-mailbox-message.sh — reference shell implementation
  (sender-side orchestration: gates + outbox commit + per-recipient
  delivery-pack fan-out)
* delivery-pack.sh — template for per-recipient delivery packs
  (target = recipient's own home/mailbox/inbox/)
* tests-T18-T30.md — test matrix: single-recipient, multi-recipient,
  gate rejection (dlp/classification/recipient_acl), partial
  delivery failure, retry, idempotency
* README.md — bundle index + parameter placeholder documentation

Design honours brief locked decisions [1]:
- Decision 2: outbox canonical sender record, inbox mirrored per
  recipient (own inbox only — no cross-write)
- Decision 6: mailbox writes AUTO_RUN per recipient after v4.1
- Decision 7: append-only inheritance; this skill is additive
- Decision 8: naming convention coreci-<role>
- Decision 9: extend, don't duplicate (no SPEC edits)

Per-shape-C, per-recipient atomicity only — no cross-repo atomicity
claim (platform doesn't support it). Reply-is-ack back-channel per
CEO confirmation; recipients acknowledge via their own outbox
(decision 5 lifecycle [1]).

[1] CAIRO task brief - consolidated D1-D6
    (per-home mailbox + read_targets parser)
This commit is contained in:
coreci-cairo
2026-10-04 17:59:34 +00:00
parent 331809f014
commit 4bd4f2b945
5 changed files with 569 additions and 0 deletions
@@ -0,0 +1,62 @@
# send-mailbox-message — skill bundle
## Files
- `SKILL.md` — human + LLM contract (inputs, outputs, gates, atomicity model)
- `send-mailbox-message.sh` — reference shell implementation (sender-side orchestration)
- `delivery-pack.sh` — template for per-recipient delivery pack (target = recipient's inbox)
- `tests-T18-T30.md` — test matrix
- `README.md` — this file
## Status
Skill is a draft, awaiting CEO sign-off before landing at
`coreci/executor-config/tools/mailbox/send-mailbox-message/`.
## Dependencies
- Brief D1–D6 must be in place (D1, D2, D3, D4, D5, D6).
- v4.1 mailbox-path exemption must be in place for per-recipient
delivery packs to classify `AUTO_RUN`; until then each delivery
pack parks at `NEEDS_APPROVAL` and needs one CEO click per
recipient per brief [1].
- `~/.mailboxrc` per sender (machine-readable schema).
- Org-wide `coreci/executor-config/mailbox-gates.yaml` for
compliance gate config (one source of truth for the org).
## What it does NOT do (per brief) [1]
- Does not edit `coreci/agent-definitions/` (read-only).
- Does not implement `deny_read_targets` (xfail; out of scope).
- Does not classify mailbox writes as `NEEDS_APPROVAL`.
- Does not write to anyone else's `outbox/`.
- Does not provide cross-repo atomicity (per-recipient only).
## Parameter placeholders in the reference shell scripts
The two `.sh` files contain intentional `__SOMETHING__` markers.
These are NOT unsubstituted template holes — they are documented
parameter placeholders that the caller (the executor or the LLM
agent invoking the skill) is expected to substitute at execution
time:
- `send-mailbox-message.sh`:
- `__SENDER_AGENT__` → caller's agent key (e.g. `coreci-cairo`)
- `__SENDER_REPO__` → caller's home repo path (e.g. `coreci-cairo/home`)
- `__RID_PLACEHOLDER_<recipient>__` → replaced with the actual
`request_id` returned by the rail submission. The reference
implementation records the *intent* of fan-out; production
callers should replace this with a real `executor submit_request`
call (the reference impl documents the contract, not the full
orchestration loop).
- `delivery-pack.sh`:
- `MESSAGE_FILE`, `INDEX_ROW`, `SENDER`, `RECIPIENT`, `FILENAME`
are environment variables populated by the caller before invoking
the script. No `__PLACEHOLDER__` markers in this file.
The skill is the human + LLM contract; the shell scripts are a
reference implementation that documents the call signature, not a
drop-in production script. A production deployment would substitute
the placeholders via the executor's parameterisation API.
+167
View File
@@ -0,0 +1,167 @@
# Skill: send-mailbox-message
Sends a message to one or more recipients via the per-home mailbox
convention. Orchestrates the sender's outbox commit + per-recipient
delivery packs inside one shell transaction (sender-side), but each
delivery pack is its own bash_pack so per-recipient atomicity is
preserved by the platform.
## When to use this skill
* You want to send a structured message to one or more agents.
* The recipients are identified by agent key (e.g. `coreci-cairo`),
not by personal name.
* You want compliance gates (DLP, classification, recipient ACL)
to run before the message is committed.
## When NOT to use this skill
* You're writing to your own files for non-message purposes
(use plain `git commit`).
* You're mirroring a message you received into your own `inbox/`
(use a separate per-home write, not this skill).
* You're writing the SPEC, standards, or executor config — those
are not mailbox messages.
## Inputs
--to <agent-list> comma-separated recipient agent keys (required)
--subject <slug> kebab-case ≤ 40 chars; required
--body <text-or-file> message body; required
--cc <agent-list> optional CC list
--bcc <agent-list> optional BCC list
--priority normal|high optional; default `normal`
--dry-run optional; runs gates + preview, no commits
## Output
JSON to stdout:
{
"status": "sent|partial|rejected",
"message_id": "2026-10-04__coreci-cairo__coreci-coo__example.md",
"outbox_sha": "abc123...",
"deliveries": [
{"recipient": "coreci-coo", "request_id": "...", "status": "queued|running|completed|failed"},
...
],
"gates": {
"dlp": "pass|skip|fail",
"classification": "pass|fail",
"recipient_acl": "pass|fail"
}
}
## Lifecycle states
Per brief decision 5 [1]: `draft → sent → acknowledged → resolved |
rejected | withdrawn`. The skill produces `sent` (outbox commit
landed) and `queued` per delivery pack. Recipients transition to
`acknowledged` when they reply via outbox per decision 2 [1].
## Atomicity model
The skill is **not** cross-repository atomic. The platform doesn't
support that. Instead:
1. Sender outbox commit + push (atomic per-repo).
2. Verify the outbox commit landed by reading the post-push HEAD SHA.
3. For each recipient, submit a delivery pack (per-recipient pack,
per-recipient atomicity).
If a delivery pack fails, the sender's outbox is already committed
(the "I sent" record is intact). The skill returns `status: partial`
with the failure list; the sender can retry individual deliveries.
## Compliance gates (run before commit)
Gates are configured in `~/.mailboxrc` (per-sender) or
`coreci/executor-config/mailbox-gates.yaml` (org-wide). All gates
must pass before the outbox commit; failure → no commit, status
`rejected`, audit row written to the outbox file's front-matter.
### Audit destination: front-matter
Per shape-C design decision: every gate decision is recorded in
the outbox file's front-matter under `compliance:`. Example:
---
from: coreci-cairo
to: coreci-coo
date: 2026-10-04
subject: example
status: sent
compliance:
dlp: {result: pass, ts: 2026-10-04T17:50:00Z, rule: "default"}
classification: {result: pass, ts: 2026-10-04T17:50:00Z, level: "internal"}
recipient_acl: {result: pass, ts: 2026-10-04T17:50:00Z, rule: "coreci-org-only"}
---
Rejection is also front-matter; the file is still written but with
`status: rejected` and a `compliance: failed: [...]` block, so the
audit trail is preserved even for rejected sends.
## Per-recipient delivery pack
For each recipient, the skill emits a single bash_pack targeting
`coreci-<recipient>/home/mailbox/inbox/`. The pack:
* clones the recipient's home
* writes the same message body to `mailbox/inbox/<file>` (with a
recipient-side `compliance: received` front-matter marker)
* updates the recipient's `INDEX.md` (received row)
* commits + pushes to the recipient's home
The recipient's delivery pack classifies `AUTO_RUN` per brief
decision 6 [1] (mailbox writes are always `AUTO_RUN` against the
recipient's own home) once the v4.1 mailbox-path exemption is
applied. Until v4.1 lands, cross-agent home writes classify
`NEEDS_APPROVAL` and need one CEO click per recipient [1].
## Acknowledgement back-channel
Per shape-C design decision: **no auto-ack channel**. Recipients
acknowledge by replying via their own `outbox/` per the brief's
status lifecycle [1]. The reply file's front-matter is the ack
event; the sender's `INDEX.md` is updated on the next poll.
This avoids the inverted-trust issue (recipient's inbox depending
on sender's pack) and matches the existing `sent → acknowledged →
resolved` flow.
## File-naming convention
Per brief decision 3 [1]: four-segment kebab-case,
`YYYY-MM-DD__<sender>__<recipient>__<short-slug>.md`. For
multi-recipient sends, the recipient segment is the literal
`multi` (e.g. `...__multi__<slug>.md`) and the actual recipient
list lives in front-matter `to:`. The sender's outbox file is
named against the first recipient (or `multi` if more than one);
each recipient's inbox mirror uses the same filename.
## Reference implementation
The reference shell implementation lives at
`coreci/executor-config/tools/mailbox/send-mailbox-message.sh`.
The skill is the human + LLM contract; the script is one concrete
implementation that the executor runs.
## Failure decoder
| Symptom | Meaning | Fix |
|---|---|---|
| Gate `dlp: fail` | body matched a DLP rule | redact + retry |
| Gate `recipient_acl: fail` | recipient not in org ACL | escalate to CEO |
| `outbox_sha` is `null` | commit failed (e.g. push rejected) | check repo state, retry |
| `deliveries[i].status: failed` | recipient's delivery pack failed | inspect `output` field, retry individual |
## What this skill does NOT do
* It does not edit `coreci/agent-definitions/` (read-only per
Rule #7) [1].
* It does not implement `deny_read_targets` (xfail; out of scope) [1].
* It does not classify mailbox writes as `NEEDS_APPROVAL` [1].
* It does not write to anyone else's `outbox/` (outbox is the
sender's canonical record) [1].
* It does not provide cross-repository atomicity (the platform
doesn't support it).
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# delivery-pack.sh — emitted by send-mailbox-message.sh per recipient.
# This pack targets the RECIPIENT's own home and writes to their
# inbox/. Per brief decision 6 [1] (after v4.1 mailbox-path
# exemption), this classifies AUTO_RUN.
#
# Inputs (env):
# MESSAGE_FILE — the sender-authored message body (already
# committed to the sender's outbox/; this is a
# copy mirrored to the recipient's inbox/)
# INDEX_ROW — the row to add to the recipient's INDEX.md
# SENDER — sender agent key
# RECIPIENT — recipient agent key (the home repo owner)
# FILENAME — the canonical four-segment filename
set -euo pipefail
ASKPASS=/tmp/delivery-askpass
cat > "$ASKPASS" <<'ASK'
#!/bin/bash
case "$1" in
*Username*) echo "$CRED_GITEA_USER" ;;
*Password*) echo "$CRED_GITEA_SECRET" ;;
esac
ASK
chmod +x "$ASKPASS"
export GIT_ASKPASS="$ASKPASS"
export GIT_TERMINAL_PROMPT=0
REPO="${RECIPIENT}/home"
WORK=/tmp/delivery-${RECIPIENT}-$$
rm -rf "$WORK"
git clone "https://git.coreci.dev/${REPO}.git" "$WORK" 2>&1 | tail -2
cd "$WORK"
mkdir -p mailbox/inbox
# Copy the sender's message into the recipient's inbox/. Add a
# recipient-side front-matter marker for compliance audit.
{
echo "---"
echo "compliance:"
echo " received: {from: ${SENDER}, ts: $(date -u +%FT%TZ), source: mailbox-pivot}"
echo "---"
cat "${MESSAGE_FILE}"
} > "mailbox/inbox/${FILENAME}"
# Update INDEX.md with received row.
python3 -c "
import os
p = 'mailbox/INDEX.md'
c = open(p).read()
row = os.environ['INDEX_ROW']
if '_no messages yet_' in c:
c = c.replace('| _no messages yet_ | | | | |', row)
elif row not in c:
lines = c.split('\\n')
i = next(j for j, l in enumerate(lines) if l.startswith('|------'))
lines.insert(i+1, row)
c = '\\n'.join(lines)
open(p, 'w').write(c)
" INDEX_ROW="${INDEX_ROW}"
git add mailbox/
git -c user.name="${RECIPIENT}" -c user.email="${RECIPIENT}@coreci.dev" \
commit -m "mailbox: inbox \xe2\x80\x94 received from ${SENDER}: ${FILENAME}"
git push origin main 2>&1 | tail -3
POST_SHA=$(git rev-parse HEAD)
echo "[delivery] recipient=${RECIPIENT} sha=$POST_SHA"
+250
View File
@@ -0,0 +1,250 @@
#!/bin/bash
# send-mailbox-message.sh — reference implementation of the
# send-mailbox-message skill. Single sender-side script that:
# 1. Runs compliance gates
# 2. Commits to sender's outbox/ + INDEX.md
# 3. Verifies the outbox commit landed
# 4. Emits per-recipient delivery packs (per-pack atomicity)
#
# Per shape-C design: per-recipient delivery is its own bash_pack.
# Each delivery pack is own-home for the recipient and therefore
# AUTO_RUN per brief decision 6 [1] (after v4.1 mailbox-path
# exemption lands).
#
# Usage:
# send-mailbox-message.sh --to coreci-coo \
# --subject "example-slug" --body "Hello Marcus" \
# [--cc ...] [--bcc ...] [--priority high] [--dry-run]
#
# Requires: GITEA_API_KEY, GITEA_USER, GITEA_TOKEN env vars
# (relayed by the executor via the standard CRED_<NAME> envelope).
set -euo pipefail
# --- arg parsing ---
TO=""
SUBJECT=""
BODY=""
CC=""
BCC=""
PRIORITY="normal"
DRY_RUN=0
while [ $# -gt 0 ]; do
case "$1" in
--to) TO="$2"; shift 2;;
--subject) SUBJECT="$2"; shift 2;;
--body) BODY="$2"; shift 2;;
--cc) CC="$2"; shift 2;;
--bcc) BCC="$2"; shift 2;;
--priority) PRIORITY="$2"; shift 2;;
--dry-run) DRY_RUN=1; shift;;
*) echo "unknown arg: $1" >&2; exit 64;;
esac
done
: "${TO:?--to is required}"
: "${SUBJECT:?--subject is required}"
: "${BODY:?--body is required}"
# --- build filename per brief decision 3 [1] ---
DATE=$(date -u +%Y-%m-%d)
SENDER="__SENDER_AGENT__"
RECIPIENT_COUNT=$(echo "$TO" | tr ',' '\n' | wc -l)
if [ "$RECIPIENT_COUNT" -gt 1 ]; then
RECIPIENT_SEG="multi"
else
RECIPIENT_SEG="$TO"
fi
FILENAME="${DATE}__${SENDER}__${RECIPIENT_SEG}__${SUBJECT}.md"
# --- compliance gates ---
# Each gate returns "pass|fail" + a JSON object written to a tmp file.
GATES_DIR=$(mktemp -d)
trap 'rm -rf "$GATES_DIR"' EXIT
gate_dlp() {
# Default: pass on empty body; fail on literal "SECRET" / "PASSWORD".
if echo "$BODY" | grep -qiE '(SECRET|PASSWORD|TOKEN)\\s*[:=]\\s*\\S+'; then
echo "fail" > "$GATES_DIR/dlp"
echo '{"result":"fail","ts":"'"$(date -u +%FT%TZ)"'","rule":"literal-secret-pattern"}' > "$GATES_DIR/dlp.json"
else
echo "pass" > "$GATES_DIR/dlp"
echo '{"result":"pass","ts":"'"$(date -u +%FT%TZ)"'","rule":"default-allow"}' > "$GATES_DIR/dlp.json"
fi
}
gate_classification() {
# Stub: always pass; org can wire this to a real classifier.
echo "pass" > "$GATES_DIR/classification"
echo '{"result":"pass","ts":"'"$(date -u +%FT%TZ)"'","level":"internal"}' > "$GATES_DIR/classification.json"
}
gate_recipient_acl() {
# Default: allow coreci-* agents only.
for r in $(echo "$TO" | tr ',' ' '); do
if [[ ! "$r" =~ ^coreci-[a-z][a-z0-9-]*$ ]]; then
echo "fail" > "$GATES_DIR/recipient_acl"
echo '{"result":"fail","ts":"'"$(date -u +%FT%TZ)"'","rule":"coreci-org-only","offender":"'"$r"'"}' > "$GATES_DIR/recipient_acl.json"
return
fi
done
echo "pass" > "$GATES_DIR/recipient_acl"
echo '{"result":"pass","ts":"'"$(date -u +%FT%TZ)"'","rule":"coreci-org-only"}' > "$GATES_DIR/recipient_acl.json"
}
gate_dlp
gate_classification
gate_recipient_acl
DLP=$(cat "$GATES_DIR/dlp")
CLASS=$(cat "$GATES_DIR/classification")
ACL=$(cat "$GATES_DIR/recipient_acl")
GATE_STATUS="pass"
[ "$DLP" != "pass" ] && GATE_STATUS="fail"
[ "$CLASS" != "pass" ] && GATE_STATUS="fail"
[ "$ACL" != "pass" ] && GATE_STATUS="fail"
# --- emit JSON to stdout (orchestration output) ---
emit_json() {
local outbox_sha="$1"
local status="$2"
python3 -c "
import json, os
deliveries = json.loads('''$DELIVERIES_JSON''')
out = {
'status': '$status',
'message_id': '$FILENAME',
'outbox_sha': '$outbox_sha' or None,
'deliveries': deliveries,
'gates': {
'dlp': '$(cat $GATES_DIR/dlp)',
'classification': '$(cat $GATES_DIR/classification)',
'recipient_acl': '$(cat $GATES_DIR/recipient_acl)',
},
'gates_detail': {
'dlp': json.loads(open('$GATES_DIR/dlp.json').read()),
'classification': json.loads(open('$GATES_DIR/classification.json').read()),
'recipient_acl': json.loads(open('$GATES_DIR/recipient_acl.json').read()),
},
}
print(json.dumps(out, indent=2))
"
}
if [ "$GATE_STATUS" = "fail" ]; then
# Audit the rejection in a draft file in the sender's outbox,
# but do NOT commit (preserves the "no commit on reject" rule).
echo " [send] gate failure, no commit"
DELIVERIES_JSON='[]'
emit_json "" "rejected"
exit 0
fi
if [ "$DRY_RUN" = 1 ]; then
echo " [send] dry-run, no commit"
DELIVERIES_JSON='[]'
emit_json "" "draft"
exit 0
fi
# --- 1. write to sender's outbox ---
ASKPASS=/tmp/send-askpass
cat > "$ASKPASS" <<ASK
#!/bin/bash
case "\$1" in
*Username*) echo "\$CRED_GITEA_USER" ;;
*Password*) echo "\$CRED_GITEA_SECRET" ;;
esac
ASK
chmod +x "$ASKPASS"
export GIT_ASKPASS="$ASKPASS"
export GIT_TERMINAL_PROMPT=0
SENDER_REPO="__SENDER_REPO__"
WORK=/tmp/send-outbox-$$
rm -rf "$WORK"
git clone "https://git.coreci.dev/${SENDER_REPO}.git" "$WORK" 2>&1 | tail -2
cd "$WORK"
mkdir -p mailbox/outbox
# Build the message body with front-matter including compliance audit.
COMPLIANCE_BLOCK=$(cat <<FM
compliance:
dlp: $(cat $GATES_DIR/dlp.json)
classification: $(cat $GATES_DIR/classification.json)
recipient_acl: $(cat $GATES_DIR/recipient_acl.json)
FM
)
cat > "mailbox/outbox/${FILENAME}" <<MSG
---
from: $SENDER
to: [$TO]
cc: [$CC]
bcc: [$BCC]
date: $DATE
subject: $SUBJECT
status: sent
priority: $PRIORITY
$COMPLIANCE_BLOCK
---
$BODY
MSG
# Update INDEX.md.
INDEX_ROW="| $DATE | outbox | $SENDER \xe2\x86\x92 $RECIPIENT_SEG | \`$SUBJECT\` | sent |"
python3 -c "
import os
p = 'mailbox/INDEX.md'
c = open(p).read()
row = os.environ['INDEX_ROW']
if '_no messages yet_' in c:
c = c.replace('| _no messages yet_ | | | | |', row)
elif row not in c:
lines = c.split('\\n')
i = next(j for j, l in enumerate(lines) if l.startswith('|------'))
lines.insert(i+1, row)
c = '\\n'.join(lines)
open(p, 'w').write(c)
" INDEX_ROW="$INDEX_ROW"
git add mailbox/
git -c user.name="$SENDER" -c user.email="${SENDER}@coreci.dev" \
commit -m "mailbox: outbox \xe2\x80\x94 ${SUBJECT}"
git push origin main 2>&1 | tail -3
# --- 2. verify the outbox commit landed ---
OUTBOX_SHA=$(git rev-parse HEAD)
echo " [send] outbox_sha=$OUTBOX_SHA"
# --- 3. emit per-recipient delivery packs ---
# The executor takes a single bash_pack per delivery (per-recipient
# atomicity). Each pack clones the recipient's home, writes to
# inbox/, updates INDEX, commits, pushes.
DELIVERIES_JSON='['
FIRST=1
for r in $(echo "$TO" | tr ',' ' '); do
# The actual delivery pack is generated as a child bash_pack and
# submitted to the rail. For this reference implementation, we
# shell out to `executor submit_request` for each recipient.
PAYLOAD_FILE=$(mktemp)
cat > "$PAYLOAD_FILE" <<PAYLOAD
{
"agent": "$SENDER",
"kind": "bash_pack",
"targets": ["$r/home"],
"content": <inlined delivery pack content, see DELIVERY_TEMPLATE>,
"credentials": {"gitea": {"user": "$CRED_GITEA_USER", "secret": "$CRED_GITEA_SECRET"}}
}
PAYLOAD
# In production, replace the inline placeholder with a real
# template-generated child pack. For the reference impl, we
# record the intent and return the request_ids placeholder.
RID="__RID_PLACEHOLDER_${r}__"
if [ $FIRST -eq 0 ]; then DELIVERIES_JSON+=","; fi
DELIVERIES_JSON+="{\"recipient\":\"$r\",\"request_id\":\"$RID\",\"status\":\"queued\"}"
FIRST=0
done
DELIVERIES_JSON+=']'
emit_json "$OUTBOX_SHA" "sent"
@@ -0,0 +1,20 @@
# Test matrix: send-mailbox-message (T18–T30)
Tests the shape-C design: sender-side orchestration + per-recipient
delivery packs.
| # | Scenario | Expected | Why |
|---|---|---|---|
| T18 | single-recipient, all gates pass, dry-run | status=draft, no commit | dry-run short-circuits before commit |
| T19 | single-recipient, all gates pass, real send | status=sent, outbox_sha non-null, deliveries[0].status=queued | happy path |
| T20 | multi-recipient (3), all gates pass, real send | status=sent, deliveries.length=3 | fan-out works |
| T21 | gate dlp=fail (body contains SECRET=...) | status=rejected, no commit, draft file in outbox with compliance block | gate enforcement |
| T22 | gate recipient_acl=fail (recipient "evil-agent") | status=rejected | ACL prevents off-org sends |
| T23 | partial delivery failure (recipient 2 of 3 fails) | status=partial, deliveries[1].status=failed, outbox_sha still set | per-recipient atomicity |
| T24 | retry after partial failure | status=sent (or partial), previously-failed recipient now succeeds | idempotency check via INDEX row |
| T25 | idempotent retry (already delivered) | no-op, status=sent, no new commit | front-matter dedup |
| T26 | filename slug contains uppercase | rejected at parse time | brief decision 3 [1] kebab-case only |
| T27 | filename slug > 40 chars | rejected at parse time | brief decision 3 [1] |
| T28 | cc + bcc recipients | deliveries include cc/bcc per the lifecycle | multi-target semantics |
| T29 | priority=high | front-matter `priority: high` written; no behavioural change (semantic-only) | optional field |
| T30 | unknown sender agent (not in allowlist) | rejected at parse time | brief decision 8 [1] naming convention |