@lapxo/bound 0.2.2 → 0.2.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 (79) hide show
  1. package/.github/workflows/release.yml +67 -0
  2. package/.github/workflows/test.yml +35 -0
  3. package/CITATION.cff +19 -0
  4. package/TARGET.bound +80 -21
  5. package/dist/cli/accept.js +103 -71
  6. package/dist/cli/place.js +3 -2
  7. package/dist/cli/standing-result.js +3 -5
  8. package/dist/cli/takes.js +4 -3
  9. package/dist/cli/verb.js +2 -1
  10. package/dist/engine/ports.d.ts +6 -1
  11. package/dist/engine/ports.js +23 -9
  12. package/dist/fold/claims.d.ts +29 -0
  13. package/dist/fold/claims.js +2 -1
  14. package/dist/fold/closed.js +2 -1
  15. package/dist/fold/digests.js +7 -7
  16. package/dist/host/admitted-place.js +3 -31
  17. package/dist/host/call-context.d.ts +20 -0
  18. package/dist/host/call-context.js +14 -0
  19. package/dist/host/capsule-process.js +2 -1
  20. package/dist/host/capsule.js +7 -4
  21. package/dist/host/commit-result.d.ts +6 -0
  22. package/dist/host/commit-result.js +5 -0
  23. package/dist/host/contextual-place.js +9 -14
  24. package/dist/host/host-controls.js +19 -0
  25. package/dist/host/in-process-act.d.ts +82 -0
  26. package/dist/host/in-process-act.js +84 -0
  27. package/dist/host/in-process-context.d.ts +46 -0
  28. package/dist/host/in-process-context.js +91 -0
  29. package/dist/host/in-process-root.d.ts +117 -0
  30. package/dist/host/in-process-root.js +84 -0
  31. package/dist/host/in-process.d.ts +41 -0
  32. package/dist/host/in-process.js +55 -0
  33. package/dist/host/index.d.ts +15 -0
  34. package/dist/host/index.js +7 -0
  35. package/dist/host/native/loader.js +1 -1
  36. package/dist/host/native/module-closure.d.ts +35 -0
  37. package/dist/host/native/module-closure.js +34 -3
  38. package/dist/host/native/module-delivery.d.ts +62 -0
  39. package/dist/host/native/module-delivery.js +151 -0
  40. package/dist/host/native/module-syntax.js +3 -1
  41. package/dist/host/native-receiver.d.ts +19 -0
  42. package/dist/host/native-receiver.js +25 -0
  43. package/dist/host/order.js +2 -0
  44. package/dist/host/place-act-transaction.d.ts +24 -0
  45. package/dist/host/place-act-transaction.js +78 -0
  46. package/dist/host/place-clock-selection.d.ts +55 -0
  47. package/dist/host/place-clock-selection.js +1 -1
  48. package/dist/host/ports/local-act.d.ts +44 -0
  49. package/dist/host/ports/local-act.js +13 -2
  50. package/dist/host/ports/place-act.d.ts +49 -0
  51. package/dist/host/ports/place-act.js +19 -54
  52. package/dist/host/ports/publication.d.ts +10 -0
  53. package/dist/host/root-clock-source.d.ts +86 -0
  54. package/dist/host/root-clock-source.js +7 -5
  55. package/dist/host/signature-era.js +12 -0
  56. package/dist/host/signing.js +1 -1
  57. package/dist/host/source-content.js +2 -1
  58. package/dist/host/standing-carrier.js +18 -0
  59. package/dist/host/standing-projection.js +50 -0
  60. package/dist/land/act.js +60 -26
  61. package/dist/observe/run.js +2 -1
  62. package/dist/observe/runner.js +16 -8
  63. package/docs/contracts/closure-delivery.md +23 -0
  64. package/docs/contracts/closure-delivery.publication.json +25 -0
  65. package/docs/contracts/native-host.md +21 -0
  66. package/docs/contracts/native-host.publication.json +25 -0
  67. package/docs/contracts/native-publication.inventory.json +2 -1
  68. package/docs/contracts/publication.inventory.json +29 -0
  69. package/docs/guide.md +10 -0
  70. package/docs/reference.md +39 -0
  71. package/docs/why.md +109 -0
  72. package/package.json +43 -2
  73. package/receipts.bound +10405 -9625
  74. package/samples/module-delivery/inputs/delivery.json +1 -0
  75. package/samples/native-host/no.json +10 -0
  76. package/samples/native-host/yes.json +20 -0
  77. package/samples/no/module-delivery/delivery.json +1 -0
  78. package/vectors/native/module-delivery.vector.json +32 -0
  79. package/vectors/native/native-host.vector.json +62 -0
