mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
Feat/plugin instance actions (#2209)
* feat(shared): scope plugin action descriptors * feat(plugin-sdk): instance-scoped settings actions * feat(plugins): run instance-scoped actions from the admin settings dialog * feat(admin): instance-scoped plugin actions in the settings dialog * fix(plugins): surface instance actions without instance fields and harden the action gate * docs(plugin-sdk): note that hosts before 4.2.0 ignore action scope * fix(admin): open the dialog for instance actions, explain inactive 404s, guard the save window * docs(wiki): plugin pages for instance actions and the admin panel changes
This commit is contained in:
@@ -13,7 +13,7 @@ import { pluginsEnabled } from './kill-switch';
|
||||
import { devLinkEnabled } from './dev-link';
|
||||
import { PluginActivateDto, PluginConfigDto, PluginEgressHostsDto, PluginInstallDto, PluginLinkDto, PluginRetrustDto, PluginUninstallDto, PluginUpdateDto } from './plugins.dto';
|
||||
import { ManagedForbidden, isManagedBlocked, MANAGED_FORBIDDEN_ERROR } from '../common/managed';
|
||||
import type { PluginInstanceConfigResponse, PluginInstanceConfigUpdated } from '@trek/shared';
|
||||
import type { PluginActionResult, PluginInstanceConfigResponse, PluginInstanceConfigUpdated } from '@trek/shared';
|
||||
import { RuntimeEnvService } from '../app-config/runtime-env.service';
|
||||
// Straight from sessionManager, not the src/mcp barrel: that one evaluates
|
||||
// readEnv().mcp at module scope and installs the sweep interval, which a domain
|
||||
@@ -141,7 +141,11 @@ export class PluginsController {
|
||||
|
||||
@Get(':id/config')
|
||||
getConfig(@Param('id') id: string): PluginInstanceConfigResponse {
|
||||
return { fields: this.plugins.instanceSettingsFields(id), config: this.plugins.getInstanceConfig(id) };
|
||||
return {
|
||||
fields: this.plugins.instanceSettingsFields(id),
|
||||
config: this.plugins.getInstanceConfig(id),
|
||||
actions: this.runtime.actionsOf(id, 'instance'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +185,41 @@ export class PluginsController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one of the plugin's `scope:'instance'` actions ("Purge cache", "Test SMTP").
|
||||
* ADMIN-INITIATED: the acting user is the clicking admin, bound host-side, so the
|
||||
* handler sees `ctx.config` plus the admin's own settings and any trip read is checked
|
||||
* against them. The scope is fixed by this route — a user-tab key is refused here.
|
||||
*
|
||||
* Returns the SAME 404 `{ error: 'Plugin is not active' }` body as
|
||||
* PluginUserSettingsController.runAction, and a failing action is a RESULT, not a
|
||||
* server error, on both — but the two routes reach 404 by checking DIFFERENT things.
|
||||
* This route asks `runtime.isActive(id)`, the supervisor's live-process check: is
|
||||
* there actually a forked child to run the action in. The user route instead reads
|
||||
* the plugin's DB `status` column. They can disagree — a plugin can be `status =
|
||||
* 'active'` in the DB with no live child (e.g. it crashed and hasn't been
|
||||
* respawned yet) — so a 404 here does not always mean the row says inactive; it can
|
||||
* also mean a stale-active row with nothing behind it. The client (`useInstanceSettings`)
|
||||
* treats this 404 as authoritative and flips its own `active` flag to match.
|
||||
*/
|
||||
@Post(':id/actions/:key')
|
||||
@HttpCode(200)
|
||||
async runAction(
|
||||
@Param('id') id: string,
|
||||
@Param('key') key: string,
|
||||
@Req() req: Request & { user?: { id: number } },
|
||||
): Promise<PluginActionResult> {
|
||||
const adminId = req.user?.id;
|
||||
if (!pluginsEnabled() || adminId == null || !this.runtime.isActive(id)) {
|
||||
throw new HttpException({ error: 'Plugin is not active' }, 404);
|
||||
}
|
||||
try {
|
||||
return await this.runtime.invokeAction(id, key, adminId, 'instance');
|
||||
} catch (e) {
|
||||
return { ok: false, message: (e instanceof Error ? e.message : 'Action failed').slice(0, 200) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator-supplied egress hosts. A plugin that talks to a SELF-HOSTED service can't
|
||||
* name the operator's hostname in its manifest, so an admin adds it here — and the
|
||||
|
||||
Reference in New Issue
Block a user