@volter/world-core 3.0.64 → 3.0.66

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.
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+ // THE WORLD'S CAPABILITY DECISION (docs/contributing/architecture.md, "Capability decisions"): what a guest's request
3
+ // gets, PASS, TWIN or REFUSE, from what the guest declares and what the World grants. Every substrate (the injector, the
4
+ // scoped proxy, a tab's relay, a native image run's Seatbelt profile) enforces this answer and decides nothing itself.
5
+ // Pure: no I/O, no environment and nothing at import. What only the substrate can observe (the twin route, the vendors
6
+ // sharing a host, which loopback ports are the World's doors or the run's own listeners) arrives as `facts`.
7
+ const { allowsWorldNetworkEgress, isWorldInternalHost, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough } = require('./network-policy.cjs');
8
+
9
+ /** The declaration of a guest with no declaration format: an ordinary host process, which can reach what the host can.
10
+ * Its own loopback is what it observably opened (`facts.ownListener`). A substrate whose guest can declare never uses it. */
11
+ const OPEN_DECLARATION = Object.freeze({ network: Object.freeze({ egress: '*', services: '*', host: true }) });
12
+
13
+ /** What an unreadable declaration, or one bound to another World, declares: nothing. */
14
+ const EMPTY_DECLARATION = Object.freeze({ network: Object.freeze({}) });
15
+
16
+ /** Where an image run's processes carry the declaration of its runtime binding (architecture, "Capability decisions"). */
17
+ const WORLD_DECLARATION_ENV = 'VOLTER_WORLD_DECLARATION';
18
+
19
+ /** The declaration `env` carries for the attached World, undefined where it carries none, and the empty declaration
20
+ * where the value is unreadable or bound to another World. The caller passes the environment; nothing is read here. */
21
+ function worldDeclarationFromEnv(env) {
22
+ const serialized = env[WORLD_DECLARATION_ENV];
23
+ if (serialized === undefined) return undefined;
24
+ let value;
25
+ try { value = JSON.parse(String(serialized)); } catch { return EMPTY_DECLARATION; }
26
+ if (!value || typeof value !== 'object' || value.version !== 1 || typeof value.world !== 'string' || value.world !== env.VOLTER_WORLD
27
+ || !value.capabilities || typeof value.capabilities !== 'object' || Array.isArray(value.capabilities)) return EMPTY_DECLARATION;
28
+ return Object.freeze({ ...value.capabilities, ...(Array.isArray(value.derivedFrom) ? { derivedFrom: value.derivedFrom } : {}) });
29
+ }
30
+
31
+ /** The World field that would grant each capability; absent where the World has no grant for it yet. */
32
+ const GRANT_FIELDS = Object.freeze({
33
+ 'network.egress': 'runtime.network.egress',
34
+ 'network.ownLoopback': 'runtime.network.ownLoopback',
35
+ 'network.services': 'services',
36
+ });
37
+
38
+ const MISSING_WORDS = Object.freeze({
39
+ declaration: 'the guest does not declare it',
40
+ grant: 'the World does not grant it',
41
+ transport: 'this transport cannot carry what the World grants',
42
+ twin: 'no twin in this World serves it',
43
+ });
44
+
45
+ function pass(capability, target, via) {
46
+ return Object.freeze({ outcome: 'pass', capability, target, via });
47
+ }
48
+
49
+ function refuse(capability, target, missing, rule, reason, declaration) {
50
+ const declaredBy = declaration && Array.isArray(declaration.derivedFrom)
51
+ ? declaration.derivedFrom.find(entry => entry && entry.capability === capability) : undefined;
52
+ return Object.freeze({
53
+ outcome: 'refuse', capability, target, missing, rule, reason,
54
+ // the field that would grant it, named only where the World's grant is what is missing
55
+ ...(missing === 'grant' && GRANT_FIELDS[capability] ? { grantField: GRANT_FIELDS[capability] } : {}),
56
+ ...(declaredBy ? { declaredBy } : {}),
57
+ });
58
+ }
59
+
60
+ /** A refusal as the one record every substrate reports. */
61
+ function refusalRecord(answer) {
62
+ const { capability, target, missing, reason, grantField, declaredBy } = answer;
63
+ return Object.freeze({ kind: 'volter-world-refusal', version: 1, capability, target, missing, reason,
64
+ ...(grantField ? { grantField } : {}), ...(declaredBy ? { declaredBy } : {}) });
65
+ }
66
+
67
+ /** The side that refused, in words, for the end of an error message. */
68
+ function refusalSide(answer) {
69
+ const words = MISSING_WORDS[answer.missing] || answer.missing;
70
+ return answer.missing === 'grant' && answer.grantField ? `${words}: ${answer.grantField}` : words;
71
+ }
72
+
73
+ function declaresEgress(declaration, url) {
74
+ const egress = declaration && declaration.network && declaration.network.egress;
75
+ if (egress === '*') return true;
76
+ if (!Array.isArray(egress)) return false;
77
+ return egress.some((entry) => {
78
+ const named = String(entry);
79
+ if (!named.includes('://')) return named.toLowerCase() === url.hostname.toLowerCase();
80
+ try { return new URL(named).origin === url.origin; } catch { return false; }
81
+ });
82
+ }
83
+
84
+ function declaresService(declaration, name) {
85
+ const services = declaration && declaration.network && declaration.network.services;
86
+ return services === '*' || (Array.isArray(services) && services.includes(name));
87
+ }
88
+
89
+ /** The run's own listener: observed by the substrate (a listener the run holds), or declared. A declared `ownLoopback`
90
+ * covers every loopback port, as a container's own loopback does (worker IPC on ephemeral ports); `listen` is inbound
91
+ * exposure, a separate question. */
92
+ function declaresOwnListener(declaration, facts) {
93
+ if (facts.ownListener === true) return true;
94
+ return !!(declaration && declaration.network && declaration.network.ownLoopback === true);
95
+ }
96
+
97
+ /**
98
+ * A loopback or private destination. `request`: { host, port, transport: 'tcp' | 'udp' }. A host process is on the
99
+ * host's network. A guest reaches its World's doors (`facts.endpoint.kind: 'door'`: its proxy, a twin, the World's own
100
+ * origin), the World's services it declares (`kind: 'service'`, by `name`) and the listeners it opened itself.
101
+ */
102
+ function decideLoopback(request, context) {
103
+ const { run = {}, declaration, grant = {}, facts = {} } = context;
104
+ const target = `${request.host}${request.port === undefined ? '' : `:${request.port}`}`;
105
+ if (!run.guest) {
106
+ return declaration && declaration.network && declaration.network.host === true ? pass('network.host', target, 'direct')
107
+ : refuse('network.host', target, 'declaration', 'host-undeclared', `the guest does not declare the host's network (${target})`, declaration);
108
+ }
109
+ if (facts.endpoint) {
110
+ if (facts.endpoint.kind !== 'service' || declaresService(declaration, facts.endpoint.name)) return pass('network.services', target, 'direct');
111
+ return refuse('network.services', target, 'declaration', 'service-undeclared', `the guest does not declare the World's service ${facts.endpoint.name} at ${target}`, declaration);
112
+ }
113
+ if ((request.transport ?? 'tcp') === 'tcp' && declaresOwnListener(declaration, facts)) {
114
+ return grant.policy && grant.policy.ownLoopback === false
115
+ ? refuse('network.ownLoopback', target, 'grant', 'own-loopback-ungranted', `this World does not let a run reach its own listener at ${target}`, declaration)
116
+ : pass('network.ownLoopback', target, 'direct');
117
+ }
118
+ // neither a door of the World nor a listener this run opened: a service of the host, which no World grants a guest
119
+ // without a listener fact the two cannot be told apart here: a host service, or a listener of this run it did not declare
120
+ const hint = declaration && declaration.network && declaration.network.ownLoopback === true ? '' : '; if it is this run\'s own listener, the run declares ownLoopback';
121
+ return refuse('network.host', target, 'grant', 'guest-private', `a guest reaches only its World's doors, its declared services and its own listeners, not ${target}${hint}`, declaration);
122
+ }
123
+
124
+ /** The World's listing of an external destination: sealed refuses, an unreadable policy refuses, a policy refuses what
125
+ * it does not list. null when listed, or when the World restricts nothing. */
126
+ function unlisted(url, target, method, { declaration, grant = {} }) {
127
+ const capability = 'network.egress';
128
+ if (grant.strict === true) return refuse(capability, target, 'grant', 'sealed', `this sealed World refuses untwinned host ${url.hostname}`, declaration);
129
+ if (grant.unreadable) return refuse(capability, target, 'grant', 'policy-unreadable', `the World network policy is unreadable (${grant.unreadable}); refusing ${url.hostname}`, declaration);
130
+ if (grant.policy && !allowsWorldNetworkEgress(grant.policy, url, method ?? 'GET')) {
131
+ return refuse(capability, target, 'grant', 'not-granted', `the World network policy does not allow ${String(method ?? 'GET').toUpperCase()} ${url.origin}`, declaration);
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * An outbound destination. `request`: { url, method?, transport?, port? }. `transport` is `http` (the default),
138
+ * `upgrade`, `http2` (a session: no path at connect), `dispatch` (an undici dispatcher hop), `socket` or `datagram`.
139
+ * A method is given only where the enforcer sees it; without one only the listing is decided, and the inspecting layer
140
+ * the answer sends it through (`via: 'inspected'`) decides the method.
141
+ * `context.facts`: { twin, twinnedHostVendors, twinOrigin, invalid, guestProxy, endpoint, ownListener }.
142
+ */
143
+ function decideEgress(request, context) {
144
+ const { run = {}, declaration, grant = {}, facts = {} } = context;
145
+ const capability = 'network.egress';
146
+ const transport = request.transport ?? 'http';
147
+ const policy = grant.policy;
148
+ const strict = grant.strict === true;
149
+ let url = null;
150
+ try { url = request.url instanceof URL ? request.url : new URL(String(request.url)); } catch { url = null; }
151
+ const target = url ? url.href : String(request.url);
152
+ // an effective authority or path the World cannot read is refused where the World lists anything
153
+ if (facts.invalid && (policy || grant.unreadable)) {
154
+ return refuse(capability, target, 'grant', 'invalid', 'the request has an invalid effective authority or path', declaration);
155
+ }
156
+ if (!url) return pass(capability, target, 'direct');
157
+ const internal = isWorldInternalHost(url.hostname);
158
+ const port = request.port ?? Number(url.port || (url.protocol === 'https:' || url.protocol === 'wss:' ? 443 : 80));
159
+ const guestMediated = run.guest && facts.guestProxy && !facts.endpoint;
160
+
161
+ // a local guest's HTTP leaves through its guest listener, which decides it again; the World's own doors stay direct
162
+ if (guestMediated && (transport === 'http' || transport === 'upgrade' || (transport === 'http2' && !facts.twin))) {
163
+ if (internal && declaresOwnListener(declaration, facts)) return decideLoopback({ host: url.hostname, port, transport: 'tcp' }, context);
164
+ if (transport === 'upgrade') return refuse(capability, target, 'transport', 'guest-upgrade', 'a local guest upgrade requires a mediated transport', declaration);
165
+ if (transport === 'http2') return refuse(capability, target, 'transport', 'guest-http2', 'local guest HTTP/2 requires a mediated transport', declaration);
166
+ return pass(capability, target, 'guest-proxy');
167
+ }
168
+
169
+ if (transport === 'socket' || transport === 'datagram') {
170
+ if (internal && run.guest) {
171
+ return decideLoopback({ host: url.hostname, port: request.port, transport: transport === 'datagram' ? 'udp' : 'tcp' }, context);
172
+ }
173
+ if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
174
+ if (!declaresEgress(declaration, url)) return refuse(capability, target, 'declaration', 'undeclared', `the guest does not declare egress to ${url.origin}`, declaration);
175
+ if (!strict && policy && worldNetworkUsesPassthrough(policy, url)) return pass(capability, target, 'passthrough');
176
+ if (facts.twinnedHostVendors && facts.twinnedHostVendors.length > 0) {
177
+ return refuse(capability, target, 'transport', 'raw-socket-twinned', `${url.hostname} is twinned in this World (${facts.twinnedHostVendors.join(', ')}); a raw socket bypasses the twin`, declaration);
178
+ }
179
+ if (facts.twin && facts.twin.vendor === 'app') {
180
+ return refuse(capability, target, 'transport', 'raw-socket-app', `${url.hostname} is routed to the application; a raw socket cannot be routed`, declaration);
181
+ }
182
+ if (!strict && policy && worldNetworkNeedsPathInspection(policy, url)) {
183
+ return refuse(capability, target, 'transport', 'inspected-transport', `inspected egress requires a mediated HTTP request to ${url.host}`, declaration);
184
+ }
185
+ if (facts.twin) return pass(capability, target, 'direct');
186
+ return unlisted(url, target, undefined, context) ?? pass(capability, target, 'direct');
187
+ }
188
+
189
+ if (!internal && !declaresEgress(declaration, url)) return refuse(capability, target, 'declaration', 'undeclared', `the guest does not declare egress to ${url.origin}`, declaration);
190
+ // an explicit public build grant wins over a twin: that exact path is inspected and sent to its real origin
191
+ const publicBuild = !strict && !!policy && policy.phase === 'build' && allowsWorldNetworkEgress(policy, url);
192
+ if (!publicBuild && facts.twin) return Object.freeze({ outcome: 'twin', capability, target, twin: facts.twin });
193
+ if (!publicBuild && facts.twinnedHostVendors && facts.twinnedHostVendors.length > 0) {
194
+ return refuse(capability, target, 'twin', 'unclaimed-path', `no twin in this World claims ${url.pathname} on ${url.hostname} (${facts.twinnedHostVendors.join(', ')})`, declaration);
195
+ }
196
+ // a guest whose HTTP no guest listener carries (an image run) reaches loopback as its own declaration says
197
+ if (internal && run.guest && !facts.guestProxy && !facts.endpoint) return decideLoopback({ host: url.hostname, port, transport: 'tcp' }, context);
198
+ if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
199
+ const refused = unlisted(url, target, request.method, context);
200
+ if (refused) return refused;
201
+ if (policy && worldNetworkNeedsPathInspection(policy, url)) {
202
+ if (transport !== 'http') {
203
+ const what = transport === 'upgrade' ? 'an upgraded transport' : transport === 'http2' ? 'HTTP/2' : 'an unmediated undici hop';
204
+ return refuse(capability, target, 'transport', 'inspected-transport', `inspected egress cannot carry ${what} to ${url.host}`, declaration);
205
+ }
206
+ return pass(capability, target, 'inspected');
207
+ }
208
+ return pass(capability, target, policy && worldNetworkUsesPassthrough(policy, url) ? 'passthrough' : 'direct');
209
+ }
210
+
211
+ /** A path the guest writes as its data (a `VOLUME`). Declared, it passes run-scoped: writable for the run, not outliving
212
+ * it, as Docker gives an unmounted VOLUME an anonymous volume. Keeping it beyond the run needs a World grant, defined
213
+ * at that need. */
214
+ function decidePersist(request, { declaration }) {
215
+ const persist = declaration && declaration.files && declaration.files.persist;
216
+ if (!Array.isArray(persist) || !persist.includes(request.path)) return refuse('files.persist', request.path, 'declaration', 'persist-undeclared', `the guest does not declare ${request.path} as its data`, declaration);
217
+ return pass('files.persist', request.path, 'run-scoped');
218
+ }
219
+
220
+ /**
221
+ * A connection the decision let through that the run's sandbox refused (a native image run's Seatbelt answers EPERM):
222
+ * the enforcer's rendering is narrower than the decision. One text for every reporter: the injector for Node, the
223
+ * images CLI's kernel-log reporter for other programs (whose target is `*:<port>`: the kernel masks the address).
224
+ */
225
+ function sandboxRefusal(target) {
226
+ return refuse('network.egress', target, 'transport', 'sandbox',
227
+ `the run's sandbox refused a connection to ${target} that the World's decision let through: the sandbox admits only `
228
+ + 'the World proxy and loopback, so a client reaches other hosts through the World proxy (HTTPS_PROXY)');
229
+ }
230
+
231
+ /**
232
+ * The one decision. `request.kind`: `egress` | `loopback` | `persist`.
233
+ * `context`: { run: { guest }, declaration, grant: { policy, strict, unreadable }, facts }.
234
+ */
235
+ function decideCapability(request, context) {
236
+ if (request.kind === 'egress') return decideEgress(request, context);
237
+ if (request.kind === 'loopback') return decideLoopback(request, context);
238
+ if (request.kind === 'persist') return decidePersist(request, context);
239
+ throw new Error(`Unknown capability request: ${request.kind}`);
240
+ }
241
+
242
+ /**
243
+ * The same rules evaluated up front, for an enforcer that renders a profile instead of asking per connection (a native
244
+ * image run's Seatbelt profile). `world.endpoints`: [{ host, port, kind: 'door' | 'service', name }].
245
+ * Egress is `proxy` when declared: twin routing and the per-request listing are the World proxy's, which a profile
246
+ * forces traffic through; `none` when the guest declares none.
247
+ */
248
+ function decideCapabilitySet(declaration, grant = {}, world = {}) {
249
+ const network = (declaration && declaration.network) || {};
250
+ const refusals = [];
251
+ const context = { run: { guest: true }, declaration, grant, facts: {} };
252
+ const endpoints = (world.endpoints || []).filter((endpoint) => {
253
+ const answer = decideLoopback({ host: endpoint.host, port: endpoint.port }, { ...context, facts: { endpoint } });
254
+ if (answer.outcome !== 'pass') refusals.push(answer);
255
+ return answer.outcome === 'pass';
256
+ });
257
+ const listen = Array.isArray(network.listen) ? [...network.listen] : [];
258
+ let loopback = 'none';
259
+ if (network.ownLoopback === true) {
260
+ const answer = decideLoopback({ host: 'localhost', transport: 'tcp' }, { ...context, facts: { ownListener: true } });
261
+ if (answer.outcome === 'pass') loopback = 'own'; else refusals.push(answer);
262
+ }
263
+ if (network.host === true) refusals.push(decideLoopback({ host: 'the host network' }, context));
264
+ const egress = network.egress === '*' || (Array.isArray(network.egress) && network.egress.length > 0) ? 'proxy' : 'none';
265
+ const persist = [];
266
+ for (const path of (declaration && declaration.files && declaration.files.persist) || []) {
267
+ const answer = decidePersist({ path }, context);
268
+ if (answer.outcome === 'pass') persist.push(path); else refusals.push(answer);
269
+ }
270
+ return Object.freeze({ network: Object.freeze({ egress, loopback, listen, endpoints }), files: Object.freeze({ persist }), refusals: Object.freeze(refusals) });
271
+ }
272
+
273
+ module.exports = { OPEN_DECLARATION, EMPTY_DECLARATION, WORLD_DECLARATION_ENV, GRANT_FIELDS, worldDeclarationFromEnv, decideCapability, decideCapabilitySet, sandboxRefusal, refusalRecord, refusalSide };
@@ -0,0 +1,60 @@
1
+ import type { WorldNetworkPolicy } from './network-policy.cjs';
2
+
3
+ export type CapabilityName = 'network.egress' | 'network.ownLoopback' | 'network.services' | 'network.host' | 'files.persist';
4
+ export type RefusalSide = 'declaration' | 'grant' | 'transport' | 'twin';
5
+ export type DeclaredBy = { readonly capability: CapabilityName; readonly file: string; readonly line: number };
6
+
7
+ /** What a guest declares (an image's runtime binding `capabilities`, a browser program's hosts). */
8
+ export interface CapabilityDeclaration {
9
+ readonly network?: {
10
+ readonly egress?: '*' | readonly string[];
11
+ readonly ownLoopback?: boolean;
12
+ readonly listen?: readonly number[];
13
+ readonly services?: '*' | readonly string[];
14
+ readonly host?: boolean;
15
+ };
16
+ readonly files?: { readonly persist?: readonly string[] };
17
+ readonly derivedFrom?: readonly DeclaredBy[];
18
+ }
19
+
20
+ export interface WorldGrant { readonly policy?: WorldNetworkPolicy; readonly strict?: boolean; readonly unreadable?: string }
21
+ export interface WorldEndpoint { readonly host: string; readonly port: number; readonly kind: 'door' | 'service'; readonly name?: string }
22
+ export interface CapabilityFacts {
23
+ readonly twin?: { readonly vendor: string; readonly origin: string } | null;
24
+ readonly twinnedHostVendors?: readonly string[];
25
+ readonly twinOrigin?: boolean;
26
+ readonly invalid?: boolean;
27
+ readonly guestProxy?: URL | null;
28
+ readonly endpoint?: Pick<WorldEndpoint, 'kind' | 'name'> | null;
29
+ readonly ownListener?: boolean;
30
+ }
31
+ export interface CapabilityContext { readonly run?: { readonly guest?: boolean }; readonly declaration?: CapabilityDeclaration; readonly grant?: WorldGrant; readonly facts?: CapabilityFacts }
32
+
33
+ export type CapabilityRequest =
34
+ | { readonly kind: 'egress'; readonly url: string | URL | null; readonly method?: string; readonly transport?: 'http' | 'upgrade' | 'http2' | 'dispatch' | 'socket' | 'datagram'; readonly port?: number }
35
+ | { readonly kind: 'loopback'; readonly host: string; readonly port?: number; readonly transport?: 'tcp' | 'udp' }
36
+ | { readonly kind: 'persist'; readonly path: string };
37
+
38
+ export type CapabilityAnswer =
39
+ | { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' }
40
+ | { readonly outcome: 'twin'; readonly capability: CapabilityName; readonly target: string; readonly twin: { readonly vendor: string; readonly origin: string } }
41
+ | { readonly outcome: 'refuse'; readonly capability: CapabilityName; readonly target: string; readonly missing: RefusalSide; readonly rule: string; readonly reason: string; readonly grantField?: string; readonly declaredBy?: DeclaredBy };
42
+
43
+ export interface WorldRefusalRecord { readonly kind: 'volter-world-refusal'; readonly version: 1; readonly capability: CapabilityName; readonly target: string; readonly missing: RefusalSide; readonly reason: string; readonly grantField?: string; readonly declaredBy?: DeclaredBy }
44
+
45
+ export interface CapabilitySet {
46
+ readonly network: { readonly egress: 'none' | 'proxy'; readonly loopback: 'none' | 'own'; readonly listen: readonly number[]; readonly endpoints: readonly WorldEndpoint[] };
47
+ readonly files: { readonly persist: readonly string[] };
48
+ readonly refusals: readonly Extract<CapabilityAnswer, { outcome: 'refuse' }>[];
49
+ }
50
+
51
+ export const OPEN_DECLARATION: CapabilityDeclaration;
52
+ export const EMPTY_DECLARATION: CapabilityDeclaration;
53
+ export const WORLD_DECLARATION_ENV: 'VOLTER_WORLD_DECLARATION';
54
+ export function worldDeclarationFromEnv(env: Readonly<Record<string, string | undefined>>): CapabilityDeclaration | undefined;
55
+ export function sandboxRefusal(target: string): Extract<CapabilityAnswer, { outcome: 'refuse' }>;
56
+ export const GRANT_FIELDS: Readonly<Partial<Record<CapabilityName, string>>>;
57
+ export function decideCapability(request: CapabilityRequest, context: CapabilityContext): CapabilityAnswer;
58
+ export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[] }): CapabilitySet;
59
+ export function refusalRecord(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): WorldRefusalRecord;
60
+ export function refusalSide(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): string;
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+ // THE WORLD'S CAPABILITY DECISION (docs/contributing/architecture.md, "Capability decisions"): what a guest's request
3
+ // gets, PASS, TWIN or REFUSE, from what the guest declares and what the World grants. Every substrate (the injector, the
4
+ // scoped proxy, a tab's relay, a native image run's Seatbelt profile) enforces this answer and decides nothing itself.
5
+ // Pure: no I/O, no environment and nothing at import. What only the substrate can observe (the twin route, the vendors
6
+ // sharing a host, which loopback ports are the World's doors or the run's own listeners) arrives as `facts`.
7
+ const { allowsWorldNetworkEgress, isWorldInternalHost, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough } = require('./network-policy.cjs');
8
+
9
+ /** The declaration of a guest with no declaration format: an ordinary host process, which can reach what the host can.
10
+ * Its own loopback is what it observably opened (`facts.ownListener`). A substrate whose guest can declare never uses it. */
11
+ const OPEN_DECLARATION = Object.freeze({ network: Object.freeze({ egress: '*', services: '*', host: true }) });
12
+
13
+ /** What an unreadable declaration, or one bound to another World, declares: nothing. */
14
+ const EMPTY_DECLARATION = Object.freeze({ network: Object.freeze({}) });
15
+
16
+ /** Where an image run's processes carry the declaration of its runtime binding (architecture, "Capability decisions"). */
17
+ const WORLD_DECLARATION_ENV = 'VOLTER_WORLD_DECLARATION';
18
+
19
+ /** The declaration `env` carries for the attached World, undefined where it carries none, and the empty declaration
20
+ * where the value is unreadable or bound to another World. The caller passes the environment; nothing is read here. */
21
+ function worldDeclarationFromEnv(env) {
22
+ const serialized = env[WORLD_DECLARATION_ENV];
23
+ if (serialized === undefined) return undefined;
24
+ let value;
25
+ try { value = JSON.parse(String(serialized)); } catch { return EMPTY_DECLARATION; }
26
+ if (!value || typeof value !== 'object' || value.version !== 1 || typeof value.world !== 'string' || value.world !== env.VOLTER_WORLD
27
+ || !value.capabilities || typeof value.capabilities !== 'object' || Array.isArray(value.capabilities)) return EMPTY_DECLARATION;
28
+ return Object.freeze({ ...value.capabilities, ...(Array.isArray(value.derivedFrom) ? { derivedFrom: value.derivedFrom } : {}) });
29
+ }
30
+
31
+ /** The World field that would grant each capability; absent where the World has no grant for it yet. */
32
+ const GRANT_FIELDS = Object.freeze({
33
+ 'network.egress': 'runtime.network.egress',
34
+ 'network.ownLoopback': 'runtime.network.ownLoopback',
35
+ 'network.services': 'services',
36
+ });
37
+
38
+ const MISSING_WORDS = Object.freeze({
39
+ declaration: 'the guest does not declare it',
40
+ grant: 'the World does not grant it',
41
+ transport: 'this transport cannot carry what the World grants',
42
+ twin: 'no twin in this World serves it',
43
+ });
44
+
45
+ function pass(capability, target, via) {
46
+ return Object.freeze({ outcome: 'pass', capability, target, via });
47
+ }
48
+
49
+ function refuse(capability, target, missing, rule, reason, declaration) {
50
+ const declaredBy = declaration && Array.isArray(declaration.derivedFrom)
51
+ ? declaration.derivedFrom.find(entry => entry && entry.capability === capability) : undefined;
52
+ return Object.freeze({
53
+ outcome: 'refuse', capability, target, missing, rule, reason,
54
+ // the field that would grant it, named only where the World's grant is what is missing
55
+ ...(missing === 'grant' && GRANT_FIELDS[capability] ? { grantField: GRANT_FIELDS[capability] } : {}),
56
+ ...(declaredBy ? { declaredBy } : {}),
57
+ });
58
+ }
59
+
60
+ /** A refusal as the one record every substrate reports. */
61
+ function refusalRecord(answer) {
62
+ const { capability, target, missing, reason, grantField, declaredBy } = answer;
63
+ return Object.freeze({ kind: 'volter-world-refusal', version: 1, capability, target, missing, reason,
64
+ ...(grantField ? { grantField } : {}), ...(declaredBy ? { declaredBy } : {}) });
65
+ }
66
+
67
+ /** The side that refused, in words, for the end of an error message. */
68
+ function refusalSide(answer) {
69
+ const words = MISSING_WORDS[answer.missing] || answer.missing;
70
+ return answer.missing === 'grant' && answer.grantField ? `${words}: ${answer.grantField}` : words;
71
+ }
72
+
73
+ function declaresEgress(declaration, url) {
74
+ const egress = declaration && declaration.network && declaration.network.egress;
75
+ if (egress === '*') return true;
76
+ if (!Array.isArray(egress)) return false;
77
+ return egress.some((entry) => {
78
+ const named = String(entry);
79
+ if (!named.includes('://')) return named.toLowerCase() === url.hostname.toLowerCase();
80
+ try { return new URL(named).origin === url.origin; } catch { return false; }
81
+ });
82
+ }
83
+
84
+ function declaresService(declaration, name) {
85
+ const services = declaration && declaration.network && declaration.network.services;
86
+ return services === '*' || (Array.isArray(services) && services.includes(name));
87
+ }
88
+
89
+ /** The run's own listener: observed by the substrate (a listener the run holds), or declared. A declared `ownLoopback`
90
+ * covers every loopback port, as a container's own loopback does (worker IPC on ephemeral ports); `listen` is inbound
91
+ * exposure, a separate question. */
92
+ function declaresOwnListener(declaration, facts) {
93
+ if (facts.ownListener === true) return true;
94
+ return !!(declaration && declaration.network && declaration.network.ownLoopback === true);
95
+ }
96
+
97
+ /**
98
+ * A loopback or private destination. `request`: { host, port, transport: 'tcp' | 'udp' }. A host process is on the
99
+ * host's network. A guest reaches its World's doors (`facts.endpoint.kind: 'door'`: its proxy, a twin, the World's own
100
+ * origin), the World's services it declares (`kind: 'service'`, by `name`) and the listeners it opened itself.
101
+ */
102
+ function decideLoopback(request, context) {
103
+ const { run = {}, declaration, grant = {}, facts = {} } = context;
104
+ const target = `${request.host}${request.port === undefined ? '' : `:${request.port}`}`;
105
+ if (!run.guest) {
106
+ return declaration && declaration.network && declaration.network.host === true ? pass('network.host', target, 'direct')
107
+ : refuse('network.host', target, 'declaration', 'host-undeclared', `the guest does not declare the host's network (${target})`, declaration);
108
+ }
109
+ if (facts.endpoint) {
110
+ if (facts.endpoint.kind !== 'service' || declaresService(declaration, facts.endpoint.name)) return pass('network.services', target, 'direct');
111
+ return refuse('network.services', target, 'declaration', 'service-undeclared', `the guest does not declare the World's service ${facts.endpoint.name} at ${target}`, declaration);
112
+ }
113
+ if ((request.transport ?? 'tcp') === 'tcp' && declaresOwnListener(declaration, facts)) {
114
+ return grant.policy && grant.policy.ownLoopback === false
115
+ ? refuse('network.ownLoopback', target, 'grant', 'own-loopback-ungranted', `this World does not let a run reach its own listener at ${target}`, declaration)
116
+ : pass('network.ownLoopback', target, 'direct');
117
+ }
118
+ // neither a door of the World nor a listener this run opened: a service of the host, which no World grants a guest
119
+ // without a listener fact the two cannot be told apart here: a host service, or a listener of this run it did not declare
120
+ const hint = declaration && declaration.network && declaration.network.ownLoopback === true ? '' : '; if it is this run\'s own listener, the run declares ownLoopback';
121
+ return refuse('network.host', target, 'grant', 'guest-private', `a guest reaches only its World's doors, its declared services and its own listeners, not ${target}${hint}`, declaration);
122
+ }
123
+
124
+ /** The World's listing of an external destination: sealed refuses, an unreadable policy refuses, a policy refuses what
125
+ * it does not list. null when listed, or when the World restricts nothing. */
126
+ function unlisted(url, target, method, { declaration, grant = {} }) {
127
+ const capability = 'network.egress';
128
+ if (grant.strict === true) return refuse(capability, target, 'grant', 'sealed', `this sealed World refuses untwinned host ${url.hostname}`, declaration);
129
+ if (grant.unreadable) return refuse(capability, target, 'grant', 'policy-unreadable', `the World network policy is unreadable (${grant.unreadable}); refusing ${url.hostname}`, declaration);
130
+ if (grant.policy && !allowsWorldNetworkEgress(grant.policy, url, method ?? 'GET')) {
131
+ return refuse(capability, target, 'grant', 'not-granted', `the World network policy does not allow ${String(method ?? 'GET').toUpperCase()} ${url.origin}`, declaration);
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * An outbound destination. `request`: { url, method?, transport?, port? }. `transport` is `http` (the default),
138
+ * `upgrade`, `http2` (a session: no path at connect), `dispatch` (an undici dispatcher hop), `socket` or `datagram`.
139
+ * A method is given only where the enforcer sees it; without one only the listing is decided, and the inspecting layer
140
+ * the answer sends it through (`via: 'inspected'`) decides the method.
141
+ * `context.facts`: { twin, twinnedHostVendors, twinOrigin, invalid, guestProxy, endpoint, ownListener }.
142
+ */
143
+ function decideEgress(request, context) {
144
+ const { run = {}, declaration, grant = {}, facts = {} } = context;
145
+ const capability = 'network.egress';
146
+ const transport = request.transport ?? 'http';
147
+ const policy = grant.policy;
148
+ const strict = grant.strict === true;
149
+ let url = null;
150
+ try { url = request.url instanceof URL ? request.url : new URL(String(request.url)); } catch { url = null; }
151
+ const target = url ? url.href : String(request.url);
152
+ // an effective authority or path the World cannot read is refused where the World lists anything
153
+ if (facts.invalid && (policy || grant.unreadable)) {
154
+ return refuse(capability, target, 'grant', 'invalid', 'the request has an invalid effective authority or path', declaration);
155
+ }
156
+ if (!url) return pass(capability, target, 'direct');
157
+ const internal = isWorldInternalHost(url.hostname);
158
+ const port = request.port ?? Number(url.port || (url.protocol === 'https:' || url.protocol === 'wss:' ? 443 : 80));
159
+ const guestMediated = run.guest && facts.guestProxy && !facts.endpoint;
160
+
161
+ // a local guest's HTTP leaves through its guest listener, which decides it again; the World's own doors stay direct
162
+ if (guestMediated && (transport === 'http' || transport === 'upgrade' || (transport === 'http2' && !facts.twin))) {
163
+ if (internal && declaresOwnListener(declaration, facts)) return decideLoopback({ host: url.hostname, port, transport: 'tcp' }, context);
164
+ if (transport === 'upgrade') return refuse(capability, target, 'transport', 'guest-upgrade', 'a local guest upgrade requires a mediated transport', declaration);
165
+ if (transport === 'http2') return refuse(capability, target, 'transport', 'guest-http2', 'local guest HTTP/2 requires a mediated transport', declaration);
166
+ return pass(capability, target, 'guest-proxy');
167
+ }
168
+
169
+ if (transport === 'socket' || transport === 'datagram') {
170
+ if (internal && run.guest) {
171
+ return decideLoopback({ host: url.hostname, port: request.port, transport: transport === 'datagram' ? 'udp' : 'tcp' }, context);
172
+ }
173
+ if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
174
+ if (!declaresEgress(declaration, url)) return refuse(capability, target, 'declaration', 'undeclared', `the guest does not declare egress to ${url.origin}`, declaration);
175
+ if (!strict && policy && worldNetworkUsesPassthrough(policy, url)) return pass(capability, target, 'passthrough');
176
+ if (facts.twinnedHostVendors && facts.twinnedHostVendors.length > 0) {
177
+ return refuse(capability, target, 'transport', 'raw-socket-twinned', `${url.hostname} is twinned in this World (${facts.twinnedHostVendors.join(', ')}); a raw socket bypasses the twin`, declaration);
178
+ }
179
+ if (facts.twin && facts.twin.vendor === 'app') {
180
+ return refuse(capability, target, 'transport', 'raw-socket-app', `${url.hostname} is routed to the application; a raw socket cannot be routed`, declaration);
181
+ }
182
+ if (!strict && policy && worldNetworkNeedsPathInspection(policy, url)) {
183
+ return refuse(capability, target, 'transport', 'inspected-transport', `inspected egress requires a mediated HTTP request to ${url.host}`, declaration);
184
+ }
185
+ if (facts.twin) return pass(capability, target, 'direct');
186
+ return unlisted(url, target, undefined, context) ?? pass(capability, target, 'direct');
187
+ }
188
+
189
+ if (!internal && !declaresEgress(declaration, url)) return refuse(capability, target, 'declaration', 'undeclared', `the guest does not declare egress to ${url.origin}`, declaration);
190
+ // an explicit public build grant wins over a twin: that exact path is inspected and sent to its real origin
191
+ const publicBuild = !strict && !!policy && policy.phase === 'build' && allowsWorldNetworkEgress(policy, url);
192
+ if (!publicBuild && facts.twin) return Object.freeze({ outcome: 'twin', capability, target, twin: facts.twin });
193
+ if (!publicBuild && facts.twinnedHostVendors && facts.twinnedHostVendors.length > 0) {
194
+ return refuse(capability, target, 'twin', 'unclaimed-path', `no twin in this World claims ${url.pathname} on ${url.hostname} (${facts.twinnedHostVendors.join(', ')})`, declaration);
195
+ }
196
+ // a guest whose HTTP no guest listener carries (an image run) reaches loopback as its own declaration says
197
+ if (internal && run.guest && !facts.guestProxy && !facts.endpoint) return decideLoopback({ host: url.hostname, port, transport: 'tcp' }, context);
198
+ if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
199
+ const refused = unlisted(url, target, request.method, context);
200
+ if (refused) return refused;
201
+ if (policy && worldNetworkNeedsPathInspection(policy, url)) {
202
+ if (transport !== 'http') {
203
+ const what = transport === 'upgrade' ? 'an upgraded transport' : transport === 'http2' ? 'HTTP/2' : 'an unmediated undici hop';
204
+ return refuse(capability, target, 'transport', 'inspected-transport', `inspected egress cannot carry ${what} to ${url.host}`, declaration);
205
+ }
206
+ return pass(capability, target, 'inspected');
207
+ }
208
+ return pass(capability, target, policy && worldNetworkUsesPassthrough(policy, url) ? 'passthrough' : 'direct');
209
+ }
210
+
211
+ /** A path the guest writes as its data (a `VOLUME`). Declared, it passes run-scoped: writable for the run, not outliving
212
+ * it, as Docker gives an unmounted VOLUME an anonymous volume. Keeping it beyond the run needs a World grant, defined
213
+ * at that need. */
214
+ function decidePersist(request, { declaration }) {
215
+ const persist = declaration && declaration.files && declaration.files.persist;
216
+ if (!Array.isArray(persist) || !persist.includes(request.path)) return refuse('files.persist', request.path, 'declaration', 'persist-undeclared', `the guest does not declare ${request.path} as its data`, declaration);
217
+ return pass('files.persist', request.path, 'run-scoped');
218
+ }
219
+
220
+ /**
221
+ * A connection the decision let through that the run's sandbox refused (a native image run's Seatbelt answers EPERM):
222
+ * the enforcer's rendering is narrower than the decision. One text for every reporter: the injector for Node, the
223
+ * images CLI's kernel-log reporter for other programs (whose target is `*:<port>`: the kernel masks the address).
224
+ */
225
+ function sandboxRefusal(target) {
226
+ return refuse('network.egress', target, 'transport', 'sandbox',
227
+ `the run's sandbox refused a connection to ${target} that the World's decision let through: the sandbox admits only `
228
+ + 'the World proxy and loopback, so a client reaches other hosts through the World proxy (HTTPS_PROXY)');
229
+ }
230
+
231
+ /**
232
+ * The one decision. `request.kind`: `egress` | `loopback` | `persist`.
233
+ * `context`: { run: { guest }, declaration, grant: { policy, strict, unreadable }, facts }.
234
+ */
235
+ function decideCapability(request, context) {
236
+ if (request.kind === 'egress') return decideEgress(request, context);
237
+ if (request.kind === 'loopback') return decideLoopback(request, context);
238
+ if (request.kind === 'persist') return decidePersist(request, context);
239
+ throw new Error(`Unknown capability request: ${request.kind}`);
240
+ }
241
+
242
+ /**
243
+ * The same rules evaluated up front, for an enforcer that renders a profile instead of asking per connection (a native
244
+ * image run's Seatbelt profile). `world.endpoints`: [{ host, port, kind: 'door' | 'service', name }].
245
+ * Egress is `proxy` when declared: twin routing and the per-request listing are the World proxy's, which a profile
246
+ * forces traffic through; `none` when the guest declares none.
247
+ */
248
+ function decideCapabilitySet(declaration, grant = {}, world = {}) {
249
+ const network = (declaration && declaration.network) || {};
250
+ const refusals = [];
251
+ const context = { run: { guest: true }, declaration, grant, facts: {} };
252
+ const endpoints = (world.endpoints || []).filter((endpoint) => {
253
+ const answer = decideLoopback({ host: endpoint.host, port: endpoint.port }, { ...context, facts: { endpoint } });
254
+ if (answer.outcome !== 'pass') refusals.push(answer);
255
+ return answer.outcome === 'pass';
256
+ });
257
+ const listen = Array.isArray(network.listen) ? [...network.listen] : [];
258
+ let loopback = 'none';
259
+ if (network.ownLoopback === true) {
260
+ const answer = decideLoopback({ host: 'localhost', transport: 'tcp' }, { ...context, facts: { ownListener: true } });
261
+ if (answer.outcome === 'pass') loopback = 'own'; else refusals.push(answer);
262
+ }
263
+ if (network.host === true) refusals.push(decideLoopback({ host: 'the host network' }, context));
264
+ const egress = network.egress === '*' || (Array.isArray(network.egress) && network.egress.length > 0) ? 'proxy' : 'none';
265
+ const persist = [];
266
+ for (const path of (declaration && declaration.files && declaration.files.persist) || []) {
267
+ const answer = decidePersist({ path }, context);
268
+ if (answer.outcome === 'pass') persist.push(path); else refusals.push(answer);
269
+ }
270
+ return Object.freeze({ network: Object.freeze({ egress, loopback, listen, endpoints }), files: Object.freeze({ persist }), refusals: Object.freeze(refusals) });
271
+ }
272
+
273
+ module.exports = { OPEN_DECLARATION, EMPTY_DECLARATION, WORLD_DECLARATION_ENV, GRANT_FIELDS, worldDeclarationFromEnv, decideCapability, decideCapabilitySet, sandboxRefusal, refusalRecord, refusalSide };