文件历史

115 次代码提交

作者 SHA1 备注 提交日期
Rohit C Prasad 65338a29e2 automations: sidebar nav row + Scheduled band with unseen-run badges; flat Recent default 2026-07-20 18:44:50 -07:00
Rohit C Prasad d6c993165d providers: retry max_tokens as max_completion_tokens when the server rejects it; log swallowed autotitle failures 2026-07-20 18:44:42 -07:00
Rohit C Prasad 378a500137 mcp: never start an oauth flow at turn start; failed one-click removes its seeded config 2026-07-20 18:44:33 -07:00
Rohit C Prasad 2cce4ae1bb gui: pin viewport when scrolled up mid-stream; hover copy + timestamps; session auto-titles 2026-07-20 16:43:25 -07:00
Rohit C Prasad 6c2fb38d77 connectors: MCP-backed one-click with pinned tool allowlists — monday.com + jira
A descriptor can carry a vendor-hosted MCP server URL (mcp_url); its curated
tools are ConnectorToolDefs named mcp__<connector>__<vendor tool>, so Access
bullets, per-tool toggles, session gating, and read/write approval all ride
the existing machinery. Connect seeds the server config (include_tools = the
pin, re-derived at session build so stale config can only shrink the surface)
and runs the local DCR OAuth flow — no cloud sign-in, no vendor app, no broker.
Connector-backed servers are hidden from the Settings MCP tab; disconnect
forgets tokens + registration + config. jira keeps its manual token path (the
profile mode picks which tool set is live); monday.com is new and one-click
only, with a custom brand glyph. asana's pins ship dormant — their V2 server
rejects dynamic client registration, so its one-click waits on a broker-routed
callback. Also: catalog typeahead aliases for jira/monday/asana.
2026-07-20 12:36:33 -07:00
Rohit Prasad 7c824c2cc9 Merge pull request #376 from andrewyng/rpOutlookCalendarJul19
OpenWorker: Support Outlook calendar
2026-07-19 22:02:25 -07:00
Rohit C Prasad 90e63daa6f lint: black formatting on the new outlook test 2026-07-19 20:54:25 -07:00
Rohit C Prasad 382d4b4716 outlook: full calendar surface — windowed event list, richer create, update/delete/respond tools
- outlook_list_events now uses Graph calendarView (recurrences expanded,
  ordered by start) with a default now→+7d window and optional start/end
- outlook_create_event gains attendees, location, and a teams_meeting flag
- new tools: outlook_update_event (PATCH, only provided fields),
  outlook_delete_event, outlook_respond_event (accept/decline/tentative)
- connector copy now covers calendar (blurb, About, Access bullets, onboarding row)
- descriptors gain an aliases field surfaced in /v1/connectors; the Access
  catalog typeahead matches on it (searching "calendar" finds Outlook)
