Skip to content

feat(watchsync): add plugin-backed providers - #475

Merged
Quick104 merged 9 commits into
Silo-Server:mainfrom
crowquillx:feat/plugin-watch-sync-provider
Aug 6, 2026
Merged

feat(watchsync): add plugin-backed providers#475
Quick104 merged 9 commits into
Silo-Server:mainfrom
crowquillx:feat/plugin-watch-sync-provider

Conversation

@crowquillx

@crowquillx crowquillx commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Adds a complete host adapter for plugins implementing watch_sync_provider.v1, using Silo's existing watch-sync connection, import, export, list, scrobble, retry, and reconciliation machinery.

Problem

Silo's watch providers are compiled into the server. The released v0.12 plugin capability proved the RPC boundary, but PR 475 originally exposed only API-key authentication, watched export, and completed playback. That was not enough to extract Trakt, Simkl, MDBList, or to implement a Floppy provider without provider-specific host code.

Approach

  • Discover enabled watch_sync_provider.v1 capabilities and atomically replace plugin-backed registry entries while retaining built-in providers during migration.
  • Support API-key and device-code authentication, refresh, and account lookup.
  • Preserve full provider credentials: access/refresh tokens, expiry, token type, scopes, and opaque secret attributes. Returned credentials are authoritative and are encrypted/persisted before results, pages, or faults from the same RPC are interpreted.
  • Resolve manifest-declared installation configuration for every call, separating public values, secret values, and profile credentials.
  • Support watched and progress import; watched and unwatched export; favorite and watchlist import/add/remove; ordered watchlists; and live start, pause, and stop scrobbles according to the descriptor.
  • Preserve family-specific cursors across paged imports, advance them only after a successful traversal, and avoid treating absent incremental-list items as deletions.
  • Give every plugin connection a stable provider-specific history source so importing from Floppy suppresses echo only to Floppy, not legitimate export to a Trakt or Simkl plugin.
  • Persist a durable local reconciliation marker when a remote stop succeeds but the history-export transition fails. The sweeper retries only the database transition and never sends a duplicate stop.
  • Continue using Silo's existing rate-limit, outbox, ordered delivery, matching, connection lifecycle, and safe-error boundaries.

The public /api/v1 surface remains additive. Existing built-in Trakt, Simkl, and MDBList providers remain registered while equivalent plugins and migration tooling are developed.

SDK dependency

Depends on silo-plugin-sdk PR #13, which expands the additive v0.13 contract with:

  • device-code authentication
  • typed remote-state families
  • watched/unwatched, favorite, watchlist, and live-scrobble operations
  • ordered-list positions and provider item keys
  • complete credentials and provider configuration on every request

This PR is temporarily pinned to SDK commit 3b705d7e882f through its immutable Go pseudo-version. Replace it with v0.13.0 after the SDK PR merges and the release is tagged. Existing v0.12 plugins remain wire-compatible and expose only capabilities the older descriptor can execute.

Reference provider

A Floppy reference implementation has been built in Silo-Server/silo-plugin-watchprovider-floppy. It validates profile API tokens, imports watched history and durable progress, exports completed watches, and forwards live start/pause/stop events. It deliberately does not advertise favorites, watchlists, or unwatch because Floppy does not currently expose safe reconciliation semantics for those operations.

Reliability and security

  • Personal credentials remain encrypted and profile-scoped in Silo; plugins receive them only at the RPC boundary.
  • Legacy access/refresh rows remain readable during migration, and device codes are encrypted at rest.
  • Plugin safe messages are bounded/redacted before persistence; transport details remain host-owned.
  • Provider credentials and configuration are never logged or placed in generic runtime settings.
  • Completed delivery remains at-least-once at the host boundary; the Floppy reference checks exact remote history identity before applying a completed retry.

Validation

