@lapxo/bound 0.1.8 → 0.1.9

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 (128) hide show
  1. package/README.md +56 -20
  2. package/TARGET.bound +25 -25
  3. package/dist/cli/accept.js +83 -59
  4. package/dist/cli/act-result.d.ts +9 -0
  5. package/dist/cli/act-result.js +68 -13
  6. package/dist/cli/args.d.ts +1 -0
  7. package/dist/cli/args.js +5 -2
  8. package/dist/cli/custodian.d.ts +1 -0
  9. package/dist/cli/custodian.js +29 -0
  10. package/dist/cli/native-descendant.d.ts +1 -0
  11. package/dist/cli/native-descendant.js +19 -0
  12. package/dist/cli/native.d.ts +1 -0
  13. package/dist/cli/native.js +60 -0
  14. package/dist/cli/observe.js +9 -2
  15. package/dist/cli/pass.js +6 -1
  16. package/dist/cli/standing-result.js +7 -26
  17. package/dist/cli/verb.js +24 -5
  18. package/dist/fold/admission-history.js +2 -2
  19. package/dist/fold/claims.d.ts +4 -0
  20. package/dist/fold/claims.js +23 -3
  21. package/dist/fold/closed.d.ts +1 -1
  22. package/dist/fold/closed.js +16 -4
  23. package/dist/fold/digests.d.ts +1 -1
  24. package/dist/fold/digests.js +3 -2
  25. package/dist/fold/evidence.d.ts +1 -1
  26. package/dist/fold/kept.js +2 -1
  27. package/dist/fold/observed.d.ts +1 -0
  28. package/dist/fold/observed.js +54 -9
  29. package/dist/fold/readers.d.ts +4 -3
  30. package/dist/fold/readers.js +18 -13
  31. package/dist/fold/resolved.js +11 -1
  32. package/dist/fold/signers.js +2 -2
  33. package/dist/host/adapters/modules/javascript.d.ts +2 -0
  34. package/dist/host/adapters/modules/javascript.js +8 -6
  35. package/dist/host/admitted-place.d.ts +5 -0
  36. package/dist/host/admitted-place.js +54 -0
  37. package/dist/host/capsule-process.d.ts +1 -1
  38. package/dist/host/capsule-process.js +4 -1
  39. package/dist/host/capsule.d.ts +4 -0
  40. package/dist/host/capsule.js +16 -2
  41. package/dist/host/contextual-place.d.ts +59 -0
  42. package/dist/host/contextual-place.js +77 -0
  43. package/dist/host/hash.d.ts +7 -0
  44. package/dist/host/hash.js +6 -0
  45. package/dist/host/io.d.ts +2 -0
  46. package/dist/host/io.js +2 -0
  47. package/dist/host/lifetime.js +61 -22
  48. package/dist/host/native/bounded-file.d.ts +8 -0
  49. package/dist/host/native/bounded-file.js +32 -0
  50. package/dist/host/native/command.d.ts +36 -0
  51. package/dist/host/native/command.js +91 -0
  52. package/dist/host/native/control-bridge-worker.d.ts +1 -0
  53. package/dist/host/native/control-bridge-worker.js +88 -0
  54. package/dist/host/native/control-bridge.d.ts +30 -0
  55. package/dist/host/native/control-bridge.js +110 -0
  56. package/dist/host/native/control-selection.d.ts +29 -0
  57. package/dist/host/native/control-selection.js +32 -0
  58. package/dist/host/native/custody.d.ts +5 -0
  59. package/dist/host/native/custody.js +91 -0
  60. package/dist/host/native/deadline.d.ts +12 -0
  61. package/dist/host/native/deadline.js +28 -0
  62. package/dist/host/native/descendants.d.ts +1 -0
  63. package/dist/host/native/descendants.js +528 -0
  64. package/dist/host/native/generated-custodian.d.ts +122 -0
  65. package/dist/host/native/generated-custodian.js +267 -0
  66. package/dist/host/native/generated.d.ts +217 -0
  67. package/dist/host/native/generated.js +379 -0
  68. package/dist/host/native/image.d.ts +36 -0
  69. package/dist/host/native/image.js +45 -0
  70. package/dist/host/native/jobs.d.ts +75 -0
  71. package/dist/host/native/jobs.js +148 -0
  72. package/dist/host/native/loader.d.ts +80 -0
  73. package/dist/host/native/loader.js +269 -0
  74. package/dist/host/native/materialize.d.ts +22 -0
  75. package/dist/host/native/materialize.js +175 -0
  76. package/dist/host/native/module-closure.d.ts +27 -0
  77. package/dist/host/native/module-closure.js +99 -0
  78. package/dist/host/native/module-syntax.d.ts +8 -0
  79. package/dist/host/native/module-syntax.js +63 -0
  80. package/dist/host/native/native-binary.d.ts +54 -0
  81. package/dist/host/native/native-binary.js +115 -0
  82. package/dist/host/native/native-program.d.ts +56 -0
  83. package/dist/host/native/native-program.js +72 -0
  84. package/dist/host/native/node-jobs.d.ts +84 -0
  85. package/dist/host/native/node-jobs.js +187 -0
  86. package/dist/host/native/node-launch.d.ts +72 -0
  87. package/dist/host/native/node-launch.js +159 -0
  88. package/dist/host/native/preload.d.ts +1 -0
  89. package/dist/host/native/preload.js +45 -0
  90. package/dist/host/native/reading.d.ts +91 -0
  91. package/dist/host/native/reading.js +249 -0
  92. package/dist/host/native/receipt-environment.d.ts +3 -0
  93. package/dist/host/native/receipt-environment.js +41 -0
  94. package/dist/host/native/references.d.ts +5 -0
  95. package/dist/host/native/references.js +30 -0
  96. package/dist/host/native/root.d.ts +17 -0
  97. package/dist/host/native/root.js +37 -0
  98. package/dist/host/native/route.d.ts +66 -0
  99. package/dist/host/native/route.js +216 -0
  100. package/dist/host/native/runtime.d.ts +4 -0
  101. package/dist/host/native/runtime.js +9 -0
  102. package/dist/host/native-reader-context.d.ts +3 -0
  103. package/dist/host/native-reader-context.js +8 -0
  104. package/dist/host/place-clock-selection.d.ts +55 -0
  105. package/dist/host/place-clock-selection.js +124 -0
  106. package/dist/host/ports/local-act.d.ts +10 -0
  107. package/dist/host/ports/local-act.js +78 -6
  108. package/dist/host/ports/place-act.d.ts +46 -0
  109. package/dist/host/ports/place-act.js +169 -0
  110. package/dist/host/process-lifetime.js +8 -3
  111. package/dist/host/provide.d.ts +25 -0
  112. package/dist/host/provide.js +115 -13
  113. package/dist/host/reading-closures.d.ts +15 -1
  114. package/dist/host/reading-closures.js +88 -6
  115. package/dist/host/reading-context.d.ts +29 -0
  116. package/dist/host/reading-context.js +53 -0
  117. package/dist/host/root-clock-source.d.ts +85 -0
  118. package/dist/host/root-clock-source.js +305 -0
  119. package/dist/host/selected-capsules.js +3 -2
  120. package/dist/land/act.d.ts +26 -0
  121. package/dist/land/act.js +96 -3
  122. package/dist/land/ledger.d.ts +4 -0
  123. package/dist/land/ledger.js +20 -2
  124. package/dist/observe/runner.d.ts +4 -0
  125. package/dist/observe/runner.js +9 -2
  126. package/docs/contracts/cli.md +13 -0
  127. package/package.json +2 -2
  128. package/receipts.bound +296 -256
