@cogenta/plugins 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.
Files changed (86) hide show
  1. package/dist/guest/sandbox-entry.mjs +155 -0
  2. package/dist/host/capabilities.d.ts +46 -0
  3. package/dist/host/capabilities.d.ts.map +1 -0
  4. package/dist/host/capabilities.js +105 -0
  5. package/dist/host/capabilities.js.map +1 -0
  6. package/dist/host/protocol.d.ts +58 -0
  7. package/dist/host/protocol.d.ts.map +1 -0
  8. package/dist/host/protocol.js +10 -0
  9. package/dist/host/protocol.js.map +1 -0
  10. package/dist/host/worker-runner.d.ts +92 -0
  11. package/dist/host/worker-runner.d.ts.map +1 -0
  12. package/dist/host/worker-runner.js +196 -0
  13. package/dist/host/worker-runner.js.map +1 -0
  14. package/dist/index.d.ts +34 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +20 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/loader.d.ts +81 -0
  19. package/dist/loader.d.ts.map +1 -0
  20. package/dist/loader.js +190 -0
  21. package/dist/loader.js.map +1 -0
  22. package/dist/manifest.d.ts +93 -0
  23. package/dist/manifest.d.ts.map +1 -0
  24. package/dist/manifest.js +197 -0
  25. package/dist/manifest.js.map +1 -0
  26. package/dist/permissions/describe.d.ts +22 -0
  27. package/dist/permissions/describe.d.ts.map +1 -0
  28. package/dist/permissions/describe.js +158 -0
  29. package/dist/permissions/describe.js.map +1 -0
  30. package/dist/permissions/disabled.d.ts +25 -0
  31. package/dist/permissions/disabled.d.ts.map +1 -0
  32. package/dist/permissions/disabled.js +29 -0
  33. package/dist/permissions/disabled.js.map +1 -0
  34. package/dist/permissions/grants.d.ts +22 -0
  35. package/dist/permissions/grants.d.ts.map +1 -0
  36. package/dist/permissions/grants.js +31 -0
  37. package/dist/permissions/grants.js.map +1 -0
  38. package/dist/permissions/resolve.d.ts +47 -0
  39. package/dist/permissions/resolve.d.ts.map +1 -0
  40. package/dist/permissions/resolve.js +51 -0
  41. package/dist/permissions/resolve.js.map +1 -0
  42. package/dist/permissions/review.d.ts +50 -0
  43. package/dist/permissions/review.d.ts.map +1 -0
  44. package/dist/permissions/review.js +46 -0
  45. package/dist/permissions/review.js.map +1 -0
  46. package/dist/permissions/tables.d.ts +18 -0
  47. package/dist/permissions/tables.d.ts.map +1 -0
  48. package/dist/permissions/tables.js +48 -0
  49. package/dist/permissions/tables.js.map +1 -0
  50. package/dist/registries/plugins.d.ts +74 -0
  51. package/dist/registries/plugins.d.ts.map +1 -0
  52. package/dist/registries/plugins.js +127 -0
  53. package/dist/registries/plugins.js.map +1 -0
  54. package/dist/registries/skills.d.ts +54 -0
  55. package/dist/registries/skills.d.ts.map +1 -0
  56. package/dist/registries/skills.js +113 -0
  57. package/dist/registries/skills.js.map +1 -0
  58. package/dist/registries/skins.d.ts +33 -0
  59. package/dist/registries/skins.d.ts.map +1 -0
  60. package/dist/registries/skins.js +65 -0
  61. package/dist/registries/skins.js.map +1 -0
  62. package/dist/registries/tables.d.ts +44 -0
  63. package/dist/registries/tables.d.ts.map +1 -0
  64. package/dist/registries/tables.js +118 -0
  65. package/dist/registries/tables.js.map +1 -0
  66. package/dist/registries/themes.d.ts +59 -0
  67. package/dist/registries/themes.d.ts.map +1 -0
  68. package/dist/registries/themes.js +82 -0
  69. package/dist/registries/themes.js.map +1 -0
  70. package/dist/semver.d.ts +24 -0
  71. package/dist/semver.d.ts.map +1 -0
  72. package/dist/semver.js +93 -0
  73. package/dist/semver.js.map +1 -0
  74. package/dist/signing/keys.d.ts +18 -0
  75. package/dist/signing/keys.d.ts.map +1 -0
  76. package/dist/signing/keys.js +15 -0
  77. package/dist/signing/keys.js.map +1 -0
  78. package/dist/signing/sign.d.ts +35 -0
  79. package/dist/signing/sign.d.ts.map +1 -0
  80. package/dist/signing/sign.js +63 -0
  81. package/dist/signing/sign.js.map +1 -0
  82. package/dist/signing/verify.d.ts +40 -0
  83. package/dist/signing/verify.d.ts.map +1 -0
  84. package/dist/signing/verify.js +73 -0
  85. package/dist/signing/verify.js.map +1 -0
  86. package/package.json +46 -0
