@astrale-os/kernel-dsl 0.2.0-beta.29 → 0.2.0-beta.30
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/dist/v1/builder/callable/authoring.d.ts +1 -0
- package/dist/v1/builder/lowering/callable.js +28 -24
- package/dist/v1/builder/lowering/policy.js +1 -0
- package/dist/v1/builder/policy/authoring.d.ts +5 -1
- package/dist/v1/builder/policy/authoring.js +5 -0
- package/dist/v1/compiled/admission/accept.js +37 -14
- package/dist/v1/compiled/admission/semantics.js +48 -14
- package/dist/v1/language/policy/analysis/branches.d.ts +2 -0
- package/dist/v1/language/policy/analysis/branches.js +51 -17
- package/dist/v1/language/policy/analysis/budgets.js +1 -1
- package/dist/v1/language/policy/model/policy.d.ts +12 -2
- package/dist/v1/language/policy/normalization/normalize.js +39 -0
- package/dist/v1/schema/resolution/origins.js +15 -0
- package/dist/v1/schema/validation/domain.structure.js +7 -0
- package/dist/v1/schema/validation/policy/budgets.js +15 -2
- package/dist/v1/schema/validation/policy/objects.d.ts +4 -0
- package/dist/v1/schema/validation/policy/objects.js +64 -0
- package/dist/v1/schema/validation/policy/references.js +8 -0
- package/dist/v1/schema/validation/policy/typing.js +10 -0
- package/dist/v1/schema/validation/policy/uses.js +13 -61
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ declare const CALLABLE_OUTPUT: unique symbol;
|
|
|
14
14
|
declare const CALLABLE_PROJECTION: unique symbol;
|
|
15
15
|
declare const CALLABLE_PROJECTION_VALUE: unique symbol;
|
|
16
16
|
export interface CallablePolicyContext {
|
|
17
|
+
sameNode(left: PolicyCheckObject, right: PolicyCheckObject): PolicyCheckExpression;
|
|
17
18
|
readonly input: Readonly<Record<string, Extract<PolicyCheckObject, {
|
|
18
19
|
readonly kind: 'input';
|
|
19
20
|
}>>>;
|
|
@@ -70,34 +70,38 @@ export class V1CallableCompiler {
|
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
72
|
const self = callableSelf();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let admittedObject = resolvedObject;
|
|
80
|
-
if (resolvedObject.kind === 'self') {
|
|
81
|
-
if (placement.kind === 'function' || isStatic) {
|
|
82
|
-
fail('BUILDER_POLICY_SELF', `${placement.pointer}/policy`, 'self is available only to a non-static Node Method.');
|
|
83
|
-
}
|
|
73
|
+
const admitObject = (object) => {
|
|
74
|
+
const resolvedObject = callablePolicyObject(object);
|
|
75
|
+
let admittedObject = resolvedObject;
|
|
76
|
+
if (resolvedObject.kind === 'self') {
|
|
77
|
+
if (placement.kind === 'function' || isStatic) {
|
|
78
|
+
fail('BUILDER_POLICY_SELF', `${placement.pointer}/policy`, 'self is available only to a non-static Node Method.');
|
|
84
79
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else {
|
|
90
|
-
fail('BUILDER_POLICY_INPUT', `${placement.pointer}/policy`, `input.${resolvedObject.field} is not one direct required single-Node path field.`);
|
|
91
|
-
}
|
|
80
|
+
}
|
|
81
|
+
else if (resolvedObject.kind === 'input') {
|
|
82
|
+
if (isRequiredDirectNodePath(inputSchema, resolvedObject.field)) {
|
|
83
|
+
// The field contract is admitted below as-is.
|
|
92
84
|
}
|
|
93
85
|
else {
|
|
94
|
-
|
|
86
|
+
fail('BUILDER_POLICY_INPUT', `${placement.pointer}/policy`, `input.${resolvedObject.field} is not one direct required single-Node path field.`);
|
|
95
87
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
admittedObject = callableRef(this.context.resolvePolicyObject(resolvedObject.ref, `${placement.pointer}/policy/ref`));
|
|
91
|
+
}
|
|
92
|
+
return admittedObject;
|
|
93
|
+
};
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
input,
|
|
96
|
+
self,
|
|
97
|
+
ref: (member) => callableRef(this.context.resolvePolicyObject(member, `${placement.pointer}/policy/ref`)),
|
|
98
|
+
sameNode: (left, right) => ({
|
|
99
|
+
sameNode: { left: admitObject(left), right: admitObject(right) },
|
|
100
|
+
}),
|
|
101
|
+
check: (policy, object) => ({
|
|
102
|
+
check: this.context.resolvePolicy(policy, `${placement.pointer}/policy/check`),
|
|
103
|
+
object: admitObject(object),
|
|
104
|
+
}),
|
|
101
105
|
allOf: (...expressions) => {
|
|
102
106
|
nonEmpty(expressions, `${placement.pointer}/policy/allOf`);
|
|
103
107
|
return { allOf: expressions };
|
|
@@ -20,6 +20,7 @@ export class V1PolicyCompiler {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
const match = input.match(createPatternContext({
|
|
23
|
+
resolveObject: (member, refPointer) => this.context.resolvePolicyObject(member, refPointer),
|
|
23
24
|
resolveEdgeClass: (definition, refPointer) => this.context.resolveEdgeClass(definition, refPointer),
|
|
24
25
|
resolveNodeClass: (definition, refPointer) => this.context.resolveNodeClass(definition, refPointer),
|
|
25
26
|
pointer,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClassRef } from '../../addressing/index.js';
|
|
2
|
-
import type { PolicyCheckExpression, PolicyPatternExpression, PolicyPatternTerm as CanonicalPolicyPatternTerm } from '../../language/policy/model/policy.js';
|
|
2
|
+
import type { PolicyCheckExpression, PolicyPatternExpression, PolicyPatternTerm as CanonicalPolicyPatternTerm, PolicyObjectRef } from '../../language/policy/model/policy.js';
|
|
3
|
+
import type { PolicyObjectInput } from '../callable/authoring.js';
|
|
3
4
|
import type { EdgeClass, NodeClass } from '../class/authoring.js';
|
|
4
5
|
import type { AuthoringValue } from '../schema/handle.js';
|
|
5
6
|
import type { Lazy } from '../schema/lazy.js';
|
|
@@ -21,6 +22,8 @@ export interface PolicyExistsContext {
|
|
|
21
22
|
readonly node: PolicyNodeBinder;
|
|
22
23
|
}
|
|
23
24
|
export interface PolicyPatternContext {
|
|
25
|
+
ref(member: PolicyObjectInput): CanonicalPolicyPatternTerm;
|
|
26
|
+
sameNode(left: CanonicalPolicyPatternTerm, right: CanonicalPolicyPatternTerm): PolicyPatternExpression;
|
|
24
27
|
readonly subject: CanonicalPolicyPatternTerm;
|
|
25
28
|
readonly object: CanonicalPolicyPatternTerm;
|
|
26
29
|
readonly source: CanonicalPolicyPatternTerm;
|
|
@@ -50,6 +53,7 @@ export interface PolicyBuilder {
|
|
|
50
53
|
export declare function policyDeclaration(value: Policy): PolicyDeclaration;
|
|
51
54
|
export declare const policy: PolicyBuilder;
|
|
52
55
|
export declare function createPatternContext(options: {
|
|
56
|
+
readonly resolveObject: (value: PolicyObjectInput, pointer: string) => PolicyObjectRef;
|
|
53
57
|
readonly resolveEdgeClass: (value: Lazy<EdgeClass>, pointer: string) => ClassRef;
|
|
54
58
|
readonly resolveNodeClass: (value: Lazy<NodeClass>, pointer: string) => ClassRef;
|
|
55
59
|
readonly pointer: string;
|
|
@@ -32,6 +32,11 @@ export function createPatternContext(options) {
|
|
|
32
32
|
const source = Object.freeze({ kind: 'source' });
|
|
33
33
|
const target = Object.freeze({ kind: 'target' });
|
|
34
34
|
const context = {
|
|
35
|
+
ref: (member) => ({
|
|
36
|
+
kind: 'ref',
|
|
37
|
+
ref: options.resolveObject(member, `${options.pointer}/match/ref`),
|
|
38
|
+
}),
|
|
39
|
+
sameNode: (left, right) => ({ sameNode: { left, right } }),
|
|
35
40
|
subject,
|
|
36
41
|
object,
|
|
37
42
|
source,
|
|
@@ -421,6 +421,14 @@ function policyPattern(input, depth, path) {
|
|
|
421
421
|
if (depth > 64)
|
|
422
422
|
invalid('Policy pattern depth', undefined, path);
|
|
423
423
|
const value = object(input, 'Policy pattern', path);
|
|
424
|
+
if (Object.hasOwn(value, 'sameNode')) {
|
|
425
|
+
exact(value, ['sameNode'], 'Policy equality', path);
|
|
426
|
+
const pair = object(value.sameNode, 'Policy equality', `${path}/sameNode`);
|
|
427
|
+
exact(pair, ['left', 'right'], 'Policy equality operands', `${path}/sameNode`);
|
|
428
|
+
policyTerm(pair.left, false, `${path}/sameNode/left`);
|
|
429
|
+
policyTerm(pair.right, false, `${path}/sameNode/right`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
424
432
|
if (Object.hasOwn(value, 'exists')) {
|
|
425
433
|
exact(value, ['exists'], 'Policy exists', path);
|
|
426
434
|
const existsPath = `${path}/exists`;
|
|
@@ -484,6 +492,11 @@ function policyPattern(input, depth, path) {
|
|
|
484
492
|
}
|
|
485
493
|
function policyTerm(input, variableOnly, path) {
|
|
486
494
|
const value = object(input, 'Policy pattern term', path);
|
|
495
|
+
if (!variableOnly && value.kind === 'ref') {
|
|
496
|
+
exact(value, ['kind', 'ref'], 'Policy reference term', path);
|
|
497
|
+
definitionRef(value.ref, undefined, 'Policy reference term', `${path}/ref`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
487
500
|
if (value.kind === 'variable') {
|
|
488
501
|
exact(value, ['kind', 'id'], 'Policy variable term', path);
|
|
489
502
|
integer(value.id, 'Policy variable id', `${path}/id`);
|
|
@@ -502,23 +515,18 @@ function policyCheck(input, depth, path) {
|
|
|
502
515
|
if (depth > 64)
|
|
503
516
|
invalid('Callable Policy depth', undefined, path);
|
|
504
517
|
const value = object(input, 'Callable Policy', path);
|
|
518
|
+
if (Object.hasOwn(value, 'sameNode')) {
|
|
519
|
+
exact(value, ['sameNode'], 'Callable equality', path);
|
|
520
|
+
const pair = object(value.sameNode, 'Callable equality', `${path}/sameNode`);
|
|
521
|
+
exact(pair, ['left', 'right'], 'Callable equality operands', `${path}/sameNode`);
|
|
522
|
+
policyCheckObject(pair.left, `${path}/sameNode/left`);
|
|
523
|
+
policyCheckObject(pair.right, `${path}/sameNode/right`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
505
526
|
if (Object.hasOwn(value, 'check')) {
|
|
506
527
|
exact(value, ['check', 'object'], 'Callable Policy check', path);
|
|
507
528
|
definitionRef(value.check, 'policy', 'Callable Policy ref', `${path}/check`);
|
|
508
|
-
|
|
509
|
-
const target = object(value.object, 'Callable Policy object', objectPath);
|
|
510
|
-
if (target.kind === 'self')
|
|
511
|
-
exact(target, ['kind'], 'Callable Policy self', objectPath);
|
|
512
|
-
else if (target.kind === 'input') {
|
|
513
|
-
exact(target, ['kind', 'field'], 'Callable Policy input', objectPath);
|
|
514
|
-
memberName(target.field, 'Callable Policy input field', `${objectPath}/field`);
|
|
515
|
-
}
|
|
516
|
-
else if (target.kind === 'ref') {
|
|
517
|
-
exact(target, ['kind', 'ref'], 'Callable Policy object ref', objectPath);
|
|
518
|
-
definitionRef(target.ref, undefined, 'Callable Policy object ref', `${objectPath}/ref`);
|
|
519
|
-
}
|
|
520
|
-
else
|
|
521
|
-
invalid('Callable Policy object', undefined, `${objectPath}/kind`);
|
|
529
|
+
policyCheckObject(value.object, `${path}/object`);
|
|
522
530
|
return;
|
|
523
531
|
}
|
|
524
532
|
const member = Object.hasOwn(value, 'allOf') ? 'allOf' : 'anyOf';
|
|
@@ -530,6 +538,21 @@ function policyCheck(input, depth, path) {
|
|
|
530
538
|
for (const [index, expression] of expressions.entries())
|
|
531
539
|
policyCheck(expression, depth + 1, `${memberPath}/${index}`);
|
|
532
540
|
}
|
|
541
|
+
function policyCheckObject(input, objectPath) {
|
|
542
|
+
const target = object(input, 'Callable Policy object', objectPath);
|
|
543
|
+
if (target.kind === 'self')
|
|
544
|
+
exact(target, ['kind'], 'Callable Policy self', objectPath);
|
|
545
|
+
else if (target.kind === 'input') {
|
|
546
|
+
exact(target, ['kind', 'field'], 'Callable Policy input', objectPath);
|
|
547
|
+
memberName(target.field, 'Callable Policy input field', `${objectPath}/field`);
|
|
548
|
+
}
|
|
549
|
+
else if (target.kind === 'ref') {
|
|
550
|
+
exact(target, ['kind', 'ref'], 'Callable Policy object ref', objectPath);
|
|
551
|
+
definitionRef(target.ref, undefined, 'Callable Policy object ref', `${objectPath}/ref`);
|
|
552
|
+
}
|
|
553
|
+
else
|
|
554
|
+
invalid('Callable Policy object', undefined, `${objectPath}/kind`);
|
|
555
|
+
}
|
|
533
556
|
function coreEndpoint(input, label, path) {
|
|
534
557
|
const value = object(input, label, path);
|
|
535
558
|
if (value.kind === 'domain')
|
|
@@ -347,6 +347,11 @@ function validatePolicyExpression(context, entry, expression, path, local) {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
function validatePattern(context, expression, path, scope, declared) {
|
|
350
|
+
if ('sameNode' in expression) {
|
|
351
|
+
validatePolicyTerm(expression.sameNode.left, `${path}/sameNode/left`, scope);
|
|
352
|
+
validatePolicyTerm(expression.sameNode.right, `${path}/sameNode/right`, scope);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
350
355
|
if ('source' in expression) {
|
|
351
356
|
if (context.classes.get(Key.of(expression.class))?.value.kind !== 'edge')
|
|
352
357
|
fail(`${path}/class`, 'Policy pattern Class must resolve to an Edge Class.');
|
|
@@ -410,21 +415,22 @@ function validatePolicyCycles(entry, local, entryIndex) {
|
|
|
410
415
|
visit(name);
|
|
411
416
|
}
|
|
412
417
|
function validateCallablePolicy(context, entry, expression, inputIndex, fields, instanceMethod, path) {
|
|
413
|
-
if ('check' in expression) {
|
|
414
|
-
if (
|
|
415
|
-
|
|
418
|
+
if ('check' in expression || 'sameNode' in expression) {
|
|
419
|
+
if ('check' in expression &&
|
|
420
|
+
(expression.check.origin !== entry.origin ||
|
|
421
|
+
context.policies.get(Key.of(expression.check)) === undefined))
|
|
416
422
|
fail(`${path}/check`, 'Callable Policy must reference a local named Policy.');
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
fields
|
|
423
|
-
|
|
424
|
-
|
|
423
|
+
const objects = 'check' in expression ? { object: expression.object } : expression.sameNode;
|
|
424
|
+
for (const [side, object] of Object.entries(objects)) {
|
|
425
|
+
if (object.kind === 'self' && !instanceMethod)
|
|
426
|
+
fail(`${path}/${side}`, 'self is valid only for an instance Method.');
|
|
427
|
+
if (object.kind === 'input' &&
|
|
428
|
+
(fields === undefined ||
|
|
429
|
+
fields[object.field] === undefined ||
|
|
430
|
+
!isSingleNodePathField(context, entry, inputIndex, object.field))) {
|
|
431
|
+
fail(`${path}/${side}`, 'Callable Policy input target must be a direct required single-Node Path field.');
|
|
432
|
+
}
|
|
425
433
|
}
|
|
426
|
-
// validateClosure already establishes that object refs name existing local
|
|
427
|
-
// or exact direct-dependency Definitions. Named Policy execution stays local.
|
|
428
434
|
return;
|
|
429
435
|
}
|
|
430
436
|
const children = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
@@ -555,6 +561,8 @@ function validateCallablePolicyTyping(context, typing, entry, expression, input,
|
|
|
555
561
|
fail(path, 'Callable Policy checks exceed the V1 depth, branch, or leaf budget.');
|
|
556
562
|
}
|
|
557
563
|
const visit = (value, pointer) => {
|
|
564
|
+
if ('sameNode' in value)
|
|
565
|
+
return;
|
|
558
566
|
if (!('check' in value)) {
|
|
559
567
|
const children = 'allOf' in value ? value.allOf : value.anyOf;
|
|
560
568
|
children.forEach((child, index) => visit(child, `${pointer}/${index}`));
|
|
@@ -668,11 +676,15 @@ class CompiledPolicyTyping {
|
|
|
668
676
|
const terms = new Set(use.kind === 'object' ? ['subject', 'object'] : ['subject', 'source', 'target']);
|
|
669
677
|
for (const key of branch.variables.keys())
|
|
670
678
|
terms.add(key);
|
|
679
|
+
for (const key of branch.terms.keys())
|
|
680
|
+
terms.add(key);
|
|
671
681
|
for (const edge of branch.edges) {
|
|
672
682
|
terms.add(termKey(edge.pattern.source, edge.namespace));
|
|
673
683
|
terms.add(termKey(edge.pattern.target, edge.namespace));
|
|
674
684
|
}
|
|
675
685
|
const union = new CompiledUnionFind(terms);
|
|
686
|
+
for (const [left, right] of branch.equalities)
|
|
687
|
+
union.join(left, right);
|
|
676
688
|
for (const variant of variants)
|
|
677
689
|
for (const [left, right] of variant.equalities)
|
|
678
690
|
union.join(left, right);
|
|
@@ -716,6 +728,11 @@ class CompiledPolicyTyping {
|
|
|
716
728
|
? this.applyExact(candidates, contract.ref)
|
|
717
729
|
: uniqueCompiledCandidates(this.allNodeClasses.flatMap((ref) => this.applyExact(candidates, ref)));
|
|
718
730
|
}
|
|
731
|
+
const referenced = branch.terms.get(term);
|
|
732
|
+
if (referenced?.kind === 'ref') {
|
|
733
|
+
const contract = callableObjectContracts(this.context, this.entry, 0, undefined, referenced)?.[0];
|
|
734
|
+
return contract?.kind === 'exact' ? this.applyExact(candidates, contract.ref) : [];
|
|
735
|
+
}
|
|
719
736
|
const selector = branch.variables.get(term);
|
|
720
737
|
if (selector === undefined)
|
|
721
738
|
return [];
|
|
@@ -870,8 +887,25 @@ function measureCompiledPolicy(origin, name, normalized, visiting) {
|
|
|
870
887
|
return combineCompiledMeasures('allOf' in expression ? 'allOf' : 'anyOf', children);
|
|
871
888
|
}
|
|
872
889
|
function measureCompiledPattern(pattern, origin) {
|
|
890
|
+
if ('sameNode' in pattern)
|
|
891
|
+
return {
|
|
892
|
+
depth: 1,
|
|
893
|
+
edgeCounts: [1],
|
|
894
|
+
domains: new Set([
|
|
895
|
+
origin,
|
|
896
|
+
...[pattern.sameNode.left, pattern.sameNode.right].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
|
|
897
|
+
]),
|
|
898
|
+
};
|
|
873
899
|
if ('source' in pattern)
|
|
874
|
-
return {
|
|
900
|
+
return {
|
|
901
|
+
depth: 1,
|
|
902
|
+
edgeCounts: [1],
|
|
903
|
+
domains: new Set([
|
|
904
|
+
origin,
|
|
905
|
+
pattern.class.origin,
|
|
906
|
+
...[pattern.source, pattern.target].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
|
|
907
|
+
]),
|
|
908
|
+
};
|
|
875
909
|
if ('exists' in pattern) {
|
|
876
910
|
const child = measureCompiledPattern(pattern.exists.where, origin);
|
|
877
911
|
return {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PolicyEdgePattern, PolicyPatternExpression, PolicyPatternTerm, PolicyVariableSelector } from '../model/policy.js';
|
|
2
2
|
export interface PatternBranch {
|
|
3
|
+
readonly terms: ReadonlyMap<string, PolicyPatternTerm>;
|
|
4
|
+
readonly equalities: readonly (readonly [string, string])[];
|
|
3
5
|
readonly edges: readonly PatternEdgeUse[];
|
|
4
6
|
readonly variables: ReadonlyMap<string, PolicyVariableSelector>;
|
|
5
7
|
readonly requiredVariables: ReadonlySet<string>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definitionRefIndex } from '../../../addressing/ordering.js';
|
|
1
2
|
import { policyVariableSelector } from '../model/policy.js';
|
|
2
3
|
import { BRANCH_OVERFLOW } from './budgets.js';
|
|
3
4
|
export function branchesOf(pattern, namespace) {
|
|
@@ -26,14 +27,7 @@ export function isConnected(branch) {
|
|
|
26
27
|
const mode = targetMode(branch);
|
|
27
28
|
if (mode === undefined)
|
|
28
29
|
return false;
|
|
29
|
-
const vertices = new Set(['subject']);
|
|
30
|
-
for (const edge of branch.edges) {
|
|
31
|
-
for (const term of [edge.pattern.source, edge.pattern.target]) {
|
|
32
|
-
if (term.kind === 'object' || term.kind === 'source' || term.kind === 'target') {
|
|
33
|
-
vertices.add(term.kind);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
30
|
+
const vertices = new Set(['subject', ...branch.terms.keys()]);
|
|
37
31
|
for (const variable of branch.requiredVariables)
|
|
38
32
|
vertices.add(variable);
|
|
39
33
|
const union = new UnionFind(vertices);
|
|
@@ -44,6 +38,8 @@ export function isConnected(branch) {
|
|
|
44
38
|
union.add(target);
|
|
45
39
|
union.join(source, target);
|
|
46
40
|
}
|
|
41
|
+
for (const [left, right] of branch.equalities)
|
|
42
|
+
union.join(left, right);
|
|
47
43
|
const root = union.find('subject');
|
|
48
44
|
return [...vertices].every((vertex) => union.find(vertex) === root);
|
|
49
45
|
}
|
|
@@ -51,12 +47,10 @@ export function targetMode(branch) {
|
|
|
51
47
|
let object = false;
|
|
52
48
|
let source = false;
|
|
53
49
|
let target = false;
|
|
54
|
-
for (const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
target ||= term.kind === 'target';
|
|
59
|
-
}
|
|
50
|
+
for (const term of branch.terms.values()) {
|
|
51
|
+
object ||= term.kind === 'object';
|
|
52
|
+
source ||= term.kind === 'source';
|
|
53
|
+
target ||= term.kind === 'target';
|
|
60
54
|
}
|
|
61
55
|
if (object && !source && !target)
|
|
62
56
|
return 'object';
|
|
@@ -65,11 +59,42 @@ export function targetMode(branch) {
|
|
|
65
59
|
return undefined;
|
|
66
60
|
}
|
|
67
61
|
export function termKey(term, namespace) {
|
|
68
|
-
return term.kind === 'variable'
|
|
62
|
+
return term.kind === 'variable'
|
|
63
|
+
? variableKey(namespace, term.id)
|
|
64
|
+
: term.kind === 'ref'
|
|
65
|
+
? `ref:${definitionRefIndex(term.ref)}`
|
|
66
|
+
: term.kind;
|
|
69
67
|
}
|
|
70
68
|
function branchesForNode(pattern, namespace, variables) {
|
|
71
69
|
if ('source' in pattern) {
|
|
72
|
-
return [
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
terms: new Map([pattern.source, pattern.target].map((term) => [termKey(term, namespace), term])),
|
|
73
|
+
equalities: [],
|
|
74
|
+
edges: [{ pattern, namespace }],
|
|
75
|
+
variables,
|
|
76
|
+
requiredVariables: new Set(),
|
|
77
|
+
namespace,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
if ('sameNode' in pattern) {
|
|
82
|
+
const terms = new Map([pattern.sameNode.left, pattern.sameNode.right].map((term) => [
|
|
83
|
+
termKey(term, namespace),
|
|
84
|
+
term,
|
|
85
|
+
]));
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
terms,
|
|
89
|
+
equalities: [
|
|
90
|
+
[termKey(pattern.sameNode.left, namespace), termKey(pattern.sameNode.right, namespace)],
|
|
91
|
+
],
|
|
92
|
+
edges: [],
|
|
93
|
+
variables,
|
|
94
|
+
requiredVariables: new Set(),
|
|
95
|
+
namespace,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
73
98
|
}
|
|
74
99
|
if ('exists' in pattern) {
|
|
75
100
|
const direct = new Set(pattern.exists.nodes.map((node) => variableKey(namespace, node.variable.id)));
|
|
@@ -82,10 +107,19 @@ function branchesForNode(pattern, namespace, variables) {
|
|
|
82
107
|
return 'allOf' in pattern ? branchProduct(children) : capBranches(children.flat());
|
|
83
108
|
}
|
|
84
109
|
function emptyBranch() {
|
|
85
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
terms: new Map(),
|
|
112
|
+
equalities: [],
|
|
113
|
+
edges: [],
|
|
114
|
+
variables: new Map(),
|
|
115
|
+
requiredVariables: new Set(),
|
|
116
|
+
namespace: '$',
|
|
117
|
+
};
|
|
86
118
|
}
|
|
87
119
|
function mergeBranches(left, right) {
|
|
88
120
|
return {
|
|
121
|
+
terms: new Map([...left.terms, ...right.terms]),
|
|
122
|
+
equalities: [...left.equalities, ...right.equalities],
|
|
89
123
|
edges: [...left.edges, ...right.edges],
|
|
90
124
|
variables: new Map([...left.variables, ...right.variables]),
|
|
91
125
|
requiredVariables: new Set([...left.requiredVariables, ...right.requiredVariables]),
|
|
@@ -8,7 +8,7 @@ export const MAX_CHECK_DEPTH = 4;
|
|
|
8
8
|
export const MAX_CHECK_LEAVES = 8;
|
|
9
9
|
export const BRANCH_OVERFLOW = MAX_BRANCHES + 1;
|
|
10
10
|
export function measureChecks(expression) {
|
|
11
|
-
if ('check' in expression)
|
|
11
|
+
if ('check' in expression || 'sameNode' in expression)
|
|
12
12
|
return { depth: 1, branches: 1, leaves: 1 };
|
|
13
13
|
const children = ('allOf' in expression ? expression.allOf : expression.anyOf).map(measureChecks);
|
|
14
14
|
const branches = 'allOf' in expression
|
|
@@ -10,7 +10,17 @@ export type PolicyPatternTerm = {
|
|
|
10
10
|
} | {
|
|
11
11
|
readonly kind: 'variable';
|
|
12
12
|
readonly id: number;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'ref';
|
|
15
|
+
readonly ref: PolicyObjectRef;
|
|
13
16
|
};
|
|
17
|
+
/** Identity equality; both terms must resolve to one valid Node. */
|
|
18
|
+
export interface PolicySameNode<Term> {
|
|
19
|
+
readonly sameNode: {
|
|
20
|
+
readonly left: Term;
|
|
21
|
+
readonly right: Term;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
14
24
|
export interface ExactPolicyVariable {
|
|
15
25
|
readonly variable: {
|
|
16
26
|
readonly kind: 'variable';
|
|
@@ -53,7 +63,7 @@ export interface PolicyEdgePattern {
|
|
|
53
63
|
readonly target: PolicyPatternTerm;
|
|
54
64
|
readonly repeat?: PolicyRepeat;
|
|
55
65
|
}
|
|
56
|
-
export type PolicyPatternExpression = PolicyEdgePattern | {
|
|
66
|
+
export type PolicyPatternExpression = PolicyEdgePattern | PolicySameNode<PolicyPatternTerm> | {
|
|
57
67
|
readonly exists: {
|
|
58
68
|
readonly nodes: readonly PolicyVariable[];
|
|
59
69
|
readonly where: PolicyPatternExpression;
|
|
@@ -89,7 +99,7 @@ export type PolicyCheckObject = {
|
|
|
89
99
|
export type PolicyCheckExpression<Policy = PolicyRef, Object = PolicyCheckObject> = {
|
|
90
100
|
readonly check: Policy;
|
|
91
101
|
readonly object: Object;
|
|
92
|
-
} | {
|
|
102
|
+
} | PolicySameNode<Object> | {
|
|
93
103
|
readonly allOf: readonly [
|
|
94
104
|
PolicyCheckExpression<Policy, Object>,
|
|
95
105
|
...PolicyCheckExpression<Policy, Object>[]
|
|
@@ -18,6 +18,8 @@ export function normalizePolicyPattern(value) {
|
|
|
18
18
|
export function normalizePolicyChecks(value) {
|
|
19
19
|
if ('check' in value)
|
|
20
20
|
return value;
|
|
21
|
+
if ('sameNode' in value)
|
|
22
|
+
return { sameNode: normalizeEquality(value.sameNode) };
|
|
21
23
|
if ('allOf' in value) {
|
|
22
24
|
const children = value.allOf.flatMap((child) => {
|
|
23
25
|
const normalized = normalizePolicyChecks(child);
|
|
@@ -37,6 +39,8 @@ function policyCheckComposition(values) {
|
|
|
37
39
|
return values;
|
|
38
40
|
}
|
|
39
41
|
function normalizePatternNode(value) {
|
|
42
|
+
if ('sameNode' in value)
|
|
43
|
+
return { sameNode: normalizeEquality(value.sameNode) };
|
|
40
44
|
if (isEdge(value)) {
|
|
41
45
|
const repeat = value.repeat;
|
|
42
46
|
if (repeat?.min === 1 && repeat.max === 1) {
|
|
@@ -115,6 +119,14 @@ function collectDeclarations(value, result) {
|
|
|
115
119
|
collectDeclarations(child, result);
|
|
116
120
|
}
|
|
117
121
|
function collectUses(value, target, colors, result) {
|
|
122
|
+
if ('sameNode' in value) {
|
|
123
|
+
const { left, right } = value.sameNode;
|
|
124
|
+
if (left.kind === 'variable' && left.id === target)
|
|
125
|
+
result.push(`same:${termColor(right, colors)}`);
|
|
126
|
+
if (right.kind === 'variable' && right.id === target)
|
|
127
|
+
result.push(`same:${termColor(left, colors)}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
118
130
|
if (isEdge(value)) {
|
|
119
131
|
if (value.source.kind === 'variable' && value.source.id === target) {
|
|
120
132
|
result.push(`s:${definitionRefIndex(value.class)}:${termColor(value.target, colors)}`);
|
|
@@ -133,9 +145,22 @@ function collectUses(value, target, colors, result) {
|
|
|
133
145
|
function termColor(term, colors) {
|
|
134
146
|
if (term.kind === 'variable')
|
|
135
147
|
return `variable:${colors.get(term.id) ?? '?'}`;
|
|
148
|
+
if (term.kind === 'ref')
|
|
149
|
+
return `ref:${definitionRefIndex(term.ref)}`;
|
|
136
150
|
return term.kind;
|
|
137
151
|
}
|
|
138
152
|
function collectFirstUseKeys(value, colors, result, prefix = '$') {
|
|
153
|
+
if ('sameNode' in value) {
|
|
154
|
+
for (const term of [value.sameNode.left, value.sameNode.right]) {
|
|
155
|
+
if (term.kind !== 'variable')
|
|
156
|
+
continue;
|
|
157
|
+
const key = `${prefix}:same`;
|
|
158
|
+
const current = result.get(term.id);
|
|
159
|
+
if (current === undefined || key < current)
|
|
160
|
+
result.set(term.id, key);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
139
164
|
if (isEdge(value)) {
|
|
140
165
|
for (const [position, term] of [
|
|
141
166
|
['source', value.source],
|
|
@@ -160,6 +185,8 @@ function collectFirstUseKeys(value, colors, result, prefix = '$') {
|
|
|
160
185
|
children.forEach((child, index) => collectFirstUseKeys(child, colors, result, `${prefix}:${key}:${index}`));
|
|
161
186
|
}
|
|
162
187
|
function patternSignature(value, colors) {
|
|
188
|
+
if ('sameNode' in value)
|
|
189
|
+
return `same:${[termColor(value.sameNode.left, colors), termColor(value.sameNode.right, colors)].sort(compareUtf8Bytes).join(':')}`;
|
|
163
190
|
if (isEdge(value)) {
|
|
164
191
|
return `edge:${definitionRefIndex(value.class)}:${termColor(value.source, colors)}:${termColor(value.target, colors)}:${value.repeat?.min ?? 1}:${value.repeat?.max ?? 1}`;
|
|
165
192
|
}
|
|
@@ -177,6 +204,13 @@ function patternSignature(value, colors) {
|
|
|
177
204
|
.join('|')}`;
|
|
178
205
|
}
|
|
179
206
|
function rewriteVariables(value, ids) {
|
|
207
|
+
if ('sameNode' in value)
|
|
208
|
+
return {
|
|
209
|
+
sameNode: {
|
|
210
|
+
left: rewriteTerm(value.sameNode.left, ids),
|
|
211
|
+
right: rewriteTerm(value.sameNode.right, ids),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
180
214
|
if (isEdge(value)) {
|
|
181
215
|
return {
|
|
182
216
|
...value,
|
|
@@ -234,3 +268,8 @@ function compareUtf8Bytes(left, right) {
|
|
|
234
268
|
function isEdge(value) {
|
|
235
269
|
return 'source' in value;
|
|
236
270
|
}
|
|
271
|
+
function normalizeEquality(value) {
|
|
272
|
+
return compareUtf8Bytes(json.serialize(value.left), json.serialize(value.right)) <= 0
|
|
273
|
+
? value
|
|
274
|
+
: { left: value.right, right: value.left };
|
|
275
|
+
}
|
|
@@ -79,7 +79,16 @@ function collectPolicyExpressionRefs(expression, add) {
|
|
|
79
79
|
refs.forEach(add);
|
|
80
80
|
}
|
|
81
81
|
function collectPolicyPatternRefs(expression, add) {
|
|
82
|
+
if ('sameNode' in expression) {
|
|
83
|
+
for (const term of [expression.sameNode.left, expression.sameNode.right])
|
|
84
|
+
if (term.kind === 'ref')
|
|
85
|
+
add(term.ref);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
82
88
|
if ('source' in expression) {
|
|
89
|
+
for (const term of [expression.source, expression.target])
|
|
90
|
+
if (term.kind === 'ref')
|
|
91
|
+
add(term.ref);
|
|
83
92
|
add(expression.class);
|
|
84
93
|
return;
|
|
85
94
|
}
|
|
@@ -96,6 +105,12 @@ function collectPolicyPatternRefs(expression, add) {
|
|
|
96
105
|
children.forEach((child) => collectPolicyPatternRefs(child, add));
|
|
97
106
|
}
|
|
98
107
|
function collectPolicyCheckRefs(expression, add) {
|
|
108
|
+
if ('sameNode' in expression) {
|
|
109
|
+
for (const term of [expression.sameNode.left, expression.sameNode.right])
|
|
110
|
+
if (term.kind === 'ref')
|
|
111
|
+
add(term.ref);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
99
114
|
if ('check' in expression) {
|
|
100
115
|
add(expression.check);
|
|
101
116
|
if (expression.object.kind === 'ref')
|
|
@@ -101,6 +101,7 @@ const policyPatternNodeSchema = discriminatedUnion('kind', [
|
|
|
101
101
|
policyPatternSourceSchema,
|
|
102
102
|
policyPatternTargetSchema,
|
|
103
103
|
policyPatternVariableSchema,
|
|
104
|
+
strictObject({ kind: literal('ref'), ref: policyObjectRefSchema }),
|
|
104
105
|
]);
|
|
105
106
|
const policyVariableSelectorSchema = discriminatedUnion('kind', [
|
|
106
107
|
strictObject({ kind: literal('any') }),
|
|
@@ -115,6 +116,9 @@ const policyRepeatSchema = strictObject({
|
|
|
115
116
|
max: number().int().min(0).max(3),
|
|
116
117
|
});
|
|
117
118
|
const policyPatternExpressionSchema = lazy(() => union([
|
|
119
|
+
strictObject({
|
|
120
|
+
sameNode: strictObject({ left: policyPatternNodeSchema, right: policyPatternNodeSchema }),
|
|
121
|
+
}),
|
|
118
122
|
strictObject({
|
|
119
123
|
source: policyPatternNodeSchema,
|
|
120
124
|
class: classRefSchema,
|
|
@@ -146,6 +150,9 @@ const policyCheckObjectSchema = discriminatedUnion('kind', [
|
|
|
146
150
|
}),
|
|
147
151
|
]);
|
|
148
152
|
const policyCheckExpressionSchema = lazy(() => union([
|
|
153
|
+
strictObject({
|
|
154
|
+
sameNode: strictObject({ left: policyCheckObjectSchema, right: policyCheckObjectSchema }),
|
|
155
|
+
}),
|
|
149
156
|
strictObject({
|
|
150
157
|
check: policyRefSchema,
|
|
151
158
|
object: policyCheckObjectSchema,
|
|
@@ -16,7 +16,7 @@ export function validatePolicyBudgets(context) {
|
|
|
16
16
|
}
|
|
17
17
|
const edgeMaximum = Math.max(0, ...measure.edgeCounts);
|
|
18
18
|
if (edgeMaximum > MAX_EDGES) {
|
|
19
|
-
context.add('PL_BUDGET', pointer, `A normalized branch contains ${edgeMaximum}
|
|
19
|
+
context.add('PL_BUDGET', pointer, `A normalized branch contains ${edgeMaximum} graph predicates; the maximum is ${MAX_EDGES}.`);
|
|
20
20
|
}
|
|
21
21
|
if (measure.domains.size > MAX_DOMAINS) {
|
|
22
22
|
context.add('PL_BUDGET', pointer, `Expanded Pattern references ${measure.domains.size} Domains; the maximum is ${MAX_DOMAINS}.`);
|
|
@@ -46,11 +46,24 @@ function measurePolicy(context, name, visiting) {
|
|
|
46
46
|
return combineMeasures('allOf' in declaration ? 'allOf' : 'anyOf', children);
|
|
47
47
|
}
|
|
48
48
|
function measurePattern(pattern, ownOrigin) {
|
|
49
|
+
if ('sameNode' in pattern)
|
|
50
|
+
return {
|
|
51
|
+
depth: 1,
|
|
52
|
+
edgeCounts: [1],
|
|
53
|
+
domains: new Set([
|
|
54
|
+
ownOrigin,
|
|
55
|
+
...[pattern.sameNode.left, pattern.sameNode.right].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
|
|
56
|
+
]),
|
|
57
|
+
};
|
|
49
58
|
if (isEdge(pattern)) {
|
|
50
59
|
return {
|
|
51
60
|
depth: 1,
|
|
52
61
|
edgeCounts: [1],
|
|
53
|
-
domains: new Set([
|
|
62
|
+
domains: new Set([
|
|
63
|
+
ownOrigin,
|
|
64
|
+
pattern.class.origin,
|
|
65
|
+
...[pattern.source, pattern.target].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
|
|
66
|
+
]),
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
69
|
if ('exists' in pattern) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PolicyObjectRef } from '../../../language/policy/model/policy.js';
|
|
2
|
+
import type { PolicyValidationContext } from './context.js';
|
|
3
|
+
import type { ObjectContract } from './typing.js';
|
|
4
|
+
export declare function referenceContracts(validation: PolicyValidationContext, ref: PolicyObjectRef, pointer: string): readonly ObjectContract[] | undefined;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Key } from '../../../addressing/index.js';
|
|
2
|
+
import { definitionRefIndex } from '../../../addressing/ordering.js';
|
|
3
|
+
const formatDefinitionRef = Key.of;
|
|
4
|
+
const KERNEL_ORIGIN = 'kernel.astrale.ai';
|
|
5
|
+
const PROJECTION_CLASS = nodeClassRef('Class');
|
|
6
|
+
const PROJECTION_FUNCTION = nodeClassRef('Function');
|
|
7
|
+
const PROJECTION_POLICY = nodeClassRef('Policy');
|
|
8
|
+
const PROJECTION_VIEW = nodeClassRef('View');
|
|
9
|
+
export function referenceContracts(validation, ref, pointer) {
|
|
10
|
+
const candidate = validation.environment.candidate;
|
|
11
|
+
const local = ref.origin === candidate.origin;
|
|
12
|
+
const direct = Object.values(candidate.dependencies).some((entry) => entry.origin === ref.origin);
|
|
13
|
+
const owner = local
|
|
14
|
+
? candidate
|
|
15
|
+
: direct
|
|
16
|
+
? validation.environment.dependencies.get(ref.origin)
|
|
17
|
+
: undefined;
|
|
18
|
+
if (owner === undefined)
|
|
19
|
+
return invalidPolicyObjectRef(validation, ref, pointer);
|
|
20
|
+
switch (ref.kind) {
|
|
21
|
+
case 'class': {
|
|
22
|
+
const resolved = validation.environment.resolve(ref);
|
|
23
|
+
return resolved?.ref.kind === ref.kind
|
|
24
|
+
? [{ kind: 'exact', ref: PROJECTION_CLASS }]
|
|
25
|
+
: invalidPolicyObjectRef(validation, ref, pointer);
|
|
26
|
+
}
|
|
27
|
+
case 'function':
|
|
28
|
+
return owner.functions[ref.name] === undefined
|
|
29
|
+
? invalidPolicyObjectRef(validation, ref, pointer)
|
|
30
|
+
: [{ kind: 'exact', ref: PROJECTION_FUNCTION }];
|
|
31
|
+
case 'policy':
|
|
32
|
+
return owner.policies[ref.name] === undefined
|
|
33
|
+
? invalidPolicyObjectRef(validation, ref, pointer)
|
|
34
|
+
: [{ kind: 'exact', ref: PROJECTION_POLICY }];
|
|
35
|
+
case 'view':
|
|
36
|
+
return owner.views[ref.name] === undefined
|
|
37
|
+
? invalidPolicyObjectRef(validation, ref, pointer)
|
|
38
|
+
: [{ kind: 'exact', ref: PROJECTION_VIEW }];
|
|
39
|
+
case 'core': {
|
|
40
|
+
const node = owner.core.nodes[ref.name];
|
|
41
|
+
if (node === undefined)
|
|
42
|
+
return invalidPolicyObjectRef(validation, ref, pointer);
|
|
43
|
+
const resolved = validation.environment.resolve(node.class);
|
|
44
|
+
const analyzed = validation.analysis.definitions.get(definitionRefIndex(node.class));
|
|
45
|
+
// Core validation owns invalid Core Class diagnostics. Avoid adding Policy noise for
|
|
46
|
+
// a Node that cannot materialize in the first place.
|
|
47
|
+
if (node.class.origin !== owner.origin ||
|
|
48
|
+
resolved?.ref.kind !== 'class' ||
|
|
49
|
+
resolved.kind !== 'node' ||
|
|
50
|
+
analyzed === undefined) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return [{ kind: 'exact', ref: node.class }];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return invalidPolicyObjectRef(validation, ref, pointer);
|
|
57
|
+
}
|
|
58
|
+
function invalidPolicyObjectRef(validation, ref, pointer) {
|
|
59
|
+
validation.add('PL_REF', `${pointer}/ref`, `${formatDefinitionRef(ref)} is not an exact local or direct-dependency projected Policy object.`, formatDefinitionRef(ref));
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function nodeClassRef(name) {
|
|
63
|
+
return { origin: KERNEL_ORIGIN, kind: 'class', name };
|
|
64
|
+
}
|
|
@@ -3,6 +3,7 @@ import { policyVariableClass } from '../../../language/policy/model/policy.js';
|
|
|
3
3
|
import { normalizePolicyExpression, normalizePolicyPattern, } from '../../../language/policy/normalization/normalize.js';
|
|
4
4
|
import { MAX_REPEAT, MAX_VARIABLES } from './budgets.js';
|
|
5
5
|
import { escapePointer } from './context.js';
|
|
6
|
+
import { referenceContracts } from './objects.js';
|
|
6
7
|
export function validatePolicyReferences(context) {
|
|
7
8
|
for (const [name, declaration] of Object.entries(context.environment.candidate.policies)) {
|
|
8
9
|
const pointer = `/policies/${escapePointer(name)}`;
|
|
@@ -44,6 +45,11 @@ function validateMatch(context, name, pattern, pointer) {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
function validatePatternNode(context, pattern, pointer, scope, declarations, uses) {
|
|
48
|
+
if ('sameNode' in pattern) {
|
|
49
|
+
validateTerm(context, pattern.sameNode.left, `${pointer}/sameNode/left`, scope, uses);
|
|
50
|
+
validateTerm(context, pattern.sameNode.right, `${pointer}/sameNode/right`, scope, uses);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
47
53
|
if (isEdge(pattern)) {
|
|
48
54
|
context.resolveEdge(pattern.class, `${pointer}/class`);
|
|
49
55
|
validateTerm(context, pattern.source, `${pointer}/source`, scope, uses);
|
|
@@ -93,6 +99,8 @@ function validatePatternNode(context, pattern, pointer, scope, declarations, use
|
|
|
93
99
|
values.forEach((child, index) => validatePatternNode(context, child, `${pointer}/${key}/${index}`, scope, declarations, uses));
|
|
94
100
|
}
|
|
95
101
|
function validateTerm(context, term, pointer, scope, uses) {
|
|
102
|
+
if (term.kind === 'ref')
|
|
103
|
+
referenceContracts(context, term.ref, pointer);
|
|
96
104
|
if (term.kind === 'variable') {
|
|
97
105
|
if (!scope.has(term.id)) {
|
|
98
106
|
context.add('PL_VARIABLE_SCOPE', pointer, `Variable ${term.id} is free, escaped, or outside its defining exists scope.`);
|
|
@@ -2,6 +2,7 @@ import { definitionRefIndex } from '../../../addressing/ordering.js';
|
|
|
2
2
|
import { branchesOf, branchProduct, capBranches, targetMode, termKey, } from '../../../language/policy/analysis/branches.js';
|
|
3
3
|
import { MAX_REPEAT } from './budgets.js';
|
|
4
4
|
import { escapePointer } from './context.js';
|
|
5
|
+
import { referenceContracts } from './objects.js';
|
|
5
6
|
export class PolicyTyping {
|
|
6
7
|
context;
|
|
7
8
|
constructor(context) {
|
|
@@ -83,11 +84,15 @@ export class PolicyTyping {
|
|
|
83
84
|
const terms = new Set(use.kind === 'object' ? ['subject', 'object'] : ['subject', 'source', 'target']);
|
|
84
85
|
for (const key of branch.variables.keys())
|
|
85
86
|
terms.add(key);
|
|
87
|
+
for (const key of branch.terms.keys())
|
|
88
|
+
terms.add(key);
|
|
86
89
|
for (const edge of branch.edges) {
|
|
87
90
|
terms.add(termKey(edge.pattern.source, edge.namespace));
|
|
88
91
|
terms.add(termKey(edge.pattern.target, edge.namespace));
|
|
89
92
|
}
|
|
90
93
|
const union = new UnionFind(terms);
|
|
94
|
+
for (const [left, right] of branch.equalities)
|
|
95
|
+
union.join(left, right);
|
|
91
96
|
for (const variant of variants) {
|
|
92
97
|
for (const [left, right] of variant.equalities)
|
|
93
98
|
union.join(left, right);
|
|
@@ -136,6 +141,11 @@ export class PolicyTyping {
|
|
|
136
141
|
}
|
|
137
142
|
return candidates;
|
|
138
143
|
}
|
|
144
|
+
const referenced = branch.terms.get(term);
|
|
145
|
+
if (referenced?.kind === 'ref') {
|
|
146
|
+
const contract = referenceContracts(this.context, referenced.ref, '/policies')?.[0];
|
|
147
|
+
return contract?.kind === 'exact' ? this.#applyExact(candidates, contract.ref) : [];
|
|
148
|
+
}
|
|
139
149
|
const selector = branch.variables.get(term);
|
|
140
150
|
if (selector === undefined)
|
|
141
151
|
return [];
|
|
@@ -4,13 +4,9 @@ import { normalizePolicyChecks } from '../../../language/policy/normalization/no
|
|
|
4
4
|
import { GRAPH_NODE_PATH_SCHEMA_ID } from '../../../language/value/syntax/node-path/schema.js';
|
|
5
5
|
import { MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, measureChecks } from './budgets.js';
|
|
6
6
|
import { escapePointer } from './context.js';
|
|
7
|
+
import { referenceContracts } from './objects.js';
|
|
7
8
|
import { contractOfNodeDefinition } from './typing.js';
|
|
8
9
|
const formatDefinitionRef = Key.of;
|
|
9
|
-
const KERNEL_ORIGIN = 'kernel.astrale.ai';
|
|
10
|
-
const PROJECTION_CLASS = nodeClassRef('Class');
|
|
11
|
-
const PROJECTION_FUNCTION = nodeClassRef('Function');
|
|
12
|
-
const PROJECTION_POLICY = nodeClassRef('Policy');
|
|
13
|
-
const PROJECTION_VIEW = nodeClassRef('View');
|
|
14
10
|
export function validateReadUses(context, typing) {
|
|
15
11
|
validateClassPolicyUses(context, typing, 'read');
|
|
16
12
|
}
|
|
@@ -95,6 +91,18 @@ function validateChecks(validation, typing, expression, context) {
|
|
|
95
91
|
visitChecks(validation, typing, normalized, context, context.pointer);
|
|
96
92
|
}
|
|
97
93
|
function visitChecks(validation, typing, expression, context, pointer) {
|
|
94
|
+
if ('sameNode' in expression) {
|
|
95
|
+
for (const [side, object] of Object.entries(expression.sameNode)) {
|
|
96
|
+
const objectPointer = `${pointer}/sameNode/${side}`;
|
|
97
|
+
if (object.kind === 'self')
|
|
98
|
+
selfContracts(validation, context, objectPointer);
|
|
99
|
+
else if (object.kind === 'input')
|
|
100
|
+
inputContracts(validation, context, object.field, objectPointer);
|
|
101
|
+
else
|
|
102
|
+
referenceContracts(validation, object.ref, objectPointer);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
98
106
|
if ('check' in expression) {
|
|
99
107
|
if (!validation.resolvePolicy(expression.check, `${pointer}/check`))
|
|
100
108
|
return;
|
|
@@ -120,59 +128,6 @@ function visitChecks(validation, typing, expression, context, pointer) {
|
|
|
120
128
|
}
|
|
121
129
|
values.forEach((child, index) => visitChecks(validation, typing, child, context, `${pointer}/${key}/${index}`));
|
|
122
130
|
}
|
|
123
|
-
function referenceContracts(validation, ref, pointer) {
|
|
124
|
-
const candidate = validation.environment.candidate;
|
|
125
|
-
const local = ref.origin === candidate.origin;
|
|
126
|
-
const direct = Object.values(candidate.dependencies).some((entry) => entry.origin === ref.origin);
|
|
127
|
-
const owner = local
|
|
128
|
-
? candidate
|
|
129
|
-
: direct
|
|
130
|
-
? validation.environment.dependencies.get(ref.origin)
|
|
131
|
-
: undefined;
|
|
132
|
-
if (owner === undefined)
|
|
133
|
-
return invalidPolicyObjectRef(validation, ref, pointer);
|
|
134
|
-
switch (ref.kind) {
|
|
135
|
-
case 'class': {
|
|
136
|
-
const resolved = validation.environment.resolve(ref);
|
|
137
|
-
return resolved?.ref.kind === ref.kind
|
|
138
|
-
? [{ kind: 'exact', ref: PROJECTION_CLASS }]
|
|
139
|
-
: invalidPolicyObjectRef(validation, ref, pointer);
|
|
140
|
-
}
|
|
141
|
-
case 'function':
|
|
142
|
-
return owner.functions[ref.name] === undefined
|
|
143
|
-
? invalidPolicyObjectRef(validation, ref, pointer)
|
|
144
|
-
: [{ kind: 'exact', ref: PROJECTION_FUNCTION }];
|
|
145
|
-
case 'policy':
|
|
146
|
-
return owner.policies[ref.name] === undefined
|
|
147
|
-
? invalidPolicyObjectRef(validation, ref, pointer)
|
|
148
|
-
: [{ kind: 'exact', ref: PROJECTION_POLICY }];
|
|
149
|
-
case 'view':
|
|
150
|
-
return owner.views[ref.name] === undefined
|
|
151
|
-
? invalidPolicyObjectRef(validation, ref, pointer)
|
|
152
|
-
: [{ kind: 'exact', ref: PROJECTION_VIEW }];
|
|
153
|
-
case 'core': {
|
|
154
|
-
const node = owner.core.nodes[ref.name];
|
|
155
|
-
if (node === undefined)
|
|
156
|
-
return invalidPolicyObjectRef(validation, ref, pointer);
|
|
157
|
-
const resolved = validation.environment.resolve(node.class);
|
|
158
|
-
const analyzed = validation.analysis.definitions.get(definitionRefIndex(node.class));
|
|
159
|
-
// Core validation owns invalid Core Class diagnostics. Avoid adding Policy noise for
|
|
160
|
-
// a Node that cannot materialize in the first place.
|
|
161
|
-
if (node.class.origin !== owner.origin ||
|
|
162
|
-
resolved?.ref.kind !== 'class' ||
|
|
163
|
-
resolved.kind !== 'node' ||
|
|
164
|
-
analyzed === undefined) {
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
return [contractOfNodeDefinition(node.class)];
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return invalidPolicyObjectRef(validation, ref, pointer);
|
|
171
|
-
}
|
|
172
|
-
function invalidPolicyObjectRef(validation, ref, pointer) {
|
|
173
|
-
validation.add('PL_REF', `${pointer}/ref`, `${formatDefinitionRef(ref)} is not an exact local or direct-dependency projected Policy object.`, formatDefinitionRef(ref));
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
131
|
function selfContracts(validation, context, pointer) {
|
|
177
132
|
if (!context.instanceMethod || context.receiver === undefined) {
|
|
178
133
|
validation.add('PL_USE', pointer, 'self is valid only for a Node instance Method.');
|
|
@@ -238,6 +193,3 @@ function checkOperands(value) {
|
|
|
238
193
|
? { key: 'allOf', values: value.allOf }
|
|
239
194
|
: { key: 'anyOf', values: value.anyOf };
|
|
240
195
|
}
|
|
241
|
-
function nodeClassRef(name) {
|
|
242
|
-
return { origin: KERNEL_ORIGIN, kind: 'class', name };
|
|
243
|
-
}
|