@volter/world-core 3.0.69 → 3.0.70
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
CHANGED
|
@@ -8,7 +8,7 @@ const { allowsWorldNetworkEgress, isWorldInternalHost, worldNetworkNeedsPathInsp
|
|
|
8
8
|
|
|
9
9
|
/** The declaration of a guest with no declaration format: an ordinary host process, which can reach what the host can.
|
|
10
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 }) });
|
|
11
|
+
const OPEN_DECLARATION = Object.freeze({ network: Object.freeze({ egress: '*', services: '*', host: true }), files: Object.freeze({ host: true }) });
|
|
12
12
|
|
|
13
13
|
/** What an unreadable declaration, or one bound to another World, declares: nothing. */
|
|
14
14
|
const EMPTY_DECLARATION = Object.freeze({ network: Object.freeze({}) });
|
|
@@ -216,6 +216,44 @@ function decideEgress(request, context) {
|
|
|
216
216
|
return pass(capability, target, policy && worldNetworkUsesPassthrough(policy, url) ? 'passthrough' : 'direct');
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/** `path` as a normalized absolute POSIX path, or null: `.` and `..` resolved, no symlink followed (that is I/O). */
|
|
220
|
+
function normalPath(path) {
|
|
221
|
+
if (typeof path !== 'string' || !path.startsWith('/') || path.includes('\0')) return null;
|
|
222
|
+
const parts = [];
|
|
223
|
+
for (const segment of path.split('/')) {
|
|
224
|
+
if (!segment || segment === '.') continue;
|
|
225
|
+
if (segment === '..') parts.pop(); else parts.push(segment);
|
|
226
|
+
}
|
|
227
|
+
return `/${parts.join('/')}`;
|
|
228
|
+
}
|
|
229
|
+
function within(path, root) {
|
|
230
|
+
const base = normalPath(root);
|
|
231
|
+
return !!base && (path === base || path.startsWith(base === '/' ? '/' : `${base}/`));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A file the guest reads (`request.path`). It reads its own tree (`files.tree`, implicit for an image) at `facts.tree`,
|
|
236
|
+
* the World's provided files (`facts.worldFiles`), the measured system paths (`facts.system`) and its declared mounts
|
|
237
|
+
* (`files.mounts`) inside the World's root (`facts.worldRoot`); a host process reads the host's files (`files.host`).
|
|
238
|
+
*/
|
|
239
|
+
function decideRead(request, { declaration, facts = {} }) {
|
|
240
|
+
const capability = 'files.read';
|
|
241
|
+
const target = String(request.path);
|
|
242
|
+
const path = normalPath(target);
|
|
243
|
+
const files = (declaration && declaration.files) || {};
|
|
244
|
+
if (!path) return refuse(capability, target, 'declaration', 'read-invalid', `${target} is not an absolute path`, declaration);
|
|
245
|
+
if (files.host === true) return pass(capability, path, 'host');
|
|
246
|
+
if (files.tree === true && facts.tree && within(path, facts.tree)) return pass(capability, path, 'own-tree');
|
|
247
|
+
if ((facts.worldFiles || []).some((root) => within(path, root))) return pass(capability, path, 'world');
|
|
248
|
+
if ((facts.system || []).some((root) => within(path, root))) return pass(capability, path, 'system');
|
|
249
|
+
const mount = (Array.isArray(files.mounts) ? files.mounts : []).find((root) => within(path, root));
|
|
250
|
+
if (mount) {
|
|
251
|
+
return facts.worldRoot && within(normalPath(mount) || '', facts.worldRoot) ? pass(capability, path, 'mount')
|
|
252
|
+
: refuse(capability, path, 'grant', 'mount-outside-world', `the mount ${mount} is outside this World's folder: no World grants a host path it does not own; mount it from inside the World's folder`, declaration);
|
|
253
|
+
}
|
|
254
|
+
return refuse(capability, path, 'declaration', 'read-undeclared', `in Docker this container cannot read ${path}: it sees its own filesystem, the World's files and its declared mounts; declare a mount`, declaration);
|
|
255
|
+
}
|
|
256
|
+
|
|
219
257
|
/** Whether the World's build-phase grant lists `url` itself, which then wins over a twin of its host. */
|
|
220
258
|
function publicBuildGrant(grant, url) {
|
|
221
259
|
return grant.strict !== true && !!grant.policy && grant.policy.phase === 'build' && allowsWorldNetworkEgress(grant.policy, url);
|
|
@@ -231,14 +269,15 @@ function decidePersist(request, { declaration }) {
|
|
|
231
269
|
}
|
|
232
270
|
|
|
233
271
|
/**
|
|
234
|
-
* A connection the decision let through that
|
|
235
|
-
* the
|
|
236
|
-
* images CLI's kernel-log reporter for other programs
|
|
272
|
+
* A connection the decision let through that a native image run's Seatbelt profile refused (EPERM): the native run
|
|
273
|
+
* admits only the World proxy and loopback, so the World sees each request, and a client that dials out directly fails
|
|
274
|
+
* there. One text for every reporter: the injector for Node, the images CLI's kernel-log reporter for other programs
|
|
275
|
+
* (whose target is `*:<port>`: the kernel masks the address). The profile reproduces Docker; it is no containment.
|
|
237
276
|
*/
|
|
238
277
|
function sandboxRefusal(target) {
|
|
239
|
-
return refuse('network.egress', target, 'transport', '
|
|
240
|
-
`the run
|
|
241
|
-
+ '
|
|
278
|
+
return refuse('network.egress', target, 'transport', 'native-direct',
|
|
279
|
+
`the native run reaches ${target} only through the World proxy, and this client dialled it directly; `
|
|
280
|
+
+ 'send it through the World proxy (HTTPS_PROXY)');
|
|
242
281
|
}
|
|
243
282
|
|
|
244
283
|
/**
|
|
@@ -249,12 +288,14 @@ function decideCapability(request, context) {
|
|
|
249
288
|
if (request.kind === 'egress') return decideEgress(request, context);
|
|
250
289
|
if (request.kind === 'loopback') return decideLoopback(request, context);
|
|
251
290
|
if (request.kind === 'persist') return decidePersist(request, context);
|
|
291
|
+
if (request.kind === 'read') return decideRead(request, context);
|
|
252
292
|
throw new Error(`Unknown capability request: ${request.kind}`);
|
|
253
293
|
}
|
|
254
294
|
|
|
255
295
|
/**
|
|
256
296
|
* The same rules evaluated up front, for an enforcer that renders a profile instead of asking per connection (a native
|
|
257
|
-
* image run's Seatbelt profile). `world.endpoints`: [{ host, port, kind: 'door' | 'service', name }].
|
|
297
|
+
* image run's Seatbelt profile). `world.endpoints`: [{ host, port, kind: 'door' | 'service', name }]; `world.files`:
|
|
298
|
+
* { tree, worldFiles, system, worldRoot }, the read facts.
|
|
258
299
|
* Egress is `proxy` when declared: twin routing and the per-request listing are the World proxy's, which a profile
|
|
259
300
|
* forces traffic through; `none` when the guest declares none.
|
|
260
301
|
*/
|
|
@@ -280,7 +321,20 @@ function decideCapabilitySet(declaration, grant = {}, world = {}) {
|
|
|
280
321
|
const answer = decidePersist({ path }, context);
|
|
281
322
|
if (answer.outcome === 'pass') persist.push(path); else refusals.push(answer);
|
|
282
323
|
}
|
|
283
|
-
|
|
324
|
+
const facts = world.files || {};
|
|
325
|
+
const declared = (declaration && declaration.files) || {};
|
|
326
|
+
const read = [];
|
|
327
|
+
if (declared.host === true) read.push({ path: '/', kind: 'host' });
|
|
328
|
+
else {
|
|
329
|
+
if (declared.tree === true && facts.tree) read.push({ path: normalPath(facts.tree), kind: 'own-tree' });
|
|
330
|
+
for (const root of facts.worldFiles || []) read.push({ path: normalPath(root), kind: 'world' });
|
|
331
|
+
for (const root of facts.system || []) read.push({ path: normalPath(root), kind: 'system' });
|
|
332
|
+
for (const mount of Array.isArray(declared.mounts) ? declared.mounts : []) {
|
|
333
|
+
const answer = decideRead({ path: mount }, { declaration, facts: { worldRoot: facts.worldRoot } });
|
|
334
|
+
if (answer.outcome === 'pass') read.push({ path: normalPath(mount), kind: 'mount' }); else refusals.push(answer);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({ network: Object.freeze({ egress, loopback, listen, endpoints }), files: Object.freeze({ persist, read: Object.freeze(read.filter((r) => r.path)) }), refusals: Object.freeze(refusals) });
|
|
284
338
|
}
|
|
285
339
|
|
|
286
340
|
module.exports = { OPEN_DECLARATION, EMPTY_DECLARATION, WORLD_DECLARATION_ENV, GRANT_FIELDS, worldDeclarationFromEnv, publicBuildGrant, decideCapability, decideCapabilitySet, sandboxRefusal, refusalRecord, refusalSide };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorldNetworkPolicy } from './network-policy.cjs';
|
|
2
2
|
|
|
3
|
-
export type CapabilityName = 'network.egress' | 'network.ownLoopback' | 'network.services' | 'network.host' | 'files.persist';
|
|
3
|
+
export type CapabilityName = 'network.egress' | 'network.ownLoopback' | 'network.services' | 'network.host' | 'files.read' | 'files.persist';
|
|
4
4
|
export type RefusalSide = 'declaration' | 'grant' | 'transport' | 'twin';
|
|
5
5
|
export type DeclaredBy = { readonly capability: CapabilityName; readonly file: string; readonly line: number };
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ export interface CapabilityDeclaration {
|
|
|
13
13
|
readonly services?: '*' | readonly string[];
|
|
14
14
|
readonly host?: boolean;
|
|
15
15
|
};
|
|
16
|
-
readonly files?: { readonly persist?: readonly string[] };
|
|
16
|
+
readonly files?: { readonly persist?: readonly string[]; readonly tree?: boolean; readonly mounts?: readonly string[]; readonly host?: boolean };
|
|
17
17
|
readonly derivedFrom?: readonly DeclaredBy[];
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -27,16 +27,22 @@ export interface CapabilityFacts {
|
|
|
27
27
|
readonly guestProxy?: URL | null;
|
|
28
28
|
readonly endpoint?: Pick<WorldEndpoint, 'kind' | 'name'> | null;
|
|
29
29
|
readonly ownListener?: boolean;
|
|
30
|
+
/** read facts: the run's tree, the World's provided files, the measured system paths, the World's root */
|
|
31
|
+
readonly tree?: string;
|
|
32
|
+
readonly worldFiles?: readonly string[];
|
|
33
|
+
readonly system?: readonly string[];
|
|
34
|
+
readonly worldRoot?: string;
|
|
30
35
|
}
|
|
31
36
|
export interface CapabilityContext { readonly run?: { readonly guest?: boolean }; readonly declaration?: CapabilityDeclaration; readonly grant?: WorldGrant; readonly facts?: CapabilityFacts }
|
|
32
37
|
|
|
33
38
|
export type CapabilityRequest =
|
|
34
39
|
| { readonly kind: 'egress'; readonly url: string | URL | null; readonly method?: string; readonly transport?: 'http' | 'upgrade' | 'http2' | 'dispatch' | 'socket' | 'datagram'; readonly port?: number }
|
|
35
40
|
| { readonly kind: 'loopback'; readonly host: string; readonly port?: number; readonly transport?: 'tcp' | 'udp' }
|
|
36
|
-
| { readonly kind: 'persist'; readonly path: string }
|
|
41
|
+
| { readonly kind: 'persist'; readonly path: string }
|
|
42
|
+
| { readonly kind: 'read'; readonly path: string };
|
|
37
43
|
|
|
38
44
|
export type CapabilityAnswer =
|
|
39
|
-
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' }
|
|
45
|
+
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' | 'host' | 'own-tree' | 'world' | 'system' | 'mount' }
|
|
40
46
|
| { readonly outcome: 'twin'; readonly capability: CapabilityName; readonly target: string; readonly twin: { readonly vendor: string; readonly origin: string } }
|
|
41
47
|
| { readonly outcome: 'refuse'; readonly capability: CapabilityName; readonly target: string; readonly missing: RefusalSide; readonly rule: string; readonly reason: string; readonly grantField?: string; readonly declaredBy?: DeclaredBy; readonly see?: string };
|
|
42
48
|
|
|
@@ -44,7 +50,7 @@ export interface WorldRefusalRecord { readonly kind: 'volter-world-refusal'; rea
|
|
|
44
50
|
|
|
45
51
|
export interface CapabilitySet {
|
|
46
52
|
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[] };
|
|
53
|
+
readonly files: { readonly persist: readonly string[]; readonly read: readonly { readonly path: string; readonly kind: 'host' | 'own-tree' | 'world' | 'system' | 'mount' }[] };
|
|
48
54
|
readonly refusals: readonly Extract<CapabilityAnswer, { outcome: 'refuse' }>[];
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -56,6 +62,6 @@ export function sandboxRefusal(target: string): Extract<CapabilityAnswer, { outc
|
|
|
56
62
|
export const GRANT_FIELDS: Readonly<Partial<Record<CapabilityName, string>>>;
|
|
57
63
|
export function publicBuildGrant(grant: WorldGrant, url: URL): boolean;
|
|
58
64
|
export function decideCapability(request: CapabilityRequest, context: CapabilityContext): CapabilityAnswer;
|
|
59
|
-
export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[] }): CapabilitySet;
|
|
65
|
+
export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[]; readonly files?: { readonly tree?: string; readonly worldFiles?: readonly string[]; readonly system?: readonly string[]; readonly worldRoot?: string } }): CapabilitySet;
|
|
60
66
|
export function refusalRecord(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): WorldRefusalRecord;
|
|
61
67
|
export function refusalSide(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): string;
|
|
@@ -8,7 +8,7 @@ const { allowsWorldNetworkEgress, isWorldInternalHost, worldNetworkNeedsPathInsp
|
|
|
8
8
|
|
|
9
9
|
/** The declaration of a guest with no declaration format: an ordinary host process, which can reach what the host can.
|
|
10
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 }) });
|
|
11
|
+
const OPEN_DECLARATION = Object.freeze({ network: Object.freeze({ egress: '*', services: '*', host: true }), files: Object.freeze({ host: true }) });
|
|
12
12
|
|
|
13
13
|
/** What an unreadable declaration, or one bound to another World, declares: nothing. */
|
|
14
14
|
const EMPTY_DECLARATION = Object.freeze({ network: Object.freeze({}) });
|
|
@@ -216,6 +216,44 @@ function decideEgress(request, context) {
|
|
|
216
216
|
return pass(capability, target, policy && worldNetworkUsesPassthrough(policy, url) ? 'passthrough' : 'direct');
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/** `path` as a normalized absolute POSIX path, or null: `.` and `..` resolved, no symlink followed (that is I/O). */
|
|
220
|
+
function normalPath(path) {
|
|
221
|
+
if (typeof path !== 'string' || !path.startsWith('/') || path.includes('\0')) return null;
|
|
222
|
+
const parts = [];
|
|
223
|
+
for (const segment of path.split('/')) {
|
|
224
|
+
if (!segment || segment === '.') continue;
|
|
225
|
+
if (segment === '..') parts.pop(); else parts.push(segment);
|
|
226
|
+
}
|
|
227
|
+
return `/${parts.join('/')}`;
|
|
228
|
+
}
|
|
229
|
+
function within(path, root) {
|
|
230
|
+
const base = normalPath(root);
|
|
231
|
+
return !!base && (path === base || path.startsWith(base === '/' ? '/' : `${base}/`));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A file the guest reads (`request.path`). It reads its own tree (`files.tree`, implicit for an image) at `facts.tree`,
|
|
236
|
+
* the World's provided files (`facts.worldFiles`), the measured system paths (`facts.system`) and its declared mounts
|
|
237
|
+
* (`files.mounts`) inside the World's root (`facts.worldRoot`); a host process reads the host's files (`files.host`).
|
|
238
|
+
*/
|
|
239
|
+
function decideRead(request, { declaration, facts = {} }) {
|
|
240
|
+
const capability = 'files.read';
|
|
241
|
+
const target = String(request.path);
|
|
242
|
+
const path = normalPath(target);
|
|
243
|
+
const files = (declaration && declaration.files) || {};
|
|
244
|
+
if (!path) return refuse(capability, target, 'declaration', 'read-invalid', `${target} is not an absolute path`, declaration);
|
|
245
|
+
if (files.host === true) return pass(capability, path, 'host');
|
|
246
|
+
if (files.tree === true && facts.tree && within(path, facts.tree)) return pass(capability, path, 'own-tree');
|
|
247
|
+
if ((facts.worldFiles || []).some((root) => within(path, root))) return pass(capability, path, 'world');
|
|
248
|
+
if ((facts.system || []).some((root) => within(path, root))) return pass(capability, path, 'system');
|
|
249
|
+
const mount = (Array.isArray(files.mounts) ? files.mounts : []).find((root) => within(path, root));
|
|
250
|
+
if (mount) {
|
|
251
|
+
return facts.worldRoot && within(normalPath(mount) || '', facts.worldRoot) ? pass(capability, path, 'mount')
|
|
252
|
+
: refuse(capability, path, 'grant', 'mount-outside-world', `the mount ${mount} is outside this World's folder: no World grants a host path it does not own; mount it from inside the World's folder`, declaration);
|
|
253
|
+
}
|
|
254
|
+
return refuse(capability, path, 'declaration', 'read-undeclared', `in Docker this container cannot read ${path}: it sees its own filesystem, the World's files and its declared mounts; declare a mount`, declaration);
|
|
255
|
+
}
|
|
256
|
+
|
|
219
257
|
/** Whether the World's build-phase grant lists `url` itself, which then wins over a twin of its host. */
|
|
220
258
|
function publicBuildGrant(grant, url) {
|
|
221
259
|
return grant.strict !== true && !!grant.policy && grant.policy.phase === 'build' && allowsWorldNetworkEgress(grant.policy, url);
|
|
@@ -231,14 +269,15 @@ function decidePersist(request, { declaration }) {
|
|
|
231
269
|
}
|
|
232
270
|
|
|
233
271
|
/**
|
|
234
|
-
* A connection the decision let through that
|
|
235
|
-
* the
|
|
236
|
-
* images CLI's kernel-log reporter for other programs
|
|
272
|
+
* A connection the decision let through that a native image run's Seatbelt profile refused (EPERM): the native run
|
|
273
|
+
* admits only the World proxy and loopback, so the World sees each request, and a client that dials out directly fails
|
|
274
|
+
* there. One text for every reporter: the injector for Node, the images CLI's kernel-log reporter for other programs
|
|
275
|
+
* (whose target is `*:<port>`: the kernel masks the address). The profile reproduces Docker; it is no containment.
|
|
237
276
|
*/
|
|
238
277
|
function sandboxRefusal(target) {
|
|
239
|
-
return refuse('network.egress', target, 'transport', '
|
|
240
|
-
`the run
|
|
241
|
-
+ '
|
|
278
|
+
return refuse('network.egress', target, 'transport', 'native-direct',
|
|
279
|
+
`the native run reaches ${target} only through the World proxy, and this client dialled it directly; `
|
|
280
|
+
+ 'send it through the World proxy (HTTPS_PROXY)');
|
|
242
281
|
}
|
|
243
282
|
|
|
244
283
|
/**
|
|
@@ -249,12 +288,14 @@ function decideCapability(request, context) {
|
|
|
249
288
|
if (request.kind === 'egress') return decideEgress(request, context);
|
|
250
289
|
if (request.kind === 'loopback') return decideLoopback(request, context);
|
|
251
290
|
if (request.kind === 'persist') return decidePersist(request, context);
|
|
291
|
+
if (request.kind === 'read') return decideRead(request, context);
|
|
252
292
|
throw new Error(`Unknown capability request: ${request.kind}`);
|
|
253
293
|
}
|
|
254
294
|
|
|
255
295
|
/**
|
|
256
296
|
* The same rules evaluated up front, for an enforcer that renders a profile instead of asking per connection (a native
|
|
257
|
-
* image run's Seatbelt profile). `world.endpoints`: [{ host, port, kind: 'door' | 'service', name }].
|
|
297
|
+
* image run's Seatbelt profile). `world.endpoints`: [{ host, port, kind: 'door' | 'service', name }]; `world.files`:
|
|
298
|
+
* { tree, worldFiles, system, worldRoot }, the read facts.
|
|
258
299
|
* Egress is `proxy` when declared: twin routing and the per-request listing are the World proxy's, which a profile
|
|
259
300
|
* forces traffic through; `none` when the guest declares none.
|
|
260
301
|
*/
|
|
@@ -280,7 +321,20 @@ function decideCapabilitySet(declaration, grant = {}, world = {}) {
|
|
|
280
321
|
const answer = decidePersist({ path }, context);
|
|
281
322
|
if (answer.outcome === 'pass') persist.push(path); else refusals.push(answer);
|
|
282
323
|
}
|
|
283
|
-
|
|
324
|
+
const facts = world.files || {};
|
|
325
|
+
const declared = (declaration && declaration.files) || {};
|
|
326
|
+
const read = [];
|
|
327
|
+
if (declared.host === true) read.push({ path: '/', kind: 'host' });
|
|
328
|
+
else {
|
|
329
|
+
if (declared.tree === true && facts.tree) read.push({ path: normalPath(facts.tree), kind: 'own-tree' });
|
|
330
|
+
for (const root of facts.worldFiles || []) read.push({ path: normalPath(root), kind: 'world' });
|
|
331
|
+
for (const root of facts.system || []) read.push({ path: normalPath(root), kind: 'system' });
|
|
332
|
+
for (const mount of Array.isArray(declared.mounts) ? declared.mounts : []) {
|
|
333
|
+
const answer = decideRead({ path: mount }, { declaration, facts: { worldRoot: facts.worldRoot } });
|
|
334
|
+
if (answer.outcome === 'pass') read.push({ path: normalPath(mount), kind: 'mount' }); else refusals.push(answer);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({ network: Object.freeze({ egress, loopback, listen, endpoints }), files: Object.freeze({ persist, read: Object.freeze(read.filter((r) => r.path)) }), refusals: Object.freeze(refusals) });
|
|
284
338
|
}
|
|
285
339
|
|
|
286
340
|
module.exports = { OPEN_DECLARATION, EMPTY_DECLARATION, WORLD_DECLARATION_ENV, GRANT_FIELDS, worldDeclarationFromEnv, publicBuildGrant, decideCapability, decideCapabilitySet, sandboxRefusal, refusalRecord, refusalSide };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorldNetworkPolicy } from './network-policy.cjs';
|
|
2
2
|
|
|
3
|
-
export type CapabilityName = 'network.egress' | 'network.ownLoopback' | 'network.services' | 'network.host' | 'files.persist';
|
|
3
|
+
export type CapabilityName = 'network.egress' | 'network.ownLoopback' | 'network.services' | 'network.host' | 'files.read' | 'files.persist';
|
|
4
4
|
export type RefusalSide = 'declaration' | 'grant' | 'transport' | 'twin';
|
|
5
5
|
export type DeclaredBy = { readonly capability: CapabilityName; readonly file: string; readonly line: number };
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ export interface CapabilityDeclaration {
|
|
|
13
13
|
readonly services?: '*' | readonly string[];
|
|
14
14
|
readonly host?: boolean;
|
|
15
15
|
};
|
|
16
|
-
readonly files?: { readonly persist?: readonly string[] };
|
|
16
|
+
readonly files?: { readonly persist?: readonly string[]; readonly tree?: boolean; readonly mounts?: readonly string[]; readonly host?: boolean };
|
|
17
17
|
readonly derivedFrom?: readonly DeclaredBy[];
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -27,16 +27,22 @@ export interface CapabilityFacts {
|
|
|
27
27
|
readonly guestProxy?: URL | null;
|
|
28
28
|
readonly endpoint?: Pick<WorldEndpoint, 'kind' | 'name'> | null;
|
|
29
29
|
readonly ownListener?: boolean;
|
|
30
|
+
/** read facts: the run's tree, the World's provided files, the measured system paths, the World's root */
|
|
31
|
+
readonly tree?: string;
|
|
32
|
+
readonly worldFiles?: readonly string[];
|
|
33
|
+
readonly system?: readonly string[];
|
|
34
|
+
readonly worldRoot?: string;
|
|
30
35
|
}
|
|
31
36
|
export interface CapabilityContext { readonly run?: { readonly guest?: boolean }; readonly declaration?: CapabilityDeclaration; readonly grant?: WorldGrant; readonly facts?: CapabilityFacts }
|
|
32
37
|
|
|
33
38
|
export type CapabilityRequest =
|
|
34
39
|
| { readonly kind: 'egress'; readonly url: string | URL | null; readonly method?: string; readonly transport?: 'http' | 'upgrade' | 'http2' | 'dispatch' | 'socket' | 'datagram'; readonly port?: number }
|
|
35
40
|
| { readonly kind: 'loopback'; readonly host: string; readonly port?: number; readonly transport?: 'tcp' | 'udp' }
|
|
36
|
-
| { readonly kind: 'persist'; readonly path: string }
|
|
41
|
+
| { readonly kind: 'persist'; readonly path: string }
|
|
42
|
+
| { readonly kind: 'read'; readonly path: string };
|
|
37
43
|
|
|
38
44
|
export type CapabilityAnswer =
|
|
39
|
-
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' }
|
|
45
|
+
| { readonly outcome: 'pass'; readonly capability: CapabilityName; readonly target: string; readonly via: 'direct' | 'inspected' | 'passthrough' | 'guest-proxy' | 'run-scoped' | 'host' | 'own-tree' | 'world' | 'system' | 'mount' }
|
|
40
46
|
| { readonly outcome: 'twin'; readonly capability: CapabilityName; readonly target: string; readonly twin: { readonly vendor: string; readonly origin: string } }
|
|
41
47
|
| { readonly outcome: 'refuse'; readonly capability: CapabilityName; readonly target: string; readonly missing: RefusalSide; readonly rule: string; readonly reason: string; readonly grantField?: string; readonly declaredBy?: DeclaredBy; readonly see?: string };
|
|
42
48
|
|
|
@@ -44,7 +50,7 @@ export interface WorldRefusalRecord { readonly kind: 'volter-world-refusal'; rea
|
|
|
44
50
|
|
|
45
51
|
export interface CapabilitySet {
|
|
46
52
|
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[] };
|
|
53
|
+
readonly files: { readonly persist: readonly string[]; readonly read: readonly { readonly path: string; readonly kind: 'host' | 'own-tree' | 'world' | 'system' | 'mount' }[] };
|
|
48
54
|
readonly refusals: readonly Extract<CapabilityAnswer, { outcome: 'refuse' }>[];
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -56,6 +62,6 @@ export function sandboxRefusal(target: string): Extract<CapabilityAnswer, { outc
|
|
|
56
62
|
export const GRANT_FIELDS: Readonly<Partial<Record<CapabilityName, string>>>;
|
|
57
63
|
export function publicBuildGrant(grant: WorldGrant, url: URL): boolean;
|
|
58
64
|
export function decideCapability(request: CapabilityRequest, context: CapabilityContext): CapabilityAnswer;
|
|
59
|
-
export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[] }): CapabilitySet;
|
|
65
|
+
export function decideCapabilitySet(declaration: CapabilityDeclaration | undefined, grant?: WorldGrant, world?: { readonly endpoints?: readonly WorldEndpoint[]; readonly files?: { readonly tree?: string; readonly worldFiles?: readonly string[]; readonly system?: readonly string[]; readonly worldRoot?: string } }): CapabilitySet;
|
|
60
66
|
export function refusalRecord(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): WorldRefusalRecord;
|
|
61
67
|
export function refusalSide(answer: Extract<CapabilityAnswer, { outcome: 'refuse' }>): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.70",
|
|
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",
|