@@ -0,0 +1,169 @@
1
+ import { existsSync, readdirSync, statSync, openSync, fstatSync, readSync, closeSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { readPlaceActContext, verifyPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
4
+ import { underAuthorityRegions, underTheRegions, underRootProjection } from "../../land/act.js";
5
+ import { requireEffects } from "../read-only.js";
6
+ import { publishBundle, nativePublicationStorage } from "./publication.js";
7
+ const fail = (why) => { throw Error('REFUSE·place act ' + why); };
8
+ const clock = (n) => { if (!/^(0|[1-9][0-9]*)$/.test(n) || !Number.isSafeInteger(Number(n)))
9
+ fail('unknown receiver clock'); return Number(n); };
10
+ const rootBytes = (r) => JSON.stringify([r.namespace, r.snapshot, r.epoch, r.prefix, r.head, r.policies]);
11
+ const budget = (b) => { if (!b || !Number.isSafeInteger(b.acts) || b.acts < 1 || !Number.isSafeInteger(b.bytes) || b.bytes < 1)
12
+ fail('missing storage budget'); };
13
+ const base = (store) => join(store, 'ledger', 'place-acts');
14
+ const at = (store, id) => { if (!id || id.includes('\0'))
15
+ fail('missing identity'); return join(base(store), 'committed-' + encodeURIComponent(id)); };
16
+ function storedInventory(store, b) {
17
+ budget(b);
18
+ if (!existsSync(base(store)))
19
+ return { names: [], bytes: 0 };
20
+ const names = readdirSync(base(store)).filter(n => n.startsWith('committed-'));
21
+ if (names.length > b.acts)
22
+ fail('stored act budget');
23
+ let bytes = 0;
24
+ for (const name of names)
25
+ for (const member of ['context.json', 'receipts.json', 'originals.json']) {
26
+ const stat = statSync(join(base(store), name, member));
27
+ if (!stat.isFile() || (bytes += stat.size) > b.bytes)
28
+ fail('stored aggregate byte budget');
29
+ }
30
+ return { names, bytes };
31
+ }
32
+ function readBundle(path, b) {
33
+ budget(b);
34
+ let bytes = 0;
35
+ const read = (name) => {
36
+ const fd = openSync(join(path, name), 'r');
37
+ try {
38
+ const size = fstatSync(fd);
39
+ if (!size.isFile() || size.size > b.bytes - bytes)
40
+ fail('stored byte budget');
41
+ const data = Buffer.alloc(size.size);
42
+ let read = 0;
43
+ while (read < data.length) {
44
+ const count = readSync(fd, data, read, data.length - read, null);
45
+ if (!count)
46
+ fail('stored member changed while reading');
47
+ read += count;
48
+ }
49
+ if (readSync(fd, Buffer.alloc(1), 0, 1, null) !== 0)
50
+ fail('stored member changed while reading');
51
+ bytes += read;
52
+ return new TextDecoder('utf-8', { fatal: true }).decode(data);
53
+ }
54
+ finally {
55
+ closeSync(fd);
56
+ }
57
+ };
58
+ try {
59
+ const context = read('context.json'), receipts = JSON.parse(read('receipts.json')), originals = JSON.parse(read('originals.json'));
60
+ return { context, receipts, originals };
61
+ }
62
+ catch (e) {
63
+ if (e instanceof Error && e.message.startsWith('REFUSE·'))
64
+ throw e;
65
+ return fail('committed bundle unreadable');
66
+ }
67
+ }
68
+ function authenticated(bundle, id, contract) {
69
+ const result = placeActReceiptsOf(bundle.context, bundle.receipts, contract), decoded = readPlaceActContext(bundle.context, contract.budget);
70
+ if (result.identity !== id || JSON.stringify(bundle.originals) !== JSON.stringify(decoded.places.map(p => p.records)))
71
+ fail('stored original/context identity mismatch');
72
+ return decoded;
73
+ }
74
+ /** Historical result reads never produce a current authorization permit. */
75
+ export function placeActResult(store, id, trust, limits) {
76
+ const path = at(store, id);
77
+ if (!existsSync(path))
78
+ return undefined;
79
+ const bundle = readBundle(path, limits);
80
+ authenticated(bundle, id, trust.historical(bundle.context));
81
+ return bundle;
82
+ }
83
+ /** Each receiver chain advances independently. Initial heads are explicit,
84
+ * authenticated host inputs, not inferred zero or legacy global epochs. */
85
+ export function placeActHeads(store, initial, trust, limits) {
86
+ budget(limits);
87
+ if (!initial.length || new Set(initial.map(h => h.place)).size !== initial.length)
88
+ fail('missing/ambiguous initial heads');
89
+ const heads = new Map(initial.map(h => { clock(h.epoch); if (!h.identity)
90
+ fail('missing initial head'); return [h.place, { ...h }]; }));
91
+ if (!existsSync(base(store)))
92
+ return [...heads.values()];
93
+ const { names } = storedInventory(store, limits);
94
+ const pending = names.map(name => { const id = decodeURIComponent(name.slice(10)), bundle = readBundle(join(base(store), name), limits); const context = authenticated(bundle, id, trust.historical(bundle.context)); if (context.root.namespace !== trust.contract.root.namespace)
95
+ fail('stored clock namespace mismatch'); return { id, context }; });
96
+ while (pending.length) {
97
+ const next = pending.findIndex(a => a.context.places.every(p => heads.get(p.place)?.identity === p.previous));
98
+ if (next < 0)
99
+ fail('receiver chain fork or missing predecessor');
100
+ const act = pending.splice(next, 1)[0];
101
+ for (const p of act.context.places) {
102
+ const prior = heads.get(p.place);
103
+ const epoch = clock(prior.epoch) + 1;
104
+ if (!Number.isSafeInteger(epoch) || clock(p.epoch) !== epoch)
105
+ fail('receiver chain clock mismatch');
106
+ heads.set(p.place, { place: p.place, identity: act.id, epoch: p.epoch });
107
+ }
108
+ }
109
+ return [...heads.values()];
110
+ }
111
+ /** Context, exact original bytes and complete signed receipts have one visibility
112
+ * point. Real Root guard exclusion spans final authority/head comparison and
113
+ * publication; this does not activate a CLI profile or reinterpret local-act@1. */
114
+ export async function commitPlaceAct(store, encoded, receipts, trust, limits, storage = nativePublicationStorage) {
115
+ requireEffects('place act commitment');
116
+ budget(limits);
117
+ const decoded = readPlaceActContext(encoded, trust.contract.budget), id = trust.contract.digest(encoded);
118
+ const regions = decoded.places.map(p => { if (p.place === '.')
119
+ return ''; const parts = p.place.split('/'); if (parts.length < 2 || parts.pop() !== 'TARGET.bound' || parts.some(s => s === '' || s === '.' || s === '..'))
120
+ fail('unknown place namespace'); return parts.join('/'); });
121
+ const enter = regions.includes('') ? underTheRegions : underAuthorityRegions;
122
+ return enter(store, regions, 'place act', () => underRootProjection(store, 'place act publication', () => {
123
+ const prior = placeActResult(store, id, trust, limits);
124
+ if (prior) {
125
+ if (prior.context !== encoded)
126
+ fail('replay bytes differ');
127
+ // Replay authenticates the supplied evidence too. Historical authentication
128
+ // grants no current authority and leaves the committed result unchanged.
129
+ placeActReceiptsOf(encoded, receipts, trust.historical(encoded));
130
+ return { ...prior, identity: id, kind: 'once' };
131
+ }
132
+ const verified = verifyPlaceActContext(encoded, trust.contract);
133
+ placeActReceiptsOf(encoded, receipts, trust.contract);
134
+ const originals = decoded.places.map(p => p.records), files = { 'context.json': encoded, 'receipts.json': JSON.stringify(receipts) + '\n', 'originals.json': JSON.stringify(originals) + '\n' };
135
+ const inventory = storedInventory(store, limits);
136
+ if (inventory.bytes + Object.values(files).reduce((n, v) => n + Buffer.byteLength(v), 0) > limits.bytes)
137
+ fail('publication byte budget');
138
+ if (inventory.names.length >= limits.acts)
139
+ fail('publication act budget');
140
+ const current = trust.current();
141
+ if (rootBytes(current.root) !== rootBytes(verified.dependencies))
142
+ fail('authority changed before publication');
143
+ if (new Set(current.heads.map(h => h.place)).size !== current.heads.length)
144
+ fail('ambiguous receiver heads');
145
+ const storedHeads = placeActHeads(store, trust.initialHeads, trust, limits);
146
+ for (const p of decoded.places) {
147
+ const actual = storedHeads.find(h => h.place === p.place), selected = current.heads.find(h => h.place === p.place);
148
+ if (!actual || !selected || actual.identity !== selected.identity || actual.epoch !== selected.epoch)
149
+ fail('receiver snapshot is not committed head ' + p.place);
150
+ }
151
+ for (const p of decoded.places) {
152
+ const head = current.heads.find(h => h.place === p.place) ?? fail('missing receiver head ' + p.place);
153
+ if (head.identity !== p.previous)
154
+ fail('receiver head moved ' + p.place);
155
+ const next = clock(head.epoch) + 1;
156
+ if (!Number.isSafeInteger(next) || clock(p.epoch) !== next)
157
+ fail('receiver epoch not next ' + p.place);
158
+ }
159
+ publishBundle(at(store, id), files, storage);
160
+ return { context: encoded, receipts, originals, identity: id, kind: 'landed' };
161
+ }));
162
+ }
163
+ /** Enumerates only committed, historically authenticated carriers. Namespace
164
+ * projection is a caller's separate pure operation, never current permission. */
165
+ export function committedPlaceActResults(store, trust, limits) {
166
+ const { names } = storedInventory(store, limits);
167
+ return names.map(name => { const identity = decodeURIComponent(name.slice(10)), bundle = placeActResult(store, identity, trust, limits); if (!bundle)
168
+ fail('committed carrier vanished'); return { identity, bundle: bundle }; });
169
+ }
@@ -2,11 +2,16 @@ import { spawnSync } from 'node:child_process';
2
2
  import { processOf } from "../observe/runner.js";
3
3
  /** A synchronous fold waits on a supervising host process; the supervisor owns cancellation of its child group. */
4
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 });
5
+ if (!Number.isSafeInteger(limits.timeoutMs) || limits.timeoutMs <= 0 || !Number.isSafeInteger(limits.responseBytes) || limits.responseBytes <= 0)
6
+ throw Error('REFUSE·reader invalid process limits');
7
+ // The same monotonic deadline covers handoff, input and execution. Starting a
8
+ // fresh timer after stdin closes would leave the handoff outside the policy.
9
+ const deadline = process.hrtime.bigint() + BigInt(limits.timeoutMs) * 1000000n;
10
+ const run = spawnSync(process.execPath, [processOf('lifetime', 'host'), String(deadline), String(limits.responseBytes)], { input: JSON.stringify({ command, args, input, limits, cwd, env }), encoding: 'utf8', maxBuffer: limits.responseBytes * 12 + 4096 });
6
11
  if (run.error || run.status !== 0)
7
- throw Error(`REFUSE·reader lifetime supervisor ${run.error?.message ?? run.status}`);
12
+ throw Error(`REFUSE·reader lifetime supervisor ${run.error?.message ?? run.signal ?? run.status}`);
8
13
  const result = JSON.parse(run.stdout);
9
14
  if (result.failure)
10
- throw Error(`REFUSE·reader ${result.failure} · status ${result.status ?? result.signal ?? 'cancelled'}`);
15
+ throw Error(`REFUSE·reader ${result.failure} · status ${result.status ?? result.signal ?? 'cancelled'}${result.stderr ? ' · child stderr: ' + result.stderr : ''}`);
11
16
  return result;
12
17
  }
@@ -19,5 +19,30 @@ export declare function resolvePinned(specifier: string, context: ResolveContext
19
19
  readonly json: string;
20
20
  readonly exports: boolean;
21
21
  }): ResolveResult;
22
+ /** Fixed execution selects libraries by the instrument's explicit local mount,
23
+ * before opening any artifact. An alias or package-name guess grants nothing. */
24
+ export declare function selectNativeDependencies(root: string, dependencies: readonly string[], manifest: string): readonly {
25
+ line: string;
26
+ library: {
27
+ readonly name: string;
28
+ readonly manifest: string;
29
+ readonly coordinate?: string;
30
+ };
31
+ }[] | undefined;
32
+ /** A declared artifact is not automatically an active Node library. This fixed
33
+ * assertion reuses only the packages selected by the actual host boundary. */
34
+ export declare function reuseNativeResolution(pins: readonly {
35
+ pkg: string;
36
+ at: string;
37
+ }[]): boolean;
38
+ /** Read one resolved archive's manifest; a selected fixed library cannot vanish
39
+ * into the optional historical provider path when its archive is absent. */
40
+ export declare function readProvidedPin(name: string, at: string, file: string, requiredName?: string): {
41
+ at: string;
42
+ pkg: string;
43
+ exports: boolean;
44
+ href: string;
45
+ json: string;
46
+ } | undefined;
22
47
  export declare function provide(): void;