@@ -0,0 +1,155 @@
1
+ // Hand-written plain JS, not compiled from TypeScript — this file is loaded
2
+ // directly by `node:worker_threads`' `Worker` constructor, which reads it as
3
+ // real Node-executable JS with no build step involved (the same reasoning
4
+ // `create-cogenta`'s scaffolded `cogenta.config.mjs` is hand-written rather
5
+ // than routed through a TS-to-JS build: this file must run as-is, before any
6
+ // bundler or `tsc` output exists during `vitest` runs against `src/`).
7
+ //
8
+ // This is the ENTIRE trusted surface a plugin's code runs against. It does
9
+ // three things, and nothing else:
10
+ // 1. Creates a fresh `vm` context whose global object has no `process`, no
11
+ // `require`, no `fs`/`net`, and no dynamic `import()` — a plugin cannot
12
+ // reach any of these because they were never put there, not because
13
+ // they were removed after being present.
14
+ // 2. Runs the plugin's code as a classic (non-module) `vm.Script` inside
15
+ // that context, with `codeGeneration: { strings: false }` — this is a
16
+ // real, documented V8/Node guarantee that blocks `eval()`/`new
17
+ // Function(...)`, closing the most common "climb the prototype chain to
18
+ // reach the real Function constructor" vm-escape technique.
19
+ // 3. Reports the outcome back over `parentPort`, JSON-round-tripped so no
20
+ // live object (a function, a class instance, a Proxy) can leak out.
21
+ //
22
+ // Honest limitation, stated plainly rather than oversold: Node's own docs
23
+ // are explicit that `vm` is not a hardened security boundary against a
24
+ // sufficiently determined co-located attacker on its own — it is one layer.
25
+ // The real isolation here is the COMBINATION of this vm sandbox (blocks the
26
+ // Node API surface) running inside a separate `worker_threads` thread
27
+ // spawned with `env: {}` (blocks secrets/environment access structurally,
28
+ // not by convention) and bounded `resourceLimits` + a host-side timeout
29
+ // (blocks resource exhaustion). No single layer is claimed to be sufficient
30
+ // alone.
31
+
32
+ import vm from 'node:vm'
33
+ import { parentPort } from 'node:worker_threads'
34
+
35
+ if (!parentPort) {
36
+ throw new Error('sandbox-entry.mjs must run inside a worker_threads Worker')
37
+ }
38
+
39
+ /** JSON round-trip strips functions/symbols/live references — nothing but plain data survives. */
40
+ function toSerializable(value) {
41
+ if (value === undefined) return null
42
+ try {
43
+ return JSON.parse(JSON.stringify(value))
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ let nextCallId = 1
50
+ /** Pending SDK calls this sandbox is waiting on a host reply for, keyed by `callId`. */
51
+ const pendingSdkCalls = new Map()
52
+
53
+ /**
54
+ * One real RPC method, bound to a specific capability name. Calling it posts
55
+ * a `sdk-call` message to the host and returns a Promise that resolves or
56
+ * rejects when the matching `sdk-result`/`sdk-error` arrives — the host
57
+ * re-verifies the request against what was actually granted before ever
58
+ * executing it (`./host/capabilities.js`).
59
+ */
60
+ function makeSdkMethod(method) {
61
+ return (args) =>
62
+ new Promise((resolve, reject) => {
63
+ const callId = nextCallId
64
+ nextCallId += 1
65
+ pendingSdkCalls.set(callId, { resolve, reject })
66
+ parentPort.postMessage({ type: 'sdk-call', callId, method, args })
67
+ })
68
+ }
69
+
70
+ /**
71
+ * Builds the plugin-visible `sdk` object from exactly the capability strings
72
+ * the host says were granted — task 4/5's central, testable property:
73
+ * "toute méthode non accordée est absente de l'objet, pas présente et
74
+ * refusée : absente." A namespace or method whose capability was never
75
+ * granted is never assigned onto `sdk` at all, so `'read' in sdk.content`
76
+ * is `false`, not a present method that throws when called.
77
+ */
78
+ function buildSdk(grantedCapabilities) {
79
+ const sdk = {}
80
+ for (const capability of grantedCapabilities) {
81
+ const separatorIndex = capability.indexOf(':')
82
+ const name = separatorIndex === -1 ? capability : capability.slice(0, separatorIndex)
83
+ const dotIndex = name.indexOf('.')
84
+ if (dotIndex === -1) continue
85
+ const namespace = name.slice(0, dotIndex)
86
+ const method = name.slice(dotIndex + 1)
87
+ if (sdk[namespace] === undefined) sdk[namespace] = {}
88
+ if (sdk[namespace][method] === undefined) sdk[namespace][method] = makeSdkMethod(name)
89
+ }
90
+ return sdk
91
+ }
92
+
93
+ function buildSandbox(grantedCapabilities) {
94
+ // Deliberately minimal. No `process`, no `require`, no `fetch`, no
95
+ // `Buffer`, no `fs`/`net` — only `sdk`, and only the namespaces/methods
96
+ // `grantedCapabilities` actually names.
97
+ return {
98
+ console,
99
+ Math,
100
+ JSON,
101
+ Promise,
102
+ setTimeout,
103
+ clearTimeout,
104
+ sdk: buildSdk(grantedCapabilities),
105
+ }
106
+ }
107
+
108
+ parentPort.on('message', (message) => {
109
+ if (message == null) return
110
+
111
+ // A reply to a pending SDK call — resolve/reject its Promise and let the
112
+ // sandbox's own `await` resume; this is not a new `run` request.
113
+ if (message.type === 'sdk-result' || message.type === 'sdk-error') {
114
+ const pending = pendingSdkCalls.get(message.callId)
115
+ if (pending === undefined) return
116
+ pendingSdkCalls.delete(message.callId)
117
+ if (message.type === 'sdk-result') pending.resolve(message.value)
118
+ else pending.reject(new Error(message.message))
119
+ return
120
+ }
121
+
122
+ if (message.type !== 'run') return
123
+ const { id, code, grantedCapabilities } = message
124
+
125
+ // Fire-and-report, not awaited by the message handler itself: plugin code
126
+ // may be a top-level `async () => {...}()` (e.g. to `await import(...)`
127
+ // and observe the rejection a blocked dynamic import produces) — its
128
+ // returned Promise is awaited here before the result is serialized.
129
+ void (async () => {
130
+ let result
131
+ try {
132
+ const context = vm.createContext(buildSandbox(grantedCapabilities ?? []), {
133
+ codeGeneration: { strings: false, wasm: false },
134
+ })
135
+ const script = new vm.Script(code, { filename: 'plugin.js' })
136
+ // A second, independent time bound on top of the host's own
137
+ // worker.terminate() timeout — this one stops a synchronous infinite
138
+ // loop from inside the same thread, which a host-side terminate() can
139
+ // be slow to land on under heavy CPU contention.
140
+ const rawValue = script.runInContext(context, { timeout: 5000 })
141
+ const value =
142
+ rawValue !== null && typeof rawValue === 'object' && typeof rawValue.then === 'function'
143
+ ? await rawValue
144
+ : rawValue
145
+ result = { id, type: 'result', value: toSerializable(value) }
146
+ } catch (error) {
147
+ result = {
148
+ id,
149
+ type: 'error',
150
+ message: error instanceof Error ? error.message : String(error),
151
+ }
152
+ }
153
+ parentPort.postMessage(result)
154
+ })()
155
+ })
@@ -0,0 +1,46 @@
1
+ import type { StorageDriver } from '@cogenta/core';
2
+ /**
3
+ * Host-side implementations of the SDK methods a sandboxed plugin can call.
4
+ * "Commencer minimal" (docs/lots/L7-extensibilite.md § Pièges connus) — this
5
+ * is a deliberately small starter set: `content.read`, `http.fetch`,
6
+ * `storage.read`/`storage.write`. Task 5 owns turning a real permission grant
7
+ * into the `grantedCapabilities` list these handlers re-verify against; this
8
+ * file owns what each capability actually DOES once granted.
9
+ *
10
+ * Every handler re-checks the SPECIFIC request against the SPECIFIC granted
11
+ * capability parameter (the exact domain, the exact storage prefix) — never
12
+ * just "was this capability name granted at all." The manifest schema
13
+ * (task 1) already validates that a declared capability's parameter is
14
+ * well-formed (a real hostname, a prefix confined to the plugin's own
15
+ * namespace), but a well-formed *declaration* says nothing about what the
16
+ * *specific call a plugin makes at runtime* asks for — a plugin granted
17
+ * `http.fetch:api.example.com` could still ask its own SDK method to fetch
18
+ * `evil.example.com`, and only a host-side re-check of the actual request
19
+ * against the actual granted parameter closes that gap.
20
+ */
21
+ export interface CapabilityCallContext {
22
+ /** The full manifest-shaped capability strings actually granted, e.g. `["http.fetch:api.example.com"]`. */
23
+ readonly grantedCapabilities: readonly string[];
24
+ }
25
+ export type CapabilityHandler = (args: unknown, context: CapabilityCallContext) => Promise<unknown>;
26
+ /**
27
+ * `content.read` — read-only, via whatever real content-read function the
28
+ * host provides (typically `ContentStore.read`, `@cogenta/schema`) for a
29
+ * single, specific collection this plugin instance is scoped to. No
30
+ * per-call re-verification beyond capability presence: `content.read` is
31
+ * bare (task 1 never parameterizes it per-collection), so the host, not
32
+ * this handler, is what decides which collection's store gets wired in.
33
+ */
34
+ export declare function createContentReadHandler(readEntry: (id: string) => Promise<unknown>): CapabilityHandler;
35
+ /**
36
+ * `http.fetch:<domain>` — the requested URL's hostname must exactly match
37
+ * one of the domains actually granted, re-checked here against the real
38
+ * request, not merely against the fact that *some* `http.fetch` capability
39
+ * exists.
40
+ */
41
+ export declare function createHttpFetchHandler(fetchImpl?: typeof fetch): CapabilityHandler;
42
+ /** `storage.read:<prefix>` — the requested key must fall within a granted prefix, re-verified per call. */
43
+ export declare function createStorageReadHandler(driver: StorageDriver): CapabilityHandler;
44
+ /** `storage.write:<prefix>` — same per-call prefix re-verification as `storage.read`. */
45
+ export declare function createStorageWriteHandler(driver: StorageDriver): CapabilityHandler;
46
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/host/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAGlD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,qBAAqB;IACpC,2GAA2G;IAC3G,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAA;CAChD;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AA6BnG;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAC1C,iBAAiB,CAQnB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,GAAE,OAAO,KAAa,GAAG,iBAAiB,CAYzF;AAgBD,2GAA2G;AAC3G,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CAejF;AAED,yFAAyF;AACzF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CAYlF"}
@@ -0,0 +1,105 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ /** Parameters (the part after `:`) granted for a given bare capability name. */
3
+ function grantedParameters(name, context) {
4
+ const prefix = `${name}:`;
5
+ return context.grantedCapabilities
6
+ .filter((capability) => capability.startsWith(prefix))
7
+ .map((capability) => capability.slice(prefix.length));
8
+ }
9
+ function refused(message) {
10
+ throw new CogentaError({
11
+ code: 'PLUGIN_CAPABILITY_REFUSED',
12
+ message,
13
+ hint: 'The plugin requested something its granted capabilities do not cover.',
14
+ });
15
+ }
16
+ function asRecord(value) {
17
+ if (value === null || typeof value !== 'object')
18
+ refused('expected an object argument');
19
+ return value;
20
+ }
21
+ function asString(value, field) {
22
+ if (typeof value !== 'string' || value.trim() === '')
23
+ refused(`"${field}" must be a non-empty string`);
24
+ return value;
25
+ }
26
+ /**
27
+ * `content.read` — read-only, via whatever real content-read function the
28
+ * host provides (typically `ContentStore.read`, `@cogenta/schema`) for a
29
+ * single, specific collection this plugin instance is scoped to. No
30
+ * per-call re-verification beyond capability presence: `content.read` is
31
+ * bare (task 1 never parameterizes it per-collection), so the host, not
32
+ * this handler, is what decides which collection's store gets wired in.
33
+ */
34
+ export function createContentReadHandler(readEntry) {
35
+ return async (args, context) => {
36
+ if (!context.grantedCapabilities.includes('content.read')) {
37
+ refused('"content.read" was not granted');
38
+ }
39
+ const id = asString(asRecord(args).id, 'id');
40
+ return await readEntry(id);
41
+ };
42
+ }
43
+ /**
44
+ * `http.fetch:<domain>` — the requested URL's hostname must exactly match
45
+ * one of the domains actually granted, re-checked here against the real
46
+ * request, not merely against the fact that *some* `http.fetch` capability
47
+ * exists.
48
+ */
49
+ export function createHttpFetchHandler(fetchImpl = fetch) {
50
+ return async (args, context) => {
51
+ const url = asString(asRecord(args).url, 'url');
52
+ const allowedDomains = grantedParameters('http.fetch', context);
53
+ const hostname = new URL(url).hostname;
54
+ if (!allowedDomains.includes(hostname)) {
55
+ refused(`"http.fetch" was not granted for domain "${hostname}"`);
56
+ }
57
+ const response = await fetchImpl(url);
58
+ const body = await response.text();
59
+ return { status: response.status, body };
60
+ };
61
+ }
62
+ function isWithinGrantedPrefix(path, prefixes) {
63
+ return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
64
+ }
65
+ /** Refuses any request whose normalised path escapes what the granted prefix set allows — closes `../` traversal. */
66
+ function normalisedStorageKey(rawKey) {
67
+ const key = asString(rawKey, 'key');
68
+ const segments = key.split('/');
69
+ if (segments.some((segment) => segment === '..' || segment === '.')) {
70
+ refused(`storage key "${key}" must not contain "." or ".." segments`);
71
+ }
72
+ return key;
73
+ }
74
+ /** `storage.read:<prefix>` — the requested key must fall within a granted prefix, re-verified per call. */
75
+ export function createStorageReadHandler(driver) {
76
+ return async (args, context) => {
77
+ const record = asRecord(args);
78
+ const key = normalisedStorageKey(asString(record.key, 'key'));
79
+ const allowedPrefixes = grantedParameters('storage.read', context);
80
+ if (!isWithinGrantedPrefix(key, allowedPrefixes)) {
81
+ refused(`"storage.read" was not granted for key "${key}"`);
82
+ }
83
+ const readable = await driver.get(key);
84
+ const chunks = [];
85
+ for await (const chunk of readable) {
86
+ chunks.push(chunk);
87
+ }
88
+ return Buffer.concat(chunks).toString('utf8');
89
+ };
90
+ }
91
+ /** `storage.write:<prefix>` — same per-call prefix re-verification as `storage.read`. */
92
+ export function createStorageWriteHandler(driver) {
93
+ return async (args, context) => {
94
+ const record = asRecord(args);
95
+ const key = normalisedStorageKey(asString(record.key, 'key'));
96
+ const content = asString(record.content, 'content');
97
+ const allowedPrefixes = grantedParameters('storage.write', context);
98
+ if (!isWithinGrantedPrefix(key, allowedPrefixes)) {
99
+ refused(`"storage.write" was not granted for key "${key}"`);
100
+ }
101
+ await driver.put(key, Buffer.from(content, 'utf8'));
102
+ return { ok: true };
103
+ };
104
+ }
105
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/host/capabilities.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The real, serialized message protocol between the host and an isolated
3
+ * plugin worker. Every field here must survive `worker_threads`' structured
4
+ * clone — no live objects, no functions, no class instances ever cross this
5
+ * boundary ("communication par messages sérialisés. Aucun objet vivant du
6
+ * noyau ne traverse la frontière" — docs/lots/L7-extensibilite.md §
7
+ * Isolation).
8
+ */
9
+ /**
10
+ * Host → worker: evaluate `code` inside the sandbox and report the result.
11
+ * `grantedCapabilities` (task 1's manifest capability strings, e.g.
12
+ * `"http.fetch:api.example.com"`) is what the guest-side sandbox uses to
13
+ * decide which SDK methods to construct — task 4's "absent, not refused"
14
+ * property is enforced entirely by which keys this list causes to exist on
15
+ * the sandbox's `sdk` object, never by a present-but-guarded method.
16
+ */
17
+ export interface WorkerRunMessage {
18
+ readonly id: number;
19
+ readonly type: 'run';
20
+ readonly code: string;
21
+ readonly grantedCapabilities: readonly string[];
22
+ }
23
+ /** Worker → host: the plugin's sandboxed code is calling an SDK method — a real capability request, not yet executed. */
24
+ export interface WorkerSdkCallMessage {
25
+ readonly type: 'sdk-call';
26
+ readonly callId: number;
27
+ /** The capability name the requested method belongs to, e.g. `"content.read"`, `"http.fetch"`. */
28
+ readonly method: string;
29
+ readonly args: unknown;
30
+ }
31
+ export type WorkerHostMessage = WorkerRunMessage;
32
+ /** Host → worker: the SDK call succeeded — a JSON-safe value only, same "no live object crosses the boundary" rule as every other message here. */
33
+ export interface WorkerSdkResultMessage {
34
+ readonly type: 'sdk-result';
35
+ readonly callId: number;
36
+ readonly value: unknown;
37
+ }
38
+ /** Host → worker: the SDK call was refused or failed — re-verified host-side, never trusted from the manifest alone. */
39
+ export interface WorkerSdkErrorMessage {
40
+ readonly type: 'sdk-error';
41
+ readonly callId: number;
42
+ readonly message: string;
43
+ }
44
+ export type WorkerHostReplyMessage = WorkerSdkResultMessage | WorkerSdkErrorMessage;
45
+ /** Worker → host: `code` completed and returned a JSON-safe value. */
46
+ export interface WorkerResultMessage {
47
+ readonly id: number;
48
+ readonly type: 'result';
49
+ readonly value: unknown;
50
+ }
51
+ /** Worker → host: `code` threw, or the sandbox itself refused to run it. */
52
+ export interface WorkerErrorMessage {
53
+ readonly id: number;
54
+ readonly type: 'error';
55
+ readonly message: string;
56
+ }
57
+ export type WorkerGuestMessage = WorkerResultMessage | WorkerErrorMessage | WorkerSdkCallMessage;
58
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/host/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAA;CAChD;AAED,yHAAyH;AACzH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,kGAAkG;IAClG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,CAAA;AAEhD,mJAAmJ;AACnJ,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB;AAED,wHAAwH;AACxH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,MAAM,sBAAsB,GAAG,sBAAsB,GAAG,qBAAqB,CAAA;AAEnF,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The real, serialized message protocol between the host and an isolated
3
+ * plugin worker. Every field here must survive `worker_threads`' structured
4
+ * clone — no live objects, no functions, no class instances ever cross this
5
+ * boundary ("communication par messages sérialisés. Aucun objet vivant du
6
+ * noyau ne traverse la frontière" — docs/lots/L7-extensibilite.md §
7
+ * Isolation).
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/host/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,92 @@
1
+ import type { PluginManifest } from '../manifest.js';
2
+ import type { PluginDisableStore, PluginViolationReason } from '../permissions/disabled.js';
3
+ import type { PluginGrant } from '../permissions/grants.js';
4
+ import type { CapabilityHandler } from './capabilities.js';
5
+ export interface RunIsolatedOptions {
6
+ /** Killed and reported as a timeout past this — the basic kill switch this task's tests need. */
7
+ readonly timeoutMs?: number;
8
+ /** Real V8 heap ceiling for the worker's old-generation heap. */
9
+ readonly maxOldGenerationSizeMb?: number;
10
+ /**
11
+ * Task 1's manifest capability strings actually granted to this run, e.g.
12
+ * `["content.read", "http.fetch:api.example.com"]`. The guest-side sandbox
13
+ * uses this to decide which SDK methods to construct at all — an absent
14
+ * capability means an absent key on `sdk`, never a present-but-refusing
15
+ * method (docs/lots/L7-extensibilite.md § Isolation, and the explicit
16
+ * acceptance criterion "une méthode non accordée est absente de l'objet
17
+ * SDK, pas seulement refusée").
18
+ */
19
+ readonly grantedCapabilities?: readonly string[];
20
+ /**
21
+ * Real host-side implementations for each granted capability's SDK
22
+ * method, keyed by capability name (`"content.read"`, `"http.fetch"`,
23
+ * `"storage.read"`, `"storage.write"`, ...). Task 4's real deliverable —
24
+ * see `./capabilities.js`. A capability granted but with no matching
25
+ * handler here is a host misconfiguration, not a security gap: the guest
26
+ * sandbox still won't expose a method for it unless BOTH `grantedCapabilities`
27
+ * names it AND a handler exists.
28
+ */
29
+ readonly handlers?: Readonly<Record<string, CapabilityHandler>>;
30
+ }
31
+ export interface IsolatedRunResult {
32
+ readonly ok: boolean;
33
+ readonly value?: unknown;
34
+ readonly error?: string;
35
+ /**
36
+ * Set only when `ok` is `false` — classifies WHY the worker failed, so a
37
+ * caller (task 6's disable policy) can tell "the plugin's own code threw a
38
+ * normal error" apart from "it exceeded its time or memory budget", which
39
+ * is the distinction "tué et désactivé" hinges on. `'crash'` covers any
40
+ * other non-zero exit that isn't recognizably a timeout or a heap-limit
41
+ * violation (e.g. a native worker fault) — still real grounds to disable,
42
+ * just not one of the two named-and-required policies.
43
+ */
44
+ readonly reason?: PluginViolationReason;
45
+ }
46
+ /**
47
+ * Runs `code` inside a fresh, isolated worker and reports the outcome.
48
+ * `code` is a classic (non-module) script body — the sandbox's real,
49
+ * documented restriction, not an accident: `vm.Script` without an
50
+ * `importModuleDynamically` callback refuses `import()`, and no `require`
51
+ * is ever injected into the sandbox, which is exactly what stops a plugin
52
+ * from reaching `node:fs`/`node:net` no matter which syntax it tries.
53
+ *
54
+ * A fresh `Worker` per call, not a pool — task 6 decides whether a
55
+ * long-lived, reusable worker (with the resource/time policy it owns) is
56
+ * worth the complexity; this task only needs a real, working isolation
57
+ * primitive to build on.
58
+ */
59
+ export declare function runIsolated(code: string, options?: RunIsolatedOptions): Promise<IsolatedRunResult>;
60
+ /** Emitted once, synchronously, the moment a plugin is disabled — never a hard dependency on any specific notification transport (channels, email, ...); wiring this to a real alert is an integration decision for whatever assembles a site, not this package's job. */
61
+ export interface PluginDisabledEvent {
62
+ readonly pluginName: string;
63
+ readonly reason: PluginViolationReason;
64
+ readonly at: string;
65
+ readonly details?: string;
66
+ }
67
+ export interface RunPluginOptions extends Omit<RunIsolatedOptions, 'grantedCapabilities'> {
68
+ /** Task 6's real, persisted "is this plugin currently disabled" gate — required, not optional, since a caller with no store to check against cannot honestly claim to enforce "tué et désactivé". */
69
+ readonly disableStore: PluginDisableStore;
70
+ /** Fired synchronously right after a violation is recorded — the real "avec alerte" half of "tué et désactivé, avec alerte". */
71
+ readonly onPluginDisabled?: (event: PluginDisabledEvent) => void;
72
+ }
73
+ /**
74
+ * The real entry point for actually running a resolved plugin — unlike
75
+ * `runIsolated`, which task 4 left accepting an externally-decided
76
+ * `grantedCapabilities` list as a placeholder, this computes that list
77
+ * itself via `resolveGrantedCapabilities` (task 5) from the plugin's real
78
+ * manifest and its real, persisted grants. A caller cannot bypass grant
79
+ * resolution by simply passing whatever capability list it wants — the only
80
+ * way a capability reaches the sandbox is a real row in the grant store.
81
+ *
82
+ * Task 6 adds the other two real gates the lot demands: a disabled plugin is
83
+ * refused BEFORE a worker is ever spawned (`PLUGIN_DISABLED`, not a silent
84
+ * no-op), and a timeout or memory-limit violation disables the plugin for
85
+ * every future run, not just the one that failed — "il ne peut pas faire
86
+ * tomber le CMS" is the isolation boundary's job (task 3); making sure it
87
+ * cannot even try again unsupervised is this task's.
88
+ */
89
+ export declare function runPlugin(manifest: PluginManifest, code: string, grants: readonly PluginGrant[], options: RunPluginOptions): Promise<IsolatedRunResult>;
90
+ /** Throws instead of returning a discriminated failure — for call sites that want a plain success value or a real error. */
91
+ export declare function runIsolatedOrThrow(code: string, options?: RunIsolatedOptions): Promise<unknown>;
92
+ //# sourceMappingURL=worker-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../src/host/worker-runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAC3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAyB1D,MAAM,WAAW,kBAAkB;IACjC,iGAAiG;IACjG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAA;IACxC;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAChD;;;;;;;;OAQG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAA;CAChE;AAKD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAA;CACxC;AAsDD;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAsE5B;AAED,0QAA0Q;AAC1Q,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAA;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,kBAAkB,EAAE,qBAAqB,CAAC;IACvF,qMAAqM;IACrM,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAA;IACzC,gIAAgI;IAChI,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CACjE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAyB5B;AAED,4HAA4H;AAC5H,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAWlB"}