reploid 0.0.0-stage → 0.1.0
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/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +534 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +83 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/** Owner-approved limits for native contribution. The policy comes only from an
|
|
2
|
+
* explicit owner file: no field has a code default, an absent section requests no
|
|
3
|
+
* control, and an unrecognized key is rejected rather than silently ignored. */
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { sha256Digest } from '../hashing.js';
|
|
7
|
+
|
|
8
|
+
export const NATIVE_OWNER_POLICY_SCHEMA = 'reploid.native-owner-policy/v1';
|
|
9
|
+
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
10
|
+
const VERBS = { reject: 'rejected', defer: 'deferred', reduce: 'reduced' };
|
|
11
|
+
const fail = message => { throw new Error('Owner policy: ' + message); };
|
|
12
|
+
const check = (ok, message) => { if (!ok) fail(message); };
|
|
13
|
+
const shape = (value, name, keys) => {
|
|
14
|
+
check(value !== null && typeof value === 'object' && !Array.isArray(value), name + ' must be an object');
|
|
15
|
+
for (const key of Object.keys(value)) check(keys.includes(key), `${name}.${key} is not a recognized control`);
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
const integer = (value, name, min, max) => {
|
|
19
|
+
check(Number.isSafeInteger(value) && value >= min && value <= max, `${name} must be an integer from ${min} to ${max}`);
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
const finite = (value, name, min, max) => {
|
|
23
|
+
check(typeof value === 'number' && Number.isFinite(value) && value > min && value < max, `${name} must be a number above ${min} and below ${max}`);
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
const text = (value, name) => {
|
|
27
|
+
check(typeof value === 'string' && value.trim().length > 0 && value.length <= 256, name + ' must be a non-empty string of at most 256 characters');
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
30
|
+
const sensors = (value, name) => {
|
|
31
|
+
check(Array.isArray(value) && value.length > 0 && value.length <= 16 && new Set(value).size === value.length
|
|
32
|
+
&& value.every(item => typeof item === 'string' && /^[a-z0-9_-]{1,64}$/.test(item)), name + ' must list distinct sensor names');
|
|
33
|
+
return [...value];
|
|
34
|
+
};
|
|
35
|
+
const minute = (value, name, allowEnd) => {
|
|
36
|
+
const match = typeof value === 'string' && /^(\d{2}):(\d{2})$/.exec(value);
|
|
37
|
+
const total = match ? Number(match[1]) * 60 + Number(match[2]) : NaN;
|
|
38
|
+
check(match && Number(match[2]) < 60 && (total < 1440 || allowEnd && total === 1440), name + ' must be HH:MM local time');
|
|
39
|
+
return total;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Bytes per second below which one paced custody transfer can outlive the
|
|
43
|
+
* protocol's per-chunk deadline or its grant. Derived from custody policy. */
|
|
44
|
+
export function custodyRateFloor(custody) {
|
|
45
|
+
const { channel, maxArtifactBytes, grantMs } = custody;
|
|
46
|
+
return Math.max(Math.ceil(2 * channel.maxPendingRequests * channel.maxChunkBytes * 1000 / channel.timeoutMs),
|
|
47
|
+
Math.ceil(2 * maxArtifactBytes * 1000 / grantMs));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Simultaneous supply transfers one paced upload allowance can finish within
|
|
51
|
+
* half of each chunk deadline and half of each grant. */
|
|
52
|
+
export function custodySupplyTransfers(custody, bytesPerSecond) {
|
|
53
|
+
const { channel, maxArtifactBytes, grantMs, maxTransfers } = custody;
|
|
54
|
+
const byChunk = bytesPerSecond * channel.timeoutMs / 2000 / (channel.maxPendingRequests * channel.maxChunkBytes);
|
|
55
|
+
const byGrant = bytesPerSecond * grantMs / 2000 / maxArtifactBytes;
|
|
56
|
+
return Math.max(1, Math.min(maxTransfers, Math.floor(Math.min(byChunk, byGrant))));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function authorization(value, name) {
|
|
60
|
+
shape(value, name, ['granted', 'ownership', 'grantedBy', 'workplaceApproval']);
|
|
61
|
+
check(value.granted === true, name + ' requires an explicit owner grant');
|
|
62
|
+
check(['personal', 'workplace'].includes(value.ownership), name + '.ownership must be "personal" or "workplace"');
|
|
63
|
+
const result = { granted: true, ownership: value.ownership, grantedBy: text(value.grantedBy, name + '.grantedBy') };
|
|
64
|
+
if (value.ownership === 'workplace') result.workplaceApproval = text(value.workplaceApproval, name + '.workplaceApproval');
|
|
65
|
+
else check(value.workplaceApproval === undefined, name + '.workplaceApproval applies only to workplace resources');
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function thresholds(value, name, reduceKey, deferKey, max) {
|
|
70
|
+
const result = {};
|
|
71
|
+
if (value[reduceKey] !== undefined) result[reduceKey] = finite(value[reduceKey], `${name}.${reduceKey}`, 0, max);
|
|
72
|
+
if (value[deferKey] !== undefined) result[deferKey] = finite(value[deferKey], `${name}.${deferKey}`, 0, max);
|
|
73
|
+
check(reduceKey in result || deferKey in result, `${name} requires ${reduceKey} or ${deferKey}`);
|
|
74
|
+
check(!(reduceKey in result && deferKey in result) || result[reduceKey] < result[deferKey], `${name}.${reduceKey} must be below ${deferKey}`);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Validates and freezes the owner's explicit choices. `custody` is the application's
|
|
79
|
+
* custody policy; rate limits below its protocol floor are refused, not weakened. */
|
|
80
|
+
export function validateNativeOwnerPolicy(value, { custody }) {
|
|
81
|
+
shape(value, 'policy', ['schema', 'authorization', 'sampleIntervalMs', 'schedule', 'memory', 'bandwidth',
|
|
82
|
+
'temperature', 'power', 'foreground', 'reduced']);
|
|
83
|
+
check(value.schema === NATIVE_OWNER_POLICY_SCHEMA, 'schema must be ' + NATIVE_OWNER_POLICY_SCHEMA);
|
|
84
|
+
shape(value.authorization, 'authorization', ['equipment', 'network']);
|
|
85
|
+
const policy = { schema: NATIVE_OWNER_POLICY_SCHEMA, authorization: {
|
|
86
|
+
equipment: authorization(value.authorization.equipment, 'authorization.equipment'),
|
|
87
|
+
network: authorization(value.authorization.network, 'authorization.network') },
|
|
88
|
+
sampleIntervalMs: integer(value.sampleIntervalMs, 'sampleIntervalMs', 1000, 60000) };
|
|
89
|
+
if (value.schedule !== undefined) {
|
|
90
|
+
const schedule = shape(value.schedule, 'schedule', ['timeZone', 'windows']);
|
|
91
|
+
text(schedule.timeZone, 'schedule.timeZone');
|
|
92
|
+
try { new Intl.DateTimeFormat('en-US', { timeZone: schedule.timeZone }); }
|
|
93
|
+
catch { fail('schedule.timeZone must be an IANA time zone'); }
|
|
94
|
+
check(Array.isArray(schedule.windows) && schedule.windows.length > 0 && schedule.windows.length <= 32, 'schedule.windows must list 1 to 32 windows');
|
|
95
|
+
policy.schedule = { timeZone: schedule.timeZone, windows: schedule.windows.map((window, index) => {
|
|
96
|
+
const name = `schedule.windows[${index}]`;
|
|
97
|
+
shape(window, name, ['days', 'start', 'end']);
|
|
98
|
+
check(Array.isArray(window.days) && window.days.length > 0 && new Set(window.days).size === window.days.length
|
|
99
|
+
&& window.days.every(day => DAYS.includes(day)), name + '.days must list distinct days (sun..sat)');
|
|
100
|
+
const start = minute(window.start, name + '.start', false), end = minute(window.end, name + '.end', true);
|
|
101
|
+
check(start !== end, name + ' must not start and end at the same time');
|
|
102
|
+
return { days: [...window.days], start: window.start, end: window.end };
|
|
103
|
+
}) };
|
|
104
|
+
}
|
|
105
|
+
if (value.memory !== undefined) {
|
|
106
|
+
const memory = shape(value.memory, 'memory', ['maxProcessResidentBytes', 'maxGpuBufferBytes']);
|
|
107
|
+
policy.memory = {};
|
|
108
|
+
for (const key of ['maxProcessResidentBytes', 'maxGpuBufferBytes']) {
|
|
109
|
+
if (memory[key] !== undefined) policy.memory[key] = integer(memory[key], 'memory.' + key, 1, Number.MAX_SAFE_INTEGER);
|
|
110
|
+
}
|
|
111
|
+
check(Object.keys(policy.memory).length > 0, 'memory requires maxProcessResidentBytes or maxGpuBufferBytes');
|
|
112
|
+
}
|
|
113
|
+
if (value.bandwidth !== undefined) {
|
|
114
|
+
const bandwidth = shape(value.bandwidth, 'bandwidth', ['uploadBytesPerSecond', 'downloadBytesPerSecond']);
|
|
115
|
+
const floor = custodyRateFloor(custody);
|
|
116
|
+
policy.bandwidth = {};
|
|
117
|
+
for (const key of ['uploadBytesPerSecond', 'downloadBytesPerSecond']) {
|
|
118
|
+
if (bandwidth[key] === undefined) continue;
|
|
119
|
+
integer(bandwidth[key], 'bandwidth.' + key, 1, Number.MAX_SAFE_INTEGER);
|
|
120
|
+
check(bandwidth[key] >= floor, `bandwidth.${key} must be at least ${floor} bytes per second, the custody transfer deadline floor`);
|
|
121
|
+
policy.bandwidth[key] = bandwidth[key];
|
|
122
|
+
}
|
|
123
|
+
check(Object.keys(policy.bandwidth).length > 0, 'bandwidth requires uploadBytesPerSecond or downloadBytesPerSecond');
|
|
124
|
+
}
|
|
125
|
+
if (value.temperature !== undefined) {
|
|
126
|
+
const temperature = shape(value.temperature, 'temperature', ['sensors', 'reduceAboveCelsius', 'deferAboveCelsius']);
|
|
127
|
+
policy.temperature = { sensors: sensors(temperature.sensors, 'temperature.sensors'),
|
|
128
|
+
...thresholds(temperature, 'temperature', 'reduceAboveCelsius', 'deferAboveCelsius', 150) };
|
|
129
|
+
}
|
|
130
|
+
if (value.power !== undefined) {
|
|
131
|
+
const power = shape(value.power, 'power', ['requireExternalPower', 'gpuSensors', 'reduceAboveWatts', 'deferAboveWatts']);
|
|
132
|
+
policy.power = {};
|
|
133
|
+
if (power.requireExternalPower !== undefined) {
|
|
134
|
+
check(power.requireExternalPower === true, 'power.requireExternalPower may only be true; omit it to request no control');
|
|
135
|
+
policy.power.requireExternalPower = true;
|
|
136
|
+
}
|
|
137
|
+
// gpuSensors alone measures GPU energy without limiting work; thresholds require it.
|
|
138
|
+
if (power.gpuSensors !== undefined || power.reduceAboveWatts !== undefined || power.deferAboveWatts !== undefined) {
|
|
139
|
+
policy.power.gpuSensors = sensors(power.gpuSensors, 'power.gpuSensors');
|
|
140
|
+
if (power.reduceAboveWatts !== undefined || power.deferAboveWatts !== undefined) {
|
|
141
|
+
Object.assign(policy.power, thresholds(power, 'power', 'reduceAboveWatts', 'deferAboveWatts', 100000));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
check(Object.keys(policy.power).length > 0, 'power requires requireExternalPower or gpuSensors');
|
|
145
|
+
}
|
|
146
|
+
if (value.foreground !== undefined) {
|
|
147
|
+
const foreground = shape(value.foreground, 'foreground', ['source', 'idleSeconds', 'signalFile', 'action']);
|
|
148
|
+
check(['input-idle', 'owner-signal'].includes(foreground.source), 'foreground.source must be "input-idle" or "owner-signal"');
|
|
149
|
+
check(['reduce', 'defer'].includes(foreground.action), 'foreground.action must be "reduce" or "defer"');
|
|
150
|
+
policy.foreground = { source: foreground.source, action: foreground.action };
|
|
151
|
+
if (foreground.source === 'input-idle') {
|
|
152
|
+
policy.foreground.idleSeconds = integer(foreground.idleSeconds, 'foreground.idleSeconds', 1, 86400);
|
|
153
|
+
check(foreground.signalFile === undefined, 'foreground.signalFile applies only to "owner-signal"');
|
|
154
|
+
} else {
|
|
155
|
+
check(typeof foreground.signalFile === 'string' && path.isAbsolute(foreground.signalFile), 'foreground.signalFile must be an absolute path');
|
|
156
|
+
check(foreground.idleSeconds === undefined, 'foreground.idleSeconds applies only to "input-idle"');
|
|
157
|
+
policy.foreground.signalFile = foreground.signalFile;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const reducing = policy.temperature?.reduceAboveCelsius !== undefined || policy.power?.reduceAboveWatts !== undefined
|
|
161
|
+
|| policy.foreground?.action === 'reduce';
|
|
162
|
+
if (value.reduced !== undefined) {
|
|
163
|
+
const reduced = shape(value.reduced, 'reduced', ['maxConcurrentAttempts', 'maxSupplyTransfers']);
|
|
164
|
+
policy.reduced = { maxConcurrentAttempts: integer(reduced.maxConcurrentAttempts, 'reduced.maxConcurrentAttempts', 1, 64),
|
|
165
|
+
maxSupplyTransfers: integer(reduced.maxSupplyTransfers, 'reduced.maxSupplyTransfers', 1, 64) };
|
|
166
|
+
}
|
|
167
|
+
check(!reducing || policy.reduced, 'a "reduce" action requires explicit reduced limits');
|
|
168
|
+
return deepFreeze(policy);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function deepFreeze(value) {
|
|
172
|
+
for (const item of Object.values(value)) if (item && typeof item === 'object') deepFreeze(item);
|
|
173
|
+
return Object.freeze(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const nativeOwnerPolicyDigest = policy => sha256Digest(JSON.stringify(policy));
|
|
177
|
+
|
|
178
|
+
/** Reads the owner file once. The path must be absolute and name a regular file. */
|
|
179
|
+
export async function readNativeOwnerPolicy(file, { custody }) {
|
|
180
|
+
check(typeof file === 'string' && path.isAbsolute(file), '--owner-policy requires an absolute path');
|
|
181
|
+
let handle;
|
|
182
|
+
try { handle = await fs.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); }
|
|
183
|
+
catch (error) { fail(`--owner-policy cannot be opened (${error.code || error.message}): ${file}`); }
|
|
184
|
+
let source;
|
|
185
|
+
try {
|
|
186
|
+
check((await handle.stat()).isFile(), '--owner-policy must be a regular file: ' + file);
|
|
187
|
+
source = await handle.readFile('utf8');
|
|
188
|
+
} finally { await handle.close(); }
|
|
189
|
+
let value;
|
|
190
|
+
try { value = JSON.parse(source); } catch { fail('--owner-policy is not valid JSON: ' + file); }
|
|
191
|
+
const policy = validateNativeOwnerPolicy(value, { custody });
|
|
192
|
+
return { policy, digest: nativeOwnerPolicyDigest(policy) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const clocks = new Map();
|
|
196
|
+
const localClock = (timeZone, at) => {
|
|
197
|
+
if (!clocks.has(timeZone)) clocks.set(timeZone, new Intl.DateTimeFormat('en-US', { timeZone, hourCycle: 'h23', weekday: 'short',
|
|
198
|
+
hour: '2-digit', minute: '2-digit' }));
|
|
199
|
+
const parts = Object.fromEntries(clocks.get(timeZone).formatToParts(new Date(at)).map(part => [part.type, part.value]));
|
|
200
|
+
return { day: DAYS.indexOf(parts.weekday.toLowerCase().slice(0, 3)), minute: Number(parts.hour) * 60 + Number(parts.minute) };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** Windows are local wall-clock times in the policy's time zone. A window whose end
|
|
204
|
+
* precedes its start continues into the following day and belongs to its start day. */
|
|
205
|
+
export function scheduleOpen(schedule, at) {
|
|
206
|
+
const { day, minute: now } = localClock(schedule.timeZone, at);
|
|
207
|
+
const previous = (day + 6) % 7;
|
|
208
|
+
return schedule.windows.some(window => {
|
|
209
|
+
const start = minute(window.start, 'start', false), end = minute(window.end, 'end', true);
|
|
210
|
+
const days = window.days.map(name => DAYS.indexOf(name));
|
|
211
|
+
if (start < end) return days.includes(day) && now >= start && now < end;
|
|
212
|
+
return days.includes(day) && now >= start || days.includes(previous) && now < end;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const round = value => String(Math.round(value * 10) / 10);
|
|
217
|
+
const mebibytes = bytes => Math.round(bytes / 1048576) + ' MiB';
|
|
218
|
+
const LABELS = { schedule: 'schedule', processMemory: 'process memory limit', gpuBuffers: 'GPU buffer limit',
|
|
219
|
+
uploadRate: 'upload rate limit', downloadRate: 'download rate limit', temperature: 'temperature limit',
|
|
220
|
+
gpuPower: 'GPU power limit', externalPower: 'external power requirement', inputIdle: 'input-idle foreground detection',
|
|
221
|
+
ownerSignal: 'foreground signal' };
|
|
222
|
+
|
|
223
|
+
/** Pure admission decision from the owner's policy, declared host support and the
|
|
224
|
+
* latest readings. Precedence: reject (unenforceable control) > defer > reduce > admit. */
|
|
225
|
+
export function decideNativeOwnerAdmission({ policy, support, readings, at }) {
|
|
226
|
+
const reasons = [];
|
|
227
|
+
const add = (action, control, message, measured = {}) => reasons.push({ action, control, message: `${VERBS[action]}: ${message}`, ...measured });
|
|
228
|
+
for (const [control, entry] of Object.entries(support.controls)) {
|
|
229
|
+
if (entry.requested && !entry.supported) add('reject', control, `${LABELS[control] || control} unsupported: ${entry.reason}`);
|
|
230
|
+
}
|
|
231
|
+
if (policy.schedule && !scheduleOpen(policy.schedule, at)) add('defer', 'schedule', 'outside schedule');
|
|
232
|
+
const wattLimit = policy.power?.reduceAboveWatts !== undefined || policy.power?.deferAboveWatts !== undefined;
|
|
233
|
+
const sensed = policy.memory?.maxProcessResidentBytes !== undefined || policy.temperature || wattLimit
|
|
234
|
+
|| policy.power?.requireExternalPower || policy.foreground;
|
|
235
|
+
const fresh = readings && at - readings.at <= 3 * policy.sampleIntervalMs;
|
|
236
|
+
if (sensed && !fresh) add('defer', 'readings', 'owner readings unavailable');
|
|
237
|
+
else if (sensed) {
|
|
238
|
+
const memoryLimit = policy.memory?.maxProcessResidentBytes;
|
|
239
|
+
if (memoryLimit !== undefined && readings.processResidentBytes > memoryLimit) {
|
|
240
|
+
add('defer', 'processMemory', `process memory ${mebibytes(readings.processResidentBytes)} > ${mebibytes(memoryLimit)}`,
|
|
241
|
+
{ value: readings.processResidentBytes, limit: memoryLimit, unit: 'bytes' });
|
|
242
|
+
}
|
|
243
|
+
const limited = (control, value, label, unit, suffix, reduce, defer) => {
|
|
244
|
+
if (!support.controls[control]?.supported) return;
|
|
245
|
+
if (value === null || value === undefined) { add('defer', control, label + ' reading unavailable'); return; }
|
|
246
|
+
if (defer !== undefined && value > defer) add('defer', control, `${label} ${round(value)}${suffix} > ${round(defer)}${suffix}`, { value, limit: defer, unit });
|
|
247
|
+
else if (reduce !== undefined && value > reduce) add('reduce', control, `${label} ${round(value)}${suffix} > ${round(reduce)}${suffix}`, { value, limit: reduce, unit });
|
|
248
|
+
};
|
|
249
|
+
if (policy.temperature) limited('temperature', readings.temperature?.maxCelsius, 'temperature', 'celsius', '°C',
|
|
250
|
+
policy.temperature.reduceAboveCelsius, policy.temperature.deferAboveCelsius);
|
|
251
|
+
if (wattLimit) limited('gpuPower', readings.gpuPower?.watts, 'GPU power', 'watts', ' W',
|
|
252
|
+
policy.power.reduceAboveWatts, policy.power.deferAboveWatts);
|
|
253
|
+
if (policy.power?.requireExternalPower && support.controls.externalPower?.supported) {
|
|
254
|
+
if (readings.externalPower === null || readings.externalPower === undefined) add('defer', 'externalPower', 'power source reading unavailable');
|
|
255
|
+
else if (readings.externalPower === false) add('defer', 'externalPower', 'running on battery power');
|
|
256
|
+
}
|
|
257
|
+
const foreground = policy.foreground;
|
|
258
|
+
if (foreground?.source === 'input-idle' && support.controls.inputIdle?.supported) {
|
|
259
|
+
const idle = readings.inputIdleSeconds;
|
|
260
|
+
if (idle === null || idle === undefined) add('defer', 'inputIdle', 'input idle reading unavailable');
|
|
261
|
+
else if (idle < foreground.idleSeconds) add(foreground.action, 'inputIdle',
|
|
262
|
+
`foreground activity (input idle ${Math.floor(idle)} s < ${foreground.idleSeconds} s)`, { value: idle, limit: foreground.idleSeconds, unit: 'seconds' });
|
|
263
|
+
}
|
|
264
|
+
if (foreground?.source === 'owner-signal' && support.controls.ownerSignal?.supported) {
|
|
265
|
+
if (readings.foregroundSignal === null || readings.foregroundSignal === undefined) add('defer', 'ownerSignal', 'foreground signal unreadable');
|
|
266
|
+
else if (readings.foregroundSignal) add(foreground.action, 'ownerSignal', 'foreground signal present');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const action = ['reject', 'defer', 'reduce'].find(name => reasons.some(reason => reason.action === name)) || 'admit';
|
|
270
|
+
return { action, at, message: action === 'admit' ? 'admitted'
|
|
271
|
+
: reasons.filter(reason => reason.action === action).map(reason => reason.message).join('; '),
|
|
272
|
+
reasons, limits: action === 'reduce' ? { ...policy.reduced } : null };
|
|
273
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Bandwidth-rate enforcement for native custody transfers. A token bucket admits
|
|
2
|
+
* bytes in FIFO order at the owner's rate. It starts empty, so a restart never
|
|
3
|
+
* grants a burst, and holds at most one custody chunk, so bytes never run ahead
|
|
4
|
+
* of the rate by more than one chunk. Cumulative REP-5 allowances stay separate. */
|
|
5
|
+
|
|
6
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
7
|
+
|
|
8
|
+
export function createByteRateLimiter({ bytesPerSecond, capacityBytes, now, setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
9
|
+
if (!positive(bytesPerSecond) || !positive(capacityBytes) || typeof now !== 'function') throw new Error('Explicit byte rate and capacity required');
|
|
10
|
+
const queue = [];
|
|
11
|
+
let tokens = 0, updatedAt = now(), timer = null, admittedBytes = 0, waitedMs = 0, closed = false;
|
|
12
|
+
const refill = () => {
|
|
13
|
+
const at = now();
|
|
14
|
+
if (at > updatedAt) tokens = Math.min(capacityBytes, tokens + (at - updatedAt) * bytesPerSecond / 1000);
|
|
15
|
+
updatedAt = Math.max(updatedAt, at);
|
|
16
|
+
};
|
|
17
|
+
function pump() {
|
|
18
|
+
refill();
|
|
19
|
+
while (queue.length && tokens >= queue[0].bytes) {
|
|
20
|
+
const waiter = queue.shift();
|
|
21
|
+
tokens -= waiter.bytes; admittedBytes += waiter.bytes; waitedMs += now() - waiter.queuedAt;
|
|
22
|
+
waiter.signal?.removeEventListener('abort', waiter.abort);
|
|
23
|
+
waiter.resolve();
|
|
24
|
+
}
|
|
25
|
+
if (timer !== null) { clearTimer(timer); timer = null; }
|
|
26
|
+
if (queue.length) {
|
|
27
|
+
const delay = Math.max(1, Math.ceil((queue[0].bytes - tokens) * 1000 / bytesPerSecond));
|
|
28
|
+
timer = setTimer(() => { timer = null; pump(); }, delay);
|
|
29
|
+
timer?.unref?.();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
bytesPerSecond, capacityBytes,
|
|
34
|
+
/** Resolves when `bytes` may move. Cancelling a waiter spends none of the allowance. */
|
|
35
|
+
take(bytes, signal) {
|
|
36
|
+
if (!positive(bytes) || bytes > capacityBytes) return Promise.reject(new Error('Paced transfer exceeds one custody chunk'));
|
|
37
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
38
|
+
if (closed) return Promise.reject(new Error('Owner rate limiter closed'));
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const waiter = { bytes, resolve, reject, signal, queuedAt: now() };
|
|
41
|
+
waiter.abort = () => {
|
|
42
|
+
const index = queue.indexOf(waiter);
|
|
43
|
+
if (index < 0) return;
|
|
44
|
+
queue.splice(index, 1); reject(signal.reason); pump();
|
|
45
|
+
};
|
|
46
|
+
signal?.addEventListener('abort', waiter.abort, { once: true });
|
|
47
|
+
queue.push(waiter); pump();
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
getState: () => ({ bytesPerSecond, capacityBytes, queued: queue.length,
|
|
51
|
+
queuedBytes: queue.reduce((total, waiter) => total + waiter.bytes, 0), admittedBytes, waitedMs }),
|
|
52
|
+
close() {
|
|
53
|
+
closed = true;
|
|
54
|
+
if (timer !== null) { clearTimer(timer); timer = null; }
|
|
55
|
+
for (const waiter of queue.splice(0)) {
|
|
56
|
+
waiter.signal?.removeEventListener('abort', waiter.abort); waiter.reject(new Error('Owner rate limiter closed'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Custody payload bytes per direction: supplied chunks released to the channel
|
|
63
|
+
* after pacing, and requested chunks received in full. */
|
|
64
|
+
export function createTransferMeter() {
|
|
65
|
+
const totals = { suppliedBytes: 0, receivedBytes: 0 };
|
|
66
|
+
return Object.freeze({ sent: bytes => { totals.suppliedBytes += bytes; }, received: bytes => { totals.receivedBytes += bytes; },
|
|
67
|
+
getState: () => ({ ...totals }) });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Wraps the custody channel host port. Supplied chunks wait for upload tokens after
|
|
71
|
+
* the supplier prepares them and before any byte is sent; requested chunks wait for
|
|
72
|
+
* download tokens before the request leaves, so the channel deadline starts afterwards. */
|
|
73
|
+
export function paceCustodyChannel(createChannel, { upload = null, download = null, meter }) {
|
|
74
|
+
return options => {
|
|
75
|
+
const serve = typeof options.serve === 'function' ? async (request, controls) => {
|
|
76
|
+
const response = await options.serve(request, controls);
|
|
77
|
+
const bytes = response?.bytes?.byteLength;
|
|
78
|
+
if (positive(bytes)) {
|
|
79
|
+
await upload?.take(bytes, controls?.signal);
|
|
80
|
+
meter.sent(bytes);
|
|
81
|
+
}
|
|
82
|
+
return response;
|
|
83
|
+
} : options.serve;
|
|
84
|
+
const bus = createChannel({ ...options, serve });
|
|
85
|
+
return { ...bus, async requestChunk(message, controls) {
|
|
86
|
+
await download?.take(message.sizeBytes, controls?.signal);
|
|
87
|
+
const response = await bus.requestChunk(message, controls);
|
|
88
|
+
meter.received(response?.bytes?.byteLength || 0);
|
|
89
|
+
return response;
|
|
90
|
+
} };
|
|
91
|
+
};
|
|
92
|
+
}
|