#200: hard instance-wide plugins.enabled kill switch
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m38s
CI / Build container images (pull_request) Successful in 4m11s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 1m9s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Failing after 51s
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 5m37s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m38s
CI / Build container images (pull_request) Successful in 4m11s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 1m9s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Failing after 51s
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 5m37s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
plugins.enabled (instance setting, default on — plugins predate the switch; the VS-NfD reference configuration turns it off) makes every plugin surface answer 404 via a shared guard: Site-Admin install/list/mode, pond activation and plugin list, the sandbox frame and asset routes. The dropzone watcher quarantines drops instead of installing. Deliberately NOT guarded: the authenticated fallback-metadata route — it serves no plugin code and existing plugin_block nodes need it to render their declared fallback (an image fallback degrades to the neutral placeholder while off, because its bytes live on the disabled asset surface). The editor offers no plugin blocks because the pond plugin list is one of the 404ing surfaces. Admin settings panel gets the toggle (i18n de+en) with the documented api-restart note (in-process settings cache). Answers "code execution inside the zone?" with one verifiable off-switch instead of per-plugin trust machinery (#232, ADR 0025). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
This commit is contained in:
parent
74970f6073
commit
c4c84b33f9
@ -24,6 +24,7 @@ import { SiteAdminGuard } from '../admin/site-admin.guard';
|
||||
import { AuthedRequest } from '../auth/auth.guard';
|
||||
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
||||
|
||||
import { PluginsEnabledGuard } from './plugins-enabled.guard';
|
||||
import { PluginsService } from './plugins.service';
|
||||
import { MAX_PLUGIN_ZIP_BYTES, PluginPackageError } from './plugin.constants';
|
||||
|
||||
@ -52,7 +53,10 @@ function toHttpException(error: PluginPackageError): HttpException {
|
||||
* and per-pond-activation writes arrive with the admin UI (#72).
|
||||
*/
|
||||
@Controller('admin/plugins')
|
||||
@UseGuards(SiteAdminGuard)
|
||||
// Kill switch first (issue #200): while plugins are disabled instance-wide,
|
||||
// even a Site Admin sees 404 here — the switch is flipped in the settings
|
||||
// panel, not by probing dead routes.
|
||||
@UseGuards(PluginsEnabledGuard, SiteAdminGuard)
|
||||
export class PluginAdminController {
|
||||
constructor(private readonly plugins: PluginsService) {}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
Req,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { PluginFallbackView } from '@dorfteich/shared';
|
||||
@ -13,9 +14,11 @@ import type { PluginFallbackView } from '@dorfteich/shared';
|
||||
import { Public } from '../auth/auth.guard';
|
||||
import { AppConfig } from '../config/app-config.service';
|
||||
import { AuthenticatedOnly } from '../permissions/permission.decorators';
|
||||
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
||||
|
||||
import { buildPluginAssetBase, buildPluginFrameCsp, buildPluginFrameHtml } from './plugin-frame';
|
||||
import { PluginStorageService } from './plugin-storage.service';
|
||||
import { PluginsEnabledGuard } from './plugins-enabled.guard';
|
||||
import { PluginsService } from './plugins.service';
|
||||
|
||||
/** Content types for the file kinds a plugin bundle ships. Unknown extensions
|
||||
@ -56,6 +59,7 @@ export class PluginAssetsController {
|
||||
private readonly plugins: PluginsService,
|
||||
private readonly storage: PluginStorageService,
|
||||
private readonly config: AppConfig,
|
||||
private readonly settings: InstanceSettingsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -63,12 +67,19 @@ export class PluginAssetsController {
|
||||
* (issue #76). Deliberately NOT `@Public`: it names installed plugins, which
|
||||
* is instance metadata for signed-in users, not sandbox-servable content.
|
||||
* 404 covers "never installed" — the client shows a neutral placeholder.
|
||||
* Deliberately NOT behind the kill switch either (issue #200): it serves no
|
||||
* plugin code, and with plugins disabled the existing blocks still need it
|
||||
* to render their declared fallback. An image fallback degrades to the
|
||||
* neutral placeholder then — its bytes live on the disabled asset surface.
|
||||
*/
|
||||
@Get(':id/fallback')
|
||||
@AuthenticatedOnly()
|
||||
async fallback(@Param('id') id: string): Promise<PluginFallbackView> {
|
||||
const view = await this.plugins.fallbackFor(id);
|
||||
if (!view) throw new NotFoundException();
|
||||
if (view.fallback?.type === 'image' && !(await this.settings.get('plugins.enabled'))) {
|
||||
return { ...view, fallback: null };
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
@ -79,6 +90,7 @@ export class PluginAssetsController {
|
||||
*/
|
||||
@Get(':id/:version/frame')
|
||||
@Public()
|
||||
@UseGuards(PluginsEnabledGuard)
|
||||
async frame(
|
||||
@Param('id') id: string,
|
||||
@Param('version') version: string,
|
||||
@ -102,6 +114,7 @@ export class PluginAssetsController {
|
||||
|
||||
@Get(':id/:version/*rest')
|
||||
@Public()
|
||||
@UseGuards(PluginsEnabledGuard)
|
||||
async serve(
|
||||
@Param('id') id: string,
|
||||
@Param('version') version: string,
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
Put,
|
||||
Body,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
pondPluginToggleInputSchema,
|
||||
@ -21,6 +22,7 @@ import { AuthedRequest } from '../auth/auth.guard';
|
||||
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
||||
import { RequiresPondRole } from '../permissions/permission.decorators';
|
||||
|
||||
import { PluginsEnabledGuard } from './plugins-enabled.guard';
|
||||
import { PluginsService } from './plugins.service';
|
||||
import { PluginPackageError } from './plugin.constants';
|
||||
|
||||
@ -31,6 +33,10 @@ import { PluginPackageError } from './plugin.constants';
|
||||
* plugins is a Pond Admin action.
|
||||
*/
|
||||
@Controller('ponds/:pondId/plugins')
|
||||
// Kill switch (issue #200): with plugins disabled instance-wide the SPA's
|
||||
// plugin-list query 404s, which its consumers treat as "no plugins" — the
|
||||
// editor then offers no plugin blocks.
|
||||
@UseGuards(PluginsEnabledGuard)
|
||||
export class PluginPondController {
|
||||
constructor(private readonly plugins: PluginsService) {}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { ClockService } from '../common/clock.service';
|
||||
import { AppConfig } from '../config/app-config.service';
|
||||
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
||||
|
||||
import { PluginStorageService } from './plugin-storage.service';
|
||||
import { PluginPackageError } from './plugin.constants';
|
||||
@ -31,6 +32,7 @@ export class PluginWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly plugins: PluginsService,
|
||||
private readonly storage: PluginStorageService,
|
||||
private readonly config: AppConfig,
|
||||
private readonly settings: InstanceSettingsService,
|
||||
private readonly clock: ClockService,
|
||||
private readonly logger: PinoLogger,
|
||||
) {
|
||||
@ -83,6 +85,13 @@ export class PluginWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
async processDropped(
|
||||
filePath: string,
|
||||
): Promise<{ installed: true; id: string } | { installed: false; code: string }> {
|
||||
// The kill switch (issue #200) covers this install surface like the GUI
|
||||
// routes: a drop is quarantined, not installed, while plugins are off.
|
||||
if (!(await this.settings.get('plugins.enabled'))) {
|
||||
await this.quarantine(filePath);
|
||||
this.logger.warn({ file: filePath }, 'plugins disabled; quarantined dropped plugin ZIP');
|
||||
return { installed: false, code: 'plugins_disabled' };
|
||||
}
|
||||
let zip: Buffer;
|
||||
try {
|
||||
zip = await readFile(filePath);
|
||||
|
||||
23
apps/api/src/plugins/plugins-enabled.guard.ts
Normal file
23
apps/api/src/plugins/plugins-enabled.guard.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CanActivate, Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
||||
|
||||
/**
|
||||
* Instance-wide plugin kill switch (issue #200, ADR 0025): while
|
||||
* `plugins.enabled` is false, every guarded plugin surface answers 404 —
|
||||
* existence stays hidden, the same semantics as `api.enabled` and
|
||||
* `mcp.enabled`. The fallback-metadata route is deliberately NOT guarded
|
||||
* (it serves no plugin code and existing blocks need it for their declared
|
||||
* fallback). The settings cache is in-process, so flipping the switch is
|
||||
* followed by an api restart like every other instance setting
|
||||
* (operations.md); the admin UI toggle documents that.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PluginsEnabledGuard implements CanActivate {
|
||||
constructor(private readonly settings: InstanceSettingsService) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
if (!(await this.settings.get('plugins.enabled'))) throw new NotFoundException();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -379,3 +379,138 @@ describe.skipIf(!hasTestDb)('plugins install (e2e, issue #71)', () => {
|
||||
expect(list.body.map((p: { id: string }) => p.id)).not.toContain('removable');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Instance-wide plugin kill switch (issue #200, ADR 0025): with
|
||||
* `plugins.enabled = false` every plugin surface answers 404 — even for a
|
||||
* Site Admin — while existing blocks keep their declared fallback readable;
|
||||
* the dropzone quarantines instead of installing; and the switch itself is
|
||||
* flipped through the admin settings surface. Lives in this file because the
|
||||
* install suite above wipes the plugin registry in its setup — a separate
|
||||
* parallel file would race that wipe.
|
||||
*/
|
||||
describe.skipIf(!hasTestDb)('plugins kill switch (e2e, issue #200)', () => {
|
||||
let app: INestApplication;
|
||||
let prisma: PrismaClient;
|
||||
let watcher: PluginWatcherService;
|
||||
let storage: PluginStorageService;
|
||||
const suffix = uniqueSuffix();
|
||||
const password = 'schalter aus heisst wirklich aus 1';
|
||||
const admin = `kira-killswitch-${suffix}`;
|
||||
const pluginId = `switched-${suffix}`;
|
||||
let adminCookie: string;
|
||||
let pondId: string;
|
||||
|
||||
const api = () => request(app.getHttpServer());
|
||||
|
||||
beforeAll(async () => {
|
||||
prisma = createTestPrisma();
|
||||
// Written BEFORE the app boots — the settings cache is in-process and
|
||||
// fills on first read. Cleared afterAll.
|
||||
await prisma.instanceSetting.upsert({
|
||||
where: { key: 'plugins.enabled' },
|
||||
create: { key: 'plugins.enabled', value: false },
|
||||
update: { value: false },
|
||||
});
|
||||
// Registry row for the fallback assertion, created directly — the
|
||||
// install route is exactly what the switch turns off.
|
||||
await prisma.plugin.create({
|
||||
data: {
|
||||
id: pluginId,
|
||||
name: 'Switched Off',
|
||||
version: '1.0.0',
|
||||
apiVersion: '1',
|
||||
kind: 'code',
|
||||
mode: 'OPTIONAL',
|
||||
manifest: {
|
||||
...codeManifest({ id: pluginId, name: 'Switched Off' }),
|
||||
fallback: { type: 'text', value: 'Der Block schlummert.' },
|
||||
},
|
||||
},
|
||||
});
|
||||
app = await createTestApp();
|
||||
watcher = app.get(PluginWatcherService);
|
||||
storage = app.get(PluginStorageService);
|
||||
const users = app.get(UsersService);
|
||||
const tokens = app.get(AuthTokensService);
|
||||
const adminUser = await users.createUser({
|
||||
username: admin,
|
||||
email: `${admin}@example.org`,
|
||||
displayName: `Kira Killswitch ${suffix}`,
|
||||
password,
|
||||
locale: 'en',
|
||||
});
|
||||
const verify = await tokens.issue(adminUser.id, 'EMAIL_VERIFICATION', 600);
|
||||
await api().post('/api/v1/auth/verify-email').send({ token: verify }).expect(204);
|
||||
await prisma.user.update({ where: { id: adminUser.id }, data: { isSiteAdmin: true } });
|
||||
const login = await api()
|
||||
.post('/api/v1/auth/login')
|
||||
.send({ usernameOrEmail: admin, password })
|
||||
.expect(200);
|
||||
adminCookie = sessionCookieOf(login);
|
||||
pondId = (await prisma.pond.findFirstOrThrow({ where: { ownerId: adminUser.id } })).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.instanceSetting.deleteMany({ where: { key: 'plugins.enabled' } });
|
||||
await prisma.plugin.deleteMany({ where: { id: pluginId } });
|
||||
const where = { pond: { owner: { username: { contains: suffix } } } };
|
||||
await prisma.roleGrant.deleteMany({ where });
|
||||
await prisma.pond.deleteMany({ where: { owner: { username: { contains: suffix } } } });
|
||||
await prisma.user.deleteMany({ where: { username: { contains: suffix } } });
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('404s every plugin surface, even for a Site Admin', async () => {
|
||||
await api().get('/api/v1/admin/plugins').set('Cookie', adminCookie).expect(404);
|
||||
await api()
|
||||
.post('/api/v1/admin/plugins')
|
||||
.set('Cookie', adminCookie)
|
||||
.attach('file', pluginZip(codeManifest({ id: 'nope', name: 'Nope' })), 'nope.zip')
|
||||
.expect(404);
|
||||
await api().get(`/api/v1/ponds/${pondId}/plugins`).set('Cookie', adminCookie).expect(404);
|
||||
await api()
|
||||
.put(`/api/v1/ponds/${pondId}/plugins/${pluginId}`)
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ enabled: true })
|
||||
.expect(404);
|
||||
await api().get(`/api/v1/plugins/${pluginId}/1.0.0/frame`).expect(404);
|
||||
await api().get(`/api/v1/plugins/${pluginId}/1.0.0/plugin.js`).expect(404);
|
||||
});
|
||||
|
||||
it('keeps the declared fallback readable so existing blocks render it', async () => {
|
||||
const res = await api()
|
||||
.get(`/api/v1/plugins/${pluginId}/fallback`)
|
||||
.set('Cookie', adminCookie)
|
||||
.expect(200);
|
||||
expect(res.body).toMatchObject({
|
||||
name: 'Switched Off',
|
||||
fallback: { type: 'text', value: 'Der Block schlummert.' },
|
||||
});
|
||||
});
|
||||
|
||||
it('quarantines a dropzone drop instead of installing it', async () => {
|
||||
await storage.ensureServiceDirs();
|
||||
const dropPath = join(storage.dropzoneDir, `switched-drop-${suffix}.zip`);
|
||||
await writeFile(dropPath, pluginZip(codeManifest({ id: 'dropped', name: 'Dropped' })));
|
||||
const outcome = await watcher.processDropped(dropPath);
|
||||
expect(outcome).toEqual({ installed: false, code: 'plugins_disabled' });
|
||||
const quarantined = await readdir(storage.quarantineDir);
|
||||
expect(quarantined.some((name) => name.endsWith(`switched-drop-${suffix}.zip`))).toBe(true);
|
||||
});
|
||||
|
||||
it('is flipped through the admin settings surface', async () => {
|
||||
await api()
|
||||
.patch('/api/v1/admin/settings')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ 'plugins.enabled': true })
|
||||
.expect(200);
|
||||
await api().get('/api/v1/admin/plugins').set('Cookie', adminCookie).expect(200);
|
||||
await api()
|
||||
.patch('/api/v1/admin/settings')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ 'plugins.enabled': false })
|
||||
.expect(200);
|
||||
await api().get('/api/v1/admin/plugins').set('Cookie', adminCookie).expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { SettingsModule } from '../settings/settings.module';
|
||||
|
||||
import { PluginAdminController } from './plugin-admin.controller';
|
||||
import { PluginAssetsController } from './plugin-assets.controller';
|
||||
@ -9,6 +10,7 @@ import { PluginPondController } from './plugin-pond.controller';
|
||||
import { PluginPackageService } from './plugin-package.service';
|
||||
import { PluginStorageService } from './plugin-storage.service';
|
||||
import { PluginWatcherService } from './plugin-watcher.service';
|
||||
import { PluginsEnabledGuard } from './plugins-enabled.guard';
|
||||
import { PluginsService } from './plugins.service';
|
||||
|
||||
/**
|
||||
@ -18,12 +20,13 @@ import { PluginsService } from './plugins.service';
|
||||
* dropzone directory watcher.
|
||||
*/
|
||||
@Module({
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, SettingsModule],
|
||||
controllers: [PluginAdminController, PluginAssetsController, PluginPondController],
|
||||
providers: [
|
||||
PluginFallbackRenderer,
|
||||
PluginPackageService,
|
||||
PluginStorageService,
|
||||
PluginsEnabledGuard,
|
||||
PluginsService,
|
||||
PluginWatcherService,
|
||||
],
|
||||
|
||||
@ -72,6 +72,15 @@ export const INSTANCE_SETTINGS = {
|
||||
// Built-in MCP endpoint master switch (issue #105, default off) —
|
||||
// independent of the REST switch; ponds opt in via `mcpEnabled`.
|
||||
'mcp.enabled': z.boolean().default(false),
|
||||
// Plugin-architecture master switch (issue #200, ADR 0025). Default ON:
|
||||
// plugins predate the switch, so existing instances keep working; the
|
||||
// VS-NfD reference configuration (#227) turns it off. While off, every
|
||||
// plugin surface answers 404 (admin install/list, pond toggles, frame
|
||||
// and asset routes) — only the authenticated fallback-metadata route
|
||||
// stays, so existing blocks still render their declared text fallback
|
||||
// (an image fallback degrades to neutral text: its bytes live on the
|
||||
// disabled asset surface).
|
||||
'plugins.enabled': z.boolean().default(true),
|
||||
// Atom feed master switch (issue #191). Default ON: feeds predate the
|
||||
// switch, so existing instances and their subscribed readers keep
|
||||
// working; the VS-NfD reference configuration (#227) turns it off.
|
||||
|
||||
@ -25,6 +25,7 @@ interface InstanceSettings {
|
||||
'api.enabled': boolean;
|
||||
'mcp.enabled': boolean;
|
||||
'feeds.enabled': boolean;
|
||||
'plugins.enabled': boolean;
|
||||
'upload.allowedExtensions': string[];
|
||||
'upload.svgPolicy': 'reject' | 'sanitize';
|
||||
'legal.imprint': string;
|
||||
@ -249,6 +250,15 @@ function PublicApiSettingsForm({ settings }: { settings: InstanceSettings }): Re
|
||||
{t('admin.feedsLabel')}
|
||||
</label>
|
||||
<p className="api-opt-in__hint">{t('admin.feedsHint')}</p>
|
||||
<label className="api-opt-in__label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings['plugins.enabled']}
|
||||
onChange={(event) => void save({ 'plugins.enabled': event.target.checked })}
|
||||
/>
|
||||
{t('admin.pluginsLabel')}
|
||||
</label>
|
||||
<p className="api-opt-in__hint">{t('admin.pluginsHint')}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -115,6 +115,23 @@ person looking at it could.
|
||||
removed, existing `plugin_block` nodes render the manifest `fallback`
|
||||
(documents are never mutated by plugin removal).
|
||||
|
||||
**Instance kill switch (issue #200, ADR 0025)**: `plugins.enabled`
|
||||
(instance setting, default on; the VS-NfD reference configuration turns
|
||||
it off) sits above the whole lifecycle. While off, every plugin surface
|
||||
answers 404 — admin install/list/mode, pond activation, the sandbox frame
|
||||
and asset routes — and the dropzone watcher quarantines instead of
|
||||
installing. Only the authenticated fallback-metadata route stays alive:
|
||||
it serves no plugin code, and existing `plugin_block` nodes use it to
|
||||
render their declared fallback (an image fallback degrades to the neutral
|
||||
placeholder, because its bytes live on the disabled asset surface — in
|
||||
the reference configuration no plugin is installed, so nothing degrades).
|
||||
The editor offers no plugin blocks because the pond plugin list is one of
|
||||
the 404ing surfaces. Like every instance setting it is cached in-process:
|
||||
flipping it is followed by an api restart to take full effect. This
|
||||
single, verifiable off-switch is what answers "code execution inside the
|
||||
zone?" at the offer stage — cheaper than per-plugin trust machinery
|
||||
(#232) and sufficient because it removes the surface entirely.
|
||||
|
||||
## Reference plugins (shipped with the product, also serving as examples)
|
||||
|
||||
- `section-styles-basic` (`section_style`): a set of colored callout/box
|
||||
|
||||
@ -110,7 +110,7 @@ chain`_
|
||||
- [x] **Attachment-Integritätshashes** · +2–3 AT · #199 ⟵ neu aus Roadmap
|
||||
SHA-256-Spalte, Berechnung beim Upload, Prüfung beim Download,
|
||||
Backfill-Migration. Nebennutzen: Orphan-Sweep, Dedup, Backup-Verifikation.
|
||||
- [ ] **Plugins hart abschaltbar** (`plugins.enabled = false`) · +2 AT · #200 ⟵ neu
|
||||
- [x] **Plugins hart abschaltbar** (`plugins.enabled = false`) · +2 AT · #200 ⟵ neu
|
||||
Deckt das Risiko „Codeausführung in der VS-Zone" für den
|
||||
Angebotsstand vollständig ab. Hash-Pinning siehe Phase 4.
|
||||
- [ ] **Syslog/SIEM: Ereigniskatalog** · +3–4 AT · #201 ⟵ neu aus Roadmap
|
||||
|
||||
@ -52,7 +52,9 @@
|
||||
"mcpLabel": "Eingebauten MCP-Endpoint aktivieren",
|
||||
"mcpHint": "Hauptschalter (standardmäßig aus), unabhängig von der REST-API. MCP-Clients verbinden sich mit einem API-Token auf /api/mcp; jeder Teich gibt sich zusätzlich über seine Teich-Einstellungen frei. Siehe docs/self-hosting/public-api.md.",
|
||||
"feedsLabel": "Atom-Feeds aktivieren",
|
||||
"feedsHint": "Hauptschalter (standardmäßig an). Ausgeschaltet antworten alle Feed-Adressen und die Feed-Token-Verwaltung mit 404 — für gehärtete Umgebungen, in denen Feed-Tokens als Lese-Zugangsdaten nicht in URLs auftauchen dürfen."
|
||||
"feedsHint": "Hauptschalter (standardmäßig an). Ausgeschaltet antworten alle Feed-Adressen und die Feed-Token-Verwaltung mit 404 — für gehärtete Umgebungen, in denen Feed-Tokens als Lese-Zugangsdaten nicht in URLs auftauchen dürfen.",
|
||||
"pluginsLabel": "Plugin-Architektur aktivieren",
|
||||
"pluginsHint": "Hauptschalter (standardmäßig an). Ausgeschaltet antworten alle Plugin-Oberflächen mit 404 — Installation, Teich-Freigaben, Sandbox-Frames und -Assets — und bestehende Plugin-Blöcke zeigen ihren hinterlegten Fallback. Für gehärtete Umgebungen, die „keine Fremdcode-Ausführung“ nachweisbar beantworten müssen. Greift vollständig nach einem api-Neustart (Einstellungen sind im Prozess gecacht)."
|
||||
},
|
||||
"feed": {
|
||||
"title": "Feed-Tokens",
|
||||
|
||||
@ -52,7 +52,9 @@
|
||||
"mcpLabel": "Enable the built-in MCP endpoint",
|
||||
"mcpHint": "Master switch (default off), independent of the REST API. MCP clients connect to /api/mcp with an API token; each pond additionally opts in via its pond settings. See docs/self-hosting/public-api.md.",
|
||||
"feedsLabel": "Enable Atom feeds",
|
||||
"feedsHint": "Master switch (default on). While off, every feed URL and the feed-token management answer 404 — for hardened environments where feed tokens must not appear in URLs as read credentials."
|
||||
"feedsHint": "Master switch (default on). While off, every feed URL and the feed-token management answer 404 — for hardened environments where feed tokens must not appear in URLs as read credentials.",
|
||||
"pluginsLabel": "Enable the plugin architecture",
|
||||
"pluginsHint": "Master switch (default on). While off, every plugin surface answers 404 — install, pond activation, sandbox frames and assets — and existing plugin blocks show their declared fallback. For hardened environments that must answer \"no third-party code execution\" verifiably. Takes full effect after an api restart (settings are cached in-process)."
|
||||
},
|
||||
"feed": {
|
||||
"title": "Feed tokens",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user