Passed on the exact published head bcb31bc:

  • go test ./internal/watchsync ./internal/plugins ./internal/pluginhost ./cmd/silo
  • go test -race ./internal/watchsync ./internal/plugins ./internal/pluginhost ./cmd/silo
  • golangci-lint v2.12.2 run --new-from-merge-base=origin/main ./... — 0 issues
  • make verify-local-paths
  • git diff --check

The repository-wide make test-go run reached the changed packages and migrations successfully, then failed two existing internal/jellycompat process-lock tests. Both tests fail identically on untouched main, confirming a baseline/environment failure rather than a PR regression.

The Floppy reference separately passes go test -race ./..., builds successfully, and renders a validated manifest through ./plugin manifest. The SDK branch passes go test ./....

Deferred work

  • Authorization-code-only plugins remain SDK-valid, but the host will reject them until Silo has a public callback-state and client-secret flow.
  • Built-in provider removal and existing-connection migration should happen provider by provider after equivalent plugins ship.
  • The SDK dependency must move from the pseudo-version to v0.13.0 before merge.
  • The new Floppy repository still needs CI/release workflows added with a GitHub credential carrying workflow scope.

AI disclosure

  • Tool: Codex
  • Model: GPT-5.6
  • Involvement: AI-assisted architecture, implementation, live-source investigation, testing, and PR preparation on behalf of the project maintainer.
  • Review notes: implementation was checked against current Silo host/SDK behavior and the live Floppy API source. The review caught credential-ordering, cross-provider echo suppression, incremental-list deletion, timestamp provenance, and duplicate-stop reconciliation hazards; each has regression coverage.

Summary by CodeRabbit

  • New Features

    • Added plugin-backed watch-sync providers with API-key or device authorization.
    • Added syncing for history, progress, favorites, watchlists, remote state, and live scrobbling.
    • Providers now refresh automatically when plugins change.
    • Added secure credential storage and installation-specific configuration.
  • Bug Fixes

    • Removed stale providers when plugin data is unavailable.
    • Improved retries, rate-limit handling, partial exports, reconciliation, and error reporting.
    • Improved incremental list updates, including removal handling and credential validation.
  • Documentation

    • Added a design specification for plugin-backed watch synchronization.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds plugin-backed watch-sync providers with RPC support, lifecycle discovery, atomic registry replacement, authentication, state synchronization, encrypted credentials, scrobble completion handling, and durable export status transitions.

Changes

Watch-sync plugin integration

Layer / File(s) Summary
Plugin RPC and configuration boundary
internal/pluginhost/..., internal/plugins/..., docs/superpowers/specs/..., go.mod
Adds the watch_sync_provider.v1 client contract, RPC deadlines, service resolution, installation configuration serialization, SDK integration, and design specification.
Watch-sync provider adapter
internal/watchsync/plugin_provider.go, internal/watchsync/plugin_provider_state.go, internal/watchsync/plugin_provider_test.go
Implements descriptor validation, API-key and device authorization, account lookup, remote-state pagination, event and media conversion, result mapping, live scrobbling, and sanitized provider errors.
Provider discovery and registry replacement
cmd/silo/..., internal/watchsync/registry.go, internal/watchsync/registry_test.go
Discovers enabled plugin capabilities, reloads providers during lifecycle changes and startup, and atomically replaces plugin providers while preserving built-ins.
Credentials, export, and scrobble reconciliation
internal/watchsync/repository.go, internal/watchsync/service.go, internal/watchsync/types.go, internal/watchsync/lists.go, internal/watchsync/service_test.go, internal/watchsync/repository_test.go, migrations/sql/...
Adds encrypted plugin credentials, device-code encryption, typed export states, partial-result handling, rate-limit deferral, incremental imports, completed-scrobble persistence, and durable history reconciliation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginLifecycle
  participant SiloServer
  participant PluginService
  participant WatchSyncProvider
  participant WatchSyncRepository

  PluginLifecycle->>SiloServer: trigger provider reload
  SiloServer->>PluginService: resolve watch-sync capability
  PluginService-->>SiloServer: return RPC client
  SiloServer->>WatchSyncProvider: construct and register provider
  WatchSyncProvider->>PluginService: apply authentication or sync events
  PluginService-->>WatchSyncProvider: return credentials, state, or event results
  SiloServer->>WatchSyncRepository: persist credentials and export state
Loading

Possibly related PRs

Suggested labels: v1

Suggested reviewers: quick104

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: adding plugin-backed watch-sync providers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the v1 Silo v1 scope - auto-adds to the Silo v1 project label Jul 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/watchsync/plugin_provider.go`:
- Around line 50-61: Update NewPluginProvider and the ExportHistory and scrobble
export paths to honor WatchSyncProviderDescriptor.supported_media_types.
Validate or retain the advertised media types during provider construction, then
reject or skip events whose Media.MediaType is not supported before applying or
exporting them; preserve existing behavior for compatible media types.

In `@internal/watchsync/service_test.go`:
- Around line 329-336: Update
serviceFakeRepo.MarkHistoryExportSatisfiedByScrobble to skip records whose
status is already sent, matching the Postgres implementation. Only set
historyExportStatusSatisfiedByScrobble for matching unsent exports, preserving
existing error and lookup behavior.

In `@internal/watchsync/service.go`:
- Around line 1372-1380: Propagate failures from MarkHistoryExportStatus instead
of discarding them in both non-retryable paths: ExportWatched at
internal/watchsync/service.go lines 1372-1380 and exportLocalPlays at lines
1487-1495. Join each status-write error with the original export error and
return the combined error while preserving the existing failed-count updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59e79505-cbfd-48e7-97d1-027609dd55db

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7fe79 and 69196cb.

📒 Files selected for processing (15)
  • cmd/silo/main.go
  • cmd/silo/main_test.go
  • docs/superpowers/specs/2026-07-25-plugin-watch-sync-provider-design.md
  • internal/pluginhost/client.go
  • internal/pluginhost/handshake.go
  • internal/plugins/service.go
  • internal/plugins/service_hot_reload_test.go
  • internal/watchsync/plugin_provider.go
  • internal/watchsync/plugin_provider_test.go
  • internal/watchsync/registry.go
  • internal/watchsync/registry_test.go
  • internal/watchsync/repository.go
  • internal/watchsync/service.go
  • internal/watchsync/service_test.go
  • internal/watchsync/types.go

Comment thread internal/watchsync/plugin_provider.go
Comment thread internal/watchsync/service_test.go
Comment thread internal/watchsync/service.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/watchsync/service.go (1)

2046-2054: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not make scrobble reconciliation best-effort.

If this update fails, the pre-created export remains pending; a later export can send an ApplyEvents watched event even though the completed stop already succeeded. Persist or durably retry this transition without re-dispatching the stop.

As per coding guidelines, “Keep Go backend behavior predictable under load and during failures.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/watchsync/service.go` around lines 2046 - 2054, Update the scrobble
reconciliation flow around MarkHistoryExportSatisfiedByScrobble so a failure is
not merely logged and ignored. Ensure the transition from pending to satisfied
is persisted or durably retried before completing reconciliation, without
dispatching the stop event again; preserve the existing connection, provider,
and history identifiers.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/watchsync/service.go`:
- Around line 1513-1515: Update the invalid-credential branch in the surrounding
watch-sync operation to persist the failure on the connection’s LastError before
returning it, matching ExportWatched’s behavior. Keep the existing
isWatchSyncInvalidCredentialError check and direct error return after saving the
error.

---

Outside diff comments:
In `@internal/watchsync/service.go`:
- Around line 2046-2054: Update the scrobble reconciliation flow around
MarkHistoryExportSatisfiedByScrobble so a failure is not merely logged and
ignored. Ensure the transition from pending to satisfied is persisted or durably
retried before completing reconciliation, without dispatching the stop event
again; preserve the existing connection, provider, and history identifiers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6936101-d5ed-49ce-9f26-e4950ec2a001

📥 Commits

Reviewing files that changed from the base of the PR and between a07bb13 and df9f85f.

📒 Files selected for processing (6)
  • internal/watchsync/plugin_provider.go
  • internal/watchsync/plugin_provider_test.go
  • internal/watchsync/repository.go
  • internal/watchsync/service.go
  • internal/watchsync/service_test.go
  • internal/watchsync/types.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/watchsync/service_test.go
  • internal/watchsync/plugin_provider.go

Comment thread internal/watchsync/service.go
@Quick104
Quick104 force-pushed the feat/plugin-watch-sync-provider branch from 0f35fd7 to bcb31bc Compare August 5, 2026 23:28

Quick104 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Expanded host support is now on bcb31bca.

This supersedes the original “first slice” scope. The adapter now covers device/API auth, complete credential/config propagation, watched/progress/favorite/watchlist import and export operations, ordered lists, unwatch, and live start/pause/stop scrobbles. It also resolves the outstanding completed-scrobble review finding: after a remote stop succeeds, a failed local history transition is recorded for durable database-only retry, so reconciliation does not dispatch the stop again.

Dependencies and evidence are in the updated PR body. The SDK contract is Silo-Server/silo-plugin-sdk#13; its CI is green. The exact server head passes focused and race tests, changed-line lint with zero issues, local-path verification, and diff checks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (6)
internal/plugins/watch_sync_config_test.go (1)

9-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding two edge cases.

The test covers the fail-closed classification path well. Two additions would guard the branches that production code already handles:

  1. A nil entry in the []*RuntimeConfig slice, which watchSyncProviderConfig skips at line 52.
  2. A field name with surrounding whitespace, which currently routes a declared public field into secret_values. See the related comment on internal/plugins/watch_sync_config.go.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/plugins/watch_sync_config_test.go` around lines 9 - 36, Extend
