@volter/world-core 3.0.65 → 3.0.67
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/capability-decision.cjs +41 -6
- package/capability-decision.d.cts +5 -1
- package/dist/capability-decision.cjs +41 -6
- package/dist/capability-decision.d.cts +5 -1
- package/dist/inject.cjs +44 -13
- package/inject.cjs +44 -13
- package/package.json +1 -1
package/capability-decision.cjs
CHANGED
|
@@ -10,6 +10,24 @@ const { allowsWorldNetworkEgress, isWorldInternalHost, worldNetworkNeedsPathInsp
|
|
|
10
10
|
* Its own loopback is what it observably opened (`facts.ownListener`). A substrate whose guest can declare never uses it. */
|
|
11
11
|
const OPEN_DECLARATION = Object.freeze({ network: Object.freeze({ egress: '*', services: '*', host: true }) });
|
|
12
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
|
+
|
|
13
31
|
/** The World field that would grant each capability; absent where the World has no grant for it yet. */
|
|
14
32
|
const GRANT_FIELDS = Object.freeze({
|
|
15
33
|
'network.egress': 'runtime.network.egress',
|
|
@@ -98,7 +116,9 @@ function decideLoopback(request, context) {
|
|
|
98
116
|
: pass('network.ownLoopback', target, 'direct');
|
|
99
117
|
}
|
|
100
118
|
// neither a door of the World nor a listener this run opened: a service of the host, which no World grants a guest
|
|
101
|
-
|
|
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);
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/** The World's listing of an external destination: sealed refuses, an unreadable policy refuses, a policy refuses what
|
|
@@ -147,7 +167,7 @@ function decideEgress(request, context) {
|
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
if (transport === 'socket' || transport === 'datagram') {
|
|
150
|
-
if (internal && run.guest
|
|
170
|
+
if (internal && run.guest) {
|
|
151
171
|
return decideLoopback({ host: url.hostname, port: request.port, transport: transport === 'datagram' ? 'udp' : 'tcp' }, context);
|
|
152
172
|
}
|
|
153
173
|
if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
|
|
@@ -173,6 +193,8 @@ function decideEgress(request, context) {
|
|
|
173
193
|
if (!publicBuild && facts.twinnedHostVendors && facts.twinnedHostVendors.length > 0) {
|
|
174
194
|
return refuse(capability, target, 'twin', 'unclaimed-path', `no twin in this World claims ${url.pathname} on ${url.hostname} (${facts.twinnedHostVendors.join(', ')})`, declaration);
|
|
175
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);
|
|
176
198
|
if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
|
|
177
199
|
const refused = unlisted(url, target, request.method, context);
|
|
178
200
|
if (refused) return refused;
|
|
@@ -186,11 +208,24 @@ function decideEgress(request, context) {
|
|
|
186
208
|
return pass(capability, target, policy && worldNetworkUsesPassthrough(policy, url) ? 'passthrough' : 'direct');
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
/** A path
|
|
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. */
|
|
190
214
|
function decidePersist(request, { declaration }) {
|
|
191
215
|
const persist = declaration && declaration.files && declaration.files.persist;
|
|
192
|
-
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
|
|
193
|
-
return
|
|
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)');
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
/**
|
|
@@ -235,4 +270,4 @@ function decideCapabilitySet(declaration, grant = {}, world = {}) {
|
|
|
235
270
|
return Object.freeze({ network: Object.freeze({ egress, loopback, listen, endpoints }), files: Object.freeze({ persist }), refusals: Object.freeze(refusals) });
|
|
236
271
|
}
|
|
237
272
|
|
|
238
|
-
module.exports = { OPEN_DECLARATION, GRANT_FIELDS, decideCapability, decideCapabilitySet, refusalRecord, refusalSide };
|
|
273
|
+
module.exports = { OPEN_DECLARATION, EMPTY_DECLARATION, WORLD_DECLARATION_ENV, GRANT_FIELDS, worldDeclarationFromEnv, decideCapability, decideCapabilitySet, sandboxRefusal, refusalRecord, refusalSide };
|
|
@@ -36,7 +36,7 @@ export type CapabilityRequest =
|
|
|
36
36
|
| { readonly kind: 'persist'; readonly path: string };
|
|
37
37
|
|
|
38
38
|
export type CapabilityAnswer =
|
|
39
|
-
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' }
|
|
39
|
+
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' }
|
|
40
40
|
| { readonly outcome: 'twin'; readonly capability: CapabilityName; readonly target: string; readonly twin: { readonly vendor: string; readonly origin: string } }
|
|
41
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
42
|
|
|
@@ -49,6 +49,10 @@ export interface CapabilitySet {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
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' }>;
|
|
52
56
|
export const GRANT_FIELDS: Readonly<Partial<Record<CapabilityName, string>>>;
|
|
53
57
|
export function decideCapability(request: CapabilityRequest, context: CapabilityContext): CapabilityAnswer;
|
|
54
58
|
export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[] }): CapabilitySet;
|
|
@@ -10,6 +10,24 @@ const { allowsWorldNetworkEgress, isWorldInternalHost, worldNetworkNeedsPathInsp
|
|
|
10
10
|
* Its own loopback is what it observably opened (`facts.ownListener`). A substrate whose guest can declare never uses it. */
|
|
11
11
|
const OPEN_DECLARATION = Object.freeze({ network: Object.freeze({ egress: '*', services: '*', host: true }) });
|
|
12
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
|
+
|
|
13
31
|
/** The World field that would grant each capability; absent where the World has no grant for it yet. */
|
|
14
32
|
const GRANT_FIELDS = Object.freeze({
|
|
15
33
|
'network.egress': 'runtime.network.egress',
|
|
@@ -98,7 +116,9 @@ function decideLoopback(request, context) {
|
|
|
98
116
|
: pass('network.ownLoopback', target, 'direct');
|
|
99
117
|
}
|
|
100
118
|
// neither a door of the World nor a listener this run opened: a service of the host, which no World grants a guest
|
|
101
|
-
|
|
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);
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/** The World's listing of an external destination: sealed refuses, an unreadable policy refuses, a policy refuses what
|
|
@@ -147,7 +167,7 @@ function decideEgress(request, context) {
|
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
if (transport === 'socket' || transport === 'datagram') {
|
|
150
|
-
if (internal && run.guest
|
|
170
|
+
if (internal && run.guest) {
|
|
151
171
|
return decideLoopback({ host: url.hostname, port: request.port, transport: transport === 'datagram' ? 'udp' : 'tcp' }, context);
|
|
152
172
|
}
|
|
153
173
|
if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
|
|
@@ -173,6 +193,8 @@ function decideEgress(request, context) {
|
|
|
173
193
|
if (!publicBuild && facts.twinnedHostVendors && facts.twinnedHostVendors.length > 0) {
|
|
174
194
|
return refuse(capability, target, 'twin', 'unclaimed-path', `no twin in this World claims ${url.pathname} on ${url.hostname} (${facts.twinnedHostVendors.join(', ')})`, declaration);
|
|
175
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);
|
|
176
198
|
if (internal || facts.twinOrigin) return pass(capability, target, 'direct');
|
|
177
199
|
const refused = unlisted(url, target, request.method, context);
|
|
178
200
|
if (refused) return refused;
|
|
@@ -186,11 +208,24 @@ function decideEgress(request, context) {
|
|
|
186
208
|
return pass(capability, target, policy && worldNetworkUsesPassthrough(policy, url) ? 'passthrough' : 'direct');
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
/** A path
|
|
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. */
|
|
190
214
|
function decidePersist(request, { declaration }) {
|
|
191
215
|
const persist = declaration && declaration.files && declaration.files.persist;
|
|
192
|
-
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
|
|
193
|
-
return
|
|
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)');
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
/**
|
|
@@ -235,4 +270,4 @@ function decideCapabilitySet(declaration, grant = {}, world = {}) {
|
|
|
235
270
|
return Object.freeze({ network: Object.freeze({ egress, loopback, listen, endpoints }), files: Object.freeze({ persist }), refusals: Object.freeze(refusals) });
|
|
236
271
|
}
|
|
237
272
|
|
|
238
|
-
module.exports = { OPEN_DECLARATION, GRANT_FIELDS, decideCapability, decideCapabilitySet, refusalRecord, refusalSide };
|
|
273
|
+
module.exports = { OPEN_DECLARATION, EMPTY_DECLARATION, WORLD_DECLARATION_ENV, GRANT_FIELDS, worldDeclarationFromEnv, decideCapability, decideCapabilitySet, sandboxRefusal, refusalRecord, refusalSide };
|
|
@@ -36,7 +36,7 @@ export type CapabilityRequest =
|
|
|
36
36
|
| { readonly kind: 'persist'; readonly path: string };
|
|
37
37
|
|
|
38
38
|
export type CapabilityAnswer =
|
|
39
|
-
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' }
|
|
39
|
+
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' }
|
|
40
40
|
| { readonly outcome: 'twin'; readonly capability: CapabilityName; readonly target: string; readonly twin: { readonly vendor: string; readonly origin: string } }
|
|
41
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
42
|
|
|
@@ -49,6 +49,10 @@ export interface CapabilitySet {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
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' }>;
|
|
52
56
|
export const GRANT_FIELDS: Readonly<Partial<Record<CapabilityName, string>>>;
|
|
53
57
|
export function decideCapability(request: CapabilityRequest, context: CapabilityContext): CapabilityAnswer;
|
|
54
58
|
export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[] }): CapabilitySet;
|
package/dist/inject.cjs
CHANGED
|
@@ -46,7 +46,7 @@ const Module = require('module');
|
|
|
46
46
|
const { urlToHttpOptions } = require('node:url');
|
|
47
47
|
const claimsRefreshChannel = require('node:diagnostics_channel').channel('volter.inject.claims-refresh');
|
|
48
48
|
const { worldNetworkPolicyFromEnv, isWorldInternalHost } = require('./network-policy.cjs');
|
|
49
|
-
const { OPEN_DECLARATION, decideCapability, refusalRecord, refusalSide } = require('./capability-decision.cjs');
|
|
49
|
+
const { OPEN_DECLARATION, worldDeclarationFromEnv, decideCapability, sandboxRefusal, refusalRecord, refusalSide } = require('./capability-decision.cjs');
|
|
50
50
|
const { createPublicKey, createVerify } = require('crypto');
|
|
51
51
|
|
|
52
52
|
// the vendor host table (VENDOR_HOSTS: a hostname, and a path where two vendors share a host, to the vendor whose twin
|
|
@@ -327,29 +327,36 @@ function strictEgressEnabled() {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
// THE WORLD'S DECISION (docs/contributing/architecture.md, "Capability decisions"): every seam below asks
|
|
330
|
-
// capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one
|
|
331
|
-
// local light guest, whose HTTP leaves through its guest listener;
|
|
332
|
-
//
|
|
330
|
+
// capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one; a
|
|
331
|
+
// local light guest, whose HTTP leaves through its guest listener; or a process of an image run, which carries its
|
|
332
|
+
// image's declaration (VOLTER_WORLD_DECLARATION) and is confined from the host's network. What the World grants is its
|
|
333
|
+
// published policy. The seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
|
|
333
334
|
function worldGrant() {
|
|
334
335
|
let policy; let unreadable;
|
|
335
336
|
try { policy = worldNetworkPolicyFromEnv(process.env); } catch (error) { unreadable = error.message; }
|
|
336
337
|
return { policy, strict: strictEgressEnabled(), unreadable };
|
|
337
338
|
}
|
|
339
|
+
/** This process's run: its declaration, and whether it is confined from the host's network. */
|
|
340
|
+
function worldRun() {
|
|
341
|
+
const declared = worldDeclarationFromEnv(process.env);
|
|
342
|
+
return declared ? { guest: true, declaration: declared } : { guest: !!localGuestProxy(), declaration: OPEN_DECLARATION };
|
|
343
|
+
}
|
|
338
344
|
function decide(url, transport, facts = {}, port) {
|
|
339
|
-
const
|
|
345
|
+
const { guest, declaration } = worldRun();
|
|
340
346
|
return decideCapability({ kind: 'egress', url, transport, port },
|
|
341
|
-
{ run: { guest
|
|
347
|
+
{ run: { guest }, declaration, grant: worldGrant(), facts: { guestProxy: localGuestProxy(), ...facts } });
|
|
342
348
|
}
|
|
343
349
|
/** What an HTTP seam observes about `url`. A local guest's request to one of the World's doors, or not over HTTP(S), is
|
|
344
350
|
* not mediated by its guest listener. */
|
|
345
351
|
function httpFacts(url, pathname) {
|
|
346
352
|
if (!url) return {};
|
|
347
353
|
const guestProxy = localGuestProxy();
|
|
354
|
+
const proxy = worldProxy();
|
|
348
355
|
return {
|
|
349
356
|
twin: resolveTwin(url.hostname, MAP, pathname === undefined ? url.pathname : pathname),
|
|
350
357
|
twinnedHostVendors: twinnedHostVendors(url.hostname, MAP),
|
|
351
358
|
...(guestProxy && !/^https?:$/.test(url.protocol) ? { guestProxy: null } : {}),
|
|
352
|
-
endpoint:
|
|
359
|
+
endpoint: worldRun().guest && (addressedToWorld(url) || explicitTwinEndpoint(url) || (proxy && url.origin === proxy.origin)) ? { kind: 'door' } : null,
|
|
353
360
|
};
|
|
354
361
|
}
|
|
355
362
|
/** A refusal as an Error: the seam's own message, the side that refused, and the World's refusal record. */
|
|
@@ -489,6 +496,10 @@ function worldBase() {
|
|
|
489
496
|
}
|
|
490
497
|
// The local Substrate agent keeps host control routing; its product commands select the
|
|
491
498
|
// existing guest listener. Route original destinations through that listener before rewriting.
|
|
499
|
+
/** The World proxy this process is pointed at, or null. */
|
|
500
|
+
function worldProxy() {
|
|
501
|
+
try { return new URL(process.env.VOLTER_WORLD_PROXY || ''); } catch { return null; }
|
|
502
|
+
}
|
|
492
503
|
function localGuestProxy() {
|
|
493
504
|
const guest = process.env.VOLTER_WORLD_GUEST_PROXY;
|
|
494
505
|
if (!guest || process.env.VOLTER_WORLD_PROXY !== guest) return null;
|
|
@@ -1197,10 +1208,9 @@ function socketRefusal(hostname, port, isTls, datagram = false) {
|
|
|
1197
1208
|
const authority = `${isIP(host) === 6 ? `[${host}]` : host}${port !== undefined && !(isTls && port === 443) ? `:${port}` : ''}`;
|
|
1198
1209
|
const url = (() => { try { return new URL(`${isTls ? 'https' : 'http'}://${authority}/`); } catch { return null; } })();
|
|
1199
1210
|
if (!url) return null;
|
|
1200
|
-
const guestProxy = localGuestProxy();
|
|
1201
1211
|
const internal = isLocalOrPrivateHost(host);
|
|
1202
1212
|
const facts = internal
|
|
1203
|
-
? { endpoint:
|
|
1213
|
+
? { endpoint: worldRun().guest && guestDoorAt(host, port) ? { kind: 'door' } : null }
|
|
1204
1214
|
: { twin: resolveTwin(host, MAP), twinnedHostVendors: twinnedHostVendors(host, MAP), twinOrigin: isTwinOriginHost(host) };
|
|
1205
1215
|
const answer = decide(url, datagram ? 'datagram' : 'socket', facts, port);
|
|
1206
1216
|
if (answer.outcome !== 'refuse') return null;
|
|
@@ -1222,9 +1232,9 @@ function socketRefusal(hostname, port, isTls, datagram = false) {
|
|
|
1222
1232
|
return refusalError(answer, message);
|
|
1223
1233
|
}
|
|
1224
1234
|
|
|
1225
|
-
/** One of the World's doors a
|
|
1235
|
+
/** One of the World's doors a guest connects to directly: its proxy (a local guest's listener), the World's origin, a twin. */
|
|
1226
1236
|
function guestDoorAt(host, port) {
|
|
1227
|
-
const doors = [localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
|
|
1237
|
+
const doors = [worldProxy(), localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
|
|
1228
1238
|
return doors.some(door => door.hostname.replace(/^\[|\]$/g, '') === host.replace(/^\[|\]$/g, '') && Number(door.port || (door.protocol === 'https:' ? 443 : 80)) === port);
|
|
1229
1239
|
}
|
|
1230
1240
|
|
|
@@ -1263,7 +1273,8 @@ function patchedInstanceConnect(originalConnect) {
|
|
|
1263
1273
|
return function guardedInstanceConnect(...args) {
|
|
1264
1274
|
const target = Array.isArray(args[0]) ? args[0] : args;
|
|
1265
1275
|
const isTls = this instanceof tls.TLSSocket;
|
|
1266
|
-
|
|
1276
|
+
const hosts = socketTargetHosts(target, isTls);
|
|
1277
|
+
for (const host of hosts) {
|
|
1267
1278
|
const refusal = socketRefusal(host, socketTargetPort(target), isTls);
|
|
1268
1279
|
if (refusal) {
|
|
1269
1280
|
// Preserve the instance and its error listeners; never return a different socket.
|
|
@@ -1271,10 +1282,30 @@ function patchedInstanceConnect(originalConnect) {
|
|
|
1271
1282
|
return this;
|
|
1272
1283
|
}
|
|
1273
1284
|
}
|
|
1285
|
+
if (hosts.length > 0) reportSandboxRefusal(this, `${hosts[0]}:${socketTargetPort(target) ?? ''}`);
|
|
1274
1286
|
return originalConnect.apply(this, args);
|
|
1275
1287
|
};
|
|
1276
1288
|
}
|
|
1277
1289
|
|
|
1290
|
+
// A process of an image run is confined by its sandbox (a native run's Seatbelt) as well as by this decision. Seatbelt
|
|
1291
|
+
// answers a connection it refuses with EPERM and, for Node, writes no kernel record (images lane, measured on Homebrew
|
|
1292
|
+
// Node 24), so a connection this decision let through and the sandbox refused is reported here, as the World's record.
|
|
1293
|
+
// The emit is wrapped rather than an 'error' listener added: a listener would make an unhandled error handled.
|
|
1294
|
+
const SANDBOX_REPORTED = Symbol('volter.inject.sandboxReported');
|
|
1295
|
+
function reportSandboxRefusal(socket, target) {
|
|
1296
|
+
if (!worldDeclarationFromEnv(process.env) || socket[SANDBOX_REPORTED] || typeof socket.emit !== 'function') return;
|
|
1297
|
+
socket[SANDBOX_REPORTED] = true;
|
|
1298
|
+
const emit = socket.emit;
|
|
1299
|
+
socket.emit = function sandboxReportingEmit(event, error, ...rest) {
|
|
1300
|
+
if (event === 'error' && error && error.code === 'EPERM' && !error.worldRefusal) {
|
|
1301
|
+
const answer = sandboxRefusal(target);
|
|
1302
|
+
error.message = `${error.message} (${answer.reason}; ${refusalSide(answer)})`;
|
|
1303
|
+
error.worldRefusal = refusalRecord(answer);
|
|
1304
|
+
}
|
|
1305
|
+
return emit.call(this, event, error, ...rest);
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1278
1309
|
function patchedDatagram(original, connecting) {
|
|
1279
1310
|
return function guardedDatagram(...args) {
|
|
1280
1311
|
let peer;
|
|
@@ -1376,7 +1407,7 @@ function install(mapOverride) {
|
|
|
1376
1407
|
// the claiming twins' doors are read from the start, so a process's first request finds what they claim
|
|
1377
1408
|
for (const vendor of Object.keys(CLAIMERS)) if (MAP[vendor]) claimsAt(MAP[vendor], CLAIMERS[vendor]);
|
|
1378
1409
|
const active = Object.keys(MAP);
|
|
1379
|
-
if (installed || (!active.length && !worldBase() && !MAP[APP_INSTANCE] && !strictEgressEnabled() && !worldNetworkPolicyFromEnv(process.env))) return { active };
|
|
1410
|
+
if (installed || (!active.length && !worldBase() && !MAP[APP_INSTANCE] && !strictEgressEnabled() && !worldNetworkPolicyFromEnv(process.env) && !worldDeclarationFromEnv(process.env))) return { active };
|
|
1380
1411
|
|
|
1381
1412
|
ORIGINALS = {
|
|
1382
1413
|
httpRequest: http.request.bind(http),
|
package/inject.cjs
CHANGED
|
@@ -46,7 +46,7 @@ const Module = require('module');
|
|
|
46
46
|
const { urlToHttpOptions } = require('node:url');
|
|
47
47
|
const claimsRefreshChannel = require('node:diagnostics_channel').channel('volter.inject.claims-refresh');
|
|
48
48
|
const { worldNetworkPolicyFromEnv, isWorldInternalHost } = require('./network-policy.cjs');
|
|
49
|
-
const { OPEN_DECLARATION, decideCapability, refusalRecord, refusalSide } = require('./capability-decision.cjs');
|
|
49
|
+
const { OPEN_DECLARATION, worldDeclarationFromEnv, decideCapability, sandboxRefusal, refusalRecord, refusalSide } = require('./capability-decision.cjs');
|
|
50
50
|
const { createPublicKey, createVerify } = require('crypto');
|
|
51
51
|
|
|
52
52
|
// the vendor host table (VENDOR_HOSTS: a hostname, and a path where two vendors share a host, to the vendor whose twin
|
|
@@ -327,29 +327,36 @@ function strictEgressEnabled() {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
// THE WORLD'S DECISION (docs/contributing/architecture.md, "Capability decisions"): every seam below asks
|
|
330
|
-
// capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one
|
|
331
|
-
// local light guest, whose HTTP leaves through its guest listener;
|
|
332
|
-
//
|
|
330
|
+
// capability-decision.cjs and enforces its answer. This process is a host process, whose declaration is the open one; a
|
|
331
|
+
// local light guest, whose HTTP leaves through its guest listener; or a process of an image run, which carries its
|
|
332
|
+
// image's declaration (VOLTER_WORLD_DECLARATION) and is confined from the host's network. What the World grants is its
|
|
333
|
+
// published policy. The seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
|
|
333
334
|
function worldGrant() {
|
|
334
335
|
let policy; let unreadable;
|
|
335
336
|
try { policy = worldNetworkPolicyFromEnv(process.env); } catch (error) { unreadable = error.message; }
|
|
336
337
|
return { policy, strict: strictEgressEnabled(), unreadable };
|
|
337
338
|
}
|
|
339
|
+
/** This process's run: its declaration, and whether it is confined from the host's network. */
|
|
340
|
+
function worldRun() {
|
|
341
|
+
const declared = worldDeclarationFromEnv(process.env);
|
|
342
|
+
return declared ? { guest: true, declaration: declared } : { guest: !!localGuestProxy(), declaration: OPEN_DECLARATION };
|
|
343
|
+
}
|
|
338
344
|
function decide(url, transport, facts = {}, port) {
|
|
339
|
-
const
|
|
345
|
+
const { guest, declaration } = worldRun();
|
|
340
346
|
return decideCapability({ kind: 'egress', url, transport, port },
|
|
341
|
-
{ run: { guest
|
|
347
|
+
{ run: { guest }, declaration, grant: worldGrant(), facts: { guestProxy: localGuestProxy(), ...facts } });
|
|
342
348
|
}
|
|
343
349
|
/** What an HTTP seam observes about `url`. A local guest's request to one of the World's doors, or not over HTTP(S), is
|
|
344
350
|
* not mediated by its guest listener. */
|
|
345
351
|
function httpFacts(url, pathname) {
|
|
346
352
|
if (!url) return {};
|
|
347
353
|
const guestProxy = localGuestProxy();
|
|
354
|
+
const proxy = worldProxy();
|
|
348
355
|
return {
|
|
349
356
|
twin: resolveTwin(url.hostname, MAP, pathname === undefined ? url.pathname : pathname),
|
|
350
357
|
twinnedHostVendors: twinnedHostVendors(url.hostname, MAP),
|
|
351
358
|
...(guestProxy && !/^https?:$/.test(url.protocol) ? { guestProxy: null } : {}),
|
|
352
|
-
endpoint:
|
|
359
|
+
endpoint: worldRun().guest && (addressedToWorld(url) || explicitTwinEndpoint(url) || (proxy && url.origin === proxy.origin)) ? { kind: 'door' } : null,
|
|
353
360
|
};
|
|
354
361
|
}
|
|
355
362
|
/** A refusal as an Error: the seam's own message, the side that refused, and the World's refusal record. */
|
|
@@ -489,6 +496,10 @@ function worldBase() {
|
|
|
489
496
|
}
|
|
490
497
|
// The local Substrate agent keeps host control routing; its product commands select the
|
|
491
498
|
// existing guest listener. Route original destinations through that listener before rewriting.
|
|
499
|
+
/** The World proxy this process is pointed at, or null. */
|
|
500
|
+
function worldProxy() {
|
|
501
|
+
try { return new URL(process.env.VOLTER_WORLD_PROXY || ''); } catch { return null; }
|
|
502
|
+
}
|
|
492
503
|
function localGuestProxy() {
|
|
493
504
|
const guest = process.env.VOLTER_WORLD_GUEST_PROXY;
|
|
494
505
|
if (!guest || process.env.VOLTER_WORLD_PROXY !== guest) return null;
|
|
@@ -1197,10 +1208,9 @@ function socketRefusal(hostname, port, isTls, datagram = false) {
|
|
|
1197
1208
|
const authority = `${isIP(host) === 6 ? `[${host}]` : host}${port !== undefined && !(isTls && port === 443) ? `:${port}` : ''}`;
|
|
1198
1209
|
const url = (() => { try { return new URL(`${isTls ? 'https' : 'http'}://${authority}/`); } catch { return null; } })();
|
|
1199
1210
|
if (!url) return null;
|
|
1200
|
-
const guestProxy = localGuestProxy();
|
|
1201
1211
|
const internal = isLocalOrPrivateHost(host);
|
|
1202
1212
|
const facts = internal
|
|
1203
|
-
? { endpoint:
|
|
1213
|
+
? { endpoint: worldRun().guest && guestDoorAt(host, port) ? { kind: 'door' } : null }
|
|
1204
1214
|
: { twin: resolveTwin(host, MAP), twinnedHostVendors: twinnedHostVendors(host, MAP), twinOrigin: isTwinOriginHost(host) };
|
|
1205
1215
|
const answer = decide(url, datagram ? 'datagram' : 'socket', facts, port);
|
|
1206
1216
|
if (answer.outcome !== 'refuse') return null;
|
|
@@ -1222,9 +1232,9 @@ function socketRefusal(hostname, port, isTls, datagram = false) {
|
|
|
1222
1232
|
return refusalError(answer, message);
|
|
1223
1233
|
}
|
|
1224
1234
|
|
|
1225
|
-
/** One of the World's doors a
|
|
1235
|
+
/** One of the World's doors a guest connects to directly: its proxy (a local guest's listener), the World's origin, a twin. */
|
|
1226
1236
|
function guestDoorAt(host, port) {
|
|
1227
|
-
const doors = [localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
|
|
1237
|
+
const doors = [worldProxy(), localGuestProxy(), worldBase() && new URL(worldBase().origin), ...Object.values(MAP).map(origin => { try { return new URL(origin); } catch { return null; } })].filter(Boolean);
|
|
1228
1238
|
return doors.some(door => door.hostname.replace(/^\[|\]$/g, '') === host.replace(/^\[|\]$/g, '') && Number(door.port || (door.protocol === 'https:' ? 443 : 80)) === port);
|
|
1229
1239
|
}
|
|
1230
1240
|
|
|
@@ -1263,7 +1273,8 @@ function patchedInstanceConnect(originalConnect) {
|
|
|
1263
1273
|
return function guardedInstanceConnect(...args) {
|
|
1264
1274
|
const target = Array.isArray(args[0]) ? args[0] : args;
|
|
1265
1275
|
const isTls = this instanceof tls.TLSSocket;
|
|
1266
|
-
|
|
1276
|
+
const hosts = socketTargetHosts(target, isTls);
|
|
1277
|
+
for (const host of hosts) {
|
|
1267
1278
|
const refusal = socketRefusal(host, socketTargetPort(target), isTls);
|
|
1268
1279
|
if (refusal) {
|
|
1269
1280
|
// Preserve the instance and its error listeners; never return a different socket.
|
|
@@ -1271,10 +1282,30 @@ function patchedInstanceConnect(originalConnect) {
|
|
|
1271
1282
|
return this;
|
|
1272
1283
|
}
|
|
1273
1284
|
}
|
|
1285
|
+
if (hosts.length > 0) reportSandboxRefusal(this, `${hosts[0]}:${socketTargetPort(target) ?? ''}`);
|
|
1274
1286
|
return originalConnect.apply(this, args);
|
|
1275
1287
|
};
|
|
1276
1288
|
}
|
|
1277
1289
|
|
|
1290
|
+
// A process of an image run is confined by its sandbox (a native run's Seatbelt) as well as by this decision. Seatbelt
|
|
1291
|
+
// answers a connection it refuses with EPERM and, for Node, writes no kernel record (images lane, measured on Homebrew
|
|
1292
|
+
// Node 24), so a connection this decision let through and the sandbox refused is reported here, as the World's record.
|
|
1293
|
+
// The emit is wrapped rather than an 'error' listener added: a listener would make an unhandled error handled.
|
|
1294
|
+
const SANDBOX_REPORTED = Symbol('volter.inject.sandboxReported');
|
|
1295
|
+
function reportSandboxRefusal(socket, target) {
|
|
1296
|
+
if (!worldDeclarationFromEnv(process.env) || socket[SANDBOX_REPORTED] || typeof socket.emit !== 'function') return;
|
|
1297
|
+
socket[SANDBOX_REPORTED] = true;
|
|
1298
|
+
const emit = socket.emit;
|
|
1299
|
+
socket.emit = function sandboxReportingEmit(event, error, ...rest) {
|
|
1300
|
+
if (event === 'error' && error && error.code === 'EPERM' && !error.worldRefusal) {
|
|
1301
|
+
const answer = sandboxRefusal(target);
|
|
1302
|
+
error.message = `${error.message} (${answer.reason}; ${refusalSide(answer)})`;
|
|
1303
|
+
error.worldRefusal = refusalRecord(answer);
|
|
1304
|
+
}
|
|
1305
|
+
return emit.call(this, event, error, ...rest);
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1278
1309
|
function patchedDatagram(original, connecting) {
|
|
1279
1310
|
return function guardedDatagram(...args) {
|
|
1280
1311
|
let peer;
|
|
@@ -1376,7 +1407,7 @@ function install(mapOverride) {
|
|
|
1376
1407
|
// the claiming twins' doors are read from the start, so a process's first request finds what they claim
|
|
1377
1408
|
for (const vendor of Object.keys(CLAIMERS)) if (MAP[vendor]) claimsAt(MAP[vendor], CLAIMERS[vendor]);
|
|
1378
1409
|
const active = Object.keys(MAP);
|
|
1379
|
-
if (installed || (!active.length && !worldBase() && !MAP[APP_INSTANCE] && !strictEgressEnabled() && !worldNetworkPolicyFromEnv(process.env))) return { active };
|
|
1410
|
+
if (installed || (!active.length && !worldBase() && !MAP[APP_INSTANCE] && !strictEgressEnabled() && !worldNetworkPolicyFromEnv(process.env) && !worldDeclarationFromEnv(process.env))) return { active };
|
|
1380
1411
|
|
|
1381
1412
|
ORIGINALS = {
|
|
1382
1413
|
httpRequest: http.request.bind(http),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.67",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|