2026-07-19 20:50:33 -07:00
Rohit C Prasad c9bde9563c MCP OAuth: browser sign-in for remote servers, Granola preset
OAuth 2.1 + PKCE + Dynamic Client Registration via the SDK's client
provider; tokens in the SecretStore, loopback callback on the sidecar.
MCP tab gets sign-in/out states and a curated Granola quick-add.
2026-07-19 19:56:10 -07:00
Rohit C Prasad d4e16da1aa settings: 3-tab IA + Models provider gallery (UX-DECISIONS §40)
Appearance renames to General and absorbs Files; Token savings moves under
Models. The onboarding provider gallery/key form is extracted to a shared
providers/ProviderSetup module rendered by both surfaces, with Settings-only
extras: last-used on cards, Remove key (new DELETE /v1/providers/{name}),
and a cross-provider composer-picker card.
2026-07-19 15:48:55 -07:00
Rohit C Prasad efb0701edf connectors: wire outlook to the managed microsoft OAuth path
One-click connect failed with "outlook has no managed OAuth path" — the
connector→provider map never got an outlook entry, though the descriptor,
broker provider, and scopes were all ready. Guard test now asserts every
managed connector has a mapping.
2026-07-19 14:56:15 -07:00
Rohit C Prasad 42c1710fa2 connectors: pre-connect detail page — about/access copy, tools disclosure (UX §38) 2026-07-18 19:34:30 -07:00
Rohit C Prasad 867ddd9e23 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	platform/coworker/connectors/descriptors.py
2026-07-18 15:38:57 -07:00
Rohit C Prasad 5ccc06f9b6 outlook: managed multi-account wiring (pre-merge snapshot with batch-3 dupes) 2026-07-18 15:37:43 -07:00
Rohit C Prasad 0f514d34fb Rebrand user-facing surfaces to OpenWorker
App name, bundle identifier (com.openworker.desktop), publisher, updater
endpoint + release artifact names, DMG background art, GUI/server copy,
scratch dir default, README + quickstart. Auth0 tenant domain and API
audience keep their registered legacy identifiers on purpose; internal
ids (ocw event names, env vars, relay stage) are unchanged.
2026-07-17 13:56:06 -07:00
Rohit Prasad aae02e7c91 Merge pull request #370 from andrewyng/rpPdfAttach
OCW - PDF attachments: native support + local fallback
2026-07-17 13:54:52 -07:00
Rohit C Prasad a8b8732a7f Support PDF attachments end-to-end
PDFs attach as OpenAI file parts; Anthropic/Gemini convert to their
native blocks. Models without native PDF support get a local fallback
at send time — pypdf text extraction or pypdfium2 page images, chosen
in Settings -> Token savings. Attach-time page/size thresholds
(default 20 pages / 10 MB) reject oversized PDFs with a notice.
2026-07-17 13:38:38 -07:00
Rohit Prasad 2fec133318 Merge pull request #365 from andrewyng/rpConnectorBatch
OCW Connectors: real brand logos + six new connectors
2026-07-17 10:31:06 -07:00
Rohit C Prasad 8704339d5b Fix five owner-reported UX issues: boot splash, sign-in latency, onboarding, drag-drop
- Boot: sidecar ships as a onedir bundle via the resources slot (onefile
  self-extraction cost ~7s of splash every launch; now ~0.5s warm); build
  scripts stage+sign the folder; splash wordmark aligned to the traffic
  lights; minimumSystemVersion 12.0 (whisper.cpp build floor).
- Cloud sign-in: connection restore runs in the background after the
  callback responds; GUI polls fast (500ms) while a sign-in is in flight.
- Onboarding: provider page keeps unsaved drafts across switches and shows
  the connected state on the form; tools page rebuilt to the owner design
  (value headline, Secure-by-design card, single primary action).
- Composer: OS file drags reach the existing drop handlers in the desktop
  shell (native drag-drop interception disabled + window drop guards).
