Skip to content
Kavachकवच

Audit Log

Also known as: audit trail compliance log record history who did what accountability soc2 disk full no space left write failure

● Intermediate3 minBefore this: Agent ModesSpec 8.6

The runtime writes down every capability call an agent makes, with its arguments, its verdict and its result. No line of your code has to remember to.

It is off until the agent's settings turn it on with an Audit. The file is audit.log in the agent's folder, agents/<Name>/, unless the Audit names another:

agent Refunder {
    settings AgentSettings(audit: Audit())
    …
}

The program ran and printed:

refused: Denied(reason: "limit broken: amount <= 500")Refunder request#1 Payments.refund(order: 1, amount: 400)  verdict: allowed  result: okRefunder request#2 Payments.refund(order: 2, amount: 900)  verdict: refused (limit broken: amount <= 500)  result: denied: limit broken: amount <= 500
the agent: Kavach code, then what running it prints
  • Each entry starts with the time (left out here, so the output is the same on every run), the agent and the event.
  • verdict says what the checks decided, and why; result says what happened.
  • Refused calls are written too. The log only grows: nothing is ever taken out.

Secrets stay out

An argument of a secret parameter is written as <secret>:

use std.fs

type PayError = Declined | Denied(reason: String)

capability Payments {
    fn charge(secret card: String, amount: Int) ! PayError
}

type Bank: Payments {
    fn charge(self, secret card: String, amount: Int) ! PayError {
    }
}

agent Checkout {
    settings AgentSettings(audit: Audit())
    can Payments.charge

    on pay(secret card: String, amount: Int) ! PayError {
        try Payments.charge(card, amount)
    }
}

fn main() ! Error {
    const shop = Checkout.start(Payments: Bank())
    try shop.pay("4242 4242 4242 4242", 499)
    for line in try fs.lines("agents/Checkout/audit.log") {
        print(line.split("Z ").last() ?? line)
    }
}

The program ran and printed:

kavach run

Checkout request#1 Payments.charge(card: <secret>, amount: 499)  verdict: allowed  result: ok
src/main.kav: Kavach code, then what running it prints

A log that can't be written

An agent never goes on unrecorded. Here the log is a folder (file: "."), so no entry can be written to it, as on a full disk:

type PayError = Declined | Denied(reason: String)

capability Payments {
    fn pay(n: Int) -> Int ! PayError
}

type Till: Payments {
    fn pay(self, n: Int) -> Int ! PayError {
        n
    }
}

agent Bot {
    settings AgentSettings(audit: Audit(file: "."))
    can Payments.pay

    on charge(n: Int) -> Int ! Error {
        try Payments.pay(n)
    }
}

fn main() ! Error {
    const bot = Bot.start(Payments: Till())
    print("paid {try bot.charge(1)}")
    const second = bot.charge(2) catch e => {
        print(e)
        return
    }
    print("paid {second}")
}

The program ran and printed:

kavach run audit-unwritable.kav

paid 1Denied(reason: "the audit log can't be written: Access is denied. (os error 5)")
audit-unwritable.kav: Kavach code, then what running it prints
  • An entry is written after its call, so the first call has run. Its entry is printed to standard error instead, with a line saying the audit log can't be written.
  • From then on, each call first tries to write to the log. Until that works, the call is refused with Denied, without running, and the rest of the program goes on.
  • The reason ends with the system's own words: here Windows's, and No space left on device (os error 28) for a full disk on Linux.
  • When the log can be written again, its first line says so: audit log writable again (1 entry printed to standard error).

A handler that panics

A handler that panics is stopped there, and the log gets a line naming the agent and the handler: Picker pick panicked: <message> at <file>:<line>. The send fails with the same panicked: …: see Running an Agent.

In a durable agent the event runs again at the next start, and each run that panics writes the line again. After it comes the outcome line below, ending error panicked: …. The sender is gone after a restart, so nothing else is told, unless a program sent the same event again: that send fails with the same panicked: ….

An event that finishes after a restart

An event that runs again after a restart has no sender: the program that sent it is gone. So however it ends, its outcome goes in the log with the event's number. Here the program stops in the middle of an event, twice. The first event then succeeds, and the second is declined:

use std.cli
use std.fs

type PayError = Declined | Denied(reason: String)

capability Payments {
    fn pay(order_id: Int) -> Int ! PayError
}

type Till: Payments {
    crash: Bool
    decline: Bool

    fn pay(self, order_id: Int) -> Int ! PayError uses Console, Env {
        if self.crash {
            cli.exit(3)
        }
        if self.decline {
            fail PayError.Declined
        }
        order_id
    }
}

agent Bot {
    settings AgentSettings(audit: Audit())
    can Payments.pay

    on request(order_id: Int) -> Int ! Error {
        try Payments.pay(order_id)
    }
}

fn main() ! Error {
    const args = cli.args()
    const crash = args.contains("crash")
    const bot = Bot.start(Payments: Till(crash: crash, decline: args.contains("decline")))
    if crash {
        const _paid = try bot.request(7)
    }
    print("started")
    for line in try fs.lines("agents/Bot/audit.log") {
        if line.contains("finished") {
            print(line.split("Z ").last() ?? line)
        }
    }
}
src/main.kav: Kavach code
terminal
kavach run src/main.kav crash
kavach run src/main.kav
started
Bot finished after a restart: event #1 of request: ok 7
kavach run src/main.kav crash
kavach run src/main.kav decline
started
Bot finished after a restart: event #1 of request: ok 7
Bot finished after a restart: event #2 of request: error Declined
error in an event run again after a restart: event #2 of request: Declined
  • The line is written for every such event, whether start waited for it, as here, or it went on after asking a person.
  • An error nothing read also goes to standard error, so it is seen with the audit log off too. That is the last line above. The program goes on, and its exit code doesn't change.
  • A program that sent the same event again gets the outcome too, and reads the error, so then nothing is printed.

What Kavach does for you

  • Every agent call is audited

    Each capability call an agent makes is written down with its verdict and result, refused ones too, without a line of logging code.

    spec 8.6