Let an agent give the connector work in one group Ping

RO
25d ago
+760-80

Another agent can now give this one work in a group Ping, when the operator lets it into that one Ping. Today every agent's line in a Ping is discarded at the gate, --allow-agent included (#914).

Does Basecamp deliver another agent's Ping line?

Yes, so this is a CLI change, not a bc3 one. Checked live on 2026-10-20, as an Agent identity, in a group Ping that holds three people, the connecting agent and another person's agent (an Agent person a member added to the Ping):

  • basecamp events poll --buckets <ping> --actor-types agent returned the other agent's chat.line.created and its boost.created. With --actor-types person they are absent, which is why a connector with no --allow-agent never sees them.
  • basecamp chat line <url> read that line as the connecting agent: bucket.type Circle, creator.personable_type Agent.
  • basecamp pings show <ping> lists the other agent among the Ping's people, so agents can be members of a group Ping.

bc3 master (Chats::LinesController, AgentPrincipals#ensure_agent_principal_in_a_project_or_ping) lets an agent create a rich-text line in an active Ping it is in. Nothing there refuses a line that carries a mention. Not checked live: an agent actually posting a line that @mentions another agent in a Ping. basecamp pings post resolves mentions among the Ping's people from the Ping read, which agents may make. The pingable-people listing (/circles/people) is refused to agents, and that is the "Basecamp refuses an agent's mention in Campfire" the skill describes.

What changes

basecamp connect setup -P <agent> --allow-agent-in-ping <ping-id>:<agent-id>      # repeatable
basecamp connect setup -P <agent> --disallow-agent-in-ping <ping-id>:<agent-id>
  • connect.json gains agents beside participants in a Ping's entry: "pings": {"<ping-id>": {"participants": [...], "agents": [...]}}. Setup writes no key when there is no entry, as before.
  • Per Ping, explicit. --allow-agent still reaches no Ping. An agent is heard in a Ping only when that Ping's entry names it, and only there. An agent named only in a Ping entry is agent_not_allowed in every project and every other Ping.
  • Mention only, as a participant. The gate opens the mentioned rule alone. In the Ping read, the line must @mention this agent, by Person id, and be written by the same agent that performed it, read back as an Agent. Without a mention it is not_addressed, even in a direct Ping. Its role is participant and owner is false, so it is never the owner's word, and a participant's request can't open a pull request or change the connector.
  • The same loop caps. An agent's Ping mention carries the agent caps. The thread cap counts every allowed agent's mentions in the Ping's conversation (ping:<id>), and the daily cap counts one agent across its projects and its Pings. The caps' agent list is now every allowed agent (Policy.AllowedAgents), so a Ping's agents are counted.
  • The feed asks for agents (actor_types=agent,person) when any agent is allowed, anywhere or in a Ping.
  • Validate refuses a Ping agent that is the agent itself, not a Person id, the operator or someone the allowlist names. It still refuses any allowed agent, from --allow-agent or a Ping entry, as a participant anywhere.
  • Setup reads the Ping as the agent, as for --allow-in-ping. It refuses a Ping the agent can't read, an agent that isn't among its people, a person (people use --allow-in-ping), and the agent itself. A recorded agent that has since left, or whose Ping can't be read, is a warning.
  • connect show lists a Ping's agents. connect setup --help, the basecamp-connect SKILL.md (setup table, the "Another agent in one Ping" section, and the owner-only list, rewritten as "every --allow* and --disallow* flag" to stay inside the 14,000-character compaction budget at 13,992), and subagent.md (a Ping participant may be an agent) say so.

Older CLIs: setup.Parse refuses unknown keys, so a CLI without this refuses a connect.json with a Ping agents entry and runs nothing. Admission's own reader ignores the key, so it can't widen anything.

Why this design

  • A ping-scoped allow that mirrors --allow-in-ping, rather than letting --allow-agent reach Pings. Turning on --allow-agent for a project shouldn't open every Ping the two agents happen to share.
  • The Ping allow doesn't require --allow-agent as well. An agent can be heard in one Ping and nowhere else, the same as a person let into one Ping.

Tests, and trying to break it

  • admission/ping_agents_test.go: a mention is admitted on both lanes as a participant, with caps, in operator and project trust. These cases are discarded: no mention (group and direct Ping), a mention of someone else, an agent with no entry (push lane agent_not_allowed, poll lane untrusted_performer, no reads), an agent let into another Ping, an --allow-agent agent with no entry (no_route), the Ping agent in a served project, in an unserved project, and in a served project keyed as a Ping, a bucket the Ping read says is no Ping, a boost in the Ping, words written by the operator, by a second Ping agent, by this agent or by a non-Agent, and a line performed for the operator. Two agents in one Ping trip the thread cap together at N+1, and it resets after the window. One agent's daily cap spans its project and its Ping. Validate refusals, AllowedAgents, and parsing.
  • setup/pings_test.go: apply, disallow, entries shared with participants, round trip, Validate refusals, and VerifyPings for agents.
  • TestConnectSetupLetsAnAgentIntoOnePing: the command allows, shows, keeps and disallows. It refuses a person, the agent itself, an agent not in the Ping, a Ping the agent isn't in, the operator, malformed ids, and an agent passed to --allow-in-ping, each checked to fail for its own reason, and a refused run writes nothing. TestConnectFeedAsksForAgentsOnlyWhenOneIsAllowed covers the Ping case.
  • Mutation check: 16 single-line mutations in the gate, matchPingAgent, the caps' agent list, Validate, AllowedAgents, the feed filter, setup's agent check and apply. 15 of 16 were caught. The miss is the agent's role returned by matchPingAgent: Decide takes least(gate role, author role), and the gate already sets participant, so changing either one alone can't make it an operator's.
  • bin/ci passes locally.

Not done here

  • Not run live end to end: another agent @mentioning this one in a Ping with a connector on this branch. To try it, run --allow-agent-in-ping <ping>:<agent> and restart the connector, then have that agent post @<this agent> ... with basecamp pings post.
  • For a two-way conversation, the other agent's connector needs the same opt-in for this one.

Summary by cubic

Lets another agent give the connector work in a group Ping, when the operator lets it into that specific Ping. Previously every agent's line in a Ping was discarded at the gate, --allow-agent included.

  • Adds --allow-agent-in-ping <ping-id>:<agent-id> and --disallow-agent-in-ping, per Ping; --allow-agent still reaches no Ping.
  • The allowed agent is heard only by @mention in words it wrote itself, as a participant request, never the owner's word.
  • Agent caps apply: the thread cap counts all allowed agents' mentions in a Ping's conversation, and the daily cap spans one agent across projects and Pings.
  • connect.json Ping entries gain an agents key beside participants. Older CLIs refuse the key and run nothing.
  • Feed asks for agent events when any agent is allowed anywhere, including a Ping. Setup validates that the agent is in the Ping and is an Agent.
  • Verified against Basecamp: Basecamp delivers another agent's Ping lines, so this is a CLI-only change. End‑to‑end live testing with two agents conversing is not yet done.