TestWatchSyncProviderConfigClassifiesManifestFields with cases for a nil
RuntimeConfig entry and a declared public field whose name has surrounding
whitespace. Verify watchSyncProviderConfig skips the nil entry without error,
and assert the whitespace-normalized public field is classified in
GetSecretValues rather than GetValues as required by the existing production
behavior.
internal/watchsync/plugin_provider.go (1)

634-648: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

A descriptor advertising both auth methods loses the device-code flow.

supportedWatchSyncAuthMethod returns a single method. It returns AuthMethodAPIKey as soon as it sees that method, so a descriptor advertising both API key and device code exposes only the API-key route. StartDeviceAuth then rejects every call at line 183.

The single authMethod string field is the constraint, not this function. If dual-method plugins are expected, the provider needs a set of supported methods and the connection routes need to select one. If they are not expected, add a short comment recording that API key takes precedence deliberately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/watchsync/plugin_provider.go` around lines 634 - 648, Update
supportedWatchSyncAuthMethod and the provider’s single authMethod representation
to preserve both API-key and device-code capabilities for descriptors
advertising both methods; adjust the connection/auth routes, including
StartDeviceAuth, to select the appropriate supported method instead of rejecting
device-code authentication. If dual-method support is intentionally out of
scope, retain the current precedence and add a concise comment documenting that
API key deliberately takes precedence.
migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql (1)

9-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that the rollback discards plugin credential data.

Both ADD COLUMN statements are metadata-only on PostgreSQL 11 and later, so the Up path is safe on a large table. The Down path is the correct inverse.

One consequence is worth recording: plugin_credentials is the only store for TokenType, Scopes, and SecretAttributes on plugin-backed connections. A rollback drops that data permanently, and affected users must reconnect their plugin providers. Add a short comment in the migration or a note in the release notes so an operator knows this before rolling back.

The Squawk ban-drop-column warnings on Lines 10 and 13 apply to the Down section and are expected for a reversible column addition.

As per coding guidelines: "Create database changes as timestamped Goose SQL migrations using make migrate-create NAME=...; never run goose fix, never create paired .up.sql and .down.sql files" — this file follows that pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql`
around lines 9 - 13, Add a concise comment near the Down-section DROP COLUMN for
plugin_credentials documenting that rollback permanently discards TokenType,
Scopes, and SecretAttributes and requires affected users to reconnect plugin
providers. Leave the migration structure and expected drop statements unchanged.

