Skip to content

feat(hooks): outward-action gate — nothing sent, shared or published unless the operator asked - #192

Open
Pipena wants to merge 2 commits into
The-AIOS:mainfrom
Pipena:feat/outward-action-gate
Open

Pipena wants to merge 2 commits into
The-AIOS:mainfrom
Pipena:feat/outward-action-gate

Conversation

@Pipena

@Pipena Pipena commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Problem

A Claude Code user reported that, asked to "push a project further", the agent pulled a contract PDF from Gmail, placed a signature image on it and was about to send it. AIOS has the same shape open: INTENT.md says "stop and ask" for external comms and legal, but only as prose, and an ask permission rule does not prompt in auto mode. Nothing mechanical stands between a model finishing a vague goal and a send.

What it does

hooks/guard-outward-action.py, a PreToolUse hook on the send / reply / forward / share tools (Gmail, Outlook, Teams, Drive, Slack; more via AIOS_OUTWARD_EXTRA):

  • Allows the call only when the operator's latest instruction asks for that kind of action: a human-typed message, or an AskUserQuestion answer given after it (EN/IT/ES verbs, whole-word). A bare "yes" counts only as an answer or a short reply.
  • Selects the operator's records by structure: origin.kind == "human" (not isMeta) and toolUseResult.answers. Subagent hand-backs, peer messages, tool output and headless (sdk) prompts are all type: "user" in the transcript, and none of them may speak for the operator — reading "the latest user text" lets a subagent's report that says "send" authorise a send.
  • Always refuses a contract-shaped PDF attachment.
  • Unattended runs never send unless the launcher names the tool in AIOS_OUTWARD_OK.
  • Fails open on a malformed hook payload; fails closed on "no operator instruction on record".
  • Escape hatches, logged: AIOS_ALLOW_OUTWARD=1, ~/aios/hooks/.outward-gate-off.

Wiring: SETUP §10 Hook D (macOS + Windows), hooks/_index.md row, CHANGELOG entry, CI step.

What it deliberately does not do

  • It does not judge the content of a send, only whether one was asked for.
  • It does not gate browser automation (a click on a web "Send" button); that needs a different mechanism.
  • It is opt-in wiring (operator's settings.json), like Hook C; nothing changes until wired.

How it was tested

  • tests/outward-action-gate.test.sh: 19 passed, 0 failed — incl. subagent-report laundering, tool-output laundering, headless sdk prompt, question-text vs chosen answer, contract vs invoice PDF, escape hatches, malformed payload.
  • Existing suites green: changelog-entry-shape 17/0, setup-operator-gates 28/0 (the Windows three-hook block is untouched), canonical-hygiene 8/0, cold-start-interview 65/0, compass-layer 47/0, google-connect 66/0, spawned-worker-context-parity 109/0, pipeline-executor-status 7/0, workflow-expressions 7/0.
  • Live in an operator vault since 2026-09-27: blocked a send against the session's real transcript; the laundering case was found and fixed there before this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_016MSPpqccgQRozGpK9UvBKb

Pipena and others added 2 commits September 27, 2026 12:22
…unless the operator asked

A PreToolUse hook on send/reply/forward/share tools. It allows one only when
the operator's latest instruction (a human-origin message or an AskUserQuestion
answer) asks for that kind of action; contract-shaped PDFs are always refused;
unattended runs never send unless AIOS_OUTWARD_OK names the tool. Records are
selected by structure, so a subagent report or tool output never counts as the
operator. Wired per SETUP §10 Hook D; guarded by tests/outward-action-gate.test.sh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MSPpqccgQRozGpK9UvBKb
The App and Glass deliver a bus request by typing it into the target
session, and the transcript records it as origin.kind human /
promptSource typed, indistinguishable from the operator. A scheduled
prompt containing "sent" would therefore unlock a send.

hooks/bus_log.py fingerprints every request text when it is written
(--hook on Write|Edit for sessions, --sweep for an inbox watcher); the
gate refuses a typed message whose fingerprint is logged, and the
"(HH:MM, launchd)" shape as a floor. Tests: 22 passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MSPpqccgQRozGpK9UvBKb
@Pipena

Pipena commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up (93f5251): a gap found in review, now closed. The AIOS App and Glass deliver spawn-inbox requests by typing them into the target session's terminal, so the transcript records them as origin.kind: "human", promptSource: "typed". We verified this on five live sessions. As first submitted, a scheduled routine's prompt that happened to say "sent" would have unlocked a send.

Fix: hooks/bus_log.py fingerprints every request text when it is written. It hooks Write|Edit for sessions and uses an inbox watcher (--sweep) for scripts. The gate refuses a "typed" message whose fingerprint is logged, and also refuses the (HH:MM, launchd) shape as a floor. The wiring is in SETUP §10 Hook D part 2, alongside a hooks/_index row and the CHANGELOG entry.

Tests: tests/outward-action-gate.test.sh is now 22/0. That includes a bus-typed "send" (blocked), the launchd shape (blocked), and the operator's own "send it" still passing with a log present. changelog-entry-shape, setup-operator-gates, canonical-hygiene and bus-verb-parity are green.

Still unverified: how a comment sent to Claude from an artifact (comments.sendToClaude) is recorded. Until someone has checked that, treat such a comment as possibly human-typed.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant