Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ace783a59 |
Generated
+6
-6
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@arbiter/evidence-dsl",
|
"name": "@arbiter/evidence-dsl",
|
||||||
"version": "1.4.0",
|
"version": "1.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@arbiter/evidence-dsl",
|
"name": "@arbiter/evidence-dsl",
|
||||||
"version": "1.4.0",
|
"version": "1.6.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arbiter/core": "^1.0.3"
|
"@arbiter/core": "^1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rigor/core": "^3.1.0",
|
"@rigor/core": "^3.1.0",
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@arbiter/core": {
|
"node_modules/@arbiter/core": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"resolved": "https://hub.kl1.tenere.ai/api/packages/Arbiter/npm/%40arbiter%2Fcore/-/1.0.3/core-1.0.3.tgz",
|
"resolved": "https://hub.kl1.tenere.ai/api/packages/Arbiter/npm/%40arbiter%2Fcore/-/1.0.4/core-1.0.4.tgz",
|
||||||
"integrity": "sha512-MCXxyeWBoYjEJMrdO8N8q9uEdX7JgDvwRH39D+8x65zFz+JCNWIQ8H4DgsP2rgF+yzv/cdVH4BX9PfFt6i0ftQ==",
|
"integrity": "sha512-1zXy3mZACjwELptsV8QpYvZJmMU7BhUQ4FsKNfp3/oJDwjfMqAImcAF3mJ+HNZpQnLQXScmELTwow5VduvN27g==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tenere/pltc-core": "^0.6.3",
|
"@tenere/pltc-core": "^0.6.3",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@arbiter/evidence-dsl",
|
"name": "@arbiter/evidence-dsl",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"description": "Evidence DSL v2 compiler: translates the natural Evidence DSL (ADR-000) into @arbiter/core relation configurations.",
|
"description": "Evidence DSL v2 compiler: translates the natural Evidence DSL (ADR-000) into @arbiter/core relation configurations.",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"generate:parser": "node scripts/generate-parser.js"
|
"generate:parser": "node scripts/generate-parser.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arbiter/core": "^1.0.3"
|
"@arbiter/core": "^1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rigor/core": "^3.1.0",
|
"@rigor/core": "^3.1.0",
|
||||||
|
|||||||
@@ -486,15 +486,22 @@ export class RuleGenerator {
|
|||||||
reverse: false
|
reverse: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subject-scoped (unary) call: the predicate call's args omit the
|
// Unary predicate calls check the relation as a self-edge on the call's
|
||||||
// evidence's object parameter (user_risk(user, 1) inside a binary
|
// subject entity (the graph stores unary facts as self-edges). The subject
|
||||||
// evidence) → check the relation on the subject itself.
|
// entity may be the evidence's SUBJECT or its OBJECT parameter — mark the
|
||||||
|
// matching rewrite flag.
|
||||||
const evidenceParams = (evidence && evidence.params) || [];
|
const evidenceParams = (evidence && evidence.params) || [];
|
||||||
const objectVar = evidenceParams[1] && evidenceParams[1].name;
|
const objectVar = evidenceParams[1] && evidenceParams[1].name;
|
||||||
const argName = a => a && (a.name !== undefined ? a.name : a.value);
|
const argName = a => a && (a.name !== undefined ? a.name : a.value);
|
||||||
if (objectVar !== undefined && !(predicate.arguments || []).some(a => argName(a) === objectVar)) {
|
const args = predicate.arguments || [];
|
||||||
|
if (objectVar !== undefined) {
|
||||||
|
const hasObjectArg = args.some(a => argName(a) === objectVar);
|
||||||
|
if (args.length === 1 && argName(args[0]) === objectVar) {
|
||||||
|
rule._subjectIsObject = true;
|
||||||
|
} else if (!hasObjectArg) {
|
||||||
rule._subjectAsObject = true;
|
rule._subjectAsObject = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rule;
|
return rule;
|
||||||
}
|
}
|
||||||
@@ -1082,7 +1089,6 @@ export class RuleGenerator {
|
|||||||
const out = [];
|
const out = [];
|
||||||
for (let idx = 0; idx < steps.length; idx++) {
|
for (let idx = 0; idx < steps.length; idx++) {
|
||||||
const step = steps[idx];
|
const step = steps[idx];
|
||||||
const isLast = idx === steps.length - 1;
|
|
||||||
const stepName = typeof step === 'string' ? step : step.relation;
|
const stepName = typeof step === 'string' ? step : step.relation;
|
||||||
if (stepName && this.evidenceNames.has(stepName)) {
|
if (stepName && this.evidenceNames.has(stepName)) {
|
||||||
if (stack.has(stepName)) {
|
if (stack.has(stepName)) {
|
||||||
@@ -1105,17 +1111,13 @@ export class RuleGenerator {
|
|||||||
out.push(...this._expandChainSteps(resolved.steps, refStack));
|
out.push(...this._expandChainSteps(resolved.steps, refStack));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isLast) {
|
// Condition step: inline the evidence's config as a rule step. As the
|
||||||
// Condition-gated final hop: inline the evidence's config as a
|
// FINAL step the engine verifies it at (intermediate, object); as an
|
||||||
// rule step the engine evaluates at (intermediate, object).
|
// INTERMEDIATE step the engine EXPANDS it from the current node
|
||||||
|
// (rule-based reachability) and continues from each discovered node.
|
||||||
out.push({ rule: this._deepCloneRule(resolved), conditionStep: true });
|
out.push({ rule: this._deepCloneRule(resolved), conditionStep: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.errors.push(`Chain step '${stepName}' references an evidence with type '${resolved.type || 'logical'}'. ` +
|
|
||||||
'Only the final chain step may reference a defeasible/logical evidence (a condition-gated hop); intermediate steps must be edge traversals.');
|
|
||||||
out.push(step);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
out.push(step);
|
out.push(step);
|
||||||
}
|
}
|
||||||
@@ -1280,16 +1282,23 @@ export class RuleGenerator {
|
|||||||
reverse: false
|
reverse: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subject-scoped (unary) predicate call: the call's variable args omit the
|
// Unary predicate calls check the relation as a self-edge on the call's
|
||||||
// evidence's object parameter (banned(user) inside can_open(user, doc)).
|
// subject entity (the graph stores unary facts as self-edges). The subject
|
||||||
// Mark _subjectAsObject so the engine checks the relation on the subject
|
// entity may be the evidence's SUBJECT or its OBJECT parameter:
|
||||||
// itself — the unary fact's self-edge — instead of (subject, object).
|
// banned(user) in can_open(user, doc) -> self-edge on the user
|
||||||
|
// trusted(other) in peer_trusted(user, other) -> self-edge on the other
|
||||||
|
// Mark _subjectAsObject (subject-as-object on the subject entity) or
|
||||||
|
// _subjectIsObject (the subject entity IS the object parameter) so the
|
||||||
|
// engine rewrites the pair accordingly.
|
||||||
const evidenceParams = (evidence && evidence.params) || [];
|
const evidenceParams = (evidence && evidence.params) || [];
|
||||||
const objectVar = evidenceParams[1] && evidenceParams[1].name;
|
const objectVar = evidenceParams[1] && evidenceParams[1].name;
|
||||||
|
const argName = a => a && (a.name !== undefined ? a.name : a.value);
|
||||||
if (objectVar !== undefined) {
|
if (objectVar !== undefined) {
|
||||||
const hasObjectArg = (expression.args || []).some(a =>
|
const args = expression.args || [];
|
||||||
a && a.type === 'Variable' && a.name === objectVar);
|
const hasObjectArg = args.some(a => a && a.type === 'Variable' && a.name === objectVar);
|
||||||
if (!hasObjectArg) {
|
if (args.length === 1 && argName(args[0]) === objectVar) {
|
||||||
|
rule._subjectIsObject = true;
|
||||||
|
} else if (!hasObjectArg) {
|
||||||
rule._subjectAsObject = true;
|
rule._subjectAsObject = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,4 +98,32 @@ describe('Chain condition step (logical evidence as final hop)', () => {
|
|||||||
// max over paths: min(0.5,0.7)=0.5, min(1.0,0.8)=0.8 -> 0.8
|
// max over paths: min(0.5,0.7)=0.5, min(1.0,0.8)=0.8 -> 0.8
|
||||||
assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0.8);
|
assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0.8);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('expands an INTERMEDIATE condition step via rule-based reachability', () => {
|
||||||
|
const { arb, result } = compile(`
|
||||||
|
definition Employee { id: string }
|
||||||
|
definition Doc { id: string }
|
||||||
|
fact peer(user: Employee, other: Employee)
|
||||||
|
fact trusted(other: Employee)
|
||||||
|
fact can_read(user: Employee, doc: Doc)
|
||||||
|
evidence peer_trusted(user: Employee, other: Employee) { WHEN peer(user, other) UNLESS trusted(other) }
|
||||||
|
evidence can_access(user: Employee, doc: Doc) { peer_trusted(user, *p) { can_read(p, doc) } }
|
||||||
|
`);
|
||||||
|
assert.ok(result.success, JSON.stringify(result.errors));
|
||||||
|
const steps = arb.relationConfigs.get('can_access').steps;
|
||||||
|
assert.equal(steps[0].conditionStep, true);
|
||||||
|
assert.equal(steps[0].rule.type, 'logical');
|
||||||
|
|
||||||
|
arb.addNode('u:1', 'Employee'); arb.addNode('p:1', 'Employee'); arb.addNode('p:2', 'Employee'); arb.addNode('doc:9', 'Doc');
|
||||||
|
arb.addRelation('u:1', 'peer', 'p:1', { possibility: 1.0 });
|
||||||
|
arb.addRelation('u:1', 'peer', 'p:2', { possibility: 1.0 });
|
||||||
|
arb.addRelation('p:1', 'trusted', 'p:1', { possibility: 1.0 }); // p:1 filtered
|
||||||
|
arb.addRelation('p:1', 'can_read', 'doc:9', { possibility: 0.9 });
|
||||||
|
arb.addRelation('p:2', 'can_read', 'doc:9', { possibility: 0.7 });
|
||||||
|
// only untrusted peer p:2 survives the intermediate condition -> 0.7
|
||||||
|
assert.equal(arb.check('u:1', 'can_access', 'doc:9').possibility, 0.7);
|
||||||
|
// trusting p:2 too removes all intermediates -> 0
|
||||||
|
arb.addRelation('p:2', 'trusted', 'p:2', { possibility: 1.0 });
|
||||||
|
assert.equal(arb.check('u:1', 'can_access', 'doc:9').possibility, 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ const FACTS = `
|
|||||||
fact owner(group: Group, doc: Doc)
|
fact owner(group: Group, doc: Doc)
|
||||||
fact granted(user: Employee, doc: Doc)
|
fact granted(user: Employee, doc: Doc)
|
||||||
fact group_perm(group: Group, doc: Doc)
|
fact group_perm(group: Group, doc: Doc)
|
||||||
fact banned(user: Employee)
|
|
||||||
fact group_banned(group: Group)
|
fact group_banned(group: Group)
|
||||||
|
fact banned(user: Employee)
|
||||||
|
fact peer(user: Employee, other: Employee)
|
||||||
|
fact trusted(other: Employee)
|
||||||
|
fact doc_read(user: Employee, doc: Doc)
|
||||||
fact mfa(user: Employee)
|
fact mfa(user: Employee)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -142,6 +145,20 @@ function buildProgram(kind, ps) {
|
|||||||
oracle = Math.min(pm, pv * (1 - pb));
|
oracle = Math.min(pm, pv * (1 - pb));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'chain_intermediate_condition': {
|
||||||
|
// peer_trusted (a defeasible evidence) as an INTERMEDIATE chain step:
|
||||||
|
// the engine expands it from the source (peer edges filtered by the
|
||||||
|
// trusted defeater) then continues to can_read. Oracle = min of the
|
||||||
|
// surviving peer leg and the read leg.
|
||||||
|
const [pp, pt, pr] = ps;
|
||||||
|
evidence = `evidence peer_trusted(user: Employee, other: Employee) { WHEN peer(user, other) UNLESS trusted(other) }
|
||||||
|
evidence can_via(user: Employee, doc: Doc) { peer_trusted(user, *p) { doc_read(p, doc) } }`;
|
||||||
|
edges.push({ src: 'u:1', relation: 'peer', dst: 'p:1', possibility: pp });
|
||||||
|
edges.push({ src: 'p:1', relation: 'trusted', dst: 'p:1', possibility: pt });
|
||||||
|
edges.push({ src: 'p:1', relation: 'doc_read', dst: 'doc:9', possibility: pr });
|
||||||
|
oracle = Math.min(pp * (1 - pt), pr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown construct: ${kind}`);
|
throw new Error(`unknown construct: ${kind}`);
|
||||||
}
|
}
|
||||||
@@ -162,6 +179,10 @@ function runCheck({ kind, ps }) {
|
|||||||
arbiter.addNode('u:1', 'Employee');
|
arbiter.addNode('u:1', 'Employee');
|
||||||
arbiter.addNode('g:1', 'Group');
|
arbiter.addNode('g:1', 'Group');
|
||||||
arbiter.addNode('doc:9', 'Doc');
|
arbiter.addNode('doc:9', 'Doc');
|
||||||
|
for (const e of edges) {
|
||||||
|
arbiter.addNode(e.src, e.dst === 'doc:9' ? 'Doc' : 'Employee');
|
||||||
|
arbiter.addNode(e.dst, e.dst === 'doc:9' ? 'Doc' : 'Employee');
|
||||||
|
}
|
||||||
const compiler = new DSLCompiler(arbiter);
|
const compiler = new DSLCompiler(arbiter);
|
||||||
const compiled = compiler.compile(dsl, 'oracle');
|
const compiled = compiler.compile(dsl, 'oracle');
|
||||||
if (!compiled.success) {
|
if (!compiled.success) {
|
||||||
@@ -178,7 +199,7 @@ function runCheck({ kind, ps }) {
|
|||||||
|
|
||||||
const CONSTRUCTS = ['direct', 'chain', 'tuple_to_userset', 'fusion_min', 'fusion_max',
|
const CONSTRUCTS = ['direct', 'chain', 'tuple_to_userset', 'fusion_min', 'fusion_max',
|
||||||
'when_unless', 'never_always', 'requires_when', 'composition', 'chain_step_composition',
|
'when_unless', 'never_always', 'requires_when', 'composition', 'chain_step_composition',
|
||||||
'chain_condition_step'];
|
'chain_condition_step', 'chain_intermediate_condition'];
|
||||||
|
|
||||||
describe('DSL generative oracle parity (rigor)', () => {
|
describe('DSL generative oracle parity (rigor)', () => {
|
||||||
it('generated legal DSL compiles and every check matches the oracle', async () => {
|
it('generated legal DSL compiles and every check matches the oracle', async () => {
|
||||||
@@ -220,6 +241,10 @@ describe('DSL generative oracle parity (rigor)', () => {
|
|||||||
arbiter.addNode('u:1', 'Employee');
|
arbiter.addNode('u:1', 'Employee');
|
||||||
arbiter.addNode('g:1', 'Group');
|
arbiter.addNode('g:1', 'Group');
|
||||||
arbiter.addNode('doc:9', 'Doc');
|
arbiter.addNode('doc:9', 'Doc');
|
||||||
|
for (const e of edges) {
|
||||||
|
arbiter.addNode(e.src, e.dst === 'doc:9' ? 'Doc' : 'Employee');
|
||||||
|
arbiter.addNode(e.dst, e.dst === 'doc:9' ? 'Doc' : 'Employee');
|
||||||
|
}
|
||||||
const compiled = new DSLCompiler(arbiter).compile(dsl, 'sweep');
|
const compiled = new DSLCompiler(arbiter).compile(dsl, 'sweep');
|
||||||
assert.ok(compiled.success, `${kind} compile failed: ${(compiled.errors || []).join('; ')}`);
|
assert.ok(compiled.success, `${kind} compile failed: ${(compiled.errors || []).join('; ')}`);
|
||||||
for (const e of edges) arbiter.addRelation(e.src, e.relation, e.dst, { possibility: e.possibility });
|
for (const e of edges) arbiter.addRelation(e.src, e.relation, e.dst, { possibility: e.possibility });
|
||||||
|
|||||||
Reference in New Issue
Block a user