Responsibility Tracking in Ocaps

I just released version 0.2.0 of ocaps. There's lots more documentation. There are even some diagrams in the introduction now.

The source code are mostly to the way dynamic sealing is implemented:

  • Brand.create[String] is now just Brand.create
  • sealer.apply() is now sealer.apply[String]("hello"), so the String type is added by the sealer.
  • Brand.Box[String] and the unsealer are the same.
  • Brand hints are applied to everything, so that you can tell whether a sealer and an unsealer belong to the same brand. Sealer and Unsealer are full on traits, instead of type aliases.

The impetus for changing dynamic sealing around was implementing responsibility tracking aka HORTON in Scala.

Responsibility tracking addresses an argument made against capabilities – because capabilities don't have an implicit identity associated with them, you don't know how they are being used and who is using them. Horton addresses this by using dynamic sealing to encode objects as "gifts", which are sealed boxes that can only be unsealed by the recipient, and return proxies which indicate their provenance.

So, assuming some traits B, C and some proxy boilerplate code:

object Main {

  class A(b: B, c: C) {
    def start(): Unit = {
      b.foo(c)
    }
  }

  def main(args: Array[String]): Unit = {
    val b = new B {
      def foo(c: C): Unit = {
        c.hi()
      }

      override def toString: String = "B"
    }
    val c = new C {
      def hi(): Unit = println("hi")

      override def toString: String = "C"
    }

    val alice = new Principal("Alice", println)
    val bob = new Principal("Bob", println)
    val carol = new Principal("Carol", println)

    // Gifts are sealed and can only be unsealed by the recipient.
    val toAliceFromBob: Gift[B] = bob.encodeFor(b, alice.who)
    val toAliceFromCarol: Gift[C] = carol.encodeFor(c, alice.who)

    // When gift is unsealed, a proxy is returned that establishes provenance.
    val p1: B = alice.decodeFrom(toAliceFromBob, bob.who)
    val p2: C = alice.decodeFrom(toAliceFromCarol, carol.who)
    val a = new A(p1, p2)

    a.start()
  }
}

We'll get the following output:

Alice said:
> I ask Bob to:
> > foo/1
Carol said:
> Alice asks me to:
> > meet Bob
Bob said:
> Alice asks me to:
> > foo/1
Bob said:
> I ask Carol to:
> > hi/1
Carol said:
> Bob asks me to:
> > meet Carol
Carol said:
> Bob asks me to:
> > hi/1
hi

The full example is here, and you can checkout and run the source code yourself.

Because Horton uses stubs and proxies internally, the process of establishing a relationship and communicating messages involves several steps. These steps are internal to the operation, but are vital to ensure that the relationship between principals cannot be faked. Horton does this all transparently, so most of the logic is in the Principal itself.

Horton looks pretty incredible when you first see it. The slides for Horton are a useful guide for going through the steps. I showed it to a friend, and his first reaction was to say his head hurt, he was tasting metal and seeing spots. Horton is only one of several capability patterns that use dynamic sealing as a primitive, but it's not well-known outside the ocap community.

I've also written a demo application of expiring capabilities using Play and Akka Actors to show ocaps in use:

https://github.com/wsargent/play-ocaps-demo

This works with a finder from a GreetingRepository:

object GreetingRepository {
  trait Finder[F[_]] {
    def find(locale: Locale, zonedDateTime: ZonedDateTime): F[Option[Greeting]]
  }
}

The GreetingService contains a Gatekeeper which messes with the finder so it's only good for 5 seconds:

object GreetingService {
  class Gatekeeper(greetingRepository: GreetingRepository) extends Actor with Timers {
    override def receive: Receive = {
        case GreetingActor.RequestFinder(locale, sealer) =>
        val finder = createFinder(locale)

        // Mess with the capability so it's only good for a few seconds...
        revokeAfter(locale, 5.seconds)
        sender() ! sealer(locale, finder)

        case Revoke(locale) =>
        revokerMap.get(locale).foreach { revoker =>
            logger.info(s"Revoking finder for locale $locale")
            revoker.revoke()
        }
        revokerMap -= locale
    }

    private def revokeAfter(locale: Locale, duration: FiniteDuration): Unit = {
        timers.startSingleTimer(TickKey, Revoke(locale), duration)
    }
  }
}

And then the hapless GreetingActor has to figure out what to do with a capability once it's been revoked.

Most of what I'm doing is translating and collating information and seeing opportunities. The demo and horton are showing use cases, but I know I also have to make the case for why you would want to use object capabilities in the first place, and why you would find them useful for a business case, as opposed to "just plain cool."

One unexpected benefit of programming with capabilities is that it forces you to think about failure. Whether it's a database failure, a timeout or a revocation, you're still crossing your fingers that the data you asked for will come back to you. There's never any guarantee. But a revoked capability makes that obvious, and you can control failure to a very fine degree. You can even program failure in at precise times, under precise circumstances, or fail entire groups at once.

This isn't even getting into the cool programming language things, like stoic functions or the research being done into the correspondence between effects and capabilities, but I'm trying to keep everything as practical as I can here, so I'll leave that to others.

Comments