2026-07-16 12:50:28 -07:00
Rohit C Prasad f1f614ddf6 connectors: real brand logos and add DocuSign, ClickUp, Google Drive, Canva, Figma, Close (+ Descript/Clay placeholders)
Replace the hand-drawn connector glyphs with official monochrome brand marks
(via simple-icons; Slack/Salesforce/Outlook/Canva vendored as path data), and
backfill logo + brand_color on every descriptor. Add six new connectors with
tools (reads never gate, writes require approval) plus two catalog placeholders.
2026-07-16 12:14:49 -07:00
Rohit C Prasad 9553e0c0c1 Apply black formatting repo-wide 2026-07-14 22:11:09 -07:00
Rohit C Prasad c362ed928a slack: attribute outbound posts to the workspace's installer (P1)
Several people can run the app into one channel and every post arrives as
the same bot. The managed install already records WHO connected (Slack's
authed_user): store that member id on the slack:team profile, resolve the
display name once via users.info (users:read, granted since wave 1),
cache it, and prefix send_message text and send_file comments with
"[<name>] ". Truthfulness rules: manual Socket-Mode installs have no
authed_user so their posts stay bare; DMs skip the prefix; resolution
failure degrades to no prefix and never blocks a send. P2 (native
username override via chat:write.customize) replaces the text prefix.
2026-07-14 21:05:51 -07:00
Rohit C Prasad e0f8bac9f1 Squash commit with multiple features and fixes.
Short list of changes -
- Automations quickstart
- Slack mentions router
- Live turns keep narration inside. Add commentary for tasks.
- Approvals card UI enhancements
- Newer models - Fable, Sol, Terra, etc added to list
2026-07-14 16:51:52 -07:00
Rohit C Prasad 68b58222ed Sign-in: token exchange must present the same redirect_uri as authorize
The broker-bounce change (eda23c9, 07-09) pointed the authorize leg's
redirect_uri at the broker's stable callback but left the token
exchange sending the old loopback URI. RFC 6749 requires the exchange
redirect_uri to byte-match the authorize one, so the auth server
rejected every exchange — all cloud sign-ins since 07-09 failed with
'token exchange failed' after a successful browser leg (caught by the
owner's fresh-install walkthrough; also explains the earlier onboarding
Slack-connect failure, which begins with sign-in).

The unit test now derives the redirect_uri from begin_login's authorize
URL and pins the exchange to it — the old test mocked the POST without
asserting redirect_uri, which is exactly how this slipped.

708 pytest green.
2026-07-13 12:51:20 -07:00
Rohit C Prasad f48f034994 Accounts GUI for multi-account connectors + registry dedupe guard
AccountsDetail: one shared detail page for every accounts-layer connector
(Notion, Attio, PostHog, Mixpanel, Amplitude, Apollo, Hunter) - accounts
group with Default badge / make-default / per-account disconnect, one-click
add when managed (manual token form always available), Tools disclosure.
Notion/Attio join the add-modal's One click | Manual pills via a generic
one-click pane. Generic disconnect/default API calls; e2e via notion
fixtures (connect, second workspace, default move, signed-out modal).

Visual review against a live seeded sidecar caught a stale Notion
placeholder descriptor + tool set from an early wave shadowing the new
connector (page listed Notion twice, tool registry carried colliding
names): removed, and a registry test now rejects duplicate descriptor or
tool names.
2026-07-13 12:31:22 -07:00
Rohit C Prasad d42af8f234 Notion + Attio connectors: managed OAuth + manual token, multi-workspace
Both ride the generic account layer keyed by the broker's account_id
(workspace id); a manual token falls back to the validator's workspace
name. Notion: search, read-page (blocks flattened to text), database
query, create-page (write). Attio: list objects, query records, get
record, create-note (write). The managed callback's generic fallback
already routes both - no new branches; managed_profile_from_callback now
carries account_id through.
2026-07-13 12:31:22 -07:00
Rohit C Prasad 4ecd8369d7 Five manual connectors on the account layer: PostHog, Mixpanel, Amplitude, Apollo, Hunter
All read-tier, all multi-account via the generic accounts layer (every
tool takes an optional account param; results are stamped with the
serving account so approvals and transcripts name it). PostHog covers
EU/self-hosted via base_url; connecting a second project/key adds an
account instead of overwriting. Validators do a one-shot whoami so the
accounts list shows a human identity, never a key.
2026-07-13 12:31:22 -07:00
Rohit C Prasad 479db317b8 Generic multi-account layer for connectors (batch-2 spec + step 0)
Slack/Gmail/Calendar/HubSpot each grew a bespoke accounts module; this
is the same proven shape parameterized by connector: profiles at
<name>:account:<id>, pointer-only :default, lazy legacy migration. A
descriptor opts in via account_field (creds field or @identity). Connect
adds accounts instead of overwriting, connector_list grows a generic
accounts branch, and /v1/connectors/{name}/accounts/{id}/disconnect|default
routes cover every account-patterned connector. Spec for the batch in
docs/CONNECTORS-BATCH2-SPEC.md.
2026-07-13 12:31:22 -07:00
Rohit C Prasad a521545391 HubSpot read tools: property selection, filters, associations
The CRM search API returns only default properties and free-text query
never matches custom ones, so any workflow built on custom properties
(e.g. qualification fields on companies) was blind. hubspot_search gains
'properties' (comma list to return) and 'filters' (JSON array, ANDed,
validated before any request); query becomes optional but one of
query/filters is required. hubspot_get_object gains 'properties' and
'associations' passthroughs. Read tier only - no scope change.
2026-07-13 12:31:22 -07:00
Rohit C Prasad cd6626d7b9 Channel typeahead: visible search state + 5x faster cold roster sweep
The first roster lookup per workspace sweeps conversations.list cold —
~11 s on a big workspace — and the dropdown showed only recent channels
in silence, which reads as 'the typeahead doesn't work' (owner report:
typed, saw recents only, concluded regression). Two fixes: a 'searching
your workspace's channels…' row while the lookup is in flight, and the
channel sweep now uses conversations.list's 1000/page maximum instead
of 200 (measured: 11 s → 3.6 s cold; warm queries stay instant on the
15-minute cache). users.list stays at Slack's recommended 200.
2026-07-13 12:31:22 -07:00
Rohit C Prasad 5f59b2184b Fresh installs relay out of the box + the sidecar finally has a log file
cloud_relay_ws_url defaulted to empty, so every machine without a
hand-edited config.toml connected Slack fine (the install is HTTPS via
the broker) but never opened the relay socket — 'connected, relay OFF,
no messages'. The default is now the production relay endpoint; an
explicit empty override still disables it and dev/BYO deployments still
point elsewhere. A config test pins prod defaults for both cloud
endpoints.

