initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from 'node:test';
|
||||
import { Arbiter } from '../../src/index.js';
|
||||
import { OWAFusion } from '../../src/utils/OWAFusion.js';
|
||||
|
||||
const EPS = 1e-6;
|
||||
|
||||
function approxEqual(actual, expected, epsilon = EPS) {
|
||||
assert.ok(Math.abs(actual - expected) <= epsilon, `expected ${expected} but got ${actual}`);
|
||||
}
|
||||
|
||||
function addValueRelation(arbiter, src, relation, dst, value, possibility, reliability) {
|
||||
arbiter.addRelation(src, relation, dst, { value, possibility, reliability });
|
||||
arbiter.setRelationConfig(relation, { type: 'direct' });
|
||||
}
|
||||
|
||||
function buildComparatorRule(leftRelations, rightRelations, weights) {
|
||||
return {
|
||||
type: 'relational_comparator',
|
||||
comparator: '>=',
|
||||
fallbackBehavior: 'deny',
|
||||
left: {
|
||||
rule: {
|
||||
union: {
|
||||
rules: leftRelations.map((relation) => ({ type: 'direct', relation })),
|
||||
aggregator: 'owa',
|
||||
owaWeights: weights
|
||||
}
|
||||
},
|
||||
extractValue: true,
|
||||
aggregator: 'owa',
|
||||
owaWeights: weights
|
||||
},
|
||||
right: {
|
||||
rule: {
|
||||
union: {
|
||||
rules: rightRelations.map((relation) => ({ type: 'direct', relation })),
|
||||
aggregator: 'owa',
|
||||
owaWeights: weights
|
||||
}
|
||||
},
|
||||
extractValue: true,
|
||||
aggregator: 'owa',
|
||||
owaWeights: weights,
|
||||
evaluateFrom: 'object'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fuseTriples(values, possibilities, reliabilities, weights) {
|
||||
const metas = values.map(() => ({}));
|
||||
return OWAFusion.fuseTriplesWithMeta(values, possibilities, reliabilities, metas, weights, 'owa');
|
||||
}
|
||||
|
||||
function computeComparatorOutcome(left, right, weights) {
|
||||
const fusedLeft = fuseTriples(left.values, left.possibilities, left.reliabilities, weights);
|
||||
const fusedRight = fuseTriples(right.values, right.possibilities, right.reliabilities, weights);
|
||||
const comparison = fusedLeft.value >= fusedRight.value ? 1 : 0;
|
||||
const averageOperandPossibility = (fusedLeft.possibility + fusedRight.possibility) / 2;
|
||||
const confidenceWeight = Math.min(averageOperandPossibility * 2, 1.0);
|
||||
return {
|
||||
possibility: comparison * confidenceWeight,
|
||||
reliability: Math.min(fusedLeft.reliability, fusedRight.reliability)
|
||||
};
|
||||
}
|
||||
|
||||
describe('Relational comparator uncertainty', () => {
|
||||
test('OWA value aggregation drives comparator possibility', () => {
|
||||
const arbiter = new Arbiter();
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('doc:1', 'doc');
|
||||
|
||||
const weights = [0.6, 0.3, 0.1];
|
||||
const left = {
|
||||
relations: ['risk_low', 'risk_mid', 'risk_high'],
|
||||
values: [10, 20, 40],
|
||||
possibilities: [0.9, 0.6, 0.4],
|
||||
reliabilities: [0.8, 0.9, 0.7]
|
||||
};
|
||||
const right = {
|
||||
relations: ['limit_low', 'limit_mid', 'limit_high'],
|
||||
values: [15, 25, 30],
|
||||
possibilities: [0.8, 0.5, 0.9],
|
||||
reliabilities: [0.9, 0.8, 0.95]
|
||||
};
|
||||
|
||||
left.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'user:alice', relation, 'doc:1', left.values[idx], left.possibilities[idx], left.reliabilities[idx]);
|
||||
});
|
||||
right.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'doc:1', relation, 'doc:1', right.values[idx], right.possibilities[idx], right.reliabilities[idx]);
|
||||
});
|
||||
|
||||
arbiter.setRelationConfig('risk_ok', buildComparatorRule(left.relations, right.relations, weights));
|
||||
|
||||
const expected = computeComparatorOutcome(left, right, weights);
|
||||
const result = arbiter.check('user:alice', 'risk_ok', 'doc:1', { fastPath: false, explain: true });
|
||||
|
||||
approxEqual(result.possibility, expected.possibility);
|
||||
approxEqual(result.reliability, expected.reliability);
|
||||
});
|
||||
|
||||
test('low operand confidence scales comparator output', () => {
|
||||
const arbiter = new Arbiter();
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('doc:1', 'doc');
|
||||
|
||||
const weights = [0.5, 0.3, 0.2];
|
||||
const left = {
|
||||
relations: ['risk_a', 'risk_b', 'risk_c'],
|
||||
values: [50, 30, 10],
|
||||
possibilities: [0.1, 0.1, 0.1],
|
||||
reliabilities: [0.9, 0.9, 0.9]
|
||||
};
|
||||
const right = {
|
||||
relations: ['limit_a', 'limit_b', 'limit_c'],
|
||||
values: [20, 15, 5],
|
||||
possibilities: [0.1, 0.1, 0.1],
|
||||
reliabilities: [0.9, 0.9, 0.9]
|
||||
};
|
||||
|
||||
left.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'user:alice', relation, 'doc:1', left.values[idx], left.possibilities[idx], left.reliabilities[idx]);
|
||||
});
|
||||
right.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'doc:1', relation, 'doc:1', right.values[idx], right.possibilities[idx], right.reliabilities[idx]);
|
||||
});
|
||||
|
||||
arbiter.setRelationConfig('risk_ok', buildComparatorRule(left.relations, right.relations, weights));
|
||||
|
||||
const expected = computeComparatorOutcome(left, right, weights);
|
||||
const result = arbiter.check('user:alice', 'risk_ok', 'doc:1', { fastPath: false, explain: true });
|
||||
|
||||
approxEqual(result.possibility, expected.possibility);
|
||||
approxEqual(result.reliability, expected.reliability);
|
||||
assert.ok(result.possibility < 1, 'expected confidence scaling to reduce possibility');
|
||||
});
|
||||
|
||||
test('OWA union aggregates comparator results with uncertainty', () => {
|
||||
const arbiter = new Arbiter();
|
||||
arbiter.addNode('user:alice', 'user');
|
||||
arbiter.addNode('doc:1', 'doc');
|
||||
|
||||
const weights = [0.6, 0.3, 0.1];
|
||||
const unionWeights = [0.7, 0.3];
|
||||
|
||||
const leftA = {
|
||||
relations: ['risk_a1', 'risk_a2', 'risk_a3'],
|
||||
values: [12, 18, 25],
|
||||
possibilities: [0.9, 0.7, 0.5],
|
||||
reliabilities: [0.9, 0.8, 0.7]
|
||||
};
|
||||
const rightA = {
|
||||
relations: ['limit_a1', 'limit_a2', 'limit_a3'],
|
||||
values: [10, 15, 20],
|
||||
possibilities: [0.8, 0.6, 0.7],
|
||||
reliabilities: [0.9, 0.9, 0.8]
|
||||
};
|
||||
const leftB = {
|
||||
relations: ['risk_b1', 'risk_b2', 'risk_b3'],
|
||||
values: [8, 9, 11],
|
||||
possibilities: [0.4, 0.5, 0.6],
|
||||
reliabilities: [0.8, 0.8, 0.8]
|
||||
};
|
||||
const rightB = {
|
||||
relations: ['limit_b1', 'limit_b2', 'limit_b3'],
|
||||
values: [9, 10, 12],
|
||||
possibilities: [0.7, 0.6, 0.5],
|
||||
reliabilities: [0.9, 0.9, 0.9]
|
||||
};
|
||||
|
||||
leftA.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'user:alice', relation, 'doc:1', leftA.values[idx], leftA.possibilities[idx], leftA.reliabilities[idx]);
|
||||
});
|
||||
rightA.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'doc:1', relation, 'doc:1', rightA.values[idx], rightA.possibilities[idx], rightA.reliabilities[idx]);
|
||||
});
|
||||
leftB.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'user:alice', relation, 'doc:1', leftB.values[idx], leftB.possibilities[idx], leftB.reliabilities[idx]);
|
||||
});
|
||||
rightB.relations.forEach((relation, idx) => {
|
||||
addValueRelation(arbiter, 'doc:1', relation, 'doc:1', rightB.values[idx], rightB.possibilities[idx], rightB.reliabilities[idx]);
|
||||
});
|
||||
|
||||
const comparatorA = buildComparatorRule(leftA.relations, rightA.relations, weights);
|
||||
const comparatorB = buildComparatorRule(leftB.relations, rightB.relations, weights);
|
||||
|
||||
arbiter.setRelationConfig('risk_union', {
|
||||
union: {
|
||||
rules: [comparatorA, comparatorB],
|
||||
aggregator: 'owa',
|
||||
owaWeights: unionWeights
|
||||
}
|
||||
});
|
||||
|
||||
const outcomeA = computeComparatorOutcome(leftA, rightA, weights);
|
||||
const outcomeB = computeComparatorOutcome(leftB, rightB, weights);
|
||||
const expectedUnion = OWAFusion.fuseWithMeta(
|
||||
[outcomeA.possibility, outcomeB.possibility],
|
||||
[{}, {}],
|
||||
unionWeights,
|
||||
'owa',
|
||||
true
|
||||
).value;
|
||||
|
||||
const result = arbiter.check('user:alice', 'risk_union', 'doc:1', { fastPath: false });
|
||||
approxEqual(result.possibility, expectedUnion);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user