Commit Graph

4 Commits

Author SHA1 Message Date
Bohan Jiang
63d215e1c3 feat(runtime): visibility (public/private) gate on CreateAgent / UpdateAgent (#2419)
* feat(runtime): visibility (public/private) gate on CreateAgent / UpdateAgent

Closes the hole where a plain workspace member could pick another member's
runtime in the Create Agent dialog and bind an agent to it — the backend
wasn't checking runtime ownership, so the agent ran on someone else's
hardware / tokens. Reported on GH #1804.

Schema
- Migration 083 adds agent_runtime.visibility ('private' default, 'public')
  with a CHECK constraint. Existing rows default to private — same
  ownership semantics as before, no behavior change for legacy data.

Backend
- canUseRuntimeForAgent predicate: allow when caller is workspace
  owner/admin, the runtime owner, or the runtime is public.
- CreateAgent and UpdateAgent both gate on it: UpdateAgent matters because
  a plain member could otherwise create on their own runtime, then re-bind
  to a private one.
- PATCH /api/runtimes/:id accepts { visibility } — owner/admin only,
  validated against the same private/public allow-list.

Frontend
- Create-agent dialog renders other-owned private runtimes disabled with a
  Lock badge + tooltip explaining who to ask.
- Inspector runtime-picker disables the same set so re-binding fails
  the same way at the UI layer.
- Runtime detail diagnostics gains a Visibility editor (owner/admin) or
  read-only chip (everyone else).
- Runtime list shows a private/public chip next to the name.

Tests
- Go: canUseRuntimeForAgent truth table; CreateAgent / UpdateAgent
  end-to-end gate tests (admin / runtime owner / plain member);
  PATCH visibility owner / admin / member / invalid-value coverage.
- Vitest: create-agent dialog disabled state on private/public runtimes,
  default-runtime selection skips locked rows; runtime detail visibility
  editor → mutation, read-only fallback.

Migrating runtimes: existing rows default to private to preserve the
"owner only" status quo. Owners switch to public via the detail page
diagnostics card.

Co-authored-by: multica-agent <github@multica.ai>

* fix(runtime): apply timezone+visibility atomically; don't seed locked template runtime

Two issues surfaced in review of MUL-2062:

1. PATCH /api/runtimes/:id ran the timezone branch first, which:
   - returned early on a tz no-op, silently dropping a concurrent
     `visibility` patch in the same body;
   - committed the timezone mutation (+ usage rollup rebuild) before
     validating visibility, so an invalid visibility left the row
     half-updated.

   Validate every field first, then run the mutations in order. The
   no-op short-circuit now only triggers when nothing else is requested.

2. The Create Agent dialog in duplicate mode unconditionally seeded
   `template.runtime_id` as the selected runtime, even when that runtime
   is now private and owned by someone else — the user saw a selected
   row they couldn't submit (Create → backend 403). Fall back to the
   first usable runtime when the template's runtime is locked, and gate
   the Create button on `selectedRuntimeLocked` as defense in depth.

Tests:
- Go: TestUpdateAgentRuntime_CombinedPatchAppliesBoth (tz no-op +
  visibility flip), TestUpdateAgentRuntime_InvalidVisibilityDoesNotMutateTimezone
  (atomic-fail invariant).
- Vitest: duplicate template pointing at a locked runtime now seeds
  the first usable one; Create button stays disabled when no usable
  alternative exists.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-11 22:53:07 +08:00
Multica Eve
d6349c16ec feat(runtime): per-runtime timezone for token-usage aggregation (MUL-1950) (#2394)
* feat: per-runtime timezone for token usage aggregation

The runtime token-usage charts (daily and hourly tabs on the
runtime-detail page) bucketed every event by the Postgres session
timezone, which is UTC in production. For an operator in UTC+8 that
meant a Tuesday afternoon's tasks landed in Tuesday early-morning's
bar — the chart was always one off.

Fix: store an IANA timezone on agent_runtime and aggregate under it.

* migrations 081 / 082 add agent_runtime.timezone (TEXT NOT NULL
  DEFAULT 'UTC') and rebuild the rollup pipeline (window function
  and both trigger functions) to compute bucket_date with
  AT TIME ZONE rt.timezone instead of bare DATE().
* No historical backfill — task_usage_daily rows already on disk
  keep their UTC bucket_date; only future writes / re-touches
  recompute under the new tz. (Product call from MUL-1950: 'guarantee
  future correctness'.)
* runtime_usage.sql gains a @tz parameter on ListRuntimeUsage and
  GetRuntimeUsageByHour and threads tz through GetRuntimeTaskHourly  Activity. ListRuntimeUsageDaily reads bucket_date as-is since the
  rollup already wrote it in tz.
* parseSinceParamInTZ replaces the raw N×24h cutoff with start-of-
  day-N in the runtime's tz so 'last 7 days' lines up with bucket
  boundaries.
* Daemon registration sends the host's IANA tz (TZ env, then
  time.Local), and UpsertAgentRuntime preserves any user override
  via a CASE-on-existing-value pattern so a daemon reconnect can't
  silently revert the operator's setting.
* New PATCH /api/runtimes/:id endpoint (UpdateAgentRuntime) lets
  the runtime detail page edit the tz; the editor seeds with the
  browser tz on first interaction.

Refs: MUL-1950

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix: harden runtime timezone rollups

Co-authored-by: multica-agent <github@multica.ai>

* fix: address runtime timezone review nits

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Eve <eve@multica-ai.local>
2026-05-11 14:39:35 +08:00
Jiayuan Zhang
3b3be9d7bd feat(comments): resolve threads with collapsible bar (MUL-1895) (#2300)
* feat(comments): resolve threads with collapsible bar (MUL-1895)

Adds a Linear-style resolve action on comment thread roots. Resolved
threads collapse to a single "N resolved comments from X" bar in the
activity feed; clicking expands the thread inline (per-session, not
persisted). Replying inside a resolved thread auto-unresolves it.

Backend
- migration 069: resolved_at, resolved_by_type, resolved_by_id on comment
- sqlc ResolveComment / UnresolveComment queries (idempotent via COALESCE)
- POST/DELETE /api/comments/{id}/resolve handlers, root-only validation
- CreateComment auto-clears resolved_at when a reply lands in a resolved
  thread, publishing comment:unresolved
- comment:resolved / comment:unresolved events; CommentResponse and
  TimelineEntry both surface the new fields

Frontend
- Comment + TimelineEntry types extended; payloads typed; WS sync wired
- useResolveComment optimistic mutation with rollback
- ResolvedThreadBar component for the collapsed view
- Resolve / Unresolve menu items on root comments; Collapse strip on the
  expanded resolved card
- en + zh-Hans locale strings

Co-authored-by: multica-agent <github@multica.ai>

* fix(comments): cover agent reply path, expand-state hygiene, nested counts (MUL-1895)

Addresses three review issues from Emacs on PR #2300:

1. TaskService.createAgentComment bypasses Handler.CreateComment, so the
   auto-unresolve wired into the handler did not fire when an agent replied
   in a resolved thread (task / mention / on_comment paths). Extracted the
   logic to TaskService.AutoUnresolveThreadOnReply so both reply paths share
   it; rewired Handler.CreateComment to call the new method.

2. Resolving an already-expanded thread no longer collapses it back to the
   bar because expandedResolved still contained the id. Added
   clearResolvedExpand + handleResolveToggle wrapper so resolve / unresolve
   always wipe the session expand entry.

3. ResolvedThreadBar received only direct children, while CommentCard's
   expanded view recurses through descendants. Extracted the recursive
   walk into thread-utils.collectThreadReplies and called from both —
   counts and author lists now match.

Co-authored-by: multica-agent <github@multica.ai>

* test(comments): mock useResolveComment + add zh-Hans plural key

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 05:49:33 +02:00
Naiyuan Qing
949dffdf7e feat: permission-aware UI across agent/comment/runtime/skill surfaces (#1915)
* feat(permissions): add core permission module and shared UI primitives

Foundation for permission-aware UI: pure rules that mirror the Go backend
permission gates, lightweight per-resource hooks, and two reusable display
components used across agent/skill/runtime detail pages.

- packages/core/permissions: types, rules, hooks (Decision-shaped — carries
  reason + message so UI can render disabled state, tooltip, and banner
  copy from one source)
- packages/core/agents/visibility-label: VISIBILITY_LABEL/DESCRIPTION/TOOLTIP
  constants ("Personal" / "Workspace") to replace scattered hard-coded copy
- packages/views/agents/visibility-badge: read-only visibility chip used on
  hover cards, list rows, and inspector when not editable
- packages/ui/components/common/capability-banner: "View only — only X and
  admins can edit Y" banner shown on agent / skill detail when current user
  lacks edit permission

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(views): permission-aware UI across agent/comment/runtime/skill surfaces

Apply the new permission rules to every surface where the UI was either
lying about who can do what or letting users hit 403s by clicking buttons
the backend would reject.

Agent detail
- Hide archive/restore actions for non-owner non-admin
- Replace inline editors (avatar, name, description, runtime/model/visibility/
  concurrency picker, skill-attach) with read-only display when canEdit is
  false — value is information, the editor is the action
- Show CapabilityBanner under the header explaining who can edit

Visibility surfaces
- visibility-picker / create-agent-dialog: replace "only you can assign"
  (false) with "Only you and workspace admins can assign" via shared
  VISIBILITY_DESCRIPTION constants
- agent-columns: truthful tooltip + "You" badge on agents the current user
  owns

Comments
- Restore admin override on comment edit/delete (backend already permits
  it via comment.go:507-512; the frontend was incorrectly hiding the menu).
  canModerate is computed once in issue-detail and threaded down.

Other
- Members tab: disable "demote" options for the last owner with tooltip
- Assignee picker: tooltip on disabled personal agents the user can't assign
- Runtime delete: tooltip and dialog explain the gate; owner column gains
  a name label next to the avatar in All scope
- Skill detail: page-level CapabilityBanner alongside the existing lock chip
- Issue delete (single + batch): note that any workspace member can delete
  issues — by-design semantics, made transparent

Backend is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agents): hide personal agents from list and @mention for non-owners

Until now an agent's "Personal" visibility only narrowed the assign-to-issue
gate — every workspace member still saw every personal agent in the list
and the @mention dropdown. Members would see, click, and fail.

This filters those surfaces with the canonical canAssignAgentToIssue rule:
regular members only see workspace-visibility agents and the personal
agents they own; workspace owners and admins continue to see everything
(admin override path is intact).

- agents-page: visibleInView layer between active/archived and Mine/All
  scope so segment counts also reflect the filter
- mention-suggestion: filter agentItems before they enter the recency-
  ranked list; expand the test mock to cover the auth + visibility paths
  and add two assertions (member hides others' personal agents; admin
  still sees them)

Backend keeps returning every agent — admin tools and direct API access
are unaffected. This is a UI-only filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:31:19 +08:00