@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.
@@ -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
- 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}`, declaration);
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 && facts.guestProxy) {
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 whose writes outlive the run: declared by the guest; the World has no grant for it yet. */
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 persistent`, declaration);
193
- return refuse('files.persist', request.path, 'grant', 'persist-ungranted', `this World has no grant for persistent paths (${request.path})`, declaration);
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
- 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}`, declaration);
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 && facts.guestProxy) {
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 whose writes outlive the run: declared by the guest; the World has no grant for it yet. */
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 persistent`, declaration);
193
- return refuse('files.persist', request.path, 'grant', 'persist-ungranted', `this World has no grant for persistent paths (${request.path})`, declaration);
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, or a
331
- // local light guest, whose HTTP leaves through its guest listener; what the World grants is its published policy. The
332
- // seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
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 guestProxy = localGuestProxy();
345
+ const { guest, declaration } = worldRun();
340
346
  return decideCapability({ kind: 'egress', url, transport, port },
341
- { run: { guest: !!guestProxy }, declaration: OPEN_DECLARATION, grant: worldGrant(), facts: { guestProxy, ...facts } });
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: guestProxy && (addressedToWorld(url) || explicitTwinEndpoint(url) || url.origin === guestProxy.origin) ? { kind: 'door' } : null,
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: guestProxy && guestDoorAt(host, port) ? { kind: 'door' } : null }
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 local guest connects to directly: its guest listener, the World's origin, a twin. */
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
- for (const host of socketTargetHosts(target, isTls)) {
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, or a
331
- // local light guest, whose HTTP leaves through its guest listener; what the World grants is its published policy. The
332
- // seams supply what they observe: the twin route, the vendors sharing a host, the World's doors.
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 guestProxy = localGuestProxy();
345
+ const { guest, declaration } = worldRun();
340
346
  return decideCapability({ kind: 'egress', url, transport, port },
341
- { run: { guest: !!guestProxy }, declaration: OPEN_DECLARATION, grant: worldGrant(), facts: { guestProxy, ...facts } });
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: guestProxy && (addressedToWorld(url) || explicitTwinEndpoint(url) || url.origin === guestProxy.origin) ? { kind: 'door' } : null,
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: guestProxy && guestDoorAt(host, port) ? { kind: 'door' } : null }
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 local guest connects to directly: its guest listener, the World's origin, a twin. */
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
- for (const host of socketTargetHosts(target, isTls)) {
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.65",
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",