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:
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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 |
|
||||
Reference in New Issue
Block a user