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
6 changes: 3 additions & 3 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,13 @@ Safety and operations tools:

- Content Guard is embedded in Brigade and powers `brigade scrub`, publish checks, and the seeded pre-push hook. `CONTENT_GUARD_DIR` remains an explicit compatibility override for older standalone checkouts.
- [Agent Pantry](https://github.com/escoffier-labs/agentpantry): encrypted browser session, cookie, and secret sync for agent machines.
- [agent-notify](https://github.com/escoffier-labs/agent-notify) (`stations/notify/` in this monorepo): optional notification hooks for long-running agent work. Released installs resolve the pinned managed `agent-notify` binary through `brigade setup` once a stable manifest publishes its assets. The standalone repository carries a migration notice and is not archived until a containing Brigade release ships and published acceptance passes.
- [agent-notify](https://github.com/escoffier-labs/brigade/tree/main/stations/notify) (`stations/notify/` in this monorepo): optional notification hooks for long-running agent work. Released installs resolve the pinned managed `agent-notify` binary through `brigade setup` once a stable manifest publishes its assets. The standalone [agent-notify](https://github.com/escoffier-labs/agent-notify) repository carries a migration notice pointing here.
- [Token Glace](https://github.com/escoffier-labs/token-glace): output compaction for terminal-heavy agent workflows.
- Built-in Scout skills: Brigade wires `brigade-work` and `ultra-work-scout` during `brigade init`; use Skillet when you want the full optional skill roster.

Evidence ledger tools:

- [MiseLedger](https://github.com/escoffier-labs/miseledger): local-first evidence ledger. One binary crawls sessions, files, git history, and chat sources (`miseledger crawl ...`), stores `miseledger.adapter.v1` JSONL in SQLite with FTS5, and emits Brigade-ready evidence bundles. No separate exporter install.
- [MiseLedger](https://github.com/escoffier-labs/brigade/tree/main/engines/evidence-ledger) (`engines/evidence-ledger/` in this monorepo): local-first evidence ledger. One binary crawls sessions, files, git history, and chat sources (`miseledger crawl ...`), stores `miseledger.adapter.v1` JSONL in SQLite with FTS5, and emits Brigade-ready evidence bundles. The archived [miseledger](https://github.com/escoffier-labs/miseledger) repository is a frozen history mirror.
- Brigade station CLI (process boundary):
- `brigade setup` installs GraphTrail, `graphtrail-mcp`, MiseLedger, SessionFind, and `agent-notify` (when published on the release manifest) from the exact release manifest
- `brigade add evidence` is a one-release compatibility fallback for an independent MiseLedger install
Expand Down Expand Up @@ -509,7 +509,7 @@ brigade pantry expiry-alert --send # optional agent-notify (install notificati

The `notifications` station wires optional `agent-notify` into the same operator workflow: private Discord, Telegram, or Signal delivery for long-running agent work. Source lives in [`stations/notify/`](../stations/notify/) in this repository. `agent-notify` stays a **separate Go binary** (process boundary). Brigade installs it, plans setup, and health-checks it; it does not send messages from doctor, status, or brief flows.

Released pipx installs resolve `agent-notify` from the pinned unified release manifest through `brigade setup` once stable publishes its assets. `go install github.com/escoffier-labs/agent-notify/cmd/agent-notify@latest` is the explicit fallback when you are on a source checkout or the component is not yet published on the running manifest. The standalone [agent-notify](https://github.com/escoffier-labs/agent-notify) repository carries a migration notice pointing here; it is not archived until a containing Brigade release ships and published acceptance passes.
Released pipx installs resolve `agent-notify` from the pinned unified release manifest through `brigade setup` once stable publishes its assets. `go install github.com/escoffier-labs/agent-notify/cmd/agent-notify@latest` is the explicit fallback when you are on a source checkout or the component is not yet published on the running manifest. Source lives in [`stations/notify/`](../stations/notify/); the standalone [agent-notify](https://github.com/escoffier-labs/agent-notify) repository carries a migration notice pointing here.

```bash
brigade add notifications
Expand Down
2 changes: 1 addition & 1 deletion docs/phase-4a-compatibility-and-archive.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ guidance from documentation.
- [x] Migrate operator MCP client configuration to the Brigade-managed absolute path installed by `brigade setup` (operator machine migrated 2026-07-21: codex, Cursor, OpenClaw, and Claude configs plus the capped MCP wrapper now use the managed set).
- [x] Confirm migration notices as ordinary commits on both mirrors (graphtrail PR #44 and miseledger PR #44, merged 2026-07-21).
- [x] Verify that neither standalone `master` branch was rewritten or force-pushed (ancestry-checked against the pre-notice heads on 2026-07-21).
- [ ] Update product and documentation links to the Brigade release path.
- [x] Update product and documentation links to the Brigade release path.
- [x] Archive `escoffier-labs/graphtrail` (archived 2026-07-21).
- [x] Archive `escoffier-labs/miseledger` (archived 2026-07-21).

Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Content Guard is Brigade's publish and memory-safety scanner. Brigade shells out

Use it in three places:

- `brigade handoff lint --content-guard --guard-policy personal` checks pending handoffs before memory ingest.
- `brigade handoff lint --content-guard --guard-policy personal` checks pending handoffs before memory ingest. The flag runs content-guard for secret and identity leaks and Brigade injection heuristics for instruction-shaped payloads in handoff bodies (for example override phrases, fake system blocks, and base64-decode chains). Injection hits are reported as line-numbered warnings; benign discussion of prompt injection may appear as info-level notes.
- `brigade handoff draft --guard --guard-policy personal ...` writes a draft and returns failure if Content Guard blocks it.
- `brigade work import content-guard --policy public-repo` runs a scan and turns blocking findings into reviewable work imports.

Expand Down
2 changes: 1 addition & 1 deletion docs/technical-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1378,7 +1378,7 @@ Use `brigade pantry status` and `brigade pantry doctor` for pantry-specific heal
Use `brigade pantry expiry-alert` to report near-expiry sessions and preview the `agent-notify` message Brigade would send. Add `--send` only after `brigade add notifications` if you want delivery.
These plan commands do not generate or copy PSKs, start services, or mutate browser, GitHub, OpenClaw, or other auth files. Product page: https://brigade.tools/agentpantry.

`notifications` is the operator notification station. `agent-notify` remains a process-boundary Go binary; Brigade never imports it. Source lives in `stations/notify/` in this repository. Released pipx installs resolve `agent-notify` from the pinned unified release manifest through `brigade setup` once stable publishes its assets. `go install github.com/escoffier-labs/agent-notify/cmd/agent-notify@latest` is the explicit fallback when you are on a source checkout or the component is not yet published on the running manifest. The standalone [agent-notify](https://github.com/escoffier-labs/agent-notify) repository carries a migration notice pointing at the monorepo; it is not archived until a containing Brigade release ships and published acceptance passes.
`notifications` is the operator notification station. `agent-notify` remains a process-boundary Go binary; Brigade never imports it. Source lives in [`stations/notify/`](../stations/notify/) in this repository. Released pipx installs resolve `agent-notify` from the pinned unified release manifest through `brigade setup` once stable publishes its assets. `go install github.com/escoffier-labs/agent-notify/cmd/agent-notify@latest` is the explicit fallback when you are on a source checkout or the component is not yet published on the running manifest. The standalone [agent-notify](https://github.com/escoffier-labs/agent-notify) repository carries a migration notice pointing at the monorepo.
`brigade add notifications` installs `agent-notify` when missing and prints manual wiring steps.
Use `brigade notifications status` and `brigade notifications setup plan` for advisory health and reviewed hook snippets without sending.
`brigade work brief`, `brigade center status`, and `brigade daily status/plan` may surface notification readiness or suggest installing the station; Brigade never sends unless the operator uses an explicit send action such as `brigade pantry expiry-alert --send`.
Expand Down
4 changes: 3 additions & 1 deletion src/brigade/cli/handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ def register(sub: argparse._SubParsersAction) -> None:
)
p_handoff_lint.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
p_handoff_lint.add_argument(
"--content-guard", action="store_true", help="Also scan handoff files with content-guard."
"--content-guard",
action="store_true",
help="Run content-guard leak scan plus handoff injection heuristics (secrets/PII and instruction-shaped payloads).",
)
p_handoff_lint.add_argument(
"--guard-policy", default="personal", help="Content Guard policy name or path for --content-guard."
Expand Down
98 changes: 77 additions & 21 deletions src/brigade/handoff_cmd/linting.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@
globals().update({name: value for name, value in vars(_family_base).items() if not name.startswith("__")})


def _injection_hit_dict(hit: Any) -> dict[str, Any]:
return {
"line": hit.line,
"severity": hit.severity,
"rule": hit.rule,
"excerpt": hit.excerpt,
}


def _injection_messages(hits: tuple[Any, ...]) -> tuple[str, ...]:
messages: list[str] = []
for hit in hits:
prefix = "info" if hit.severity == "info" else "warning"
messages.append(f"line {hit.line}: {prefix}: [{hit.rule}] {hit.excerpt}")
return tuple(messages)


def _read_handoff_text(path: Path) -> str | None:
try:
return path.read_text(errors="replace")
except OSError:
return None


def lint(
*,
target: Path,
Expand All @@ -36,29 +60,55 @@ def lint(
if not target.is_dir():
print(f"error: --target is not a directory: {target}", file=sys.stderr)
return 2
from ..untrusted import scan_untrusted
from ..untrusted import scan_handoff_injection_heuristics, scan_untrusted

results = lint_targets(target, paths=paths)
guard_results = (
[_guard_handoff_path(path, target=target, policy=guard_policy) for path in [result.path for result in results]]
if content_guard
else []
)
guard_results: list[dict[str, Any]] = []
if content_guard:
for result in results:
guard_item = _guard_handoff_path(result.path, target=target, policy=guard_policy)
text = _read_handoff_text(result.path)
hits = scan_handoff_injection_heuristics(text or "") if text is not None else ()
guard_item["injection_heuristics"] = [_injection_hit_dict(hit) for hit in hits]
guard_item["injection_warning_count"] = len([hit for hit in hits if hit.severity == "warning"])
guard_results.append(guard_item)
guard_ok = all(item.get("exit_code") == 0 for item in guard_results)
# Content-guard checks egress (secrets/PII), not instructions. Surface the
# injection signal here too so a poisoned note never reads as fully clean.
injection_counts: dict[str, int] = {}
injection_hits_by_path: dict[str, tuple[Any, ...]] = {}
enriched_results: list[HandoffLintResult] = []
for result in results:
try:
signal = scan_untrusted(result.path.read_text(errors="replace"))
except OSError:
text = _read_handoff_text(result.path)
if text is None:
enriched_results.append(result)
continue
hits = scan_handoff_injection_heuristics(text)
injection_hits_by_path[str(result.path)] = hits
signal = scan_untrusted(text)
if signal.flagged:
injection_counts[str(result.path)] = signal.count
injection_messages = _injection_messages(hits) if content_guard or hits else ()
if not injection_messages and signal.flagged:
injection_messages = (
f"line ?: warning: [{signal.count} prompt-injection signal(s); see `brigade security scan`]",
)
enriched_results.append(
HandoffLintResult(
path=result.path,
action=result.action,
valid=result.valid,
errors=result.errors,
warnings=result.warnings + injection_messages,
hints=result.hints,
)
)
results = tuple(enriched_results)
result_dicts = []
for result in results:
row = result.as_dict()
row["injection_signals"] = injection_counts.get(str(result.path), 0)
path_key = str(result.path)
row["injection_signals"] = injection_counts.get(path_key, 0)
hits = injection_hits_by_path.get(path_key, ())
row["injection_heuristics"] = [_injection_hit_dict(hit) for hit in hits]
result_dicts.append(row)
payload = {
"target": str(target),
Expand All @@ -82,17 +132,23 @@ def lint(
for hint in result.hints:
print(f" hint: {hint}")
for warning in result.warnings:
print(f" warning: {warning}")
signals = injection_counts.get(str(result.path), 0)
if signals:
print(
f" warning: {signals} prompt-injection signal(s); content-guard does not check this, see `brigade security scan`"
)
if warning.startswith("line "):
print(f" {warning}")
else:
print(f" warning: {warning}")
if content_guard:
print(f"content_guard_policy: {guard_policy}")
print(f"content_guard_policy: {guard_policy} (leak scan + injection heuristics)")
for item in guard_results:
status = OK if item.get("exit_code") == 0 else FAIL
print(f"[{status}] content_guard: {item.get('path')} {item.get('detail')}")
leak_status = OK if item.get("exit_code") == 0 else FAIL
print(f"[{leak_status}] content_guard leaks: {item.get('path')} {item.get('detail')}")
warning_count = int(item.get("injection_warning_count") or 0)
if warning_count:
print(f" warning: {warning_count} injection heuristic hit(s)")
for hit in item.get("injection_heuristics") or []:
if hit.get("severity") == "info":
print(f" line {hit['line']}: info: [{hit['rule']}] {hit['excerpt']}")
elif hit.get("severity") == "warning":
print(f" line {hit['line']}: warning: [{hit['rule']}] {hit['excerpt']}")
return 0 if payload["valid"] else 1


Expand Down
2 changes: 1 addition & 1 deletion src/brigade/templates/policies/personal.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_comment": [
"Personal policy: for local working notes and memory handoffs before ingest.",
"It blocks secrets and attribution trailers, while warning on personal or infrastructure-like context.",
"Use via: brigade handoff lint --content-guard --guard-policy personal"
"Use via: brigade handoff lint --content-guard --guard-policy personal (leak scan + injection heuristics)"
],
"_brigade_version": "0.25.1",
"categories": {
Expand Down
Loading
Loading