@@ -0,0 +1,86 @@
1
+ import { type PlaceReceipt, type ClockBudget } from '@lapxo/topos/core';
2
+ import type { PlaceActBudget } from './ports/place-act.ts';
3
+ export interface RootClockSourceSelection {
4
+ readonly selector: string;
5
+ readonly profileSelector: string;
6
+ readonly bootstrapCheckpoint: {
7
+ scope: string;
8
+ measure: string;
9
+ };
10
+ readonly resources: {
11
+ bytes: number;
12
+ lines: number;
13
+ places: number;
14
+ acts: number;
15
+ };
16
+ readonly inherited: {
17
+ clock: ClockBudget;
18
+ storage: PlaceActBudget;
19
+ };
20
+ }
21
+ export declare function rootClockSource(root: string, selected: RootClockSourceSelection): {
22
+ observation: Readonly<{
23
+ history: readonly string[];
24
+ anchor: import("@lapxo/topos/wire").Signer;
25
+ algorithms: readonly string[];
26
+ }>;
27
+ resources: Readonly<{
28
+ bytes: number;
29
+ lines: number;
30
+ places: number;
31
+ acts: number;
32
+ }>;
33
+ authority: (encoded: string) => {
34
+ anchor: import("@lapxo/topos/wire").Signer;
35
+ history: readonly string[];
36
+ checkpoint: string;
37
+ };
38
+ selection: import("./place-clock-selection.ts").HostClockSelection;
39
+ trust: import("./ports/place-act.ts").PlaceActTrust;
40
+ };
41
+ /** Source mapping is an explicit Root declaration; absence never means a zero
42
+ * clock or an inferred cursor. Existing legacy callers do not request it. */
43
+ export declare function rootClockSelectionOf(live: readonly string[], anchor: import('@lapxo/topos/wire').Signer): RootClockSourceSelection;
44
+ export declare function declaredRootClockSource(root: string): {
45
+ observation: Readonly<{
46
+ history: readonly string[];
47
+ anchor: import("@lapxo/topos/wire").Signer;
48
+ algorithms: readonly string[];
49
+ }>;
50
+ resources: Readonly<{
51
+ bytes: number;
52
+ lines: number;
53
+ places: number;
54
+ acts: number;
55
+ }>;
56
+ authority: (encoded: string) => {
57
+ anchor: import("@lapxo/topos/wire").Signer;
58
+ history: readonly string[];
59
+ checkpoint: string;
60
+ };
61
+ selection: import("./place-clock-selection.ts").HostClockSelection;
62
+ trust: import("./ports/place-act.ts").PlaceActTrust;
63
+ };
64
+ export declare function hasRootClockSource(root: string): boolean;
65
+ export interface PortablePlaceContext {
66
+ readonly identity: string;
67
+ readonly context: string;
68
+ readonly receipts: readonly PlaceReceipt[];
69
+ readonly authority: {
70
+ anchor: import('@lapxo/topos/wire').Signer;
71
+ history: readonly string[];
72
+ checkpoint: string;
73
+ };
74
+ }
75
+ /** Historical inspection ONLY. The expected acting Root anchor and resource
76
+ * selection come from the consumer's selected place, never from this packet.
77
+ * This function returns data and cannot construct a current command permit. */
78
+ export declare function inspectPortablePlaceContexts(encoded: string, expectedAnchor: import('@lapxo/topos/wire').Signer, selected: RootClockSourceSelection): {
79
+ identity: string;
80
+ context: import("@lapxo/topos/core").PlaceAct;
81
+ }[];
82
+ /** Prospective configuration is checked as authenticated Root history, never
83
+ * against a prose birth condition. Initial heads are explicit typed selections;
84
+ * stored committed chains cannot disappear when a selection is activated or
85
+ * replaced. Preview reads only; the real admission repeats under its Root lock. */
86
+ export declare function assertProspectiveRootClockSelection(root: string, signed: readonly string[]): void;
@@ -6,7 +6,7 @@ import { committedLocalActs, actLines } from "./ports/local-act.js";
6
6
  import { verifiedLegacyActResult } from "../cli/act-result.js";
7
7
  import { admissionAnchor, admissionHistoryAt } from "../fold/admission-history.js";
8
8
  import { authorityFor } from "../fold/signers.js";
9
- import { eraOf } from "../fold/digests.js";
9
+ import { signatureEraOf as eraOf } from "./signature-era.js";
10
10
  import { fieldOf, foldClaims, isConfig, selfName } from "../fold/claims.js";
11
11
  import { authenticatedRootStanding } from "./admitted-place.js";
12
12
  import { verifiesOwnerLine } from "../land/sign.js";
@@ -158,11 +158,11 @@ export function rootClockSource(root, selected) {
158
158
  }
159
159
  /** Source mapping is an explicit Root declaration; absence never means a zero
160
160
  * clock or an inferred cursor. Existing legacy callers do not request it. */