23
48
  export {};
@@ -6,7 +6,9 @@ import { fieldOf } from "../fold/claims.js";
6
6
  import { observeText } from "../observe/files.js";
7
7
  import { ownLock, ownStore, ownRoot } from "../observe/runner.js";
8
8
  import { runtimeTreeAt } from "./release.js";
9
+ import { ContentNeeded } from "./content-needed.js";
9
10
  import { theWord } from "../fold/wire.js";
11
+ const findPackageJSON = nodeModule.findPackageJSON;
10
12
  const registerHooks = nodeModule.registerHooks;
11
13
  const taken = (exp) => (typeof exp === 'string' ? exp : exp && typeof exp === 'object' && 'default' in exp && typeof exp.default === 'string' ? exp.default : '');
12
14
  /**
@@ -53,29 +55,129 @@ export function resolvePinned(specifier, context, nextResolve, pin) {
53
55
  * blob's folder.
54
56
  */
55
57
  let hooked = false;
58
+ function nativeResolution() {
59
+ const installed = Object.getOwnPropertyDescriptor(globalThis, Symbol.for('bound.native.resolution@1'));
60
+ if (!installed)
61
+ return undefined;
62
+ const capability = installed.value;
63
+ if (installed.configurable !== false || installed.writable !== false || !capability || !Object.isFrozen(capability) || capability.profile !== 'fixed-native-resolution@1' || !Array.isArray(capability.libraryNames) || !Object.isFrozen(capability.libraryNames) || capability.libraryNames.some(name => typeof name !== 'string') || !Array.isArray(capability.libraries) || !Object.isFrozen(capability.libraries) || capability.libraries.some(lib => !lib || !Object.isFrozen(lib) || typeof lib.name !== 'string' || typeof lib.manifest !== 'string') || typeof capability.assertPins !== 'function')
64
+ throw Error('REFUSE·pin fixed resolution capability invalid');
65
+ return capability;
66
+ }
67
+ /** Fixed execution selects libraries by the instrument's explicit local mount,
68
+ * before opening any artifact. An alias or package-name guess grants nothing. */
69
+ export function selectNativeDependencies(root, dependencies, manifest) {
70
+ const capability = nativeResolution();
71
+ if (!capability)
72
+ return undefined;
73
+ if (dependencies.length && capability.libraries.some(library => typeof library.coordinate !== 'string' || !library.coordinate))
74
+ throw Error('REFUSE·pin fixed resolution logical library coordinate missing');
75
+ const selected = new Map(capability.libraries.map(library => [library.coordinate, library]));
76
+ return dependencies.flatMap(line => {
77
+ const mount = fieldOf(line, 'needs');
78
+ if (!mount)
79
+ return [];
80
+ const library = selected.get(mount.replace(/\/+$/, '') + '/' + manifest);
81
+ if (!library)
82
+ return [];
83
+ let actual;
84
+ try {
85
+ actual = realpathSync(join(root, mount, manifest));
86
+ }
87
+ catch {
88
+ throw Error('REFUSE·pin selected library mount absent ' + library.name);
89
+ }
90
+ if (actual !== library.manifest)
91
+ throw Error('REFUSE·pin selected library mount differs ' + library.name);
92
+ return [{ line, library }];
93
+ });
94
+ }
95
+ /** A declared artifact is not automatically an active Node library. This fixed
96
+ * assertion reuses only the packages selected by the actual host boundary. */
97
+ export function reuseNativeResolution(pins) {
98
+ const capability = nativeResolution();
99
+ if (!capability)
100
+ return false;
101
+ const selected = new Set(capability.libraryNames);
102
+ // The fixed preload owns this closed assertion. Package names select which
103
+ // declared pins participate; exact manifests and member bytes still decide.
104
+ capability.assertPins(pins.filter(pin => selected.has(pin.pkg)).map(pin => ({ name: pin.pkg, at: pin.at })));
105
+ return true;
106
+ }
107
+ /** Read one resolved archive's manifest; a selected fixed library cannot vanish
108
+ * into the optional historical provider path when its archive is absent. */
109
+ export function readProvidedPin(name, at, file, requiredName) {
110
+ if (!at) {
111
+ if (requiredName !== undefined)
112
+ throw Error('REFUSE·pin fixed resolution requires declared library ' + name);
113
+ return undefined;
114
+ }
115
+ const manifest = JSON.parse(observeText(join(at, file)) ?? '{}'), pkg = typeof manifest.name === 'string' ? manifest.name : '';
116
+ if (requiredName !== undefined && pkg !== requiredName)
117
+ throw Error('REFUSE·pin fixed resolution library name differs ' + name);
118
+ return pkg ? { at, pkg, exports: Object.prototype.hasOwnProperty.call(manifest, 'exports'), href: pathToFileURL(at).href, json: pathToFileURL(join(at, file)).href } : undefined;
119
+ }
56
120
  export function provide() {
57
121
  if (hooked)
58
122
  return;
59
- hooked = true;
60
- const pins = ownLock()
61
- .filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads')
62
- .flatMap((line) => {
63
- const name = fieldOf(line, 'scope').slice('dep/'.length);
64
- const at = pinPlace(name);
65
- const pkg = pinName(name);
66
- const file = manifestOf();
67
- const manifest = JSON.parse(observeText(join(at, file)) ?? '{}');
68
- return at && pkg ? [{ at, pkg, exports: Object.prototype.hasOwnProperty.call(manifest, 'exports'), href: pathToFileURL(at).href, json: pathToFileURL(join(at, file)).href }] : [];
123
+ const dependencies = ownLock().filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads');
124
+ const file = manifestOf(), selected = selectNativeDependencies(ownRoot(), dependencies, file);
125
+ const load = (line, requiredName) => {
126
+ const name = fieldOf(line, 'scope').slice('dep/'.length), at = pinPlace(name);
127
+ if (!at)
128
+ throw new ContentNeeded(fieldOf(line, 'value'), 'REFUSE·pin declared library unavailable ' + name);
129
+ const pin = readProvidedPin(name, at, file, requiredName);
130
+ if (!pin)
131
+ throw Error('REFUSE·pin declared library unavailable ' + name);
132
+ return pin;
133
+ };
134
+ if (selected !== undefined) {
135
+ reuseNativeResolution(selected.map(({ line, library }) => load(line, library.name)));
136
+ hooked = true;
137
+ return;
138
+ }
139
+ // A mount selects a library, but its manifest is only a routing hint until
140
+ // an import requests it. The selected archive alone supplies executable bytes.
141
+ const mounts = dependencies.filter(line => fieldOf(line, 'needs') !== '').map(line => {
142
+ const mount = join(ownRoot(), fieldOf(line, 'needs'));
143
+ const manifest = JSON.parse(observeText(join(mount, file)) ?? '{}');
144
+ return { line, mount, pkg: typeof manifest.name === 'string' ? manifest.name : '' };
69
145
  });
146
+ const verified = new Map();
70
147
  registerHooks({
71
148
  resolve(specifier, context, nextResolve) {
72
149
  if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.includes(':'))
73
150
  return nextResolve(specifier, context);
74
- const parent = context.parentURL ?? '';
75
- const pin = pins.find((one) => specifier === one.pkg || specifier.startsWith(`${one.pkg}/`));
76
- if (pin === undefined || parent.startsWith(pin.href))
151
+ let selected = mounts.find(one => one.pkg && (specifier === one.pkg || specifier.startsWith(one.pkg + '/')));
152
+ if (!selected) {
153
+ // Identify the installed package before following its executable entry:
154
+ // a renamed manifest and an escaping entry cannot bypass the declared mount.
155
+ let manifest;
156
+ try {
157
+ manifest = findPackageJSON(specifier, pathToFileURL(join(ownRoot(), file)).href);
158
+ }
159
+ catch { }
160
+ if (manifest)
161
+ selected = mounts.find(one => {
162
+ try {
163
+ return realpathSync(join(one.mount, file)) === realpathSync(manifest);
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ });
169
+ }
170
+ if (!selected)
77
171
  return nextResolve(specifier, context);
172
+ let pin = verified.get(selected.line);
173
+ if (!pin) {
174
+ pin = load(selected.line);
175
+ verified.set(selected.line, pin);
176
+ }
177
+ if (specifier !== pin.pkg && !specifier.startsWith(pin.pkg + '/'))
178
+ throw Error('REFUSE·pin selected library name differs');
78
179
  return resolvePinned(specifier, context, nextResolve, pin);
79
180
  },
80
181
  });
182
+ hooked = true;
81
183
  }
@@ -1,5 +1,14 @@
1
1
  import type { ReaderLifetime, ReaderReference } from '@lapxo/topos/wire';
2
2
  import type { Capsule } from './capsule.ts';
3
+ import { snapshotAccess } from '@lapxo/topos/contract';
4
+ import type { ReadingIdentityContext, Snapshot, ResponseContext } from '@lapxo/topos/contract';
5
+ import type { ReadingExecution, ReadingReference } from './reading-context.ts';
6
+ export interface CurrentReading {
7
+ readonly snapshot: Snapshot;
8
+ readonly kept: ResponseContext;
9
+ readonly current: ResponseContext;
10
+ readonly permission: Parameters<typeof snapshotAccess>[3];
11
+ }
3
12
  type File = {
4
13
  readonly place: string;
5
14
  readonly text: string;
@@ -18,7 +27,12 @@ export declare function readingClosures(options: {
18
27
  readerReference?: ReaderReference;
19
28
  lifetime?: ReaderLifetime;
20
29
  allowsEmpty?: boolean;
30
+ context?: ReadingIdentityContext;
31
+ execution?: ReadingExecution;
32
+ current?: CurrentReading;
21
33
  files: readonly File[];
34
+ livePlace?: () => readonly string[];
35
+ assertCurrent?: () => void;
22
36
  emit?: (line: string) => void;
23
37
  }): {
24
38
  lines: readonly string[];
@@ -27,5 +41,5 @@ export declare function readingClosures(options: {
27
41
  executed: number;
28
42
  };
29
43
  /** Verify the native reading carriers before a file seal may reuse them. These are semantic receipts, not file-region seals. */
30
- export declare function verifiedReadingReceipts(store: string, lines: readonly string[], digest: (text: string) => string): number | undefined;
44
+ export declare function verifiedReadingReceipts(store: string, lines: readonly string[], digest: (text: string) => string, environment?: readonly ReadingReference[] | ((context: ReadingIdentityContext) => readonly ReadingReference[] | undefined), requireContext?: boolean): number | undefined;
31
45
  export {};
@@ -5,12 +5,48 @@ import { canonical, parse, PROTOCOL, byBytes } from '@lapxo/topos/wire';
5
5
  import { replaceWhole, landShard, shardLines } from "../land/ledger.js";
6
6
  import { fieldOf } from "../fold/claims.js";
7
7
  import { readOnly } from "./read-only.js";
8
+ import { readingIdentityBytes, readingReceiptIdentity, responseIdentity, snapshotAccess, placeInputs, withPlaceInputs } from '@lapxo/topos/contract';
9
+ import { withReadingBoundary } from "./reading-context.js";
10
+ const receiptScope = (scope) => [scope, 'identity'].join('/');
8
11
  const fields = (line) => { const got = parse(line); if (got.kind !== 'fact')
9
12
  throw Error('REFUSE·input projection contains a non-wire line'); return got.value.fields; };
10
13
  export const inputLines = (files) => files.map(file => canonical({ scope: file.place, form: 'alphabet', measure: 'text', role: 'writes', value: 'lock', shape: file.place, about: file.text, by: 'host', at: 'policy:handed-input' }));
11
14
  /** The selected world owns input projection and reading. The host only verifies closures and keeps native count/bytes receipts. */
12
15
  export function readingClosures(options) {
13
16
  const { root, store, reader, speaker, digest: hash, capsule, region, projection, files } = options, emit = options.emit ?? (() => { });
17
+ options.assertCurrent?.();
18
+ if (options.context)
19
+ readingIdentityBytes(options.context);
20
+ let access = [];
21
+ if (options.current) {
22
+ if (!options.context)
23
+ throw Error('REFUSE·current reading requires a declared identity');
24
+ const { snapshot, kept, current, permission } = options.current;
25
+ if (current.implementation !== options.context.implementation)
26
+ throw Error('REFUSE·current reading implementation differs from its verified context');
27
+ const response = responseIdentity(snapshot, current, hash), allowed = snapshotAccess(snapshot, kept, current, permission, hash);
28
+ if (!allowed.inspect || !allowed.reuse)
29
+ throw Error('REFUSE·current reading is not admitted for reuse');
30
+ access = [['response', response], ['permission', hash(JSON.stringify([permission.inspect, permission.reuse, permission.freshness]))]];
31
+ }
32
+ if (options.execution && (!options.context || JSON.stringify(readingIdentityBytes(options.execution.context)) !== JSON.stringify(readingIdentityBytes(options.context))))
33
+ throw Error('REFUSE·reading execution differs from declared context');
34
+ const projectionFor = (name) => {
35
+ if (!capsule.selectedWorld)
36
+ return undefined;
37
+ const { standing, offer } = capsule.selectedWorld;
38
+ const selectors = standing.parts.some(part => part.scope === `${offer}/place-inputs/${name}` && part.measure === 'reads');
39
+ if (!selectors)
40
+ return undefined;
41
+ if (!options.context || !options.execution || !options.livePlace)
42
+ throw Error('REFUSE·place inputs require authenticated shadow and reading context ' + name);
43
+ return placeInputs(standing, offer, name, options.livePlace(), bytes => hash(new TextDecoder().decode(bytes)));
44
+ };
45
+ const projectedPlace = projectionFor(projection), readingPlace = projectionFor(region);
46
+ const commit = (context, name, place) => place && context ? withPlaceInputs(context, JSON.stringify(['place-inputs', capsule.selectedWorld.offer, name]), place) : context;
47
+ const ask = (identity, requests, context = options.context) => withReadingBoundary(identity, options.execution && context ? { ...options.execution, context } : options.execution, () => capsule.ask(requests, options.readerReference));
48
+ const projectionContext = commit(options.context, projection, projectedPlace);
49
+ const base = projectionContext ? hash(new TextDecoder().decode(readingIdentityBytes(projectionContext))) : '';
14
50
  const algorithm = hash('').split(':')[0], handed = new Map(files.map(file => [file.place, file.text]));
15
51
  if (handed.size !== files.length)
16
52
  throw Error('REFUSE·input projection has duplicate handed coordinates');
@@ -18,7 +54,7 @@ export function readingClosures(options) {
18
54
  if (!readFileSync(join(root, file.place)).equals(Buffer.from(file.text)))
19
55
  throw Error('REFUSE·input changed ' + file.place); };
20
56
  verify();
21
- const projected = capsule.ask([{ protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }], options.readerReference)[0];
57
+ const projected = ask(base, [{ ...(projectedPlace ? { place: projectedPlace } : {}), protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }], projectionContext)[0];
22
58
  if (projected?.kind !== 'fact' || !Array.isArray(projected.lines))
23
59
  throw Error('REFUSE·input projection ' + (projected?.why ?? 'no lines'));
24
60
  const plans = new Map();
@@ -56,7 +92,12 @@ export function readingClosures(options) {
56
92
  let read = 0, opened = 0, executed = 0;
57
93
  for (const plan of ordered) {
58
94
  actCosts.measure(() => {
59
- const closure = [plan.line, canonical({ scope: 'implementation', role: 'writes', form: 'alphabet', measure: 'digest', value: identity, by: 'host', at: 'policy:input-identity' }), ...plan.inputs.map(input => canonical({ scope: input, role: 'writes', form: 'alphabet', measure: 'digest', value: hash(handed.get(input)), by: 'host', at: 'policy:input-identity' })), ...plan.needs.map(need => canonical({ scope: need, role: 'writes', form: 'alphabet', measure: 'digest', value: results.get(need).digest, by: 'host', at: 'policy:input-identity' }))].sort(byBytes).join('\n') + '\n';
95
+ // Retained evidence does not authorize a current answer.
96
+ options.assertCurrent?.();
97
+ const legacyClosure = [plan.line, canonical({ scope: 'implementation', role: 'writes', form: 'alphabet', measure: 'digest', value: identity, by: 'host', at: 'policy:input-identity' }), ...plan.inputs.map(input => canonical({ scope: input, role: 'writes', form: 'alphabet', measure: 'digest', value: hash(handed.get(input)), by: 'host', at: 'policy:input-identity' })), ...plan.needs.map(need => canonical({ scope: need, role: 'writes', form: 'alphabet', measure: 'digest', value: results.get(need).digest, by: 'host', at: 'policy:input-identity' }))].sort(byBytes).join('\n') + '\n';
98
+ const leafContext = commit(options.context, region, readingPlace);
99
+ const context = leafContext && { ...leafContext, policy: hash(JSON.stringify([leafContext.policy, options.allowsEmpty === true, options.lifetime ?? null, options.readerReference ?? null, capsule.selection ?? null, capsule.lines])), inputs: [...[...leafContext.inputs, ...access].map(([name, digest]) => [JSON.stringify(['context', name]), digest]), [JSON.stringify(['plan']), hash(plan.line)], ...plan.inputs.map(input => [JSON.stringify(['file', input]), hash(handed.get(input))]), ...plan.needs.map(need => [JSON.stringify(['receipt', need]), results.get(need).digest])] };
100
+ const closure = context ? new TextDecoder().decode(readingIdentityBytes(context)) : legacyClosure;
60
101
  const input = hash(closure), scope = 'receipts/readings/' + hash(plan.scope).split(':')[1] + '/' + input.split(':')[1], matching = prior.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'value') === input).at(-1);
61
102
  const count = matching && prior.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === by).at(-1);
62
103
  const heldOutput = count && load(fieldOf(count, 'at').replace(/^place:/, ''));
@@ -75,7 +116,7 @@ export function readingClosures(options) {
75
116
  if (readOnly())
76
117
  throw Error('REFUSE·preview reading closure is unpaid ' + plan.scope);
77
118
  const started = process.hrtime.bigint();
78
- const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = capsule.ask([{ protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }], options.readerReference)[0];
119
+ const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = ask(input, [{ ...(readingPlace ? { place: readingPlace } : {}), protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }], context)[0];
79
120
  if (answer?.kind !== 'fact' || !Array.isArray(answer.claims))
80
121
  throw Error('REFUSE·reading ' + plan.scope + ' · ' + (answer?.why ?? 'no claims'));
81
122
  lines = answer.claims.map((unknown) => { if (typeof unknown?.scope !== 'string' || typeof unknown?.measure !== 'string' || typeof unknown?.role !== 'string' || !unknown.bound || !['interval', 'enumerated'].includes(unknown.bound.kind))
@@ -84,6 +125,7 @@ export function readingClosures(options) {
84
125
  throw Error('REFUSE·reading empty response is not admitted ' + plan.scope);
85
126
  executed++;
86
127
  verify();
128
+ options.assertCurrent?.();
87
129
  payload = saved(lines.length ? [...lines].sort(byBytes).join('\n') + '\n' : '');
88
130
  if (saved(closure) !== input)
89
131
  throw Error('REFUSE·receipt input closure mismatch');
@@ -92,8 +134,17 @@ export function readingClosures(options) {
92
134
  if (plan.resolution === 1)
93
135
  opened++;
94
136
  }
95
- results.set(plan.scope, { digest: payload, lines });
96
- out.push(canonical({ scope: plan.scope, role: 'writes', form: 'alphabet', measure: 'observed', value: input, by, at: 'place:' + input }), ...lines);
137
+ const receipt = context ? readingReceiptIdentity(input, payload, bytes => hash(new TextDecoder().decode(bytes))) : payload;
138
+ if (context)
139
+ out.push(canonical({ scope: receiptScope(scope), role: 'writes', form: 'alphabet', measure: 'digest', value: receipt, by, at: 'place:' + input }));
140
+ results.set(plan.scope, { digest: receipt, lines });
141
+ out.push(canonical({ scope: plan.scope, role: 'writes', form: 'alphabet', measure: 'observed', value: input, by, at: 'place:' + input }),
142
+ // A logical reading name is not a physical input coordinate. Preserve the
143
+ // host-verified handoff beside the result so portable file seals retain
144
+ // every input, including explicitly read foreign files.
145
+ // Input witnesses do not locate the result at each input. Result attribution
146
+ // remains the logical reading's place; two inputs are not two new origins.
147
+ ...plan.inputs.map(coordinate => canonical({ scope: coordinate, role: 'writes', form: 'alphabet', measure: 'observed', value: hash(handed.get(coordinate)), by, at: 'witness:' + input })), ...lines);
97
148
  if (plan.resolution === 1) {
98
149
  read++;
99
150
  const native = shardLines(store, speaker, journal).filter(line => fieldOf(line, 'scope') === scope && ((fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'value') === input) || (fieldOf(line, 'measure') === 'count') || (fieldOf(line, 'measure') === 's' && fieldOf(line, 'at') === 'place:' + input))).map(line => {
@@ -116,7 +167,7 @@ export function readingClosures(options) {
116
167
  return { lines: out, read, opened, executed };
117
168
  }
118
169
  /** Verify the native reading carriers before a file seal may reuse them. These are semantic receipts, not file-region seals. */
119
- export function verifiedReadingReceipts(store, lines, digest) {
170
+ export function verifiedReadingReceipts(store, lines, digest, environment, requireContext = false) {
120
171
  const units = lines.filter(line => fieldOf(line, 'scope').startsWith('receipts/readings/') && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'role') === 'writes');
121
172
  const load = (id, row) => {
122
173
  const hex = id.split(':')[1];
@@ -139,6 +190,37 @@ export function verifiedReadingReceipts(store, lines, digest) {
139
190
  const count = counts[0], output = load(fieldOf(count, 'at').replace(/^place:/, ''), count);
140
191
  if (output === undefined)
141
192
  return;
193
+ const inputBytes = load(input, unit);
194
+ if (inputBytes.trimStart().startsWith('[')) {
195
+ let context;
196
+ try {
197
+ context = JSON.parse(inputBytes);
198
+ }
199
+ catch {
200
+ return;
201
+ }
202
+ if (!Array.isArray(context) || context.length !== 6 || context[0] !== 'reading-context@1')
203
+ return;
204
+ const canonicalContext = { instrument: context[1], implementation: context[2], policy: context[3], inputs: context[4], environment: context[5] };
205
+ try {
206
+ if (new TextDecoder().decode(readingIdentityBytes(canonicalContext)) !== inputBytes)
207
+ return;
208
+ }
209
+ catch {
210
+ return;
211
+ }
212
+ if (environment) {
213
+ const expected = typeof environment === 'function' ? environment(canonicalContext) : environment;
214
+ if (expected === undefined || JSON.stringify([...expected].sort((a, b) => byBytes(a[0], b[0]))) !== JSON.stringify(context[5]))
215
+ return;
216
+ }
217
+ const identity = lines.filter(line => fieldOf(line, 'scope') === receiptScope(scope) && fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'by') === by && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'at') === 'place:' + input);
218
+ const expected = readingReceiptIdentity(input, fieldOf(count, 'at').replace(/^place:/, ''), bytes => digest(new TextDecoder().decode(bytes)));
219
+ if (identity.length !== 1 || fieldOf(identity[0], 'value') !== expected)
220
+ return;
221
+ }
222
+ else if (requireContext || !inputBytes.endsWith('\n') || inputBytes.split('\n').filter(Boolean).some(line => parse(line).kind !== 'fact'))
223
+ return;
142
224
  const rows = output.split('\n').filter(Boolean);
143
225
  if (fieldOf(count, 'value') !== `${rows.length}..${rows.length}` || rows.some(line => fieldOf(line, 'by') !== by || fieldOf(line, 'at') !== 'place:' + input))
144
226
  return;
@@ -0,0 +1,29 @@
1
+ import type { ReadingIdentityContext } from '@lapxo/topos/contract';
2
+ export type ReadingReference = readonly [string, string];
3
+ export interface ReadingExecution {
4
+ readonly context: ReadingIdentityContext;
5
+ readonly env: Readonly<Record<string, string>>;
6
+ readonly cwd: string;
7
+ readonly flags: readonly string[];
8
+ }
9
+ interface Boundary {
10
+ readonly identity: string;
11
+ readonly execution?: ReadingExecution;
12
+ }
13
+ export declare const readingBoundary: () => Boundary | undefined;
14
+ export declare function withReadingBoundary<T>(identity: string, execution: ReadingExecution | undefined, run: () => T): T;
15
+ export declare function readingEnvironment(hash: (text: string) => string, env?: Readonly<Record<string, string | undefined>>, cwd?: string, flags?: readonly string[]): readonly ReadingReference[];
16
+ export declare function readingProfile(lines: readonly string[], field: (line: string, name: string) => string): boolean;
17
+ /** The caller supplies verified implementation and transitive references. This
18
+ * constructor adds the process boundary that the host will actually hand over. */
19
+ export declare function readingExecution(input: {
20
+ instrument: string;
21
+ implementation: string;
22
+ policy: string;
23
+ inputs: readonly ReadingReference[];
24
+ }, hash: (text: string) => string, boundary?: {
25
+ env: Readonly<Record<string, string | undefined>>;
26
+ cwd: string;
27
+ flags: readonly string[];
28
+ }): ReadingExecution;
29
+ export {};