Field debugging was impossible with the sidecar's output on /dev/null:
the desktop shell now writes it to <state_dir>/logs/coworker-server.log
(fresh per launch, previous run kept as .old, never blocks startup if
the directory can't be created).
2026-07-13 12:21:12 -07:00
Rohit C Prasad e49e07f02f Sign-in: route the Auth0 callback through the broker bounce
begin_login used config.port (8765) for the loopback redirect, but the
packaged shell runs the sidecar on a random free port — Auth0 finished
and the browser hit a port nobody was listening on ("Firefox can't
connect"), the same bug the connector flow fixed via COWORKER_PORT.
Loopback can't simply follow the real port because Auth0 rejects
unregistered ports, so the redirect now targets the broker's stable
/v1/auth/callback and the actual port rides as state's `.port` suffix;
the broker bounces the browser home and GET /auth/callback completes
PKCE unchanged.
2026-07-13 12:21:12 -07:00
Rohit C Prasad a793267224 Slack rosters: pick people from the directory, resolve channels by name
The park→approve flow stays as the backstop for unknown senders, but the
everyday path is now "find the name in a list": a per-workspace people
picker (users.list — bots/deleted filtered, guests tagged) whose pick
lands on that workspace's allow-list with the display name recorded in
the people directory, so the chip reads immediately. The channel picker
gains live roster typeahead (conversations.list): type a NAME, we
resolve the id and compose the team-qualified address; private channels
appear only where the bot is a member, and non-member public channels
carry an honest invite-the-bot hint instead of a silent dead listen.

Both are reads on scopes every install already granted (users:read,
channels:read, groups:read) — no consent bump, manual Socket Mode
included via the "default" workspace. Rosters cache in memory for 15
minutes and never persist or leave the machine. The person-picker drop
is fixed-position because the group cards clip overflow.

New sidecar routes: GET workspaces/{team}/directory + /channels; the
allow route accepts an optional display name. 5 pytest + 4 e2e cover
ranking/filtering/caching, allow-with-name, picker→chip, and the
name→address resolution.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 6df835f83b Google Calendar: multi-account profiles + availability/update/delete tools
Calendar reaches gmail parity: each Google sign-in lands in its own
google_calendar:account:<email> profile (legacy token-bearing :default
migrates lazily), the first account becomes the default, and every gcal
tool takes an optional account param that resolves by email with default
fallback — successes name the account so approvals and transcripts say
whose calendar moved. Managed tokens refresh in place per account.

New tools alongside list/create: gcal_free_busy (availability across a
comma-separated calendar list), gcal_update_event (PATCH semantics — only
provided fields change), and gcal_delete_event; all writes stay
approval-gated. Server routes add per-account disconnect (cloud metadata
best-effort first) and default selection; whole-connector disconnect
drops every account profile.

GUI: CalendarDetail page (accounts group with Default badge, make
default, per-account disconnect, direct one-click add) registered on the
detail-pages map; e2e covers connect→second account→default flip→
disconnect repoint through the mocked account routes.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 4da3c24946 GitHub tools: list commits + clone/pull into session folders
For activity digests and local code exploration (both auth paths):

- github_list_commits: newest-first with ISO since/until + author filters,
  per_page capped at 100, response trimmed to sha/author/date/message
  (500 chars) so summaries don't drown the model. No approval (read).
  Combined with github_search's date qualifiers this covers on-demand
  "what happened this week" digests and scheduled summary automations.
- github_clone / github_pull (approval-gated): target must resolve inside
  a WRITABLE granted root; private repos authenticate via a per-invocation
  HTTP header on the git command line — the token is never written to
  .git/config or any credential store, and the clone is scrubbed-or-
  aborted if anything credential-shaped would persist. Pull re-mints and
  fast-forwards only. Public repos work tokenless. GITHUB_GIT_URL
  override keeps tests hermetic (local bare-repo 'githost').
- Tests: commit filters/trim, full clone→push→pull round-trip with an
  on-disk no-token scan of the whole .git tree, path-guard refusals
  (outside roots, no writable root, non-empty target).

659 pytest / 23 vitest / e2e green.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 9be6e881c9 Desktop GitHub relay: shared-hub adapter, minted tokens, reply/review tools (MG3a)
The desktop half of the GitHub App relay (github-relay-spec §13 Step 3):

- relay_client.py: extract RelayHub — ONE authenticated cloud socket owns
  the transport/read-loop/reconnect watchdog; frames fan out by provider
  tag. SlackRelayAdapter becomes its first consumer (public surface
  unchanged); GitHubRelayAdapter its second. Also pin the relay tests'
  name resolution to a dead loopback port — unstubbed lookups were
  reaching slack.com and intermittently blowing the 2s dispatch window.
- github_relay.py: GitHubRelayAdapter — github:owner/repo#N addressing,
  installation_id rides source.team_id so the gateway's per-team
  allow-list (park → allow & deliver) applies per installation unchanged;
  missed/revoked frames handled; send() posts an issue/PR comment with a
  minted installation token.
- github_installs.py: github:install:<id> profiles (metadata ONLY — no
  token field by design), default pointer + per-installation allow-lists
  on the profile; last-removal turns relay off without resurrecting a
  stored manual PAT.
- cloud.py: github_installation_token — memory-only ~50-min cache, force
  re-mint for the 401 path, never written to the secret store;
  per-installation cloud disconnect; managed-connect flow passthrough.
- Managed OAuth callback branch (token-less by design, so it precedes the
  access_token check) + gateway hot-add; per-installation disconnect and
  status routes; allow-list edits target github:install:<id>.
- Tools: _github_call resolves auth per repo owner — manual PAT wins
  untouched, managed profiles mint and re-mint once on 401; new
  github_reply + github_review (approval-gated). The §12.5 write ceiling
  is enforced by what exists: no push/delete/settings tools on any path.
- github descriptor: two_way + managed.

655 pytest + 23 vitest green; 13 new tests cover install callback,
per-installation allow-lists, hub fan-out, missed/revoked, addressing,
send, token cache/re-mint, tool auth on both paths.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 3f2721fbf3 Slack workspaces: show the workspace domain as the group differentiator
Two workspaces can share a display name (both of Rohit's are
"deeplearning.ai"), leaving only the raw team id to tell them apart. The
workspace's slack.com subdomain is unique, so the broker now resolves it at
install (auth.test, no extra scopes) and ships it as team_domain in the
loopback POST; the desktop persists it on the team profile and the group
header renders "name · domain" with the team id demoted to hover.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 8cb8f24e34 Personas: fresh installs ship with only the default persona enabled
Every persona used to default to enabled, so a new install's picker listed
Code/Ops/etc. before the user ever chose them. Now, absent explicit state,
only the default persona (Coworker) is enabled; everything else is opt-in
from Settings ▸ Personas. Explicit user choices persist and always win,
and disabled personas still resolve so live sessions keep working.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 6dca863bc0 Model picker: derive from curated matrix per configured provider, drop seed list
The composer picker was seeded with a built-in list of OpenAI ids on first
run, so fresh installs offered models whose provider had no (or a broken)
key while other configured providers' models never appeared until added by
hand. The picker is now derived: every curated-matrix model whose provider
has a key (plus live-pulled Ollama models and user-added custom ids), with
nothing shown until the first key exists.

prefs"models" now holds only user additions; removing a matrix model
persists as a hidden id so the derived list doesn't resurrect it. Settings
datalist suggestions come from the same matrix (compat-vendor extras kept),
removing the duplicated per-provider hardcoded lists.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 022837163b HubSpot multi-portal: scoped consent, hidden fields, detail page
Portals: each connect lands in hubspot:portal:<hub_id> (managed OAuth and
private-app paste field-compatible; a legacy token default migrates lazily,
hub id parsed from the stored identity). hubspot:default shrinks to the
default pointer + hidden-fields policy. Tools take an optional portal
argument (id or name, default fallback) and name the portal in results and
approval args; broker refresh targets portal-keyed profiles.

Consent tier: one-click connect passes access=read|write by NAME; the broker
owns the scope sets. Write surface = create contact, update record, log
note, create task - no delete tools exist. Hidden fields (property denylist)
are stripped from every record agents read, with stripped counts audited via
the _display sidecar; this is model-facing policy, portal permission sets
remain the human ACL.

GUI: HubSpotDetail (portals with Default/Sandbox/access tags, Access &
privacy group, collapsed Tools), add-modal One click pane with read/write
radios | Manual private-app pane; routes for portal disconnect/default and
PATCH hidden-fields.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 83cbe9688a Gmail multi-account + Never-show-agents privacy filters
Accounts: each sign-in lands in its own gmail:account:<email> profile;
gmail:default shrinks to the default pointer + filters. A legacy token-bearing
default migrates lazily into one account (no user action). The three gmail
tools take an optional account argument (default fallback), name the mailbox
in results and approval args, and broker refresh targets account-keyed
profiles.

Filters: senders (addr or @domain) and label names, enforced in the DESKTOP
tool layer - matching messages silently omitted from search, a direct fetch
reads like a real 404, no tombstone an agent could probe. The hidden count
rides a _display sidecar on the tool-result message (mirroring source):
persisted for the GUI, stripped from every provider feed, audited as
rule+count, and shown on the tool card / step-group line.

GUI: GmailDetail page (Accounts with Default badge + per-account disconnect,
one-click add, filter chip rows, collapsed Tools); routes for account
disconnect/default and PATCH filters.
2026-07-13 12:21:12 -07:00
Rohit C Prasad bcfca1dab5 Slack connection health: status endpoint + honest chips
GET /v1/connectors/slack/status reports three independently-observable
layers: the desktop-to-relay socket (live/reconnecting/offline, last event,
last error), the cloud sign-in that authorizes it, and per-workspace bot
token health (invalid_auth/account_inactive/token_revoked recorded from Web
API reads and sends, cleared on the next success). The relay adapter now
tracks socket state and stamps last_event_at on every dispatched event.

The GUI maps the layers to the connectors-list chip (Live / Reconnecting /
Offline / Sign-in needed / Token) and the Slack detail status line, plus a
per-workspace revoked-token tag. No state ever claims the provider-to-cloud
leg is down - the desktop can't see it, and event silence is not an outage.
2026-07-13 12:21:12 -07:00
Rohit C Prasad 9a83e371be Add/remove Slack workspaces without a restart
Adding a workspace is just another managed OAuth run, so the missing piece
was reload: the managed callback now hot-reloads the gateway after a Slack
install, so the new team's token (and, on a first install, the relay socket
itself) comes up immediately.

Removing is new: POST /v1/connectors/slack/workspaces/{team_id}/disconnect
stops relaying that one workspace — cloud routing row deleted best-effort
(cloud.slack_disconnect_workspace), local slack:team:<id> token dropped,
gateway reloaded. Removing the LAST workspace clears relay mode so the
connector reads disconnected; manual Socket Mode creds stored before the
relay switch are kept but disabled, never silently re-activated.
2026-07-13 12:17:13 -07:00
Rohit C Prasad dbac5d789a Per-workspace allow-list for the managed Slack relay
Slack user ids are workspace-scoped, so a single flat allow-list cannot
authorize senders across multiple installed workspaces. Each slack:team:*
profile now carries its own allowed_users/allow_all, and authorization for
a team-qualified relay event is decided by that team's list alone (unknown
team = deny/park). Team-less sources — manual Socket Mode — keep the flat
slack:default list unchanged.

