Skip to content

feat(portal): share assets to channels, sharing permission (#2241) - #2244

Merged
cjimti merged 1 commit into
mainfrom
feat/2241-share-to-channel
Oct 21, 2026
Merged

cjimti merged 1 commit into
mainfrom
feat/2241-share-to-channel

Conversation

@cjimti

@cjimti cjimti commented Oct 21, 2026

Copy link
Copy Markdown
Member

Changes the Share dialog, notify (action=publish and action=send), platform.publish/platform.notify, manage_asset (share, list_versions, diff), the portal routes POST /api/v1/portal/assets/{id}/channel-shares (new), GET /api/v1/portal/channels, the asset version and content-url routes, /api/v1/portal/me, the admin persona API, persona resolution on every surface, feedback thread rendering and email, the sidebar footer and a new /portal/profile page, the script page's change summaries, make preverify, and the Go toolchain. Two migrations (000210, 000211). Removes POST /api/v1/portal/channels/{name}/publish and the asset page's Post to a channel button.

Closes #2241
Closes #2088
Closes #2226
Closes #2228
Closes #2215
Closes #2214
Closes #2185
Closes #2239

Share to a channel (#2241)

  • Share on an asset its owner holds offers Share to a Channel beside a person and a link: the channel, who can open the posted link (Signed-in users, or Anyone with the link with an expiration), and an optional message. A warning states that everyone in a Slack, Mattermost or email channel sees the preview image and, for a text asset, the first part of its content.
  • The channels offered are the chat channels each Slack or Mattermost bot is in, read live when the dialog opens (Slack users.conversations, public and private, or public only when the token lacks groups:read; Mattermost, the open and private channels of every team the bot is on), plus the other channels the person reaches. A slack/mattermost channel's target is now optional on an immediate channel: without one it stands for the bot, and one channel per workspace is enough. A daily chat channel still needs its own target and takes none per post.
  • Posting creates a link share with origin: channel_post and posted_to naming where it went (#general (slack)), and posts that share's URL rather than /portal/assets/{id}. The share is listed in the asset's shares as Posted to …, and revoking it stops the posted link from opening.
  • POST /api/v1/portal/assets/{id}/channel-shares (channel, target, access_mode authenticated|public, expires_in, message) replaces POST /api/v1/portal/channels/{name}/publish. It is a notify call made as the signed-in person, so connection scoping, the per-session rate limit and audit apply unchanged.
  • A Slack post now shares an asset's preview image into the channel itself, the line under the message, rather than into its thread; other files still go to the thread.
  • The asset page's Post to a channel button and PostToChannelDialog are removed.

The sharing permission (#2241)

  • allow_sharing on a persona: file config (*bool, absent allows), persona_definitions.allow_sharing (migration 000211, default true), the admin persona API, and a Sharing checkbox on the persona editor's AI Assistant Behavior tab. /api/v1/portal/me reports sharing.
  • Without it: no Share action on assets, collections or prompts; 403 sharing is not available to your persona from POST .../shares (asset, collection, prompt), POST .../channel-shares and GET /api/v1/portal/channels; the same refusal from manage_asset action=share, an export's create_public_link (through a gated share store), and notify action=publish with access signed_in or public. Administrators can always share.

Who opens a published link (#2088)

  • notify action=publish and platform.publish take access: signed_in (default) creates, or reuses, a link share any signed-in user opens; public creates one that expires after expires_in (required); existing posts the asset page link, today's behavior, which opens only for people who already have access. signed_in and public need the asset's owner (a script run acts for its author) or an administrator.
  • Posting the same asset to the same place again with signed_in reuses the share. A post held back by a repeat key, or refused, revokes a share it created.
  • Both send and publish take target, one of the chat channels action=list now reports under each bot channel's targets. The result carries access, share_id and, for public, expires_at.
  • platform.notify gains target; platform.publish gains access, expires_in and target. The script contract text is updated.
  • Migration 000210 adds portal_shares.posted_to.

Viewers see only the current version (#2226)

  • A reader whose access to an asset is viewer (a direct or collection share) gets 403 from GET /api/v1/portal/assets/{id}/versions, from .../versions/{n}/content for any version but the current one, and from .../content-url?version= naming an earlier version. The owner, editors and administrators keep the full history.
  • manage_asset list_versions and diff refuse the same way. list_versions had no access check at all before this: any caller could list any asset's versions by id. It now requires read access, then the history check.
  • The asset page fetches and shows no history, version picker or version number for such a reader. Threads anchored to older versions still open; the public and guest views never showed history.

One persona resolution and one admin rule (#2228)

  • personas.role_mapping.oidc_to_persona is applied by the persona registry's GetForRoles, so the portal, the admin API and managed resources resolve the persona the MCP path does. Two personas of equal priority now resolve to the name that sorts first, on every request, instead of in map order.
  • The administrator rule everywhere is the resolved persona being admin.persona (Registry.AdminRule). It replaces the role-intersection checks in the portal access core, the script, prompt-version, mention, table and API-browser identities and the portal catalog routes. Managed resources no longer treat a role literally named admin or platform-admin as administration, and table registration over MCP reads the call's resolved IsAdmin.
  • Behavior change: a user whose roles match the admin persona and a higher-priority persona is no longer an administrator in the portal (it already was not in the admin API).

Profile (#2215)

The sidebar footer names the signed-in account above Sign Out: the email, or the user ID when there is none, with an avatar of initials (avatar only with a tooltip when collapsed). It opens /portal/profile, which shows email, user ID, persona, roles, administrator yes/no and the sign-in method (single sign-on or API key), with Sign Out. It reads the /me payload already loaded and shows no token or key.

Markdown in feedback threads (#2214)

  • Thread event bodies render as Markdown (GFM, single newlines kept as breaks, raw HTML dropped, script links not followed), with delivered mentions still chips read from metadata.mentions. A mention inside code stays as written. CommentBody replaces MentionText.
  • The composer has Write / Preview.
  • Comment, feedback and mention emails render the message as HTML through the same goldmark renderer channel bodies use, with each @local(domain) mention written as its address; the text part keeps the Markdown as written. List views show thread titles only, so no body excerpt is involved.

Script change summaries (#2185)

The script page shows the newest three change summaries; the rest open with Show N more, remembered in the browser. A line points to the full history under Source.

Migrations in preverify (#2239)

make preverify runs make migrate-check-diff, which runs migrate-check on pg16 and pg17 when the diff against main adds or changes a file under pkg/database/migrate/migrations/, and skips it otherwise. TestPreverifyChecksMigrationsOnBothMajors pins it.

Go 1.27.3

go.mod's toolchain and every workflow's go-version move from 1.27.2 to 1.27.3, which fixes GO-2026-6809 (crypto/x509 distrusts compromised intermediates), published 2026-10-21 and reported by govulncheck against 1.27.2.

Readers of this state

  • meResponse.sharing (/api/v1/portal/me): ui/src/stores/auth.ts (canShare), read by AssetViewer, CollectionViewerPage, PromptViewerPage.
  • Share.posted_to / origin: ui/src/api/portal/types.ts, rendered by ui/src/components/share/ActiveShares.tsx.
  • Admin persona allow_sharing: ui/src/api/admin/types/tools.ts, PersonasPanel.tsx, persona/usePersonaEditor.ts, persona/BehaviorTab.tsx, MSW persona mocks.
  • GET /api/v1/portal/channels targets/targets_error/target, and POST .../channel-shares: ui/src/api/portal/hooks/channels.ts, ui/src/components/share/ChannelShareSection.tsx, MSW handlers.
  • Version routes' 403 for viewers: ui/src/pages/viewer/AssetViewerPage.tsx (no versions request for a viewer), useAssetVersions(assetId, enabled).
  • Notification channel target now optional: ui/src/pages/settings/NotificationChannelEditor.tsx (required only for daily).
  • The removed publish route had one reader, PostToChannelDialog, removed with it.

Size and structure

  • internal/httpserver/notifywire/channels.go moved to internal/platform/notifywiring/upstream.go so the notify tool can list a bot's channels through the connection resolution the send worker uses.
  • The refusal messages live in internal/portal/access (pkg/portal's exported-surface budget), the export share gate in internal/platform/portalstore (pkg/platform's size budget), and the sharing check for the channel routes in channelhttp (notifywire's cohesion).
  • The import-ratchet allowlist adds 13 edges, each from the moves above or the new dependencies (notifyrender on pkg/portal/mention for mention addresses, the toolkit and exporters on internal/portal/access).

Acceptance

Run through the real surfaces against make dev (two replicas behind the proxy), the Mattermost fixture, a developer Slack workspace and mailpit; transcripts at build/<n>/acceptance.md, each opening with its wire forms.

Notes for review

  • A signed-in person who opens the posted share page is promoted to a viewer share of their own by the existing public-link rule (A signed-in user opening a share link lands on the public viewer instead of the asset in their portal #1473). Revoking the channel share stops the link; those derived shares remain listed for the owner to revoke.
  • New UI copy: "Share to a Channel", "Who can open the posted link", "Posted link expiration", "Posted to …", the "Sharing" checkbox ("Share what they own with a person or to a channel. Administrators always can."), "Profile", "Show N more" / "Show fewer", "Write" / "Preview".
  • Screenshots are regenerated (the sidebar footer is on every page); user-asset-post-to-channel is replaced by user-asset-share-to-channel, and user-profile is new.

Nine tickets plus a Go toolchain bump: the Share dialog's channel destination and POST /api/v1/portal/assets/{id}/channel-shares (#2241), notify/platform.publish access and target (#2088), viewer-only current version (#2226), one persona resolution and admin rule (#2228), the portal Profile page (#2215), Markdown feedback bodies (#2214), collapsed script change summaries (#2185), migrate-check in preverify (#2239), and go 1.27.3 for GO-2026-6809.

1. Share offers a channel: the chat channels each Slack or Mattermost bot is in (a chat channel's target is now optional on an immediate channel), Signed-in users or Anyone with the link with an expiration, and a message. Posting creates a channel_post link share with posted_to and posts its URL; Slack shares the preview image into the channel. The Post to a channel button, PostToChannelDialog and POST /api/v1/portal/channels/{name}/publish are removed.
2. allow_sharing on personas (migration 000211, default true; Sharing checkbox; /me sharing). Without it: no Share action, and 403 from the share routes, the channel list and channel share; manage_asset share, an export's create_public_link and notify publish with a share refuse too.
3. notify publish and platform.publish take access (signed_in default, public with expires_in, existing) and target; a signed_in post reuses the share an earlier post to the same place made; a held-back or refused post revokes a share it created. Migration 000210 adds portal_shares.posted_to.
4. A reader whose access is viewer gets 403 from the version routes, an earlier version's content and a signed content URL naming one, and from manage_asset list_versions and diff; the asset page shows no history to them. list_versions had no access check at all and now requires read access.
5. personas.role_mapping.oidc_to_persona is applied by the registry's GetForRoles, ties go to the name that sorts first, and the administrator rule is the resolved persona being admin.persona on the portal, admin API, resources and MCP; the literal admin/platform-admin role rule in resources is removed.
6. Feedback thread bodies render as Markdown with mention chips and a Write/Preview composer; comment, feedback and mention emails render the message as HTML.
7. The sidebar footer shows the signed-in account and opens /portal/profile.
8. The script page shows the newest three change summaries behind Show N more.
9. make preverify runs migrate-check on pg16 and pg17 when the diff touches a migration.

Closes #2241
Closes #2088
Closes #2226
Closes #2228
Closes #2215
Closes #2214
Closes #2185
Closes #2239
@codecov

codecov Bot commented Oct 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.83%. Comparing base (7790188) to head (f61d36d).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2244      +/-   ##
==========================================
+ Coverage   94.80%   94.83%   +0.03%     
==========================================
  Files        1376     1381       +5     
  Lines      111500   111991     +491     
==========================================
+ Hits       105705   106211     +506     
+ Misses       5778     5763      -15     
  Partials       17       17              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cjimti
cjimti merged commit ad83295 into main Oct 21, 2026
14 checks passed
@cjimti
cjimti deleted the feat/2241-share-to-channel branch October 21, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment