Repository navigation
[argus patch #175] Owner tier for Slack channel runs, with taint gating - #134
Merged
Merged
Conversation
…aint gating The router now sends tier "owner" for a channel run it verified the Atlas's own owner triggered. That run keeps the owner's agent (tier_agents.owner, else the default) and tools, minus subagents and update_agent; reads memory but never writes it; and caps Pythia at the internal ring. - tiers.owner in argus-tier-policy.json classes tools as PUBLIC, WRITE or PRIVATE; anything not listed, MCP tools included, is PRIVATE. - The first PRIVATE read in a run (or injected memory) taints it: the tier guard refuses its WRITE and egress calls with a tool error and stamps its AI messages with the source labels. - The manager carries the stamp to the outbound, and the Slack adapter posts that answer and its files with X-Argus-Turn-Private: 1 and X-Argus-Turn-Sources; acks and reactions stay unflagged. - MCP calls send X-Argus-Run-Tier: owner; the caller token is withheld only on the policy's knowledge servers. - The capability probe lists owner when tiers.owner and its agent resolve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyvERcWGR5Xj4WmoBEtpGx
…vate reads and self-writes Nicholas's decisions on the owner tier's gaps: - tiers.owner gains two required lists, private_read_tools and self_write_tools. Before the first PRIVATE call a run keeps the owner's full ceiling; after it only PUBLIC tools, private_read_tools and self_write_tools may run, never anything in egress_tools or write_tools. Writes, egress, bash, browsers, sends and unknown tools get the "ask again to act" tool error. Private reads still taint; self-writes, like write_tools, do not. - The first PRIVATE call itself runs: a call is after taint when another call of the run, earlier or in the same step, is PRIVATE. - memory_mode "off" (what acropolis ships) builds neither the memory injector's memory half nor MemoryMiddleware, and admission sets the run to off; a test pins both. - Tests: the allowlist (including egress that is also public, and the egress/write lists winning), the first private call, same-step siblings, infra tools as PUBLIC when listed, and every post and upload of a tainted turn flagged through the channel's own dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyvERcWGR5Xj4WmoBEtpGx
…ough they are writes self_write_tools is a subset of write_tools by design: gmail_create_draft is a write before taint and stays allowed after it, because it only writes to the owner's own account. After taint the allowlist is (public_tools - egress_tools) | private_read_tools | self_write_tools; write_tools is no longer subtracted from the self-writes, and egress_tools still beats every list. A self-write missing from write_tools is allowed with a warning. Checked against the acropolis policy file (acropolis PR bytedance#1428): owner validates, memory off, and the probe lists owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyvERcWGR5Xj4WmoBEtpGx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on bytedance#174. Cross-owner steering is gone, so every channel run is triggered by the Atlas's own owner. This adds a restricted
ownertier for those runs.In the owner tier, the owner acts with their own tools in a channel (an Asana task, a GitHub issue, a Gmail draft) and can read their private data. Once a turn has read private data, it is gated:
bash, no sends, no unknown tools.The tier is inert until the router sends
tier: owner. The router does that only after the probe listsowner.tiers.owner(the acropolis overlay ships this inargus-tier-policy.json)The tool names in
private_read_toolsandself_write_toolsabove are examples. The lists are the acropolis side's to fill.Field by field:
public_tools,private_read_tools,self_write_tools,write_toolsandegress_tools. Each is a list, and empty lists are allowed.memory_mode:read-only(the default: injected, never written) oroff(nothing injected and nothing written: neither the injector's memory half nor MemoryMiddleware is built).pythia_ring_max: defaults tointernal.subagents: must befalse.mcp_servers: names the knowledge servers. An MCP tool from them can be PUBLIC, and their Pythia caller token is withheld.strip_context,disable_clarificationandprompt, as in 如何集成marp使用? bytedance/deer-flow#174.agentandtools. The owner tier has no fixed agent or ceiling.Classification (default deny): what taints
write_toolsorself_write_tools.public_tools. For an MCP tool, it must also come from a deployment server whose URL is inmcp_servers. An in-process tool that carries a listed server's prefix (an in-processpythia_query) is not public.private_read_tools, MCP tools and unknown names. Memory injected into the turn (memory_mode: read-only) also counts as a PRIVATE read, labelledmemory.After taint: an allowlist
egress_tools,private_read_toolsandself_write_tools.self_write_toolsis a subset ofwrite_tools, by design.gmail_create_draftis in both. Before taint it is a write like any other. After taint it stays allowed, because it only writes to the owner's own account. A self-write missing fromwrite_toolsis allowed too, with a warning.egress_tools, which beats every other list (soweb_searchis refused although it is PUBLIC, and an egress tool listed as a self-write or private read is refused too). Also anything inwrite_toolsthat is not a self-write.bash,browser_*, sends,present_files, unknown tools.What a run gets
Relay and probe:
tier_agents.owner, or the channel default when unset (no per-message override).tiers.owner, or when that agent (or the session's default) is not on the stack.GET /api/channels/slack/relay/capabilitieslistsownerintiersunder the same conditions.Admission and manager. The policy is the last write, as in bytedance#174, except that the agent is kept:
memory_modefrom the policy: withread-only, MemoryMiddleware and the summary flush write nothing; withoff, nothing is injected either;user_emailstripped.The owner tier is not stamped on the thread, because a later web run there is the owner's own. A thread already stamped
publicstays public. The strictest tier wins, in the order dm < owner < private < public.Assembly:
task,batch_*,read_batch_result,invoke_acp_agent,update_agentandsetup_agent. Subagents and external agents run outside the guard, andupdate_agentwould let channel text rewrite the owner's agent.read-only, andoff(what acropolis will ship) builds no memory writer and no memory injection.internal.Taint is handled by
TierToolGuardMiddleware, the same per-call guard bytedance#174 uses. It wraps every tool call, built-in and MCP.after_modelandafter_agentstamp the run's AI messages withargus_turn_private: [labels]. A label is the MCP server key, else the tool name's first word, ormemory.Signal to the router. The manager copies the turn's stamps onto the outbound. On the relay transport, the Slack adapter posts that answer, and every one of its attachments, through a client that sends two headers:
X-Argus-Turn-Private: 1X-Argus-Turn-Sources: calendar,gmail(sorted, header-safe, at most 16 labels of 32 characters)The headers ride
chat.postMessageand, for each file,files.getUploadURLExternalandfiles.completeUploadExternal.present_filesoutput arrives as those attachments.The "Working on it" ack, the reactions and the ack cleanup go through the plain client. File bytes go to Slack's upload URL without the headers. An untainted turn sends no new headers on its post or its uploads.
Taint stays per turn. The router makes later posts in a thread drafts once it has seen the header, and a new turn may write again under the allowlist rule.
MCP:
X-Argus-Run-Tier: owner. That is the same value an untiered run already sends; atlas-mcp and kb-api should treat it as the full owner.mcp_servers, which caps Pythia at the internal ring.Simplified, or worth deciding on the acropolis side
present_filesandview_imageare PRIVATE. The first call taints the turn, and its files go up flagged. After another private read, they are refused unlessprivate_read_toolslists them. Listing them keeps them PRIVATE but usable after taint.public_tools.tool_search,describe_skillandask_clarificationare PUBLIC: they never taint and stay allowed after taint. A tooltool_searchpromotes is then classified by its own name.ask_clarificationnever interrupts here, because clarification is disabled in this tier.private_read_tools,self_write_toolsandwrite_toolsmatch by name only. Only PUBLIC MCP tools are bound to a server URL.ownerchanged meaning. In 如何集成marp使用? bytedance/deer-flow#174,ownerwas the label for "no tier". Now it names the owner tier. Untiered runs are unchanged and still sendX-Argus-Run-Tier: owner.Carry justification
The classes, the guard and the signal live in lead-agent assembly, the tool-call path and the Slack adapter's post. None of these has an overlay seam. The policy ships from acropolis in the image.
Class: argus-edit. It builds on bytedance#174's argus-additive modules. Delete-when: with bytedance#174.
Upstream-file footprint (lines added/removed, app code excluding tests and docs):
slack.pyagent.pymanager.pyconfig.example.yamlalso gains two lines. The rest is in bytedance#174'stier_policy.py,tier_tool_guard_middleware.pyandrun_tier.py. Noconfig_versionbump.Tests
backend/tests/test_slack_owner_tier.pyis new, with 48 cases.present_files/view_imageare PRIVATE;bash, browsers, sends,present_filesand unknown tools are refused;gmail_create_draftin bothwrite_toolsandself_write_toolsruns after taint, whilegithub_create_issue(write only) is refused;self_write_toolsandprivate_read_tools;write_toolsis allowed, with a warning.bash, a send,present_filesand an unknown tool are refused;update_agentand memory tools;memory_mode: off: no memory injection, no MemoryMiddleware, memory modeoffat admission,memory_write_allowedfalse;build_middlewares.memory_write_allowedis false, so no memory write happens in an owner turn;owner;tier_agents.owner, and the refusals.backend/tests/blocking_io/test_tier_policy_offloop.pyadds owner admission, owner MCP calls, the probe, and the guard plus the flagged post, all under the strict gate.make test-blocking-iois green.backend/tests/test_slack_public_tier.py:owneris now restricted.Ledger: the bytedance#175 section and TOC row are in, plus a bytedance#175 note in bytedance#174's section.
scripts/check_patches_ledger.pypasses.🤖 Generated with Claude Code
https://claude.ai/code/session_01LyvERcWGR5Xj4WmoBEtpGx