Changed together so the check, the settings load, the write path, and
parked capture can never disagree:
- config: TeamAuth + ConnectorSettings.teams, loaded from slack:team:*;
  is_authorized branches on source.team_id
- parked: ParkedMessage.team_id (defaulted; old files load fine)
- manager: _set_allowed/allow_user/disallow_user take team_id and write
  the workspace profile + live gateway teams entry; parking records the
  workspace; resolve_unauthorized allows into the right team and replays
  the message with its team_id
- app: /allow and /disallow accept an optional team_id
- setup: connector_list exposes a per-workspace 'workspaces' field (+
  mode) for the GUI; manager enriches it with display names and judges
  recent senders against their own workspace's list
- gateway: recent senders keyed and tagged by (platform, team, user)
2026-07-13 12:17:13 -07:00
Rohit C Prasad eb72bac043 Relay client: per-workspace name + mention resolution
SlackRelayAdapter now resolves user/channel display names and rewrites
<@U…> mention tokens via the workspace's own bot token (the OAuth already
granted users:read/channels:read/groups:read — no extra token), cached
per (team_id, id) since an id only means something inside its workspace.
Mirrors the Socket-Mode adapter so relay cards read '@ocw'/'Rohit'/
'#ocw-test' instead of raw ids. Shared _dispatch_slack_event feeds both
live events and the nudge history replay.
2026-07-13 12:17:13 -07:00
Rohit C Prasad 3ebda97473 Managed OAuth: redirect to the sidecar's actual port, not config.port
The desktop shell runs the sidecar on a random free port (to coexist
with a hand-run server on 8765), but begin_managed_connect built the
loopback redirect from config.port (default 8765). In the packaged app
the OAuth callback therefore hit port 8765 — a different process (or
nothing) — instead of this sidecar, so managed connect failed for every
connector. run.py now publishes the real bound port as COWORKER_PORT and
the redirect follows it. Regression test added.
2026-07-13 12:17:13 -07:00
Rohit C Prasad ae3ff8d7e3 Desktop managed-relay Slack client + team-qualified addressing (Milestone 3)
Add a second inbound Slack path so users choose either Socket Mode
(manual bot+app tokens, one workspace, direct to Slack) or the managed
cloud relay (Add to Slack OAuth, no tokens, many workspaces, events
pushed from the cloud) -- both register as platform 'slack' and produce
the same MessageEvent/InteractionEvent.

