RFC: Plugin-Registrable Actions #1380
Replies: 8 comments 63 replies
|
Gap 1, i was thinking free form so plugins can pick the same categories as the core if they want to. Gap 2, dynamic. Gap 3, we could surface it, if we want to support it in the first place. gap 4, I'd lean on UUIDs gap 5, obviously the id would change if the class name changes. that was intentional in the proposal i made in chat, but if you want stable ids across refactors and renames then we can introduce a new key or id field, in which the id is either used as is, or derived from. but now we need to manage key/id collision across all actions across all active plugins, whereas if we leave it on auto inferred from the class name (+ namespace) then there will be less likely that there would be collisions from different plugins. gap 6, for api shape, then whatever you (@hidden4003) or @harshithmohan wants will do. gap 7, depends on what we want gap 8. all actions would require admin. privileges, as normal users shouldn't need to nor have the right to run them. if someone disagrees then we can take it from there. gap 9, ephemeral instances, created when iterated through the service. if a plugin wants a singleton, they can declare it as such. we only ask the DI to initialize or grab an instance for us to use, so not our concern. gap 10, depending on if we run it in the queue or not. if it's ran in the queue, then we log it the same way as when running in the queue. if we run it directly, then we can surface it to the client or caller. gap 11, cancellation would be set by the caller. so for direct calls by a client, then would be bound to the request timeout/abort, whereas for other plugins it would be whatever they decide. timeout would just be a cancellation token which cancels after a set time. gap 12, we won't remove the older actions, but we'll port all of them to use the newer system at once, so there will be a grace period where both the older actions and new actions will function for the client to implement it's needed changes to use the newer system. gap 13, no need to document here. just know there are a few. gap 14, everything would be parameterless (except the cancellation token, which is optional), but there could still be actions to run which depends on other factors than caller parameters. gap 15, I'd lean on opt-in, so the service has a direct call and schedule in queue, and the client can decide to run it then and there, or run it in the queue. if ran then and there, then the cancellation token will be tied to the request, but if ran in the queue, then it would be tied to the system uptime or just not set. gap 16, that's for you (@hidden4003) and @harshithmohan to decide if you want to change it or keep it. gap 17, |
|
But we could
…On Wed, Jul 8, 2026, 20:01 hidden4003 ***@***.***> wrote:
we currently have descriptions for actions, not for categories
—
Reply to this email directly, view it on GitHub
<#1380?email_source=notifications&email_token=AGGOKQTIKRWMT5A4IXYVXV35DZLLVA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCNZVG4ZTQOJXUZZGKYLTN5XKO3LFNZ2GS33OUVSXMZLOOSWGM33PORSXEX3DNRUWG2Y#discussioncomment-17573897>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AGGOKQUIA3HWTISLBZSKGA35DZLLVAVCNFSNUABHKJSXA33TNF2G64TZHMZDEOJXHE4DQOB3IRUXGY3VONZWS33OHMYTAMZYGE3TQMFBOYBA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***
com>
|
|
will this be User Based/Profile-like settings for plugins ? or it all-for-one ? |
|
@revam @harshithmohan @da3dsoul Still waiting for feedback on latest v5 revision, hopefully most concerns are addressed in this revision. |
Gap 6 Proposal: Endpoint ShapeListing: Single endpoint with
Invocation: Per-entity routes because the entity ID is real execution context that gets resolved before the action is enqueued.
Rationale against split listing routes: The controller implementation already has a single Rationale for split invoke routes: The entity ID is a meaningful part of the resource path — Summary: 1 listing route + 4 invoke routes = 5 total. |
|
Gap 16 -> Out of scope for this discussion. If the server side changes are done, the webui can still defer the modal for later. For now it send nothing. BUT on the assumption that if a certain parameter is not sent for an action with parameters, it would have a default already set which can be used. Gap 25/31 -> There is no consumer for the return data from Execute, so it can just log. So option 3 |
|
I suggest we add a new scope |
|
Gap 27 follow-up: the WebUI has no content for the confirmation prompt
Proposal: add an optional per-action confirmation message with a generic client-side fallback. public interface IExecutableAction
{
// ... existing members ...
/// <summary>
/// Interrogative prompt shown by the WebUI before invoking when
/// RequiresConfirmation is true (e.g. "Do you want to overwrite all
/// AniDB MyList state?"). Null → WebUI falls back to a generic prompt.
/// </summary>
string? ConfirmationMessage => null;
}And on public string? ConfirmationMessage { get; set; } // mapped through like Name/DescriptionWebUI behavior: when Backward compatibility: fully additive. Core actions migrate at their own pace, plugins opt in, and every action without one still gets a sane generic prompt. This closes the loop Gap 27 opened — the flag currently obligates the WebUI to prompt while handing it nothing to prompt with. |


Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Plugin-Registerable Actions — Spec (v9, 2026-08-22 — every tracked gap resolved, incl. Gap 33)
Problem
Right now the Actions menu in the web UI is a fixed, server-hardcoded list. Plugins have no way to add entries to it. As we add more provider-style plugins (AniZip, and potentially a Relay integration), each needs a way to expose "run this thing on demand" behavior to the user without core needing to know about it in advance.
Goal
Let plugins (and core, by migrating existing actions to the same shape) register discrete, invokable units of work — "actions" — that:
Unchanged since v1.
Proposed interface
Category & plugin-owned groups (Gap 1 / Gap 29)
ActionCategoryis a closed, core-owned enum — a plugin cannot invent a new core-owned category at runtime; adding one requires a PR against core. Two special members carry the fallback/opt-in behavior:Miscellaneousis the shared fallback for any action — core or plugin — that declares no category. This is the RFC's original single-fallbackGenericconcept, reconciled and correctly named.PluginInferredis an explicit opt-in a plugin uses to request its own dedicated group. Its display label is always the owning plugin's own name — collision-free by construction, since plugin names are already unique identifiers. This isn't a new mechanism; it's the same labeling trick the reference implementation already had for the fallback case, now something a plugin chooses rather than something it's defaulted into.Plugins cannot contribute actions directly into an existing core-owned group (e.g. adding an action under
Importalongside core's own). That was the other half of the original proposal and was not adopted.Scope & context (Gap 18/20, hardened per Gap 28, extended per Gap 32)
Five lightweight base classes, one per entity level, using an explicitly-implemented
IScopedAction.SetContextso a plugin author writing a scoped action never sees the setter — see the Appendix for the full listing.IScopedActionisinternalto the core assembly, so a plugin cannot implement it directly and bypass the five base classes;ActionService.AddPartsalso rejects, at load time, any registered type that implementsIScopedActionwithout being one of the five base classes.The fifth,
VideoAction, is scoped toIVideo— for per-video actions that don't fit Episode/Series/Group/Global (Gap 32). Its HTTP surface is named "File", not "Video" (FileActionController, see Endpoint shape below) — this matches the existingFileController's established external naming forVideoLocalelsewhere in the v3 API; internally the scope enum member, base class, and context type all stayVideo/IVideo.Free-form invocation parameters (Gap 23)
Not a single mechanism — the parameter space shape decides which of two paths an action takes.
Bucket 1 — small, closed variant sets. Where the UI only ever needs a handful of fixed, known combinations (
RemoveMissingFiles's two WebUI entries,SyncVotes's export/import split,Update AniDB Info's three-way split), register oneIExecutableActionper variant with the value baked in:Executestays genuinely parameterless. No settable properties, no UI schema dependency, no regression against current behavior — same number of WebUI entries, same effective values, just modeled as distinct actions instead of one parameterized endpoint.Bucket 2 — genuinely open-ended parameter spaces. Where the domain isn't fixed at compile time (
PurgeAllUsedReleases/PurgeAllUnusedReleases'sHashSet<string>? providerNames— the valid set is plugin-extensible, so there's no finite list of combinations to enumerate as variant classes), WebUI does not send free-form parameters for v1. The action runs with its own server-side defaults, same as today. Full per-invocation control is deferred until a dynamic-UI-schema mechanism exists (Gap 30 — explicitly out of scope here).Mechanically, when bucket 2 properties do need to be populated (e.g. by a plugin calling an action programmatically, not via WebUI), it's ordinary public settable properties on the action instance, populated the same way
IQueueJobproperties already are fromJobDataJson:Parameter type constraint (harshithmohan, 2026-08-06): booleans, numbers, and string lists are fine; no nested objects.
Known remaining work, not a design gap: sorting each of the ~20 non-queue-backed actions in
ActionController.cstoday into bucket 1 or bucket 2 is a per-action judgment call — someone has to look at each one once before migration can start.Synchronous validation (Gap 24)
Validateruns on the API thread, before anything is enqueued. A non-null result means the invoke endpoint returns 400 with the givenReasonand never touches the queue. See the Appendix'sSyncVotesExportActionfor a worked example combining this with calling-user identity.Calling-user identity (Gap 26)
Distinct from
Scope's entity context. Any action can opt in to knowing who invoked it via a small marker interface:SyncVotesActionwould implement this to replace today'sUser.JMMUserID/User.IsAniDBUserchecks.RequiresConfirmation&ConfirmationMessage(Gap 27/33)RequiresConfirmationis a UI hint on destructive actions, present in the reference implementation (PR #1383). Server does not enforce a confirmation step; WebUI is expected to prompt before callingExecutewhen this istrue. See Proposed interface above.On its own, that flag only says when to prompt, not what to say —
Descriptionis declarative prose ("This can overwrite AniDB data irreversibly."), not an interrogative confirm/cancel question, and there's no reliable way to derive one client-side: action IDs are UUIDv5 and deliberately unstable across renames (Gap 5), so the WebUI can't maintain its own per-action message map, and plugin-registered actions are invisible to the WebUI ahead of time, so a hardcoded copy would miss them entirely regardless.ConfirmationMessagecloses that gap: an optional interrogative string, shown when set; whennull, the WebUI falls back to a generic"Are you sure you want to run \"{Name}\"?", withDescriptionshown as body context either way. Fully additive —RequiresConfirmation's existing behavior is unchanged, and every action without aConfirmationMessagestill gets a sane generic prompt.Already implemented and merged: PR #1413 added
ConfirmationMessagetoIExecutableActionand all four scoped base classes, threaded it throughExecutableActionInfo/ActionInfo, and gave all 15 actions that setRequiresConfirmation => truea real message (e.g.DeleteSeriesAllDataAction: "Are you sure you want to permanently delete this series and remove all associated files and data?").Registration & validation at plugin load (Gap 20/28)
ActionService.AddParts(called fromPluginManager.InitPlugins) validates each registered type at startup and fails fast with a named error:Scope(via its base class) doesn't match what it derives from — rejected.Permissionnot overridden — rejected (no silent default).IScopedActionwithout being one of the five base classes — rejected (Gap 28; redundant once the interface isinternal, kept as a documented, load-time-checked guard).ActionServiceknows each action's scope at registration time.Execution service (Gap 15/18/23/32)
A single
ActionExecutionJob : IQueueJobhandles every action, all five scopes included:IExecutableActioninstance from DI (transient, per Gap 9).JobDataJson(bucket 2 actions only, per Gap 23 above).VideoLocalRepositoryforVideo, per Gap 32) and callsSetContext.IActionCaller, resolves the callingIUserand callsSetCaller.Execute.Full listing in the Appendix.
Result reporting (Gap 25/31)
Executereturns bareTask. No result-reporting hook was added toIExecutableAction, andActionExecutionJobdoes not carry aDetails-style field for this purpose — there is no consumer for a structured per-action result today. Actions that want to report something (e.g. "Saved 42 AddToMyList Commands") log it, same as the rest of the queue already does.Listing/execution endpoint shape (Gap 6, extended per Gap 32)
A single listing endpoint with an optional
scopefilter, plus one invoke route per entity type:/Executesuffix —POSTto anAction/{id}path implies invocation.Validate, with reason).Listing is scope-filtered, not entity-filtered — actions available for a given scope don't vary by which series/group/episode/video is being viewed, only by which type it is, so there's no per-entity listing route. This settles Gap 7 (bare ack, no tracking ID) and Gap 25/31 (no result payload to shape a response around).
Migration (Gap 12/13)
Of the ~30 actions in
ActionController.cstoday, the roughly 20 that currently bypass the queue via rawTask.Run/Task.Factory.StartNewneed real queue-backedIExecutableActionimplementations written, not just interface conformance. A full inventory-with-effort-estimate pass is not required. A lighter pass — classifying each into the Gap 23 two-bucket split above — is required, since that decision can't be made without looking at each action once. Old endpoints stay deprecated-but-running in parallel until WebUI migrates.Dynamic UI schema — explicitly deferred (Gap 30)
A possible longer-term fix for parameterized actions the two-bucket split above doesn't cover generically (a form/modal driven by a schema the action advertises). Needs its own separate spec. Not part of this revision.
Unchanged since v1/v4
IExecutableActionnaming — Gap 17.SetContext— Gap 18/20/32.Gap tracker (full — v9, all gaps resolved)
Miscellaneous;PluginInferredis an explicit opt-in, not a default. See Category & groups above.EnabledflagAllowConcurrentExecutionPermissionproperty, independent ofScopeExecutethrowsExecutereceives a queue-lifecycle-bound token, not a request-bound oneExecutestays genuinely parameterless under the Gap 23 two-bucket splitIExecutableActionSetContext, extended to 5 scopes per Gap 32ActionService.AddPartsValidate, runs before enqueueIActionCaller, "subject user" phrasing droppedRequiresConfirmationUI-hint flagIExecutableAction, message content given by Gap 33IScopedActionhardeningIScopedActionisinternal;ActionService.AddPartsrejects any bypass at load timeActionCategory.PluginInferred; contributing into existing core-owned groups was not adoptedExecutestays bareTask; no result-reporting hook addedVideoActionScope.Video/VideoAction, HTTP surface namedFileto match existingFileControllernaming forVideoLocal. Adopted by the maintainer directly off a real implementation commit, not reviewer sign-off — seegap-tracker.md.RequiresConfirmationhas no message contentstring? ConfirmationMessage => nullonIExecutableActionand all four scoped base classes, threaded throughExecutableActionInfo/ActionInfo. SeeRequiresConfirmation & ConfirmationMessageabove andgap-tracker.md.33 gaps tracked. All resolved. What's left is implementation — including the per-action Gap 12/13 bucket classification pass, which is planning work, not a design decision.
Appendix: Reference implementation
Not production code, not a decision to react to — a concrete, coherent version of the full chain from HTTP controller down to a specific
IExecutableAction, matching every resolution above. Use it as the base for an actual spike/PR. TheVideo-scope pieces (marked Gap 32 below) are taken directly from revam's real, compiling commit0c273d9e1612a0d733ad38837f1433b8209d9a17, and theConfirmationMessagepieces (marked Gap 33 below) are taken directly from harshithmohan's real, merged PR #1413 (mastercommit18b202b1a) — neither is hand-sketched like the rest of this appendix.1. Core types
2. Six actions, one per shape that's actually in the current
ActionController.cs(plus one Video-scoped example)3.
ActionService— registration, validation, and the invoke entry point4.
ActionExecutionJob— the one generic wrapper job5. Controller — the resolved endpoint shape
All reactions