Repository navigation
fix(skill-gate): close the #3208 validation gaps before the gate map lands (#3274) - #3345
Conversation
…lands (#3274) The gated-skill check (#3208) is inert until the gate map supplies a row. The #3208 merge-train validation listed seven gaps that a row makes reachable; this closes them, plus #3233 and the UI's handling of the 202. It lands before or together with the gate map, never after it. - Context scan: a schedule's name, an MCP key's name and the requester's email reach the executor's system prompt beside the request. The gate now scans them (raw and as the prompt renders them), apart from the request, shows them on the card when they alone matched, and never replays them. A parity test classifies every ExecutionContext string field. - Telegram: a group reply is scanned with the quote line it carries. Group history, sender labels and the group title stay unscanned (decided; stated limit, naming the runtimes without the hook). - Wiring: the event-loopback flag, the ending-observer registration and the sweep call are each pinned by a test that runs the line. - Replay: an approved /task or fan-out run is sent the caller's message and system prompt apart, never their concatenation. - MCP: the inline connector tier reads the gate result like the other tiers; a pending answer promises delivery only when the backend makes one (outcome_delivery), and the delegation contract defers to the message (#3233). - Matcher: -/x, _/x_ and 1/x match; every Default_Ignorable character is matched removed (a run before a slash reads as a space) and spaced. - Notices: the requester never sees the approver's email; a platform request reads the display name, everyone else "the agent's approver". A requester nobody notifies is told so and where the outcome shows. - UI: the Chat tab, /m, the public link, the Tasks tab, Playbooks and Dashboard show the server's message instead of an error and poll nothing; held chat lines stay out of the next turn's history. Mutation: each call site reverted from a copy turns its test red — 21 backend sites (test_3274_*, test_ent751_skill_invocation), 15 UI sender branches (skillGateSenders / mobileAdminSkillGate), 2 MCP lines (chat-gate.test.ts). Fixes #3274 Fixes #3233 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI note: |
…eQL py/overly-large-range) CodeQL reads a character range beyond the Basic Multilingual Plane as `�-�`, so the three such ranges sharing one class in `_INVISIBLE` (U+1BCA0, U+1D173, U+E0000 blocks) were flagged as overlapping (alerts 385, 386). Each now sits in a class of its own, joined by alternation. Same characters either way: 0 of 1,111,998 non-surrogate code points match differently (4,174 invisible before and after), and the before-slash rule's output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inear matcher, honest refusals (#3274) From the full /review of PR #3345 (two independent reviewers): - Card vs run (critical): the card sanitised the JOIN of the message and the caller's system prompt, while the approved run sent each sanitised alone. A credential pattern spanning the line between them redacted the system prompt from the card only, so Approve ran an instruction the approver never saw. The card is now built from the exact replay parts, with the system prompt labelled ("With these instructions as its system prompt:"). - Matcher: the before-slash rule was quadratic on a long run of invisible characters (50k took ~10 s on the event loop, reachable from a public-link message); a run is now matched only from its start. `_AFTER` regressed `/x...y` and `/x._y` against #751; the dot rule is #751's again, and a property test holds that every input the #751 matcher caught is still caught. - /m: a refused gated request is kept out of the next message's history too; the notice gets its own gray-400 colour (theme token) and role="status". - Playbooks: a gate refusal goes to the agent page's error toast, which stays until dismissed. The agent page toast carries role status/alert. - Public link: the status route returns `gate` (held | refused); a held turn shows the grey notice, a refusal the red error box. - Tests: the Workspace decider-label test now reaches its own arm; refusal assertions reject the JSON-rendered detail; the public-link spec fails on an assertion, not a timeout. Mutation: each fix reverted from a copy turns its test red (7/7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train (mechanical): merged |
…ening # Conflicts: # tests/registry.json
…o feature/3274-gate-hardening
Summary
The gated-skill check (#3208, abilityai/trinity-enterprise#751) is on
devand inert until the gate map (abilityai/trinity-enterprise#753) holds a row. The #3208 merge-train validation listed seven gaps that a row makes reachable. This closes them, plus #3233 and the UI's handling of the 202.Merge order: this lands before, or in the same window as, abilityai/trinity-enterprise#753, never after it.
---and cuts at 80 characters with…. The text is scanned apart from the request, never joined to it. It appears on the approver's card only when it is what matched, and it is never replayed. A parity test classifies everyExecutionContextstring field as scanned or platform-controlled.schedule_nameto/api/internal/execute-task. That has been the case since Inject execution context metadata into agent system prompt #171 and is tracked as bug: scheduled runs never get their schedule context — the scheduler doesn't send schedule_name, schedule_cron or schedule_next_run #3341, which should land after this. So a scheduled run carries no schedule context, and the name never reaches the executor. The key-name and email parts are live.[Replying to …: "…"]), matching the Workspace twin.get_current_userwith a real loopback token checksis_event_loopback;main._start_maintenance_servicesout withastand runs it;OperatorQueueSyncService._poll_cyclecheckssweep()./taskor fan-out run is sent the caller's message as the message and its system prompt as the system prompt (frozen_replay), never their concatenation. A record frozen before this change replays its request text, as before.inlineConnectorChatreads the gate result before its own 403 and non-2xx handling, so a refusal keeps its named code.-/x,_/x_and1/xmatch..,_or-ends a name./m: the server's message appears as a platform line. Held lines stay out of the next turn's history.skippedrow, and the status route returns its notice.pending_approval.status-infoarm, which also fixes the existing info toast rendering red.outcome_delivery(agent_task/inbox/none) is the one rule_notifyfollows, and it rides on the 202. When nothing will be delivered, the message says so: "Nothing will be sent back when it is decided: if it is approved, it runs as a new execution on ; if not, nothing runs." The MCP layer promises delivery only foragent_taskorinbox. The delegation contract defers to the message; the line was trimmed to fit its 1,730-character budget.Rider:
connector.test.tsnow pinsrun_playbook's exact message.Deferred (from the same validation, marked "no urgency"):
read_gates, a perf nicety that would put a union type in security code.All three are tracked in #3344.
Found during
/cso --diffand fixed here: an invisible character before the slash plus one inside the name evaded both normalisations (run/pay-invoice). The report isdocs/security-reports/cso-diff-2026-10-07-3274-gate-hardening.md, and the learnings fragment is2026-10-07-a-matcher-must-read-every-form-the-reader-sees.md.Review round (
b5ad0e862). A full/reviewwith two independent reviewers found and fixed the following._AFTERmissed/x...y. That input is caught again, and a property test against the fix: backend Dockerfile missing COPY for canary/ package (a4eec13 breaks startup) #751 matcher holds that the matcher only ever widens./mre-sent a refused gated request. It now stays out of the next message's history./mnotice now has its own gray-400 colour;gate(held/refused), so a refusal shows as an error;No DB migration: the replay fields live in the existing
dispatchJSON column. Nodocker/change.Changes
services/skill_gate_service.py,services/skill_gate_errors.py,utils/skill_invocation.py,services/task_execution_service.py(backstop +gate_replay),services/chat_execution_service.py(/task),services/dispatch_admission_service.py(/chat),services/fan_out_service.py,adapters/message_router.py,routers/public.py,services/platform_prompt_service.py.src/mcp-server/src/client.ts,delegation_contract.ts.utils/skillGate.js,chat/ChatSystemLine.vue,ChatBubble.vue,ChatPanel.vue,PublicChat.vue,TasksPanel.vue,PlaybooksPanel.vue,DashboardPanel.vue,MobileAdmin.vue,AgentDetail.vue.feature-flows/skill-gate.md,requirements/security.md§26.13,feature-flows/mcp-connector.md,feature-flows/mcp-orchestration.md.Test Plan
cd tests && pytest unit/test_3274_*.py unit/test_ent751_skill_invocation.py unit/test_ent751_gate_http_mapping.py -vThese cover context, replay, notice, wiring, the channel quote, the public skipped status, and the matcher rows.
test_ent751_*,test_ent752_*,test_ent568_delegation_contract,test_ent600_*,test_1028_lifespan_phases. That run was 707 passed.test_ent549_file_audience::test_whatsapp_media…. It is order-dependent and fails identically on the base under the same fixed order.node --import tsx --testsuite passes (811/0), andtsc --noEmitis clean.skillGateSenders.spec.js,mobileAdminSkillGate.spec.jsandskillGate.spec.jspass, plus theagentDetailGateNotice.spec.jssource pin. The raw-colour, loading-gate and source-text ratchets pass.test_a_platform_requester_reads_the_deciders_display_name) and the before-slash rule, plus 7 more in the review round;/verify-local(--skip-agent; nothing underdocker/base-image/changed):import mainand boot passed;test_circuit_breaker::TestDormantState::test_dormant_transition_emits_operator_queue_alert, is stale ondevsince bug: platform alerts pile up in the operator queue — every new reading files a new row and nothing ends the ones that are no longer true #3246 part 1 and fails identically on the base. It is tracked as bug: circuit-breaker integration test still counts the old operator-queue call for the dormant alert (stale since #3246) #3343;test_ent666_objective_join,test_ent477_*,test_retention_floor). All of them pass alone.skillGateSenders.spec.js./mchat) is covered by the mountedmobileAdminSkillGate.spec.js. Since fix(frontend): /m says the agent list is admin-only instead of "Couldn't load agents" (#3041) #3071 the/magent list is admin-only, so the eyeball needs an admin who is not the approver.portalBackgroundAskInboxOnly.spec.js. That is not caused by this branch. The spec's "ended" fixture dated 09-30 aged out of its 7-day window on 10-07, and open PR fix(workspace): a turn shows its own reply on a shared thread (#3166) #3332 fixes it.Eyeball recipe (needs a temporary gate; never commit the shim)
src/backend/services/skill_gate_service.pywithcp, then replace the body oflist_skill_gateswith:/datais a Docker volume, so write it through the container:/eyeball-gate hi→ a centred gray "Not run … waiting for a decision" line, and the message is kept. A follow-up message runs, and only one card is raised.pending_approvalwith the notice./m: needs an admin who is not the approver.cpthe snapshot back and confirmgit diff --quieton the file;docker exec trinity-backend rm /data/eyeball-skill-gates.json;~/.claude/skills/eyeball-gatefrom the agent;Fixes #3274
Fixes #3233
🤖 Generated with Claude Code