@lapxo/bound 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/TARGET.bound +27 -20
  3. package/dist/cli/accept.js +112 -71
  4. package/dist/cli/owner.js +3 -0
  5. package/dist/cli/verb.js +54 -12
  6. package/dist/fold/configures.js +3 -1
  7. package/dist/fold/digests.js +7 -1
  8. package/dist/fold/kept.js +5 -1
  9. package/dist/fold/observed.d.ts +1 -0
  10. package/dist/fold/observed.js +30 -11
  11. package/dist/fold/readers.d.ts +2 -0
  12. package/dist/fold/readers.js +3 -3
  13. package/dist/fold/region-declarations.d.ts +2 -0
  14. package/dist/fold/region-declarations.js +13 -0
  15. package/dist/fold/resolved.js +9 -1
  16. package/dist/fold/second.js +3 -0
  17. package/dist/host/adapters/transport/http.d.ts +5 -0
  18. package/dist/host/adapters/transport/http.js +28 -0
  19. package/dist/host/capsule-process.d.ts +2 -1
  20. package/dist/host/capsule-process.js +4 -3
  21. package/dist/host/capsule.d.ts +1 -0
  22. package/dist/host/capsule.js +11 -7
  23. package/dist/host/cell-receipts.js +1 -1
  24. package/dist/host/content-needed.d.ts +5 -0
  25. package/dist/host/content-needed.js +8 -0
  26. package/dist/host/content-store.d.ts +8 -0
  27. package/dist/host/content-store.js +31 -0
  28. package/dist/host/lifetime.d.ts +1 -0
  29. package/dist/host/lifetime.js +40 -0
  30. package/dist/host/objects.js +1 -1
  31. package/dist/host/ports/transport.d.ts +1 -0
  32. package/dist/host/ports/transport.js +48 -0
  33. package/dist/host/process-lifetime.d.ts +11 -0
  34. package/dist/host/process-lifetime.js +12 -0
  35. package/dist/host/read-only.d.ts +3 -0
  36. package/dist/host/read-only.js +9 -0
  37. package/dist/host/release.js +2 -1
  38. package/dist/host/selected-capsules.js +38 -12
  39. package/dist/host/selected-topos.d.ts +5 -5
  40. package/dist/host/selected-topos.js +9 -36
  41. package/dist/host/source-content.d.ts +5 -3
  42. package/dist/host/source-content.js +36 -30
  43. package/dist/land/leaving.js +4 -0
  44. package/dist/land/ledger.js +3 -0
  45. package/dist/observe/identity.d.ts +2 -0
  46. package/dist/observe/identity.js +3 -3
  47. package/dist/observe/regions.js +2 -2
  48. package/dist/observe/run.d.ts +2 -1
  49. package/dist/observe/run.js +10 -4
  50. package/dist/render/receipts.js +2 -1
  51. package/package.json +2 -2
  52. package/receipts.bound +73 -72
@@ -1,3 +1,4 @@
1
+ import type { Capsule } from '../host/capsule.ts';
1
2
  import type { CoordinateRole } from './wire.ts';
2
3
  import { capsulesFor } from './reach.ts';
3
4
  import type { Kind } from '../observe/run.ts';
@@ -8,6 +9,7 @@ export interface ReaderClaim {
8
9
  readonly module: string;
9
10
  readonly kind: Kind;
10
11
  readonly capsule?: {
12
+ readonly selected?: Capsule;
11
13
  readonly digest: string;
12
14
  readonly entry: string;
13
15
  readonly region: string;
@@ -62,13 +62,13 @@ export function capsuleReadersOf(root, store, standing, lines, resolve = capsule
62
62
  const pkg = fieldOf(by?.offer ?? '', 'scope').split('/')[1] ?? '';
63
63
  if (by === undefined || reaching.has(`${pkg} ${region} ${where || '/'}`))
64
64
  continue;
65
- const key = `${by.capsule.digest} ${region}`;
66
- const group = groups.get(key) ?? groups.set(key, { module: `${pkg}/${region}`, digest: by.capsule.digest, entry: fieldOf(by.offer, 'shape'), region, reads: by.capsule.declaration.regions[region] ?? [], globs: new Map() }).get(key);
65
+ const key = `${by.capsule.selection ?? by.capsule.digest} ${region}`;
66
+ const group = groups.get(key) ?? groups.set(key, { module: `${pkg}/${region}`, digest: by.capsule.digest, entry: fieldOf(by.offer, 'shape'), selected: by.capsule, region, reads: by.capsule.declaration.regions[region] ?? [], globs: new Map() }).get(key);
67
67
  group.globs.set(where, fieldOf(line, 'value').split('|').filter(Boolean));
68
68
  }
69
69
  }
70
70
  return [...groups.values()].map((group) => ({ id: group.module, shape: ['run'], where: [...group.globs.keys()], module: group.module, kind: 'process',
71
- capsule: { digest: group.digest, entry: group.entry, region: group.region, reads: group.reads, globs: group.globs } }));
71
+ capsule: { selected: group.selected, digest: group.digest, entry: group.entry, region: group.region, reads: group.reads, globs: group.globs } }));
72
72
  }
