mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* feat(slack): bind the remaining eight core standard messaging ops Slack bound 8 of the 16 core standard messaging operations. The other eight were a named fast-follow the framework design spec deferred (§13 "Deliberately not built"); this lands them, so Slack now covers the full core vocabulary. Manifest: eight new standard_op-bound [[tools]] — edit_message, delete_message, add_reaction, remove_reaction, open_dm, get_message, resolve_user, list_members — appended after the v2-era eight so the projection-parity gate's positional comparison still holds. Guest: vendor mechanics over chat.update, chat.delete, reactions.add/remove/get, conversations.open/members/history/replies, and users.list. Three behaviours worth a reviewer's attention: - get_message has no Slack endpoint. It reads the conversation at the exact ts and falls back to the thread when the message is a threaded reply, accepting only an exact match — a near miss is unknown_message, never the neighbouring message. - remove_reaction's optional emoji is implemented, not rejected. Slack's endpoint requires a name, so the omit-emoji variant reads the message's reactions and removes each one the connected account added. That is why reactions:read joins the grant, and why that variant returns no emoji. - already_reacted / no_reaction are treated as success. Slack returns them only for a message it resolved, so the requested end state holds; an error would push the model into retrying a no-op. It also makes the removal loop converge on retry after a partial failure. Scopes: the [auth.slack] union gains reactions:read, reactions:write and im:write. COMPATIBILITY: there is no scope-upgrade re-consent flow, so an account connected before this change holds a token without the new scopes, and Slack answers those three tools with missing_scope -> mapped to messaging.permission_denied (a model-visible denial, deliberately NOT an AuthRequired re-auth gate, since re-running OAuth would request the same manifest scopes). Widening an existing grant means disconnecting and reconnecting Slack; the other 13 tools are unaffected. ROLLBACK: reverting this commit restores the 8-tool surface and the narrower grant; already -widened user tokens keep unused scopes, which is inert. Gates: two pins were evolved rather than relaxed. The v2->v3 parity suite gained a PackageAdditions declaration, so the v2-era eight still project identically and positionally while the additions and the exact scope delta are declared explicitly; the catalog scope pin moved from assert!(matches!(..)) to assert_eq! so a future drift prints which scope moved. Tests: 24 guest unit tests — canonical input serde, output shapes, the reaction authorship filter, exact-ts matching, the idempotent-success arms, directory matching, the error taxonomy, and capability-id dispatch — plus manifest-projection assertions that all 16 bind exactly the core set with host-synthesized schema refs and external_write on writes. The authorship filter and exact-ts matching were sabotage-verified, as were both evolved gate assertions. Not covered: the I/O orchestration inside each operation (which calls run, in what order, and that a failed auth.test aborts the omit-emoji removal) is unreachable without a host seam in the WASM guest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(slack): correct four guest edge cases and pin all eight new ops at the dispatch seam Guest fixes (artifact rebuilt, digest re-recorded): - reactions.get now sends full=true — without it Slack truncates per-reaction users arrays on popular messages and the omit-emoji remove_reaction could skip the connected account's own reaction while reporting success. - open_dm validates user_ref with is_slack_user_id before calling Slack: conversations.open takes a comma-separated list, so an unvalidated "U1,U2" silently opened a group DM against the 1:1 contract. - get_message's thread fallback pinches the range to the exact target ts (oldest=latest=ts&inclusive=true&limit=5) instead of scanning a fixed 999-message page — finds a reply at any depth and stops shipping ~1MB pages to keep one message. - resolve_user's users.list page size now equals the match limit (default raised to the full 200): Slack cursors are page-granular, so the old mid-page break dropped matches between the cap and next_cursor that no amount of paging could recover. - next_cursor extraction deduplicated into one helper across all four paging ops; add_reaction's per-tool scopes drop reactions:read (it only calls reactions.add — the read scope belongs to remove_reaction alone, exactly as the scope table documents). The manifest's scope-widening compatibility comment now describes the real mechanism: a pre-widening account is refused by the HOST's provider-scope gate at credential staging and lands on the AuthRequired re-auth gate, which reconnects the same account with the widened union (binding skips the scope gate). Slack's own missing_scope only fires for server-side drift after staging passed, and that maps to messaging.permission_denied. Coverage (contradicting the "no host seam exists" claim this PR shipped with): the existing host-runtime WASM harness drives the committed slack_user_tool.wasm through invoke_capability with scripted egress, so the conformance sweep now covers all sixteen ops, plus ten behavioral pins — missing_scope-vs-AuthRequired layering, both reaction end-state codes, the omit-emoji orchestration (ordering, full=true, ownership filter, fail-closed identity), near-miss/threaded get_message flows, zero-egress input rejections (blank query, empty emoji, malformed open_dm user_ref), missing provider evidence, the list_members clamp, and loss-free resolve_user paging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): classify the eight slack ops in the product-surface coverage map The eight new slack.* capability ids were shipped unclassified, which is what turned the "Validate product-surface evidence contracts" step red (summary.missing == 8) and cascaded into the Reborn E2E roll-up. - classifications.tested gains the eight ids. - Eleven typed ProviderOperationCases: five writes with provider readback and cleanup against the never-reset Slack world (edit and delete seed their own subject message; remove_reaction seeds its own reaction as the connected account; open_dm proves idempotence by re-opening directly), get_message driving the history-miss -> thread-fallback path at the seeded reply, resolve_user with a natural empty, and proxy-served list_members pages (Emulate answers conversations.members POST-only at the pinned ref while the guest reads via GET, as real Slack allows). - get_message's canonical output requires the message, so its empty class is the typed model-visible miss: ProviderOperationCase gains expected_status ("completed" default, "failed" for exactly this shape) and the runner asserts the declared status instead of hardcoding completed. All three gate files pass under the hermetic wrapper (98 passed), and the product-surface generator reports 131 capabilities, 0 missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update guidance for the sixteen-tool slack surface - reborn-extension-surfaces no longer hardcodes a tool count for the slack manifest (the grep recipe is the source of truth) and names github as the plain schema-declaring exemplar now that every slack tool is standard_op-bound. - standard-operations.md repoints slack_error_to_standard_code at the live crates/extensions path instead of the dead pre-restructure assets/ path. - tests/e2e/CLAUDE.md stops pinning a literal capability count and defers to the generated product-surface report (131 as of this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): declare the expected failed tool result on the get_message empty case The mock-LLM trace replayer treats any failed capability result as a replay error unless the step's request_hint names the expected failure — the same mechanism the provider fault cases already use. slack_get_message_empty declares expected_failed_tool_result_contains="messaging.unknown_message", and the operation-case runner forwards it onto the synthesized trace; the adjacent slack_list_members_empty failure in the provider-2-3 lane was collateral from the poisoned replay state and needs no change of its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): widen the seeded slack accounts to the manifest scope union The merge queue runs the root integration lanes that skip on PRs, and every one of them seeded the slack account with the pre-widening eleven-scope union. The manifest's [auth.slack] ceiling now includes reactions:read, reactions:write, and im:write, so slack installs parked on the auth gate (BlockedAuth) — the exact provider-scope-gate mechanism this PR documents, biting its own lockstep fixtures. All six seeds (extension_delivery, extension_runtime, delivery_user_journeys, tool_call, the slack lifecycle group scenario, and the QA harness profile that reborn_qa_smoke_scenarios composes) now carry the full union, and the group scenario's lockstep comment names the write additions. Locally green with Docker-backed postgres legs: extension_delivery 23/23, extension_runtime 25/25, delivery_user_journeys 25/25, group_extensions 16/16, tool_call 38/38, and the bundled-extension-surface QA smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>