- SlackRelayAdapter: one authenticated outbound WebSocket to the relay
  (injectable transport; default is a websockets client sending the
  cloud sign-in JWT), self-healing reconnect, per-team bot tokens for
  replies, dispatch of routed events / interactivity / revoke / missed
  nudge (pulls channel history via the per-team token).
- Team-qualified addressing (slack_addr): managed reply handles are
  slack:T../C.. so multi-workspace replies pick the right token; senders
  strip the team prefix; send_message selects the per-team token, bare
  handles keep the Socket-Mode single-workspace token.
- make_adapter picks socket vs relay by profile mode; load_settings
  enables slack in relay mode without a bot_token; per-team tokens load
  from slack:team:* profiles.
- Managed OAuth callback stores slack:team:<team_id> and flips
  slack:default to relay mode; new cloud_relay_ws_url config.

15 new hermetic tests (fake relay transport, no network); full platform
suite green.
2026-07-13 12:17:13 -07:00
Rohit C Prasad b4bc2d08bf slack: watchdog to revive a silently-dead Socket Mode connection
The Socket Mode handler's start_async() opens the socket then sleeps
forever — the connection lives in slack_sdk's background tasks. When
slack_sdk stops reviving it (a transient failure during Slack's periodic
connection cycling), the socket is dead but our task still 'sleeps
forever', so it looks alive, receives nothing, and never recovers —
silently (desktop logs go to /dev/null). This is the multi-hour stall
where inbound Slack messages just stopped.