73
73
  export function handedAt(root, reader, roleOf, look, place) {
74
74
  const { reads, globs } = reader.capsule;
@@ -0,0 +1,2 @@
1
+ /** Region input selectors consumed by the host, rather than measured ceilings. */
2
+ export declare function isRegionDeclaration(line: string, wire: readonly string[]): boolean;
@@ -0,0 +1,13 @@
1
+ import { fieldOf, isConfig } from "./claims.js";
2
+ import { wordsOf } from "./wire.js";
3
+ /** Region input selectors consumed by the host, rather than measured ceilings. */
4
+ export function isRegionDeclaration(line, wire) {
5
+ if (!isConfig(line) || !fieldOf(line, 'scope').startsWith('region/') || fieldOf(line, 'value') === 'withdraw')
6
+ return false;
7
+ const named = ['region-coordinate', 'region-leaf'].flatMap(name => {
8
+ const words = wordsOf(wire, name);
9
+ return words.length === 1 ? words : [];
10
+ });
11
+ const measures = ['lines', 'receipts', 'reads', ...named];
12
+ return measures.includes(fieldOf(line, 'measure'));
13
+ }
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { createHash, dirname, existsSync, join, linkSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
2
3
  import { canonical, parse } from '@lapxo/topos/wire';
3
4
  import { RECEIPTS, fieldOf, isWire } from "./claims.js";
@@ -11,6 +12,10 @@ import { sourced } from "../land/vouched.js";
11
12
  import { entriesIn, observeFile, observeText } from "../observe/files.js";
12
13
  export const bytesOnly = (line) => {
13
14
  const got = parse(line);
15
+ // Authenticated evidence is delivered verbatim. A shortened observation is
16
+ // a different signed message, even when it describes the same input.
17
+ if (got.kind === 'fact' && got.value.fields.sig)
18
+ return line;
14
19
  return got.kind === 'fact' && fieldOf(line, 'measure') === 'observed' ? canonical({ ...got.value.fields, value: fieldOf(line, 'at').replace(/^.*:/, '').slice(0, 16) }) : line;
15
20
  };
16
21
  const held = new WeakMap();
@@ -45,7 +50,8 @@ export function receiptsOf(fold) {
45
50
  return canonical({ scope: `${sourced(fold.standing)}/${place}${shape}`, role: 'writes', form: 'alphabet', measure: 'digest', value: digest, by: writerFor(fold.standing, 'fold') ?? '', at: `place:${digest}` });
46
51
  });
47
52
  const place = under.replace(/\/$/, '');
48
- const lines = [...named, ...[...cones].sort(), ...[...ran].sort(), ...paid].map((line) => (place ? zoomed(line, place) : line));
53
+ const lines = [...named, ...[...cones].sort(), ...[...ran].sort(), ...paid]
54
+ .map((line) => (place && !fieldOf(line, 'sig') ? zoomed(line, place) : line));
49
55
  if (!lines.length) {
50
56
  const shipped = (observeText(join(fold.root, under, RECEIPTS)) ?? '').split('\n').filter(isWire);
51
57
  const carried = carriedFrom(fold.store, shipped);
@@ -64,6 +70,8 @@ export function keepCarried(store, lines, from) {
64
70
  verifiedCarried(at, digest);
65
71
  }
66
72
  else {
73
+ if (readOnly())
74
+ return `sha256:${digest}`;
67
75
  mkdirSync(dirname(at), { recursive: true });
68
76
  const src = from === undefined ? '' : storeAt(from, 'cas', 'carried', digest);
69
77
  if (src && existsSync(src)) {
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { snapshotVerdict } from "./evidence.js";
2
3
  import { dirname, resolve, spawnSync } from "../host/io.js";
3
4
  import { ledgerLines } from "../land/ledger.js";
@@ -9,6 +10,8 @@ import { said } from "./wire.js";
9
10
  import { sha } from "../host/hash.js";
10
11
  const asked = new Map();
11
12
  function askedNow(store, rests) {
13
+ if (readOnly())
14
+ return undefined;
12
15
  const root = dirname(store);
13
16
  const key = `${root} ${rests}`;
14
17
  if (asked.has(key))
@@ -0,0 +1,5 @@
1
+ /** Reference HTTP adapter. Location and budgets are already selected by admitted host policy. */
2
+ export declare function fetchBytes(location: string, limits: {
3
+ readonly timeoutMs: number;
4
+ readonly responseBytes: number;
5
+ }): Promise<Uint8Array>;
@@ -0,0 +1,28 @@
1
+ /** Reference HTTP adapter. Location and budgets are already selected by admitted host policy. */
2
+ export async function fetchBytes(location, limits) {
3
+ const signal = AbortSignal.timeout(limits.timeoutMs), response = await fetch(location, { signal });
4
+ if (!response.ok)
5
+ throw Error(`HTTP ${response.status}`);
6
+ const reader = response.body?.getReader();
7
+ if (!reader)
8
+ return new Uint8Array();
9
+ const chunks = [];
10
+ let size = 0;
11
+ try {
12
+ for (;;) {
13
+ const part = await reader.read();
14
+ if (part.done)
15
+ break;
16
+ size += part.value.length;
17
+ if (size > limits.responseBytes) {
18
+ await reader.cancel();
19
+ throw Error('response exceeds declared byte bound');
20
+ }
21
+ chunks.push(part.value);
22
+ }
23
+ }
24
+ finally {
25
+ reader.releaseLock();
26
+ }
27
+ return Buffer.concat(chunks);
28
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReaderLifetime } from '@lapxo/topos/wire';
1
2
  import type { SpawnSyncOptionsWithStringEncoding } from 'node:child_process';
2
3
  import type { Request, Response } from '@lapxo/topos/contract';
3
4
  /** Operational process failure, distinct from a valid contract refusal or abstention. */
@@ -17,4 +18,4 @@ export declare class CapsuleProcessError extends Error {
17
18
  }
18
19
  export declare function validResponse(value: unknown, request: Request): value is Response;
19
20
  /** Last-line JSON is the wire framing. No partial batch, failed exit or synthetic missing response is accepted. */
20
- export declare function executeCapsule(command: string, args: readonly string[], requests: readonly Request[], limits?: Pick<SpawnSyncOptionsWithStringEncoding, 'timeout' | 'maxBuffer'>): readonly Response[];
21
+ export declare function executeCapsule(command: string, args: readonly string[], requests: readonly Request[], limits?: Pick<SpawnSyncOptionsWithStringEncoding, 'timeout' | 'maxBuffer'>, lifetime?: ReaderLifetime): readonly Response[];
@@ -1,3 +1,4 @@
1
+ import { boundedProcess } from "./process-lifetime.js";
1
2
  import { spawnSync } from 'node:child_process';
2
3
  import { PROTOCOL } from '@lapxo/topos/wire';
3
4
  /** Operational process failure, distinct from a valid contract refusal or abstention. */
@@ -31,9 +32,9 @@ export function validResponse(value, request) {
31
32
  return true;
32
33
  }
33
34
  /** Last-line JSON is the wire framing. No partial batch, failed exit or synthetic missing response is accepted. */
34
- export function executeCapsule(command, args, requests, limits = {}) {
35
- const run = spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits });
36
- if (run.error)
35
+ export function executeCapsule(command, args, requests, limits = {}, lifetime) {
36
+ const run = lifetime === undefined ? spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits }) : boundedProcess(command, args, JSON.stringify(requests), lifetime);
37
+ if ('error' in run && run.error)
37
38
  throw new CapsuleProcessError(`failed: ${run.error.message}`, run);
38
39
  if (run.signal !== null || run.status !== 0)
39
40
  throw new CapsuleProcessError(`exited ${run.status ?? `with signal ${run.signal}`}`, run);
@@ -1,6 +1,7 @@
1
1
  import type { Declaration } from '@lapxo/topos/capsule';
2
2
  import type { Request, Response } from '@lapxo/topos/contract';
3
3
  export interface Capsule {
4
+ readonly selection?: string;
4
5
  readonly digest: string;
5
6
  readonly declaration: Declaration;
6
7
  readonly lines: readonly string[];
@@ -1,3 +1,6 @@
1
+ import { readerLifetime, parse } from '@lapxo/topos/wire';
2
+ import { contentStore, verifiedContent, contentPolicy } from "./content-store.js";
3
+ import { readOnly } from "./read-only.js";
1
4
  import { sha } from "./hash.js";
2
5
  import { wireLinesOf as linesOf } from "../fold/claims.js";
3
6
  import { createHash } from 'node:crypto';
@@ -8,7 +11,7 @@ import { CAPSULE } from '@lapxo/topos/wire';
8
11
  import { declarationOf } from '@lapxo/topos/capsule';
9
12
  import { unpack } from "./archive.js";
10
13
  import { executeCapsule, validResponse } from "./capsule-process.js";
11
- import { blobAt, holdStore, replaceWhole, storeAt } from "../land/ledger.js";
14
+ import { holdStore, replaceWhole, storeAt } from "../land/ledger.js";
12
15
  import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
13
16
  import { hostOf } from "../observe/run.js";
14
17
  import { ownLock, ownRoot, ownStore, processOf } from "../observe/runner.js";
@@ -42,13 +45,12 @@ function unpacked(digest, bytes) {
42
45
  const capsules = new Map();
43
46
  /** A blob is loaded once in an act. A later ask for the same digest reads that capsule, and does not hash or unpack it again. */
44
47
  export function capsuleAt(digest, entry, store) {
45
- const identity = `${store ?? ownStore()}\0${digest}\0${entry}`;
48
+ const cacheStore = store ?? contentStore();
49
+ const { bytes } = verifiedContent(digest, cacheStore);
50
+ const identity = `${cacheStore}\0${digest}\0${entry}`;
46
51
  const held = capsules.get(identity);
47
52
  if (held !== undefined)
48
53
  return held;
49
- const bytes = observeFile(blobAt(store ?? ownStore(), digest));
50
- if (bytes === undefined || `sha256:${createHash('sha256').update(bytes).digest('hex')}` !== digest)
51
- return undefined;
52
54
  const known = declared.get(digest) ?? ((lines) => ({ lines, declaration: declarationOf(lines) }))(linesOf(observeText(join(unpacked(digest, bytes), CAPSULE))));
53
55
  declared.set(digest, known);
54
56
  const names = Object.keys(known.declaration.regions);
@@ -61,7 +63,7 @@ export function capsuleAt(digest, entry, store) {
61
63
  const capsule = { digest, ...known, ask: (requests) => {
62
64
  if (!statSync(module, { throwIfNoEntry: false })?.isFile())
63
65
  throw new Error(`REFUSE·capsule ${digest} declared entry ${entry} unavailable`);
64
- return answered(digest, bytes, requests, names, root, store ?? ownStore(), module);
66
+ return answered(digest, bytes, requests, names, root, cacheStore, module);
65
67
  } };
66
68
  capsules.set(identity, capsule);
67
69
  return capsule;
@@ -108,10 +110,12 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
108
110
  }
109
111
  });
110
112
  const missing = requests.filter((_, i) => held[i] === undefined);
113
+ if (missing.length && readOnly())
114
+ throw Error(`REFUSE·preview capsule ${digest} has no verified answer for the proposed inputs; fold the declared inputs first`);
111
115
  if (missing.length)
112
116
  process.stderr.write(`CAPSULE ${digest.slice(7, 19)} · ${requests.length} asked · ${missing.length} run · ${missing.map((one) => `${one.rootScope}${one.region}`).join(' ')}\n`);
113
117
  // Execute and validate the entire batch before committing any answer to the cache.
114
- const ran = !missing.length ? [] : executeCapsule(process.execPath, [...childFlags(), hostOf(), ...child], missing);
118
+ const ran = !missing.length ? [] : executeCapsule(process.execPath, [...childFlags(), hostOf(), ...child], missing, {}, readerLifetime(contentPolicy().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; }), digest));
115
119
  return requests.map((request, i) => {
116
120
  if (held[i] !== undefined)
117
121
  return held[i];
@@ -43,7 +43,7 @@ export async function cellsWithReceipts(root, history, emit) {
43
43
  for (const record of parsed.value.objects.filter(r => r.record === 'cell')) {
44
44
  const pin = record.fields.topos;
45
45
  if (!pins.has(pin)) {
46
- const top = toposAt(pin);
46
+ const top = toposAt(pin, store);
47
47
  pins.set(pin, [pin, ...top.dependencies]);
48
48
  }
49
49
  const scope = `receipts/cells/${record.fields.scope}`;
@@ -0,0 +1,5 @@
1
+ /** The synchronous host names unavailable content; the enclosing fold resolves exactly that request. */
2
+ export declare class ContentNeeded extends Error {
3
+ readonly digest: string;
4
+ constructor(digest: string, why?: string);
5
+ }
@@ -0,0 +1,8 @@
1
+ /** The synchronous host names unavailable content; the enclosing fold resolves exactly that request. */
2
+ export class ContentNeeded extends Error {
3
+ constructor(digest, why = `REFUSE·pin ${digest} content unavailable · not laid`) {
4
+ super(why);
5
+ this.digest = digest;
6
+ this.name = 'ContentNeeded';
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ export declare const withContentStore: <T>(store: string, run: () => T, policy?: () => readonly string[]) => T;
2
+ export declare const contentPolicy: () => readonly string[];
3
+ export declare const contentStore: () => string;
4
+ /** Only immutable verified bytes may be shared with the instrument. Local corruption is never hidden by fallback. */
5
+ export declare function verifiedContent(digest: string, store?: string, instrument?: string): {
6
+ readonly bytes: Uint8Array;
7
+ readonly path: string;
8
+ };
@@ -0,0 +1,31 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { createHash } from 'node:crypto';
3
+ import { blobAt } from "../land/ledger.js";
4
+ import { observeFile } from "../observe/files.js";
5
+ import { ownLock, ownStore } from "../observe/runner.js";
6
+ import { parse, wireAt } from '@lapxo/topos/wire';
7
+ import { ContentNeeded } from "./content-needed.js";
8
+ // Each invocation carries its place explicitly; asynchronous requests cannot share a mutable current root.
9
+ const requested = new AsyncLocalStorage();
10
+ export const withContentStore = (store, run, policy = () => []) => requested.run({ store, policy }, run);
11
+ export const contentPolicy = () => requested.getStore()?.policy() ?? [];
12
+ export const contentStore = () => requested.getStore()?.store ?? ownStore();
13
+ /** Only immutable verified bytes may be shared with the instrument. Local corruption is never hidden by fallback. */
14
+ export function verifiedContent(digest, store = contentStore(), instrument = ownStore()) {
15
+ const fields = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
16
+ const wire = wireAt(fields, Number.MAX_SAFE_INTEGER);
17
+ if (!wire)
18
+ throw Error('REFUSE·pin instrument has no admitted wire');
19
+ const split = digest.indexOf(':'), algorithm = digest.slice(0, split), hex = digest.slice(split + 1);
20
+ if (split < 1 || !wire.digests.has(algorithm) || !/^[a-f0-9]+$/.test(hex))
21
+ throw Error(`REFUSE·pin ${digest} digest is not admitted`);
22
+ for (const at of new Set([store, instrument])) {
23
+ const path = blobAt(at, digest), bytes = observeFile(path);
24
+ if (bytes === undefined)
25
+ continue;
26
+ if (createHash(algorithm).update(bytes).digest('hex') !== hex)
27
+ throw Error(`REFUSE·pin ${digest} content hash mismatch · store ${at} · not laid`);
28
+ return { bytes, path };
29
+ }
30
+ throw new ContentNeeded(digest);
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ // This is a host mechanism, not a wire reader or a provider. The parent supplies its already selected policy.
4
+ const request = JSON.parse(readFileSync(0, 'utf8'));
5
+ if (!Number.isSafeInteger(request.limits.timeoutMs) || request.limits.timeoutMs <= 0 || !Number.isSafeInteger(request.limits.responseBytes) || request.limits.responseBytes <= 0)
6
+ throw Error('REFUSE·reader invalid process limits');
7
+ const grouped = process.platform !== 'win32';
8
+ const child = spawn(request.command, [...request.args], { cwd: request.cwd, env: request.env, detached: grouped, stdio: ['pipe', 'pipe', 'pipe'] });
9
+ const stdout = [], stderr = [];
10
+ let size = 0, failure, finished = false;
11
+ const cancel = () => { if (child.pid === undefined)
12
+ return; try {
13
+ if (grouped)
14
+ process.kill(-child.pid, 'SIGKILL');
15
+ else
16
+ child.kill('SIGKILL');
17
+ }
18
+ catch { } };
19
+ const refuse = (why) => { failure ?? (failure = why); cancel(); };
20
+ const receive = (chunks, bytes) => { size += bytes.length; if (size > request.limits.responseBytes) {
21
+ refuse('response exceeds declared byte bound');
22
+ return;
23
+ } chunks.push(bytes); };
24
+ child.stdout.on('data', (bytes) => receive(stdout, bytes));
25
+ child.stderr.on('data', (bytes) => receive(stderr, bytes));
26
+ child.stdin.on('error', error => { if (!finished)
27
+ refuse(`input failed: ${error.message}`); });
28
+ const timer = setTimeout(() => refuse('declared timeout exhausted'), request.limits.timeoutMs);
29
+ const finish = (status, signal) => {
30
+ if (finished)
31
+ return;
32
+ finished = true;
33
+ clearTimeout(timer);
34
+ cancel();
35
+ const result = { stdout: failure ? '' : Buffer.concat(stdout).toString('utf8'), stdoutBytes: failure ? '' : Buffer.concat(stdout).toString('base64'), stderr: failure ? '' : Buffer.concat(stderr).toString('utf8'), status, signal, ...(failure ? { failure } : {}) };
36
+ process.stdout.write(JSON.stringify(result));
37
+ };
38
+ child.on('error', error => { failure = error.message; finish(null, null); });
39
+ child.on('close', finish);
40
+ child.stdin.end(request.input);
@@ -10,7 +10,7 @@ export async function admittedObjects(root, lines, emit = () => { }) {
10
10
  if (history.kind !== 'fact')
11
11
  throw Error(`REFUSE·wire ${history.why}`);
12
12
  const authority = authorityFor(lines, rootSigner(root), signaturesOf(storeOf(root)).admitted);
13
- const context = await objectProvider(history.value.objects, history.value.configuration, { admit: fields => authority.of(canonical(fields)).kind === 'admitted', emit, topos: toposAt, module: contextModuleAt });
13
+ const context = await objectProvider(history.value.objects, history.value.configuration, { admit: fields => authority.of(canonical(fields)).kind === 'admitted', emit, topos: digest => toposAt(digest, storeOf(root)), module: digest => contextModuleAt(digest, storeOf(root)) });
14
14
  const checked = validateObjectContext(history.value.objects, context);
15
15
  if (checked.kind !== 'fact')
16
16
  throw Error(`REFUSE·context ${checked.why}`);
@@ -0,0 +1 @@
1
+ export declare function transportBytes(location: string, digest: string, lines: readonly string[]): Promise<Uint8Array>;
@@ -0,0 +1,48 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { contentRequest, transportSelection, parse } from '@lapxo/topos/wire';
3
+ import { boundedProcess } from "../process-lifetime.js";
4
+ import { fetchBytes } from "../adapters/transport/http.js";
5
+ export async function transportBytes(location, digest, lines) {
6
+ const scheme = new URL(location).protocol.slice(0, -1);
7
+ const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
8
+ const selected = transportSelection(fields, scheme);
9
+ if (selected === undefined)
10
+ throw Error(`REFUSE·transport ${scheme} no admitted content-request/1 policy`);
11
+ if (selected.name === 'fetch') {
12
+ if (!['http', 'https'].includes(scheme))
13
+ throw Error(`REFUSE·transport ${scheme} is not supported by this host adapter`);
14
+ return fetchBytes(location, selected);
15
+ }
16
+ const registryPath = process.env['BOUND_TRANSPORTS'];
17
+ if (!registryPath)
18
+ throw Error(`REFUSE·transport ${scheme} no host binding for ${selected.name}`);
19
+ let record;
20
+ try {
21
+ const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
22
+ if (!registry || typeof registry !== 'object' || !Object.prototype.hasOwnProperty.call(registry, selected.name))
23
+ throw Error();
24
+ const value = registry[selected.name];
25
+ if (!value || typeof value !== 'object')
26
+ throw Error();
27
+ const f = value;
28
+ if (f.kind !== 'command' || typeof f.executable !== 'string' || !f.executable || !Array.isArray(f.args) || !f.args.every(x => typeof x === 'string'))
29
+ throw Error();
30
+ if (f.environment !== undefined && (!Array.isArray(f.environment) || !f.environment.every(x => typeof x === 'string')))
31
+ throw Error();
32
+ const names = (f.environment ?? []);
33
+ const env = {};
34
+ if (process.env.PATH !== undefined)
35
+ env.PATH = process.env.PATH;
36
+ for (const name of names)
37
+ if (process.env[name] !== undefined)
38
+ env[name] = process.env[name];
39
+ record = { executable: f.executable, args: f.args, env };
40
+ }
41
+ catch {
42
+ throw Error(`REFUSE·transport ${scheme} host binding unavailable or invalid`);
43
+ }
44
+ const result = boundedProcess(record.executable, record.args, contentRequest(location, digest, selected), selected, undefined, record.env);
45
+ if (result.status !== 0 || result.signal !== null)
46
+ throw Error(`REFUSE·transport ${scheme} failed · status ${result.status ?? result.signal}`);
47
+ return Buffer.from(result.stdoutBytes ?? '', 'base64');
48
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReaderLifetime } from '@lapxo/topos/wire';
2
+ export interface ProcessResult {
3
+ readonly stdout: string;
4
+ readonly stdoutBytes?: string;
5
+ readonly stderr: string;
6
+ readonly status: number | null;
7
+ readonly signal: NodeJS.Signals | null;
8
+ readonly failure?: string;
9
+ }
10
+ /** A synchronous fold waits on a supervising host process; the supervisor owns cancellation of its child group. */
11
+ export declare function boundedProcess(command: string, args: readonly string[], input: string, limits: ReaderLifetime, cwd?: string, env?: Readonly<Record<string, string>>): ProcessResult;
@@ -0,0 +1,12 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { processOf } from "../observe/runner.js";
3
+ /** A synchronous fold waits on a supervising host process; the supervisor owns cancellation of its child group. */
4
+ export function boundedProcess(command, args, input, limits, cwd, env) {
5
+ const run = spawnSync(process.execPath, [processOf('lifetime', 'host')], { input: JSON.stringify({ command, args, input, limits, cwd, env }), encoding: 'utf8', maxBuffer: limits.responseBytes * 12 + 4096 });
6
+ if (run.error || run.status !== 0)
7
+ throw Error(`REFUSE·reader lifetime supervisor ${run.error?.message ?? run.status}`);
8
+ const result = JSON.parse(run.stdout);
9
+ if (result.failure)
10
+ throw Error(`REFUSE·reader ${result.failure} · status ${result.status ?? result.signal ?? 'cancelled'}`);
11
+ return result;
12
+ }
@@ -0,0 +1,3 @@
1
+ export declare const readOnly: () => boolean;
2
+ export declare const withoutEffects: <T>(evaluate: () => T) => T;
3
+ export declare function requireEffects(operation: string): void;
@@ -0,0 +1,9 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ /** Prospective evaluation may read existing evidence and calculate values, but cannot acquire evidence or persist caches. */
3
+ const mode = new AsyncLocalStorage();
4
+ export const readOnly = () => mode.getStore() === true;
5
+ export const withoutEffects = (evaluate) => mode.run(true, evaluate);
6
+ export function requireEffects(operation) {
7
+ if (readOnly())
8
+ throw Error(`REFUSE·preview ${operation} requires an act; fold the declared inputs first`);
9
+ }
@@ -1,3 +1,4 @@
1
+ import { ContentNeeded } from "./content-needed.js";
1
2
  import { createHash } from 'node:crypto';
2
3
  import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
4
  import { dirname, join, sep } from 'node:path';
@@ -143,7 +144,7 @@ export function layReleases(store, own, lock, contentStore = own) {
143
144
  if (bytes !== undefined && current !== undefined && !dir && !sameSnapshot(snapshotOf(current), members(bytes).map((one) => [one.name, digestOf(one.bytes)])))
144
145
  throw new Error(`REFUSE·release snapshot for ${digest} omits or changes archive members; explicit rebuild required`);
145
146
  if (bytes === undefined && !dir && current === undefined)
146
- throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
147
+ throw new ContentNeeded(digest, `REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
147
148
  const root = fieldOf(line, 'shape');
148
149
  const place = join(own, archiveCoordinate(needs));
149
150
  const link = lstatSync(place, { throwIfNoEntry: false });
@@ -1,5 +1,7 @@
1
1
  import { capsuleAt } from "./capsule.js";
2
- import { capsuleOffers } from '@lapxo/topos/standing';
2
+ import { capsuleOffers, providerInputs, providerInputRegions, capsuleDescriptor, assertCapsuleDescriptor } from '@lapxo/topos/standing';
3
+ import { fullDigest } from "./digest.js";
4
+ import { wireAt, parse } from '@lapxo/topos/wire';
3
5
  import { selectedTopoi, toposAt } from "./selected-topos.js";
4
6
  import { canonical } from '@lapxo/topos/wire';
5
7
  import { fieldOf } from "../fold/claims.js";
@@ -8,16 +10,40 @@ import { archiveCoordinate } from "./archive.js";
8
10
  /** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
9
11
  export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, host = ownLock()) {
10
12
  return selectedTopoi(pins, select).flatMap(({ digest, topos }) => capsuleOffers(topos).map(offer => {
11
- const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
12
- if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
13
- throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
14
- const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
15
- if (entries.length !== 1)
16
- throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
17
- const projected = { ...offer, shape: entries[0] };
18
- const capsule = resolve(offer.value, projected.shape);
19
- if (capsule === undefined)
20
- throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
21
- return { capsule, offer: canonical(projected) };
13
+ const load = () => {
14
+ const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
15
+ if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
16
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
17
+ const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
18
+ if (entries.length !== 1)
19
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
20
+ const loaded = resolve(offer.value, entries[0]);
21
+ if (loaded === undefined)
22
+ throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
23
+ return { capsule: loaded, offer: canonical({ ...offer, shape: entries[0] }) };
24
+ };
25
+ const descriptor = capsuleDescriptor(topos, offer.scope ?? '');
26
+ const loaded = descriptor === undefined ? load() : undefined;
27
+ const capsule = loaded?.capsule ?? {
28
+ digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: requests => {
29
+ if (!requests.length)
30
+ return [];
31
+ const actual = load();
32
+ assertCapsuleDescriptor(descriptor.lines, actual.capsule.lines);
33
+ return actual.capsule.ask(requests);
34
+ },
35
+ };
36
+ const projected = loaded?.offer ?? canonical(offer);
37
+ if (!providerInputRegions(topos, offer.scope ?? '').length)
38
+ return { capsule, offer: projected };
39
+ const fields = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
40
+ const wire = wireAt(fields, Number.MAX_SAFE_INTEGER), algorithm = wire && [...wire.digests][0];
41
+ if (!algorithm)
42
+ throw Error('REFUSE·input instrument has no admitted digest algorithm');
43
+ const selected = { ...capsule, selection: `${digest} ${offer.scope}`, ask: requests => capsule.ask(requests.map(request => {
44
+ const provider = providerInputs(topos, digest, offer.scope ?? '', offer.value, request.region ?? '', bytes => fullDigest(bytes, algorithm));
45
+ return provider === undefined ? request : { ...request, provider };
46
+ })) };
47
+ return { capsule: selected, offer: projected };
22
48
  }));
23
49
  }
@@ -6,11 +6,11 @@ export interface SelectedTopos {
6
6
  }
7
7
  export type ToposResolver = (digest: string) => StandingTopos;
8
8
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
9
- export declare function toposAt(digest: string): StandingTopos;
10
- export declare function contentAt(digest: string): Uint8Array;
11
- /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
12
- export declare function contextModuleAt(digest: string): string;
13
- /** Selection verifies the standing and its directly declared closure. It does not request any capability. */
9
+ export declare function toposAt(digest: string, store?: string): StandingTopos;
10
+ export declare function contentAt(digest: string, store?: string): Uint8Array;
11
+ /** The declared module runs from the store that supplied its verified bytes. No copy or live-checkout fallback. */
12
+ export declare function contextModuleAt(digest: string, store?: string): string;
13
+ /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
14
14
  export declare function selectedTopoi(pins: readonly string[], resolve?: ToposResolver): readonly SelectedTopos[];
15
15
  /** Read a declared wire list without copying a world's claims or executing its offers.
16
16
  * Independent declarations must agree; selection order cannot choose a contract.
@@ -1,47 +1,20 @@
1
- import { createHash } from 'node:crypto';
1
+ import { verifiedContent } from "./content-store.js";
2
2
  import { readStanding } from '@lapxo/topos/standing';
3
3
  import { parse, wireAt } from '@lapxo/topos/wire';
4
4
  import { fieldOf } from "../fold/claims.js";
5
- import { blobAt } from "../land/ledger.js";
6
- import { observeFile } from "../observe/files.js";
7
- import { ownStore, ownLock } from "../observe/runner.js";
8
- import { replaceWhole } from "../land/ledger.js";
9
- import { join } from 'node:path';
10
5
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
11
- export function toposAt(digest) {
12
- const topos = readStanding(Buffer.from(contentAt(digest)).toString('utf8'));
13
- for (const dependency of topos.dependencies)
14
- contentAt(dependency);
6
+ export function toposAt(digest, store) {
7
+ const topos = readStanding(Buffer.from(contentAt(digest, store)).toString('utf8'));
15
8
  return topos;
16
9
  }
17
- export function contentAt(digest) {
18
- const fields = ownLock().flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
19
- const wire = wireAt(fields, Number.MAX_SAFE_INTEGER);
20
- if (wire === null)
21
- throw Error('REFUSE·pin instrument has no admitted wire');
22
- const algorithms = wire.digests;
23
- const bytesAt = (name) => {
24
- const split = name.indexOf(':');
25
- const algorithm = name.slice(0, split), hex = name.slice(split + 1);
26
- if (split < 1 || !algorithms.has(algorithm) || !/^[a-f0-9]+$/.test(hex))
27
- throw Error(`REFUSE·pin ${name} digest is not admitted`);
28
- const bytes = observeFile(blobAt(ownStore(), name));
29
- if (bytes === undefined)
30
- throw Error(`REFUSE·pin ${name} content unavailable · not laid`);
31
- if (createHash(algorithm).update(bytes).digest('hex') !== hex)
32
- throw Error(`REFUSE·pin ${name} content hash mismatch · not laid`);
33
- return bytes;
34
- };
35
- return bytesAt(digest);
10
+ export function contentAt(digest, store) {
11
+ return verifiedContent(digest, store).bytes;
36
12
  }
37
- /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
38
- export function contextModuleAt(digest) {
39
- const bytes = contentAt(digest);
40
- const at = join(ownStore(), 'cas', 'runtime', digest.replace(':', '_') + '.mjs');
41
- replaceWhole(at, bytes);
42
- return at;
13
+ /** The declared module runs from the store that supplied its verified bytes. No copy or live-checkout fallback. */
14
+ export function contextModuleAt(digest, store) {
15
+ return verifiedContent(digest, store).path;
43
16
  }
44
- /** Selection verifies the standing and its directly declared closure. It does not request any capability. */
17
+ /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
45
18
  export function selectedTopoi(pins, resolve = toposAt) {
46
19
  return [...new Set(pins.map(pin => fieldOf(pin, 'value')))].map(digest => ({ digest, topos: resolve(digest) }));
47
20
  }