@pulse-compute/wasm-host-runtime 1.0.0-beta.5 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +7 -7
- package/src/runtime/canonical-api-runtime.js +32 -10
- package/src/runtime/canonical-native-host.js +184 -39
- package/src/runtime/effect-invocations.js +52 -0
- package/src/runtime/native-crypto-verifier.js +17 -1
- package/src/runtime/native-value-budget.js +64 -0
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
> **Supported entry points:** `None for application authors.`<br>
|
|
8
8
|
> **Stability:** Internal compiler/runtime interface synchronized with this release set.<br>
|
|
9
9
|
> **npm:** [`@pulse-compute/wasm-host-runtime`](https://www.npmjs.com/package/@pulse-compute/wasm-host-runtime)<br>
|
|
10
|
-
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/)
|
|
11
11
|
>
|
|
12
|
-
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.6` package policy.
|
|
13
13
|
<!-- pulse-package-status:end -->
|
|
14
14
|
|
|
15
15
|
Package-owned PulseWasm host-runtime build-time builders and local runtime helpers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/wasm-host-runtime",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"./runtime/canonical-native-host": "./src/runtime/canonical-native-host.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@pulse-compute/crypto": "1.0.0-beta.
|
|
32
|
-
"@pulse-compute/runtime": "1.0.0-beta.
|
|
33
|
-
"@pulse-compute/wasm-build-support": "1.0.0-beta.
|
|
34
|
-
"@pulse-compute/wasm-contracts": "1.0.0-beta.
|
|
35
|
-
"@pulse-compute/wasm-schema-json": "1.0.0-beta.
|
|
31
|
+
"@pulse-compute/crypto": "1.0.0-beta.6",
|
|
32
|
+
"@pulse-compute/runtime": "1.0.0-beta.6",
|
|
33
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.6",
|
|
34
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.6",
|
|
35
|
+
"@pulse-compute/wasm-schema-json": "1.0.0-beta.6"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
49
49
|
"directory": "wasm/packages/host-runtime"
|
|
50
50
|
},
|
|
51
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
51
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/",
|
|
52
52
|
"bugs": {
|
|
53
53
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
54
54
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { createEffectInvocations, normalizeMaxEffects } = require('./effect-invocations.js');
|
|
2
3
|
const portableKv = require('@pulse-compute/runtime/host');
|
|
3
4
|
|
|
4
5
|
|
|
@@ -262,7 +263,10 @@ function redactRuntimeError(error, sensitiveValues = new Set(), seen = new WeakS
|
|
|
262
263
|
const rawDetail = ownDataValue(error, 'detail');
|
|
263
264
|
const message = redactString(typeof rawMessage === 'string' ? rawMessage : 'Pulse runtime failure.', sensitiveValues);
|
|
264
265
|
const name = redactString(typeof rawName === 'string' ? rawName : 'Error', sensitiveValues);
|
|
265
|
-
|
|
266
|
+
// Keep the finite admitted discriminator intact; messages, causes and arbitrary
|
|
267
|
+
// provider codes still pass through ordinary sensitive-value redaction.
|
|
268
|
+
const code = portableKv.isApplicationError(error) ? rawCode
|
|
269
|
+
: typeof rawCode === 'string' ? redactString(rawCode, sensitiveValues) : undefined;
|
|
266
270
|
const detail = rawDetail === undefined ? undefined : redactRuntimeValue(rawDetail, sensitiveValues, seen, 'detail', depth + 1);
|
|
267
271
|
|
|
268
272
|
const safe = code === undefined
|
|
@@ -786,11 +790,12 @@ function normalizedInit(init = {}, schemaCodecs, options = {}) {
|
|
|
786
790
|
let body;
|
|
787
791
|
let bodyMode = 'none';
|
|
788
792
|
if (hasBody) {
|
|
789
|
-
|
|
793
|
+
const incoming = options.incomingBody && value.body === options.incomingBody.marker();
|
|
794
|
+
if (typeof value.body !== 'string' && !incoming) {
|
|
790
795
|
throw new CanonicalRuntimeError('FetchRequestError', 'PULSE_FETCH_BODY_INVALID', 'Canonical fetch body must be a string.', { valueType: typeof value.body });
|
|
791
796
|
}
|
|
792
797
|
body = value.body;
|
|
793
|
-
bodyMode = 'text';
|
|
798
|
+
bodyMode = incoming ? 'incoming-request-v1' : 'text';
|
|
794
799
|
} else if (hasJson) {
|
|
795
800
|
if (!headerValue(headers, 'content-type')) headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
796
801
|
if (hasSchema) {
|
|
@@ -936,6 +941,8 @@ function normalizeProviderEffect(effect, schemaCodecs, options = {}) {
|
|
|
936
941
|
if (effect.kind === 'fetch') return normalizeFetchEffect(effect, schemaCodecs, options);
|
|
937
942
|
const id = String(effect.id || '');
|
|
938
943
|
if (!id) throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical provider effect requires an id.', { effect });
|
|
944
|
+
if (effect.kind === 'output.readTextChunk') return Object.freeze({ id, kind: effect.kind, providerKind: 'output', operation: 'readTextChunk', capability: 'request.body.transform' });
|
|
945
|
+
if (effect.kind === 'output.start' || effect.kind === 'output.write') return Object.freeze({ id, kind: effect.kind, providerKind: 'output', operation: effect.kind.slice(7), capability: 'response.output', argument0: effect.argument0 });
|
|
939
946
|
if (effect.kind === 'time.now') return Object.freeze({ id, kind: 'time.now', providerKind: 'time', operation: 'now', capability: 'time.wall-clock', source: effect.source, ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }) });
|
|
940
947
|
if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
|
|
941
948
|
return Object.freeze({ id, kind: effect.kind, name: String(effect.name), ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
|
|
@@ -1364,6 +1371,9 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1364
1371
|
const metadata = assertCanonicalProgramCompatibility(programModule);
|
|
1365
1372
|
executionSequence += 1;
|
|
1366
1373
|
const executionId = String(executionOptions.executionId || `canonical-${adapter.id}-${executionSequence}`);
|
|
1374
|
+
const maxEffects = normalizeMaxEffects(executionOptions.maxEffects ?? options.maxEffects);
|
|
1375
|
+
const invocations = createEffectInvocations(() => executionOptions.requestBudget.check());
|
|
1376
|
+
const continuationIds = [];
|
|
1367
1377
|
const sensitiveValues = initialSensitiveValues({ ...options, ...executionOptions });
|
|
1368
1378
|
let executionStatus = 'failed';
|
|
1369
1379
|
let executionFailure;
|
|
@@ -1416,23 +1426,24 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1416
1426
|
let dispatchedEffects = 0;
|
|
1417
1427
|
|
|
1418
1428
|
function executionContinuations() {
|
|
1419
|
-
|
|
1420
|
-
return registry.list().filter((entry) => entry.id.startsWith(prefix));
|
|
1429
|
+
return continuationIds.map(id => registry.get(id));
|
|
1421
1430
|
}
|
|
1422
1431
|
|
|
1423
1432
|
async function dispatchOne(effect) {
|
|
1424
1433
|
executionOptions.requestBudget.check();
|
|
1434
|
+
const ticket = invocations.open(effect.id, effect.id);
|
|
1425
1435
|
const normalized = normalizeProviderEffect(effect, schemaCodecs, runtimeOptions);
|
|
1426
1436
|
const conditional = portableKv.isConditionalKv(normalized.kind);
|
|
1427
1437
|
if (conditional) portableKv.registerKvRedactions(normalized, (value) => sensitiveValues.add(value));
|
|
1428
|
-
recordTrace(effectTraceStart(adapter.id, executionId, normalized, sensitiveValues));
|
|
1438
|
+
recordTrace({ ...effectTraceStart(adapter.id, executionId, normalized, sensitiveValues), invocationId: ticket.invocationId });
|
|
1429
1439
|
dispatchedEffects += 1;
|
|
1430
1440
|
try {
|
|
1431
|
-
const effectExecution = { ...options, ...executionOptions, metadata, executionId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
|
|
1441
|
+
const effectExecution = { ...options, ...executionOptions, metadata, executionId, invocationId: ticket.invocationId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
|
|
1432
1442
|
let snapshot = conditional
|
|
1433
1443
|
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)), effectExecution, runtimeOptions)
|
|
1434
1444
|
: await executionOptions.requestBudget.race(adapter.dispatchEffect(normalized, effectExecution));
|
|
1435
1445
|
executionOptions.requestBudget.check();
|
|
1446
|
+
invocations.assertPending(ticket);
|
|
1436
1447
|
if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') {
|
|
1437
1448
|
if (snapshot !== undefined && typeof snapshot !== 'string') {
|
|
1438
1449
|
throw new CanonicalRuntimeError(
|
|
@@ -1480,9 +1491,13 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1480
1491
|
})
|
|
1481
1492
|
: snapshot;
|
|
1482
1493
|
resolutionOrder.push(effect.id);
|
|
1483
|
-
recordTrace(effectTraceResolved(adapter.id, executionId, normalized, value));
|
|
1484
|
-
return value;
|
|
1494
|
+
recordTrace({ ...effectTraceResolved(adapter.id, executionId, normalized, value), invocationId: ticket.invocationId });
|
|
1495
|
+
return invocations.settle(ticket, () => value);
|
|
1485
1496
|
} catch (error) {
|
|
1497
|
+
// A request deadline outranks a capability's concurrent abort result.
|
|
1498
|
+
try { executionOptions.requestBudget.check(); } catch (failure) {
|
|
1499
|
+
if (typeof failure?.code === 'string' && failure.code.startsWith('PULSE_REQUEST_')) error = failure;
|
|
1500
|
+
}
|
|
1486
1501
|
recordTrace(Object.freeze({ type: 'effect-failed', provider: adapter.id, executionId, effectId: effect.id, kind: normalized.kind, error: error.name || 'Error', code: error.code }));
|
|
1487
1502
|
throw redactRuntimeError(error, sensitiveValues);
|
|
1488
1503
|
}
|
|
@@ -1533,12 +1548,18 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1533
1548
|
const marker = step.value;
|
|
1534
1549
|
if (!marker || marker.protocol !== canonicalRuntimeContract.CANONICAL_RUNTIME_PROTOCOL_VERSION || !canonicalRuntimeContract.CANONICAL_EFFECT_MARKER_KINDS.includes(marker.kind)) throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical handler yielded an unsupported runtime marker.', { marker });
|
|
1535
1550
|
const effects = marker.kind === 'group' ? marker.effects : [marker.effect];
|
|
1536
|
-
const continuationId = `${executionId}:${marker.continuationId}`;
|
|
1551
|
+
const continuationId = `${executionId}:${invocations.execution}:${marker.continuationId}:${continuationIds.length + 1}`;
|
|
1537
1552
|
registry.create({ id: continuationId, branchPoint: marker.continuationId, effectIds: effects.map((effect) => effect.id), ttlMs: executionOptions.continuationTtlMs || options.continuationTtlMs });
|
|
1553
|
+
continuationIds.push(continuationId);
|
|
1538
1554
|
registry.wait(continuationId);
|
|
1539
1555
|
recordTrace(Object.freeze({ type: 'continuation-waiting', provider: adapter.id, executionId, continuationId, effectIds: effects.map((effect) => effect.id) }));
|
|
1540
1556
|
let value;
|
|
1541
1557
|
try {
|
|
1558
|
+
if (dispatchedEffects + effects.length > maxEffects) {
|
|
1559
|
+
throw new CanonicalRuntimeError('EffectLimitError', 'PULSE_RUNTIME_EFFECT_LIMIT_EXCEEDED',
|
|
1560
|
+
`Pulse request exceeded the maximum of ${maxEffects} request-owned effects.`,
|
|
1561
|
+
{ effectCount: dispatchedEffects, pendingEffects: effects.length, maxEffects });
|
|
1562
|
+
}
|
|
1542
1563
|
value = marker.kind === 'group' ? await dispatchGroup(effects) : await dispatchOne(effects[0]);
|
|
1543
1564
|
executionOptions.requestBudget.check();
|
|
1544
1565
|
registry.resume(continuationId);
|
|
@@ -1584,6 +1605,7 @@ function createCanonicalHostRuntime(options = {}) {
|
|
|
1584
1605
|
executionFailure = redactRuntimeError(error, sensitiveValues);
|
|
1585
1606
|
throw executionFailure;
|
|
1586
1607
|
} finally {
|
|
1608
|
+
invocations.close();
|
|
1587
1609
|
try { adapter.disposeExecution(Object.freeze({ executionId, metadata, status: executionStatus, error: executionFailure })); }
|
|
1588
1610
|
catch (_) { /* provider cleanup must not mask execution results */ }
|
|
1589
1611
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const portableKv = require('@pulse-compute/runtime/host');
|
|
3
|
+
const { createEffectInvocations, normalizeMaxEffects } = require('./effect-invocations.js');
|
|
4
|
+
const { NativeValueBudget } = require('./native-value-budget.js');
|
|
3
5
|
|
|
4
6
|
const crypto = require('node:crypto');
|
|
5
7
|
const {
|
|
@@ -45,7 +47,6 @@ const canonicalRuntime = loadCanonicalRuntime();
|
|
|
45
47
|
const schemaCodecTools = loadSchemaCodecTools();
|
|
46
48
|
const eventContract = loadEventContract();
|
|
47
49
|
let executionSequence = 0;
|
|
48
|
-
const DEFAULT_MAX_EFFECTS = 1024;
|
|
49
50
|
|
|
50
51
|
class CanonicalNativeHostError extends Error {
|
|
51
52
|
constructor(message, code = 'PULSE_CANONICAL_NATIVE_HOST_FAILED', detail = {}) {
|
|
@@ -60,14 +61,6 @@ function sha256(value) {
|
|
|
60
61
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function normalizeMaxEffects(value) {
|
|
64
|
-
const maxEffects = value === undefined ? DEFAULT_MAX_EFFECTS : Number(value);
|
|
65
|
-
if (!Number.isSafeInteger(maxEffects) || maxEffects <= 0) {
|
|
66
|
-
throw new TypeError('Pulse maxEffects must be a positive safe integer.');
|
|
67
|
-
}
|
|
68
|
-
return maxEffects;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
64
|
function staticDecoderArgument(expression, effect) {
|
|
72
65
|
if (expression && expression.kind === 'literal' && ['string', 'number', 'boolean'].includes(typeof expression.value)) return expression.value;
|
|
73
66
|
if (expression && expression.kind === 'literal' && expression.value === null) return null;
|
|
@@ -117,13 +110,40 @@ function createSchemaCodecs(registryInput = {}) {
|
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
class ValueHeap {
|
|
120
|
-
constructor() {
|
|
113
|
+
constructor(budget) {
|
|
114
|
+
this.budget = budget;
|
|
121
115
|
this.values = new Map();
|
|
122
116
|
this.next = 1;
|
|
117
|
+
// Immutable scalars have value semantics. Reuse common handles rather than
|
|
118
|
+
// retaining a new boxed value for every literal/property read in a loop.
|
|
119
|
+
// Bound the index itself and rotate its oldest mapping as the working set
|
|
120
|
+
// changes. Eviction never releases a handle or refunds its cumulative charge.
|
|
121
|
+
this.scalars = budget ? new Map() : undefined;
|
|
122
|
+
this.identities = budget ? new WeakMap() : undefined;
|
|
123
|
+
this.identityCount = 0;
|
|
124
|
+
this.negativeZero = Symbol('negative-zero');
|
|
123
125
|
}
|
|
124
126
|
put(value) {
|
|
127
|
+
this.budget?.charge(0, 0); // Cached reads cannot recover a latched failure.
|
|
128
|
+
const type = typeof value;
|
|
129
|
+
const scalar = value === null || type === 'undefined' || type === 'boolean' || type === 'number' || type === 'string';
|
|
130
|
+
const object = value !== null && type === 'object';
|
|
131
|
+
if (object && this.identities?.has(value)) return this.identities.get(value);
|
|
132
|
+
const key = Object.is(value, -0) ? this.negativeZero : value;
|
|
133
|
+
if (scalar && this.scalars?.has(key)) return this.scalars.get(key);
|
|
134
|
+
const cache = scalar && this.scalars;
|
|
135
|
+
const cacheIdentity = object && this.identities && this.identityCount < 8192;
|
|
136
|
+
// Charge index growth once. Replacing a scalar mapping reuses its bounded
|
|
137
|
+
// capacity; every new value handle below remains cumulatively charged.
|
|
138
|
+
if ((cache && this.scalars.size < 8192) || cacheIdentity) this.budget.charge(1, this.budget.policy.edgeBytes * 2);
|
|
139
|
+
this.budget?.retain(value);
|
|
125
140
|
const handle = this.next++;
|
|
126
141
|
this.values.set(handle, value);
|
|
142
|
+
if (cache) {
|
|
143
|
+
if (this.scalars.size === 8192) this.scalars.delete(this.scalars.keys().next().value);
|
|
144
|
+
this.scalars.set(key, handle);
|
|
145
|
+
}
|
|
146
|
+
if (cacheIdentity) { this.identities.set(value, handle); this.identityCount++; }
|
|
127
147
|
return handle;
|
|
128
148
|
}
|
|
129
149
|
has(handle) { return this.values.has(Number(handle)); }
|
|
@@ -244,6 +264,7 @@ function nativeEffect(planEffect, payload) {
|
|
|
244
264
|
resource: planEffect.resource,
|
|
245
265
|
source: planEffect.source
|
|
246
266
|
};
|
|
267
|
+
if (planEffect.kind === 'output.start' || planEffect.kind === 'output.write') return { ...base, argument0: payload.argument0 };
|
|
247
268
|
if (planEffect.kind === 'time.now') return { ...base, providerKind: 'time', operation: 'now', capability: 'time.wall-clock' };
|
|
248
269
|
if (planEffect.kind === 'fetch') {
|
|
249
270
|
const response = nativeFetchResponseContract(planEffect, payload);
|
|
@@ -348,6 +369,9 @@ function validateNativeModule(module, plan) {
|
|
|
348
369
|
const missing = runtimeContract.CANONICAL_NATIVE_EXPORT_NAMES.filter((name) => !exportNames.has(name));
|
|
349
370
|
if (missing.length > 0) throw new CanonicalNativeHostError('Native module is missing required ABI exports.', 'PULSE_CANONICAL_NATIVE_ABI_EXPORT_MISMATCH', { missing });
|
|
350
371
|
if (!plan || typeof plan !== 'object' || !plan.planHash) throw new TypeError('Native module execution requires its canonical native plan.');
|
|
372
|
+
if (plan.packages?.application?.effectFailure === 'package-completion' && !exports.some(entry => entry.name === 'pulse_package_set_effect_failure' && entry.kind === 'function')) {
|
|
373
|
+
throw new CanonicalNativeHostError('Native package application is missing its effect failure ABI.', 'PULSE_CANONICAL_NATIVE_ABI_EXPORT_MISMATCH');
|
|
374
|
+
}
|
|
351
375
|
const eventReachable = Boolean(plan.events && plan.events.catalog && plan.events.catalog.events.length > 0);
|
|
352
376
|
const eventExportNames = eventContract.EVENT_NATIVE_ABI_EXTENSION.exports.map((entry) => entry.name);
|
|
353
377
|
const presentEventExports = eventExportNames.filter((name) => exportNames.has(name));
|
|
@@ -371,9 +395,32 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
371
395
|
const moduleShape = validateNativeModule(module, plan);
|
|
372
396
|
const adapter = normalizeProviderAdapter(options.providerAdapter || options.provider);
|
|
373
397
|
const executionPlane = options.executionPlane === 'event' ? 'event' : 'http';
|
|
374
|
-
const
|
|
398
|
+
const memoryBudget = (runtimeContract.hasBoundedReadLoop(plan) || plan.capabilities.includes('response.output') || plan.capabilities.includes('request.body.transform'))
|
|
399
|
+
? new NativeValueBudget(runtimeContract.CANONICAL_NATIVE_READ_LOOP_MEMORY, close) : undefined;
|
|
400
|
+
const heap = new ValueHeap(memoryBudget);
|
|
375
401
|
const pending = [];
|
|
402
|
+
const invocations = createEffectInvocations(() => {
|
|
403
|
+
options.requestBudget?.check();
|
|
404
|
+
if (options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
405
|
+
});
|
|
406
|
+
function close() {
|
|
407
|
+
invocations.close();
|
|
408
|
+
pending.length = 0;
|
|
409
|
+
options.signal?.removeEventListener('abort', close);
|
|
410
|
+
}
|
|
411
|
+
function run(operation) {
|
|
412
|
+
try {
|
|
413
|
+
invocations.assertOpen();
|
|
414
|
+
const status = operation();
|
|
415
|
+
if (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.COMPLETE || status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.FAILED) close();
|
|
416
|
+
return status;
|
|
417
|
+
} catch (error) { close(); throw error; }
|
|
418
|
+
}
|
|
376
419
|
const trace = [];
|
|
420
|
+
if (memoryBudget) Object.defineProperty(trace, 'push', { value(...entries) {
|
|
421
|
+
for (const entry of entries) memoryBudget.retain(entry);
|
|
422
|
+
return Array.prototype.push.apply(this, entries);
|
|
423
|
+
} });
|
|
377
424
|
const sensitiveValues = new Set();
|
|
378
425
|
const configuredSecrets = options.secrets && typeof options.secrets === 'object' ? options.secrets : {};
|
|
379
426
|
for (const value of Object.values(configuredSecrets)) if (typeof value === 'string' && value.length > 0) sensitiveValues.add(value);
|
|
@@ -569,6 +616,15 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
569
616
|
}
|
|
570
617
|
|
|
571
618
|
const pulseHost = {
|
|
619
|
+
value_json(handle) {
|
|
620
|
+
const text = JSON.stringify(value(handle) ?? null);
|
|
621
|
+
if (Buffer.byteLength(text, 'utf8') > runtimeContract.CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES) throw new CanonicalNativeHostError('Native value transfer exceeds 1 MiB.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
622
|
+
const exports = instance.exports;
|
|
623
|
+
const pointer = exports.__new(text.length * 2, exports.pulse_schema_string_id());
|
|
624
|
+
const view = new Uint16Array(memory().buffer, pointer, text.length);
|
|
625
|
+
for (let index = 0; index < text.length; index++) view[index] = text.charCodeAt(index);
|
|
626
|
+
return pointer;
|
|
627
|
+
},
|
|
572
628
|
value_undefined() { return put(undefined); },
|
|
573
629
|
value_null() { return put(null); },
|
|
574
630
|
value_boolean(input) { return put(Number(input) !== 0); },
|
|
@@ -578,26 +634,43 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
578
634
|
value_array_push(arrayHandle, valueHandle) {
|
|
579
635
|
const target = value(arrayHandle);
|
|
580
636
|
if (!Array.isArray(target)) throw new CanonicalNativeHostError('value_array_push target is not an array.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { arrayHandle });
|
|
637
|
+
memoryBudget?.write(target, String(target.length), value(valueHandle));
|
|
581
638
|
target.push(value(valueHandle));
|
|
582
639
|
},
|
|
583
640
|
value_array_spread(arrayHandle, valueHandle) {
|
|
584
641
|
const target = value(arrayHandle);
|
|
585
642
|
const source = value(valueHandle);
|
|
586
643
|
if (!Array.isArray(target) || !source || typeof source[Symbol.iterator] !== 'function') throw new CanonicalNativeHostError('value_array_spread requires iterable input.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
587
|
-
|
|
644
|
+
const append = item => {
|
|
645
|
+
memoryBudget?.write(target, String(target.length), item);
|
|
646
|
+
target.push(item);
|
|
647
|
+
};
|
|
648
|
+
if (Array.isArray(source)) {
|
|
649
|
+
const length = source.length;
|
|
650
|
+
for (let index = 0; index < length; index++) append(source[index]);
|
|
651
|
+
} else for (const item of source) append(item);
|
|
588
652
|
},
|
|
589
653
|
value_object() { return put({}); },
|
|
590
654
|
value_object_set(objectHandle, keyHandle, valueHandle) {
|
|
591
655
|
const target = value(objectHandle);
|
|
592
656
|
if (!target || typeof target !== 'object') throw new CanonicalNativeHostError('value_object_set target is not an object.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
593
|
-
|
|
657
|
+
const key = String(value(keyHandle));
|
|
658
|
+
memoryBudget?.write(target, key, value(valueHandle));
|
|
659
|
+
target[key] = value(valueHandle);
|
|
594
660
|
},
|
|
595
661
|
value_object_spread(objectHandle, valueHandle) {
|
|
596
662
|
const target = value(objectHandle);
|
|
597
663
|
const source = value(valueHandle);
|
|
598
664
|
if (!target || typeof target !== 'object') throw new CanonicalNativeHostError('value_object_spread target is not an object.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
599
665
|
if (source === null || source === undefined) return;
|
|
600
|
-
|
|
666
|
+
// String spreading materializes one property per UTF-16 code unit.
|
|
667
|
+
// Reserve that capacity before asking the engine to enumerate its keys.
|
|
668
|
+
if (typeof source === 'string') memoryBudget?.charge(source.length,
|
|
669
|
+
source.length * (runtimeContract.CANONICAL_NATIVE_READ_LOOP_MEMORY.valueBytes + runtimeContract.CANONICAL_NATIVE_READ_LOOP_MEMORY.edgeBytes));
|
|
670
|
+
for (const key of Object.keys(source)) {
|
|
671
|
+
memoryBudget?.write(target, key, source[key]);
|
|
672
|
+
target[key] = source[key];
|
|
673
|
+
}
|
|
601
674
|
},
|
|
602
675
|
value_property(objectHandle, keyHandle) {
|
|
603
676
|
const target = value(objectHandle);
|
|
@@ -608,6 +681,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
608
681
|
const target = value(objectHandle);
|
|
609
682
|
if (target === null || target === undefined) throw new CanonicalNativeHostError('Cannot write a property on null or undefined.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
610
683
|
const assigned = value(valueHandle);
|
|
684
|
+
memoryBudget?.write(target, String(value(keyHandle)), assigned);
|
|
611
685
|
target[String(value(keyHandle))] = assigned;
|
|
612
686
|
return valueHandle;
|
|
613
687
|
},
|
|
@@ -620,6 +694,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
620
694
|
const target = value(objectHandle);
|
|
621
695
|
if (target === null || target === undefined) throw new CanonicalNativeHostError('Cannot write an element on null or undefined.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE', { objectHandle });
|
|
622
696
|
const assigned = value(valueHandle);
|
|
697
|
+
memoryBudget?.write(target, value(keyHandle), assigned);
|
|
623
698
|
target[value(keyHandle)] = assigned;
|
|
624
699
|
return valueHandle;
|
|
625
700
|
},
|
|
@@ -657,9 +732,19 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
657
732
|
const header = context.ctx.req.headers.find(([key]) => String(key).toLowerCase() === name);
|
|
658
733
|
return put(header ? header[1] : undefined);
|
|
659
734
|
},
|
|
660
|
-
|
|
735
|
+
request_body() {
|
|
736
|
+
requireHttpSurface('ctx.req.body');
|
|
737
|
+
if (!options.incomingBody || adapter.id !== 'node') throw new CanonicalNativeHostError('Incoming forwarding requires an admitted Node request owner.', 'PULSE_REQUEST_FORWARDING_UNAVAILABLE');
|
|
738
|
+
return put(options.incomingBody.marker());
|
|
739
|
+
},
|
|
740
|
+
request_text() {
|
|
741
|
+
requireHttpSurface('ctx.req.text');
|
|
742
|
+
if (options.incomingBody) throw new CanonicalNativeHostError('Native forwarding/transform admission cannot also project structured request bytes.', 'PULSE_REQUEST_BODY_OWNERSHIP');
|
|
743
|
+
return put(context.ctx.req.text());
|
|
744
|
+
},
|
|
661
745
|
request_json(schemaHandle) {
|
|
662
746
|
requireHttpSurface('ctx.req.json');
|
|
747
|
+
if (options.incomingBody) throw new CanonicalNativeHostError('Native forwarding/transform admission cannot also project structured request bytes.', 'PULSE_REQUEST_BODY_OWNERSHIP');
|
|
663
748
|
const schema = value(schemaHandle);
|
|
664
749
|
return put(schema === undefined ? context.ctx.req.json() : context.ctx.req.json(schema));
|
|
665
750
|
},
|
|
@@ -736,14 +821,26 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
736
821
|
if (!response || typeof response.header !== 'function') throw new CanonicalNativeHostError('fetch_header requires a fetch response.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
|
|
737
822
|
return put(response.header(value(nameHandle)));
|
|
738
823
|
},
|
|
824
|
+
output_close() {
|
|
825
|
+
if (!options.outputExecution) throw new portableKv.PulseRuntimeContractError('PULSE_OUTPUT_UNAVAILABLE', 'Generated output requires Node HTTP execution.');
|
|
826
|
+
return put(options.outputExecution.close());
|
|
827
|
+
},
|
|
739
828
|
effect_begin(effectIndex, payloadHandle) {
|
|
829
|
+
options.outputExecution?.assertEffect();
|
|
740
830
|
const index = Number(effectIndex);
|
|
741
831
|
const effect = plan.effects && plan.effects[index];
|
|
742
832
|
if (!effect) throw new CanonicalNativeHostError(`Native module requested unknown effect index ${index}.`, 'PULSE_CANONICAL_NATIVE_EFFECT_INDEX_INVALID', { effectIndex: index });
|
|
743
833
|
const payload = value(payloadHandle);
|
|
834
|
+
// Admission happens while the guest builds its suspension group. A bad
|
|
835
|
+
// later member throws before the host dispatch loop starts any member.
|
|
836
|
+
if (effect.kind === 'fetch' && options.incomingBody) {
|
|
837
|
+
const admitted = canonicalRuntime.normalizeProviderEffect(nativeEffect(effect, payload), schemaCodecs, { ...options, strict, target: 'native', provider: adapter.id });
|
|
838
|
+
if (admitted.init.bodyMode === 'incoming-request-v1') options.incomingBody.claim(admitted.init.body, admitted.init);
|
|
839
|
+
}
|
|
744
840
|
const kvAdmission = portableKv.isConditionalKv(effect.kind) ? portableKv.admitConditionalKv(nativeEffect(effect, payload), options) : undefined;
|
|
745
841
|
if (kvAdmission) portableKv.registerKvRedactions(kvAdmission, (value) => sensitiveValues.add(value));
|
|
746
|
-
|
|
842
|
+
const ticket = invocations.open(index, effect.id);
|
|
843
|
+
pending.push(Object.freeze({ index, effect, payload, ticket, ...(kvAdmission ? { kvAdmission } : {}) }));
|
|
747
844
|
}
|
|
748
845
|
};
|
|
749
846
|
|
|
@@ -763,6 +860,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
763
860
|
let applicationError = 0;
|
|
764
861
|
const applicationErrors = executionPlane === 'http' && (plan.routing?.entries || []).some(entry => entry.kind === 'error');
|
|
765
862
|
function captureApplicationError(error) {
|
|
863
|
+
if (options.outputExecution?.started) return false;
|
|
766
864
|
if (!applicationErrors || options.signal?.aborted || !portableKv.isApplicationError(error)) return false;
|
|
767
865
|
if (!applicationError) {
|
|
768
866
|
const safe = canonicalRuntime.redactRuntimeError(error, sensitiveValues);
|
|
@@ -857,13 +955,25 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
857
955
|
return result;
|
|
858
956
|
}
|
|
859
957
|
|
|
860
|
-
function setEffectResult(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
958
|
+
function setEffectResult(ticket, result) {
|
|
959
|
+
return invocations.settle(ticket, () => {
|
|
960
|
+
const handle = put(result);
|
|
961
|
+
const accepted = instance.exports.pulse_set_effect_result(ticket.slot, handle);
|
|
962
|
+
if (accepted !== runtimeContract.CANONICAL_NATIVE_RESULT_STATUS.ACCEPTED) {
|
|
963
|
+
close();
|
|
964
|
+
throw new CanonicalNativeHostError('Native module rejected an invocation result.', 'PULSE_CANONICAL_NATIVE_EFFECT_INDEX_INVALID', { effectIndex: ticket.slot, errorCode: instance.exports.pulse_last_error_code() });
|
|
965
|
+
}
|
|
966
|
+
return handle;
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function setEffectFailure(ticket) {
|
|
971
|
+
return invocations.settle(ticket, () => {
|
|
972
|
+
if (instance.exports.pulse_package_set_effect_failure(ticket.slot) !== runtimeContract.CANONICAL_NATIVE_RESULT_STATUS.ACCEPTED) {
|
|
973
|
+
close();
|
|
974
|
+
throw new CanonicalNativeHostError('Native package rejected an invocation failure.', 'PULSE_CANONICAL_NATIVE_EFFECT_INDEX_INVALID');
|
|
975
|
+
}
|
|
976
|
+
});
|
|
867
977
|
}
|
|
868
978
|
|
|
869
979
|
function resultValue() {
|
|
@@ -903,9 +1013,11 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
903
1013
|
schemaId: selected.schemaId,
|
|
904
1014
|
payloadHandle
|
|
905
1015
|
});
|
|
906
|
-
return instance.exports.pulse_event_start(selected.runtimeId, payloadHandle);
|
|
1016
|
+
return run(() => instance.exports.pulse_event_start(selected.runtimeId, payloadHandle));
|
|
907
1017
|
}
|
|
908
1018
|
|
|
1019
|
+
options.signal?.addEventListener('abort', close, { once: true });
|
|
1020
|
+
if (options.signal?.aborted) close();
|
|
909
1021
|
return Object.freeze({
|
|
910
1022
|
version: runtimeContract.CANONICAL_NATIVE_HOST_VERSION,
|
|
911
1023
|
plan,
|
|
@@ -922,14 +1034,17 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
|
|
|
922
1034
|
cryptoVerifier: nativeCrypto.verifier,
|
|
923
1035
|
cryptoRealization: nativeCrypto.realization,
|
|
924
1036
|
executionPlane,
|
|
925
|
-
start() { requireExecutionPlane('http'); return instance.exports.pulse_start(); },
|
|
1037
|
+
start() { requireExecutionPlane('http'); return run(() => instance.exports.pulse_start()); },
|
|
926
1038
|
startEvent,
|
|
927
1039
|
eventSelection() { return activeEvent; },
|
|
928
|
-
resume() { return instance.exports.pulse_resume(); },
|
|
1040
|
+
resume() { return run(() => instance.exports.pulse_resume()); },
|
|
1041
|
+
close,
|
|
1042
|
+
assertPendingEffect: invocations.assertPending,
|
|
929
1043
|
pendingEffects: takePending,
|
|
930
1044
|
prepareEffectResult,
|
|
931
1045
|
captureApplicationError,
|
|
932
1046
|
setEffectResult,
|
|
1047
|
+
setEffectFailure,
|
|
933
1048
|
resultValue,
|
|
934
1049
|
response() { requireHttpSurface('HTTP completion'); return canonicalRuntime.finalResponse(resultValue(), context.ctx.req.method); },
|
|
935
1050
|
programCounter() { return instance.exports.pulse_program_counter(); },
|
|
@@ -977,6 +1092,13 @@ function raceNativeSignal(value, signal, eventMode = true) {
|
|
|
977
1092
|
}
|
|
978
1093
|
|
|
979
1094
|
async function executeCanonicalNativeInvocation(compiled, options = {}, invocation = Object.freeze({ plane: 'http' })) {
|
|
1095
|
+
const ownedResponseBodies = new Set();
|
|
1096
|
+
let responseBodyTransferred, invocationClosed = false;
|
|
1097
|
+
const cancelBody = body => { try { void body.cancel().catch(() => {}); } catch (_) {} };
|
|
1098
|
+
options = { ...options, registerResponseBody(body) {
|
|
1099
|
+
if (invocationClosed) cancelBody(body);
|
|
1100
|
+
else ownedResponseBodies.add(body);
|
|
1101
|
+
} };
|
|
980
1102
|
const eventMode = invocation && invocation.plane === 'event';
|
|
981
1103
|
const maxEffects = normalizeMaxEffects(options.maxEffects);
|
|
982
1104
|
const controller = instantiateCanonicalNativeModule(compiled, { ...options, executionPlane: eventMode ? 'event' : 'http' });
|
|
@@ -1020,6 +1142,7 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1020
1142
|
}
|
|
1021
1143
|
activeContinuation = {
|
|
1022
1144
|
id: continuation.id,
|
|
1145
|
+
invocationId: `continuation-${pending[0].ticket.invocationId}`,
|
|
1023
1146
|
kind: continuation.kind,
|
|
1024
1147
|
effectIds: Object.freeze([...pendingEffectIds]),
|
|
1025
1148
|
stateIndex: continuationState,
|
|
@@ -1027,7 +1150,7 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1027
1150
|
states: ['created', 'waiting']
|
|
1028
1151
|
};
|
|
1029
1152
|
continuations.push(activeContinuation);
|
|
1030
|
-
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-waiting', executionId, continuationId: continuation.id, continuationState, effectIds: Object.freeze([...pendingEffectIds]) }, controller.sensitiveValues)));
|
|
1153
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-waiting', executionId, continuationId: continuation.id, invocationId: activeContinuation.invocationId, continuationState, effectIds: Object.freeze([...pendingEffectIds]) }, controller.sensitiveValues)));
|
|
1031
1154
|
if (effectCount + pending.length > maxEffects) {
|
|
1032
1155
|
throw new CanonicalNativeHostError(
|
|
1033
1156
|
eventMode
|
|
@@ -1039,6 +1162,7 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1039
1162
|
}
|
|
1040
1163
|
effectCount += pending.length;
|
|
1041
1164
|
|
|
1165
|
+
if (pending.length > 1 && pending.some(entry => entry.effect.kind.startsWith('output.'))) throw new portableKv.PulseRuntimeContractError('PULSE_OUTPUT_PARALLEL_FORBIDDEN', 'Output effects cannot be grouped.');
|
|
1042
1166
|
const settled = await raceNativeSignal(Promise.allSettled(pending.map(async (entry) => {
|
|
1043
1167
|
options.requestBudget?.check();
|
|
1044
1168
|
if (eventMode && options.signal && options.signal.aborted) {
|
|
@@ -1046,7 +1170,7 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1046
1170
|
}
|
|
1047
1171
|
const rawEffect = { ...nativeEffect(entry.effect, entry.payload), ...entry.kvAdmission };
|
|
1048
1172
|
const conditional = portableKv.isConditionalKv(rawEffect.kind);
|
|
1049
|
-
const privateEffect = conditional || ((controller.plan.packages && controller.plan.packages.effects) || []).some((item) =>
|
|
1173
|
+
const privateEffect = rawEffect.kind.startsWith('output.') || conditional || ((controller.plan.packages && controller.plan.packages.effects) || []).some((item) =>
|
|
1050
1174
|
item.package === entry.effect.package && item.contractId === entry.effect.contractId && item.operation === entry.effect.operation
|
|
1051
1175
|
&& item.redaction && Object.keys(item.redaction).length > 0);
|
|
1052
1176
|
const normalized = canonicalRuntime.normalizeProviderEffect(rawEffect, controller.schemaCodecs, {
|
|
@@ -1058,10 +1182,12 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1058
1182
|
target: 'native',
|
|
1059
1183
|
provider: adapter.id
|
|
1060
1184
|
});
|
|
1061
|
-
controller.
|
|
1185
|
+
controller.assertPendingEffect(entry.ticket);
|
|
1186
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-effect-start', executionId, invocationId: entry.ticket.invocationId, provider: adapter.id, effectId: normalized.id, kind: normalized.kind, payload: privateEffect ? '<redacted>' : entry.payload }, controller.sensitiveValues)));
|
|
1062
1187
|
const effectExecution = {
|
|
1063
1188
|
...options,
|
|
1064
1189
|
executionId,
|
|
1190
|
+
invocationId: entry.ticket.invocationId,
|
|
1065
1191
|
metadata: compiled && compiled.plan ? compiled.plan.canonical : controller.plan.canonical,
|
|
1066
1192
|
plan: controller.plan,
|
|
1067
1193
|
packageArtifacts,
|
|
@@ -1075,13 +1201,20 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1075
1201
|
return controller.schemaCodecs.decode(String(schemaId), value, context.source || 'package-effect');
|
|
1076
1202
|
}
|
|
1077
1203
|
};
|
|
1078
|
-
|
|
1204
|
+
options.outputExecution?.assertEffect();
|
|
1205
|
+
const rawResult = normalized.kind.startsWith('output.')
|
|
1206
|
+
? await (options.outputExecution ? options.outputExecution.dispatch(normalized) : Promise.reject(new portableKv.PulseRuntimeContractError('PULSE_OUTPUT_UNAVAILABLE', 'Generated output requires Node HTTP execution.')))
|
|
1207
|
+
: conditional
|
|
1079
1208
|
? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)),
|
|
1080
1209
|
{ ...effectExecution, onKvObservation: (event) => controller.trace.push(event) }, options)
|
|
1081
1210
|
: await raceNativeSignal(adapter.dispatchEffect(normalized, effectExecution), options.signal, eventMode);
|
|
1082
1211
|
options.requestBudget?.check();
|
|
1083
|
-
|
|
1084
|
-
|
|
1212
|
+
controller.assertPendingEffect(entry.ticket);
|
|
1213
|
+
// executeConditionalKv already validates, detaches, freezes and registers
|
|
1214
|
+
// redactions using these execution limits. Reuse only that internal
|
|
1215
|
+
// result; raw controller callers still use prepareEffectResult.
|
|
1216
|
+
const result = conditional ? rawResult : controller.prepareEffectResult(entry.index, rawResult);
|
|
1217
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-effect-resolved', executionId, invocationId: entry.ticket.invocationId, provider: adapter.id, effectId: normalized.id, kind: normalized.kind, result: privateEffect || entry.effect.kind === 'secret.get' ? '<redacted>' : result && typeof result.toJSON === 'function' ? result.toJSON() : result }, controller.sensitiveValues)));
|
|
1085
1218
|
resolutionOrder.push(entry.effect.id);
|
|
1086
1219
|
return { entry, result };
|
|
1087
1220
|
})), options.signal, eventMode);
|
|
@@ -1093,12 +1226,15 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1093
1226
|
if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
1094
1227
|
const failures = settled.filter(entry => entry.status === 'rejected');
|
|
1095
1228
|
const failed = failures.find(entry => !portableKv.isApplicationError(entry.reason)) || failures[0];
|
|
1096
|
-
|
|
1229
|
+
const packageFailure = controller.plan.packages?.application?.effectFailure === 'package-completion';
|
|
1230
|
+
if (failed && !packageFailure && (!failures.every(entry => portableKv.isApplicationError(entry.reason))
|
|
1097
1231
|
|| !controller.captureApplicationError(failed.reason))) {
|
|
1098
1232
|
throw canonicalRuntime.redactRuntimeError(failed.reason, controller.sensitiveValues);
|
|
1099
1233
|
}
|
|
1100
|
-
settled.forEach((item, index) =>
|
|
1101
|
-
item.status === '
|
|
1234
|
+
settled.forEach((item, index) => {
|
|
1235
|
+
if (item.status === 'rejected' && packageFailure) controller.setEffectFailure(pending[index].ticket);
|
|
1236
|
+
else controller.setEffectResult(pending[index].ticket, item.status === 'fulfilled' ? item.value.result : undefined);
|
|
1237
|
+
});
|
|
1102
1238
|
const previousPc = controller.programCounter();
|
|
1103
1239
|
activeContinuation.state = 'resumed';
|
|
1104
1240
|
activeContinuation.states.push('resumed');
|
|
@@ -1108,7 +1244,7 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1108
1244
|
}
|
|
1109
1245
|
activeContinuation.state = failed ? 'failed' : 'completed';
|
|
1110
1246
|
activeContinuation.states.push(activeContinuation.state);
|
|
1111
|
-
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, continuationState }, controller.sensitiveValues)));
|
|
1247
|
+
controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, invocationId: activeContinuation.invocationId, continuationState }, controller.sensitiveValues)));
|
|
1112
1248
|
activeContinuation = undefined;
|
|
1113
1249
|
}
|
|
1114
1250
|
|
|
@@ -1136,12 +1272,15 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1136
1272
|
resolutionOrder: Object.freeze([...resolutionOrder]),
|
|
1137
1273
|
continuations: Object.freeze(continuations.map((entry) => Object.freeze({ ...entry, states: Object.freeze([...entry.states]) }))),
|
|
1138
1274
|
trace: Object.freeze([...controller.trace]),
|
|
1139
|
-
valueHandleCount: controller.heap.size()
|
|
1275
|
+
valueHandleCount: controller.heap.size(),
|
|
1276
|
+
...(controller.heap.budget ? { memory: controller.heap.budget.snapshot() } : {})
|
|
1140
1277
|
});
|
|
1141
1278
|
}
|
|
1142
1279
|
if (options.signal?.aborted) throw nativeRequestCancelled(options.signal);
|
|
1143
1280
|
options.requestBudget?.check();
|
|
1281
|
+
options.outputExecution?.validateResult(controller.resultValue());
|
|
1144
1282
|
const response = controller.response();
|
|
1283
|
+
responseBodyTransferred = response.bodyStream;
|
|
1145
1284
|
controller.trace.push(Object.freeze(redactValue({ type: 'native-execution-completed', executionId, provider: adapter.id, status: response.status, bodyClass: response.bodyClass }, controller.sensitiveValues)));
|
|
1146
1285
|
return Object.freeze({
|
|
1147
1286
|
status: 'completed',
|
|
@@ -1155,10 +1294,12 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1155
1294
|
wasmSha256: controller.wasmSha256,
|
|
1156
1295
|
response,
|
|
1157
1296
|
effectCount,
|
|
1297
|
+
...((options.incomingBody || options.outputExecution) ? {guestMemoryBytes:controller.exports.memory.buffer.byteLength} : {}),
|
|
1158
1298
|
resolutionOrder: Object.freeze([...resolutionOrder]),
|
|
1159
1299
|
continuations: Object.freeze(continuations.map((entry) => Object.freeze({ ...entry, states: Object.freeze([...entry.states]) }))),
|
|
1160
1300
|
trace: Object.freeze([...controller.trace]),
|
|
1161
|
-
valueHandleCount: controller.heap.size()
|
|
1301
|
+
valueHandleCount: controller.heap.size(),
|
|
1302
|
+
...(controller.heap.budget ? { memory: controller.heap.budget.snapshot() } : {})
|
|
1162
1303
|
});
|
|
1163
1304
|
} catch (error) {
|
|
1164
1305
|
executionFailure = canonicalRuntime.redactRuntimeError(error, controller.sensitiveValues);
|
|
@@ -1175,6 +1316,10 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
|
|
|
1175
1316
|
}) });
|
|
1176
1317
|
throw executionFailure;
|
|
1177
1318
|
} finally {
|
|
1319
|
+
invocationClosed = true;
|
|
1320
|
+
for (const body of ownedResponseBodies) if (body !== responseBodyTransferred) cancelBody(body);
|
|
1321
|
+
ownedResponseBodies.clear();
|
|
1322
|
+
controller.close();
|
|
1178
1323
|
try { adapter.disposeExecution(Object.freeze({ executionId, metadata: controller.plan.canonical, plan: controller.plan, status: executionFailure ? 'failed' : 'completed', error: executionFailure })); }
|
|
1179
1324
|
catch (_) { /* provider cleanup must not mask execution */ }
|
|
1180
1325
|
}
|
|
@@ -1184,7 +1329,7 @@ async function executeCanonicalNativeModule(compiled, options = {}) {
|
|
|
1184
1329
|
const budget = portableKv.createRequestBudget(options);
|
|
1185
1330
|
try {
|
|
1186
1331
|
budget.check();
|
|
1187
|
-
return await executeCanonicalNativeInvocation(compiled, { ...options, requestBudget: budget, signal: budget.signal, deadlineMonotonicMs: budget.deadlineMonotonicMs ?? options.deadlineMonotonicMs, kvClock: budget.deadlineMonotonicMs === undefined ? options.kvClock : budget.clock }, Object.freeze({ plane: 'http' }));
|
|
1332
|
+
return await executeCanonicalNativeInvocation(compiled, { ...options, responseSignal: options.signal, requestBudget: budget, signal: budget.signal, deadlineMonotonicMs: budget.deadlineMonotonicMs ?? options.deadlineMonotonicMs, kvClock: budget.deadlineMonotonicMs === undefined ? options.kvClock : budget.clock }, Object.freeze({ plane: 'http' }));
|
|
1188
1333
|
} finally { if (!options.requestBudget) budget.close(); }
|
|
1189
1334
|
}
|
|
1190
1335
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CANONICAL_EFFECT_INVOCATION_POLICY } = require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
|
|
4
|
+
let executionSequence = 0;
|
|
5
|
+
|
|
6
|
+
function normalizeMaxEffects(value) {
|
|
7
|
+
const max = value === undefined ? CANONICAL_EFFECT_INVOCATION_POLICY.defaultMaxEffects : Number(value);
|
|
8
|
+
if (!Number.isSafeInteger(max) || max <= 0) throw new TypeError('Pulse maxEffects must be a positive safe integer.');
|
|
9
|
+
return max;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A ticket is authenticated by object identity, never by caller-supplied fields.
|
|
13
|
+
// Only pending slots are retained; the sequence is cumulative across loop visits.
|
|
14
|
+
function createEffectInvocations(check = () => {}) {
|
|
15
|
+
const execution = ++executionSequence;
|
|
16
|
+
const active = new Map();
|
|
17
|
+
let sequence = 0;
|
|
18
|
+
let closed = false;
|
|
19
|
+
function invalid() {
|
|
20
|
+
const error = new Error('Effect invocation is stale, already settled, foreign, or closed.');
|
|
21
|
+
error.name = 'EffectInvocationError';
|
|
22
|
+
error.code = 'PULSE_EFFECT_INVOCATION_INVALID';
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
function assertOpen() { if (closed) invalid(); check(); }
|
|
26
|
+
function assertPending(ticket) {
|
|
27
|
+
assertOpen();
|
|
28
|
+
if (!ticket || active.get(ticket.slot) !== ticket) invalid();
|
|
29
|
+
return ticket;
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
execution,
|
|
33
|
+
open(slot, siteId) {
|
|
34
|
+
assertOpen();
|
|
35
|
+
if (active.has(slot) || !Number.isSafeInteger(sequence + 1)) invalid();
|
|
36
|
+
const ticket = Object.freeze({ slot, siteId, sequence: ++sequence, invocationId: `effect-${execution}-${sequence}` });
|
|
37
|
+
active.set(slot, ticket);
|
|
38
|
+
return ticket;
|
|
39
|
+
},
|
|
40
|
+
assertOpen,
|
|
41
|
+
assertPending,
|
|
42
|
+
settle(ticket, consume) {
|
|
43
|
+
assertPending(ticket);
|
|
44
|
+
active.delete(ticket.slot); // Consume before calling into the guest.
|
|
45
|
+
return consume();
|
|
46
|
+
},
|
|
47
|
+
close() { closed = true; active.clear(); },
|
|
48
|
+
get closed() { return closed; }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { createEffectInvocations, normalizeMaxEffects };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION,
|
|
5
|
+
CRYPTO_RS256_GUEST_LINKED_IMPLEMENTATION, CRYPTO_RS256_GUEST_LINKED_REALIZATION,
|
|
5
6
|
CRYPTO_ES256_GUEST_LINKED_REALIZATION,
|
|
6
7
|
CRYPTO_GUEST_SOURCE_IMPLEMENTATION,
|
|
7
8
|
CRYPTO_REALIZATION_PLAN_VERSION
|
|
@@ -230,6 +231,9 @@ function createNativeGuestSourceCryptoVerifier(input = {}) {
|
|
|
230
231
|
const moduleExports = input.exports;
|
|
231
232
|
const selected = selectedHs256(plan);
|
|
232
233
|
const selectedSignature = selectedEs256(plan);
|
|
234
|
+
const selectedRsa = selectedAlgorithm(plan, 'RS256', CRYPTO_RS256_GUEST_LINKED_REALIZATION, 'guest-linked', CRYPTO_RS256_GUEST_LINKED_IMPLEMENTATION);
|
|
235
|
+
let rsa;
|
|
236
|
+
if (selectedRsa) { try { rsa = require('@pulse-compute/crypto/pulsewasm-native').bindNativeRs256(moduleExports); } catch {} }
|
|
233
237
|
const memory = moduleExports && moduleExports.memory;
|
|
234
238
|
const verifyHs256 = moduleExports && moduleExports[HS256_EXPORT];
|
|
235
239
|
const verifyEs256 = moduleExports && moduleExports[ES256_EXPORT];
|
|
@@ -249,6 +253,7 @@ function createNativeGuestSourceCryptoVerifier(input = {}) {
|
|
|
249
253
|
const digestMac = byteAlgorithms.length > 0
|
|
250
254
|
? require('@pulse-compute/crypto/pulsewasm-native').bindNativeDigestMac(moduleExports) : undefined;
|
|
251
255
|
const selectedRealizations = Object.freeze([
|
|
256
|
+
...(selectedRsa ? [{ algorithm: 'RS256', realization: CRYPTO_RS256_GUEST_LINKED_REALIZATION, implementation: CRYPTO_RS256_GUEST_LINKED_IMPLEMENTATION, guestUnitRequired: true, available: Boolean(rsa) }] : []),
|
|
252
257
|
...byteAlgorithms.map((algorithm) => Object.freeze({ algorithm, realization: HS256_REALIZATION,
|
|
253
258
|
implementation: CRYPTO_GUEST_SOURCE_IMPLEMENTATION, guestUnitRequired: false, available: Boolean(digestMac) })),
|
|
254
259
|
...(selected ? [Object.freeze({
|
|
@@ -276,8 +281,12 @@ function createNativeGuestSourceCryptoVerifier(input = {}) {
|
|
|
276
281
|
...(selectedRealizations.length === 1 ? selectedRealizations[0] : {})
|
|
277
282
|
});
|
|
278
283
|
|
|
284
|
+
const signer = es256Available && typeof moduleExports.pulse_crypto_es256_sign === 'function'
|
|
285
|
+
? require('@pulse-compute/crypto/pulsewasm-native').bindNativeEs256Signer(moduleExports) : undefined;
|
|
279
286
|
const verifier = Object.freeze({
|
|
280
|
-
...(digestMac ? { bytes: Object.freeze({
|
|
287
|
+
...(digestMac || signer || rsa ? { bytes: Object.freeze({
|
|
288
|
+
...(signer ? { es256Sign: signer } : {}),
|
|
289
|
+
...(rsa ? { rs256Sign: rsa.sign } : {}),
|
|
281
290
|
...(byteAlgorithms.includes('SHA-256') ? { sha256: digestMac.sha256 } : {}),
|
|
282
291
|
...(byteAlgorithms.includes('HMAC-SHA256') ? { hmacSha256: digestMac.hmacSha256 } : {})
|
|
283
292
|
}) } : {}),
|
|
@@ -313,6 +322,13 @@ function createNativeGuestSourceCryptoVerifier(input = {}) {
|
|
|
313
322
|
}),
|
|
314
323
|
signature: Object.freeze({
|
|
315
324
|
verify(request) {
|
|
325
|
+
if (ownData(request, 'algorithm') === 'RS256') {
|
|
326
|
+
const key = ownData(request, 'key');
|
|
327
|
+
if (ownData(key, 'type') !== 'rsa-public-key-bytes') return result('invalid-key');
|
|
328
|
+
if (!rsa) return result('realization-failure');
|
|
329
|
+
try { return rsa.verify(ownData(key, 'bytes'), ownData(request, 'data'), ownData(request, 'signature')); }
|
|
330
|
+
catch (error) { return result(error.code === 'PULSE_CRYPTO_KEY_INVALID' ? 'invalid-key' : 'invalid-input'); }
|
|
331
|
+
}
|
|
316
332
|
const normalized = normalizeSignatureRequest(request);
|
|
317
333
|
if (normalized.result) return normalized.result;
|
|
318
334
|
if (!es256Available) return result('realization-failure');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A conservative cumulative charge, deliberately never refunded. Counting a
|
|
4
|
+
// primitive again on a property read can overestimate retention. Object graphs
|
|
5
|
+
// are visited once; guest mutations are charged separately at the host imports.
|
|
6
|
+
// No charge releases an alias, pending payload, schema value, or returned root.
|
|
7
|
+
class NativeValueBudget {
|
|
8
|
+
constructor(policy, onFailure = () => {}) {
|
|
9
|
+
this.policy = policy;
|
|
10
|
+
this.onFailure = onFailure;
|
|
11
|
+
this.bytes = 0;
|
|
12
|
+
this.values = 0;
|
|
13
|
+
this.failure = undefined;
|
|
14
|
+
this.objects = new WeakSet();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
charge(values, bytes) {
|
|
18
|
+
if (this.failure) throw this.failure;
|
|
19
|
+
if (values > this.policy.maxValues - this.values || bytes > this.policy.maxBytes - this.bytes) {
|
|
20
|
+
const error = new Error('Native read-loop execution exceeded its retained-value budget.');
|
|
21
|
+
error.code = 'PULSE_RUNTIME_MEMORY_LIMIT_EXCEEDED';
|
|
22
|
+
error.detail = Object.freeze({ bytes: this.bytes, values: this.values,
|
|
23
|
+
maxBytes: this.policy.maxBytes, maxValues: this.policy.maxValues });
|
|
24
|
+
this.failure = error;
|
|
25
|
+
this.onFailure();
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
this.values += values;
|
|
29
|
+
this.bytes += bytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
retain(value) {
|
|
33
|
+
const visit = input => {
|
|
34
|
+
this.charge(1, this.policy.valueBytes);
|
|
35
|
+
if (typeof input === 'string') this.charge(0, input.length * 2);
|
|
36
|
+
else if (input && typeof input === 'object' && !this.objects.has(input)) {
|
|
37
|
+
this.objects.add(input);
|
|
38
|
+
// Charge sparse capacity before visiting elements. Avoid allocating an
|
|
39
|
+
// intermediate array proportional to an untrusted container's length.
|
|
40
|
+
if (Array.isArray(input)) this.charge(input.length, input.length * this.policy.edgeBytes);
|
|
41
|
+
for (const key in input) if (Object.hasOwn(input, key)) {
|
|
42
|
+
this.charge(1, this.policy.edgeBytes + key.length * 2);
|
|
43
|
+
visit(input[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
visit(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
write(target, key, value) {
|
|
51
|
+
this.charge(1, this.policy.edgeBytes + String(key).length * 2);
|
|
52
|
+
if (Array.isArray(target)) {
|
|
53
|
+
const index = Number(key);
|
|
54
|
+
const length = key === 'length' ? Number(value) :
|
|
55
|
+
Number.isInteger(index) && index >= 0 && String(index) === String(key) ? index + 1 : target.length;
|
|
56
|
+
if (length > target.length) this.charge(length - target.length, (length - target.length) * this.policy.edgeBytes);
|
|
57
|
+
}
|
|
58
|
+
this.retain(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
snapshot() { return Object.freeze({ version: this.policy.version, bytes: this.bytes, values: this.values }); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { NativeValueBudget };
|