@vonzio/plugin-api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +5 -0
- package/capabilities.d.ts +69 -0
- package/capabilities.js +286 -0
- package/errors.d.ts +72 -0
- package/errors.js +106 -0
- package/frontend.d.ts +13 -0
- package/frontend.js +16 -0
- package/index.d.ts +945 -0
- package/index.js +24 -0
- package/manifest-validate.d.ts +44 -0
- package/manifest-validate.js +338 -0
- package/manifest.d.ts +121 -0
- package/manifest.js +37 -0
- package/package.json +49 -0
- package/policy.d.ts +98 -0
- package/policy.js +223 -0
- package/version.d.ts +16 -0
- package/version.js +54 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every capability a plugin may declare, as a runtime-iterable tuple.
|
|
3
|
+
* Adding a capability is additive (minor bump); removing/renaming is
|
|
4
|
+
* breaking (major bump). Keep the ordering grouped by domain to match §5.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PLUGIN_CAPABILITIES: readonly ["storage.kv", "db.scoped", "db.access", "encryption.encrypt", "encryption.decrypt", "integrations.read.masked", "integrations.read.decrypted", "integrations.write", "profiles.read", "profiles.resolve", "workspaces.read", "workspaces.write", "auth.gate", "presence.register", "tasks.submit", "sessions.register", "sessions.extend", "sessions.setStatus", "sessions.getConnectedIds", "orchestrator.wake", "events.append", "events.read", "events.subscribe", "dashboard.push", "images.rewrite", "models.list", "notifications.channel", "mcp.register", "scheduler.run", "http.outbound", "secrets.mtls"];
|
|
7
|
+
/**
|
|
8
|
+
* The capability union. Derived from the runtime tuple so there is exactly
|
|
9
|
+
* one place to edit. Total count: 31 (asserted by capabilities.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
export type PluginCapability = (typeof PLUGIN_CAPABILITIES)[number];
|
|
12
|
+
/** Type guard: is an arbitrary string a known capability? */
|
|
13
|
+
export declare function isPluginCapability(value: string): value is PluginCapability;
|
|
14
|
+
/**
|
|
15
|
+
* How a capability is enforced against its surface.
|
|
16
|
+
* - `property`: the whole surface object is included iff the cap is granted.
|
|
17
|
+
* A property-access Proxy can gate this on its own.
|
|
18
|
+
* - `method`: the cap unlocks specific named methods on a surface shared
|
|
19
|
+
* with other caps (e.g. `core.sessionLifecycle.register`). The membrane
|
|
20
|
+
* assembles the surface method-by-method; a property Proxy can't split it.
|
|
21
|
+
* - `argument`: the cap is distinguished from a sibling cap by a call
|
|
22
|
+
* ARGUMENT, not a method name (integrations masked-vs-decrypted via the
|
|
23
|
+
* `opts.decrypt` flag). The membrane wraps the methods to enforce/clamp
|
|
24
|
+
* the argument.
|
|
25
|
+
*/
|
|
26
|
+
export type SurfaceKind = "property" | "method" | "argument";
|
|
27
|
+
export interface CapabilitySurface {
|
|
28
|
+
/**
|
|
29
|
+
* Dotted surface path. `core.*` lives behind the legibility membrane
|
|
30
|
+
* (`ctx.core`); `ctx.*` is a top-level context surface attached by
|
|
31
|
+
* conditional inclusion (membrane only wraps `core`, so non-core
|
|
32
|
+
* surfaces are gated by presence — see §7 + membrane.ts).
|
|
33
|
+
*/
|
|
34
|
+
surface: string;
|
|
35
|
+
kind: SurfaceKind;
|
|
36
|
+
/**
|
|
37
|
+
* For `method` / `argument` kinds: the methods on `surface` this cap
|
|
38
|
+
* unlocks. Omitted for `property` (the whole surface is unlocked).
|
|
39
|
+
*/
|
|
40
|
+
methods?: readonly string[];
|
|
41
|
+
/** One-line description, mirrors the §5 doc comment. */
|
|
42
|
+
description: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Maps every capability to the surface it unlocks. Total `Record` over the
|
|
46
|
+
* union — capabilities.test.ts asserts every member of PLUGIN_CAPABILITIES
|
|
47
|
+
* has an entry so a newly-added capability can't ship ungated.
|
|
48
|
+
*
|
|
49
|
+
* Read-method names for `core.integrations` are shared by the masked and
|
|
50
|
+
* decrypted caps; they differ only by the `opts.decrypt` argument the
|
|
51
|
+
* membrane clamps. `backfillExternalId` is a read-path lazy index helper
|
|
52
|
+
* and travels with the read caps.
|
|
53
|
+
*/
|
|
54
|
+
export declare const CAPABILITY_SURFACE_MAP: Record<PluginCapability, CapabilitySurface>;
|
|
55
|
+
/**
|
|
56
|
+
* Capability pairs that are effectively root and are REFUSED for external
|
|
57
|
+
* plugins at load (§3 step 9, §5). Built-ins are exempt. Each inner array
|
|
58
|
+
* is an AND-set: an external plugin declaring all members is refused.
|
|
59
|
+
*
|
|
60
|
+
* `secrets.mtls` is deliberately NOT listed: the cert/key files are
|
|
61
|
+
* operator-provisioned (policy-declared host paths the plugin can't choose)
|
|
62
|
+
* and resolve to an OPAQUE ref the plugin can't read, so it can't exfiltrate
|
|
63
|
+
* the key even alongside `integrations.read.decrypted` — the design-time
|
|
64
|
+
* guarantee that motivates the combos above doesn't apply here.
|
|
65
|
+
*/
|
|
66
|
+
export declare const ROOT_EQUIVALENT_COMBINATIONS: ReadonlyArray<readonly PluginCapability[]>;
|
|
67
|
+
/** Capabilities only built-in plugins may declare (§5). */
|
|
68
|
+
export declare const BUILTIN_ONLY_CAPABILITIES: ReadonlySet<PluginCapability>;
|
|
69
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
package/capabilities.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Plugin capability enum + the surface map that routes each capability to
|
|
2
|
+
// the `ctx` / `ctx.core` surface it unlocks. This file is the SINGLE SOURCE
|
|
3
|
+
// OF TRUTH consumed by the loader (to validate manifests + cross-check the
|
|
4
|
+
// operator policy) and by the membrane (to assemble the per-plugin `core`
|
|
5
|
+
// object method-by-method and to decide which non-core `ctx` surfaces to
|
|
6
|
+
// attach). See docs/PLUGIN_LOADER_SPEC.md §5, §7.
|
|
7
|
+
//
|
|
8
|
+
// The runtime array `PLUGIN_CAPABILITIES` is authoritative; the
|
|
9
|
+
// `PluginCapability` type is derived from it so a completeness test can
|
|
10
|
+
// iterate every member and the map can be a total `Record`.
|
|
11
|
+
/**
|
|
12
|
+
* Every capability a plugin may declare, as a runtime-iterable tuple.
|
|
13
|
+
* Adding a capability is additive (minor bump); removing/renaming is
|
|
14
|
+
* breaking (major bump). Keep the ordering grouped by domain to match §5.
|
|
15
|
+
*/
|
|
16
|
+
export const PLUGIN_CAPABILITIES = [
|
|
17
|
+
// ── Storage (preferred for new plugins) ──────────────────────────
|
|
18
|
+
"storage.kv",
|
|
19
|
+
// ── Database (use sparingly) ─────────────────────────────────────
|
|
20
|
+
"db.scoped",
|
|
21
|
+
"db.access",
|
|
22
|
+
// ── Crypto ───────────────────────────────────────────────────────
|
|
23
|
+
"encryption.encrypt",
|
|
24
|
+
"encryption.decrypt",
|
|
25
|
+
// ── Integrations ─────────────────────────────────────────────────
|
|
26
|
+
"integrations.read.masked",
|
|
27
|
+
"integrations.read.decrypted",
|
|
28
|
+
"integrations.write",
|
|
29
|
+
// ── Profiles ─────────────────────────────────────────────────────
|
|
30
|
+
"profiles.read",
|
|
31
|
+
"profiles.resolve",
|
|
32
|
+
// ── Workspaces ───────────────────────────────────────────────────
|
|
33
|
+
"workspaces.read",
|
|
34
|
+
"workspaces.write",
|
|
35
|
+
// ── Auth ─────────────────────────────────────────────────────────
|
|
36
|
+
"auth.gate",
|
|
37
|
+
// ── Presence ─────────────────────────────────────────────────────
|
|
38
|
+
"presence.register",
|
|
39
|
+
// ── Tasks / sessions / orchestration ─────────────────────────────
|
|
40
|
+
"tasks.submit",
|
|
41
|
+
"sessions.register",
|
|
42
|
+
"sessions.extend",
|
|
43
|
+
"sessions.setStatus",
|
|
44
|
+
"sessions.getConnectedIds",
|
|
45
|
+
"orchestrator.wake",
|
|
46
|
+
// ── Event log + dashboard push ───────────────────────────────────
|
|
47
|
+
"events.append",
|
|
48
|
+
"events.read",
|
|
49
|
+
"events.subscribe",
|
|
50
|
+
"dashboard.push",
|
|
51
|
+
// ── Chat-surface utilities ───────────────────────────────────────
|
|
52
|
+
"images.rewrite",
|
|
53
|
+
"models.list",
|
|
54
|
+
// ── Plugin-contributed surfaces ──────────────────────────────────
|
|
55
|
+
"notifications.channel",
|
|
56
|
+
"mcp.register",
|
|
57
|
+
"scheduler.run",
|
|
58
|
+
// ── Outbound HTTP ────────────────────────────────────────────────
|
|
59
|
+
"http.outbound",
|
|
60
|
+
// ── Secrets (operator-provisioned material) ──────────────────────
|
|
61
|
+
"secrets.mtls",
|
|
62
|
+
];
|
|
63
|
+
/** O(1) membership check used by the manifest validator. */
|
|
64
|
+
const CAPABILITY_SET = new Set(PLUGIN_CAPABILITIES);
|
|
65
|
+
/** Type guard: is an arbitrary string a known capability? */
|
|
66
|
+
export function isPluginCapability(value) {
|
|
67
|
+
return CAPABILITY_SET.has(value);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Maps every capability to the surface it unlocks. Total `Record` over the
|
|
71
|
+
* union — capabilities.test.ts asserts every member of PLUGIN_CAPABILITIES
|
|
72
|
+
* has an entry so a newly-added capability can't ship ungated.
|
|
73
|
+
*
|
|
74
|
+
* Read-method names for `core.integrations` are shared by the masked and
|
|
75
|
+
* decrypted caps; they differ only by the `opts.decrypt` argument the
|
|
76
|
+
* membrane clamps. `backfillExternalId` is a read-path lazy index helper
|
|
77
|
+
* and travels with the read caps.
|
|
78
|
+
*/
|
|
79
|
+
export const CAPABILITY_SURFACE_MAP = {
|
|
80
|
+
"storage.kv": {
|
|
81
|
+
surface: "ctx.storage",
|
|
82
|
+
kind: "property",
|
|
83
|
+
description: "Per-plugin namespaced key/value store backed by plugin_storage.",
|
|
84
|
+
},
|
|
85
|
+
"db.scoped": {
|
|
86
|
+
surface: "core.db",
|
|
87
|
+
kind: "property",
|
|
88
|
+
description: "Scoped Drizzle handle restricted to manifest.schemaPrefix tables; raw SQL refused.",
|
|
89
|
+
},
|
|
90
|
+
"db.access": {
|
|
91
|
+
surface: "core.db",
|
|
92
|
+
kind: "property",
|
|
93
|
+
description: "Unscoped Drizzle handle + raw SQL. Built-ins only.",
|
|
94
|
+
},
|
|
95
|
+
"encryption.encrypt": {
|
|
96
|
+
surface: "core.encryption",
|
|
97
|
+
kind: "method",
|
|
98
|
+
methods: ["encrypt"],
|
|
99
|
+
description: "Encrypt plugin-owned secrets for persistence.",
|
|
100
|
+
},
|
|
101
|
+
"encryption.decrypt": {
|
|
102
|
+
surface: "core.encryption",
|
|
103
|
+
kind: "method",
|
|
104
|
+
methods: ["decrypt"],
|
|
105
|
+
description: "Decrypt plugin-owned secrets.",
|
|
106
|
+
},
|
|
107
|
+
"integrations.read.masked": {
|
|
108
|
+
surface: "core.integrations",
|
|
109
|
+
kind: "argument",
|
|
110
|
+
methods: [
|
|
111
|
+
"get",
|
|
112
|
+
"getByUserAndType",
|
|
113
|
+
"listByType",
|
|
114
|
+
"listByUserAndType",
|
|
115
|
+
"findByTypeAndExternalId",
|
|
116
|
+
"listByTypeAndExternalId",
|
|
117
|
+
"backfillExternalId",
|
|
118
|
+
],
|
|
119
|
+
description: "Read integration rows with secrets MASKED (opts.decrypt clamped to false).",
|
|
120
|
+
},
|
|
121
|
+
"integrations.read.decrypted": {
|
|
122
|
+
surface: "core.integrations",
|
|
123
|
+
kind: "argument",
|
|
124
|
+
methods: [
|
|
125
|
+
"get",
|
|
126
|
+
"getByUserAndType",
|
|
127
|
+
"listByType",
|
|
128
|
+
"listByUserAndType",
|
|
129
|
+
"findByTypeAndExternalId",
|
|
130
|
+
"listByTypeAndExternalId",
|
|
131
|
+
"backfillExternalId",
|
|
132
|
+
],
|
|
133
|
+
description: "Read integration rows with secrets DECRYPTED. Highest-risk read.",
|
|
134
|
+
},
|
|
135
|
+
"integrations.write": {
|
|
136
|
+
surface: "core.integrations",
|
|
137
|
+
kind: "method",
|
|
138
|
+
methods: ["create", "update", "delete"],
|
|
139
|
+
description: "Create / update / delete integration rows.",
|
|
140
|
+
},
|
|
141
|
+
"profiles.read": {
|
|
142
|
+
surface: "core.profiles",
|
|
143
|
+
kind: "property",
|
|
144
|
+
description: "Narrow read-only profile lookup (list / get).",
|
|
145
|
+
},
|
|
146
|
+
"profiles.resolve": {
|
|
147
|
+
surface: "core.profileResolver",
|
|
148
|
+
kind: "property",
|
|
149
|
+
description: "Full ResolvedProfile lookup (credentials, env, setup_commands).",
|
|
150
|
+
},
|
|
151
|
+
"workspaces.read": {
|
|
152
|
+
surface: "core.workspaces",
|
|
153
|
+
kind: "method",
|
|
154
|
+
methods: ["get", "list"],
|
|
155
|
+
description: "Workspace get + list.",
|
|
156
|
+
},
|
|
157
|
+
"workspaces.write": {
|
|
158
|
+
surface: "core.workspaces",
|
|
159
|
+
kind: "method",
|
|
160
|
+
methods: ["update"],
|
|
161
|
+
description: "Workspace mutations (name, starred, archived, tags, model_override).",
|
|
162
|
+
},
|
|
163
|
+
"auth.gate": {
|
|
164
|
+
surface: "core.authHook",
|
|
165
|
+
kind: "property",
|
|
166
|
+
description: "Opt route scopes into the user-auth hook.",
|
|
167
|
+
},
|
|
168
|
+
"presence.register": {
|
|
169
|
+
surface: "core.sessionPresence",
|
|
170
|
+
kind: "property",
|
|
171
|
+
description: "Register a chat-surface presence provider.",
|
|
172
|
+
},
|
|
173
|
+
"tasks.submit": {
|
|
174
|
+
surface: "core.tasks",
|
|
175
|
+
kind: "property",
|
|
176
|
+
description: "Submit new tasks.",
|
|
177
|
+
},
|
|
178
|
+
"sessions.register": {
|
|
179
|
+
surface: "core.sessionLifecycle",
|
|
180
|
+
kind: "method",
|
|
181
|
+
methods: ["register"],
|
|
182
|
+
description: "Register a new session row.",
|
|
183
|
+
},
|
|
184
|
+
"sessions.extend": {
|
|
185
|
+
surface: "core.sessionLifecycle",
|
|
186
|
+
kind: "method",
|
|
187
|
+
methods: ["extendExpiry"],
|
|
188
|
+
description: "Push session expiry forward.",
|
|
189
|
+
},
|
|
190
|
+
"sessions.setStatus": {
|
|
191
|
+
surface: "core.sessionLifecycle",
|
|
192
|
+
kind: "method",
|
|
193
|
+
methods: ["setStatus"],
|
|
194
|
+
description: "Move sessions between status states.",
|
|
195
|
+
},
|
|
196
|
+
"sessions.getConnectedIds": {
|
|
197
|
+
surface: "core.sessionLifecycle",
|
|
198
|
+
kind: "method",
|
|
199
|
+
methods: ["getConnectedSessionIds"],
|
|
200
|
+
description: "Read the set of dashboard-connected session ids.",
|
|
201
|
+
},
|
|
202
|
+
"orchestrator.wake": {
|
|
203
|
+
surface: "core.orchestrator",
|
|
204
|
+
kind: "property",
|
|
205
|
+
description: "Wake a workspace container before submitting a task.",
|
|
206
|
+
},
|
|
207
|
+
"events.append": {
|
|
208
|
+
surface: "core.eventLog",
|
|
209
|
+
kind: "method",
|
|
210
|
+
methods: ["append"],
|
|
211
|
+
description: "Append entries to the dashboard timeline.",
|
|
212
|
+
},
|
|
213
|
+
"events.read": {
|
|
214
|
+
surface: "core.eventLog",
|
|
215
|
+
kind: "method",
|
|
216
|
+
methods: ["read"],
|
|
217
|
+
description: "Read entries from the dashboard timeline.",
|
|
218
|
+
},
|
|
219
|
+
"events.subscribe": {
|
|
220
|
+
surface: "ctx.sessionEvents",
|
|
221
|
+
kind: "property",
|
|
222
|
+
description: "Subscribe to orchestrator session events (task:token, task:done).",
|
|
223
|
+
},
|
|
224
|
+
"dashboard.push": {
|
|
225
|
+
surface: "core.connectionManager",
|
|
226
|
+
kind: "property",
|
|
227
|
+
description: "Push messages to dashboard WebSocket clients.",
|
|
228
|
+
},
|
|
229
|
+
"images.rewrite": {
|
|
230
|
+
surface: "core.imageRewriter",
|
|
231
|
+
kind: "property",
|
|
232
|
+
description: "Strip inline images from agent output.",
|
|
233
|
+
},
|
|
234
|
+
"models.list": {
|
|
235
|
+
surface: "core.modelList",
|
|
236
|
+
kind: "property",
|
|
237
|
+
description: "List models available to a profile.",
|
|
238
|
+
},
|
|
239
|
+
"notifications.channel": {
|
|
240
|
+
surface: "ctx.notificationBus",
|
|
241
|
+
kind: "property",
|
|
242
|
+
description: 'Claim a notification kind ("telegram", "slack", "email").',
|
|
243
|
+
},
|
|
244
|
+
"mcp.register": {
|
|
245
|
+
surface: "ctx.mcpRegistry",
|
|
246
|
+
kind: "property",
|
|
247
|
+
description: "Contribute an MCP server.",
|
|
248
|
+
},
|
|
249
|
+
"scheduler.run": {
|
|
250
|
+
surface: "ctx.scheduler",
|
|
251
|
+
kind: "property",
|
|
252
|
+
description: "Register cron + interval scheduled work.",
|
|
253
|
+
},
|
|
254
|
+
"http.outbound": {
|
|
255
|
+
surface: "ctx.http",
|
|
256
|
+
kind: "property",
|
|
257
|
+
description: "Use ctx.http.fetch. Requires manifest.outboundHosts populated.",
|
|
258
|
+
},
|
|
259
|
+
"secrets.mtls": {
|
|
260
|
+
surface: "ctx.secrets",
|
|
261
|
+
kind: "property",
|
|
262
|
+
description: "Use ctx.secrets.mtls(name) to resolve an operator-provisioned mTLS client cert/key " +
|
|
263
|
+
"(declared in manifest.mtlsSecrets, mapped to host files in policy) into an opaque ref " +
|
|
264
|
+
"for ctx.http.fetch({ mtls }). The plugin never reads the key bytes.",
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Capability pairs that are effectively root and are REFUSED for external
|
|
269
|
+
* plugins at load (§3 step 9, §5). Built-ins are exempt. Each inner array
|
|
270
|
+
* is an AND-set: an external plugin declaring all members is refused.
|
|
271
|
+
*
|
|
272
|
+
* `secrets.mtls` is deliberately NOT listed: the cert/key files are
|
|
273
|
+
* operator-provisioned (policy-declared host paths the plugin can't choose)
|
|
274
|
+
* and resolve to an OPAQUE ref the plugin can't read, so it can't exfiltrate
|
|
275
|
+
* the key even alongside `integrations.read.decrypted` — the design-time
|
|
276
|
+
* guarantee that motivates the combos above doesn't apply here.
|
|
277
|
+
*/
|
|
278
|
+
export const ROOT_EQUIVALENT_COMBINATIONS = [
|
|
279
|
+
["integrations.read.decrypted", "db.scoped"],
|
|
280
|
+
["integrations.read.decrypted", "db.access"],
|
|
281
|
+
];
|
|
282
|
+
/** Capabilities only built-in plugins may declare (§5). */
|
|
283
|
+
export const BUILTIN_ONLY_CAPABILITIES = new Set([
|
|
284
|
+
"db.access",
|
|
285
|
+
]);
|
|
286
|
+
//# sourceMappingURL=capabilities.js.map
|
package/errors.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refusal reason codes emitted in the audit "plugin refused" event (§11).
|
|
3
|
+
* Kept as a runtime tuple so the audit logger + tests can enumerate them.
|
|
4
|
+
*/
|
|
5
|
+
export declare const REFUSAL_REASONS: readonly ["manifest_invalid", "api_version_incompatible", "external_db_access", "external_db_scoped_not_opted_in", "external_root_combination", "policy_missing", "policy_hash_mismatch", "policy_capability_drift", "policy_outbound_host_drift", "policy_mtls_secret_drift", "policy_version_mismatch", "unapproved_frontend", "mtls_secret_unreadable", "schema_prefix_invalid", "frontend_path_escape", "backend_path_escape", "package_unresolvable", "absolute_route_denied"];
|
|
6
|
+
export type RefusalReason = (typeof REFUSAL_REASONS)[number];
|
|
7
|
+
/**
|
|
8
|
+
* Thrown by the membrane's `get` trap when a plugin accesses a `core`
|
|
9
|
+
* surface (or a non-core ctx surface) it did not declare + get granted.
|
|
10
|
+
* Throwing at the violation site (rather than returning undefined) names
|
|
11
|
+
* the underlying bug instead of surfacing a confusing downstream TypeError.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CapabilityViolationError extends Error {
|
|
14
|
+
readonly plugin: string;
|
|
15
|
+
readonly capability: string | null;
|
|
16
|
+
readonly key: string;
|
|
17
|
+
constructor(args: {
|
|
18
|
+
plugin: string;
|
|
19
|
+
capability: string | null;
|
|
20
|
+
key: string;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by `ctx.http.fetch` / webSocket / eventSource when the target
|
|
25
|
+
* hostname is not in the manifest∩policy outbound allowlist (§10).
|
|
26
|
+
*/
|
|
27
|
+
export declare class OutboundHostViolationError extends Error {
|
|
28
|
+
readonly plugin: string;
|
|
29
|
+
readonly host: string;
|
|
30
|
+
constructor(args: {
|
|
31
|
+
plugin: string;
|
|
32
|
+
host: string;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Thrown by the scoped Drizzle wrapper when a `db.scoped` plugin references
|
|
37
|
+
* a table outside its schema prefix, or attempts raw SQL (§9).
|
|
38
|
+
*/
|
|
39
|
+
export declare class DbScopeViolationError extends Error {
|
|
40
|
+
readonly plugin: string;
|
|
41
|
+
readonly schemaPrefix: string;
|
|
42
|
+
readonly offending: string;
|
|
43
|
+
constructor(args: {
|
|
44
|
+
plugin: string;
|
|
45
|
+
schemaPrefix: string;
|
|
46
|
+
offending: string;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by the loader when a plugin is refused at load. Carries the §11
|
|
51
|
+
* reason code + an operator-facing remediation hint where applicable. The
|
|
52
|
+
* loader catches this per-plugin, emits the refusal audit event, and skips
|
|
53
|
+
* the plugin (boot continues).
|
|
54
|
+
*/
|
|
55
|
+
export declare class PluginRefusedError extends Error {
|
|
56
|
+
readonly plugin: string;
|
|
57
|
+
readonly reason: RefusalReason;
|
|
58
|
+
readonly remediation?: string;
|
|
59
|
+
readonly detail?: Record<string, unknown>;
|
|
60
|
+
constructor(args: {
|
|
61
|
+
plugin: string;
|
|
62
|
+
reason: RefusalReason;
|
|
63
|
+
message: string;
|
|
64
|
+
remediation?: string;
|
|
65
|
+
detail?: Record<string, unknown>;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Thrown by the policy validator when the operator policy file is malformed. */
|
|
69
|
+
export declare class PolicyViolationError extends Error {
|
|
70
|
+
constructor(message: string);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/errors.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Error types thrown by the loader + the runtime membrane/wrappers. Exported
|
|
2
|
+
// from @vonzio/plugin-api so plugin authors and the attack corpus can assert
|
|
3
|
+
// on them by class. See docs/PLUGIN_LOADER_SPEC.md §7, §9, §10, §11.
|
|
4
|
+
/**
|
|
5
|
+
* Refusal reason codes emitted in the audit "plugin refused" event (§11).
|
|
6
|
+
* Kept as a runtime tuple so the audit logger + tests can enumerate them.
|
|
7
|
+
*/
|
|
8
|
+
export const REFUSAL_REASONS = [
|
|
9
|
+
"manifest_invalid",
|
|
10
|
+
"api_version_incompatible",
|
|
11
|
+
"external_db_access",
|
|
12
|
+
"external_db_scoped_not_opted_in",
|
|
13
|
+
"external_root_combination",
|
|
14
|
+
"policy_missing",
|
|
15
|
+
"policy_hash_mismatch",
|
|
16
|
+
"policy_capability_drift",
|
|
17
|
+
"policy_outbound_host_drift",
|
|
18
|
+
"policy_mtls_secret_drift",
|
|
19
|
+
"policy_version_mismatch",
|
|
20
|
+
"unapproved_frontend",
|
|
21
|
+
"mtls_secret_unreadable",
|
|
22
|
+
"schema_prefix_invalid",
|
|
23
|
+
"frontend_path_escape",
|
|
24
|
+
"backend_path_escape",
|
|
25
|
+
// Loader-internal refusals not in the §11 enumeration but needed in
|
|
26
|
+
// practice (malformed env entry, unresolvable package, etc.).
|
|
27
|
+
"package_unresolvable",
|
|
28
|
+
"absolute_route_denied",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Thrown by the membrane's `get` trap when a plugin accesses a `core`
|
|
32
|
+
* surface (or a non-core ctx surface) it did not declare + get granted.
|
|
33
|
+
* Throwing at the violation site (rather than returning undefined) names
|
|
34
|
+
* the underlying bug instead of surfacing a confusing downstream TypeError.
|
|
35
|
+
*/
|
|
36
|
+
export class CapabilityViolationError extends Error {
|
|
37
|
+
plugin;
|
|
38
|
+
capability;
|
|
39
|
+
key;
|
|
40
|
+
constructor(args) {
|
|
41
|
+
super(`Plugin "${args.plugin}" accessed "${args.key}" without a granted capability` +
|
|
42
|
+
(args.capability ? ` (needs ${args.capability})` : ""));
|
|
43
|
+
this.name = "CapabilityViolationError";
|
|
44
|
+
this.plugin = args.plugin;
|
|
45
|
+
this.capability = args.capability;
|
|
46
|
+
this.key = args.key;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by `ctx.http.fetch` / webSocket / eventSource when the target
|
|
51
|
+
* hostname is not in the manifest∩policy outbound allowlist (§10).
|
|
52
|
+
*/
|
|
53
|
+
export class OutboundHostViolationError extends Error {
|
|
54
|
+
plugin;
|
|
55
|
+
host;
|
|
56
|
+
constructor(args) {
|
|
57
|
+
super(`Plugin "${args.plugin}" attempted outbound request to disallowed host "${args.host}"`);
|
|
58
|
+
this.name = "OutboundHostViolationError";
|
|
59
|
+
this.plugin = args.plugin;
|
|
60
|
+
this.host = args.host;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Thrown by the scoped Drizzle wrapper when a `db.scoped` plugin references
|
|
65
|
+
* a table outside its schema prefix, or attempts raw SQL (§9).
|
|
66
|
+
*/
|
|
67
|
+
export class DbScopeViolationError extends Error {
|
|
68
|
+
plugin;
|
|
69
|
+
schemaPrefix;
|
|
70
|
+
offending;
|
|
71
|
+
constructor(args) {
|
|
72
|
+
super(`Plugin "${args.plugin}" (schema "${args.schemaPrefix}") attempted to access "${args.offending}" outside its scope`);
|
|
73
|
+
this.name = "DbScopeViolationError";
|
|
74
|
+
this.plugin = args.plugin;
|
|
75
|
+
this.schemaPrefix = args.schemaPrefix;
|
|
76
|
+
this.offending = args.offending;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Thrown by the loader when a plugin is refused at load. Carries the §11
|
|
81
|
+
* reason code + an operator-facing remediation hint where applicable. The
|
|
82
|
+
* loader catches this per-plugin, emits the refusal audit event, and skips
|
|
83
|
+
* the plugin (boot continues).
|
|
84
|
+
*/
|
|
85
|
+
export class PluginRefusedError extends Error {
|
|
86
|
+
plugin;
|
|
87
|
+
reason;
|
|
88
|
+
remediation;
|
|
89
|
+
detail;
|
|
90
|
+
constructor(args) {
|
|
91
|
+
super(args.message);
|
|
92
|
+
this.name = "PluginRefusedError";
|
|
93
|
+
this.plugin = args.plugin;
|
|
94
|
+
this.reason = args.reason;
|
|
95
|
+
this.remediation = args.remediation;
|
|
96
|
+
this.detail = args.detail;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Thrown by the policy validator when the operator policy file is malformed. */
|
|
100
|
+
export class PolicyViolationError extends Error {
|
|
101
|
+
constructor(message) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "PolicyViolationError";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=errors.js.map
|
package/frontend.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape a plugin's frontend entry point must default-export from
|
|
3
|
+
* its `/frontend` module. Called once during dashboard boot. The
|
|
4
|
+
* plugin imports the dashboard registry directly (e.g.
|
|
5
|
+
* `import { registerSettingsSection } from "@vonzio/dashboard-registry/api"`)
|
|
6
|
+
* and calls whichever register* methods it needs.
|
|
7
|
+
*
|
|
8
|
+
* Errors thrown here are caught by the dashboard's plugin loader and
|
|
9
|
+
* surface as a console warning -- the rest of the dashboard keeps
|
|
10
|
+
* rendering.
|
|
11
|
+
*/
|
|
12
|
+
export type PluginFrontendEntry = () => void;
|
|
13
|
+
//# sourceMappingURL=frontend.d.ts.map
|
package/frontend.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Frontend half of the plugin contract. v0.1 is intentionally minimal:
|
|
2
|
+
// a plugin's frontend entry is just a default-exported function called
|
|
3
|
+
// once at dashboard boot. The plugin registers whatever UI it needs by
|
|
4
|
+
// importing from `@vonzio/dashboard-registry/api` directly -- that registry
|
|
5
|
+
// already supports settings sections, nav items, topbar slots, composer
|
|
6
|
+
// slots, workspace header slots, onboarding steps, and routes, so
|
|
7
|
+
// plugins don't need a separate slot taxonomy.
|
|
8
|
+
//
|
|
9
|
+
// This file deliberately defines no slot enum and no DashboardSlots
|
|
10
|
+
// map: those would either duplicate the dashboard registry's types or
|
|
11
|
+
// drift from them. The dashboard registry IS the truth.
|
|
12
|
+
//
|
|
13
|
+
// Type-only react import keeps backend-only plugins from acquiring a
|
|
14
|
+
// runtime react dep.
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=frontend.js.map
|