@serve.zone/interfaces 27.4.0 → 27.5.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/.smartconfig.json +11 -4
- package/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/runtimeassignment.d.ts +120 -0
- package/dist_ts/data/runtimeassignment.js +267 -0
- package/package.json +4 -4
- package/readme.md +35 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimeassignment.ts +402 -0
- /package/{license → license.md} +0 -0
package/.smartconfig.json
CHANGED
|
@@ -20,10 +20,16 @@
|
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"release": {
|
|
23
|
+
"preflight": {
|
|
24
|
+
"test": true,
|
|
25
|
+
"build": true
|
|
26
|
+
},
|
|
23
27
|
"targets": {
|
|
24
28
|
"git": {
|
|
25
29
|
"enabled": true,
|
|
26
|
-
"remote": "origin"
|
|
30
|
+
"remote": "origin",
|
|
31
|
+
"pushBranch": true,
|
|
32
|
+
"pushTags": true
|
|
27
33
|
},
|
|
28
34
|
"npm": {
|
|
29
35
|
"enabled": true,
|
|
@@ -31,7 +37,8 @@
|
|
|
31
37
|
"https://verdaccio.lossless.digital",
|
|
32
38
|
"https://registry.npmjs.org"
|
|
33
39
|
],
|
|
34
|
-
"accessLevel": "public"
|
|
40
|
+
"accessLevel": "public",
|
|
41
|
+
"alreadyPublished": "success"
|
|
35
42
|
},
|
|
36
43
|
"docker": {
|
|
37
44
|
"enabled": false,
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
}
|
|
42
49
|
},
|
|
43
50
|
"@git.zone/tsdoc": {
|
|
44
|
-
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
51
|
+
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license.md](license.md) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
45
52
|
},
|
|
46
53
|
"@ship.zone/szci": {}
|
|
47
|
-
}
|
|
54
|
+
}
|
package/changelog.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-05 - 27.5.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add node-bound runtime assignment contracts (data)
|
|
8
|
+
- Add runtime assignment interfaces, canonical digest helpers, and data namespace exports.
|
|
9
|
+
- Add assignment and observation validators/evaluators for scope binding, CAS progression, replay handling, and sequence checks.
|
|
10
|
+
- Document node-bound runtime assignment semantics and add node test coverage.
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- update release workflow and package metadata (release)
|
|
15
|
+
- Enable test and build preflight checks before releases
|
|
16
|
+
- Configure git releases to push branches and tags
|
|
17
|
+
- Update packaged license file references to license.md
|
|
18
|
+
- Bump dev tooling and package manager versions
|
|
19
|
+
|
|
3
20
|
## 2026-08-29 - 27.4.0
|
|
4
21
|
|
|
5
22
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '27.
|
|
6
|
+
version: '27.5.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
package/dist_ts/data/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './isolatedrestore.js';
|
|
|
21
21
|
export * from './isolatedrestore.golden.js';
|
|
22
22
|
export * from './mail.js';
|
|
23
23
|
export * from './registry.js';
|
|
24
|
+
export * from './runtimeassignment.js';
|
|
24
25
|
export * from './secret.js';
|
|
25
26
|
export * from './baremetal.js';
|
|
26
27
|
export * from './baseos.js';
|
package/dist_ts/data/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export * from './isolatedrestore.js';
|
|
|
21
21
|
export * from './isolatedrestore.golden.js';
|
|
22
22
|
export * from './mail.js';
|
|
23
23
|
export * from './registry.js';
|
|
24
|
+
export * from './runtimeassignment.js';
|
|
24
25
|
export * from './secret.js';
|
|
25
26
|
export * from './baremetal.js';
|
|
26
27
|
export * from './baseos.js';
|
|
@@ -35,4 +36,4 @@ export * from './traffic.js';
|
|
|
35
36
|
export * from './user.js';
|
|
36
37
|
export * from './version.js';
|
|
37
38
|
export * from './webpush.js';
|
|
38
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
39
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGNBQWMsQ0FBQyJ9
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { TSha256Digest, TImmutableContainerPlatform } from './immutableimage.js';
|
|
2
|
+
export interface IRuntimeControllerV1 {
|
|
3
|
+
kind: 'cloudly' | 'onebox';
|
|
4
|
+
id: string;
|
|
5
|
+
/** Persisted authority incarnation; changing it requires explicit re-admission. */
|
|
6
|
+
epoch: string;
|
|
7
|
+
}
|
|
8
|
+
/** Content-bound reference, not a credential, mount path or permission grant. */
|
|
9
|
+
export interface IRuntimeAuthorityReferenceV1 {
|
|
10
|
+
id: string;
|
|
11
|
+
generation: number;
|
|
12
|
+
digest: TSha256Digest;
|
|
13
|
+
}
|
|
14
|
+
export interface IRuntimeAssignmentReferenceV1 {
|
|
15
|
+
assignmentId: string;
|
|
16
|
+
generation: number;
|
|
17
|
+
digest: TSha256Digest;
|
|
18
|
+
}
|
|
19
|
+
/** Derived from authenticated transport and locally configured ownership. */
|
|
20
|
+
export interface IRuntimeAssignmentScopeV1 {
|
|
21
|
+
controller: IRuntimeControllerV1;
|
|
22
|
+
organizationId: string;
|
|
23
|
+
nodeId: string;
|
|
24
|
+
runtimeNamespace: string;
|
|
25
|
+
}
|
|
26
|
+
export type TRuntimeAssignmentDispositionV1 = 'run' | 'stop-preserve' | 'remove-runtime-preserve-storage';
|
|
27
|
+
export interface IRuntimeAssignmentV1 extends IRuntimeAssignmentScopeV1 {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
assignmentId: string;
|
|
30
|
+
serviceId: string;
|
|
31
|
+
/** Stable logical replica; independent of container IDs and execution attempts. */
|
|
32
|
+
replicaId: string;
|
|
33
|
+
/** One execution attempt, immutable for this assignment. */
|
|
34
|
+
attemptId: string;
|
|
35
|
+
/** Logical topology provenance; placement authority remains nodeId. */
|
|
36
|
+
workerGroupId: string | null;
|
|
37
|
+
siteId: string | null;
|
|
38
|
+
generation: number;
|
|
39
|
+
/** Exact CAS predecessor; null only at generation 1. */
|
|
40
|
+
previous: IRuntimeAssignmentReferenceV1 | null;
|
|
41
|
+
disposition: TRuntimeAssignmentDispositionV1;
|
|
42
|
+
/** No automatic relocation or expiry-based claim of a stopped writer. */
|
|
43
|
+
offlinePolicy: 'continue-node-bound';
|
|
44
|
+
workload: {
|
|
45
|
+
registryHost: string;
|
|
46
|
+
repository: string;
|
|
47
|
+
imageDigest: TSha256Digest;
|
|
48
|
+
platform: TImmutableContainerPlatform;
|
|
49
|
+
/** Immutable complete invocation/configuration owned by the controller. */
|
|
50
|
+
config: IRuntimeAuthorityReferenceV1;
|
|
51
|
+
network: IRuntimeAuthorityReferenceV1 | null;
|
|
52
|
+
secrets: IRuntimeAuthorityReferenceV1 | null;
|
|
53
|
+
/** Sorted unique reference IDs. Empty means no persistent storage claims. */
|
|
54
|
+
storage: IRuntimeAuthorityReferenceV1[];
|
|
55
|
+
};
|
|
56
|
+
digest: TSha256Digest;
|
|
57
|
+
}
|
|
58
|
+
export type TRuntimeAssignmentDecisionV1 = {
|
|
59
|
+
status: 'accept' | 'replay';
|
|
60
|
+
assignment: IRuntimeAssignmentV1;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'reject';
|
|
63
|
+
reason: 'invalid' | 'scope-mismatch' | 'conflict' | 'stale' | 'predecessor-mismatch' | 'attempt-mismatch' | 'disposition-regression';
|
|
64
|
+
};
|
|
65
|
+
/** Value-free execution evidence. Receipt/acceptance alone never means ready. */
|
|
66
|
+
export interface IRuntimeAssignmentObservationV1 {
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
assignment: IRuntimeAssignmentReferenceV1;
|
|
69
|
+
/** Authenticated reporter session; its authority is verified by the receiver. */
|
|
70
|
+
reporterSessionId: string;
|
|
71
|
+
/** Durable per-assignment counter; never resets when a reporter reconnects. */
|
|
72
|
+
sequence: number;
|
|
73
|
+
observedAt: number;
|
|
74
|
+
phase: 'pending' | 'running' | 'stopped' | 'removed' | 'failed' | 'unknown';
|
|
75
|
+
ready: boolean;
|
|
76
|
+
runtime: {
|
|
77
|
+
sandboxId: string;
|
|
78
|
+
containerId: string;
|
|
79
|
+
imageDigest: TSha256Digest;
|
|
80
|
+
} | null;
|
|
81
|
+
digest: TSha256Digest;
|
|
82
|
+
}
|
|
83
|
+
export declare const runtimeAssignmentV1Contract: Readonly<{
|
|
84
|
+
readonly assignmentDigestDomain: "serve.zone/runtime-assignment-v1";
|
|
85
|
+
readonly observationDigestDomain: "serve.zone/runtime-assignment-observation-v1";
|
|
86
|
+
readonly maximumStorageReferences: 64;
|
|
87
|
+
readonly omission: "no-effect";
|
|
88
|
+
readonly removal: "retain-assignment-tombstone-and-storage";
|
|
89
|
+
}>;
|
|
90
|
+
/** Digest helpers validate structure and exclude only the object's own digest. */
|
|
91
|
+
export declare const computeRuntimeAssignmentV1Digest: (assignmentArg: IRuntimeAssignmentV1) => Promise<TSha256Digest>;
|
|
92
|
+
export declare const validateRuntimeAssignmentV1: (valueArg: unknown) => Promise<string[]>;
|
|
93
|
+
export declare const getRuntimeAssignmentV1Reference: (assignmentArg: IRuntimeAssignmentV1) => IRuntimeAssignmentReferenceV1;
|
|
94
|
+
/**
|
|
95
|
+
* Pure CAS decision, not authentication or execution. The caller must persist
|
|
96
|
+
* acceptance atomically before effects, serialize replica ownership, and retain
|
|
97
|
+
* removed assignments as tombstones. New attempts require separate stop/fencing
|
|
98
|
+
* admission for every older writer. An omitted assignment causes no transition.
|
|
99
|
+
* Persist only the returned detached snapshot, never the mutable input object.
|
|
100
|
+
*/
|
|
101
|
+
export declare const evaluateRuntimeAssignmentV1: (incomingArg: IRuntimeAssignmentV1, acceptedArg: IRuntimeAssignmentV1 | null, trustedScopeArg: IRuntimeAssignmentScopeV1) => Promise<TRuntimeAssignmentDecisionV1>;
|
|
102
|
+
export declare const computeRuntimeAssignmentObservationV1Digest: (observationArg: IRuntimeAssignmentObservationV1) => Promise<TSha256Digest>;
|
|
103
|
+
/** Checks exact assignment evidence; transport must separately bind reporter/session. */
|
|
104
|
+
export declare const validateRuntimeAssignmentObservationV1: (observationArg: unknown, assignmentArg: IRuntimeAssignmentV1) => Promise<string[]>;
|
|
105
|
+
export type TRuntimeObservationDecisionV1 = {
|
|
106
|
+
status: 'accept' | 'replay';
|
|
107
|
+
observation: IRuntimeAssignmentObservationV1;
|
|
108
|
+
} | {
|
|
109
|
+
status: 'reject';
|
|
110
|
+
reason: 'invalid' | 'session-mismatch' | 'stale' | 'conflict' | 'sequence-gap';
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Receiver must authorize the assignment's node before calling this helper.
|
|
114
|
+
* Compare/persist the returned snapshot atomically with the old receipt. A new
|
|
115
|
+
* session continues the durable counter; it does not reset observation history.
|
|
116
|
+
* Across dispositions, the old receipt must reference the exact predecessor;
|
|
117
|
+
* receivers must process each revision's observations in order.
|
|
118
|
+
* These observations are never sufficient authority to relocate a storage writer.
|
|
119
|
+
*/
|
|
120
|
+
export declare const evaluateRuntimeAssignmentObservationV1: (incomingArg: IRuntimeAssignmentObservationV1, acceptedArg: IRuntimeAssignmentObservationV1 | null, assignmentArg: IRuntimeAssignmentV1, trustedReporterSessionIdArg: string) => Promise<TRuntimeObservationDecisionV1>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { buildDigestPinnedImageReference, isSha256Digest } from './immutableimage.js';
|
|
2
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
3
|
+
export const runtimeAssignmentV1Contract = Object.freeze({
|
|
4
|
+
assignmentDigestDomain: 'serve.zone/runtime-assignment-v1',
|
|
5
|
+
observationDigestDomain: 'serve.zone/runtime-assignment-observation-v1',
|
|
6
|
+
maximumStorageReferences: 64,
|
|
7
|
+
omission: 'no-effect',
|
|
8
|
+
removal: 'retain-assignment-tombstone-and-storage',
|
|
9
|
+
});
|
|
10
|
+
const fail = (messageArg) => { throw new Error(messageArg); };
|
|
11
|
+
const isIdentifier = (valueArg) => typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
12
|
+
const isPositiveInteger = (valueArg) => typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg > 0;
|
|
13
|
+
const isDigest = (valueArg) => typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
14
|
+
/** Reject accessors before reading any fields; do not run input-owned code. */
|
|
15
|
+
const isExactRecord = (valueArg, keysArg) => {
|
|
16
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg))
|
|
17
|
+
return false;
|
|
18
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
19
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
20
|
+
return false;
|
|
21
|
+
const keys = Reflect.ownKeys(valueArg);
|
|
22
|
+
return keys.length === keysArg.length && keys.every((keyArg) => {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, keyArg);
|
|
24
|
+
return typeof keyArg === 'string' && keysArg.includes(keyArg)
|
|
25
|
+
&& descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const isController = (valueArg) => isExactRecord(valueArg, ['kind', 'id', 'epoch'])
|
|
29
|
+
&& (valueArg.kind === 'cloudly' || valueArg.kind === 'onebox')
|
|
30
|
+
&& isIdentifier(valueArg.id) && isIdentifier(valueArg.epoch);
|
|
31
|
+
const isAuthorityReference = (valueArg) => isExactRecord(valueArg, ['id', 'generation', 'digest'])
|
|
32
|
+
&& isIdentifier(valueArg.id) && isPositiveInteger(valueArg.generation) && isDigest(valueArg.digest);
|
|
33
|
+
const isAssignmentReference = (valueArg) => isExactRecord(valueArg, ['assignmentId', 'generation', 'digest'])
|
|
34
|
+
&& isIdentifier(valueArg.assignmentId) && isPositiveInteger(valueArg.generation)
|
|
35
|
+
&& isDigest(valueArg.digest);
|
|
36
|
+
const assignmentKeys = [
|
|
37
|
+
'schemaVersion', 'controller', 'organizationId', 'nodeId', 'runtimeNamespace',
|
|
38
|
+
'assignmentId', 'serviceId', 'replicaId', 'attemptId', 'workerGroupId', 'siteId',
|
|
39
|
+
'generation', 'previous', 'disposition', 'offlinePolicy', 'workload', 'digest',
|
|
40
|
+
];
|
|
41
|
+
const assertAssignment = (valueArg) => {
|
|
42
|
+
if (!isExactRecord(valueArg, assignmentKeys)
|
|
43
|
+
|| valueArg.schemaVersion !== 1 || !isController(valueArg.controller)
|
|
44
|
+
|| !isPositiveInteger(valueArg.generation) || !isDigest(valueArg.digest)
|
|
45
|
+
|| valueArg.offlinePolicy !== 'continue-node-bound'
|
|
46
|
+
|| !['run', 'stop-preserve', 'remove-runtime-preserve-storage'].includes(valueArg.disposition))
|
|
47
|
+
fail('runtime assignment must use its exact v1 schema');
|
|
48
|
+
for (const key of ['organizationId', 'nodeId', 'runtimeNamespace', 'assignmentId',
|
|
49
|
+
'serviceId', 'replicaId', 'attemptId']) {
|
|
50
|
+
if (!isIdentifier(valueArg[key]))
|
|
51
|
+
fail(`runtime assignment ${key} must be a canonical identifier`);
|
|
52
|
+
}
|
|
53
|
+
for (const key of ['workerGroupId', 'siteId']) {
|
|
54
|
+
if (valueArg[key] !== null && !isIdentifier(valueArg[key]))
|
|
55
|
+
fail('invalid runtime topology ID');
|
|
56
|
+
}
|
|
57
|
+
if (valueArg.generation === 1 ? valueArg.previous !== null
|
|
58
|
+
: !isAssignmentReference(valueArg.previous)
|
|
59
|
+
|| valueArg.previous.assignmentId !== valueArg.assignmentId
|
|
60
|
+
|| valueArg.previous.generation !== valueArg.generation - 1) {
|
|
61
|
+
fail('runtime assignment must bind its exact preceding generation');
|
|
62
|
+
}
|
|
63
|
+
const workload = valueArg.workload;
|
|
64
|
+
if (!isExactRecord(workload, ['registryHost', 'repository', 'imageDigest', 'platform',
|
|
65
|
+
'config', 'network', 'secrets', 'storage'])
|
|
66
|
+
|| typeof workload.registryHost !== 'string' || workload.registryHost.length > 253
|
|
67
|
+
|| typeof workload.repository !== 'string' || workload.repository.length > 255
|
|
68
|
+
|| !isDigest(workload.imageDigest)
|
|
69
|
+
|| !['linux/amd64', 'linux/arm64'].includes(workload.platform)
|
|
70
|
+
|| !isAuthorityReference(workload.config)
|
|
71
|
+
|| (workload.network !== null && !isAuthorityReference(workload.network))
|
|
72
|
+
|| (workload.secrets !== null && !isAuthorityReference(workload.secrets))
|
|
73
|
+
|| !Array.isArray(workload.storage)
|
|
74
|
+
|| workload.storage.length > runtimeAssignmentV1Contract.maximumStorageReferences) {
|
|
75
|
+
fail('runtime assignment workload must use exact immutable references');
|
|
76
|
+
}
|
|
77
|
+
buildDigestPinnedImageReference(workload.registryHost, workload.repository, workload.imageDigest);
|
|
78
|
+
let previousId = '';
|
|
79
|
+
for (let index = 0; index < workload.storage.length; index++) {
|
|
80
|
+
const descriptor = Object.getOwnPropertyDescriptor(workload.storage, String(index));
|
|
81
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')
|
|
82
|
+
|| !isAuthorityReference(descriptor.value) || descriptor.value.id <= previousId) {
|
|
83
|
+
fail('runtime assignment storage references must be dense, sorted and unique');
|
|
84
|
+
}
|
|
85
|
+
previousId = descriptor.value.id;
|
|
86
|
+
}
|
|
87
|
+
canonicalizeStrictJson(valueArg, fail);
|
|
88
|
+
return valueArg;
|
|
89
|
+
};
|
|
90
|
+
const digestPayload = async (domainArg, valueArg) => {
|
|
91
|
+
const { digest: _digest, ...payload } = valueArg;
|
|
92
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({ domain: domainArg, payload }, fail), fail)}`;
|
|
93
|
+
};
|
|
94
|
+
/** Digest helpers validate structure and exclude only the object's own digest. */
|
|
95
|
+
export const computeRuntimeAssignmentV1Digest = async (assignmentArg) => digestPayload(runtimeAssignmentV1Contract.assignmentDigestDomain, assertAssignment(assignmentArg));
|
|
96
|
+
export const validateRuntimeAssignmentV1 = async (valueArg) => {
|
|
97
|
+
try {
|
|
98
|
+
const assignment = snapshotAssignment(valueArg);
|
|
99
|
+
return assignment.digest === await computeRuntimeAssignmentV1Digest(assignment)
|
|
100
|
+
? [] : ['runtime assignment digest does not match'];
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return ['runtime assignment must use its exact canonical v1 schema'];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const snapshotAssignment = (valueArg) => JSON.parse(canonicalizeStrictJson(assertAssignment(valueArg), fail));
|
|
107
|
+
export const getRuntimeAssignmentV1Reference = (assignmentArg) => ({
|
|
108
|
+
assignmentId: assignmentArg.assignmentId,
|
|
109
|
+
generation: assignmentArg.generation,
|
|
110
|
+
digest: assignmentArg.digest,
|
|
111
|
+
});
|
|
112
|
+
const canonical = (valueArg) => canonicalizeStrictJson(valueArg, fail);
|
|
113
|
+
const immutableAttempt = (assignmentArg) => {
|
|
114
|
+
const { generation, previous, disposition, digest, ...attempt } = assignmentArg;
|
|
115
|
+
return canonical(attempt);
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Pure CAS decision, not authentication or execution. The caller must persist
|
|
119
|
+
* acceptance atomically before effects, serialize replica ownership, and retain
|
|
120
|
+
* removed assignments as tombstones. New attempts require separate stop/fencing
|
|
121
|
+
* admission for every older writer. An omitted assignment causes no transition.
|
|
122
|
+
* Persist only the returned detached snapshot, never the mutable input object.
|
|
123
|
+
*/
|
|
124
|
+
export const evaluateRuntimeAssignmentV1 = async (incomingArg, acceptedArg, trustedScopeArg) => {
|
|
125
|
+
try {
|
|
126
|
+
incomingArg = snapshotAssignment(incomingArg);
|
|
127
|
+
acceptedArg = acceptedArg === null ? null : snapshotAssignment(acceptedArg);
|
|
128
|
+
if (!isExactRecord(trustedScopeArg, ['controller', 'organizationId', 'nodeId', 'runtimeNamespace'])
|
|
129
|
+
|| !isController(trustedScopeArg.controller)
|
|
130
|
+
|| !isIdentifier(trustedScopeArg.organizationId) || !isIdentifier(trustedScopeArg.nodeId)
|
|
131
|
+
|| !isIdentifier(trustedScopeArg.runtimeNamespace))
|
|
132
|
+
return { status: 'reject', reason: 'invalid' };
|
|
133
|
+
trustedScopeArg = JSON.parse(canonical(trustedScopeArg));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { status: 'reject', reason: 'invalid' };
|
|
137
|
+
}
|
|
138
|
+
if ((await validateRuntimeAssignmentV1(incomingArg)).length
|
|
139
|
+
|| (acceptedArg !== null && (await validateRuntimeAssignmentV1(acceptedArg)).length)) {
|
|
140
|
+
return { status: 'reject', reason: 'invalid' };
|
|
141
|
+
}
|
|
142
|
+
const matchesScope = (assignmentArg) => assignmentArg.controller.kind === trustedScopeArg.controller.kind
|
|
143
|
+
&& assignmentArg.controller.id === trustedScopeArg.controller.id
|
|
144
|
+
&& assignmentArg.controller.epoch === trustedScopeArg.controller.epoch
|
|
145
|
+
&& assignmentArg.organizationId === trustedScopeArg.organizationId
|
|
146
|
+
&& assignmentArg.nodeId === trustedScopeArg.nodeId
|
|
147
|
+
&& assignmentArg.runtimeNamespace === trustedScopeArg.runtimeNamespace;
|
|
148
|
+
if (!matchesScope(incomingArg) || (acceptedArg !== null && !matchesScope(acceptedArg))) {
|
|
149
|
+
return { status: 'reject', reason: 'scope-mismatch' };
|
|
150
|
+
}
|
|
151
|
+
if (acceptedArg === null) {
|
|
152
|
+
return incomingArg.generation === 1
|
|
153
|
+
&& incomingArg.disposition === 'run'
|
|
154
|
+
? { status: 'accept', assignment: incomingArg }
|
|
155
|
+
: { status: 'reject', reason: 'predecessor-mismatch' };
|
|
156
|
+
}
|
|
157
|
+
if (incomingArg.assignmentId !== acceptedArg.assignmentId) {
|
|
158
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
159
|
+
}
|
|
160
|
+
if (incomingArg.generation < acceptedArg.generation)
|
|
161
|
+
return { status: 'reject', reason: 'stale' };
|
|
162
|
+
if (incomingArg.generation === acceptedArg.generation) {
|
|
163
|
+
return incomingArg.digest === acceptedArg.digest
|
|
164
|
+
? { status: 'replay', assignment: acceptedArg } : { status: 'reject', reason: 'conflict' };
|
|
165
|
+
}
|
|
166
|
+
if (canonical(incomingArg.previous) !== canonical(getRuntimeAssignmentV1Reference(acceptedArg))) {
|
|
167
|
+
return { status: 'reject', reason: 'predecessor-mismatch' };
|
|
168
|
+
}
|
|
169
|
+
if (immutableAttempt(incomingArg) !== immutableAttempt(acceptedArg)) {
|
|
170
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
171
|
+
}
|
|
172
|
+
const order = {
|
|
173
|
+
run: 0, 'stop-preserve': 1, 'remove-runtime-preserve-storage': 2,
|
|
174
|
+
};
|
|
175
|
+
if (order[incomingArg.disposition] !== order[acceptedArg.disposition] + 1) {
|
|
176
|
+
return { status: 'reject', reason: 'disposition-regression' };
|
|
177
|
+
}
|
|
178
|
+
return { status: 'accept', assignment: incomingArg };
|
|
179
|
+
};
|
|
180
|
+
const assertObservation = (valueArg) => {
|
|
181
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'assignment', 'reporterSessionId',
|
|
182
|
+
'sequence', 'observedAt', 'phase', 'ready', 'runtime', 'digest'])
|
|
183
|
+
|| valueArg.schemaVersion !== 1 || !isAssignmentReference(valueArg.assignment)
|
|
184
|
+
|| !isIdentifier(valueArg.reporterSessionId) || !isPositiveInteger(valueArg.sequence)
|
|
185
|
+
|| !isPositiveInteger(valueArg.observedAt) || !isDigest(valueArg.digest)
|
|
186
|
+
|| typeof valueArg.ready !== 'boolean'
|
|
187
|
+
|| !['pending', 'running', 'stopped', 'removed', 'failed', 'unknown'].includes(valueArg.phase)) {
|
|
188
|
+
fail('runtime observation must use its exact v1 schema');
|
|
189
|
+
}
|
|
190
|
+
if (valueArg.runtime !== null && (!isExactRecord(valueArg.runtime, [
|
|
191
|
+
'sandboxId', 'containerId', 'imageDigest',
|
|
192
|
+
]) || !isIdentifier(valueArg.runtime.sandboxId) || !isIdentifier(valueArg.runtime.containerId)
|
|
193
|
+
|| !isDigest(valueArg.runtime.imageDigest)))
|
|
194
|
+
fail('runtime identity must be canonical');
|
|
195
|
+
if ((valueArg.ready && valueArg.phase !== 'running')
|
|
196
|
+
|| (valueArg.phase === 'running' && valueArg.runtime === null)
|
|
197
|
+
|| (valueArg.phase === 'removed' && valueArg.runtime !== null)) {
|
|
198
|
+
fail('runtime observation readiness and identity contradict its phase');
|
|
199
|
+
}
|
|
200
|
+
canonical(valueArg);
|
|
201
|
+
return valueArg;
|
|
202
|
+
};
|
|
203
|
+
export const computeRuntimeAssignmentObservationV1Digest = async (observationArg) => digestPayload(runtimeAssignmentV1Contract.observationDigestDomain, assertObservation(observationArg));
|
|
204
|
+
/** Checks exact assignment evidence; transport must separately bind reporter/session. */
|
|
205
|
+
export const validateRuntimeAssignmentObservationV1 = async (observationArg, assignmentArg) => {
|
|
206
|
+
try {
|
|
207
|
+
const observation = JSON.parse(canonical(assertObservation(observationArg)));
|
|
208
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
209
|
+
if ((await validateRuntimeAssignmentV1(assignmentArg)).length)
|
|
210
|
+
return ['invalid assignment'];
|
|
211
|
+
if (canonical(observation.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))) {
|
|
212
|
+
return ['runtime observation does not match the exact assignment'];
|
|
213
|
+
}
|
|
214
|
+
if (observation.digest !== await computeRuntimeAssignmentObservationV1Digest(observation)) {
|
|
215
|
+
return ['runtime observation digest does not match'];
|
|
216
|
+
}
|
|
217
|
+
if (observation.ready && (assignmentArg.disposition !== 'run'
|
|
218
|
+
|| observation.runtime?.imageDigest !== assignmentArg.workload.imageDigest)) {
|
|
219
|
+
return ['runtime readiness does not match authorized execution'];
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return ['runtime observation must use its exact canonical v1 schema'];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Receiver must authorize the assignment's node before calling this helper.
|
|
229
|
+
* Compare/persist the returned snapshot atomically with the old receipt. A new
|
|
230
|
+
* session continues the durable counter; it does not reset observation history.
|
|
231
|
+
* Across dispositions, the old receipt must reference the exact predecessor;
|
|
232
|
+
* receivers must process each revision's observations in order.
|
|
233
|
+
* These observations are never sufficient authority to relocate a storage writer.
|
|
234
|
+
*/
|
|
235
|
+
export const evaluateRuntimeAssignmentObservationV1 = async (incomingArg, acceptedArg, assignmentArg, trustedReporterSessionIdArg) => {
|
|
236
|
+
try {
|
|
237
|
+
incomingArg = JSON.parse(canonical(assertObservation(incomingArg)));
|
|
238
|
+
acceptedArg = acceptedArg === null ? null
|
|
239
|
+
: JSON.parse(canonical(assertObservation(acceptedArg)));
|
|
240
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
241
|
+
if (!isIdentifier(trustedReporterSessionIdArg)
|
|
242
|
+
|| (await validateRuntimeAssignmentObservationV1(incomingArg, assignmentArg)).length
|
|
243
|
+
|| (acceptedArg !== null && (acceptedArg.digest !== await computeRuntimeAssignmentObservationV1Digest(acceptedArg)
|
|
244
|
+
|| acceptedArg.assignment.assignmentId !== assignmentArg.assignmentId
|
|
245
|
+
|| (canonical(acceptedArg.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))
|
|
246
|
+
&& canonical(acceptedArg.assignment) !== canonical(assignmentArg.previous)))))
|
|
247
|
+
return { status: 'reject', reason: 'invalid' };
|
|
248
|
+
if (incomingArg.reporterSessionId !== trustedReporterSessionIdArg) {
|
|
249
|
+
return { status: 'reject', reason: 'session-mismatch' };
|
|
250
|
+
}
|
|
251
|
+
if (acceptedArg !== null && incomingArg.sequence <= acceptedArg.sequence) {
|
|
252
|
+
if (incomingArg.sequence < acceptedArg.sequence)
|
|
253
|
+
return { status: 'reject', reason: 'stale' };
|
|
254
|
+
return incomingArg.digest === acceptedArg.digest
|
|
255
|
+
? { status: 'replay', observation: acceptedArg }
|
|
256
|
+
: { status: 'reject', reason: 'conflict' };
|
|
257
|
+
}
|
|
258
|
+
if (incomingArg.sequence !== (acceptedArg?.sequence ?? 0) + 1) {
|
|
259
|
+
return { status: 'reject', reason: 'sequence-gap' };
|
|
260
|
+
}
|
|
261
|
+
return { status: 'accept', observation: incomingArg };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { status: 'reject', reason: 'invalid' };
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVudGltZWFzc2lnbm1lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL3J1bnRpbWVhc3NpZ25tZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSwrQkFBK0IsRUFBRSxjQUFjLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUN0RixPQUFPLEVBQUUsc0JBQXNCLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQTJGbkcsTUFBTSxDQUFDLE1BQU0sMkJBQTJCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN2RCxzQkFBc0IsRUFBRSxrQ0FBa0M7SUFDMUQsdUJBQXVCLEVBQUUsOENBQThDO0lBQ3ZFLHdCQUF3QixFQUFFLEVBQUU7SUFDNUIsUUFBUSxFQUFFLFdBQVc7SUFDckIsT0FBTyxFQUFFLHlDQUF5QztDQUMxQyxDQUFDLENBQUM7QUFFWixNQUFNLElBQUksR0FBa0MsQ0FBQyxVQUFVLEVBQUUsRUFBRSxHQUFHLE1BQU0sSUFBSSxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDN0YsTUFBTSxZQUFZLEdBQUcsQ0FBQyxRQUFpQixFQUFzQixFQUFFLENBQzdELE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxxQ0FBcUMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDdkYsTUFBTSxpQkFBaUIsR0FBRyxDQUFDLFFBQWlCLEVBQXNCLEVBQUUsQ0FDbEUsT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLElBQUksUUFBUSxHQUFHLENBQUMsQ0FBQztBQUNqRixNQUFNLFFBQVEsR0FBRyxDQUFDLFFBQWlCLEVBQTZCLEVBQUUsQ0FDaEUsT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLGNBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUUzRCwrRUFBK0U7QUFDL0UsTUFBTSxhQUFhLEdBQUcs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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/interfaces",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
|
|
6
6
|
"exports": {
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@git.zone/tsbuild": "^4.4.2",
|
|
31
|
-
"@git.zone/tsdoc": "^2.
|
|
31
|
+
"@git.zone/tsdoc": "^2.2.0",
|
|
32
32
|
"@git.zone/tsrun": "^2.0.6",
|
|
33
33
|
"@git.zone/tstest": "^4.0.0",
|
|
34
|
-
"@types/node": "^26.
|
|
34
|
+
"@types/node": "^26.4.1"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"ts/**/*",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
".smartconfig.json",
|
|
42
42
|
"readme.md",
|
|
43
43
|
"changelog.md",
|
|
44
|
-
"license"
|
|
44
|
+
"license.md"
|
|
45
45
|
],
|
|
46
46
|
"repository": {
|
|
47
47
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -47,6 +47,40 @@ validator; unlike `@serve.zone/interfaces/runtime`, it does not load the general
|
|
|
47
47
|
runtime graph, `plugins.js`, SmartCrypto, TypedRequest, Corestore, or settlement
|
|
48
48
|
runtime modules.
|
|
49
49
|
|
|
50
|
+
### Node-Bound Runtime Assignments
|
|
51
|
+
|
|
52
|
+
The `data` namespace exports `IRuntimeAssignmentV1`, canonical digest helpers,
|
|
53
|
+
`evaluateRuntimeAssignmentV1`, and assignment observation validators/evaluators
|
|
54
|
+
for Cloudly/Onebox controllers and Pallet nodes. These are shared contracts;
|
|
55
|
+
they do not install a runtime, schedule containers or expose an RPC endpoint.
|
|
56
|
+
|
|
57
|
+
Each assignment binds a controller incarnation, organization, stable node and
|
|
58
|
+
replica IDs, one immutable execution attempt, image/configuration and opaque
|
|
59
|
+
network/storage/secret references. Group and site IDs are nullable topology
|
|
60
|
+
provenance. All fields except the digest itself participate in a domain-separated
|
|
61
|
+
SHA-256 digest. Digests provide integrity and compare-and-swap identity;
|
|
62
|
+
authentication must separately establish the trusted controller/node scope.
|
|
63
|
+
|
|
64
|
+
An attempt starts at generation one with `run`, then advances through
|
|
65
|
+
`stop-preserve` and `remove-runtime-preserve-storage` using exact predecessor
|
|
66
|
+
generation/digest references. Exact retries return `replay`; conflicts, stale
|
|
67
|
+
revisions, gaps, changed execution specifications and resurrection are rejected.
|
|
68
|
+
Consumers must atomically persist the evaluator's detached snapshot before effects,
|
|
69
|
+
retain removal tombstones and storage, and serialize ownership of each replica.
|
|
70
|
+
Omission causes no change. A new attempt requires separate admission proving any
|
|
71
|
+
older exclusive writer stopped or fenced. The supported offline policy is
|
|
72
|
+
`continue-node-bound`; no heartbeat timeout or lease expiry proves a writer absent.
|
|
73
|
+
|
|
74
|
+
Observations bind the exact assignment, authenticated reporter session, durable
|
|
75
|
+
per-assignment sequence and digest. Reconnect does not reset the counter. Observation
|
|
76
|
+
history must reference the same assignment revision or its exact predecessor;
|
|
77
|
+
receivers process each disposition's observations in order. Session
|
|
78
|
+
authorization, clock/freshness policy and durable receipt persistence belong to
|
|
79
|
+
the receiving service. Readiness requires matching running image evidence and
|
|
80
|
+
`run` intent; assignment acceptance alone is never readiness. A valid observation
|
|
81
|
+
can describe drift (such as a removed runtime while intent remains `run`), and
|
|
82
|
+
does not authorize storage relocation or prove fencing.
|
|
83
|
+
|
|
50
84
|
### Offline SmartDB Settlement And Packaging
|
|
51
85
|
|
|
52
86
|
The Node-only `@serve.zone/interfaces/runtime` export includes isolated V1
|
|
@@ -1108,7 +1142,7 @@ The package is authored as ESM TypeScript and built with `tsbuild tsfolders`.
|
|
|
1108
1142
|
|
|
1109
1143
|
## License and Legal Information
|
|
1110
1144
|
|
|
1111
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
|
1145
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
|
|
1112
1146
|
|
|
1113
1147
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
1114
1148
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './isolatedrestore.js';
|
|
|
21
21
|
export * from './isolatedrestore.golden.js';
|
|
22
22
|
export * from './mail.js';
|
|
23
23
|
export * from './registry.js';
|
|
24
|
+
export * from './runtimeassignment.js';
|
|
24
25
|
export * from './secret.js';
|
|
25
26
|
export * from './baremetal.js';
|
|
26
27
|
export * from './baseos.js';
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { TSha256Digest, TImmutableContainerPlatform } from './immutableimage.js';
|
|
2
|
+
import { buildDigestPinnedImageReference, isSha256Digest } from './immutableimage.js';
|
|
3
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
4
|
+
|
|
5
|
+
export interface IRuntimeControllerV1 {
|
|
6
|
+
kind: 'cloudly' | 'onebox';
|
|
7
|
+
id: string;
|
|
8
|
+
/** Persisted authority incarnation; changing it requires explicit re-admission. */
|
|
9
|
+
epoch: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Content-bound reference, not a credential, mount path or permission grant. */
|
|
13
|
+
export interface IRuntimeAuthorityReferenceV1 {
|
|
14
|
+
id: string;
|
|
15
|
+
generation: number;
|
|
16
|
+
digest: TSha256Digest;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IRuntimeAssignmentReferenceV1 {
|
|
20
|
+
assignmentId: string;
|
|
21
|
+
generation: number;
|
|
22
|
+
digest: TSha256Digest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Derived from authenticated transport and locally configured ownership. */
|
|
26
|
+
export interface IRuntimeAssignmentScopeV1 {
|
|
27
|
+
controller: IRuntimeControllerV1;
|
|
28
|
+
organizationId: string;
|
|
29
|
+
nodeId: string;
|
|
30
|
+
runtimeNamespace: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type TRuntimeAssignmentDispositionV1 =
|
|
34
|
+
| 'run'
|
|
35
|
+
| 'stop-preserve'
|
|
36
|
+
| 'remove-runtime-preserve-storage';
|
|
37
|
+
|
|
38
|
+
export interface IRuntimeAssignmentV1 extends IRuntimeAssignmentScopeV1 {
|
|
39
|
+
schemaVersion: 1;
|
|
40
|
+
assignmentId: string;
|
|
41
|
+
serviceId: string;
|
|
42
|
+
/** Stable logical replica; independent of container IDs and execution attempts. */
|
|
43
|
+
replicaId: string;
|
|
44
|
+
/** One execution attempt, immutable for this assignment. */
|
|
45
|
+
attemptId: string;
|
|
46
|
+
/** Logical topology provenance; placement authority remains nodeId. */
|
|
47
|
+
workerGroupId: string | null;
|
|
48
|
+
siteId: string | null;
|
|
49
|
+
generation: number;
|
|
50
|
+
/** Exact CAS predecessor; null only at generation 1. */
|
|
51
|
+
previous: IRuntimeAssignmentReferenceV1 | null;
|
|
52
|
+
disposition: TRuntimeAssignmentDispositionV1;
|
|
53
|
+
/** No automatic relocation or expiry-based claim of a stopped writer. */
|
|
54
|
+
offlinePolicy: 'continue-node-bound';
|
|
55
|
+
workload: {
|
|
56
|
+
registryHost: string;
|
|
57
|
+
repository: string;
|
|
58
|
+
imageDigest: TSha256Digest;
|
|
59
|
+
platform: TImmutableContainerPlatform;
|
|
60
|
+
/** Immutable complete invocation/configuration owned by the controller. */
|
|
61
|
+
config: IRuntimeAuthorityReferenceV1;
|
|
62
|
+
network: IRuntimeAuthorityReferenceV1 | null;
|
|
63
|
+
secrets: IRuntimeAuthorityReferenceV1 | null;
|
|
64
|
+
/** Sorted unique reference IDs. Empty means no persistent storage claims. */
|
|
65
|
+
storage: IRuntimeAuthorityReferenceV1[];
|
|
66
|
+
};
|
|
67
|
+
digest: TSha256Digest;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type TRuntimeAssignmentDecisionV1 =
|
|
71
|
+
| { status: 'accept' | 'replay'; assignment: IRuntimeAssignmentV1 }
|
|
72
|
+
| { status: 'reject'; reason: 'invalid' | 'scope-mismatch' | 'conflict' | 'stale'
|
|
73
|
+
| 'predecessor-mismatch' | 'attempt-mismatch' | 'disposition-regression' };
|
|
74
|
+
|
|
75
|
+
/** Value-free execution evidence. Receipt/acceptance alone never means ready. */
|
|
76
|
+
export interface IRuntimeAssignmentObservationV1 {
|
|
77
|
+
schemaVersion: 1;
|
|
78
|
+
assignment: IRuntimeAssignmentReferenceV1;
|
|
79
|
+
/** Authenticated reporter session; its authority is verified by the receiver. */
|
|
80
|
+
reporterSessionId: string;
|
|
81
|
+
/** Durable per-assignment counter; never resets when a reporter reconnects. */
|
|
82
|
+
sequence: number;
|
|
83
|
+
observedAt: number;
|
|
84
|
+
phase: 'pending' | 'running' | 'stopped' | 'removed' | 'failed' | 'unknown';
|
|
85
|
+
ready: boolean;
|
|
86
|
+
runtime: {
|
|
87
|
+
sandboxId: string;
|
|
88
|
+
containerId: string;
|
|
89
|
+
imageDigest: TSha256Digest;
|
|
90
|
+
} | null;
|
|
91
|
+
digest: TSha256Digest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const runtimeAssignmentV1Contract = Object.freeze({
|
|
95
|
+
assignmentDigestDomain: 'serve.zone/runtime-assignment-v1',
|
|
96
|
+
observationDigestDomain: 'serve.zone/runtime-assignment-observation-v1',
|
|
97
|
+
maximumStorageReferences: 64,
|
|
98
|
+
omission: 'no-effect',
|
|
99
|
+
removal: 'retain-assignment-tombstone-and-storage',
|
|
100
|
+
} as const);
|
|
101
|
+
|
|
102
|
+
const fail: (messageArg: string) => never = (messageArg) => { throw new Error(messageArg); };
|
|
103
|
+
const isIdentifier = (valueArg: unknown): valueArg is string =>
|
|
104
|
+
typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
105
|
+
const isPositiveInteger = (valueArg: unknown): valueArg is number =>
|
|
106
|
+
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg > 0;
|
|
107
|
+
const isDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
108
|
+
typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
109
|
+
|
|
110
|
+
/** Reject accessors before reading any fields; do not run input-owned code. */
|
|
111
|
+
const isExactRecord = (
|
|
112
|
+
valueArg: unknown,
|
|
113
|
+
keysArg: readonly string[],
|
|
114
|
+
): valueArg is Record<string, unknown> => {
|
|
115
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
|
|
116
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
117
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
118
|
+
const keys = Reflect.ownKeys(valueArg);
|
|
119
|
+
return keys.length === keysArg.length && keys.every((keyArg) => {
|
|
120
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, keyArg);
|
|
121
|
+
return typeof keyArg === 'string' && keysArg.includes(keyArg)
|
|
122
|
+
&& descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const isController = (valueArg: unknown): valueArg is IRuntimeControllerV1 =>
|
|
127
|
+
isExactRecord(valueArg, ['kind', 'id', 'epoch'])
|
|
128
|
+
&& (valueArg.kind === 'cloudly' || valueArg.kind === 'onebox')
|
|
129
|
+
&& isIdentifier(valueArg.id) && isIdentifier(valueArg.epoch);
|
|
130
|
+
const isAuthorityReference = (valueArg: unknown): valueArg is IRuntimeAuthorityReferenceV1 =>
|
|
131
|
+
isExactRecord(valueArg, ['id', 'generation', 'digest'])
|
|
132
|
+
&& isIdentifier(valueArg.id) && isPositiveInteger(valueArg.generation) && isDigest(valueArg.digest);
|
|
133
|
+
const isAssignmentReference = (valueArg: unknown): valueArg is IRuntimeAssignmentReferenceV1 =>
|
|
134
|
+
isExactRecord(valueArg, ['assignmentId', 'generation', 'digest'])
|
|
135
|
+
&& isIdentifier(valueArg.assignmentId) && isPositiveInteger(valueArg.generation)
|
|
136
|
+
&& isDigest(valueArg.digest);
|
|
137
|
+
|
|
138
|
+
const assignmentKeys = [
|
|
139
|
+
'schemaVersion', 'controller', 'organizationId', 'nodeId', 'runtimeNamespace',
|
|
140
|
+
'assignmentId', 'serviceId', 'replicaId', 'attemptId', 'workerGroupId', 'siteId',
|
|
141
|
+
'generation', 'previous', 'disposition', 'offlinePolicy', 'workload', 'digest',
|
|
142
|
+
] as const;
|
|
143
|
+
|
|
144
|
+
const assertAssignment = (valueArg: unknown): IRuntimeAssignmentV1 => {
|
|
145
|
+
if (!isExactRecord(valueArg, assignmentKeys)
|
|
146
|
+
|| valueArg.schemaVersion !== 1 || !isController(valueArg.controller)
|
|
147
|
+
|| !isPositiveInteger(valueArg.generation) || !isDigest(valueArg.digest)
|
|
148
|
+
|| valueArg.offlinePolicy !== 'continue-node-bound'
|
|
149
|
+
|| !['run', 'stop-preserve', 'remove-runtime-preserve-storage'].includes(
|
|
150
|
+
valueArg.disposition as string,
|
|
151
|
+
)) fail('runtime assignment must use its exact v1 schema');
|
|
152
|
+
for (const key of ['organizationId', 'nodeId', 'runtimeNamespace', 'assignmentId',
|
|
153
|
+
'serviceId', 'replicaId', 'attemptId']) {
|
|
154
|
+
if (!isIdentifier(valueArg[key])) fail(`runtime assignment ${key} must be a canonical identifier`);
|
|
155
|
+
}
|
|
156
|
+
for (const key of ['workerGroupId', 'siteId']) {
|
|
157
|
+
if (valueArg[key] !== null && !isIdentifier(valueArg[key])) fail('invalid runtime topology ID');
|
|
158
|
+
}
|
|
159
|
+
if (valueArg.generation === 1 ? valueArg.previous !== null
|
|
160
|
+
: !isAssignmentReference(valueArg.previous)
|
|
161
|
+
|| valueArg.previous.assignmentId !== valueArg.assignmentId
|
|
162
|
+
|| valueArg.previous.generation !== (valueArg.generation as number) - 1) {
|
|
163
|
+
fail('runtime assignment must bind its exact preceding generation');
|
|
164
|
+
}
|
|
165
|
+
const workload = valueArg.workload;
|
|
166
|
+
if (!isExactRecord(workload, ['registryHost', 'repository', 'imageDigest', 'platform',
|
|
167
|
+
'config', 'network', 'secrets', 'storage'])
|
|
168
|
+
|| typeof workload.registryHost !== 'string' || workload.registryHost.length > 253
|
|
169
|
+
|| typeof workload.repository !== 'string' || workload.repository.length > 255
|
|
170
|
+
|| !isDigest(workload.imageDigest)
|
|
171
|
+
|| !['linux/amd64', 'linux/arm64'].includes(workload.platform as string)
|
|
172
|
+
|| !isAuthorityReference(workload.config)
|
|
173
|
+
|| (workload.network !== null && !isAuthorityReference(workload.network))
|
|
174
|
+
|| (workload.secrets !== null && !isAuthorityReference(workload.secrets))
|
|
175
|
+
|| !Array.isArray(workload.storage)
|
|
176
|
+
|| workload.storage.length > runtimeAssignmentV1Contract.maximumStorageReferences) {
|
|
177
|
+
fail('runtime assignment workload must use exact immutable references');
|
|
178
|
+
}
|
|
179
|
+
buildDigestPinnedImageReference(workload.registryHost as string,
|
|
180
|
+
workload.repository as string, workload.imageDigest as TSha256Digest);
|
|
181
|
+
let previousId = '';
|
|
182
|
+
for (let index = 0; index < (workload.storage as unknown[]).length; index++) {
|
|
183
|
+
const descriptor = Object.getOwnPropertyDescriptor(workload.storage, String(index));
|
|
184
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')
|
|
185
|
+
|| !isAuthorityReference(descriptor.value) || descriptor.value.id <= previousId) {
|
|
186
|
+
fail('runtime assignment storage references must be dense, sorted and unique');
|
|
187
|
+
}
|
|
188
|
+
previousId = descriptor.value.id;
|
|
189
|
+
}
|
|
190
|
+
canonicalizeStrictJson(valueArg, fail);
|
|
191
|
+
return valueArg as unknown as IRuntimeAssignmentV1;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const digestPayload = async (domainArg: string, valueArg: { digest: TSha256Digest }): Promise<TSha256Digest> => {
|
|
195
|
+
const { digest: _digest, ...payload } = valueArg;
|
|
196
|
+
return `sha256:${await createCanonicalJsonSha256Hex(
|
|
197
|
+
canonicalizeStrictJson({ domain: domainArg, payload }, fail), fail,
|
|
198
|
+
)}`;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** Digest helpers validate structure and exclude only the object's own digest. */
|
|
202
|
+
export const computeRuntimeAssignmentV1Digest = async (
|
|
203
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
204
|
+
): Promise<TSha256Digest> => digestPayload(
|
|
205
|
+
runtimeAssignmentV1Contract.assignmentDigestDomain, assertAssignment(assignmentArg),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const validateRuntimeAssignmentV1 = async (valueArg: unknown): Promise<string[]> => {
|
|
209
|
+
try {
|
|
210
|
+
const assignment = snapshotAssignment(valueArg);
|
|
211
|
+
return assignment.digest === await computeRuntimeAssignmentV1Digest(assignment)
|
|
212
|
+
? [] : ['runtime assignment digest does not match'];
|
|
213
|
+
} catch {
|
|
214
|
+
return ['runtime assignment must use its exact canonical v1 schema'];
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const snapshotAssignment = (valueArg: unknown): IRuntimeAssignmentV1 =>
|
|
219
|
+
JSON.parse(canonicalizeStrictJson(assertAssignment(valueArg), fail)) as IRuntimeAssignmentV1;
|
|
220
|
+
|
|
221
|
+
export const getRuntimeAssignmentV1Reference = (
|
|
222
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
223
|
+
): IRuntimeAssignmentReferenceV1 => ({
|
|
224
|
+
assignmentId: assignmentArg.assignmentId,
|
|
225
|
+
generation: assignmentArg.generation,
|
|
226
|
+
digest: assignmentArg.digest,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const canonical = (valueArg: unknown): string => canonicalizeStrictJson(valueArg, fail);
|
|
230
|
+
const immutableAttempt = (assignmentArg: IRuntimeAssignmentV1): string => {
|
|
231
|
+
const { generation, previous, disposition, digest, ...attempt } = assignmentArg;
|
|
232
|
+
return canonical(attempt);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Pure CAS decision, not authentication or execution. The caller must persist
|
|
237
|
+
* acceptance atomically before effects, serialize replica ownership, and retain
|
|
238
|
+
* removed assignments as tombstones. New attempts require separate stop/fencing
|
|
239
|
+
* admission for every older writer. An omitted assignment causes no transition.
|
|
240
|
+
* Persist only the returned detached snapshot, never the mutable input object.
|
|
241
|
+
*/
|
|
242
|
+
export const evaluateRuntimeAssignmentV1 = async (
|
|
243
|
+
incomingArg: IRuntimeAssignmentV1,
|
|
244
|
+
acceptedArg: IRuntimeAssignmentV1 | null,
|
|
245
|
+
trustedScopeArg: IRuntimeAssignmentScopeV1,
|
|
246
|
+
): Promise<TRuntimeAssignmentDecisionV1> => {
|
|
247
|
+
try {
|
|
248
|
+
incomingArg = snapshotAssignment(incomingArg);
|
|
249
|
+
acceptedArg = acceptedArg === null ? null : snapshotAssignment(acceptedArg);
|
|
250
|
+
if (!isExactRecord(trustedScopeArg, ['controller', 'organizationId', 'nodeId', 'runtimeNamespace'])
|
|
251
|
+
|| !isController(trustedScopeArg.controller)
|
|
252
|
+
|| !isIdentifier(trustedScopeArg.organizationId) || !isIdentifier(trustedScopeArg.nodeId)
|
|
253
|
+
|| !isIdentifier(trustedScopeArg.runtimeNamespace)) return { status: 'reject', reason: 'invalid' };
|
|
254
|
+
trustedScopeArg = JSON.parse(canonical(trustedScopeArg)) as IRuntimeAssignmentScopeV1;
|
|
255
|
+
} catch {
|
|
256
|
+
return { status: 'reject', reason: 'invalid' };
|
|
257
|
+
}
|
|
258
|
+
if ((await validateRuntimeAssignmentV1(incomingArg)).length
|
|
259
|
+
|| (acceptedArg !== null && (await validateRuntimeAssignmentV1(acceptedArg)).length)) {
|
|
260
|
+
return { status: 'reject', reason: 'invalid' };
|
|
261
|
+
}
|
|
262
|
+
const matchesScope = (assignmentArg: IRuntimeAssignmentV1): boolean =>
|
|
263
|
+
assignmentArg.controller.kind === trustedScopeArg.controller.kind
|
|
264
|
+
&& assignmentArg.controller.id === trustedScopeArg.controller.id
|
|
265
|
+
&& assignmentArg.controller.epoch === trustedScopeArg.controller.epoch
|
|
266
|
+
&& assignmentArg.organizationId === trustedScopeArg.organizationId
|
|
267
|
+
&& assignmentArg.nodeId === trustedScopeArg.nodeId
|
|
268
|
+
&& assignmentArg.runtimeNamespace === trustedScopeArg.runtimeNamespace;
|
|
269
|
+
if (!matchesScope(incomingArg) || (acceptedArg !== null && !matchesScope(acceptedArg))) {
|
|
270
|
+
return { status: 'reject', reason: 'scope-mismatch' };
|
|
271
|
+
}
|
|
272
|
+
if (acceptedArg === null) {
|
|
273
|
+
return incomingArg.generation === 1
|
|
274
|
+
&& incomingArg.disposition === 'run'
|
|
275
|
+
? { status: 'accept', assignment: incomingArg }
|
|
276
|
+
: { status: 'reject', reason: 'predecessor-mismatch' };
|
|
277
|
+
}
|
|
278
|
+
if (incomingArg.assignmentId !== acceptedArg.assignmentId) {
|
|
279
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
280
|
+
}
|
|
281
|
+
if (incomingArg.generation < acceptedArg.generation) return { status: 'reject', reason: 'stale' };
|
|
282
|
+
if (incomingArg.generation === acceptedArg.generation) {
|
|
283
|
+
return incomingArg.digest === acceptedArg.digest
|
|
284
|
+
? { status: 'replay', assignment: acceptedArg } : { status: 'reject', reason: 'conflict' };
|
|
285
|
+
}
|
|
286
|
+
if (canonical(incomingArg.previous) !== canonical(getRuntimeAssignmentV1Reference(acceptedArg))) {
|
|
287
|
+
return { status: 'reject', reason: 'predecessor-mismatch' };
|
|
288
|
+
}
|
|
289
|
+
if (immutableAttempt(incomingArg) !== immutableAttempt(acceptedArg)) {
|
|
290
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
291
|
+
}
|
|
292
|
+
const order: Record<TRuntimeAssignmentDispositionV1, number> = {
|
|
293
|
+
run: 0, 'stop-preserve': 1, 'remove-runtime-preserve-storage': 2,
|
|
294
|
+
};
|
|
295
|
+
if (order[incomingArg.disposition] !== order[acceptedArg.disposition] + 1) {
|
|
296
|
+
return { status: 'reject', reason: 'disposition-regression' };
|
|
297
|
+
}
|
|
298
|
+
return { status: 'accept', assignment: incomingArg };
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const assertObservation = (valueArg: unknown): IRuntimeAssignmentObservationV1 => {
|
|
302
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'assignment', 'reporterSessionId',
|
|
303
|
+
'sequence', 'observedAt', 'phase', 'ready', 'runtime', 'digest'])
|
|
304
|
+
|| valueArg.schemaVersion !== 1 || !isAssignmentReference(valueArg.assignment)
|
|
305
|
+
|| !isIdentifier(valueArg.reporterSessionId) || !isPositiveInteger(valueArg.sequence)
|
|
306
|
+
|| !isPositiveInteger(valueArg.observedAt) || !isDigest(valueArg.digest)
|
|
307
|
+
|| typeof valueArg.ready !== 'boolean'
|
|
308
|
+
|| !['pending', 'running', 'stopped', 'removed', 'failed', 'unknown'].includes(valueArg.phase as string)) {
|
|
309
|
+
fail('runtime observation must use its exact v1 schema');
|
|
310
|
+
}
|
|
311
|
+
if (valueArg.runtime !== null && (!isExactRecord(valueArg.runtime, [
|
|
312
|
+
'sandboxId', 'containerId', 'imageDigest',
|
|
313
|
+
]) || !isIdentifier(valueArg.runtime.sandboxId) || !isIdentifier(valueArg.runtime.containerId)
|
|
314
|
+
|| !isDigest(valueArg.runtime.imageDigest))) fail('runtime identity must be canonical');
|
|
315
|
+
if ((valueArg.ready && valueArg.phase !== 'running')
|
|
316
|
+
|| (valueArg.phase === 'running' && valueArg.runtime === null)
|
|
317
|
+
|| (valueArg.phase === 'removed' && valueArg.runtime !== null)) {
|
|
318
|
+
fail('runtime observation readiness and identity contradict its phase');
|
|
319
|
+
}
|
|
320
|
+
canonical(valueArg);
|
|
321
|
+
return valueArg as unknown as IRuntimeAssignmentObservationV1;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const computeRuntimeAssignmentObservationV1Digest = async (
|
|
325
|
+
observationArg: IRuntimeAssignmentObservationV1,
|
|
326
|
+
): Promise<TSha256Digest> => digestPayload(
|
|
327
|
+
runtimeAssignmentV1Contract.observationDigestDomain, assertObservation(observationArg),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
/** Checks exact assignment evidence; transport must separately bind reporter/session. */
|
|
331
|
+
export const validateRuntimeAssignmentObservationV1 = async (
|
|
332
|
+
observationArg: unknown,
|
|
333
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
334
|
+
): Promise<string[]> => {
|
|
335
|
+
try {
|
|
336
|
+
const observation = JSON.parse(canonical(assertObservation(observationArg))) as IRuntimeAssignmentObservationV1;
|
|
337
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
338
|
+
if ((await validateRuntimeAssignmentV1(assignmentArg)).length) return ['invalid assignment'];
|
|
339
|
+
if (canonical(observation.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))) {
|
|
340
|
+
return ['runtime observation does not match the exact assignment'];
|
|
341
|
+
}
|
|
342
|
+
if (observation.digest !== await computeRuntimeAssignmentObservationV1Digest(observation)) {
|
|
343
|
+
return ['runtime observation digest does not match'];
|
|
344
|
+
}
|
|
345
|
+
if (observation.ready && (assignmentArg.disposition !== 'run'
|
|
346
|
+
|| observation.runtime?.imageDigest !== assignmentArg.workload.imageDigest)) {
|
|
347
|
+
return ['runtime readiness does not match authorized execution'];
|
|
348
|
+
}
|
|
349
|
+
return [];
|
|
350
|
+
} catch {
|
|
351
|
+
return ['runtime observation must use its exact canonical v1 schema'];
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export type TRuntimeObservationDecisionV1 =
|
|
356
|
+
| { status: 'accept' | 'replay'; observation: IRuntimeAssignmentObservationV1 }
|
|
357
|
+
| { status: 'reject'; reason: 'invalid' | 'session-mismatch' | 'stale' | 'conflict' | 'sequence-gap' };
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Receiver must authorize the assignment's node before calling this helper.
|
|
361
|
+
* Compare/persist the returned snapshot atomically with the old receipt. A new
|
|
362
|
+
* session continues the durable counter; it does not reset observation history.
|
|
363
|
+
* Across dispositions, the old receipt must reference the exact predecessor;
|
|
364
|
+
* receivers must process each revision's observations in order.
|
|
365
|
+
* These observations are never sufficient authority to relocate a storage writer.
|
|
366
|
+
*/
|
|
367
|
+
export const evaluateRuntimeAssignmentObservationV1 = async (
|
|
368
|
+
incomingArg: IRuntimeAssignmentObservationV1,
|
|
369
|
+
acceptedArg: IRuntimeAssignmentObservationV1 | null,
|
|
370
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
371
|
+
trustedReporterSessionIdArg: string,
|
|
372
|
+
): Promise<TRuntimeObservationDecisionV1> => {
|
|
373
|
+
try {
|
|
374
|
+
incomingArg = JSON.parse(canonical(assertObservation(incomingArg))) as IRuntimeAssignmentObservationV1;
|
|
375
|
+
acceptedArg = acceptedArg === null ? null
|
|
376
|
+
: JSON.parse(canonical(assertObservation(acceptedArg))) as IRuntimeAssignmentObservationV1;
|
|
377
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
378
|
+
if (!isIdentifier(trustedReporterSessionIdArg)
|
|
379
|
+
|| (await validateRuntimeAssignmentObservationV1(incomingArg, assignmentArg)).length
|
|
380
|
+
|| (acceptedArg !== null && (
|
|
381
|
+
acceptedArg.digest !== await computeRuntimeAssignmentObservationV1Digest(acceptedArg)
|
|
382
|
+
|| acceptedArg.assignment.assignmentId !== assignmentArg.assignmentId
|
|
383
|
+
|| (canonical(acceptedArg.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))
|
|
384
|
+
&& canonical(acceptedArg.assignment) !== canonical(assignmentArg.previous))
|
|
385
|
+
))) return { status: 'reject', reason: 'invalid' };
|
|
386
|
+
if (incomingArg.reporterSessionId !== trustedReporterSessionIdArg) {
|
|
387
|
+
return { status: 'reject', reason: 'session-mismatch' };
|
|
388
|
+
}
|
|
389
|
+
if (acceptedArg !== null && incomingArg.sequence <= acceptedArg.sequence) {
|
|
390
|
+
if (incomingArg.sequence < acceptedArg.sequence) return { status: 'reject', reason: 'stale' };
|
|
391
|
+
return incomingArg.digest === acceptedArg.digest
|
|
392
|
+
? { status: 'replay', observation: acceptedArg }
|
|
393
|
+
: { status: 'reject', reason: 'conflict' };
|
|
394
|
+
}
|
|
395
|
+
if (incomingArg.sequence !== (acceptedArg?.sequence ?? 0) + 1) {
|
|
396
|
+
return { status: 'reject', reason: 'sequence-gap' };
|
|
397
|
+
}
|
|
398
|
+
return { status: 'accept', observation: incomingArg };
|
|
399
|
+
} catch {
|
|
400
|
+
return { status: 'reject', reason: 'invalid' };
|
|
401
|
+
}
|
|
402
|
+
};
|
/package/{license → license.md}
RENAMED
|
File without changes
|