Add a watchdog that polls the client's real is_connected() and forces a
fresh endpoint when it's down; slack_sdk's own auto-reconnect stays on
(seamless on graceful cycling) with the watchdog as the safety net. Track
a reconnect count for observability.

Test: FakeSlack gains close_sockets() + a connection counter; the new
test simulates a silently-dead socket (is_connected stuck False) and
asserts the watchdog re-opens the endpoint and message flow resumes.
2026-07-13 12:14:06 -07:00
Rohit C Prasad c4d734598e security: lock the local API + WS to the app's own origins
The sidecar binds to 127.0.0.1, but a page in the user's browser can
still reach loopback. With CORS set to '*' and no auth on the WebSocket,
any website the user visited could read GET /v1/sessions cross-origin,
pick a session, open /ws/session/{id}, and drive it into shell/file
tools (RCE-class, worst on the fixed-port browser build). CORS never
covers WebSockets, so the socket was fully open.

Pin both surfaces to the desktop webview origins (tauri://localhost,
Windows http(s)://tauri.localhost) and localhost dev/browser builds:
CORS via allow_origin_regex; the WS rejects a disallowed browser Origin
with 1008 before accept. Requests with no Origin (native clients, curl,
tests) still pass — the gate targets browsers, which send an unforgeable
Origin. Verified live: foreign origin gets no ACAO, localhost + tauri do;
the running app's session still loads over the WS.
2026-07-13 08:36:44 -07:00
Rohit C Prasad e91d0fa9fa windows: fix path display, persona-id safety, native folder picker
Audit of Windows-readiness. The foundations were already in place
(APPDATA state dir, PowerShell shell backend, parent watcher via
OpenProcess, CREATE_NO_WINDOW sidecar spawn, tzdata dep, .ico icons,
build_windows.ps1) — three real gaps remained:

- GUI baseName split on '/' only, so a Windows workspace rendered as
  its full C:\ path everywhere (sidebar, search, roots). One shared
  helper in src/paths.ts now splits on both separators.
- Persona ids from manifests were unvalidated free text that becomes a
  directory name: '..'/slashes escape the install area (snapshot write
  + uninstall rmtree), ':*?"<>|' are invalid on Windows. Explicit ids
  must now match a strict slug; filename-derived ids are slugified.
- pick_native_folder had no Windows branch (browser build fell through
  to zenity); PowerShell WinForms FolderBrowserDialog added.
2026-07-13 08:36:44 -07:00