161
- export function declaredRootClockSource(root) {
162
- const live = authenticatedRootStanding(root), rows = live.filter(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw');
161
+ export function rootClockSelectionOf(live, anchor) {
162
+ const rows = live.filter(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw');
163
163
  if (rows.length !== 1)
164
164
  fail('requires one admitted source mapping');
165
- const line = rows[0], anchor = admissionAnchor(root), algorithms = eraOf(live).admitted;
165
+ const line = rows[0], algorithms = eraOf(live).admitted;
166
166
  if (fieldOf(line, 'by') !== anchor.id || fieldOf(line, 'role') !== 'writes' || fieldOf(line, 'form') !== 'alphabet' || fieldOf(line, 'value') !== 'lock' || !verifiesOwnerLine(line, anchor.publicKey, algorithms))
167
167
  fail('mapping is not Root signed');
168
168
  const about = fieldOf(line, 'about');
@@ -175,8 +175,10 @@ export function declaredRootClockSource(root) {
175
175
  }
176
176
  if (!value || Object.keys(value).sort().join(',') !== 'bootstrapCheckpoint,inherited,profile,profileSelector,resources,selector' || value.profile !== 'place-act-context@1' || JSON.stringify(value) !== about)
177
177
  fail('unknown/noncanonical mapping');
178
- return rootClockSource(root, value);
178
+ sourceSelection(value);
179
+ return value;
179
180
  }
181
+ export function declaredRootClockSource(root) { return rootClockSource(root, rootClockSelectionOf(authenticatedRootStanding(root), admissionAnchor(root))); }
180
182
  export function hasRootClockSource(root) { return authenticatedRootStanding(root).some(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw'); }
181
183
  /** Historical inspection ONLY. The expected acting Root anchor and resource
182
184
  * selection come from the consumer's selected place, never from this packet.
@@ -0,0 +1,12 @@
1
+ import { wireLine } from "../fold/wire.js";
2
+ import { fieldOf } from "../fold/claims.js";
3
+ /** Pure signature vocabulary; no instrument discovery for diagnostics. */
4
+ export function signatureEraOf(standing) {
5
+ const said = wireLine(standing, 'signature-algorithms');
6
+ const admitted = (said === undefined ? [] : fieldOf(said, 'value').split('|')).filter(Boolean);
7
+ const current = wireLine(standing, 'era');
8
+ const era = (current === undefined ? undefined : admitted.find((one) => one.endsWith(`:${fieldOf(current, 'value')}`))) ?? admitted.find((one) => one.includes(':'));
9
+ if (era === undefined)
10
+ throw new Error(`REFUSE·runtime the lock names no signature algorithm by digest: nothing can be signed here`);
11
+ return { admitted, era };
12
+ }
@@ -40,7 +40,7 @@ export function signerFor(lines, keyId, selected, keyFile, injected) {
40
40
  if (injected) {
41
41
  if (injected.name !== declaration.name || keyFile)
42
42
  throw new SigningFailure('REFUSE·signer injected host binding conflicts with declaration or key file');
43
- return (lot) => invokeSigningPort(lot, injected.sign, { timeoutMs: declaration.timeoutMs, maxBytes: declaration.responseBytes });
43
+ return (lot) => invokeSigningPort(lot, injected.sign, { timeoutMs: declaration.timeoutMs, maxBytes: declaration.responseBytes }, injected.signal);
44
44
  }
45
45
  const mechanism = binding(declaration.name, keyFile);
46
46
  const protocol = { request: signerRequestBytes, response: signerResponseBytes, reply: signerResponseOf };
@@ -1,3 +1,4 @@
1
+ import { hostOutput } from "./call-context.js";
1
2
  import { transportBytes } from "./ports/transport.js";
2
3
  import { contentStore, verifiedContent } from "./content-store.js";
3
4
  import { ContentNeeded } from "./content-needed.js";
@@ -91,7 +92,7 @@ export async function resolveSources(standing, requested) {
91
92
  fetch: async (url, digest) => transportBytes(url, digest, [...ownLock(), ...standing]),
92
93
  }, requested);
93
94
  for (const digest of received)
94
- process.stdout.write(`FETCHED ${digest} · verified\n`);
95
+ hostOutput('out', `FETCHED ${digest} · verified\n`);
95
96
  }
96
97
  /** A changed verified store is the only reason to retry. A repeated miss never loops or changes a named pin. */
97
98
  export async function resolveRequested(resolve, fold) {
@@ -0,0 +1,18 @@
1
+ import { PLACE_ACT_CONTEXT } from '@lapxo/topos/core';
2
+ import { foldClaims, fieldOf } from "../fold/claims.js";
3
+ /** Projection only, after the caller authenticates the complete carrier. The
4
+ * CLI and in-process reader use this same fold and output shape. */
5
+ export function projectContextualStanding(root, legacy, contexts, decoded) {
6
+ const groups = new Map();
7
+ for (const act of contexts) {
8
+ const context = decoded.find(one => one.identity === act.identity).context;
9
+ for (const p of context.places) {
10
+ const key = JSON.stringify([context.root.namespace, p.place]), group = groups.get(key) ?? { namespace: context.root.namespace, place: p.place, lines: [], contexts: [] };
11
+ group.lines.push(...p.records);
12
+ group.contexts.push({ identity: act.identity, context: act.context, receipts: act.receipts });
13
+ groups.set(key, group);
14
+ }
15
+ }
16
+ const legacyPlaces = legacy.map(place => ({ namespace: 'legacy-global', place: place.place, standing: foldClaims(place.history).standing.filter(line => fieldOf(line, 'value') !== 'withdraw'), history: place.history }));
17
+ return { profile: PLACE_ACT_CONTEXT, root, legacyPlaces, contexts, places: [...groups.values()].map(group => { const folded = foldClaims(group.lines); return { namespace: group.namespace, place: group.place, standing: folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'), retracted: folded.retracted, superseded: folded.superseded, forks: folded.forks, contexts: group.contexts }; }) };
18
+ }
@@ -0,0 +1,50 @@
1
+ import { viewsOf } from "../fold/views.js";
2
+ import { BoundRefusal } from "./refusals.js";
3
+ import { authorityFor } from "../fold/signers.js";
4
+ import { signatureEraOf as eraOf } from "./signature-era.js";
5
+ import { fieldOf, foldClaims, isConfig } from "../fold/claims.js";
6
+ import { foldedEvidence } from '@lapxo/topos/contract';
7
+ /** Project the supplied signed history against the host's original trust anchor.
8
+ * No current-root selection, storage access, effects or retained authority.
9
+ * The file host and the in-process host use this same admission-snapshot fold. */
10
+ export function projectAuthenticatedStanding(history, anchor) {
11
+ const snapshots = new Map();
12
+ const admitted = history.filter(line => {
13
+ const epoch = Number(fieldOf(line, 'epoch')) || 0;
14
+ let authority = snapshots.get(epoch);
15
+ if (!authority) {
16
+ // This invocation has freshly read the complete signed inventory. Derive
17
+ // the same historical cutoff without reopening it for every epoch; the
18
+ // next invocation still rereads every authority input.
19
+ const snapshot = history.filter(record => !isConfig(record) || Number(fieldOf(record, 'epoch')) <= epoch);
20
+ // The signature vocabulary is configuration, not the as-yet
21
+ // unauthenticated evidence whose values are about to be folded.
22
+ const wire = snapshot.filter(record => {
23
+ const scope = fieldOf(record, 'scope');
24
+ return scope.startsWith('wire/') || scope.startsWith('audit/wire/');
25
+ });
26
+ authority = authorityFor(snapshot, anchor, eraOf(foldClaims(wire).standing).admitted);
27
+ snapshots.set(epoch, authority);
28
+ }
29
+ return authority.of(line).kind === 'admitted';
30
+ });
31
+ // The host authenticates against historical authority; Topos projects the
32
+ // authenticated history using the instrument's existing fold.
33
+ const { standing } = foldedEvidence(admitted, {
34
+ classification: () => 'local',
35
+ fold: lines => foldClaims(lines).standing,
36
+ });
37
+ return [...standing];
38
+ }
39
+ /** One check shared by the CLI and the read-only native host. */
40
+ export function assertStandingView(lines) {
41
+ const view = viewsOf(lines).get('standing');
42
+ if (!view || view.shape || !view.regions.some(region => region.name === 'standing'))
43
+ throw new BoundRefusal('standing/view', 'REFUSE·view standing requires a declared standing view');
44
+ }
45
+ export function publicStandingProjection(history, anchor) {
46
+ return publishedStandingLines(projectAuthenticatedStanding(history, anchor));
47
+ }
48
+ export function publishedStandingLines(standing) {
49
+ return standing.filter(line => !isConfig(line) || fieldOf(line, 'value') !== 'withdraw');
50
+ }
package/dist/land/act.js CHANGED
@@ -1,9 +1,32 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { basename, dirname, existsSync, join, mkdirSync, rmSync, writeFileSync } from "../host/io.js";
3
+ import { basename, dirname, existsSync, join, mkdirSync, rmSync, writeFileSync, realpathSync, resolve } from "../host/io.js";
4
+ import { BoundRefusal } from "../host/refusals.js";
4
5
  import { selfName } from "../fold/claims.js";
5
6
  import { placesIn } from "../fold/places.js";
6
7
  import { coordinatesUnder, observeText } from "../observe/files.js";
8
+ const hostContext = new AsyncLocalStorage();
9
+ export const withActHost = (host, run) => hostContext.run(host, run);
10
+ const actName = () => hostContext.getStore()?.name ?? selfName();
11
+ const report = (text) => { const host = hostContext.getStore(); if (host)
12
+ host.report(text);
13
+ else
14
+ process.stderr.write(text); };
15
+ const checkCancellation = () => { if (hostContext.getStore()?.signal?.aborted)
16
+ throw new BoundRefusal('host/cancelled', 'REFUSE·host/cancelled'); };
17
+ /** Resolve aliases before acquiring the same native lock protocol as the CLI. */
18
+ export function canonicalActStore(store) {
19
+ let at = resolve(store);
20
+ const tail = [];
21
+ while (!existsSync(at)) {
22
+ const parent = dirname(at);
23
+ if (parent === at)
24
+ break;
25
+ tail.unshift(basename(at));
26
+ at = parent;
27
+ }
28
+ return join(realpathSync(at), ...tail);
29
+ }
7
30
  const context = new AsyncLocalStorage();
8
31
  const held = new Map();
9
32
  const pauseFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
@@ -20,8 +43,8 @@ function overAMinute(at, began, holder) {
20
43
  if (Date.now() - began < MINUTE)
21
44
  return;
22
45
  const who = holder === undefined ? 'a process' : `${holder.pid} · ${holder.what} since ${holder.since}`;
23
- process.stderr.write(`RED wait · ${named(at)} is held by ${who} · a wait over a minute is a red, not a pause\n`);
24
- throw new Error(`${selfName()}: REFUSE·stale nothing landed · a wait over a minute is a red, not a pause`);
46
+ report(`RED wait · ${named(at)} is held by ${who} · a wait over a minute is a red, not a pause\n`);
47
+ throw new Error(`${actName()}: REFUSE·stale nothing landed · a wait over a minute is a red, not a pause`);
25
48
  }
26
49
  const alive = (pid) => {
27
50
  if (!Number.isSafeInteger(pid) || pid <= 0)
@@ -37,7 +60,7 @@ const alive = (pid) => {
37
60
  const regionAt = (store, region) => {
38
61
  const coordinate = region.replace(/\/+$/, '');
39
62
  if ((coordinate && coordinate.split('/').some((step) => step === '..' || step === '.' || step === '')) || coordinate.startsWith('/'))
40
- throw new Error(`${selfName()}: REFUSE·lock invalid region coordinate`);
63
+ throw new Error(`${actName()}: REFUSE·lock invalid region coordinate`);
41
64
  return join(store, 'locks', coordinate ? encodeURIComponent(coordinate).replace(/^\./, '%2E').replace(/^_root$/, '%5Froot') : ROOT);
42
65
  };
43
66
  const coordinateOf = (file) => {
@@ -77,12 +100,13 @@ async function hold(at, what) {
77
100
  return;
78
101
  const frame = context.getStore();
79
102
  if (frame === undefined)
80
- throw new Error(`${selfName()}: REFUSE·lock no act owns this request`);
103
+ throw new Error(`${actName()}: REFUSE·lock no act owns this request`);
81
104
  const began = Date.now();
82
105
  const token = randomUUID();
83
106
  let told = false;
84
107
  mkdirSync(dirname(at), { recursive: true });
85
108
  for (;;) {
109
+ checkCancellation();
86
110
  try {
87
111
  writeFileSync(at, `${process.pid} ${what.replace(/\s+/g, '_')} ${new Date().toISOString()} ${token}\n`, { flag: 'wx' });
88
112
  break;
@@ -97,17 +121,17 @@ async function hold(at, what) {
97
121
  const holder = holderOf(at);
98
122
  // Removing a dead owner's lock here would race with another process's recovery. Recovery is a separate host act.
99
123
  if (holder !== undefined && !alive(holder.pid)) {
100
- throw new Error(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
124
+ throw new Error(`${actName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
101
125
  }
102
126
  overAMinute(at, began, holder);
103
127
  if (!told && holder !== undefined) {
104
- process.stderr.write(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
128
+ report(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
105
129
  told = true;
106
130
  }
107
131
  await pauseFor(250);
108
132
  }
109
133
  if (told)
110
- process.stderr.write(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
134
+ report(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
111
135
  held.set(at, token);
112
136
  frame.locks.set(at, token);
113
137
  if (!process.listeners('exit').includes(releaseAll))
@@ -125,16 +149,17 @@ function releaseAll() {
125
149
  for (const [at, token] of [...held])
126
150
  release(at, token);
127
151
  }
128
- process.once('SIGTERM', () => {
129
- process.stderr.write(`${selfName()}: REFUSE·crash signal\n`);
130
- releaseAll();
131
- process.exit(1);
132
- });
152
+ /** Only the CLI installs process termination behavior. Importing a host does not. */
153
+ export function installActSignalHandler() {
154
+ process.once('SIGTERM', () => { report(`${actName()}: REFUSE·crash signal\n`); releaseAll(); process.exit(1); });
155
+ }
133
156
  async function framed(takes, act) {
134
157
  const frame = { parent: context.getStore(), locks: new Map() };
135
158
  return context.run(frame, async () => {
136
159
  try {
160
+ checkCancellation();
137
161
  await takes();
162
+ checkCancellation();
138
163
  return await act();
139
164
  }
140
165
  finally {
@@ -149,14 +174,14 @@ function guardedRegions(store, regions, what, act, rootAuthority) {
149
174
  const tree = treeAt(store);
150
175
  const asked = [...new Set((regions.length ? regions : ['']).map((one) => one.replace(/\/+$/, '')))];
151
176
  if (rootAuthority && (asked.includes('') || (ownsRegion(store) && !owns(tree) && !ownsAuthority(store))))
152
- throw new Error(`${selfName()}: REFUSE·lock authority dependencies and child regions must be declared together`);
177
+ throw new Error(`${actName()}: REFUSE·lock authority dependencies and child regions must be declared together`);
153
178
  if (owns(tree) || asked.every((region) => owns(regionAt(store, region)))) {
154
179
  for (const region of asked)
155
180
  await hold(regionAt(store, region), what);
156
181
  return;
157
182
  }
158
183
  if (ownsRegion(store))
159
- throw new Error(`${selfName()}: REFUSE·lock declare every region before entering an act`);
184
+ throw new Error(`${actName()}: REFUSE·lock declare every region before entering an act`);
160
185
  const already = owns(tree);
161
186
  // The tree lock is the acquisition gate. A whole-tree act cannot enter between checking it and taking regions.
162
187
  await hold(tree, what);
@@ -172,17 +197,18 @@ function guardedRegions(store, regions, what, act, rootAuthority) {
172
197
  const began = Date.now();
173
198
  let told = false;
174
199
  for (let holder = holderOf(at); holder !== undefined; holder = holderOf(at)) {
200
+ checkCancellation();
175
201
  if (!alive(holder.pid))
176
- throw new Error(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
202
+ throw new Error(`${actName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
177
203
  overAMinute(at, began, holder);
178
204
  if (!told) {
179
- process.stderr.write(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
205
+ report(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
180
206
  told = true;
181
207
  }
182
208
  await pauseFor(250);
183
209
  }
184
210
  if (told)
185
- process.stderr.write(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
211
+ report(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
186
212
  }
187
213
  // Registered under the same acquisition gate as every legacy writer.
188
214
  // Root writes overlap all locks; siblings keep distinct region locks.
@@ -207,12 +233,14 @@ function guardedRegions(store, regions, what, act, rootAuthority) {
207
233
  }, act);
208
234
  }
209
235
  export function underTheRegions(store, regions, what, act) {
236
+ store = canonicalActStore(store);
210
237
  return guardedRegions(store, regions, what, act, false);
211
238
  }
212
239
  /** Protect the root authority/policy read through child publication. This is
213
240
  * exclusion only: callers must authenticate contexts and compare prepared heads
214
241
  * while held. It creates no grant, clock advance, lease or automatic recovery. */
215
242
  export function underAuthorityRegions(store, regions, what, act) {
243
+ store = canonicalActStore(store);
216
244
  return guardedRegions(store, regions, what, act, true);
217
245
  }
218
246
  /** Protect an authority snapshot without taking a child region. The acquisition
@@ -220,12 +248,13 @@ export function underAuthorityRegions(store, regions, what, act) {
220
248
  * shared read; existing child acts continue independently. This is exclusion,
221
249
  * never authorization or a clock advance. */
222
250
  export function underRootAuthority(store, what, act) {
251
+ store = canonicalActStore(store);
223
252
  return framed(async () => {
224
253
  const tree = treeAt(store), root = regionAt(store, '');
225
254
  if (owns(tree) || owns(root) || ownsAuthority(store))
226
255
  return;
227
256
  if (ownsRegion(store))
228
- throw new Error(`${selfName()}: REFUSE·lock authority dependencies must be declared before entering a region act`);
257
+ throw new Error(`${actName()}: REFUSE·lock authority dependencies must be declared before entering a region act`);
229
258
  await hold(tree, what);
230
259
  try {
231
260
  await hold(root, what);
@@ -246,49 +275,54 @@ export function underRootAuthority(store, what, act) {
246
275
  /** The fixed host may hand its child evidence of an actual held exclusion.
247
276
  * It is an operational envelope, not a grant, signer or receipt identity. */
248
277
  export function rootAuthorityProtection(store) {
278
+ store = canonicalActStore(store);
249
279
  const locks = [treeAt(store), regionAt(store, ''), ...held.keys()].filter(at => at === treeAt(store) || at === regionAt(store, '') || dirname(at) === join(store, 'locks') && basename(at).startsWith('@authority-'));
250
280
  const lock = locks.find(owns);
251
281
  if (lock === undefined)
252
- throw Error(`${selfName()}: REFUSE·lock root authority is not protected`);
282
+ throw Error(`${actName()}: REFUSE·lock root authority is not protected`);
253
283
  const holder = holderOf(lock);
254
284
  if (holder?.pid !== process.pid || holder.token !== held.get(lock))
255
- throw Error(`${selfName()}: REFUSE·lock authority protection changed`);
285
+ throw Error(`${actName()}: REFUSE·lock authority protection changed`);
256
286
  return Object.freeze({ store, lock, pid: holder.pid, token: holder.token });
257
287
  }
258
288
  /** expectedStore and expectedParent come from the fixed host's inheritance
259
289
  * channel, never from world data. Matching a live lock proves exclusion only;
260
290
  * signature, coverage and current context still require verification. */
261
291
  export function assertRootAuthorityProtection(proof, expectedStore, expectedParent) {
292
+ expectedStore = canonicalActStore(expectedStore);
262
293
  if (!proof || proof.store !== expectedStore || !Number.isSafeInteger(expectedParent) || expectedParent <= 0 || proof.pid !== expectedParent
263
294
  || typeof proof.lock !== 'string' || typeof proof.token !== 'string' || !proof.token
264
295
  || !(proof.lock === treeAt(expectedStore) || proof.lock === regionAt(expectedStore, '')
265
296
  || dirname(proof.lock) === join(expectedStore, 'locks') && basename(proof.lock).startsWith('@authority-'))) {
266
- throw Error(`${selfName()}: REFUSE·lock invalid inherited authority protection`);
297
+ throw Error(`${actName()}: REFUSE·lock invalid inherited authority protection`);
267
298
  }
268
299
  const holder = holderOf(proof.lock);
269
300
  if (!alive(expectedParent) || holder?.pid !== proof.pid || holder.token !== proof.token)
270
- throw Error(`${selfName()}: REFUSE·lock inherited authority protection ended`);
301
+ throw Error(`${actName()}: REFUSE·lock inherited authority protection ended`);
271
302
  }
272
303
  /** A child can publish independently, but the joined root projection and its
273
304
  * derived ledger appends have one writer. Authority protection already excludes
274
305
  * root mutations, so this lock needs no acquisition-gate upgrade. */
275
306
  export function underRootProjection(store, what, act) {
307
+ store = canonicalActStore(store);
276
308
  return framed(async () => {
277
309
  if (owns(treeAt(store)) || owns(regionAt(store, '')))
278
310
  return;
279
311
  if (!ownsAuthority(store))
280
- throw new Error(`${selfName()}: REFUSE·lock root projection requires authority protection`);
312
+ throw new Error(`${actName()}: REFUSE·lock root projection requires authority protection`);
281
313
  await hold(projectionAt(store), what);
282
314
  }, act);
283
315
  }
284
316
  export const holdRoot = async (store, what) => {
317
+ store = canonicalActStore(store);
285
318
  if (!owns(treeAt(store)))
286
319
  await hold(regionAt(store, ''), what);
287
320
  };
288
321
  export function underTheLock(store, what, act) {
322
+ store = canonicalActStore(store);
289
323
  return framed(async () => {
290
324
  if (!owns(treeAt(store)) && ownsRegion(store))
291
- throw new Error(`${selfName()}: REFUSE·lock a region act cannot upgrade to the tree; declare the tree before entering`);
325
+ throw new Error(`${actName()}: REFUSE·lock a region act cannot upgrade to the tree; declare the tree before entering`);
292
326
  await hold(treeAt(store), what);
293
327
  const locks = join(store, 'locks');
294
328
  const active = lockFiles(locks);
@@ -299,4 +333,4 @@ export function underTheLock(store, what, act) {
299
333
  await hold(regionAt(store, ''), what);
300
334
  }, act);
301
335
  }
302
- export const holds = (store, region) => [treeAt(store), regionAt(store, region)].some(owns);
336
+ export const holds = (store, region) => { store = canonicalActStore(store); return [treeAt(store), regionAt(store, region)].some(owns); };
@@ -1,3 +1,4 @@
1
+ import { hostOutput } from "../host/call-context.js";
1
2
  import { boundedProcess } from "../host/process-lifetime.js";
2
3
  import { spawnSync } from 'node:child_process';
3
4
  import { existsSync } from 'node:fs';
@@ -31,7 +32,7 @@ export function runReader(module, root, inputs, limits) {
31
32
  if (ran.status !== 0)
32
33
  throw new Error(`${module}: reader exited ${ran.status ?? `with signal ${ran.signal}`}\n${ran.stderr ?? ''}${ran.stdout ?? ''}`);
33
34
  if (ran.stderr)
34
- process.stderr.write(ran.stderr);
35
+ hostOutput('error', ran.stderr);
35
36
  return responsesOf(ran.stdout ?? '', inputs.length).map((answer) => {
36
37
  if (answer.kind !== 'fact')
37
38
  throw new Error(`REFUSE·reader ${module} · ${answer.why ?? 'no reading answered'}`);
@@ -1,3 +1,4 @@
1
+ import { hostInstrumentRuntime } from "../host/call-context.js";
1
2
  import { spawnSync } from 'node:child_process';
2
3
  import { sha } from "../host/hash.js";
3
4
  import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
@@ -10,17 +11,21 @@ import { landBlob } from "../land/ledger.js";
10
11
  import { EXTENSION, LOCK, publicLock } from '@lapxo/topos/wire';
11
12
  import { foldClaims, isWire } from "../fold/claims.js";
12
13
  const self = () => process.argv[1] ?? '';
13
- const fromBlob = () => extname(existsSync(self()) ? realpathSync(self()) : self()) === '';
14
+ const fromBlob = () => hostInstrumentRuntime() !== undefined ? false : extname(existsSync(self()) ? realpathSync(self()) : self()) === '';
14
15
  /**
15
16
  * Where the instrument lies and what it is run from: from source or compiled modules, bound is two steps above this runner module; as a blob, the command is a blob of the tree's store, named by the digest bound's own lock names for
16
17
  * it, and bound lies beside that store. Other processes use the same runtime directory and extension; only source readers receive source conditions. A name that resolves to no blob beside it is refused by name
17
18
  * before anything is started, never handed to the runtime as the store's own directory.
18
19
  */
19
20
  // Blob bundles have no module URL; source and compiled modules carry their own entry location.
20
- const moduleAt = fromBlob() ? undefined : fileURLToPath(import.meta.url);
21
- const runtimeAt = moduleAt === undefined ? '' : resolve(dirname(moduleAt), '..');
22
- const runtimeExtension = moduleAt === undefined ? '' : extname(moduleAt);
23
- export const ownRoot = () => (fromBlob() ? join(resolve(dirname(self()), '..', '..', '..'), 'bound') : resolve(runtimeAt, '..'));
21
+ // Native library imports do not inspect the embedding application's argv.
22
+ // The CLI discovers its default runtime only when a CLI operation requests it.
23
+ const moduleAt = () => fromBlob() ? undefined : fileURLToPath(import.meta.url);
24
+ const runtimeAt = () => { const explicit = hostInstrumentRuntime(); if (explicit)
25
+ return explicit.directory; const at = moduleAt(); return at === undefined ? '' : resolve(dirname(at), '..'); };
26
+ const runtimeExtension = () => { const explicit = hostInstrumentRuntime(); if (explicit)
27
+ return explicit.extension; const at = moduleAt(); return at === undefined ? '' : extname(at); };
28
+ export const ownRoot = () => hostInstrumentRuntime()?.root ?? (fromBlob() ? join(resolve(dirname(self()), '..', '..', '..'), 'bound') : resolve(runtimeAt(), '..'));
24
29
  /** A published instrument carries its own content store even before any ledger
25
30
  * exists. Public-lock recognition grants no authority; legacy signed delivery
26
31
  * trees retain their historical colocated-ledger or sibling-store layout. */
@@ -34,6 +39,9 @@ export const ownStore = () => instrumentStoreAt(ownRoot());
34
39
  // An act is judged by the instrument it started with, even when that act changes its colocated lock.
35
40
  let instrumentLock;
36
41
  export const ownLock = () => {
42
+ const explicit = hostInstrumentRuntime();
43
+ if (explicit)
44
+ return explicit.lock;
37
45
  if (instrumentLock !== undefined)
38
46
  return instrumentLock;
39
47
  const lines = (observeText(join(ownRoot(), LOCK)) ?? '').split('\n').filter(isWire);
@@ -43,10 +51,10 @@ export const ownLock = () => {
43
51
  : foldClaims(published).standing;
44
52
  return instrumentLock;
45
53
  };
46
- export const sourceOf = () => (fromBlob() ? join(ownRoot(), 'src', 'cli', 'verb.ts') : self());
54
+ export const sourceOf = () => hostInstrumentRuntime()?.entry ?? (fromBlob() ? join(ownRoot(), 'src', 'cli', 'verb.ts') : self());
47
55
  export function processOf(name, folder) {
48
56
  if (!fromBlob())
49
- return join(runtimeAt, folder ?? 'cli', `${name}${runtimeExtension}`);
57
+ return join(runtimeAt(), folder ?? 'cli', `${name}${runtimeExtension()}`);
50
58
  const named = ownLock().find((line) => fieldOf(line, 'scope') === `runner/${name}`);
51
59
  const blob = fieldOf(named ?? '', 'value').replace(/^[^:]*:/, '');
52
60
  if (!blob || !existsSync(join(dirname(self()), blob))) {
@@ -87,7 +95,7 @@ export function bundleOf(module) {
87
95
  }
88
96
  /** A reader run by a blob is a blob itself: its bundle, with everything it imports but the compiler. */
89
97
  export function readerOf(module) {
90
- const source = { path: module, flags: runtimeExtension === '.ts' ? ['--disable-warning=ExperimentalWarning', '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON'] : [] };
98
+ const source = { path: module, flags: runtimeExtension() === '.ts' ? ['--disable-warning=ExperimentalWarning', '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON'] : [] };
91
99
  const hex = fromBlob() ? bundleOf(module) : undefined;
92
100
  return hex === undefined ? source : { path: join(ownStore(), 'cas', 'blobs', hex), flags: [] };
93
101
  }
@@ -0,0 +1,23 @@
1
+ # Verified executable closure delivery
2
+
3
+ Needs are arbitrary declared names, not a kernel profile enumeration.
4
+
5
+ All requested entries share the existing moduleClosure traversal; no second walker, parser or algebra.
6
+
7
+ Host requires already authenticated snapshot identities; a bundle digest establishes content identity, not authority or correctness.
8
+
9
+ Delivery preserves each reached JS/JSON member and explicitly pinned notices byte for byte. Separate digests identify the closure manifest and the exact UTF-8 JSON container.
10
+
11
+ Canonical delivery sorts needs, reached modules, library bindings and notices; duplicate needs do not duplicate code; unsupported executable forms refuse by name.
12
+
13
+ Mandatory member, source byte and response byte budgets apply before delivery. Pre-aborted or observed cancellation refuses. Synchronous work is not a preemptible execution sandbox.
14
+
15
+ Portable ESM delivery refuses host builtins, runtime loader access, CommonJS loads, calculated/escaped loader expressions and unsupported URL spellings before entry execution.
16
+
17
+ Consumer verifies the expected container digest and repeats the same closure preflight over exactly the delivered members. Derived import maps are never trusted as authority.
18
+
19
+ The host must verify actual mounted bytes again before execution; the test host demonstrates this but is not a public browser host.
20
+
21
+ Existing native, program and worker custody remain unchanged. Acorn routes stay explicitly retained under A-F gate.
22
+
23
+ No external fetch or general demand resolver is introduced. Missing bytes remain outside completed delivery and must be resolved by the existing host contract.
@@ -0,0 +1,25 @@
1
+ {
2
+ "scope": "contract/published/closure-delivery",
3
+ "members": [
4
+ {
5
+ "role": "contract",
6
+ "path": "docs/contracts/closure-delivery.md",
7
+ "digest": "sha256:bdadeefcf8042749e642a453adc7d07944ceab3fbd62de46209ea3ed25d05c2e"
8
+ },
9
+ {
10
+ "role": "yes",
11
+ "path": "samples/module-delivery/inputs/delivery.json",
12
+ "digest": "sha256:a6df1d11537a0792c9772b1874c7e127e0fda22203f756d6f0eb104d072a9df1"
13
+ },
14
+ {
15
+ "role": "vector",
16
+ "path": "vectors/native/module-delivery.vector.json",
17
+ "digest": "sha256:3de9f160f89764e1153a2719ab04447142fe187bf476b3060d860e81e9c37c02"
18
+ },
19
+ {
20
+ "role": "no",
21
+ "path": "samples/no/module-delivery/delivery.json",
22
+ "digest": "sha256:f95b1c3129896baaecb4ae7860afc11dcaaef0a2e47221b73044bc5a4681181f"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,21 @@
1
+ # Native in-process host
2
+
3
+ The public `@lapxo/bound/host` entry offers authenticated Root and contextual standing reads, Root-lot and contextual-act admission, historical recovery, and explicit native instrument and receiver adapters. Node is required for this native host. The pure `/engine` and reference backends retain their separate contracts.
4
+
5
+ ## Classic Root lot
6
+
7
+ `landRootLotInProcess(context, ports)` accepts LF-framed, canonical, unsigned target configuration records. The receiver supplies the admitted place, exact history snapshot, implementation runtime, reading identity, byte/record/time bounds, content port and canonical-line signer. The lot cannot choose those capabilities. It calls the same admission judges and atomic result implementation as the CLI; a signed line is not automatically an admitted line.
8
+
9
+ The snapshot is checked before acquisition and before admission. Coverage is checked before invoking the signer. A change while signing refuses instead of rebasing. A fresh repeated held lot does not acquire another signature. `rootLotSnapshot` names complete admission history; it is not a signature or permission.
10
+
11
+ ## Effects and recovery
12
+
13
+ Cancellation before visibility leaves no partial act. A publication error whose visibility is unknown returns `uncertain` with the native act identity. A failure after commit retains that identity and reports incomplete conformance, not rollback. `recoverRootActInProcess` authenticates the historical result without signing or reviving current grants. Root and contextual admission remain distinct operations; the latter consumes already signed contextual originals and acquires receiver receipts.
14
+
15
+ The host selects context explicitly on every call. Calls do not interpret consumer argv or print to its console. Diagnostics are bounded returned data. Native storage shares the CLI exclusion protocol. Returned reading commitments are unsigned data and do not pay release demands.
16
+
17
+ ## Public quartet
18
+
19
+ The two positive places are a seed accession library and a coastal tide survey. Each installs the public host boundary, bootstraps disposable fixture authority through the real CLI, admits a domain-specific Root lot, compares native result recovery and repeats the held lot without signing again. Negatives alter bytes, remove signer coverage, cancel before commit and offer a noncanonical lot; no negative may create a visible act. Test keys are created only for the disposable fixture, never shipped.
20
+
21
+ The executable vector captures the instrument output. The quartet proves this declared public boundary; it does not certify all remaining host views, another machine, a release policy or an owner admission. Head, browser hosts, algebra, wire and ceilings are outside this change.