Source: Linters/SAST tools

internal/watchsync/service_test.go (1)

592-594: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard ListOpenScrobbleSessions like the other scrobble accessors.

This change puts every other reader and writer of the fake's scrobble state behind scrobbleMu, and adds scrobbleUpdatesSnapshot for safe reads. ListOpenScrobbleSessions is now the only accessor that takes no lock, and it returns the shared slice itself rather than a copy. Current tests set scrobbleSessions before the sweep, so nothing races today. A future test that mutates sessions during a sweep would trip -race.

♻️ Proposed fix
 func (r *serviceFakeRepo) ListOpenScrobbleSessions(_ context.Context) ([]ScrobbleSession, error) {
-	return r.scrobbleSessions, nil
+	r.scrobbleMu.Lock()
+	defer r.scrobbleMu.Unlock()
+	return append([]ScrobbleSession(nil), r.scrobbleSessions...), nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/watchsync/service_test.go` around lines 592 - 594, Update
serviceFakeRepo.ListOpenScrobbleSessions to read scrobbleSessions under
scrobbleMu and return a copy of the slice, matching the locking and snapshot
behavior of the other scrobble accessors. Preserve the existing session contents
and error result while preventing callers from sharing the backing slice.
internal/watchsync/repository.go (1)

1191-1201: 🚀 Performance & Scalability | 🔵 Trivial

Bound the reconciliation query and add a matching partial index.

ListPendingScrobbleReconciliations selects every unreconciled row with no LIMIT. The sweeper calls it on each pass. If reconciliation keeps failing for some sessions, the result set grows without bound and each sweep loads all of them into memory.

Consider a LIMIT with ORDER BY stop_sent_at ASC (already present) so each sweep drains a bounded batch. A partial index also keeps the scan cheap as the session table grows:

CREATE INDEX CONCURRENTLY IF NOT EXISTS watch_provider_scrobble_sessions_pending_reconciliation_idx
    ON watch_provider_scrobble_sessions (stop_sent_at)
    WHERE stop_sent_at IS NOT NULL AND completed = true AND history_reconciled_at IS NULL;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/watchsync/repository.go` around lines 1191 - 1201, Update
ListPendingScrobbleReconciliations to add a bounded LIMIT to the existing
stop_sent_at ASC query so each sweep returns only a fixed batch of pending
reconciliations. Add the matching partial index on
watch_provider_scrobble_sessions(stop_sent_at) for rows with stop_sent_at set,
completed true, and history_reconciled_at unset, using the project’s migration
mechanism.
internal/watchsync/repository_test.go (1)

41-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a negative case for the AAD binding.

The test proves the happy-path round trip. It does not prove the property the AAD exists for: a bundle encrypted for one connection must not decrypt into a different connection. Add a second decode with a changed UserID or Provider and assert that it returns an error. This locks in the cross-connection substitution guarantee.

♻️ Proposed addition
 	if output.AccessToken != input.AccessToken || output.RefreshToken != input.RefreshToken ||
 		output.TokenType != input.TokenType || !output.TokenExpiresAt.Equal(expiresAt) ||
 		!reflect.DeepEqual(output.Scopes, input.Scopes) || !reflect.DeepEqual(output.SecretAttributes, input.SecretAttributes) {
 		t.Fatalf("decoded credentials = %#v", output)
 	}
+	foreign := Connection{Provider: input.Provider, UserID: input.UserID + 1, ProfileID: input.ProfileID}
+	if err := repository.decodePluginCredentials(&foreign, encoded); err == nil {
+		t.Fatalf("credentials decrypted for a different connection: %#v", foreign)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/watchsync/repository_test.go` around lines 41 - 49, Extend the
credential round-trip test around decodePluginCredentials with a negative
AAD-binding case: create a second Connection differing in UserID or Provider,
decode the same encoded bundle into it, and assert that decodePluginCredentials
returns an error. Keep the existing matching-connection assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@go.mod`:
- Line 112: Update the github.com/Silo-Server/silo-plugin-sdk dependency in
go.mod to use an approved released version or a silo-server feature-branch
reference, rather than the pseudo-version sourced from the unmerged
codex/watch-sync-provider-v2 branch; document the follow-up plan if a release is
still required.

In `@internal/plugins/watch_sync_config.go`:
- Around line 55-72: Update the config-field loop around watchSyncConfigString
to trim config.Key and field once, skip entries when either trimmed component is
empty, and build the key from those trimmed values. Use the trimmed field name
for the public lookup so it matches the normalized keys produced by
GlobalConfigFieldSets, while preserving the existing public and protected map
assignment behavior.

In `@internal/watchsync/plugin_provider_state.go`:
- Around line 246-256: The event-building loops in
internal/watchsync/plugin_provider_state.go at lines 246-256 and 284-300 must
record unsupported media as terminal failures instead of silently skipping
items. In both loops, collect the affected play.HistoryID or item.MediaItemID
with unsupportedWatchSyncMediaMessage(...) and merge those entries into
result.Failed after applyPluginEvents returns; in lines 284-300, derive
ListPosition from len(events) so skipped items do not create ordering gaps.
- Around line 188-235: Bound accumulated remote-state items in the traversal
loop around result.items and ListRemoteState by adding a total-item cap
independent of maxRemoteStatePages. Before appending each response’s items,
detect whether the cap would be exceeded and return an error without advancing
or returning a cursor; preserve normal pagination behavior when within the cap
so the next run restarts the family after rejection.

In `@internal/watchsync/plugin_provider.go`:
- Around line 434-447: Update applyScrobble’s handling of applyPluginEvents
results to preserve retry classification: return retryableProviderError for
WATCH_SYNC_APPLY_STATUS_RETRY when the fault is not rate-limited, and return a
terminal error for WATCH_SYNC_APPLY_STATUS_REJECTED. Follow the existing
ExportHistory pattern so SweepOpenScrobbles does not redispatch terminal
failures indefinitely.
- Around line 204-222: Update the device-authorization conversion before
constructing DeviceAuthSession: call response.GetExpiresAt().CheckValid() and
return an incomplete/invalid authorization error when validation fails, then
parse verificationURL with url.Parse and require an allowed scheme before
assigning it. Preserve the existing complete-URL preference and only persist the
session after both validations succeed.

In `@internal/watchsync/repository.go`:
- Around line 1375-1418: Ensure every token write path, including
UpsertConnection and traktCollectionTokenResolver.updateTokens, persists
plugin_credentials consistently by re-encoding credentials through
encodePluginCredentials after Trakt refresh or any
watch_provider_connections.update_tokens SQL update. Alternatively, remove
access-token fields from storedPluginCredentials and treat the connection
columns as authoritative; choose one source of truth and apply it consistently
across all writers.

---

Nitpick comments:
In `@internal/plugins/watch_sync_config_test.go`:
- Around line 9-36: Extend TestWatchSyncProviderConfigClassifiesManifestFields
with cases for a nil RuntimeConfig entry and a declared public field whose name
has surrounding whitespace. Verify watchSyncProviderConfig skips the nil entry
without error, and assert the whitespace-normalized public field is classified
in GetSecretValues rather than GetValues as required by the existing production
behavior.

In `@internal/watchsync/plugin_provider.go`:
- Around line 634-648: Update supportedWatchSyncAuthMethod and the provider’s
single authMethod representation to preserve both API-key and device-code
capabilities for descriptors advertising both methods; adjust the
connection/auth routes, including StartDeviceAuth, to select the appropriate
supported method instead of rejecting device-code authentication. If dual-method
support is intentionally out of scope, retain the current precedence and add a
concise comment documenting that API key deliberately takes precedence.

In `@internal/watchsync/repository_test.go`:
- Around line 41-49: Extend the credential round-trip test around
decodePluginCredentials with a negative AAD-binding case: create a second
Connection differing in UserID or Provider, decode the same encoded bundle into
it, and assert that decodePluginCredentials returns an error. Keep the existing
matching-connection assertions unchanged.

In `@internal/watchsync/repository.go`:
- Around line 1191-1201: Update ListPendingScrobbleReconciliations to add a
bounded LIMIT to the existing stop_sent_at ASC query so each sweep returns only
a fixed batch of pending reconciliations. Add the matching partial index on
watch_provider_scrobble_sessions(stop_sent_at) for rows with stop_sent_at set,
completed true, and history_reconciled_at unset, using the project’s migration
mechanism.

In `@internal/watchsync/service_test.go`:
- Around line 592-594: Update serviceFakeRepo.ListOpenScrobbleSessions to read
scrobbleSessions under scrobbleMu and return a copy of the slice, matching the
locking and snapshot behavior of the other scrobble accessors. Preserve the
existing session contents and error result while preventing callers from sharing
the backing slice.

In `@migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql`:
- Around line 9-13: Add a concise comment near the Down-section DROP COLUMN for
plugin_credentials documenting that rollback permanently discards TokenType,
Scopes, and SecretAttributes and requires affected users to reconnect plugin
providers. Leave the migration structure and expected drop statements unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cfa87662-020c-493b-8311-1c5cb05853bb

📥 Commits

Reviewing files that changed from the base of the PR and between df9f85f and bcb31bc.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (17)
  • cmd/silo/main.go
  • cmd/silo/main_test.go
  • docs/superpowers/specs/2026-07-25-plugin-watch-sync-provider-design.md
  • go.mod
  • internal/pluginhost/client.go
  • internal/plugins/watch_sync_config.go
  • internal/plugins/watch_sync_config_test.go
  • internal/watchsync/lists.go
  • internal/watchsync/plugin_provider.go
  • internal/watchsync/plugin_provider_state.go
  • internal/watchsync/plugin_provider_test.go
  • internal/watchsync/repository.go
  • internal/watchsync/repository_test.go
  • internal/watchsync/service.go
  • internal/watchsync/service_test.go
  • internal/watchsync/types.go
  • migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • cmd/silo/main_test.go
  • cmd/silo/main.go
  • internal/watchsync/service.go

Comment thread go.mod Outdated
Comment thread internal/plugins/watch_sync_config.go Outdated
Comment thread internal/watchsync/plugin_provider_state.go
Comment thread internal/watchsync/plugin_provider_state.go
Comment thread internal/watchsync/plugin_provider.go Outdated
Comment thread internal/watchsync/plugin_provider.go Outdated
Comment thread internal/watchsync/repository.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql`:
- Around line 8-13: Move watch_provider_scrobble_reconcile_pending_idx out of
the current migration into a new timestamped Goose migration, created through
the standard migrate-create workflow. Define the Up migration with a NO
TRANSACTION header, cleanup of any invalid prior index before retrying, and
CREATE INDEX CONCURRENTLY IF NOT EXISTS; define Down with DROP INDEX
CONCURRENTLY IF EXISTS. Remove the normal index creation and rollback statements
from the original migration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a447e62-3442-44bb-b2c9-2641df82f490

📥 Commits

Reviewing files that changed from the base of the PR and between bcb31bc and f296227.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • docs/superpowers/specs/2026-07-25-plugin-watch-sync-provider-design.md
  • go.mod
  • internal/pluginhost/client.go
  • internal/plugins/watch_sync_config.go
  • internal/plugins/watch_sync_config_test.go
  • internal/watchsync/lists.go
  • internal/watchsync/plugin_provider.go
  • internal/watchsync/plugin_provider_state.go
  • internal/watchsync/plugin_provider_test.go
  • internal/watchsync/repository.go
  • internal/watchsync/repository_test.go
  • internal/watchsync/service.go
  • internal/watchsync/service_test.go
  • internal/watchsync/types.go
  • migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/plugins/watch_sync_config_test.go
  • go.mod
  • internal/pluginhost/client.go
  • internal/plugins/watch_sync_config.go
  • docs/superpowers/specs/2026-07-25-plugin-watch-sync-provider-design.md
  • internal/watchsync/plugin_provider.go
  • internal/watchsync/service.go

Comment thread migrations/sql/20260805221735_add_watch_provider_plugin_credentials.sql Outdated
@Quick104

Quick104 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Addressed the current review feedback through d3ec706c (with SDK PR #13 at 206f05f):

  • preserved the released SDK provider interface and CapabilityServers shape while adding device authorization through an additive service/registration API;
  • made pending provider-state updates presence-aware, including explicit empty-state clears;
  • kept provider state out of the public device-auth response;
  • moved the reconciliation partial index into a NO TRANSACTION concurrent migration with invalid-index cleanup;
  • applied the prior credential, config, bounds, list semantics, scrobble classification, reconciliation, and race-safety review fixes.

Validation completed:

  • SDK: go test -race ./... and go vet ./...; CI and CodeRabbit are green at 206f05f.
  • Host: focused race and vet suites for migrations/database/API handlers/watchsync/pluginhost/plugins, plus make migrate-validate.
  • Integration: PR head d3ec706c merges conflict-free with current main 31a26b25; that tree completed the production frontend and backend build on an isolated dev-builder sandbox.
  • Migration: after removing the old sandbox index, startup applied migration 20260806015420; the partial index is recorded, valid, and ready.
  • Floppy: public branch head bea05a4 passed its race suite, built and installed through the real admin plugin API, rejected an invalid key, connected with a valid key, imported watched history and resume progress, exported/scrobbled a watched event, survived restart, and was revalidated after the final SDK update.
  • Device authorization: a disposable SDK-built plugin completed Start -> pending non-empty rotation -> pending explicit-empty clear -> authorized, with polling interval/expiry updates, account lookup, encrypted credentials, no provider state in the HTTP response, and no plaintext credential markers in logs.
  • Final doctor: healthy container, database query, API, and frontend; final logs had no errors, HTTP 5xx responses, or test credential markers.

One intentional merge gate remains open: merge SDK PR #13, publish the approved SDK release, and replace the pseudo-version in go.mod before merging this PR.

@Quick104

Quick104 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The remaining SDK publication gate is closed in faca64a4: this PR now depends on the released github.com/Silo-Server/silo-plugin-sdk v0.13.0 rather than a pseudo-version.

Fresh isolated dev-builder validation also completed against this head:

  • full production frontend/backend build and doctor passed;
  • Floppy 26.8.6 was deployed as a separate healthy companion and the plugin was installed through Silo's real admin plugin API;
  • inbound sync imported exactly one completed watch and one paused-progress item;
  • an incomplete Floppy history session was correctly excluded from watched history after the plugin fix;
  • outbound Silo watched state reached Floppy as a completed history entry;
  • plugin discovery, connection configuration, encrypted credentials, and sync state survived a Silo restart;
  • the deployed Watch Providers page shows Floppy connected with the expected counts and capability toggles, with no browser-console warnings/errors;
  • access tokens and plugin credential bundles are encrypted at rest, and no Floppy token/password markers appear in Silo, Floppy, or Redis logs.

The sandbox and Floppy companion remain running for reviewer verification.

@Quick104 Quick104 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified at head faca64a: all review threads are resolved, the released SDK v0.13.0 dependency is pinned, focused/race/migration validation passed, and the exact head was deployed and exercised with the Floppy plugin on both isolated and shared development environments.

@Quick104
Quick104 merged commit 40a9de7 into Silo-Server:main Aug 6, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this to Done in Silo v1 Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v1 Silo v1 scope - auto-adds to the Silo v1 project

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants