Conversation
…2807) Every access gate on the MCP tool surface RETURNS its denial as JSON (the envelope agents parse), and the audit wrapper labelled a call by throw/no-throw. So a refused chat_with_agent / chat_with_<slug> / fan_out / run_agent_loop call left an mcp_operation row reading `success: true`, and an operator reading the audit log could not tell a permitted call from a refused one. Reproduced live before the fix: the refused call's row was {"tool": "chat_with_agent", "duration_ms": 32, "success": true}. The deny sites now serialise through one helper, access.ts::accessDenied, which stamps `context.outcome = {kind: "denied", reason}` on the per-call tool context (the seam #905 already uses for requestId); withAudit reads it after execute and writes `success: false`, `denied: true`, `error: <reason>`. A thrown backend 403 is marked denied too. The caller's JSON is byte-identical. All 29 deny branches across 10 tool modules go through the helper, and a decision-based guard fails `npm test` for a `!allowed` branch or an `error: "Access denied"` envelope that bypasses it. createServer injects the audit URL and secret (configureAudit), so a test can observe the row. Tests: audit-denial.test.ts (captured audit POST, byte-identical envelopes, root-context-only stamp, stamp-then-throw, thrown 403, the guard); access-wiring.test.ts records /api/internal/audit over the real transport and runs deny-then-allow on one session; the J10 strict xfail test_the_operator_can_see_that_a_call_was_refused is flipped. Fixes #2807 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # docs/memory/learnings.md
…e that slides up, for every live run; the chat scrolls to the card on your own send (trinity-enterprise#620)
The card's "current step" slot was blank: its handler matched
`evt.type === 'tool_use'`, a shape the raw stream-json frames never carry,
so only the backend-injected `error` ever labelled — and no run but the
chat's own turn had a live path at all.
One vocabulary, two feeds:
- `utils/workActivity.js` composes the line from `{tool, summary}` —
"Reading .../x.py", "Running pytest …", "Searching for …", "Fetching …",
"Using <server>", "Delegating to <agent>: …", "Thinking". The Chat tab
(`execution-status.js`) and Agent Detail (`useSessionActivity`) compose
from it too. The stream path parses the real frame shape and summarises
the input with a port of the agent's `get_input_summary`, held to parity
by `tests/fixtures/tool_input_summary.json` (pytest + vitest).
- The agent server keys its active tool PER EXECUTION
(`session_activity.by_execution`, threaded through both live parse sites
and the Codex parser) and the 5 s heartbeat carries a bounded
`executions[]` for the registry's running set — a finished run leaves by
construction. `HeartbeatPayload` bounds it (≤20, summary ≤120, tool ≤64,
id shape, extra keys refused). `heartbeat_service.read_execution_activity`
keys it; the Work read folds it onto live, non-stale rows of rostered
agents as `WorkItem.activity` through the title sanitiser + roster mask,
dropping a line older than 30 s; `GET …/work/activity` (Redis only, same
gates) is polled every 2.5 s while a card is live.
- `PortalWorkCard` reserves one `h-4 overflow-hidden` row for the live
life; a keyed slide-up `<Transition>` (a swap under reduced motion);
`createActivityLineQueue` holds ≥700 ms, collapses a burst, never re-keys
an identical line, keeps the last line on a quiet run, clears at terminal.
The Work tab's Now rows and the room's cards receive the line too.
- A person's own send re-pins the transcript once the card mounts, guarded
by `following` (#2624).
Mutations (all red): stream parser on the old shape, queue without the
minimum, identical line re-keyed, stale line kept, summariser drift; fold
onto stale rows, no roster mask, age ceiling ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…running execution, not the capped slice (trinity-enterprise#620 /review C1); drop the dead Sync branch A 21st concurrent execution is still alive; forgetting its slot on every beat reset it to "Thinking" for as long as the fleet stayed that busy. The wire stays bounded at ACTIVITY_MAX_EXECUTIONS; the prune set is the registry's whole running set. Pinned by test_a_run_past_the_cap_keeps_its_slot (mutation: prune from the slice → red). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…o name the shard that hangs on this branch (revert before merge) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…imeout to name the shard that hangs on this branch (revert before merge)" This reverts commit 125df89.
…v desktop (trinity-enterprise#620) The #620 heartbeat work added test_ent620_agent_activity.py, which evicts the whole `agent_server` package from sys.modules at module level, unconditionally. Collected after test_drain_bounded.py, that re-registers the package under a fresh module object, so the drain test's dotted-string monkeypatches (`monkeypatch.setattr("agent_server.services.subprocess_lifecycle...", ...)`) land on the wrong copy. The REAL `_drain_bounded` then runs with a MagicMock process (pid=99999): `terminate_process_group` + the cgroup orphan sweep SIGKILL whatever real process group that pid resolves to and everything in the host's root cgroup. That took the CI runner down on every pytest shard ("The runner has received a shutdown signal") and a developer's whole desktop session twice in one afternoon (`user@1000.service: code=killed, status=9/KILL`). The #728 class, third occurrence. Four independent layers, so this cannot ship again: 1. Production fence (docker/base-image/.../orphan_sweep.py): `kill_cgroup_orphans` refuses to sweep unless the `cgroup.procs` it read lists PID 1 — true inside an agent container (cgroupns=private, verified live), never true of a host/runner root cgroup or a cgroupns=host agent. Fail-safe: leak an orphan rather than kill the host, logged. 2. Test signal guard (tests/signal_guard.py, installed for the whole unit suite in tests/unit/conftest.py): wraps os.kill/os.killpg — one module, no package copy can bypass it — and refuses any signal to the session's own group/ancestors or to a process outside the session's cgroup, recording it so the autouse fixture fails the test even when the caller swallows the exception (the production drain catches Exception). Cgroup membership, not parent chain, so a test's own setsid-reparented child is still recognised as ours. 3. test_drain_bounded.py patches through `_drain_bounded.__globals__` (the one dict the bound function reads regardless of how many copies exist) instead of dotted strings, and stubs `_terminate_process_group` for every test. 4. Lint (tests/lint_sys_modules.py): an unguarded module-level `for ... in list(sys.modules): ... pop` registry-scan eviction is now a hard failure, never baselined. The five files that legitimately evict `agent_server` (heartbeat, auto_sync, git_maintenance, 2742, ent620_agent_activity) are converted to the path-guarded form (test_git_status_dual_ahead_behind.py precedent). Verified: the full local unit suite (16503 passed) and the previously-lethal subprocess/drain/orphan set under xdist × three CI seeds now run on the host with zero signal refusals and the session intact. New tests test_2845_orphan_sweep_host_fence.py and test_2845_signal_guard.py pin the fence and the guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
… runner and the desktop (trinity-enterprise#620) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…e 0.9.5 cut (work-order F1, ent#500) The assignments layer is not shipping in 0.9.5, so the MCP tool that reads it must not be advertised. `server.ts` no longer imports/registers `createAssignmentTools`; `access.ts` drops the policy row (the ent#628 totality test forbids a row for an unregistered tool); `tool-visibility.test.ts` pins the ABSENCE of the registration instead of its presence. `tools/assignments.ts` and `assignments.test.ts` stay — re-enabling is one import + one line + the row. Docs rows (architecture, user-docs, feature-flow) say "fenced". MCP suite: 409/409; `tsc --noEmit` clean. Related to Abilityai/trinity-enterprise#500 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….0 too (#2419) The provider's anthropic-ratelimit-unified-{5h,7d}-utilization header is a fraction of the cap. _parse_utilization multiplied by 100 only up to 1.0 and passed anything larger through as an "already-percent" value, so an overage reading of 1.2 came out as 1.2%: the ent#434 weekly alert filed an exhausted subscription as has_headroom and the #2409 ranker rated it the best destination. The parser now always scales, matching the provider's own client; non-finite or negative results are None (the guard sits on the scaled value, since a finite 1e307 overflows only after the multiply), and -0.0 is normalised. A window reading past 100% logs the raw header strings at INFO, the capture path for the first real overage sample. Residuals: cached Redis snapshots self-heal within one refresh interval; pre-fix history rows cannot be repaired (1.2 from "0.012" and from "1.2" are identical); the first post-deploy sweep may raise critical items for every overage subscription; a negative reading now classifies as unassessable rather than has_headroom. Fixes #2419 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…llow-up, #2865 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fallback_to_api_key` fails OPEN on a raising settings read, so the test kept running past its mocked `db` into two call-time reads it never stubbed: the agent container and the platform API key. Whatever the pytest process held decided the outcome — on CI under one randomly order a key leaked in from an earlier test, the fallback ran for real, and the `is None` assertion failed on a PR that touched only the MCP server (regression-diff job on #2863). Reproduced locally with ANTHROPIC_API_KEY set: identical assertion message. Stub both reads so the assertion states its actual premise — no key ⇒ nothing to fall back onto ⇒ None, and no exception. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lled (abilityai/trinity-enterprise#620) — mechanical Where /proc cgroup is unreadable (macOS) install() returns False and the real os.kill/os.killpg stay in place, so test_our_own_process_group_is_refused SIGTERMed the session's own process group (pytest, xdist, the shell). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ling (abilityai/trinity-enterprise#620) — mechanical HeartbeatExecutionActivity.tool has max_length=64 but the agent never bounded it (codex MCP names are server.tool, Task:<type> comes from the model); one over-long name 422'd every beat and read as a lost heartbeat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…129 (F1) — mechanical /review I1: the ent#500 describe/it names said the tool is advertised to operator scopes; those cases exercise the operatorOnly gate, and the block now pins that the tool is NOT registered. /review I2: the booted server registers 129 tools (124 operator) with inline login on; the user doc said 130/125. The architecture doc already reads 129. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 17, 2026
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.
Integration surface for #2863, #2864, #2844, #2855. Never merged; members merge individually once green.
Train-only resolutions (member-vs-member, not member-vs-dev):
docs/memory/learnings.md(keep both),tests/registry.json(rebuilt from stages),src/mcp-server/src/server.ts(import block: #2863's fence comment + #2855'sconfigureAuditimport). Combined MCP tree:tsc --noEmitclean,npm test423/423 locally.🤖 Generated with Claude Code