|
Auditors
Ka-Ping Yee (ping zesty ca)
|
I'm going to try to implement auditors for E.
The following describes "Non-Gozarian Auditing Protocol 2" (NGAP2).
audit(ast, env) -> booleanwhich gets called when the interpreter loads an object expression with a declared auditor.
audited(auditor, instance) -> booleanwhich is available in the universal scope; a guard can use it in its 'adapt' method if it wants to.
adapt(instance, ejector) -> conforming-instancewhich gets called automatically whenever a value is passed into a formal parameter or slot with a declared guard. (This is exactly the current 'coerce' method; i explained previously why i prefer the name 'adapt'.)
conform(guard) -> allegedly-conforming-instanceIt is up to the guard to decide whether, and when, to call 'conform' during 'adapt'. E provides a Miranda implementation for 'conform' that simply returns the object itself.
getSecret(brand) -> sealed-boxThe semantics of the contents of the box are entirely up to the object and the brand. If no secret associated with the given brand is available, an exception is thrown. Since this kind of dispatch is done only at the request of other objects, the use of the name 'getSecret' is only a convention, though it may become more entrenched if E introduces shorthand syntax for brand dispatch. It may also be more entrenched if E provides a Miranda implementation for 'getSecret' that always throws a standard "unknown brand" exception.
Definition:
A conformance check is Gozarian if the object can tell whether the check occurred.
The getOptMeta protocol is strictly Gozarian; the new NGAP2 (non-Gozarian auditing protocol 2) allows for both Gozarian and non-Gozarian guards.
Suppose:
This reduces the number of commonly-seen names per type to two; it doesn't seem that the ability to audit is so special that it needs to be separated from the ability to guard.
Here's an example. The following is what a simple interface and implementation might look like in E 0.8.16, based on my understanding of the release notes on "implements":
interface Operator guards OperatorAuditor {
to op(a : integer, b : integer) : integer
}
def AdderMaker() : Operator {
def adder implements OperatorAuditor {
to op(a : integer, b : integer) : integer { a + b }
}
}
def opuser(op : Operator, a : integer, b : integer) {
op op(a, b)
}
Assuming that we standardize on 'OperatorAuditor' so the author of the implementation doesn't have to read the interface in order to find out whether to use 'OperatorAuditor' or 'OperatorStamp', there are still five places where someone has to correctly choose whether to say 'Operator' or 'OperatorAuditor'.
With my hypothetical changes, this becomes the following, which i think is easier to read and understand:
interface operator {
to op(a : integer, b : integer) : integer
}
def Adder() {
def self : operator {
to op(a : integer, b : integer) : integer { a + b }
}
}
def opuser(op : operator, a : integer, b : integer) {
op op(a, b)
}
We can tell from the position of ":" in an object expression that it requests auditing; so we don't need to introduce another operator like "::" or "implements" -- we can just use ":".
Also, it's easy to see that Adder() returns something conforming to the 'operator' guard, so we don't have to repeat ": operator" after Adder() as well.
I see two main attributes to consider:
Dean Tribble wrote: > In the protocol you described, the guard authority includes the stamp > authority, so the stamp authority cannot be separately closely held.
In this taxonomy, i don't need to separate the concept of the stamp from the auditor; attribute #2 is analogous to "stamp closely held" in your description. If the auditor is discriminating, there is no concern; if the auditor is non-discriminating, then we will usually want the auditor to be closely held. As long as we distinguish between the auditor and the guard, i don't think it's necessary to also separate out the stamp.
I think this is a perspective that retains enough distinctions to be useful, while remaining fairly simple on the surface, and offering the simple single-object conceptual model for many forseeable use cases. The simplicity is good because it makes writing and using auditors easier to get right.
The only thing i can see that you do lose is the ability to directly provide multiple auditors that share the same stamp. I would suggest that this is probably a useful restriction. If it's really necessary to accept verification from one of a set of acceptable auditors, this can be implemented in a smarter guard that checks more than one stamp.
Since a hypothetical multiple-auditor problem can be solved either by (a) putting complexity in the auditors, in a world where auditors and stamps are separate, or by (b) putting complexity in the guard, my design intuition tells me that it's better to restrict the complexity to one place by eliminating option (a) than to leave both options open (a.k.a. There's Only One Way To Do It). Option (b) is better than (a) because (a) adds a little complexity to all auditors, whereas (b) adds a little complexity just to the special cases where you need to accept multiple auditors (and i don't even know how often that will turn up, if it ever does).
Here are some examples to illustrate the taxonomy:
auditor is discriminating, closely held
# public: guard
# private: auditor
def auditor {
to audit(ast, env) : boolean {
if (...inspect...) { true }
}
}
def guard {
to adapt(object) : any {
if (audited(auditor, object)) { object }
else { ...problem... }
}
}
def object : auditor {
...
}
def user(object : guard) {
...
}
auditor is non-discriminating, closely held
(the "interface declaration" or "code signing" use case: needs two objects)
# public: guard
# private: auditor
def auditor {
to audit(ast, env) : boolean { true }
}
def guard {
to adapt(object) : any {
if (audited(auditor, object)) { object }
else { ...problem... }
}
}
def object : auditor {
...
}
def user(object : guard) {
...
}
auditor is discriminating, not closely held
(the "semantic verification" use case: needs just one object)
# public: auditor
def auditor {
to audit(ast, env) : boolean {
if (...inspect...) { true }
}
to adapt(object) : any {
if (audited(auditor, object)) { object }
else { ...problem... }
}
}
def object : auditor {
...
}
def user(object : auditor) {
...
}
auditor is non-discriminating, not closely held
# public: auditor
def auditor {
to audit(ast, env) : boolean { true }
to adapt(object) : any {
if (audited(auditor, object)) { object }
else { ...problem... }
}
}
def object : auditor {
...
}
def user(object : auditor) {
...
}
two auditors -- one discriminating, not closely held;
one non-discriminating, closely held
(this requires three objects)
# public: auditor1, guard
# private: auditor2
def auditor1 {
to audit(ast, env) : boolean {
if (...inspect...) { true }
}
}
def auditor2 {
to audit(ast, env) : boolean { true }
}
def guard {
to adapt(object) : any {
if (audited(auditor1, object) ||
audited(auditor2, object)) { object }
else { ...problem... }
}
}
# non-privileged declaration
def object : auditor1 {
...
}
# privileged declaration
def object : auditor2 {
...
}
def user(object : guard) {
...
}
for comparison, the last example implemented in a
hypothetical scheme with stamps separated from auditors
(this requires four objects)
# public: auditor1, guard
# private: stamp, auditor2
def stamp := Stamp()
def auditor1 {
to audit(ast, env) {
if (...inspect...) { stamp stamp(ast, env) }
}
}
def auditor2 {
to audit(ast, env) { stamp stamp(ast, env) }
}
def guard {
to adapt(object) : any {
if (stamp stamped(object)) { object }
else { ...problem... }
}
}
# non-privileged declaration
def object : auditor1 {
...
}
# privileged declaration
def object : auditor2 {
...
}
def user(object : guard) {
...
}
I am now going to use the word "stamp" as an abbreviation for "non-discriminating (i.e. rubber-stamping) auditor".
auditor is non-discriminating
maker for rubber-stamping guard/auditor pairs: Stamp()
maker name: Point, Tree, etc.
guard name: point, tree, gpl, etc.
auditor name: pointStamp, treeStamp, gplStamp, etc.
example:
def [point, pointStamp] := Stamp()
def Point(x : int, y : int) : point {
def self : pointStamp {
to getX : int { x }
to getY : int { y }
}
}
auditor is discriminating
auditor name: frozen, confined, etc.
def Brand() : any {
def key { }
def envtype := GuardStamp()
def Envelope(contents) : any {
def self : envtype {
to open(k) : any {
if (k == key) {
contents
}
}
}
}
def sealer {
to seal(contents) : any {
Envelope(contents)
}
}
def unsealer {
to unseal(envelope : envtype) : any {
envelope open(key)
}
}
[sealer, unsealer]
}
I have used "GuardStamp" above to signify a maker that produces a single object willing to serve as both a guard and a rubber stamp.
Passing the key to the envelope's "open" method was the fatal leak; the addition of the "envtype" guard, i believe, closes this leak.